PluginProbe ʕ •ᴥ•ʔ
Backup Migration / 2.1.7
Backup Migration v2.1.7
2.1.7 2.1.6 2.1.5.2 trunk 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.6.1 1.4.7 1.4.8 1.4.9 1.4.9.1 2.0.0 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.5.1
backup-backup / includes / services / class-file-hasher.php
backup-backup / includes / services Last commit date
class-file-hasher.php 3 weeks ago class-incremental-chained-hasher.php 3 weeks ago
class-file-hasher.php
125 lines
1 <?php
2
3 declare(strict_types=1);
4
5 namespace BMI\Plugin\Services;
6
7 use RuntimeException;
8
9 require_once __DIR__ . '/class-incremental-chained-hasher.php';
10
11 /**
12 * One-shot file-hashing utility.
13 *
14 * Computes either a standard (v1) or chained (v2) hash of an entire file.
15 *
16 * ── Modes ─────────────────────────────────────────────────────────────────
17 * STANDARD (v1) — equivalent to PHP's hash_file(); one digest over the
18 * whole file.
19 *
20 * CHAINED (v2) — splits the file into $chunkSize-byte links and builds a
21 * rolling chained digest via IncrementalChainedHasher.
22 * The chain formula is:
23 * Hₙ = hash(algo, Hₙ₋₁ ‖ hash(algo, piece))
24 *
25 * ── Usage ─────────────────────────────────────────────────────────────────
26 * // Standard hash (v1)
27 * $hash = FileHasher::compute('/path/to/file.zip');
28 *
29 * // Chained hash (v2) with CHAINED_HASH_CHUNK_SIZE MiB links and md5 algorithm
30 * $hash = FileHasher::compute('/path/to/file.zip', FileHasher::CHAINED, CHAINED_HASH_CHUNK_SIZE, 'md5');
31 */
32 class FileHasher
33 {
34 // ── Mode constants ─────────────────────────────────────────────────────
35
36 /** Standard hash: hash_file() equivalent (v1). */
37 public const STANDARD = 'standard';
38
39 /** Chained hash: linked rolling digest over per-chunk hashes (v2). */
40 public const CHAINED = 'chained';
41
42 /** Internal read-buffer size when streaming the file (64 KiB). */
43 private const IO_BUFFER = 65536;
44
45 // ── Public API ─────────────────────────────────────────────────────────
46
47 /**
48 * Compute the hash of a file.
49 *
50 * @param string $filePath Absolute path to the file.
51 * @param string $mode FileHasher::STANDARD or FileHasher::CHAINED.
52 * @param string $algorithm PHP hash() algorithm (default 'md5').
53 * @param int|null $chunkSize Chain-link size in bytes for CHAINED mode.
54 * Ignored in STANDARD mode.
55 * Defaults to 1 MiB when null.
56 * @return string Hex-encoded hash.
57 * @throws RuntimeException If the file is not readable or the mode is unrecognised.
58 */
59 public static function compute(
60 string $filePath,
61 string $mode = self::STANDARD,
62 int $chunkSize = CHAINED_HASH_CHUNK_SIZE,
63 string $algorithm = 'md5'
64 ): string {
65 if (!is_readable($filePath)) {
66 throw new RuntimeException('File is not readable: ' . $filePath);
67 }
68
69 if ($mode === self::CHAINED) {
70 return self::computeChained($filePath, $algorithm, $chunkSize);
71 }
72
73 if ($mode === self::STANDARD) {
74 return self::computeStandard($filePath, $algorithm);
75 }
76
77 throw new RuntimeException('Unknown hash mode: ' . $mode);
78 }
79
80 // ── Private ────────────────────────────────────────────────────────────
81
82 /**
83 * Standard hash: stream the file and produce a single digest.
84 */
85 private static function computeStandard(string $filePath, string $algorithm): string
86 {
87 $hash = hash_file($algorithm, $filePath);
88 if ($hash === false) {
89 throw new RuntimeException('hash_file() failed for: ' . $filePath);
90 }
91
92 return $hash;
93 }
94
95 /**
96 * Chained hash: stream the file through IncrementalChainedHasher in
97 * IO_BUFFER-sized reads; chain links are $chunkSize bytes.
98 */
99 private static function computeChained(string $filePath, string $algorithm, int $chunkSize): string
100 {
101 $handle = fopen($filePath, 'rb');
102 if ($handle === false) {
103 throw new RuntimeException('Cannot open file for hashing: ' . $filePath);
104 }
105
106 $hasher = new IncrementalChainedHasher($algorithm, $chunkSize);
107
108 while (!feof($handle)) {
109 $piece = fread($handle, self::IO_BUFFER);
110 if ($piece === false || $piece === '') {
111 break;
112 }
113 $hasher->update($piece);
114 }
115
116 fclose($handle);
117
118 return $hasher->finalize();
119 }
120
121 // ── Prevent instantiation ──────────────────────────────────────────────
122
123 private function __construct() {}
124 }
125