Skip to main content

ipfrs_storage/
object_integrity_checker.rs

1//! Object Integrity Checker — multi-level hash-based object integrity verification.
2//!
3//! Provides `ObjectIntegrityChecker`, a production-grade component that computes
4//! FNV-1a-64, Adler-32, and CRC-16 CCITT checksums over arbitrary byte payloads,
5//! stores canonical `ObjectRecord`s, and verifies them at configurable integrity
6//! levels (Quick, Standard, Full, Custom).
7
8use std::collections::HashMap;
9use std::time::{SystemTime, UNIX_EPOCH};
10use thiserror::Error;
11
12// ---------------------------------------------------------------------------
13// Pure-Rust hash primitives (no external crate)
14// ---------------------------------------------------------------------------
15
16/// FNV-1a 64-bit hash.
17#[inline]
18fn fnv1a_64(data: &[u8]) -> u64 {
19    const OFFSET_BASIS: u64 = 14695981039346656037;
20    const PRIME: u64 = 1099511628211;
21    let mut hash = OFFSET_BASIS;
22    for &byte in data {
23        hash ^= byte as u64;
24        hash = hash.wrapping_mul(PRIME);
25    }
26    hash
27}
28
29/// Adler-32 checksum (modulo 65521).
30#[inline]
31fn adler32(data: &[u8]) -> u32 {
32    let (mut a, mut b) = (1u32, 0u32);
33    for &byte in data {
34        a = a.wrapping_add(byte as u32) % 65521;
35        b = b.wrapping_add(a) % 65521;
36    }
37    (b << 16) | a
38}
39
40/// CRC-16 CCITT.
41#[inline]
42fn crc16(data: &[u8]) -> u16 {
43    let mut crc = 0xFFFFu16;
44    for &byte in data {
45        let mut x = (crc >> 8) ^ (byte as u16);
46        x ^= x >> 4;
47        crc = (crc << 8) ^ (x << 12) ^ (x << 5) ^ x;
48    }
49    crc
50}
51
52// ---------------------------------------------------------------------------
53// Public types
54// ---------------------------------------------------------------------------
55
56/// Composite integrity digest computed over a byte slice.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct IntegrityHash {
59    /// FNV-1a 64-bit digest.
60    pub fnv1a: u64,
61    /// Adler-32 digest.
62    pub adler32: u32,
63    /// CRC-16 CCITT digest.
64    pub crc16: u16,
65    /// Byte length of the source data.
66    pub size_bytes: usize,
67}
68
69impl IntegrityHash {
70    fn compute(data: &[u8]) -> Self {
71        Self {
72            fnv1a: fnv1a_64(data),
73            adler32: adler32(data),
74            crc16: crc16(data),
75            size_bytes: data.len(),
76        }
77    }
78}
79
80/// Specifies which hash algorithms are executed during verification.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum IntegrityLevel {
83    /// Only CRC-16 (fastest).
84    Quick,
85    /// FNV-1a-64 + Adler-32 (no CRC).
86    Standard,
87    /// All three algorithms.
88    Full,
89    /// Named algorithm set (validated against supported names at runtime).
90    Custom(Vec<String>),
91}
92
93impl IntegrityLevel {
94    /// Supported algorithm names for `Custom` variants.
95    pub const SUPPORTED: &'static [&'static str] = &["fnv1a", "adler32", "crc16"];
96}
97
98/// Lifecycle status of a registered object.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum OicIntegrityStatus {
101    /// No verification has been performed yet.
102    Unknown,
103    /// Successfully verified at the given Unix-microsecond timestamp.
104    Verified(u64),
105    /// A mismatch was detected.
106    Corrupted {
107        /// When the corruption was first detected (Unix µs).
108        detected_at: u64,
109        /// The hash that was expected (stored canonical value).
110        expected: IntegrityHash,
111        /// The hash computed from the presented data.
112        got: IntegrityHash,
113    },
114    /// Flagged as possibly corrupted with a human-readable reason.
115    PossiblyCorrupted(String),
116}
117
118/// Persistent record for a single registered object.
119#[derive(Debug, Clone)]
120pub struct OicObjectRecord {
121    /// Stable identifier supplied by the caller.
122    pub id: String,
123    /// Canonical hash computed at registration (or last `update`).
124    pub hash: IntegrityHash,
125    /// Unix-microsecond timestamp of registration.
126    pub created_at: u64,
127    /// Unix-microsecond timestamp of the most recent successful verification (or 0).
128    pub last_verified_at: u64,
129    /// Total number of times `verify` has been called for this object.
130    pub verification_count: u64,
131    /// Current integrity status.
132    pub status: OicIntegrityStatus,
133}
134
135/// Detailed result of a single `verify` call.
136#[derive(Debug, Clone)]
137pub struct OicVerificationResult {
138    /// Object identifier.
139    pub object_id: String,
140    /// Whether all checks at the requested level passed.
141    pub passed: bool,
142    /// Integrity level that was applied.
143    pub level: IntegrityLevel,
144    /// Human-readable descriptions of any mismatches detected.
145    pub errors: Vec<String>,
146    /// Wall-clock duration of the verification in microseconds.
147    pub duration_us: u64,
148}
149
150/// Configuration for `ObjectIntegrityChecker`.
151#[derive(Debug, Clone)]
152pub struct CheckerConfig {
153    /// Default level used when callers do not supply one explicitly.
154    pub default_level: IntegrityLevel,
155    /// When `true` the checker automatically performs a Full verification on every
156    /// `verify` call even if only Quick is requested (future hook; not enforced
157    /// beyond storing the intent).
158    pub auto_verify_on_read: bool,
159    /// Alert threshold: if `corrupted_objects / total_objects` exceeds this ratio
160    /// the stats will surface it.  Range `[0.0, 1.0]`.
161    pub corruption_threshold: f64,
162    /// Hard cap on the number of objects that may be registered simultaneously.
163    pub max_objects: usize,
164}
165
166impl Default for CheckerConfig {
167    fn default() -> Self {
168        Self {
169            default_level: IntegrityLevel::Full,
170            auto_verify_on_read: false,
171            corruption_threshold: 0.05,
172            max_objects: 1_000_000,
173        }
174    }
175}
176
177/// Aggregated statistics over all registered objects.
178#[derive(Debug, Clone)]
179pub struct CheckerStats {
180    /// Number of currently registered objects.
181    pub total_objects: usize,
182    /// Number of objects whose status is `Verified(_)`.
183    pub verified_objects: usize,
184    /// Number of objects whose status is `Corrupted{..}`.
185    pub corrupted_objects: usize,
186    /// `corrupted_objects / total_objects`, or 0.0 when `total_objects == 0`.
187    pub corruption_rate: f64,
188    /// Cumulative number of `verify` invocations across all objects.
189    pub total_verifications: u64,
190    /// Rolling mean verification duration in microseconds.
191    pub avg_verification_us: f64,
192}
193
194/// Errors that can be returned by `ObjectIntegrityChecker`.
195#[derive(Debug, Error, PartialEq, Eq)]
196pub enum CheckerError {
197    /// The requested object identifier is not registered.
198    #[error("object not found: {0}")]
199    ObjectNotFound(String),
200    /// A specific hash field did not match.
201    #[error("hash mismatch for '{id}' on field '{field}'")]
202    HashMismatch {
203        /// Object identifier.
204        id: String,
205        /// Name of the mismatched field (`fnv1a`, `adler32`, `crc16`, or `size`).
206        field: String,
207    },
208    /// The `max_objects` limit in `CheckerConfig` would be exceeded.
209    #[error("maximum object capacity exceeded")]
210    MaxObjectsExceeded,
211    /// A `Custom` level contained an unrecognised algorithm name.
212    #[error("invalid integrity level: {0}")]
213    InvalidLevel(String),
214}
215
216// ---------------------------------------------------------------------------
217// Core checker
218// ---------------------------------------------------------------------------
219
220/// Production-grade multi-level object integrity checker.
221///
222/// Thread-safety is the caller's responsibility; wrap in `Arc<Mutex<…>>` if
223/// shared across threads.
224pub struct ObjectIntegrityChecker {
225    config: CheckerConfig,
226    objects: HashMap<String, OicObjectRecord>,
227    /// Accumulated verification durations (µs) for rolling-average calculation.
228    total_verify_us: u64,
229    /// Total number of `verify` calls dispatched.
230    total_verifications: u64,
231}
232
233impl ObjectIntegrityChecker {
234    /// Create a new checker with the provided configuration.
235    pub fn new(config: CheckerConfig) -> Self {
236        Self {
237            objects: HashMap::new(),
238            total_verify_us: 0,
239            total_verifications: 0,
240            config,
241        }
242    }
243
244    /// Create a new checker with default configuration.
245    pub fn with_defaults() -> Self {
246        Self::new(CheckerConfig::default())
247    }
248
249    // -----------------------------------------------------------------------
250    // Internal helpers
251    // -----------------------------------------------------------------------
252
253    fn now_us() -> u64 {
254        SystemTime::now()
255            .duration_since(UNIX_EPOCH)
256            .map(|d| d.as_micros() as u64)
257            .unwrap_or(0)
258    }
259
260    fn validate_level(level: &IntegrityLevel) -> Result<(), CheckerError> {
261        if let IntegrityLevel::Custom(algos) = level {
262            for algo in algos {
263                if !IntegrityLevel::SUPPORTED.contains(&algo.as_str()) {
264                    return Err(CheckerError::InvalidLevel(format!(
265                        "unsupported algorithm '{}'; supported: {:?}",
266                        algo,
267                        IntegrityLevel::SUPPORTED
268                    )));
269                }
270            }
271        }
272        Ok(())
273    }
274
275    /// Run the configured level checks, returning a list of error strings.
276    fn run_checks(
277        stored: &IntegrityHash,
278        fresh: &IntegrityHash,
279        level: &IntegrityLevel,
280    ) -> Vec<String> {
281        let mut errors = Vec::new();
282
283        // Size check always applies.
284        if stored.size_bytes != fresh.size_bytes {
285            errors.push(format!(
286                "size mismatch: expected {} bytes, got {}",
287                stored.size_bytes, fresh.size_bytes
288            ));
289        }
290
291        match level {
292            IntegrityLevel::Quick => {
293                if stored.crc16 != fresh.crc16 {
294                    errors.push(format!(
295                        "crc16 mismatch: expected {:#06x}, got {:#06x}",
296                        stored.crc16, fresh.crc16
297                    ));
298                }
299            }
300            IntegrityLevel::Standard => {
301                if stored.fnv1a != fresh.fnv1a {
302                    errors.push(format!(
303                        "fnv1a mismatch: expected {:#018x}, got {:#018x}",
304                        stored.fnv1a, fresh.fnv1a
305                    ));
306                }
307                if stored.adler32 != fresh.adler32 {
308                    errors.push(format!(
309                        "adler32 mismatch: expected {:#010x}, got {:#010x}",
310                        stored.adler32, fresh.adler32
311                    ));
312                }
313            }
314            IntegrityLevel::Full => {
315                if stored.fnv1a != fresh.fnv1a {
316                    errors.push(format!(
317                        "fnv1a mismatch: expected {:#018x}, got {:#018x}",
318                        stored.fnv1a, fresh.fnv1a
319                    ));
320                }
321                if stored.adler32 != fresh.adler32 {
322                    errors.push(format!(
323                        "adler32 mismatch: expected {:#010x}, got {:#010x}",
324                        stored.adler32, fresh.adler32
325                    ));
326                }
327                if stored.crc16 != fresh.crc16 {
328                    errors.push(format!(
329                        "crc16 mismatch: expected {:#06x}, got {:#06x}",
330                        stored.crc16, fresh.crc16
331                    ));
332                }
333            }
334            IntegrityLevel::Custom(algos) => {
335                for algo in algos {
336                    match algo.as_str() {
337                        "fnv1a" => {
338                            if stored.fnv1a != fresh.fnv1a {
339                                errors.push(format!(
340                                    "fnv1a mismatch: expected {:#018x}, got {:#018x}",
341                                    stored.fnv1a, fresh.fnv1a
342                                ));
343                            }
344                        }
345                        "adler32" => {
346                            if stored.adler32 != fresh.adler32 {
347                                errors.push(format!(
348                                    "adler32 mismatch: expected {:#010x}, got {:#010x}",
349                                    stored.adler32, fresh.adler32
350                                ));
351                            }
352                        }
353                        "crc16" => {
354                            if stored.crc16 != fresh.crc16 {
355                                errors.push(format!(
356                                    "crc16 mismatch: expected {:#06x}, got {:#06x}",
357                                    stored.crc16, fresh.crc16
358                                ));
359                            }
360                        }
361                        _ => {
362                            // Already validated in validate_level; this branch is unreachable
363                            // in normal flow.
364                            errors.push(format!("unknown algorithm '{algo}' skipped"));
365                        }
366                    }
367                }
368            }
369        }
370
371        errors
372    }
373
374    // -----------------------------------------------------------------------
375    // Public API
376    // -----------------------------------------------------------------------
377
378    /// Register a new object and compute its canonical hashes.
379    ///
380    /// Returns the `IntegrityHash` on success.  Returns
381    /// `CheckerError::MaxObjectsExceeded` if the configured cap is reached.
382    /// Re-registering an existing id overwrites the stored record.
383    pub fn register(&mut self, id: String, data: &[u8]) -> Result<IntegrityHash, CheckerError> {
384        if !self.objects.contains_key(&id) && self.objects.len() >= self.config.max_objects {
385            return Err(CheckerError::MaxObjectsExceeded);
386        }
387        let hash = IntegrityHash::compute(data);
388        let now = Self::now_us();
389        let record = OicObjectRecord {
390            id: id.clone(),
391            hash: hash.clone(),
392            created_at: now,
393            last_verified_at: 0,
394            verification_count: 0,
395            status: OicIntegrityStatus::Unknown,
396        };
397        self.objects.insert(id, record);
398        Ok(hash)
399    }
400
401    /// Verify `data` against the stored hashes for `id` at the given `level`.
402    ///
403    /// Updates the object's `status`, `last_verified_at`, and
404    /// `verification_count`.  Returns a detailed `OicVerificationResult`.
405    pub fn verify(
406        &mut self,
407        id: &str,
408        data: &[u8],
409        level: IntegrityLevel,
410    ) -> Result<OicVerificationResult, CheckerError> {
411        Self::validate_level(&level)?;
412
413        let record = self
414            .objects
415            .get(id)
416            .ok_or_else(|| CheckerError::ObjectNotFound(id.to_string()))?;
417
418        let stored = record.hash.clone();
419        let t_start = Self::now_us();
420        let fresh = IntegrityHash::compute(data);
421        let errors = Self::run_checks(&stored, &fresh, &level);
422        let t_end = Self::now_us();
423        let duration_us = t_end.saturating_sub(t_start);
424
425        let passed = errors.is_empty();
426        let now = Self::now_us();
427
428        // Update the record.
429        let record = self
430            .objects
431            .get_mut(id)
432            .ok_or_else(|| CheckerError::ObjectNotFound(id.to_string()))?;
433
434        record.verification_count += 1;
435        self.total_verifications += 1;
436        self.total_verify_us = self.total_verify_us.saturating_add(duration_us);
437
438        if passed {
439            record.last_verified_at = now;
440            record.status = OicIntegrityStatus::Verified(now);
441        } else {
442            record.status = OicIntegrityStatus::Corrupted {
443                detected_at: now,
444                expected: stored.clone(),
445                got: fresh.clone(),
446            };
447        }
448
449        Ok(OicVerificationResult {
450            object_id: id.to_string(),
451            passed,
452            level,
453            errors,
454            duration_us,
455        })
456    }
457
458    /// Batch-verify multiple `(id, data)` pairs at the given `level`.
459    ///
460    /// Objects that are not found are reported as failed verifications with an
461    /// error message rather than causing a panic or early abort.
462    pub fn verify_all(
463        &mut self,
464        objects: &[(&str, &[u8])],
465        level: IntegrityLevel,
466    ) -> Vec<OicVerificationResult> {
467        let mut results = Vec::with_capacity(objects.len());
468        for &(id, data) in objects {
469            let result = match self.verify(id, data, level.clone()) {
470                Ok(r) => r,
471                Err(e) => OicVerificationResult {
472                    object_id: id.to_string(),
473                    passed: false,
474                    level: level.clone(),
475                    errors: vec![e.to_string()],
476                    duration_us: 0,
477                },
478            };
479            results.push(result);
480        }
481        results
482    }
483
484    /// Mark an object as `PossiblyCorrupted` with a caller-supplied reason.
485    pub fn mark_corrupted(&mut self, id: &str, reason: String) -> Result<(), CheckerError> {
486        let record = self
487            .objects
488            .get_mut(id)
489            .ok_or_else(|| CheckerError::ObjectNotFound(id.to_string()))?;
490        record.status = OicIntegrityStatus::PossiblyCorrupted(reason);
491        Ok(())
492    }
493
494    /// Replace the stored canonical hashes with freshly computed values from
495    /// `new_data` and reset the verification counter.
496    pub fn update(&mut self, id: &str, new_data: &[u8]) -> Result<IntegrityHash, CheckerError> {
497        let record = self
498            .objects
499            .get_mut(id)
500            .ok_or_else(|| CheckerError::ObjectNotFound(id.to_string()))?;
501        let hash = IntegrityHash::compute(new_data);
502        record.hash = hash.clone();
503        record.verification_count = 0;
504        record.last_verified_at = 0;
505        record.status = OicIntegrityStatus::Unknown;
506        Ok(hash)
507    }
508
509    /// Deregister an object, freeing its stored record.
510    pub fn remove(&mut self, id: &str) -> Result<(), CheckerError> {
511        self.objects
512            .remove(id)
513            .map(|_| ())
514            .ok_or_else(|| CheckerError::ObjectNotFound(id.to_string()))
515    }
516
517    /// Return references to all objects currently in a `Corrupted` state.
518    pub fn corrupted_objects(&self) -> Vec<&OicObjectRecord> {
519        self.objects
520            .values()
521            .filter(|r| matches!(r.status, OicIntegrityStatus::Corrupted { .. }))
522            .collect()
523    }
524
525    /// Return references to objects whose `last_verified_at` is older than
526    /// `since_us` (i.e. `last_verified_at < since_us`), including those that
527    /// have never been verified (`last_verified_at == 0`).
528    pub fn objects_needing_verification(&self, since_us: u64) -> Vec<&OicObjectRecord> {
529        self.objects
530            .values()
531            .filter(|r| r.last_verified_at < since_us)
532            .collect()
533    }
534
535    /// Recompute hashes from `correct_data` and clear any corruption status.
536    ///
537    /// This is the "repair" path: after retrieving a known-good copy of an
538    /// object, call this to reset the canonical digest and mark the object as
539    /// verified.
540    pub fn repair_hash(&mut self, id: &str, correct_data: &[u8]) -> Result<(), CheckerError> {
541        let record = self
542            .objects
543            .get_mut(id)
544            .ok_or_else(|| CheckerError::ObjectNotFound(id.to_string()))?;
545        let now = Self::now_us();
546        record.hash = IntegrityHash::compute(correct_data);
547        record.status = OicIntegrityStatus::Verified(now);
548        record.last_verified_at = now;
549        Ok(())
550    }
551
552    /// Snapshot current aggregate statistics.
553    pub fn stats(&self) -> CheckerStats {
554        let total_objects = self.objects.len();
555        let verified_objects = self
556            .objects
557            .values()
558            .filter(|r| matches!(r.status, OicIntegrityStatus::Verified(_)))
559            .count();
560        let corrupted_objects = self
561            .objects
562            .values()
563            .filter(|r| matches!(r.status, OicIntegrityStatus::Corrupted { .. }))
564            .count();
565        let corruption_rate = if total_objects == 0 {
566            0.0
567        } else {
568            corrupted_objects as f64 / total_objects as f64
569        };
570        let avg_verification_us = if self.total_verifications == 0 {
571            0.0
572        } else {
573            self.total_verify_us as f64 / self.total_verifications as f64
574        };
575        CheckerStats {
576            total_objects,
577            verified_objects,
578            corrupted_objects,
579            corruption_rate,
580            total_verifications: self.total_verifications,
581            avg_verification_us,
582        }
583    }
584
585    /// Access the underlying configuration.
586    pub fn config(&self) -> &CheckerConfig {
587        &self.config
588    }
589
590    /// Number of currently registered objects.
591    pub fn len(&self) -> usize {
592        self.objects.len()
593    }
594
595    /// Returns `true` when no objects are registered.
596    pub fn is_empty(&self) -> bool {
597        self.objects.is_empty()
598    }
599}
600
601// ---------------------------------------------------------------------------
602// Tests
603// ---------------------------------------------------------------------------
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608
609    // -----------------------------------------------------------------------
610    // Inline xorshift64 PRNG — no `rand` crate
611    // -----------------------------------------------------------------------
612
613    struct Xorshift64(u64);
614
615    impl Xorshift64 {
616        fn new(seed: u64) -> Self {
617            // Ensure a non-zero seed.
618            Self(if seed == 0 { 0xdeadbeef_cafebabe } else { seed })
619        }
620
621        fn next(&mut self) -> u64 {
622            let mut x = self.0;
623            x ^= x << 13;
624            x ^= x >> 7;
625            x ^= x << 17;
626            self.0 = x;
627            x
628        }
629
630        fn next_bytes(&mut self, buf: &mut [u8]) {
631            let mut i = 0;
632            while i < buf.len() {
633                let v = self.next().to_le_bytes();
634                let take = (buf.len() - i).min(8);
635                buf[i..i + take].copy_from_slice(&v[..take]);
636                i += take;
637            }
638        }
639    }
640
641    fn rng_bytes(seed: u64, len: usize) -> Vec<u8> {
642        let mut rng = Xorshift64::new(seed);
643        let mut buf = vec![0u8; len];
644        rng.next_bytes(&mut buf);
645        buf
646    }
647
648    fn checker() -> ObjectIntegrityChecker {
649        ObjectIntegrityChecker::with_defaults()
650    }
651
652    // -----------------------------------------------------------------------
653    // Hash primitive unit tests
654    // -----------------------------------------------------------------------
655
656    #[test]
657    fn test_fnv1a_empty() {
658        // Known FNV-1a offset basis for empty input
659        assert_eq!(fnv1a_64(b""), 14695981039346656037u64);
660    }
661
662    #[test]
663    fn test_fnv1a_hello() {
664        // FNV-1a of "hello" is a well-known value
665        let h = fnv1a_64(b"hello");
666        assert_ne!(h, fnv1a_64(b"world"));
667    }
668
669    #[test]
670    fn test_adler32_empty() {
671        // adler32("") = (0 << 16) | 1 = 1
672        assert_eq!(adler32(b""), 1);
673    }
674
675    #[test]
676    fn test_adler32_abc() {
677        // "ABC" = bytes 65, 66, 67
678        // a: 1 + 65 = 66; 66 + 66 = 132; 132 + 67 = 199
679        // b: 0 + 66 = 66; 66 + 132 = 198; 198 + 199 = 397
680        // result: (397 << 16) | 199 = 26017991
681        assert_eq!(adler32(b"ABC"), 26017991);
682    }
683
684    #[test]
685    fn test_crc16_empty() {
686        // No bytes processed → init value 0xFFFF is returned unchanged
687        assert_eq!(crc16(b""), 0xFFFF);
688    }
689
690    #[test]
691    fn test_crc16_deterministic() {
692        let a = crc16(b"deterministic");
693        let b = crc16(b"deterministic");
694        assert_eq!(a, b);
695    }
696
697    #[test]
698    fn test_crc16_distinct() {
699        assert_ne!(crc16(b"hello"), crc16(b"world"));
700    }
701
702    // -----------------------------------------------------------------------
703    // IntegrityHash::compute
704    // -----------------------------------------------------------------------
705
706    #[test]
707    fn test_integrity_hash_compute_size() {
708        let data = b"test data";
709        let h = IntegrityHash::compute(data);
710        assert_eq!(h.size_bytes, data.len());
711    }
712
713    #[test]
714    fn test_integrity_hash_equality() {
715        let data = b"consistent";
716        assert_eq!(IntegrityHash::compute(data), IntegrityHash::compute(data));
717    }
718
719    #[test]
720    fn test_integrity_hash_inequality() {
721        assert_ne!(
722            IntegrityHash::compute(b"aaa"),
723            IntegrityHash::compute(b"bbb")
724        );
725    }
726
727    // -----------------------------------------------------------------------
728    // register
729    // -----------------------------------------------------------------------
730
731    #[test]
732    fn test_register_basic() {
733        let mut c = checker();
734        let data = b"hello world";
735        let h = c.register("obj1".to_string(), data).unwrap();
736        assert_eq!(h, IntegrityHash::compute(data));
737    }
738
739    #[test]
740    fn test_register_stores_record() {
741        let mut c = checker();
742        c.register("a".to_string(), b"payload").unwrap();
743        assert_eq!(c.len(), 1);
744    }
745
746    #[test]
747    fn test_register_overwrite() {
748        let mut c = checker();
749        c.register("x".to_string(), b"v1").unwrap();
750        let h2 = c.register("x".to_string(), b"v2").unwrap();
751        assert_eq!(h2, IntegrityHash::compute(b"v2"));
752        assert_eq!(c.len(), 1);
753    }
754
755    #[test]
756    fn test_register_empty_data() {
757        let mut c = checker();
758        let h = c.register("empty".to_string(), b"").unwrap();
759        assert_eq!(h.size_bytes, 0);
760    }
761
762    #[test]
763    fn test_register_max_objects_exceeded() {
764        let config = CheckerConfig {
765            max_objects: 2,
766            ..Default::default()
767        };
768        let mut c = ObjectIntegrityChecker::new(config);
769        c.register("a".to_string(), b"data").unwrap();
770        c.register("b".to_string(), b"data").unwrap();
771        let err = c.register("c".to_string(), b"data").unwrap_err();
772        assert_eq!(err, CheckerError::MaxObjectsExceeded);
773    }
774
775    // -----------------------------------------------------------------------
776    // verify — Quick level
777    // -----------------------------------------------------------------------
778
779    #[test]
780    fn test_verify_quick_pass() {
781        let mut c = checker();
782        c.register("q".to_string(), b"data").unwrap();
783        let r = c.verify("q", b"data", IntegrityLevel::Quick).unwrap();
784        assert!(r.passed);
785        assert!(r.errors.is_empty());
786    }
787
788    #[test]
789    fn test_verify_quick_fail() {
790        let mut c = checker();
791        c.register("q".to_string(), b"original").unwrap();
792        let r = c.verify("q", b"tampered", IntegrityLevel::Quick).unwrap();
793        assert!(!r.passed);
794        assert!(!r.errors.is_empty());
795    }
796
797    #[test]
798    fn test_verify_quick_size_mismatch() {
799        let mut c = checker();
800        c.register("q".to_string(), b"hello").unwrap();
801        let r = c.verify("q", b"hi", IntegrityLevel::Quick).unwrap();
802        assert!(!r.passed);
803    }
804
805    // -----------------------------------------------------------------------
806    // verify — Standard level
807    // -----------------------------------------------------------------------
808
809    #[test]
810    fn test_verify_standard_pass() {
811        let mut c = checker();
812        c.register("s".to_string(), b"payload").unwrap();
813        let r = c.verify("s", b"payload", IntegrityLevel::Standard).unwrap();
814        assert!(r.passed);
815    }
816
817    #[test]
818    fn test_verify_standard_fail_fnv() {
819        let mut c = checker();
820        c.register("s".to_string(), b"original").unwrap();
821        let r = c
822            .verify("s", b"modified", IntegrityLevel::Standard)
823            .unwrap();
824        assert!(!r.passed);
825    }
826
827    // -----------------------------------------------------------------------
828    // verify — Full level
829    // -----------------------------------------------------------------------
830
831    #[test]
832    fn test_verify_full_pass() {
833        let mut c = checker();
834        c.register("f".to_string(), b"full check").unwrap();
835        let r = c.verify("f", b"full check", IntegrityLevel::Full).unwrap();
836        assert!(r.passed);
837        assert!(r.errors.is_empty());
838    }
839
840    #[test]
841    fn test_verify_full_fail_multiple_errors() {
842        let mut c = checker();
843        c.register("f".to_string(), b"abcdefgh").unwrap();
844        let r = c.verify("f", b"ABCDEFGH", IntegrityLevel::Full).unwrap();
845        assert!(!r.passed);
846        // All three hash fields should report mismatch.
847        assert!(
848            r.errors.len() >= 3,
849            "expected ≥3 errors, got {}",
850            r.errors.len()
851        );
852    }
853
854    // -----------------------------------------------------------------------
855    // verify — Custom level
856    // -----------------------------------------------------------------------
857
858    #[test]
859    fn test_verify_custom_fnv_only_pass() {
860        let mut c = checker();
861        c.register("cust".to_string(), b"data").unwrap();
862        let lvl = IntegrityLevel::Custom(vec!["fnv1a".to_string()]);
863        let r = c.verify("cust", b"data", lvl).unwrap();
864        assert!(r.passed);
865    }
866
867    #[test]
868    fn test_verify_custom_fnv_only_fail() {
869        let mut c = checker();
870        c.register("cust".to_string(), b"data").unwrap();
871        let lvl = IntegrityLevel::Custom(vec!["fnv1a".to_string()]);
872        let r = c.verify("cust", b"DATA", lvl).unwrap();
873        assert!(!r.passed);
874    }
875
876    #[test]
877    fn test_verify_custom_adler32_only() {
878        let mut c = checker();
879        c.register("ad".to_string(), b"check").unwrap();
880        let lvl = IntegrityLevel::Custom(vec!["adler32".to_string()]);
881        let r = c.verify("ad", b"check", lvl).unwrap();
882        assert!(r.passed);
883    }
884
885    #[test]
886    fn test_verify_custom_invalid_algo() {
887        let mut c = checker();
888        c.register("inv".to_string(), b"x").unwrap();
889        let lvl = IntegrityLevel::Custom(vec!["sha256".to_string()]);
890        let err = c.verify("inv", b"x", lvl).unwrap_err();
891        assert!(matches!(err, CheckerError::InvalidLevel(_)));
892    }
893
894    #[test]
895    fn test_verify_custom_all_algos() {
896        let mut c = checker();
897        c.register("all".to_string(), b"alldata").unwrap();
898        let lvl = IntegrityLevel::Custom(vec![
899            "fnv1a".to_string(),
900            "adler32".to_string(),
901            "crc16".to_string(),
902        ]);
903        let r = c.verify("all", b"alldata", lvl).unwrap();
904        assert!(r.passed);
905    }
906
907    // -----------------------------------------------------------------------
908    // Verification side-effects
909    // -----------------------------------------------------------------------
910
911    #[test]
912    fn test_verify_increments_count() {
913        let mut c = checker();
914        c.register("v".to_string(), b"data").unwrap();
915        c.verify("v", b"data", IntegrityLevel::Full).unwrap();
916        c.verify("v", b"data", IntegrityLevel::Quick).unwrap();
917        let s = c.stats();
918        assert_eq!(s.total_verifications, 2);
919    }
920
921    #[test]
922    fn test_verify_updates_status_on_pass() {
923        let mut c = checker();
924        c.register("u".to_string(), b"data").unwrap();
925        c.verify("u", b"data", IntegrityLevel::Full).unwrap();
926        let s = c.stats();
927        assert_eq!(s.verified_objects, 1);
928    }
929
930    #[test]
931    fn test_verify_updates_status_on_fail() {
932        let mut c = checker();
933        c.register("u".to_string(), b"original").unwrap();
934        c.verify("u", b"tampered", IntegrityLevel::Full).unwrap();
935        let s = c.stats();
936        assert_eq!(s.corrupted_objects, 1);
937    }
938
939    #[test]
940    fn test_verify_not_found() {
941        let mut c = checker();
942        let err = c
943            .verify("ghost", b"data", IntegrityLevel::Quick)
944            .unwrap_err();
945        assert!(matches!(err, CheckerError::ObjectNotFound(_)));
946    }
947
948    // -----------------------------------------------------------------------
949    // verify_all — batch
950    // -----------------------------------------------------------------------
951
952    #[test]
953    fn test_verify_all_all_pass() {
954        let mut c = checker();
955        c.register("a1".to_string(), b"alpha").unwrap();
956        c.register("b1".to_string(), b"beta").unwrap();
957        let pairs: Vec<(&str, &[u8])> = vec![("a1", b"alpha"), ("b1", b"beta")];
958        let results = c.verify_all(&pairs, IntegrityLevel::Full);
959        assert_eq!(results.len(), 2);
960        assert!(results.iter().all(|r| r.passed));
961    }
962
963    #[test]
964    fn test_verify_all_mixed() {
965        let mut c = checker();
966        c.register("good".to_string(), b"ok").unwrap();
967        c.register("bad".to_string(), b"original").unwrap();
968        let pairs: Vec<(&str, &[u8])> = vec![("good", b"ok"), ("bad", b"tampered")];
969        let results = c.verify_all(&pairs, IntegrityLevel::Full);
970        assert!(results[0].passed);
971        assert!(!results[1].passed);
972    }
973
974    #[test]
975    fn test_verify_all_missing_object() {
976        let mut c = checker();
977        let pairs: Vec<(&str, &[u8])> = vec![("ghost", b"data")];
978        let results = c.verify_all(&pairs, IntegrityLevel::Quick);
979        assert_eq!(results.len(), 1);
980        assert!(!results[0].passed);
981        assert!(!results[0].errors.is_empty());
982    }
983
984    #[test]
985    fn test_verify_all_empty_input() {
986        let mut c = checker();
987        let results = c.verify_all(&[], IntegrityLevel::Full);
988        assert!(results.is_empty());
989    }
990
991    #[test]
992    fn test_verify_all_large_batch() {
993        let mut c = checker();
994        let data: Vec<Vec<u8>> = (0u64..20).map(|i| rng_bytes(i + 1, 64)).collect();
995        let ids: Vec<String> = (0..20).map(|i| format!("obj{i}")).collect();
996        for (id, d) in ids.iter().zip(data.iter()) {
997            c.register(id.clone(), d).unwrap();
998        }
999        let pairs: Vec<(&str, &[u8])> = ids
1000            .iter()
1001            .zip(data.iter())
1002            .map(|(id, d)| (id.as_str(), d.as_slice()))
1003            .collect();
1004        let results = c.verify_all(&pairs, IntegrityLevel::Standard);
1005        assert!(results.iter().all(|r| r.passed));
1006    }
1007
1008    // -----------------------------------------------------------------------
1009    // mark_corrupted
1010    // -----------------------------------------------------------------------
1011
1012    #[test]
1013    fn test_mark_corrupted_sets_status() {
1014        let mut c = checker();
1015        c.register("mc".to_string(), b"data").unwrap();
1016        c.mark_corrupted("mc", "disk error".to_string()).unwrap();
1017        let s = c.stats();
1018        // PossiblyCorrupted does NOT count as corrupted_objects (which tracks
1019        // hard Corrupted state), but let's verify the status was set.
1020        assert_eq!(s.corrupted_objects, 0);
1021        // Verify via objects_needing_verification — it hasn't been verified.
1022        let needing = c.objects_needing_verification(u64::MAX);
1023        assert_eq!(needing.len(), 1);
1024    }
1025
1026    #[test]
1027    fn test_mark_corrupted_not_found() {
1028        let mut c = checker();
1029        let err = c.mark_corrupted("ghost", "reason".to_string()).unwrap_err();
1030        assert!(matches!(err, CheckerError::ObjectNotFound(_)));
1031    }
1032
1033    #[test]
1034    fn test_mark_corrupted_reason_stored() {
1035        let mut c = checker();
1036        c.register("r".to_string(), b"data").unwrap();
1037        c.mark_corrupted("r", "sector read failure".to_string())
1038            .unwrap();
1039        // Verify all triggers the "possibly corrupted" path but the status
1040        // can be overwritten by a subsequent verify.
1041        let r = c.verify("r", b"data", IntegrityLevel::Full).unwrap();
1042        assert!(r.passed);
1043    }
1044
1045    // -----------------------------------------------------------------------
1046    // update
1047    // -----------------------------------------------------------------------
1048
1049    #[test]
1050    fn test_update_changes_hash() {
1051        let mut c = checker();
1052        c.register("upd".to_string(), b"v1").unwrap();
1053        let h2 = c.update("upd", b"v2").unwrap();
1054        assert_eq!(h2, IntegrityHash::compute(b"v2"));
1055    }
1056
1057    #[test]
1058    fn test_update_resets_verification_count() {
1059        let mut c = checker();
1060        c.register("upd".to_string(), b"v1").unwrap();
1061        c.verify("upd", b"v1", IntegrityLevel::Full).unwrap();
1062        c.update("upd", b"v2").unwrap();
1063        // After update the record's own counter resets; stats reflect the
1064        // cumulative total_verifications which still includes the prior call.
1065        let r = c.verify("upd", b"v2", IntegrityLevel::Full).unwrap();
1066        assert!(r.passed);
1067    }
1068
1069    #[test]
1070    fn test_update_not_found() {
1071        let mut c = checker();
1072        let err = c.update("ghost", b"data").unwrap_err();
1073        assert!(matches!(err, CheckerError::ObjectNotFound(_)));
1074    }
1075
1076    #[test]
1077    fn test_update_verify_old_data_fails() {
1078        let mut c = checker();
1079        c.register("u2".to_string(), b"old").unwrap();
1080        c.update("u2", b"new").unwrap();
1081        let r = c.verify("u2", b"old", IntegrityLevel::Full).unwrap();
1082        assert!(!r.passed);
1083    }
1084
1085    // -----------------------------------------------------------------------
1086    // remove
1087    // -----------------------------------------------------------------------
1088
1089    #[test]
1090    fn test_remove_decrements_count() {
1091        let mut c = checker();
1092        c.register("rm".to_string(), b"data").unwrap();
1093        assert_eq!(c.len(), 1);
1094        c.remove("rm").unwrap();
1095        assert_eq!(c.len(), 0);
1096    }
1097
1098    #[test]
1099    fn test_remove_not_found() {
1100        let mut c = checker();
1101        let err = c.remove("ghost").unwrap_err();
1102        assert!(matches!(err, CheckerError::ObjectNotFound(_)));
1103    }
1104
1105    #[test]
1106    fn test_remove_allows_reregistration() {
1107        let mut c = checker();
1108        c.register("x".to_string(), b"v1").unwrap();
1109        c.remove("x").unwrap();
1110        c.register("x".to_string(), b"v2").unwrap();
1111        let r = c.verify("x", b"v2", IntegrityLevel::Full).unwrap();
1112        assert!(r.passed);
1113    }
1114
1115    // -----------------------------------------------------------------------
1116    // corrupted_objects
1117    // -----------------------------------------------------------------------
1118
1119    #[test]
1120    fn test_corrupted_objects_empty() {
1121        let c = checker();
1122        assert!(c.corrupted_objects().is_empty());
1123    }
1124
1125    #[test]
1126    fn test_corrupted_objects_after_failed_verify() {
1127        let mut c = checker();
1128        c.register("co".to_string(), b"original").unwrap();
1129        c.verify("co", b"tampered", IntegrityLevel::Full).unwrap();
1130        assert_eq!(c.corrupted_objects().len(), 1);
1131        assert_eq!(c.corrupted_objects()[0].id, "co");
1132    }
1133
1134    #[test]
1135    fn test_corrupted_objects_multiple() {
1136        let mut c = checker();
1137        for i in 0..5 {
1138            c.register(format!("obj{i}"), b"data").unwrap();
1139        }
1140        for i in 0..3 {
1141            c.verify(&format!("obj{i}"), b"wrong", IntegrityLevel::Full)
1142                .unwrap();
1143        }
1144        assert_eq!(c.corrupted_objects().len(), 3);
1145    }
1146
1147    // -----------------------------------------------------------------------
1148    // objects_needing_verification
1149    // -----------------------------------------------------------------------
1150
1151    #[test]
1152    fn test_objects_needing_verification_all_new() {
1153        let mut c = checker();
1154        c.register("n1".to_string(), b"d").unwrap();
1155        c.register("n2".to_string(), b"d").unwrap();
1156        // since_us = MAX ⇒ all objects with last_verified_at < MAX qualify.
1157        let needing = c.objects_needing_verification(u64::MAX);
1158        assert_eq!(needing.len(), 2);
1159    }
1160
1161    #[test]
1162    fn test_objects_needing_verification_after_verify() {
1163        let mut c = checker();
1164        c.register("v1".to_string(), b"d").unwrap();
1165        c.verify("v1", b"d", IntegrityLevel::Full).unwrap();
1166        // since_us = 0 ⇒ only objects with last_verified_at < 0 (none) qualify.
1167        let needing = c.objects_needing_verification(0);
1168        assert_eq!(needing.len(), 0);
1169    }
1170
1171    #[test]
1172    fn test_objects_needing_verification_since_past() {
1173        let mut c = checker();
1174        c.register("past".to_string(), b"d").unwrap();
1175        // since_us = 1 ⇒ objects with last_verified_at == 0 are returned.
1176        let needing = c.objects_needing_verification(1);
1177        assert_eq!(needing.len(), 1);
1178    }
1179
1180    // -----------------------------------------------------------------------
1181    // repair_hash
1182    // -----------------------------------------------------------------------
1183
1184    #[test]
1185    fn test_repair_hash_clears_corruption() {
1186        let mut c = checker();
1187        c.register("rh".to_string(), b"original").unwrap();
1188        c.verify("rh", b"tampered", IntegrityLevel::Full).unwrap();
1189        assert_eq!(c.corrupted_objects().len(), 1);
1190        c.repair_hash("rh", b"correct").unwrap();
1191        assert_eq!(c.corrupted_objects().len(), 0);
1192    }
1193
1194    #[test]
1195    fn test_repair_hash_verify_with_correct_data() {
1196        let mut c = checker();
1197        c.register("rh2".to_string(), b"old").unwrap();
1198        c.repair_hash("rh2", b"new_correct").unwrap();
1199        let r = c
1200            .verify("rh2", b"new_correct", IntegrityLevel::Full)
1201            .unwrap();
1202        assert!(r.passed);
1203    }
1204
1205    #[test]
1206    fn test_repair_hash_not_found() {
1207        let mut c = checker();
1208        let err = c.repair_hash("ghost", b"data").unwrap_err();
1209        assert!(matches!(err, CheckerError::ObjectNotFound(_)));
1210    }
1211
1212    // -----------------------------------------------------------------------
1213    // stats
1214    // -----------------------------------------------------------------------
1215
1216    #[test]
1217    fn test_stats_empty() {
1218        let c = checker();
1219        let s = c.stats();
1220        assert_eq!(s.total_objects, 0);
1221        assert_eq!(s.verified_objects, 0);
1222        assert_eq!(s.corrupted_objects, 0);
1223        assert_eq!(s.corruption_rate, 0.0);
1224        assert_eq!(s.total_verifications, 0);
1225        assert_eq!(s.avg_verification_us, 0.0);
1226    }
1227
1228    #[test]
1229    fn test_stats_after_registration() {
1230        let mut c = checker();
1231        c.register("s1".to_string(), b"a").unwrap();
1232        c.register("s2".to_string(), b"b").unwrap();
1233        let s = c.stats();
1234        assert_eq!(s.total_objects, 2);
1235        assert_eq!(s.verified_objects, 0);
1236    }
1237
1238    #[test]
1239    fn test_stats_corruption_rate() {
1240        let mut c = checker();
1241        c.register("s1".to_string(), b"a").unwrap();
1242        c.register("s2".to_string(), b"b").unwrap();
1243        c.verify("s1", b"tampered", IntegrityLevel::Full).unwrap();
1244        let s = c.stats();
1245        assert_eq!(s.corrupted_objects, 1);
1246        assert!((s.corruption_rate - 0.5).abs() < 1e-9);
1247    }
1248
1249    #[test]
1250    fn test_stats_avg_verification_us_non_negative() {
1251        let mut c = checker();
1252        c.register("t1".to_string(), b"data").unwrap();
1253        c.verify("t1", b"data", IntegrityLevel::Full).unwrap();
1254        let s = c.stats();
1255        assert!(s.avg_verification_us >= 0.0);
1256    }
1257
1258    #[test]
1259    fn test_stats_total_verifications() {
1260        let mut c = checker();
1261        c.register("tv".to_string(), b"data").unwrap();
1262        for _ in 0..5 {
1263            c.verify("tv", b"data", IntegrityLevel::Quick).unwrap();
1264        }
1265        let s = c.stats();
1266        assert_eq!(s.total_verifications, 5);
1267    }
1268
1269    // -----------------------------------------------------------------------
1270    // Error path coverage
1271    // -----------------------------------------------------------------------
1272
1273    #[test]
1274    fn test_error_display_not_found() {
1275        let e = CheckerError::ObjectNotFound("xyz".to_string());
1276        let msg = e.to_string();
1277        assert!(msg.contains("xyz"));
1278    }
1279
1280    #[test]
1281    fn test_error_display_max_exceeded() {
1282        let e = CheckerError::MaxObjectsExceeded;
1283        let msg = e.to_string();
1284        assert!(msg.contains("maximum"));
1285    }
1286
1287    #[test]
1288    fn test_error_display_invalid_level() {
1289        let e = CheckerError::InvalidLevel("bad algo".to_string());
1290        let msg = e.to_string();
1291        assert!(msg.contains("invalid"));
1292    }
1293
1294    // -----------------------------------------------------------------------
1295    // Edge-case / property-style tests
1296    // -----------------------------------------------------------------------
1297
1298    #[test]
1299    fn test_large_data_verify() {
1300        let mut c = checker();
1301        let data = rng_bytes(42, 1_024 * 1_024);
1302        c.register("large".to_string(), &data).unwrap();
1303        let r = c.verify("large", &data, IntegrityLevel::Full).unwrap();
1304        assert!(r.passed);
1305    }
1306
1307    #[test]
1308    fn test_many_objects_no_collision() {
1309        let mut c = checker();
1310        for i in 0u64..100 {
1311            let d = rng_bytes(i, 32);
1312            c.register(format!("obj{i}"), &d).unwrap();
1313        }
1314        let s = c.stats();
1315        assert_eq!(s.total_objects, 100);
1316    }
1317
1318    #[test]
1319    fn test_verify_returns_level_in_result() {
1320        let mut c = checker();
1321        c.register("lvl".to_string(), b"d").unwrap();
1322        let r = c.verify("lvl", b"d", IntegrityLevel::Standard).unwrap();
1323        assert_eq!(r.level, IntegrityLevel::Standard);
1324    }
1325
1326    #[test]
1327    fn test_repair_then_verify_large() {
1328        let mut c = checker();
1329        let original = rng_bytes(10, 512);
1330        let corrupted = rng_bytes(99, 512);
1331        let correct = rng_bytes(7, 512);
1332        c.register("repair_large".to_string(), &original).unwrap();
1333        c.verify("repair_large", &corrupted, IntegrityLevel::Full)
1334            .unwrap();
1335        c.repair_hash("repair_large", &correct).unwrap();
1336        let r = c
1337            .verify("repair_large", &correct, IntegrityLevel::Full)
1338            .unwrap();
1339        assert!(r.passed);
1340    }
1341
1342    #[test]
1343    fn test_is_empty() {
1344        let mut c = checker();
1345        assert!(c.is_empty());
1346        c.register("x".to_string(), b"d").unwrap();
1347        assert!(!c.is_empty());
1348        c.remove("x").unwrap();
1349        assert!(c.is_empty());
1350    }
1351
1352    #[test]
1353    fn test_config_accessor() {
1354        let c = checker();
1355        assert_eq!(c.config().max_objects, 1_000_000);
1356    }
1357
1358    #[test]
1359    fn test_verify_duration_recorded() {
1360        let mut c = checker();
1361        c.register("dur".to_string(), b"data").unwrap();
1362        let r = c.verify("dur", b"data", IntegrityLevel::Full).unwrap();
1363        // duration_us is non-negative; may be 0 on very fast systems.
1364        assert!(r.duration_us < u64::MAX);
1365    }
1366
1367    #[test]
1368    fn test_custom_empty_algo_list() {
1369        let mut c = checker();
1370        c.register("emp".to_string(), b"data").unwrap();
1371        // An empty custom list checks only size.
1372        let lvl = IntegrityLevel::Custom(vec![]);
1373        let r = c.verify("emp", b"data", lvl).unwrap();
1374        assert!(r.passed);
1375    }
1376
1377    #[test]
1378    fn test_size_mismatch_always_detected() {
1379        let mut c = checker();
1380        c.register("sz".to_string(), b"hello").unwrap();
1381        // Even Quick level reports size mismatch.
1382        let r = c.verify("sz", b"hi", IntegrityLevel::Quick).unwrap();
1383        assert!(!r.passed);
1384        let has_size_error = r.errors.iter().any(|e| e.contains("size"));
1385        assert!(has_size_error);
1386    }
1387
1388    #[test]
1389    fn test_verify_object_id_in_result() {
1390        let mut c = checker();
1391        c.register("myid".to_string(), b"data").unwrap();
1392        let r = c.verify("myid", b"data", IntegrityLevel::Quick).unwrap();
1393        assert_eq!(r.object_id, "myid");
1394    }
1395}