Skip to main content

ipfrs_storage/
data_integrity_checker.rs

1//! Data Integrity Checker - Deep integrity verification for stored blocks
2//!
3//! Provides FNV-1a based checksum verification, size validation,
4//! quarantine management, and batch integrity checking for block storage.
5
6use std::collections::{HashMap, HashSet};
7
8/// Status of a block's integrity check.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum IntegrityStatus {
11    /// Block data is valid and matches expected checksum and size.
12    Valid,
13    /// Block data is corrupted (checksum and size both wrong).
14    Corrupted,
15    /// Block is not found in the registry.
16    Missing,
17    /// Block size does not match expected size.
18    SizeMismatch,
19    /// Block checksum does not match expected checksum.
20    ChecksumMismatch,
21}
22
23/// Report of a single block's integrity check.
24#[derive(Debug, Clone)]
25pub struct IntegrityReport {
26    /// Content identifier of the block.
27    pub cid: String,
28    /// Result status of the integrity check.
29    pub status: IntegrityStatus,
30    /// Expected FNV-1a checksum.
31    pub expected_checksum: u64,
32    /// Actual computed FNV-1a checksum.
33    pub actual_checksum: u64,
34    /// Expected block size in bytes.
35    pub expected_size: u64,
36    /// Actual block size in bytes.
37    pub actual_size: u64,
38    /// Timestamp (epoch seconds) when the check was performed.
39    pub checked_at: u64,
40    /// Human-readable details about the check result.
41    pub details: String,
42}
43
44/// Configuration for the integrity checker.
45#[derive(Debug, Clone)]
46pub struct IntegrityCheckerConfig {
47    /// Number of blocks to process per batch.
48    pub batch_size: usize,
49    /// Number of parallel checks (advisory, not used in sync impl).
50    pub parallel_checks: usize,
51    /// Whether to verify checksums during checks.
52    pub verify_checksums: bool,
53    /// Whether to verify sizes during checks.
54    pub verify_sizes: bool,
55    /// Whether to automatically quarantine failed blocks.
56    pub auto_quarantine: bool,
57}
58
59impl Default for IntegrityCheckerConfig {
60    fn default() -> Self {
61        Self {
62            batch_size: 100,
63            parallel_checks: 4,
64            verify_checksums: true,
65            verify_sizes: true,
66            auto_quarantine: false,
67        }
68    }
69}
70
71/// A registered block record containing data and expected metadata.
72#[derive(Debug, Clone)]
73pub struct BlockRecord {
74    /// Content identifier.
75    pub cid: String,
76    /// Raw block data.
77    pub data: Vec<u8>,
78    /// Expected FNV-1a checksum of the data.
79    pub expected_checksum: u64,
80    /// Expected size in bytes.
81    pub expected_size: u64,
82}
83
84/// Aggregate statistics for integrity checks.
85#[derive(Debug, Clone, Default)]
86pub struct IntegrityStats {
87    /// Total number of blocks checked.
88    pub total_checked: u64,
89    /// Number of valid blocks.
90    pub valid: u64,
91    /// Number of corrupted blocks.
92    pub corrupted: u64,
93    /// Number of missing blocks.
94    pub missing: u64,
95    /// Number of size mismatches.
96    pub size_mismatches: u64,
97    /// Number of checksum mismatches.
98    pub checksum_mismatches: u64,
99    /// Number of quarantined blocks.
100    pub quarantined: u64,
101}
102
103/// Deep integrity checker for stored blocks.
104///
105/// Maintains a registry of block records, performs FNV-1a checksum and
106/// size verification, tracks quarantined blocks, and accumulates statistics.
107pub struct DataIntegrityChecker {
108    config: IntegrityCheckerConfig,
109    records: HashMap<String, BlockRecord>,
110    reports: Vec<IntegrityReport>,
111    quarantined: HashSet<String>,
112    stats: IntegrityStats,
113}
114
115// FNV-1a 64-bit constants
116const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
117const FNV_PRIME: u64 = 0x00000100000001B3;
118
119impl DataIntegrityChecker {
120    /// Create a new integrity checker with the given configuration.
121    pub fn new(config: IntegrityCheckerConfig) -> Self {
122        Self {
123            config,
124            records: HashMap::new(),
125            reports: Vec::new(),
126            quarantined: HashSet::new(),
127            stats: IntegrityStats::default(),
128        }
129    }
130
131    /// Register a block record for future integrity checks.
132    pub fn register_block(&mut self, record: BlockRecord) {
133        self.records.insert(record.cid.clone(), record);
134    }
135
136    /// Check the integrity of a single block by CID.
137    ///
138    /// Returns `None` if the CID is not registered (and records a Missing report).
139    pub fn check_block(&mut self, cid: &str) -> Option<IntegrityReport> {
140        let now = current_epoch_secs();
141
142        let record = match self.records.get(cid) {
143            Some(r) => r.clone(),
144            None => {
145                let report = IntegrityReport {
146                    cid: cid.to_string(),
147                    status: IntegrityStatus::Missing,
148                    expected_checksum: 0,
149                    actual_checksum: 0,
150                    expected_size: 0,
151                    actual_size: 0,
152                    checked_at: now,
153                    details: format!("Block '{}' not found in registry", cid),
154                };
155                self.stats.total_checked += 1;
156                self.stats.missing += 1;
157                if self.config.auto_quarantine && self.quarantined.insert(cid.to_string()) {
158                    self.stats.quarantined += 1;
159                }
160                self.reports.push(report.clone());
161                return Some(report);
162            }
163        };
164
165        let actual_size = record.data.len() as u64;
166        let actual_checksum = Self::compute_checksum(&record.data);
167
168        let size_ok = !self.config.verify_sizes || actual_size == record.expected_size;
169        let checksum_ok =
170            !self.config.verify_checksums || actual_checksum == record.expected_checksum;
171
172        let (status, details) = if size_ok && checksum_ok {
173            (
174                IntegrityStatus::Valid,
175                format!("Block '{}' passed integrity check", cid),
176            )
177        } else if !size_ok && !checksum_ok {
178            (
179                IntegrityStatus::Corrupted,
180                format!(
181                    "Block '{}' corrupted: size expected {} got {}, checksum expected {:#018x} got {:#018x}",
182                    cid, record.expected_size, actual_size, record.expected_checksum, actual_checksum
183                ),
184            )
185        } else if !size_ok {
186            (
187                IntegrityStatus::SizeMismatch,
188                format!(
189                    "Block '{}' size mismatch: expected {} got {}",
190                    cid, record.expected_size, actual_size
191                ),
192            )
193        } else {
194            (
195                IntegrityStatus::ChecksumMismatch,
196                format!(
197                    "Block '{}' checksum mismatch: expected {:#018x} got {:#018x}",
198                    cid, record.expected_checksum, actual_checksum
199                ),
200            )
201        };
202
203        self.stats.total_checked += 1;
204        match &status {
205            IntegrityStatus::Valid => self.stats.valid += 1,
206            IntegrityStatus::Corrupted => self.stats.corrupted += 1,
207            IntegrityStatus::SizeMismatch => self.stats.size_mismatches += 1,
208            IntegrityStatus::ChecksumMismatch => self.stats.checksum_mismatches += 1,
209            IntegrityStatus::Missing => self.stats.missing += 1,
210        }
211
212        if self.config.auto_quarantine
213            && status != IntegrityStatus::Valid
214            && self.quarantined.insert(cid.to_string())
215        {
216            self.stats.quarantined += 1;
217        }
218
219        let report = IntegrityReport {
220            cid: cid.to_string(),
221            status,
222            expected_checksum: record.expected_checksum,
223            actual_checksum,
224            expected_size: record.expected_size,
225            actual_size,
226            checked_at: now,
227            details,
228        };
229
230        self.reports.push(report.clone());
231        Some(report)
232    }
233
234    /// Check integrity of all registered blocks.
235    ///
236    /// Processes blocks in batches according to the configured `batch_size`.
237    pub fn check_all(&mut self) -> Vec<IntegrityReport> {
238        let cids: Vec<String> = self.records.keys().cloned().collect();
239        let mut results = Vec::with_capacity(cids.len());
240
241        for batch in cids.chunks(self.config.batch_size.max(1)) {
242            for cid in batch {
243                if let Some(report) = self.check_block(cid) {
244                    results.push(report);
245                }
246            }
247        }
248
249        results
250    }
251
252    /// Compute FNV-1a 64-bit hash of the given data.
253    pub fn compute_checksum(data: &[u8]) -> u64 {
254        let mut hash = FNV_OFFSET_BASIS;
255        for &byte in data {
256            hash ^= byte as u64;
257            hash = hash.wrapping_mul(FNV_PRIME);
258        }
259        hash
260    }
261
262    /// Quarantine a block by CID. Returns `true` if newly quarantined.
263    pub fn quarantine(&mut self, cid: &str) -> bool {
264        let inserted = self.quarantined.insert(cid.to_string());
265        if inserted {
266            self.stats.quarantined += 1;
267        }
268        inserted
269    }
270
271    /// Remove a block from quarantine. Returns `true` if it was quarantined.
272    pub fn unquarantine(&mut self, cid: &str) -> bool {
273        let removed = self.quarantined.remove(cid);
274        if removed {
275            self.stats.quarantined = self.stats.quarantined.saturating_sub(1);
276        }
277        removed
278    }
279
280    /// Check if a block is currently quarantined.
281    pub fn is_quarantined(&self, cid: &str) -> bool {
282        self.quarantined.contains(cid)
283    }
284
285    /// Number of currently quarantined blocks.
286    pub fn quarantined_count(&self) -> usize {
287        self.quarantined.len()
288    }
289
290    /// Get the most recent report for a given CID.
291    pub fn get_report(&self, cid: &str) -> Option<&IntegrityReport> {
292        self.reports.iter().rev().find(|r| r.cid == cid)
293    }
294
295    /// Get all reports for corrupted blocks.
296    pub fn corrupted_blocks(&self) -> Vec<&IntegrityReport> {
297        self.reports
298            .iter()
299            .filter(|r| r.status == IntegrityStatus::Corrupted)
300            .collect()
301    }
302
303    /// Get the current aggregate statistics.
304    pub fn stats(&self) -> &IntegrityStats {
305        &self.stats
306    }
307
308    /// Clear all stored reports (does not reset stats or quarantine).
309    pub fn clear_reports(&mut self) {
310        self.reports.clear();
311    }
312
313    /// Remove a block record from the registry. Returns `true` if it existed.
314    pub fn remove_block(&mut self, cid: &str) -> bool {
315        self.records.remove(cid).is_some()
316    }
317}
318
319/// Get the current time as epoch seconds, falling back to 0 on error.
320fn current_epoch_secs() -> u64 {
321    std::time::SystemTime::now()
322        .duration_since(std::time::UNIX_EPOCH)
323        .unwrap_or_default()
324        .as_secs()
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    fn default_config() -> IntegrityCheckerConfig {
332        IntegrityCheckerConfig::default()
333    }
334
335    fn make_block(cid: &str, data: &[u8]) -> BlockRecord {
336        let checksum = DataIntegrityChecker::compute_checksum(data);
337        BlockRecord {
338            cid: cid.to_string(),
339            data: data.to_vec(),
340            expected_checksum: checksum,
341            expected_size: data.len() as u64,
342        }
343    }
344
345    fn make_block_with_wrong_checksum(cid: &str, data: &[u8]) -> BlockRecord {
346        BlockRecord {
347            cid: cid.to_string(),
348            data: data.to_vec(),
349            expected_checksum: 0xDEADBEEF,
350            expected_size: data.len() as u64,
351        }
352    }
353
354    fn make_block_with_wrong_size(cid: &str, data: &[u8]) -> BlockRecord {
355        let checksum = DataIntegrityChecker::compute_checksum(data);
356        BlockRecord {
357            cid: cid.to_string(),
358            data: data.to_vec(),
359            expected_checksum: checksum,
360            expected_size: data.len() as u64 + 999,
361        }
362    }
363
364    fn make_corrupted_block(cid: &str, data: &[u8]) -> BlockRecord {
365        BlockRecord {
366            cid: cid.to_string(),
367            data: data.to_vec(),
368            expected_checksum: 0xDEADBEEF,
369            expected_size: data.len() as u64 + 100,
370        }
371    }
372
373    // 1. Valid block verification
374    #[test]
375    fn test_valid_block() {
376        let mut checker = DataIntegrityChecker::new(default_config());
377        checker.register_block(make_block("cid1", b"hello world"));
378        let report = checker.check_block("cid1").expect("should return report");
379        assert_eq!(report.status, IntegrityStatus::Valid);
380        assert_eq!(report.expected_checksum, report.actual_checksum);
381        assert_eq!(report.expected_size, report.actual_size);
382    }
383
384    // 2. Corrupted data detection (both checksum and size wrong)
385    #[test]
386    fn test_corrupted_block() {
387        let mut checker = DataIntegrityChecker::new(default_config());
388        checker.register_block(make_corrupted_block("cid1", b"data"));
389        let report = checker.check_block("cid1").expect("should return report");
390        assert_eq!(report.status, IntegrityStatus::Corrupted);
391    }
392
393    // 3. Size mismatch
394    #[test]
395    fn test_size_mismatch() {
396        let mut checker = DataIntegrityChecker::new(default_config());
397        checker.register_block(make_block_with_wrong_size("cid1", b"some data"));
398        let report = checker.check_block("cid1").expect("should return report");
399        assert_eq!(report.status, IntegrityStatus::SizeMismatch);
400    }
401
402    // 4. Checksum mismatch
403    #[test]
404    fn test_checksum_mismatch() {
405        let mut checker = DataIntegrityChecker::new(default_config());
406        checker.register_block(make_block_with_wrong_checksum("cid1", b"payload"));
407        let report = checker.check_block("cid1").expect("should return report");
408        assert_eq!(report.status, IntegrityStatus::ChecksumMismatch);
409    }
410
411    // 5. Missing block
412    #[test]
413    fn test_missing_block() {
414        let mut checker = DataIntegrityChecker::new(default_config());
415        let report = checker
416            .check_block("nonexistent")
417            .expect("should return report");
418        assert_eq!(report.status, IntegrityStatus::Missing);
419    }
420
421    // 6. Auto-quarantine on failure
422    #[test]
423    fn test_auto_quarantine_on_corrupted() {
424        let mut config = default_config();
425        config.auto_quarantine = true;
426        let mut checker = DataIntegrityChecker::new(config);
427        checker.register_block(make_corrupted_block("cid1", b"bad data"));
428        checker.check_block("cid1");
429        assert!(checker.is_quarantined("cid1"));
430        assert_eq!(checker.quarantined_count(), 1);
431    }
432
433    // 7. Auto-quarantine does NOT quarantine valid blocks
434    #[test]
435    fn test_auto_quarantine_valid_block() {
436        let mut config = default_config();
437        config.auto_quarantine = true;
438        let mut checker = DataIntegrityChecker::new(config);
439        checker.register_block(make_block("cid1", b"good data"));
440        checker.check_block("cid1");
441        assert!(!checker.is_quarantined("cid1"));
442    }
443
444    // 8. Manual quarantine
445    #[test]
446    fn test_manual_quarantine() {
447        let mut checker = DataIntegrityChecker::new(default_config());
448        assert!(checker.quarantine("cid1"));
449        assert!(checker.is_quarantined("cid1"));
450        // Second call returns false (already quarantined)
451        assert!(!checker.quarantine("cid1"));
452    }
453
454    // 9. Unquarantine
455    #[test]
456    fn test_unquarantine() {
457        let mut checker = DataIntegrityChecker::new(default_config());
458        checker.quarantine("cid1");
459        assert!(checker.unquarantine("cid1"));
460        assert!(!checker.is_quarantined("cid1"));
461        // Second call returns false
462        assert!(!checker.unquarantine("cid1"));
463    }
464
465    // 10. check_all with batch
466    #[test]
467    fn test_check_all() {
468        let mut checker = DataIntegrityChecker::new(default_config());
469        checker.register_block(make_block("a", b"alpha"));
470        checker.register_block(make_block("b", b"bravo"));
471        checker.register_block(make_block("c", b"charlie"));
472        let reports = checker.check_all();
473        assert_eq!(reports.len(), 3);
474        assert!(reports.iter().all(|r| r.status == IntegrityStatus::Valid));
475    }
476
477    // 11. Stats accuracy after mixed checks
478    #[test]
479    fn test_stats_accuracy() {
480        let mut checker = DataIntegrityChecker::new(default_config());
481        checker.register_block(make_block("ok1", b"valid1"));
482        checker.register_block(make_block("ok2", b"valid2"));
483        checker.register_block(make_corrupted_block("bad1", b"corrupt"));
484        checker.register_block(make_block_with_wrong_size("sz1", b"sizewrong"));
485        checker.register_block(make_block_with_wrong_checksum("cs1", b"cswrong"));
486
487        checker.check_block("ok1");
488        checker.check_block("ok2");
489        checker.check_block("bad1");
490        checker.check_block("sz1");
491        checker.check_block("cs1");
492        checker.check_block("missing_one"); // missing
493
494        let s = checker.stats();
495        assert_eq!(s.total_checked, 6);
496        assert_eq!(s.valid, 2);
497        assert_eq!(s.corrupted, 1);
498        assert_eq!(s.size_mismatches, 1);
499        assert_eq!(s.checksum_mismatches, 1);
500        assert_eq!(s.missing, 1);
501    }
502
503    // 12. corrupted_blocks filter
504    #[test]
505    fn test_corrupted_blocks_filter() {
506        let mut checker = DataIntegrityChecker::new(default_config());
507        checker.register_block(make_block("ok", b"fine"));
508        checker.register_block(make_corrupted_block("bad", b"nope"));
509        checker.check_block("ok");
510        checker.check_block("bad");
511        let corrupted = checker.corrupted_blocks();
512        assert_eq!(corrupted.len(), 1);
513        assert_eq!(corrupted[0].cid, "bad");
514    }
515
516    // 13. clear_reports
517    #[test]
518    fn test_clear_reports() {
519        let mut checker = DataIntegrityChecker::new(default_config());
520        checker.register_block(make_block("x", b"data"));
521        checker.check_block("x");
522        assert!(!checker.reports.is_empty());
523        checker.clear_reports();
524        assert!(checker.reports.is_empty());
525        // Stats should still be there
526        assert_eq!(checker.stats().total_checked, 1);
527    }
528
529    // 14. Register and remove block
530    #[test]
531    fn test_register_remove() {
532        let mut checker = DataIntegrityChecker::new(default_config());
533        checker.register_block(make_block("cid1", b"data"));
534        assert!(checker.remove_block("cid1"));
535        assert!(!checker.remove_block("cid1")); // already removed
536    }
537
538    // 15. Empty checker
539    #[test]
540    fn test_empty_checker() {
541        let checker = DataIntegrityChecker::new(default_config());
542        assert_eq!(checker.stats().total_checked, 0);
543        assert_eq!(checker.quarantined_count(), 0);
544        assert!(checker.corrupted_blocks().is_empty());
545    }
546
547    // 16. FNV-1a checksum consistency
548    #[test]
549    fn test_fnv1a_consistency() {
550        let data = b"hello";
551        let c1 = DataIntegrityChecker::compute_checksum(data);
552        let c2 = DataIntegrityChecker::compute_checksum(data);
553        assert_eq!(c1, c2);
554    }
555
556    // 17. FNV-1a empty data
557    #[test]
558    fn test_fnv1a_empty() {
559        let checksum = DataIntegrityChecker::compute_checksum(b"");
560        assert_eq!(checksum, FNV_OFFSET_BASIS);
561    }
562
563    // 18. FNV-1a different inputs produce different hashes
564    #[test]
565    fn test_fnv1a_different_inputs() {
566        let c1 = DataIntegrityChecker::compute_checksum(b"aaa");
567        let c2 = DataIntegrityChecker::compute_checksum(b"bbb");
568        assert_ne!(c1, c2);
569    }
570
571    // 19. get_report returns most recent
572    #[test]
573    fn test_get_report_most_recent() {
574        let mut checker = DataIntegrityChecker::new(default_config());
575        checker.register_block(make_block("cid1", b"data"));
576        checker.check_block("cid1");
577        // Re-register with corrupted and re-check
578        checker.register_block(make_corrupted_block("cid1", b"data"));
579        checker.check_block("cid1");
580        let report = checker.get_report("cid1").expect("should exist");
581        assert_eq!(report.status, IntegrityStatus::Corrupted);
582    }
583
584    // 20. get_report returns None for unknown cid
585    #[test]
586    fn test_get_report_none() {
587        let checker = DataIntegrityChecker::new(default_config());
588        assert!(checker.get_report("unknown").is_none());
589    }
590
591    // 21. Quarantined count tracks correctly
592    #[test]
593    fn test_quarantined_count_tracking() {
594        let mut checker = DataIntegrityChecker::new(default_config());
595        checker.quarantine("a");
596        checker.quarantine("b");
597        checker.quarantine("c");
598        assert_eq!(checker.quarantined_count(), 3);
599        checker.unquarantine("b");
600        assert_eq!(checker.quarantined_count(), 2);
601        assert_eq!(checker.stats().quarantined, 2);
602    }
603
604    // 22. check_all with mixed results
605    #[test]
606    fn test_check_all_mixed() {
607        let mut checker = DataIntegrityChecker::new(default_config());
608        checker.register_block(make_block("good", b"ok"));
609        checker.register_block(make_corrupted_block("bad", b"no"));
610        let reports = checker.check_all();
611        assert_eq!(reports.len(), 2);
612        let valid_count = reports
613            .iter()
614            .filter(|r| r.status == IntegrityStatus::Valid)
615            .count();
616        let corrupted_count = reports
617            .iter()
618            .filter(|r| r.status == IntegrityStatus::Corrupted)
619            .count();
620        assert_eq!(valid_count, 1);
621        assert_eq!(corrupted_count, 1);
622    }
623
624    // 23. Disable checksum verification
625    #[test]
626    fn test_disable_checksum_verification() {
627        let mut config = default_config();
628        config.verify_checksums = false;
629        let mut checker = DataIntegrityChecker::new(config);
630        checker.register_block(make_block_with_wrong_checksum("cid1", b"data"));
631        let report = checker.check_block("cid1").expect("should return report");
632        // With checksum verification disabled, only size is checked (size is correct)
633        assert_eq!(report.status, IntegrityStatus::Valid);
634    }
635
636    // 24. Disable size verification
637    #[test]
638    fn test_disable_size_verification() {
639        let mut config = default_config();
640        config.verify_sizes = false;
641        let mut checker = DataIntegrityChecker::new(config);
642        checker.register_block(make_block_with_wrong_size("cid1", b"data"));
643        let report = checker.check_block("cid1").expect("should return report");
644        // With size verification disabled, only checksum is checked (checksum is correct)
645        assert_eq!(report.status, IntegrityStatus::Valid);
646    }
647
648    // 25. Auto-quarantine on checksum mismatch
649    #[test]
650    fn test_auto_quarantine_checksum_mismatch() {
651        let mut config = default_config();
652        config.auto_quarantine = true;
653        let mut checker = DataIntegrityChecker::new(config);
654        checker.register_block(make_block_with_wrong_checksum("cs1", b"xxx"));
655        checker.check_block("cs1");
656        assert!(checker.is_quarantined("cs1"));
657    }
658
659    // 26. Auto-quarantine on missing block
660    #[test]
661    fn test_auto_quarantine_missing() {
662        let mut config = default_config();
663        config.auto_quarantine = true;
664        let mut checker = DataIntegrityChecker::new(config);
665        checker.check_block("ghost");
666        assert!(checker.is_quarantined("ghost"));
667    }
668
669    // 27. Report details contain cid
670    #[test]
671    fn test_report_details_contain_cid() {
672        let mut checker = DataIntegrityChecker::new(default_config());
673        checker.register_block(make_block("my-cid", b"test"));
674        let report = checker.check_block("my-cid").expect("report");
675        assert!(report.details.contains("my-cid"));
676    }
677
678    // 28. Batch size of 1
679    #[test]
680    fn test_batch_size_one() {
681        let mut config = default_config();
682        config.batch_size = 1;
683        let mut checker = DataIntegrityChecker::new(config);
684        checker.register_block(make_block("a", b"1"));
685        checker.register_block(make_block("b", b"2"));
686        checker.register_block(make_block("c", b"3"));
687        let reports = checker.check_all();
688        assert_eq!(reports.len(), 3);
689    }
690
691    // 29. FNV-1a known value test
692    #[test]
693    fn test_fnv1a_known_value() {
694        // FNV-1a 64-bit of "foobar" is a well-known test vector
695        let checksum = DataIntegrityChecker::compute_checksum(b"foobar");
696        // Manually verify: start with offset basis and apply algorithm
697        let mut expected = FNV_OFFSET_BASIS;
698        for &b in b"foobar" {
699            expected ^= b as u64;
700            expected = expected.wrapping_mul(FNV_PRIME);
701        }
702        assert_eq!(checksum, expected);
703    }
704
705    // 30. Remove block then check returns missing
706    #[test]
707    fn test_remove_then_check() {
708        let mut checker = DataIntegrityChecker::new(default_config());
709        checker.register_block(make_block("cid1", b"data"));
710        checker.remove_block("cid1");
711        let report = checker.check_block("cid1").expect("report");
712        assert_eq!(report.status, IntegrityStatus::Missing);
713    }
714
715    // 31. Re-register overwrites block
716    #[test]
717    fn test_re_register_overwrites() {
718        let mut checker = DataIntegrityChecker::new(default_config());
719        checker.register_block(make_block("cid1", b"old"));
720        checker.register_block(make_block("cid1", b"new"));
721        let report = checker.check_block("cid1").expect("report");
722        assert_eq!(report.status, IntegrityStatus::Valid);
723        assert_eq!(report.actual_size, 3); // "new" is 3 bytes
724    }
725
726    // 32. checked_at is non-zero
727    #[test]
728    fn test_checked_at_nonzero() {
729        let mut checker = DataIntegrityChecker::new(default_config());
730        checker.register_block(make_block("cid1", b"data"));
731        let report = checker.check_block("cid1").expect("report");
732        assert!(report.checked_at > 0);
733    }
734}