[PHP]hash_update_file関数の使い方と実例 – ファイルハッシュ化の最も効率的な方法

PHP

ファイルの整合性チェックやセキュリティ検証において、ファイルのハッシュ値計算は欠かせない処理です。PHPのhash_update_file関数を使用することで、大きなファイルでもメモリ効率的にハッシュ値を計算できます。この記事では、hash_update_file関数の基本的な使い方から実践的な応用例まで、詳しく解説していきます。

hash_update_file関数とは?

hash_update_file関数は、ファイルの内容を直接ハッシュコンテキストに追加する関数です。ファイル全体をメモリに読み込むことなく、内部的に分割して処理するため、大容量ファイルでも効率的にハッシュ値を計算できます。

他の方法との比較

方法1: hash_file()関数

php// シンプルだが、内部的には全体を処理
$hash = hash_file('sha256', 'large_file.txt');

方法2: 手動でのファイル読み込み + hash_update()

php// 手動でファイルを分割処理
$context = hash_init('sha256');
$handle = fopen('large_file.txt', 'r');
while (($chunk = fread($handle, 8192)) !== false) {
    hash_update($context, $chunk);
}
$hash = hash_final($context);
fclose($handle);

方法3: hash_update_file()(推奨)

php// 最も効率的でシンプル
$context = hash_init('sha256');
hash_update_file($context, 'large_file.txt');
$hash = hash_final($context);

hash_update_file関数の基本構文

phphash_update_file(
    HashContext $context,
    string $filename,
    ?resource $stream_context = null
): bool

パラメータの詳細

$context(ハッシュコンテキスト) hash_init()関数で初期化されたハッシュコンテキストオブジェクトです。

$filename(ファイル名) ハッシュ計算を行うファイルのパスを指定します。相対パスまたは絶対パスが使用可能です。

$stream_context(ストリームコンテキスト) オプションのストリームコンテキスト。HTTPファイルやFTPファイルにアクセスする際に使用します。

戻り値 成功時はtrue、失敗時はfalseを返します。

基本的な使用例

例1: 単一ファイルのハッシュ計算

php<?php
/**
 * 基本的なファイルハッシュ計算
 */
function calculateFileHash($filename, $algorithm = 'sha256') {
    // ファイルの存在確認
    if (!file_exists($filename)) {
        throw new InvalidArgumentException("ファイルが存在しません: {$filename}");
    }
    
    if (!is_readable($filename)) {
        throw new InvalidArgumentException("ファイルが読み取れません: {$filename}");
    }
    
    // ハッシュコンテキストを初期化
    $context = hash_init($algorithm);
    
    // ファイルをハッシュコンテキストに追加
    if (!hash_update_file($context, $filename)) {
        throw new RuntimeException("ファイルのハッシュ計算に失敗しました: {$filename}");
    }
    
    // 最終ハッシュ値を取得
    return hash_final($context);
}

// 使用例
try {
    $filename = 'document.pdf';
    $hash = calculateFileHash($filename);
    
    echo "ファイル: {$filename}\n";
    echo "SHA256ハッシュ: {$hash}\n";
    echo "ファイルサイズ: " . number_format(filesize($filename)) . " バイト\n";
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

例2: 複数ファイルの結合ハッシュ計算

php<?php
/**
 * 複数ファイルを結合したハッシュ値を計算
 */
class MultiFileHasher {
    private $context;
    private $algorithm;
    private $fileCount = 0;
    private $totalSize = 0;
    
    public function __construct($algorithm = 'sha256') {
        $this->algorithm = $algorithm;
        $this->context = hash_init($algorithm);
    }
    
    /**
     * ファイルをハッシュ計算に追加
     */
    public function addFile($filename) {
        if (!file_exists($filename)) {
            throw new InvalidArgumentException("ファイルが存在しません: {$filename}");
        }
        
        // ファイル名も含めてハッシュ化(順序の重要性を保つため)
        hash_update($this->context, basename($filename) . "\0");
        
        // ファイル内容をハッシュに追加
        if (!hash_update_file($this->context, $filename)) {
            throw new RuntimeException("ファイルのハッシュ計算に失敗: {$filename}");
        }
        
        $this->fileCount++;
        $this->totalSize += filesize($filename);
        
        return $this;
    }
    
    /**
     * ディレクトリ内の全ファイルを追加
     */
    public function addDirectory($directory, $recursive = false) {
        if (!is_dir($directory)) {
            throw new InvalidArgumentException("ディレクトリが存在しません: {$directory}");
        }
        
        $iterator = $recursive 
            ? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory))
            : new DirectoryIterator($directory);
        
        $files = [];
        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $files[] = $file->getPathname();
            }
        }
        
        // ソートして結果を一意にする
        sort($files);
        
        foreach ($files as $filename) {
            $this->addFile($filename);
        }
        
        return $this;
    }
    
    /**
     * 最終ハッシュ値を取得
     */
    public function getHash() {
        return hash_final($this->context);
    }
    
    /**
     * 統計情報を取得
     */
    public function getStats() {
        return [
            'file_count' => $this->fileCount,
            'total_size' => $this->totalSize,
            'algorithm' => $this->algorithm
        ];
    }
}

