Skip to main content

ipfrs_storage/
data_integrity_auditor.rs

1//! Data Integrity Auditor — proactive continuous verification of stored blocks.
2//!
3//! Computes and re-computes checksums with multiple pure-Rust algorithms (CRC-32,
4//! Adler-32, FNV-XOR-64, MultiCheck), detects silent corruption, and maintains a
5//! bounded repair history.
6
7use std::collections::{HashMap, VecDeque};
8
9// ---------------------------------------------------------------------------
10// Checksum algorithm selection
11// ---------------------------------------------------------------------------
12
13/// Algorithm used when computing a block checksum.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum ChecksumAlgo {
16    /// CRC-32 with polynomial 0xEDB88320 (Castagnoli / IEEE 802.3).
17    Crc32,
18    /// Adler-32 (RFC 1950).
19    Adler32,
20    /// FNV-1a 64-bit hash, result folded to 32 bits via XOR of the two halves.
21    FnvXor64,
22    /// CRC-32 XOR Adler-32 XOR FnvXor64 — paranoia mode.
23    MultiCheck,
24}
25
26// ---------------------------------------------------------------------------
27// Pure-Rust algorithm implementations
28// ---------------------------------------------------------------------------
29
30/// Pre-computed CRC-32 look-up table (polynomial 0xEDB88320).
31const fn build_crc32_table() -> [u32; 256] {
32    let mut table = [0u32; 256];
33    let mut i = 0usize;
34    while i < 256 {
35        let mut crc = i as u32;
36        let mut j = 0usize;
37        while j < 8 {
38            if crc & 1 != 0 {
39                crc = (crc >> 1) ^ 0xEDB8_8320;
40            } else {
41                crc >>= 1;
42            }
43            j += 1;
44        }
45        table[i] = crc;
46        i += 1;
47    }
48    table
49}
50
51static CRC32_TABLE: [u32; 256] = build_crc32_table();
52
53/// Compute CRC-32 (Castagnoli variant, polynomial 0xEDB88320).
54pub fn compute_crc32(data: &[u8]) -> u32 {
55    let mut crc: u32 = 0xFFFF_FFFF;
56    for &byte in data {
57        let idx = ((crc ^ u32::from(byte)) & 0xFF) as usize;
58        crc = (crc >> 8) ^ CRC32_TABLE[idx];
59    }
60    crc ^ 0xFFFF_FFFF
61}
62
63/// Adler-32 modulus.
64const ADLER32_MOD: u32 = 65521;
65
66/// Compute Adler-32.
67///
68/// A = 1 + Σ bytes\[i\]  (mod 65521)
69/// B = Σ A_i           (mod 65521)
70/// result = (B << 16) | A
71pub fn compute_adler32(data: &[u8]) -> u32 {
72    let mut a: u32 = 1;
73    let mut b: u32 = 0;
74    for &byte in data {
75        a = (a + u32::from(byte)) % ADLER32_MOD;
76        b = (b + a) % ADLER32_MOD;
77    }
78    (b << 16) | a
79}
80
81/// FNV-1a 64-bit offset basis and prime.
82const FNV1A_64_OFFSET: u64 = 14_695_981_039_346_656_037;
83const FNV1A_64_PRIME: u64 = 1_099_511_628_211;
84
85/// Compute FNV-1a 64-bit hash, then fold to 32 bits via high32 XOR low32.
86pub fn compute_fnv_xor64(data: &[u8]) -> u32 {
87    let mut hash = FNV1A_64_OFFSET;
88    for &byte in data {
89        hash ^= u64::from(byte);
90        hash = hash.wrapping_mul(FNV1A_64_PRIME);
91    }
92    let high = (hash >> 32) as u32;
93    let low = (hash & 0xFFFF_FFFF) as u32;
94    high ^ low
95}
96
97// ---------------------------------------------------------------------------
98// Dispatcher
99// ---------------------------------------------------------------------------
100
101/// Dispatch checksum computation to the correct algorithm.
102pub fn compute_checksum(data: &[u8], algo: &ChecksumAlgo) -> u32 {
103    match algo {
104        ChecksumAlgo::Crc32 => compute_crc32(data),
105        ChecksumAlgo::Adler32 => compute_adler32(data),
106        ChecksumAlgo::FnvXor64 => compute_fnv_xor64(data),
107        ChecksumAlgo::MultiCheck => {
108            compute_crc32(data) ^ compute_adler32(data) ^ compute_fnv_xor64(data)
109        }
110    }
111}
112
113// ---------------------------------------------------------------------------
114// Domain types
115// ---------------------------------------------------------------------------
116
117/// Stored checksum record for a content block.
118#[derive(Debug, Clone)]
119pub struct BlockChecksum {
120    /// Content identifier of the block.
121    pub cid: String,
122    /// Algorithm used to produce the checksum.
123    pub algo: ChecksumAlgo,
124    /// The checksum value.
125    pub checksum: u32,
126    /// Unix timestamp (seconds) when the checksum was recorded.
127    pub computed_at: u64,
128    /// Size of the block in bytes at registration time.
129    pub block_size: usize,
130}
131
132/// Result of auditing a single block.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum AuditResult {
135    /// Block data matches the stored checksum.
136    Passed {
137        /// Content identifier.
138        cid: String,
139        /// Verified checksum.
140        checksum: u32,
141    },
142    /// Computed checksum differs from the stored one — corruption detected.
143    Failed {
144        /// Content identifier.
145        cid: String,
146        /// Checksum recorded at registration time.
147        stored_checksum: u32,
148        /// Checksum freshly computed from the supplied data.
149        computed_checksum: u32,
150    },
151    /// No registered checksum for this CID — block was never registered.
152    Missing {
153        /// Content identifier.
154        cid: String,
155    },
156}
157
158/// Repair history entry.
159#[derive(Debug, Clone)]
160pub struct RepairRecord {
161    /// Content identifier of the corrupted block.
162    pub cid: String,
163    /// Unix timestamp when corruption was detected.
164    pub detected_at: u64,
165    /// Unix timestamp when the repair was completed, `None` if still pending.
166    pub repaired_at: Option<u64>,
167    /// Name or URL of the repair source (e.g., peer ID, backup path).
168    pub source: String,
169    /// Whether the repair attempt succeeded.
170    pub success: bool,
171}
172
173/// Configuration for a `DataIntegrityAuditor`.
174#[derive(Debug, Clone)]
175pub struct AuditConfig {
176    /// Algorithm to use for checksum computation.
177    pub algo: ChecksumAlgo,
178    /// Maximum blocks to audit per `audit_batch` invocation.
179    pub audit_batch_size: usize,
180    /// Maximum number of repair records retained.
181    pub max_repair_history: usize,
182    /// When `true`, `audit_block` automatically schedules a repair on failure.
183    pub auto_repair: bool,
184}
185
186impl Default for AuditConfig {
187    fn default() -> Self {
188        Self {
189            algo: ChecksumAlgo::MultiCheck,
190            audit_batch_size: 100,
191            max_repair_history: 1000,
192            auto_repair: false,
193        }
194    }
195}
196
197/// Snapshot statistics exposed by the auditor.
198#[derive(Debug, Clone)]
199pub struct AuditorStats {
200    /// Number of blocks currently registered.
201    pub registered_blocks: usize,
202    /// Cumulative number of audit checks performed.
203    pub total_audited: u64,
204    /// Cumulative number of checks that passed.
205    pub total_passed: u64,
206    /// Cumulative number of checks that failed.
207    pub total_failed: u64,
208    /// `total_passed / total_audited` (0.0 when nothing has been audited yet).
209    pub integrity_rate: f64,
210    /// Number of repair records with `repaired_at.is_none()`.
211    pub pending_repairs: usize,
212}
213
214// ---------------------------------------------------------------------------
215// DataIntegrityAuditor
216// ---------------------------------------------------------------------------
217
218/// Proactive data integrity auditing system.
219///
220/// Continuously verifies stored blocks using configurable checksum algorithms,
221/// detects silent corruption, and tracks repair history.
222#[derive(Debug)]
223pub struct DataIntegrityAuditor {
224    /// Configuration.
225    pub config: AuditConfig,
226    /// Map from CID → stored checksum record.
227    pub checksums: HashMap<String, BlockChecksum>,
228    /// Bounded ring of repair records (newest at the back).
229    pub repair_history: VecDeque<RepairRecord>,
230    /// Total audit operations performed.
231    pub total_audited: u64,
232    /// Total audits that produced a `Passed` result.
233    pub total_passed: u64,
234    /// Total audits that produced a `Failed` result.
235    pub total_failed: u64,
236}
237
238impl DataIntegrityAuditor {
239    // -----------------------------------------------------------------------
240    // Construction
241    // -----------------------------------------------------------------------
242
243    /// Create a new auditor with the supplied configuration.
244    pub fn new(config: AuditConfig) -> Self {
245        Self {
246            config,
247            checksums: HashMap::new(),
248            repair_history: VecDeque::new(),
249            total_audited: 0,
250            total_passed: 0,
251            total_failed: 0,
252        }
253    }
254
255    // -----------------------------------------------------------------------
256    // Checksum helpers (forwarding to free functions for testability)
257    // -----------------------------------------------------------------------
258
259    /// Compute a checksum over `data` using `algo`.
260    pub fn compute_checksum(data: &[u8], algo: &ChecksumAlgo) -> u32 {
261        compute_checksum(data, algo)
262    }
263
264    /// CRC-32 (Castagnoli, polynomial 0xEDB88320).
265    pub fn compute_crc32(data: &[u8]) -> u32 {
266        compute_crc32(data)
267    }
268
269    /// Adler-32.
270    pub fn compute_adler32(data: &[u8]) -> u32 {
271        compute_adler32(data)
272    }
273
274    /// FNV-1a 64-bit → 32-bit via XOR folding.
275    pub fn compute_fnv_xor64(data: &[u8]) -> u32 {
276        compute_fnv_xor64(data)
277    }
278
279    // -----------------------------------------------------------------------
280    // Registration
281    // -----------------------------------------------------------------------
282
283    /// Compute the block's checksum and store it, overwriting any prior record.
284    ///
285    /// # Parameters
286    /// - `cid`  — content identifier.
287    /// - `data` — raw block bytes.
288    /// - `now`  — current Unix timestamp (seconds).
289    pub fn register_block(&mut self, cid: String, data: &[u8], now: u64) {
290        let checksum = compute_checksum(data, &self.config.algo);
291        let record = BlockChecksum {
292            cid: cid.clone(),
293            algo: self.config.algo,
294            checksum,
295            computed_at: now,
296            block_size: data.len(),
297        };
298        self.checksums.insert(cid, record);
299    }
300
301    // -----------------------------------------------------------------------
302    // Audit
303    // -----------------------------------------------------------------------
304
305    /// Audit a single block: recompute its checksum and compare with the stored value.
306    ///
307    /// Updates `total_audited`, `total_passed`, and `total_failed`.
308    /// If `config.auto_repair` is `true` and the check fails, a repair is
309    /// automatically scheduled (with `source = "auto"` and `detected_at = 0`).
310    pub fn audit_block(&mut self, cid: &str, data: &[u8]) -> AuditResult {
311        self.total_audited += 1;
312
313        let stored = match self.checksums.get(cid) {
314            Some(s) => s.clone(),
315            None => {
316                return AuditResult::Missing {
317                    cid: cid.to_owned(),
318                };
319            }
320        };
321
322        let computed = compute_checksum(data, &stored.algo);
323
324        if computed == stored.checksum {
325            self.total_passed += 1;
326            AuditResult::Passed {
327                cid: cid.to_owned(),
328                checksum: computed,
329            }
330        } else {
331            self.total_failed += 1;
332            if self.config.auto_repair {
333                self.schedule_repair(cid.to_owned(), 0, "auto".to_owned());
334            }
335            AuditResult::Failed {
336                cid: cid.to_owned(),
337                stored_checksum: stored.checksum,
338                computed_checksum: computed,
339            }
340        }
341    }
342
343    /// Audit a batch of blocks, respecting `config.audit_batch_size`.
344    ///
345    /// Processes at most `audit_batch_size` entries and returns one
346    /// `AuditResult` per processed block.
347    pub fn audit_batch(&mut self, blocks: &[(String, Vec<u8>)]) -> Vec<AuditResult> {
348        let limit = self.config.audit_batch_size.min(blocks.len());
349        let mut results = Vec::with_capacity(limit);
350        for (cid, data) in &blocks[..limit] {
351            let result = self.audit_block(cid, data);
352            results.push(result);
353        }
354        results
355    }
356
357    // -----------------------------------------------------------------------
358    // Repair tracking
359    // -----------------------------------------------------------------------
360
361    /// Record a repair request for `cid`.
362    ///
363    /// Pushes a new `RepairRecord` with `repaired_at = None` to the back of
364    /// the history ring.  If the ring exceeds `max_repair_history` the oldest
365    /// entry is evicted from the front.
366    pub fn schedule_repair(&mut self, cid: String, now: u64, source: String) {
367        let record = RepairRecord {
368            cid,
369            detected_at: now,
370            repaired_at: None,
371            source,
372            success: false,
373        };
374        self.repair_history.push_back(record);
375        // Evict oldest entries to stay within the configured limit.
376        while self.repair_history.len() > self.config.max_repair_history {
377            self.repair_history.pop_front();
378        }
379    }
380
381    /// Mark the most-recently scheduled (still-pending) repair for `cid` as
382    /// completed.
383    ///
384    /// Scans the history from newest to oldest.  Returns `true` if a pending
385    /// record was found and updated, `false` otherwise.
386    pub fn mark_repaired(&mut self, cid: &str, now: u64) -> bool {
387        // Iterate from back (newest) to find the latest pending record.
388        for record in self.repair_history.iter_mut().rev() {
389            if record.cid == cid && record.repaired_at.is_none() {
390                record.repaired_at = Some(now);
391                record.success = true;
392                return true;
393            }
394        }
395        false
396    }
397
398    // -----------------------------------------------------------------------
399    // Metrics
400    // -----------------------------------------------------------------------
401
402    /// Ratio of passed audits to total audits.  Returns `0.0` when nothing
403    /// has been audited yet.
404    pub fn integrity_rate(&self) -> f64 {
405        let denom = self.total_audited.max(1) as f64;
406        self.total_passed as f64 / denom
407    }
408
409    /// All repair records for which `repaired_at.is_none()`.
410    pub fn pending_repairs(&self) -> Vec<&RepairRecord> {
411        self.repair_history
412            .iter()
413            .filter(|r| r.repaired_at.is_none())
414            .collect()
415    }
416
417    /// Snapshot of the current auditor statistics.
418    pub fn auditor_stats(&self) -> AuditorStats {
419        AuditorStats {
420            registered_blocks: self.checksums.len(),
421            total_audited: self.total_audited,
422            total_passed: self.total_passed,
423            total_failed: self.total_failed,
424            integrity_rate: self.integrity_rate(),
425            pending_repairs: self.pending_repairs().len(),
426        }
427    }
428}
429
430// ---------------------------------------------------------------------------
431// Tests
432// ---------------------------------------------------------------------------
433
434#[cfg(test)]
435mod tests {
436    use crate::data_integrity_auditor::{
437        compute_adler32, compute_checksum, compute_crc32, compute_fnv_xor64, AuditConfig,
438        AuditResult, ChecksumAlgo, DataIntegrityAuditor,
439    };
440
441    // -----------------------------------------------------------------------
442    // Helpers
443    // -----------------------------------------------------------------------
444
445    fn default_auditor() -> DataIntegrityAuditor {
446        DataIntegrityAuditor::new(AuditConfig::default())
447    }
448
449    fn auditor_with_algo(algo: ChecksumAlgo) -> DataIntegrityAuditor {
450        DataIntegrityAuditor::new(AuditConfig {
451            algo,
452            ..AuditConfig::default()
453        })
454    }
455
456    // -----------------------------------------------------------------------
457    // CRC-32
458    // -----------------------------------------------------------------------
459
460    #[test]
461    fn crc32_empty() {
462        // CRC-32 of empty slice is well-known: 0x00000000
463        assert_eq!(compute_crc32(&[]), 0x0000_0000);
464    }
465
466    #[test]
467    fn crc32_single_byte() {
468        // Sanity: single byte must produce non-trivial output.
469        let v = compute_crc32(&[0x61]); // b'a'
470        assert_ne!(v, 0);
471    }
472
473    #[test]
474    fn crc32_known_vector() {
475        // CRC-32/ISO-HDLC ("123456789") = 0xCBF43926
476        let data = b"123456789";
477        assert_eq!(compute_crc32(data), 0xCBF4_3926);
478    }
479
480    #[test]
481    fn crc32_deterministic() {
482        let data = b"hello world";
483        assert_eq!(compute_crc32(data), compute_crc32(data));
484    }
485
486    #[test]
487    fn crc32_different_inputs_differ() {
488        assert_ne!(compute_crc32(b"abc"), compute_crc32(b"abd"));
489    }
490
491    // -----------------------------------------------------------------------
492    // Adler-32
493    // -----------------------------------------------------------------------
494
495    #[test]
496    fn adler32_empty() {
497        // Empty input: A=1, B=0  →  0x00000001
498        assert_eq!(compute_adler32(&[]), 1);
499    }
500
501    #[test]
502    fn adler32_known_vector() {
503        // Adler-32("Wikipedia") = 0x11E60398
504        let data = b"Wikipedia";
505        assert_eq!(compute_adler32(data), 0x11E6_0398);
506    }
507
508    #[test]
509    fn adler32_deterministic() {
510        let data = b"data integrity";
511        assert_eq!(compute_adler32(data), compute_adler32(data));
512    }
513
514    #[test]
515    fn adler32_different_inputs_differ() {
516        assert_ne!(compute_adler32(b"foo"), compute_adler32(b"bar"));
517    }
518
519    // -----------------------------------------------------------------------
520    // FNV-XOR-64
521    // -----------------------------------------------------------------------
522
523    #[test]
524    fn fnv_xor64_empty() {
525        // FNV-1a of empty input equals the offset basis.
526        // 14695981039346656037 → high = 0xCBF29CE4, low = 0x84222325
527        // XOR  → 0x4FD0BFC1
528        let v = compute_fnv_xor64(&[]);
529        assert_eq!(v, 0x4FD0_BFC1);
530    }
531
532    #[test]
533    fn fnv_xor64_deterministic() {
534        let data = b"consistency check";
535        assert_eq!(compute_fnv_xor64(data), compute_fnv_xor64(data));
536    }
537
538    #[test]
539    fn fnv_xor64_different_inputs_differ() {
540        assert_ne!(compute_fnv_xor64(b"alpha"), compute_fnv_xor64(b"beta"));
541    }
542
543    // -----------------------------------------------------------------------
544    // MultiCheck
545    // -----------------------------------------------------------------------
546
547    #[test]
548    fn multicheck_combines_three_algos() {
549        let data = b"multi";
550        let expected = compute_crc32(data) ^ compute_adler32(data) ^ compute_fnv_xor64(data);
551        let got = compute_checksum(data, &ChecksumAlgo::MultiCheck);
552        assert_eq!(got, expected);
553    }
554
555    #[test]
556    fn multicheck_deterministic() {
557        let data = b"reproducible";
558        assert_eq!(
559            compute_checksum(data, &ChecksumAlgo::MultiCheck),
560            compute_checksum(data, &ChecksumAlgo::MultiCheck)
561        );
562    }
563
564    // -----------------------------------------------------------------------
565    // Checksum dispatcher
566    // -----------------------------------------------------------------------
567
568    #[test]
569    fn dispatch_crc32() {
570        let data = b"dispatch crc32";
571        assert_eq!(
572            compute_checksum(data, &ChecksumAlgo::Crc32),
573            compute_crc32(data)
574        );
575    }
576
577    #[test]
578    fn dispatch_adler32() {
579        let data = b"dispatch adler32";
580        assert_eq!(
581            compute_checksum(data, &ChecksumAlgo::Adler32),
582            compute_adler32(data)
583        );
584    }
585
586    #[test]
587    fn dispatch_fnvxor64() {
588        let data = b"dispatch fnv";
589        assert_eq!(
590            compute_checksum(data, &ChecksumAlgo::FnvXor64),
591            compute_fnv_xor64(data)
592        );
593    }
594
595    // -----------------------------------------------------------------------
596    // AuditConfig defaults
597    // -----------------------------------------------------------------------
598
599    #[test]
600    fn default_config_values() {
601        let cfg = AuditConfig::default();
602        assert_eq!(cfg.algo, ChecksumAlgo::MultiCheck);
603        assert_eq!(cfg.audit_batch_size, 100);
604        assert_eq!(cfg.max_repair_history, 1000);
605        assert!(!cfg.auto_repair);
606    }
607
608    // -----------------------------------------------------------------------
609    // register_block / audit_block
610    // -----------------------------------------------------------------------
611
612    #[test]
613    fn register_and_audit_passes() {
614        let mut aud = default_auditor();
615        aud.register_block("cid1".to_owned(), b"hello", 1000);
616        let result = aud.audit_block("cid1", b"hello");
617        assert!(matches!(result, AuditResult::Passed { .. }));
618    }
619
620    #[test]
621    fn audit_detects_corruption() {
622        let mut aud = default_auditor();
623        aud.register_block("cid2".to_owned(), b"original", 1000);
624        let result = aud.audit_block("cid2", b"corrupted");
625        assert!(matches!(result, AuditResult::Failed { .. }));
626    }
627
628    #[test]
629    fn audit_missing_returns_missing() {
630        let mut aud = default_auditor();
631        let result = aud.audit_block("nonexistent", b"data");
632        assert!(matches!(result, AuditResult::Missing { .. }));
633    }
634
635    #[test]
636    fn register_block_overwrites_previous() {
637        let mut aud = default_auditor();
638        aud.register_block("cid3".to_owned(), b"v1", 1000);
639        aud.register_block("cid3".to_owned(), b"v2", 2000);
640        // Should pass with v2 data, fail with v1 data.
641        assert!(matches!(
642            aud.audit_block("cid3", b"v2"),
643            AuditResult::Passed { .. }
644        ));
645        assert!(matches!(
646            aud.audit_block("cid3", b"v1"),
647            AuditResult::Failed { .. }
648        ));
649    }
650
651    #[test]
652    fn audit_increments_counters() {
653        let mut aud = default_auditor();
654        aud.register_block("cid4".to_owned(), b"data", 0);
655        aud.audit_block("cid4", b"data");
656        aud.audit_block("cid4", b"bad");
657        assert_eq!(aud.total_audited, 2);
658        assert_eq!(aud.total_passed, 1);
659        assert_eq!(aud.total_failed, 1);
660    }
661
662    #[test]
663    fn missing_audit_increments_total_only() {
664        let mut aud = default_auditor();
665        aud.audit_block("ghost", b"x");
666        assert_eq!(aud.total_audited, 1);
667        assert_eq!(aud.total_passed, 0);
668        assert_eq!(aud.total_failed, 0);
669    }
670
671    // -----------------------------------------------------------------------
672    // audit_batch
673    // -----------------------------------------------------------------------
674
675    #[test]
676    fn audit_batch_respects_batch_size() {
677        let mut aud = DataIntegrityAuditor::new(AuditConfig {
678            audit_batch_size: 3,
679            ..AuditConfig::default()
680        });
681        for i in 0u8..10 {
682            let cid = format!("cid{i}");
683            aud.register_block(cid, &[i], 0);
684        }
685        let blocks: Vec<(String, Vec<u8>)> =
686            (0u8..10).map(|i| (format!("cid{i}"), vec![i])).collect();
687        let results = aud.audit_batch(&blocks);
688        assert_eq!(results.len(), 3);
689    }
690
691    #[test]
692    fn audit_batch_all_pass() {
693        let mut aud = default_auditor();
694        for i in 0u8..5 {
695            aud.register_block(format!("b{i}"), &[i, i + 1], 0);
696        }
697        let blocks: Vec<(String, Vec<u8>)> = (0u8..5)
698            .map(|i| (format!("b{i}"), vec![i, i + 1]))
699            .collect();
700        let results = aud.audit_batch(&blocks);
701        assert!(results
702            .iter()
703            .all(|r| matches!(r, AuditResult::Passed { .. })));
704    }
705
706    #[test]
707    fn audit_batch_empty_input() {
708        let mut aud = default_auditor();
709        let results = aud.audit_batch(&[]);
710        assert!(results.is_empty());
711    }
712
713    // -----------------------------------------------------------------------
714    // schedule_repair / mark_repaired / pending_repairs
715    // -----------------------------------------------------------------------
716
717    #[test]
718    fn schedule_repair_adds_record() {
719        let mut aud = default_auditor();
720        aud.schedule_repair("cid5".to_owned(), 100, "peer-A".to_owned());
721        assert_eq!(aud.pending_repairs().len(), 1);
722    }
723
724    #[test]
725    fn mark_repaired_succeeds() {
726        let mut aud = default_auditor();
727        aud.schedule_repair("cid6".to_owned(), 100, "peer-B".to_owned());
728        let ok = aud.mark_repaired("cid6", 200);
729        assert!(ok);
730        assert_eq!(aud.pending_repairs().len(), 0);
731    }
732
733    #[test]
734    fn mark_repaired_sets_timestamp() {
735        let mut aud = default_auditor();
736        aud.schedule_repair("cid7".to_owned(), 50, "src".to_owned());
737        aud.mark_repaired("cid7", 999);
738        let record = aud
739            .repair_history
740            .iter()
741            .find(|r| r.cid == "cid7")
742            .expect("record must exist");
743        assert_eq!(record.repaired_at, Some(999));
744        assert!(record.success);
745    }
746
747    #[test]
748    fn mark_repaired_unknown_cid_returns_false() {
749        let mut aud = default_auditor();
750        assert!(!aud.mark_repaired("unknown", 0));
751    }
752
753    #[test]
754    fn pending_repairs_filters_completed() {
755        let mut aud = default_auditor();
756        aud.schedule_repair("c1".to_owned(), 1, "s1".to_owned());
757        aud.schedule_repair("c2".to_owned(), 2, "s2".to_owned());
758        aud.mark_repaired("c1", 10);
759        let pending = aud.pending_repairs();
760        assert_eq!(pending.len(), 1);
761        assert_eq!(pending[0].cid, "c2");
762    }
763
764    #[test]
765    fn repair_history_evicts_oldest_when_full() {
766        let mut aud = DataIntegrityAuditor::new(AuditConfig {
767            max_repair_history: 3,
768            ..AuditConfig::default()
769        });
770        for i in 0u64..5 {
771            aud.schedule_repair(format!("c{i}"), i, "src".to_owned());
772        }
773        // Only the 3 most recent should remain.
774        assert_eq!(aud.repair_history.len(), 3);
775        // Oldest (c0, c1) must be evicted.
776        let cids: Vec<&str> = aud.repair_history.iter().map(|r| r.cid.as_str()).collect();
777        assert!(!cids.contains(&"c0"));
778        assert!(!cids.contains(&"c1"));
779        assert!(cids.contains(&"c4"));
780    }
781
782    #[test]
783    fn mark_repaired_picks_most_recent_pending() {
784        let mut aud = default_auditor();
785        // Two records for same CID.
786        aud.schedule_repair("dup".to_owned(), 1, "s1".to_owned());
787        aud.schedule_repair("dup".to_owned(), 2, "s2".to_owned());
788        // Mark repaired: should affect the second (newest) one.
789        aud.mark_repaired("dup", 99);
790        let pending = aud.pending_repairs();
791        assert_eq!(pending.len(), 1);
792        // The first (older) record should still be pending.
793        assert_eq!(pending[0].detected_at, 1);
794    }
795
796    // -----------------------------------------------------------------------
797    // integrity_rate
798    // -----------------------------------------------------------------------
799
800    #[test]
801    fn integrity_rate_zero_when_nothing_audited() {
802        let aud = default_auditor();
803        // total_audited == 0 → denominator clamped to 1 → rate = 0/1 = 0.0
804        assert_eq!(aud.integrity_rate(), 0.0);
805    }
806
807    #[test]
808    fn integrity_rate_all_pass() {
809        let mut aud = default_auditor();
810        aud.register_block("cx".to_owned(), b"x", 0);
811        aud.audit_block("cx", b"x");
812        assert!((aud.integrity_rate() - 1.0).abs() < f64::EPSILON);
813    }
814
815    #[test]
816    fn integrity_rate_mixed() {
817        let mut aud = default_auditor();
818        aud.register_block("cy".to_owned(), b"y", 0);
819        aud.audit_block("cy", b"y"); // pass
820        aud.audit_block("cy", b"z"); // fail
821        let rate = aud.integrity_rate();
822        assert!((rate - 0.5).abs() < f64::EPSILON);
823    }
824
825    // -----------------------------------------------------------------------
826    // auditor_stats
827    // -----------------------------------------------------------------------
828
829    #[test]
830    fn auditor_stats_reflects_state() {
831        let mut aud = default_auditor();
832        aud.register_block("s1".to_owned(), b"data", 0);
833        aud.register_block("s2".to_owned(), b"more", 0);
834        aud.audit_block("s1", b"data");
835        aud.schedule_repair("s1".to_owned(), 0, "peer".to_owned());
836
837        let stats = aud.auditor_stats();
838        assert_eq!(stats.registered_blocks, 2);
839        assert_eq!(stats.total_audited, 1);
840        assert_eq!(stats.total_passed, 1);
841        assert_eq!(stats.total_failed, 0);
842        assert!((stats.integrity_rate - 1.0).abs() < f64::EPSILON);
843        assert_eq!(stats.pending_repairs, 1);
844    }
845
846    // -----------------------------------------------------------------------
847    // auto_repair
848    // -----------------------------------------------------------------------
849
850    #[test]
851    fn auto_repair_schedules_on_failure() {
852        let mut aud = DataIntegrityAuditor::new(AuditConfig {
853            auto_repair: true,
854            ..AuditConfig::default()
855        });
856        aud.register_block("ar1".to_owned(), b"good", 0);
857        aud.audit_block("ar1", b"bad");
858        assert_eq!(aud.pending_repairs().len(), 1);
859    }
860
861    #[test]
862    fn auto_repair_does_not_schedule_on_pass() {
863        let mut aud = DataIntegrityAuditor::new(AuditConfig {
864            auto_repair: true,
865            ..AuditConfig::default()
866        });
867        aud.register_block("ar2".to_owned(), b"ok", 0);
868        aud.audit_block("ar2", b"ok");
869        assert_eq!(aud.pending_repairs().len(), 0);
870    }
871
872    // -----------------------------------------------------------------------
873    // Per-algorithm registration
874    // -----------------------------------------------------------------------
875
876    #[test]
877    fn crc32_algo_round_trip() {
878        let mut aud = auditor_with_algo(ChecksumAlgo::Crc32);
879        aud.register_block("rc".to_owned(), b"crc check", 0);
880        assert!(matches!(
881            aud.audit_block("rc", b"crc check"),
882            AuditResult::Passed { .. }
883        ));
884    }
885
886    #[test]
887    fn adler32_algo_round_trip() {
888        let mut aud = auditor_with_algo(ChecksumAlgo::Adler32);
889        aud.register_block("ra".to_owned(), b"adler check", 0);
890        assert!(matches!(
891            aud.audit_block("ra", b"adler check"),
892            AuditResult::Passed { .. }
893        ));
894    }
895
896    #[test]
897    fn fnvxor64_algo_round_trip() {
898        let mut aud = auditor_with_algo(ChecksumAlgo::FnvXor64);
899        aud.register_block("rf".to_owned(), b"fnv check", 0);
900        assert!(matches!(
901            aud.audit_block("rf", b"fnv check"),
902            AuditResult::Passed { .. }
903        ));
904    }
905
906    #[test]
907    fn multicheck_algo_round_trip() {
908        let mut aud = auditor_with_algo(ChecksumAlgo::MultiCheck);
909        aud.register_block("rm".to_owned(), b"multi check", 0);
910        assert!(matches!(
911            aud.audit_block("rm", b"multi check"),
912            AuditResult::Passed { .. }
913        ));
914    }
915
916    // -----------------------------------------------------------------------
917    // Large block / binary data
918    // -----------------------------------------------------------------------
919
920    #[test]
921    fn large_block_passes() {
922        let mut aud = default_auditor();
923        let data: Vec<u8> = (0u8..=255).cycle().take(65536).collect();
924        aud.register_block("large".to_owned(), &data, 0);
925        assert!(matches!(
926            aud.audit_block("large", &data),
927            AuditResult::Passed { .. }
928        ));
929    }
930
931    #[test]
932    fn single_bit_flip_detected() {
933        let mut aud = default_auditor();
934        let mut data = vec![0xABu8; 512];
935        aud.register_block("flip".to_owned(), &data, 0);
936        // Flip a single bit.
937        data[256] ^= 0x01;
938        assert!(matches!(
939            aud.audit_block("flip", &data),
940            AuditResult::Failed { .. }
941        ));
942    }
943
944    #[test]
945    fn zero_length_block_round_trip() {
946        let mut aud = default_auditor();
947        aud.register_block("empty_block".to_owned(), &[], 0);
948        assert!(matches!(
949            aud.audit_block("empty_block", &[]),
950            AuditResult::Passed { .. }
951        ));
952    }
953
954    // -----------------------------------------------------------------------
955    // RepairRecord source field
956    // -----------------------------------------------------------------------
957
958    #[test]
959    fn repair_record_stores_source() {
960        let mut aud = default_auditor();
961        aud.schedule_repair("src_test".to_owned(), 0, "peer-XYZ".to_owned());
962        let pending = aud.pending_repairs();
963        assert_eq!(pending[0].source, "peer-XYZ");
964    }
965
966    // -----------------------------------------------------------------------
967    // AuditResult fields
968    // -----------------------------------------------------------------------
969
970    #[test]
971    fn failed_result_contains_both_checksums() {
972        let mut aud = default_auditor();
973        aud.register_block("fc".to_owned(), b"original", 0);
974        match aud.audit_block("fc", b"tampered") {
975            AuditResult::Failed {
976                stored_checksum,
977                computed_checksum,
978                ..
979            } => {
980                assert_ne!(stored_checksum, computed_checksum);
981            }
982            other => panic!("expected Failed, got {other:?}"),
983        }
984    }
985
986    #[test]
987    fn passed_result_contains_correct_checksum() {
988        let mut aud = default_auditor();
989        let data = b"pass me";
990        aud.register_block("pm".to_owned(), data, 0);
991        let expected = compute_checksum(data, &ChecksumAlgo::MultiCheck);
992        match aud.audit_block("pm", data) {
993            AuditResult::Passed { checksum, .. } => assert_eq!(checksum, expected),
994            other => panic!("expected Passed, got {other:?}"),
995        }
996    }
997
998    // -----------------------------------------------------------------------
999    // Registered block count
1000    // -----------------------------------------------------------------------
1001
1002    #[test]
1003    fn registered_block_count() {
1004        let mut aud = default_auditor();
1005        for i in 0u8..7 {
1006            aud.register_block(format!("blk{i}"), &[i], 0);
1007        }
1008        assert_eq!(aud.checksums.len(), 7);
1009    }
1010
1011    // -----------------------------------------------------------------------
1012    // block_size stored in record
1013    // -----------------------------------------------------------------------
1014
1015    #[test]
1016    fn block_checksum_records_size() {
1017        let mut aud = default_auditor();
1018        let data = b"size test";
1019        aud.register_block("sz".to_owned(), data, 42);
1020        let rec = aud.checksums.get("sz").expect("record must exist");
1021        assert_eq!(rec.block_size, data.len());
1022        assert_eq!(rec.computed_at, 42);
1023    }
1024}