Skip to main content

ipfrs_storage/
checksum_engine.rs

1//! Multi-algorithm checksum computation and verification for storage integrity.
2//!
3//! Provides `StorageChecksumEngine` with pure-Rust implementations of:
4//! FNV-1a-64, DJB2, Murmur3-32, Adler-32, CRC-32/ISO-HDLC, xxHash-64 (simplified),
5//! and a Blake3-inspired 256-bit hash (4 independent FNV-1a rounds).
6
7use std::collections::HashMap;
8
9// ─────────────────────────────────────────────────────────────────────────────
10// Pure-Rust algorithm implementations (public free functions)
11// ─────────────────────────────────────────────────────────────────────────────
12
13/// FNV-1a 64-bit hash.
14pub fn fnv1a_64(data: &[u8]) -> u64 {
15    const OFFSET: u64 = 14_695_981_039_346_656_037;
16    const PRIME: u64 = 1_099_511_628_211;
17    let mut hash = OFFSET;
18    for &b in data {
19        hash ^= b as u64;
20        hash = hash.wrapping_mul(PRIME);
21    }
22    hash
23}
24
25/// DJB2 hash (32-bit result stored in u32).
26pub fn djb2(data: &[u8]) -> u32 {
27    let mut hash: u64 = 5381;
28    for &b in data {
29        hash = hash.wrapping_mul(33).wrapping_add(b as u64);
30    }
31    (hash & 0xFFFF_FFFF) as u32
32}
33
34/// Simplified Murmur3-32 hash with seed=0.
35pub fn murmur3_32(data: &[u8]) -> u32 {
36    const C1: u32 = 0xcc9e_2d51;
37    const C2: u32 = 0x1b87_3593;
38    let len = data.len();
39    let mut h: u32 = 0;
40
41    let nblocks = len / 4;
42    for i in 0..nblocks {
43        let idx = i * 4;
44        let k = u32::from_le_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]);
45        let k = k.wrapping_mul(C1).rotate_left(15).wrapping_mul(C2);
46        h ^= k;
47        h = h.rotate_left(13);
48        h = h.wrapping_mul(5).wrapping_add(0xe654_6b64);
49    }
50
51    // Tail bytes
52    let tail_start = nblocks * 4;
53    let tail = &data[tail_start..];
54    let mut k: u32 = 0;
55    match tail.len() {
56        3 => {
57            k ^= (tail[2] as u32) << 16;
58            k ^= (tail[1] as u32) << 8;
59            k ^= tail[0] as u32;
60            k = k.wrapping_mul(C1).rotate_left(15).wrapping_mul(C2);
61            h ^= k;
62        }
63        2 => {
64            k ^= (tail[1] as u32) << 8;
65            k ^= tail[0] as u32;
66            k = k.wrapping_mul(C1).rotate_left(15).wrapping_mul(C2);
67            h ^= k;
68        }
69        1 => {
70            k ^= tail[0] as u32;
71            k = k.wrapping_mul(C1).rotate_left(15).wrapping_mul(C2);
72            h ^= k;
73        }
74        _ => {}
75    }
76
77    h ^= len as u32;
78    // Finalize (fmix32)
79    h ^= h >> 16;
80    h = h.wrapping_mul(0x85eb_ca6b);
81    h ^= h >> 13;
82    h = h.wrapping_mul(0xc2b2_ae35);
83    h ^= h >> 16;
84    h
85}
86
87/// Adler-32 checksum.
88pub fn adler32(data: &[u8]) -> u32 {
89    const MOD: u32 = 65521;
90    let mut s1: u32 = 1;
91    let mut s2: u32 = 0;
92    for &b in data {
93        s1 = (s1 + b as u32) % MOD;
94        s2 = (s2 + s1) % MOD;
95    }
96    (s2 << 16) | s1
97}
98
99/// Build the CRC-32/ISO-HDLC lookup table.
100fn crc32_table() -> [u32; 256] {
101    let mut table = [0u32; 256];
102    for i in 0u32..256 {
103        let mut crc = i;
104        for _ in 0..8 {
105            if crc & 1 != 0 {
106                crc = (crc >> 1) ^ 0xEDB8_8320;
107            } else {
108                crc >>= 1;
109            }
110        }
111        table[i as usize] = crc;
112    }
113    table
114}
115
116/// CRC-32/ISO-HDLC (polynomial 0xEDB88320).
117pub fn crc32_iso(data: &[u8]) -> u32 {
118    let table = crc32_table();
119    let mut crc: u32 = 0xFFFF_FFFF;
120    for &b in data {
121        let idx = ((crc ^ b as u32) & 0xFF) as usize;
122        crc = (crc >> 8) ^ table[idx];
123    }
124    crc ^ 0xFFFF_FFFF
125}
126
127/// Simplified xxHash-64 with seed=0.
128pub fn xxhash64_simple(data: &[u8]) -> u64 {
129    const PRIME1: u64 = 11_400_714_785_074_694_791;
130    const PRIME2: u64 = 14_029_467_366_897_019_727;
131    const PRIME3: u64 = 1_609_587_929_392_839_161;
132    const PRIME4: u64 = 9_650_029_242_287_828_579;
133    const PRIME5: u64 = 2_870_177_450_012_600_261;
134
135    let len = data.len();
136    let mut pos = 0usize;
137    let mut h64: u64;
138
139    if len >= 32 {
140        let mut v1 = 0u64.wrapping_add(PRIME1).wrapping_add(PRIME2);
141        let mut v2 = 0u64.wrapping_add(PRIME2);
142        let mut v3 = 0u64;
143        let mut v4 = 0u64.wrapping_sub(PRIME1);
144
145        while pos + 32 <= len {
146            let lane = |off: usize| {
147                u64::from_le_bytes([
148                    data[pos + off],
149                    data[pos + off + 1],
150                    data[pos + off + 2],
151                    data[pos + off + 3],
152                    data[pos + off + 4],
153                    data[pos + off + 5],
154                    data[pos + off + 6],
155                    data[pos + off + 7],
156                ])
157            };
158            v1 = v1
159                .wrapping_add(lane(0).wrapping_mul(PRIME2))
160                .rotate_left(31)
161                .wrapping_mul(PRIME1);
162            v2 = v2
163                .wrapping_add(lane(8).wrapping_mul(PRIME2))
164                .rotate_left(31)
165                .wrapping_mul(PRIME1);
166            v3 = v3
167                .wrapping_add(lane(16).wrapping_mul(PRIME2))
168                .rotate_left(31)
169                .wrapping_mul(PRIME1);
170            v4 = v4
171                .wrapping_add(lane(24).wrapping_mul(PRIME2))
172                .rotate_left(31)
173                .wrapping_mul(PRIME1);
174            pos += 32;
175        }
176
177        h64 = v1
178            .rotate_left(1)
179            .wrapping_add(v2.rotate_left(7))
180            .wrapping_add(v3.rotate_left(12))
181            .wrapping_add(v4.rotate_left(18));
182
183        let merge = |acc: u64, v: u64| -> u64 {
184            let v = v.wrapping_mul(PRIME2).rotate_left(31).wrapping_mul(PRIME1);
185            acc.wrapping_mul(PRIME1)
186                .wrapping_add(v)
187                .wrapping_mul(PRIME4)
188                .wrapping_add(PRIME3)
189        };
190        h64 = merge(h64, v1);
191        h64 = merge(h64, v2);
192        h64 = merge(h64, v3);
193        h64 = merge(h64, v4);
194    } else {
195        h64 = 0u64.wrapping_add(PRIME5);
196    }
197
198    h64 = h64.wrapping_add(len as u64);
199
200    // Process remaining 8-byte chunks
201    while pos + 8 <= len {
202        let lane = u64::from_le_bytes([
203            data[pos],
204            data[pos + 1],
205            data[pos + 2],
206            data[pos + 3],
207            data[pos + 4],
208            data[pos + 5],
209            data[pos + 6],
210            data[pos + 7],
211        ]);
212        let k1 = lane
213            .wrapping_mul(PRIME2)
214            .rotate_left(31)
215            .wrapping_mul(PRIME1);
216        h64 ^= k1;
217        h64 = h64
218            .rotate_left(27)
219            .wrapping_mul(PRIME1)
220            .wrapping_add(PRIME4);
221        pos += 8;
222    }
223
224    // Remaining 4-byte chunk
225    if pos + 4 <= len {
226        let lane = u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
227        h64 ^= (lane as u64).wrapping_mul(PRIME1);
228        h64 = h64
229            .rotate_left(23)
230            .wrapping_mul(PRIME2)
231            .wrapping_add(PRIME3);
232        pos += 4;
233    }
234
235    // Remaining bytes
236    while pos < len {
237        h64 ^= (data[pos] as u64).wrapping_mul(PRIME5);
238        h64 = h64.rotate_left(11).wrapping_mul(PRIME1);
239        pos += 1;
240    }
241
242    // Avalanche / finalization
243    h64 ^= h64 >> 33;
244    h64 = h64.wrapping_mul(PRIME2);
245    h64 ^= h64 >> 29;
246    h64 = h64.wrapping_mul(PRIME1);
247    h64 ^= h64 >> 32;
248    h64
249}
250
251/// Blake3-inspired 256-bit hash: 4 independent FNV-1a-64 rounds with different seeds.
252pub fn blake3_256_simple(data: &[u8]) -> [u8; 32] {
253    const SEEDS: [u64; 4] = [
254        0x0000_0000_0000_0000,
255        0x1234_5678_90AB_CDEF,
256        0xFEDC_BA98_7654_3210,
257        0xDEAD_BEEF_CAFE_BABE,
258    ];
259    const PRIME: u64 = 1_099_511_628_211;
260    const OFFSET: u64 = 14_695_981_039_346_656_037;
261
262    let mut result = [0u8; 32];
263    for (i, &seed) in SEEDS.iter().enumerate() {
264        let mut hash = OFFSET ^ seed;
265        for &b in data {
266            hash ^= b as u64;
267            hash = hash.wrapping_mul(PRIME);
268        }
269        let bytes = hash.to_le_bytes();
270        result[i * 8..(i + 1) * 8].copy_from_slice(&bytes);
271    }
272    result
273}
274
275// ─────────────────────────────────────────────────────────────────────────────
276// Core types
277// ─────────────────────────────────────────────────────────────────────────────
278
279/// Supported checksum algorithms, all implemented in pure Rust.
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
281pub enum ChecksumAlgorithm {
282    /// FNV-1a 64-bit hash.
283    Fnv1a64,
284    /// DJB2 hash (32-bit).
285    Djb2,
286    /// Simplified Murmur3-32 hash.
287    Murmur3_32,
288    /// Adler-32 checksum.
289    Adler32,
290    /// CRC-32/ISO-HDLC.
291    Crc32,
292    /// Simplified xxHash-64.
293    Xxhash64,
294    /// Blake3-inspired 256-bit hash (4 FNV-1a rounds with different seeds).
295    Blake3_256,
296}
297
298impl ChecksumAlgorithm {
299    /// Human-readable name of the algorithm.
300    pub fn name(&self) -> &'static str {
301        match self {
302            Self::Fnv1a64 => "fnv1a64",
303            Self::Djb2 => "djb2",
304            Self::Murmur3_32 => "murmur3_32",
305            Self::Adler32 => "adler32",
306            Self::Crc32 => "crc32",
307            Self::Xxhash64 => "xxhash64",
308            Self::Blake3_256 => "blake3_256",
309        }
310    }
311}
312
313/// The raw checksum value plus its hex representation.
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct Checksum {
316    /// Algorithm that produced this checksum.
317    pub algorithm: ChecksumAlgorithm,
318    /// Raw bytes of the hash output.
319    pub value: Vec<u8>,
320    /// Lowercase hexadecimal string of `value`.
321    pub hex: String,
322}
323
324impl Checksum {
325    /// Construct a new `Checksum` from raw bytes.
326    pub fn new(algorithm: ChecksumAlgorithm, value: Vec<u8>) -> Self {
327        let hex = value.iter().map(|b| format!("{b:02x}")).collect();
328        Self {
329            algorithm,
330            value,
331            hex,
332        }
333    }
334}
335
336/// A stored checksum record for a specific object.
337#[derive(Debug, Clone)]
338pub struct ChecksumRecord {
339    /// Identifier for the stored object.
340    pub object_id: String,
341    /// Computed checksum.
342    pub checksum: Checksum,
343    /// Unix timestamp (seconds) when the checksum was computed.
344    pub computed_at: u64,
345    /// Unix timestamp (seconds) of the last successful verification, if any.
346    pub verified_at: Option<u64>,
347    /// Size of the object in bytes at time of computation.
348    pub size_bytes: u64,
349}
350
351/// Result of a single verification attempt.
352#[derive(Debug, Clone)]
353pub struct CeVerificationResult {
354    /// Identifier for the checked object.
355    pub object_id: String,
356    /// The checksum stored at index time.
357    pub expected: Checksum,
358    /// The checksum freshly computed from the provided data.
359    pub actual: Checksum,
360    /// Whether `expected == actual`.
361    pub matches: bool,
362    /// Unix timestamp (seconds) when verification was performed.
363    pub verified_at: u64,
364}
365
366/// Summary statistics over a set of verification results.
367#[derive(Debug, Clone)]
368pub struct ChecksumStats {
369    /// Total number of stored records.
370    pub total_records: usize,
371    /// Number of objects that have been successfully verified at least once.
372    pub verified_count: usize,
373    /// Fraction of provided results where `!matches`.
374    pub corruption_rate: f64,
375    /// Name of the engine's current algorithm.
376    pub algorithm: String,
377}
378
379// ─────────────────────────────────────────────────────────────────────────────
380// Engine
381// ─────────────────────────────────────────────────────────────────────────────
382
383/// Multi-algorithm checksum engine for storage integrity verification.
384///
385/// Stores one `ChecksumRecord` per object and allows efficient batch
386/// computation and verification.
387#[derive(Debug, Clone)]
388pub struct StorageChecksumEngine {
389    records: HashMap<String, ChecksumRecord>,
390    /// Default algorithm used for new computations.
391    pub algorithm: ChecksumAlgorithm,
392    /// When `true`, callers are encouraged to verify on every read.
393    pub verify_on_read: bool,
394}
395
396impl StorageChecksumEngine {
397    /// Create a new engine with the given default algorithm.
398    pub fn new(algorithm: ChecksumAlgorithm) -> Self {
399        Self {
400            records: HashMap::new(),
401            algorithm,
402            verify_on_read: false,
403        }
404    }
405
406    // ── Core computation ───────────────────────────────────────────────────
407
408    /// Compute a checksum for `data` using the engine's current algorithm.
409    pub fn compute(&self, data: &[u8]) -> Checksum {
410        Self::compute_with_algorithm(self.algorithm, data)
411    }
412
413    /// Compute a checksum for `data` using the specified algorithm.
414    pub fn compute_with_algorithm(algorithm: ChecksumAlgorithm, data: &[u8]) -> Checksum {
415        let value: Vec<u8> = match algorithm {
416            ChecksumAlgorithm::Fnv1a64 => fnv1a_64(data).to_le_bytes().to_vec(),
417            ChecksumAlgorithm::Djb2 => djb2(data).to_le_bytes().to_vec(),
418            ChecksumAlgorithm::Murmur3_32 => murmur3_32(data).to_le_bytes().to_vec(),
419            ChecksumAlgorithm::Adler32 => adler32(data).to_le_bytes().to_vec(),
420            ChecksumAlgorithm::Crc32 => crc32_iso(data).to_le_bytes().to_vec(),
421            ChecksumAlgorithm::Xxhash64 => xxhash64_simple(data).to_le_bytes().to_vec(),
422            ChecksumAlgorithm::Blake3_256 => blake3_256_simple(data).to_vec(),
423        };
424        Checksum::new(algorithm, value)
425    }
426
427    /// Compute a checksum for `data`, store the resulting record, and return it.
428    pub fn compute_for(&mut self, object_id: String, data: &[u8], now: u64) -> ChecksumRecord {
429        let checksum = self.compute(data);
430        let record = ChecksumRecord {
431            object_id: object_id.clone(),
432            checksum,
433            computed_at: now,
434            verified_at: None,
435            size_bytes: data.len() as u64,
436        };
437        self.records.insert(object_id, record.clone());
438        record
439    }
440
441    // ── Verification ───────────────────────────────────────────────────────
442
443    /// Compare the stored checksum for `object_id` with a freshly computed one.
444    ///
445    /// Returns `None` when no record exists for `object_id`.
446    /// On a successful match the record's `verified_at` is updated to `now`.
447    pub fn verify(
448        &mut self,
449        object_id: &str,
450        data: &[u8],
451        now: u64,
452    ) -> Option<CeVerificationResult> {
453        let record = self.records.get_mut(object_id)?;
454        let actual = Self::compute_with_algorithm(record.checksum.algorithm, data);
455        let matches = actual.value == record.checksum.value;
456        if matches {
457            record.verified_at = Some(now);
458        }
459        Some(CeVerificationResult {
460            object_id: object_id.to_owned(),
461            expected: record.checksum.clone(),
462            actual,
463            matches,
464            verified_at: now,
465        })
466    }
467
468    /// Retrieve the stored record for `object_id`, if any.
469    pub fn record(&self, object_id: &str) -> Option<&ChecksumRecord> {
470        self.records.get(object_id)
471    }
472
473    /// Remove the record for `object_id`. Returns `true` if the record existed.
474    pub fn remove(&mut self, object_id: &str) -> bool {
475        self.records.remove(object_id).is_some()
476    }
477
478    /// Verify all stored records by calling `data_fn` for each `object_id`.
479    ///
480    /// If `data_fn` returns `None` for a given id the object is skipped.
481    pub fn verify_all(
482        &mut self,
483        data_fn: impl Fn(&str) -> Option<Vec<u8>>,
484        now: u64,
485    ) -> Vec<CeVerificationResult> {
486        let ids: Vec<String> = self.records.keys().cloned().collect();
487        let mut results = Vec::with_capacity(ids.len());
488        for id in ids {
489            if let Some(data) = data_fn(&id) {
490                if let Some(result) = self.verify(&id, &data, now) {
491                    results.push(result);
492                }
493            }
494        }
495        results
496    }
497
498    /// Compute and store checksums for a batch of `(object_id, data)` pairs.
499    pub fn batch_compute(&mut self, items: &[(&str, &[u8])], now: u64) -> Vec<ChecksumRecord> {
500        items
501            .iter()
502            .map(|&(id, data)| self.compute_for(id.to_owned(), data, now))
503            .collect()
504    }
505
506    // ── Statistics ─────────────────────────────────────────────────────────
507
508    /// Count the number of verification results where `!matches`.
509    pub fn corruption_count(results: &[CeVerificationResult]) -> usize {
510        results.iter().filter(|r| !r.matches).count()
511    }
512
513    /// Number of objects currently tracked.
514    pub fn object_count(&self) -> usize {
515        self.records.len()
516    }
517
518    /// Compute aggregate statistics from the engine's state and a set of results.
519    pub fn stats(&self, results: &[CeVerificationResult]) -> ChecksumStats {
520        let total_records = self.records.len();
521        let verified_count = self
522            .records
523            .values()
524            .filter(|r| r.verified_at.is_some())
525            .count();
526        let corrupted = Self::corruption_count(results);
527        let corruption_rate = if results.is_empty() {
528            0.0
529        } else {
530            corrupted as f64 / results.len() as f64
531        };
532        ChecksumStats {
533            total_records,
534            verified_count,
535            corruption_rate,
536            algorithm: self.algorithm.name().to_owned(),
537        }
538    }
539}
540
541// ─────────────────────────────────────────────────────────────────────────────
542// Tests
543// ─────────────────────────────────────────────────────────────────────────────
544
545#[cfg(test)]
546mod tests {
547    use crate::checksum_engine::{
548        adler32, blake3_256_simple, crc32_iso, djb2, fnv1a_64, murmur3_32, xxhash64_simple,
549        CeVerificationResult, ChecksumAlgorithm, ChecksumRecord, StorageChecksumEngine,
550    };
551
552    // ── Algorithm unit tests ───────────────────────────────────────────────
553
554    #[test]
555    fn test_fnv1a64_empty() {
556        // FNV-1a-64 of empty slice is the offset basis
557        assert_eq!(fnv1a_64(b""), 14_695_981_039_346_656_037);
558    }
559
560    #[test]
561    fn test_fnv1a64_known_value() {
562        // "hello" FNV-1a-64 known reference
563        let h = fnv1a_64(b"hello");
564        assert_ne!(h, 0);
565        // Deterministic
566        assert_eq!(fnv1a_64(b"hello"), h);
567    }
568
569    #[test]
570    fn test_fnv1a64_different_inputs_differ() {
571        assert_ne!(fnv1a_64(b"foo"), fnv1a_64(b"bar"));
572    }
573
574    #[test]
575    fn test_djb2_empty() {
576        let h = djb2(b"");
577        assert_eq!(h, (5381u64 & 0xFFFF_FFFF) as u32);
578    }
579
580    #[test]
581    fn test_djb2_hello() {
582        let h = djb2(b"hello");
583        assert_ne!(h, 0);
584        assert_eq!(djb2(b"hello"), h);
585    }
586
587    #[test]
588    fn test_djb2_different() {
589        assert_ne!(djb2(b"abc"), djb2(b"xyz"));
590    }
591
592    #[test]
593    fn test_murmur3_32_empty() {
594        // Empty input should produce a stable value
595        let h = murmur3_32(b"");
596        assert_eq!(h, murmur3_32(b""));
597    }
598
599    #[test]
600    fn test_murmur3_32_4bytes() {
601        let h = murmur3_32(b"test");
602        assert_ne!(h, 0);
603        assert_eq!(murmur3_32(b"test"), h);
604    }
605
606    #[test]
607    fn test_murmur3_32_partial_tail() {
608        // 5 bytes: one full block + 1 tail byte
609        let h = murmur3_32(b"abcde");
610        assert_eq!(murmur3_32(b"abcde"), h);
611    }
612
613    #[test]
614    fn test_adler32_empty() {
615        // Empty → s1=1, s2=0 → result = 1
616        assert_eq!(adler32(b""), 1);
617    }
618
619    #[test]
620    fn test_adler32_abc() {
621        // Known: adler32("abc") = 0x024d0127 per RFC 1950
622        assert_eq!(adler32(b"abc"), 0x024d_0127);
623    }
624
625    #[test]
626    fn test_adler32_deterministic() {
627        let h = adler32(b"hello world");
628        assert_eq!(adler32(b"hello world"), h);
629    }
630
631    #[test]
632    fn test_crc32_empty() {
633        // CRC-32/ISO-HDLC of empty = 0x00000000
634        assert_eq!(crc32_iso(b""), 0x0000_0000);
635    }
636
637    #[test]
638    fn test_crc32_known_value() {
639        // "123456789" → CRC-32 = 0xCBF43926
640        assert_eq!(crc32_iso(b"123456789"), 0xCBF4_3926);
641    }
642
643    #[test]
644    fn test_crc32_deterministic() {
645        assert_eq!(crc32_iso(b"hello"), crc32_iso(b"hello"));
646    }
647
648    #[test]
649    fn test_xxhash64_empty() {
650        let h = xxhash64_simple(b"");
651        assert_eq!(xxhash64_simple(b""), h);
652    }
653
654    #[test]
655    fn test_xxhash64_hello() {
656        let h = xxhash64_simple(b"hello");
657        assert_ne!(h, 0);
658        assert_eq!(xxhash64_simple(b"hello"), h);
659    }
660
661    #[test]
662    fn test_xxhash64_large_input() {
663        // 64 bytes triggers the wide-lane path
664        let data: Vec<u8> = (0u8..64).collect();
665        let h = xxhash64_simple(&data);
666        assert_eq!(xxhash64_simple(&data), h);
667    }
668
669    #[test]
670    fn test_blake3_256_empty() {
671        let h = blake3_256_simple(b"");
672        assert_eq!(h.len(), 32);
673        assert_eq!(blake3_256_simple(b""), h);
674    }
675
676    #[test]
677    fn test_blake3_256_hello() {
678        let h = blake3_256_simple(b"hello");
679        assert_eq!(h.len(), 32);
680        assert_ne!(h, [0u8; 32]);
681    }
682
683    #[test]
684    fn test_blake3_256_different_inputs() {
685        assert_ne!(blake3_256_simple(b"a"), blake3_256_simple(b"b"));
686    }
687
688    // ── Checksum type tests ────────────────────────────────────────────────
689
690    #[test]
691    fn test_checksum_hex_encoding() {
692        let engine = StorageChecksumEngine::new(ChecksumAlgorithm::Fnv1a64);
693        let cs = engine.compute(b"hello");
694        assert_eq!(cs.hex.len(), 16); // 8 bytes → 16 hex chars
695        assert!(cs.hex.chars().all(|c| c.is_ascii_hexdigit()));
696    }
697
698    #[test]
699    fn test_checksum_hex_lowercase() {
700        let engine = StorageChecksumEngine::new(ChecksumAlgorithm::Crc32);
701        let cs = engine.compute(b"123456789");
702        assert!(cs.hex.chars().all(|c| !c.is_ascii_uppercase()));
703    }
704
705    #[test]
706    fn test_checksum_blake3_hex_length() {
707        let engine = StorageChecksumEngine::new(ChecksumAlgorithm::Blake3_256);
708        let cs = engine.compute(b"data");
709        assert_eq!(cs.hex.len(), 64); // 32 bytes → 64 hex chars
710    }
711
712    // ── Engine: compute_for / record / remove ──────────────────────────────
713
714    #[test]
715    fn test_compute_for_stores_record() {
716        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Crc32);
717        let rec = engine.compute_for("obj1".to_owned(), b"data", 1000);
718        assert_eq!(rec.object_id, "obj1");
719        assert_eq!(rec.computed_at, 1000);
720        assert_eq!(rec.size_bytes, 4);
721        assert!(rec.verified_at.is_none());
722        assert_eq!(engine.object_count(), 1);
723    }
724
725    #[test]
726    fn test_record_retrieval() {
727        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Adler32);
728        engine.compute_for("x".to_owned(), b"hello", 42);
729        let rec = engine.record("x");
730        assert!(rec.is_some());
731        assert_eq!(rec.map(|r| r.object_id.as_str()), Some("x"));
732    }
733
734    #[test]
735    fn test_record_missing_returns_none() {
736        let engine = StorageChecksumEngine::new(ChecksumAlgorithm::Djb2);
737        assert!(engine.record("nonexistent").is_none());
738    }
739
740    #[test]
741    fn test_remove_existing() {
742        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Fnv1a64);
743        engine.compute_for("a".to_owned(), b"x", 0);
744        assert!(engine.remove("a"));
745        assert_eq!(engine.object_count(), 0);
746    }
747
748    #[test]
749    fn test_remove_nonexistent() {
750        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Fnv1a64);
751        assert!(!engine.remove("ghost"));
752    }
753
754    // ── Engine: verify ─────────────────────────────────────────────────────
755
756    #[test]
757    fn test_verify_matching() {
758        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Crc32);
759        engine.compute_for("doc".to_owned(), b"content", 100);
760        let result = engine
761            .verify("doc", b"content", 200)
762            .expect("result expected");
763        assert!(result.matches);
764        assert_eq!(result.verified_at, 200);
765        // verified_at updated
766        assert_eq!(engine.record("doc").and_then(|r| r.verified_at), Some(200));
767    }
768
769    #[test]
770    fn test_verify_mismatch() {
771        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Crc32);
772        engine.compute_for("doc".to_owned(), b"original", 100);
773        let result = engine.verify("doc", b"corrupted", 200).expect("result");
774        assert!(!result.matches);
775        // verified_at not updated on mismatch
776        assert!(engine.record("doc").and_then(|r| r.verified_at).is_none());
777    }
778
779    #[test]
780    fn test_verify_missing_object() {
781        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Fnv1a64);
782        assert!(engine.verify("missing", b"data", 0).is_none());
783    }
784
785    #[test]
786    fn test_verify_updates_verified_at() {
787        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Xxhash64);
788        engine.compute_for("item".to_owned(), b"payload", 50);
789        let r = engine.verify("item", b"payload", 999).expect("ok");
790        assert!(r.matches);
791        assert_eq!(engine.record("item").and_then(|r| r.verified_at), Some(999));
792    }
793
794    // ── Engine: batch_compute ──────────────────────────────────────────────
795
796    #[test]
797    fn test_batch_compute() {
798        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Murmur3_32);
799        let items: Vec<(&str, &[u8])> = vec![("a", b"alpha"), ("b", b"beta"), ("c", b"gamma")];
800        let records: Vec<ChecksumRecord> = engine.batch_compute(&items, 777);
801        assert_eq!(records.len(), 3);
802        assert_eq!(engine.object_count(), 3);
803    }
804
805    #[test]
806    fn test_batch_compute_all_stored() {
807        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Adler32);
808        let items: Vec<(&str, &[u8])> = vec![("x", b"1"), ("y", b"2")];
809        engine.batch_compute(&items, 10);
810        assert!(engine.record("x").is_some());
811        assert!(engine.record("y").is_some());
812    }
813
814    // ── Engine: verify_all ─────────────────────────────────────────────────
815
816    #[test]
817    fn test_verify_all_clean() {
818        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Crc32);
819        engine.compute_for("p".to_owned(), b"ping", 1);
820        engine.compute_for("q".to_owned(), b"pong", 1);
821        let results = engine.verify_all(
822            |id| {
823                if id == "p" {
824                    Some(b"ping".to_vec())
825                } else {
826                    Some(b"pong".to_vec())
827                }
828            },
829            2,
830        );
831        assert_eq!(results.len(), 2);
832        assert!(results.iter().all(|r| r.matches));
833    }
834
835    #[test]
836    fn test_verify_all_with_corruption() {
837        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Crc32);
838        engine.compute_for("good".to_owned(), b"ok", 1);
839        engine.compute_for("bad".to_owned(), b"original", 1);
840        let results = engine.verify_all(
841            |id| {
842                if id == "good" {
843                    Some(b"ok".to_vec())
844                } else {
845                    Some(b"corrupted".to_vec())
846                }
847            },
848            2,
849        );
850        let corrupt = StorageChecksumEngine::corruption_count(&results);
851        assert_eq!(corrupt, 1);
852    }
853
854    #[test]
855    fn test_verify_all_data_fn_returns_none() {
856        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Fnv1a64);
857        engine.compute_for("present".to_owned(), b"data", 0);
858        engine.compute_for("absent".to_owned(), b"x", 0);
859        let results = engine.verify_all(
860            |id| {
861                if id == "present" {
862                    Some(b"data".to_vec())
863                } else {
864                    None
865                }
866            },
867            5,
868        );
869        // Only "present" was verifiable
870        assert_eq!(results.len(), 1);
871    }
872
873    // ── Engine: stats / corruption_count ──────────────────────────────────
874
875    #[test]
876    fn test_corruption_count_zero() {
877        let results: Vec<CeVerificationResult> = vec![];
878        assert_eq!(StorageChecksumEngine::corruption_count(&results), 0);
879    }
880
881    #[test]
882    fn test_stats_empty() {
883        let engine = StorageChecksumEngine::new(ChecksumAlgorithm::Blake3_256);
884        let stats = engine.stats(&[]);
885        assert_eq!(stats.total_records, 0);
886        assert_eq!(stats.verified_count, 0);
887        assert_eq!(stats.corruption_rate, 0.0);
888        assert_eq!(stats.algorithm, "blake3_256");
889    }
890
891    #[test]
892    fn test_stats_after_verification() {
893        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Crc32);
894        engine.compute_for("a".to_owned(), b"hello", 1);
895        engine.compute_for("b".to_owned(), b"world", 1);
896        let results = engine.verify_all(
897            |id| {
898                if id == "a" {
899                    Some(b"hello".to_vec())
900                } else {
901                    Some(b"corrupted".to_vec())
902                }
903            },
904            2,
905        );
906        let stats = engine.stats(&results);
907        assert_eq!(stats.total_records, 2);
908        assert_eq!(stats.verified_count, 1); // only "a" passed
909        assert!((stats.corruption_rate - 0.5).abs() < f64::EPSILON);
910    }
911
912    #[test]
913    fn test_algorithm_name() {
914        assert_eq!(ChecksumAlgorithm::Fnv1a64.name(), "fnv1a64");
915        assert_eq!(ChecksumAlgorithm::Djb2.name(), "djb2");
916        assert_eq!(ChecksumAlgorithm::Blake3_256.name(), "blake3_256");
917        assert_eq!(ChecksumAlgorithm::Crc32.name(), "crc32");
918    }
919
920    #[test]
921    fn test_all_algorithms_produce_nonzero_for_nonempty() {
922        let data = b"checksum test data";
923        let algos = [
924            ChecksumAlgorithm::Fnv1a64,
925            ChecksumAlgorithm::Djb2,
926            ChecksumAlgorithm::Murmur3_32,
927            ChecksumAlgorithm::Adler32,
928            ChecksumAlgorithm::Crc32,
929            ChecksumAlgorithm::Xxhash64,
930            ChecksumAlgorithm::Blake3_256,
931        ];
932        for algo in algos {
933            let cs = StorageChecksumEngine::compute_with_algorithm(algo, data);
934            assert!(
935                cs.value.iter().any(|&b| b != 0),
936                "Algorithm {:?} returned all-zero for non-empty input",
937                algo
938            );
939        }
940    }
941
942    #[test]
943    fn test_overwrite_record_on_recompute() {
944        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Crc32);
945        engine.compute_for("obj".to_owned(), b"v1", 10);
946        engine.compute_for("obj".to_owned(), b"v2", 20);
947        // Should be overwritten
948        assert_eq!(engine.object_count(), 1);
949        let rec = engine.record("obj").expect("present");
950        assert_eq!(rec.computed_at, 20);
951        assert_eq!(rec.size_bytes, 2);
952    }
953
954    #[test]
955    fn test_verify_on_read_flag() {
956        let mut engine = StorageChecksumEngine::new(ChecksumAlgorithm::Fnv1a64);
957        engine.verify_on_read = true;
958        assert!(engine.verify_on_read);
959    }
960
961    #[test]
962    fn test_compute_with_algorithm_static() {
963        let cs = StorageChecksumEngine::compute_with_algorithm(ChecksumAlgorithm::Adler32, b"abc");
964        assert_eq!(cs.algorithm, ChecksumAlgorithm::Adler32);
965        // adler32("abc") is known
966        let expected = adler32(b"abc").to_le_bytes().to_vec();
967        assert_eq!(cs.value, expected);
968    }
969}