// 使用例
try {
    $hasher = new MultiFileHasher('sha256');
    
    // 複数ファイルを追加
    $hasher->addFile('config.php')
           ->addFile('index.php')
           ->addFile('style.css');
    
    // ディレクトリ全体を追加
    $hasher->addDirectory('./images', false);
    
    $hash = $hasher->getHash();
    $stats = $hasher->getStats();
    
    echo "結合ハッシュ値: {$hash}\n";
    echo "処理ファイル数: {$stats['file_count']}\n";
    echo "総ファイルサイズ: " . number_format($stats['total_size']) . " バイト\n";
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

実践的な応用例

例3: ファイル整合性チェッカー

php<?php
/**
 * ファイル整合性チェック機能
 */
class FileIntegrityChecker {
    private $checksumFile;
    private $algorithm;
    
    public function __construct($checksumFile = 'checksums.txt', $algorithm = 'sha256') {
        $this->checksumFile = $checksumFile;
        $this->algorithm = $algorithm;
    }
    
    /**
     * チェックサムファイルを生成
     */
    public function generateChecksums($files) {
        $checksums = [];
        
        foreach ($files as $filename) {
            if (!file_exists($filename)) {
                echo "警告: ファイルが見つかりません - {$filename}\n";
                continue;
            }
            
            try {
                $context = hash_init($this->algorithm);
                hash_update_file($context, $filename);
                $hash = hash_final($context);
                
                $checksums[$filename] = $hash;
                echo "処理完了: {$filename}\n";
                
            } catch (Exception $e) {
                echo "エラー: {$filename} - " . $e->getMessage() . "\n";
            }
        }
        
        // チェックサムファイルに保存
        $this->saveChecksums($checksums);
        return $checksums;
    }
    
    /**
     * ファイル整合性を検証
     */
    public function verifyIntegrity() {
        $storedChecksums = $this->loadChecksums();
        $results = [];
        
        foreach ($storedChecksums as $filename => $expectedHash) {
            if (!file_exists($filename)) {
                $results[$filename] = [
                    'status' => 'missing',
                    'message' => 'ファイルが見つかりません'
                ];
                continue;
            }
            
            try {
                $context = hash_init($this->algorithm);
                hash_update_file($context, $filename);
                $currentHash = hash_final($context);
                
                if (hash_equals($expectedHash, $currentHash)) {
                    $results[$filename] = [
                        'status' => 'ok',
                        'message' => '整合性OK'
                    ];
                } else {
                    $results[$filename] = [
                        'status' => 'modified',
                        'message' => 'ファイルが変更されています',
                        'expected' => $expectedHash,
                        'current' => $currentHash
                    ];
                }
                
            } catch (Exception $e) {
                $results[$filename] = [
                    'status' => 'error',
                    'message' => $e->getMessage()
                ];
            }
        }
        
        return $results;
    }
    
    /**
     * チェックサムをファイルに保存
     */
    private function saveChecksums($checksums) {
        $content = "# ファイル整合性チェックサム\n";
        $content .= "# 生成日時: " . date('Y-m-d H:i:s') . "\n";
        $content .= "# アルゴリズム: {$this->algorithm}\n\n";
        
        foreach ($checksums as $filename => $hash) {
            $content .= "{$hash}  {$filename}\n";
        }
        
        file_put_contents($this->checksumFile, $content);
    }
    
    /**
     * チェックサムをファイルから読み込み
     */
    private function loadChecksums() {
        if (!file_exists($this->checksumFile)) {
            throw new RuntimeException("チェックサムファイルが見つかりません: {$this->checksumFile}");
        }
        
        $lines = file($this->checksumFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        $checksums = [];
        
        foreach ($lines as $line) {
            if (strpos($line, '#') === 0) continue; // コメント行をスキップ
            
            if (preg_match('/^([a-f0-9]+)\s+(.+)$/', $line, $matches)) {
                $checksums[$matches[2]] = $matches[1];
            }
        }
        
        return $checksums;
    }
    
    /**
     * 検証結果を表示
     */
    public function displayResults($results) {
        $okCount = 0;
        $errorCount = 0;
        
        foreach ($results as $filename => $result) {
            $status = $result['status'];
            $message = $result['message'];
            
            switch ($status) {
                case 'ok':
                    echo "✓ {$filename}: {$message}\n";
                    $okCount++;
                    break;
                case 'modified':
                    echo "✗ {$filename}: {$message}\n";
                    $errorCount++;
                    break;
                case 'missing':
                    echo "? {$filename}: {$message}\n";
                    $errorCount++;
                    break;
                case 'error':
                    echo "! {$filename}: {$message}\n";
                    $errorCount++;
                    break;
            }
        }
        
        echo "\n検証結果: " . ($okCount + $errorCount) . "ファイル中 ";
        echo "{$okCount}個正常, {$errorCount}個に問題あり\n";
    }
}

// 使用例
try {
    $checker = new FileIntegrityChecker('project_checksums.txt');
    
    // 重要ファイルのリスト
    $importantFiles = [
        'config.php',
        'database.php',
        'admin/login.php',
        'includes/functions.php'
    ];
    
    echo "=== チェックサム生成 ===\n";
    $checker->generateChecksums($importantFiles);
    
    echo "\n=== 整合性検証 ===\n";
    $results = $checker->verifyIntegrity();
    $checker->displayResults($results);
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

例4: リモートファイルとローカルファイルの比較

php<?php
/**
 * リモートファイルとローカルファイルのハッシュ比較
 */
function compareRemoteAndLocalFile($remoteUrl, $localFile) {
    // ローカルファイルのハッシュを計算
    if (!file_exists($localFile)) {
        throw new InvalidArgumentException("ローカルファイルが存在しません: {$localFile}");
    }
    
    $localContext = hash_init('sha256');
    hash_update_file($localContext, $localFile);
    $localHash = hash_final($localContext);
    
    // リモートファイルのハッシュを計算
    $remoteContext = hash_init('sha256');
    
    // ストリームコンテキストを作成(タイムアウト設定)
    $streamContext = stream_context_create([
        'http' => [
            'timeout' => 30,
            'user_agent' => 'PHP File Checker/1.0'
        ]
    ]);
    
    // リモートファイルをハッシュコンテキストに追加
    if (!hash_update_file($remoteContext, $remoteUrl, $streamContext)) {
        throw new RuntimeException("リモートファイルの取得に失敗しました: {$remoteUrl}");
    }
    
    $remoteHash = hash_final($remoteContext);
    
    return [
        'local_file' => $localFile,
        'remote_url' => $remoteUrl,
        'local_hash' => $localHash,
        'remote_hash' => $remoteHash,
        'identical' => hash_equals($localHash, $remoteHash),
        'local_size' => filesize($localFile)
    ];
}

// 使用例
try {
    $remoteUrl = 'https://example.com/latest/update.zip';
    $localFile = './downloads/update.zip';
    
    echo "ファイル比較を実行中...\n";
    $comparison = compareRemoteAndLocalFile($remoteUrl, $localFile);
    
    echo "ローカルファイル: {$comparison['local_file']}\n";
    echo "リモートURL: {$comparison['remote_url']}\n";
    echo "ローカルハッシュ: {$comparison['local_hash']}\n";
    echo "リモートハッシュ: {$comparison['remote_hash']}\n";
    echo "ファイルサイズ: " . number_format($comparison['local_size']) . " バイト\n";
    echo "一致状況: " . ($comparison['identical'] ? '一致' : '不一致') . "\n";
    
    if (!$comparison['identical']) {
        echo "\n警告: ファイルが一致しません。更新が必要かもしれません。\n";
    }
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

パフォーマンス最適化のコツ

1. 大容量ファイル処理のベンチマーク

php<?php
/**
 * 異なる方法でのファイルハッシュ計算パフォーマンス比較
 */
function benchmarkFileHashing($filename) {
    if (!file_exists($filename)) {
        throw new InvalidArgumentException("テストファイルが存在しません: {$filename}");
    }
    
    $fileSize = filesize($filename);
    echo "テストファイル: {$filename}\n";
    echo "ファイルサイズ: " . number_format($fileSize / 1024 / 1024, 2) . "MB\n\n";
    
    // 方法1: hash_file()
    $start = microtime(true);
    $hash1 = hash_file('sha256', $filename);
    $time1 = microtime(true) - $start;
    
    // 方法2: hash_update_file()
    $start = microtime(true);
    $context = hash_init('sha256');
    hash_update_file($context, $filename);
    $hash2 = hash_final($context);
    $time2 = microtime(true) - $start;
    
    // 方法3: 手動分割処理
    $start = microtime(true);
    $context = hash_init('sha256');
    $handle = fopen($filename, 'rb');
    while (($chunk = fread($handle, 8192)) !== false && $chunk !== '') {
        hash_update($context, $chunk);
    }
    $hash3 = hash_final($context);
    fclose($handle);
    $time3 = microtime(true) - $start;
    
    echo "結果:\n";
    echo "hash_file(): " . number_format($time1 * 1000, 2) . "ms\n";
    echo "hash_update_file(): " . number_format($time2 * 1000, 2) . "ms\n";
    echo "手動分割処理: " . number_format($time3 * 1000, 2) . "ms\n\n";
    
    echo "ハッシュ値の一致確認:\n";
    echo "hash_file vs hash_update_file: " . ($hash1 === $hash2 ? '一致' : '不一致') . "\n";
    echo "hash_update_file vs 手動処理: " . ($hash2 === $hash3 ? '一致' : '不一致') . "\n";
    
    return [
        'hash_file_time' => $time1,
        'hash_update_file_time' => $time2,
        'manual_time' => $time3,
        'file_size' => $fileSize
    ];
}

// 使用例
try {
    $testFile = 'large_test_file.dat';
    
    // テストファイルが存在しない場合は作成
    if (!file_exists($testFile)) {
        echo "テストファイルを作成中...\n";
        $handle = fopen($testFile, 'wb');
        for ($i = 0; $i < 1024; $i++) {
            fwrite($handle, str_repeat('A', 1024)); // 1MB作成
        }
        fclose($handle);
    }
    
    benchmarkFileHashing($testFile);
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

よくある質問と回答

Q: hash_file()とhash_update_file()の違いは何ですか? A: hash_file()は単体でハッシュ値を計算する関数ですが、hash_update_file()は既存のハッシュコンテキストにファイル内容を追加する関数です。複数ファイルの結合ハッシュや、ファイルとテキストデータの組み合わせハッシュを計算する際にhash_update_file()が有用です。

Q: 大きなファイルでもメモリ不足にならないのはなぜですか? A: hash_update_file()は内部的にファイルを小さなチャンクに分割して処理するため、ファイル全体をメモリに読み込むことがありません。そのため、数GBの大きなファイルでも安全に処理できます。

Q: ネットワーク越しのファイルも処理できますか? A: はい、HTTPやFTPなどのストリームラッパーに対応したURLを指定できます。ただし、ネットワークの状況によってはタイムアウトエラーが発生する可能性があるため、適切なストリームコンテキストの設定が推奨されます。

Q: エラーハンドリングで注意すべき点は? A: ファイルの存在確認、読み取り権限の確認、ネットワークエラーの処理が重要です。また、hash_update_file()の戻り値をチェックして処理の成功・失敗を確認することも大切です。

まとめ

hash_update_file関数は、PHPでファイルのハッシュ値を効率的に計算するための強力なツールです。特に大容量ファイルの処理や、複数ファイルの結合ハッシュ計算において、メモリ効率性とパフォーマンスの両方を実現できます。

ファイル整合性チェック、セキュリティ検証、バックアップの検証など、様々な場面で活用できる実用的な関数です。今回紹介した実例を参考に、あなたのプロジェクトでもhash_update_file関数を活用してみてください。

適切なエラーハンドリングと組み合わせることで、信頼性の高いファイル処理システムを構築できるでしょう。

タイトルとURLをコピーしました