1use std::collections::{HashMap, HashSet};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum IntegrityStatus {
11 Valid,
13 Corrupted,
15 Missing,
17 SizeMismatch,
19 ChecksumMismatch,
21}
22
23#[derive(Debug, Clone)]
25pub struct IntegrityReport {
26 pub cid: String,
28 pub status: IntegrityStatus,
30 pub expected_checksum: u64,
32 pub actual_checksum: u64,
34 pub expected_size: u64,
36 pub actual_size: u64,
38 pub checked_at: u64,
40 pub details: String,
42}
43
44#[derive(Debug, Clone)]
46pub struct IntegrityCheckerConfig {
47 pub batch_size: usize,
49 pub parallel_checks: usize,
51 pub verify_checksums: bool,
53 pub verify_sizes: bool,
55 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#[derive(Debug, Clone)]
73pub struct BlockRecord {
74 pub cid: String,
76 pub data: Vec<u8>,
78 pub expected_checksum: u64,
80 pub expected_size: u64,
82}
83
84#[derive(Debug, Clone, Default)]
86pub struct IntegrityStats {
87 pub total_checked: u64,
89 pub valid: u64,
91 pub corrupted: u64,
93 pub missing: u64,
95 pub size_mismatches: u64,
97 pub checksum_mismatches: u64,
99 pub quarantined: u64,
101}
102
103pub struct DataIntegrityChecker {
108 config: IntegrityCheckerConfig,
109 records: HashMap<String, BlockRecord>,
110 reports: Vec<IntegrityReport>,
111 quarantined: HashSet<String>,
112 stats: IntegrityStats,
113}
114
115const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
117const FNV_PRIME: u64 = 0x00000100000001B3;
118
119impl DataIntegrityChecker {
120 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 pub fn register_block(&mut self, record: BlockRecord) {
133 self.records.insert(record.cid.clone(), record);
134 }
135
136 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 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 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 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 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 pub fn is_quarantined(&self, cid: &str) -> bool {
282 self.quarantined.contains(cid)
283 }
284
285 pub fn quarantined_count(&self) -> usize {
287 self.quarantined.len()
288 }
289
290 pub fn get_report(&self, cid: &str) -> Option<&IntegrityReport> {
292 self.reports.iter().rev().find(|r| r.cid == cid)
293 }
294
295 pub fn corrupted_blocks(&self) -> Vec<&IntegrityReport> {
297 self.reports
298 .iter()
299 .filter(|r| r.status == IntegrityStatus::Corrupted)
300 .collect()
301 }
302
303 pub fn stats(&self) -> &IntegrityStats {
305 &self.stats
306 }
307
308 pub fn clear_reports(&mut self) {
310 self.reports.clear();
311 }
312
313 pub fn remove_block(&mut self, cid: &str) -> bool {
315 self.records.remove(cid).is_some()
316 }
317}
318
319fn 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 assert!(!checker.quarantine("cid1"));
452 }
453
454 #[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 assert!(!checker.unquarantine("cid1"));
463 }
464
465 #[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 #[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"); 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 #[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 #[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 assert_eq!(checker.stats().total_checked, 1);
527 }
528
529 #[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")); }
537
538 #[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 #[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 #[test]
558 fn test_fnv1a_empty() {
559 let checksum = DataIntegrityChecker::compute_checksum(b"");
560 assert_eq!(checksum, FNV_OFFSET_BASIS);
561 }
562
563 #[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 #[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 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 #[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 #[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 #[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 #[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 assert_eq!(report.status, IntegrityStatus::Valid);
634 }
635
636 #[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 assert_eq!(report.status, IntegrityStatus::Valid);
646 }
647
648 #[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 #[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 #[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 #[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 #[test]
693 fn test_fnv1a_known_value() {
694 let checksum = DataIntegrityChecker::compute_checksum(b"foobar");
696 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 #[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 #[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); }
725
726 #[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}