1use std::collections::{HashMap, HashSet};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum CorruptionType {
17 BitFlip,
19 Truncation,
21 ZeroFill,
23 HeaderDamage,
25 Unknown,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum RepairAction {
32 Restored,
34 RebuiltFromParity,
36 FetchedFromPeer,
38 Quarantined,
40 Unrecoverable,
42}
43
44#[derive(Debug, Clone)]
46pub struct CorruptionReport {
47 pub cid: String,
49 pub corruption_type: CorruptionType,
51 pub byte_offset: Option<usize>,
53 pub bytes_affected: usize,
55 pub detected_at: u64,
57 pub repair_action: Option<RepairAction>,
59}
60
61#[derive(Debug, Clone)]
63pub struct RepairConfig {
64 pub enable_parity: bool,
66 pub parity_block_size: usize,
68 pub max_repair_attempts: u32,
70 pub auto_quarantine_on_fail: bool,
72}
73
74impl Default for RepairConfig {
75 fn default() -> Self {
76 Self {
77 enable_parity: true,
78 parity_block_size: 256,
79 max_repair_attempts: 3,
80 auto_quarantine_on_fail: true,
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct BlockWithParity {
88 pub cid: String,
90 pub data: Vec<u8>,
92 pub parity: Vec<u8>,
94 pub checksum: u64,
96}
97
98#[derive(Debug, Clone, Default)]
100pub struct RepairStats {
101 pub total_scanned: u64,
103 pub corruptions_found: u64,
105 pub repairs_successful: u64,
107 pub repairs_failed: u64,
109 pub quarantined: u64,
111}
112
113pub struct CorruptionRepairer {
119 config: RepairConfig,
120 blocks: HashMap<String, BlockWithParity>,
121 reports: Vec<CorruptionReport>,
122 quarantined: HashSet<String>,
123 stats: RepairStats,
124}
125
126impl CorruptionRepairer {
127 pub fn new(config: RepairConfig) -> Self {
129 Self {
130 config,
131 blocks: HashMap::new(),
132 reports: Vec::new(),
133 quarantined: HashSet::new(),
134 stats: RepairStats::default(),
135 }
136 }
137
138 pub fn add_block(&mut self, cid: &str, data: &[u8]) {
140 let parity = Self::compute_parity(data, self.config.parity_block_size);
141 let checksum = Self::compute_checksum(data);
142 self.blocks.insert(
143 cid.to_string(),
144 BlockWithParity {
145 cid: cid.to_string(),
146 data: data.to_vec(),
147 parity,
148 checksum,
149 },
150 );
151 }
152
153 pub fn compute_parity(data: &[u8], block_size: usize) -> Vec<u8> {
158 let bs = if block_size == 0 { 256 } else { block_size };
159 let mut parity = vec![0u8; bs];
160 for (i, &byte) in data.iter().enumerate() {
161 parity[i % bs] ^= byte;
162 }
163 parity
164 }
165
166 pub fn compute_checksum(data: &[u8]) -> u64 {
168 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
169 const FNV_PRIME: u64 = 0x00000100000001B3;
170 let mut hash = FNV_OFFSET;
171 for &byte in data {
172 hash ^= byte as u64;
173 hash = hash.wrapping_mul(FNV_PRIME);
174 }
175 hash
176 }
177
178 pub fn detect_corruption(&self, cid: &str) -> Option<CorruptionType> {
182 let block = self.blocks.get(cid)?;
183 let current_checksum = Self::compute_checksum(&block.data);
184 if current_checksum == block.checksum {
185 return None;
186 }
187 Some(self.classify_corruption(block))
188 }
189
190 fn classify_corruption(&self, block: &BlockWithParity) -> CorruptionType {
192 let original_len = self.estimate_original_length(block);
193
194 if block.data.len() < original_len {
196 return CorruptionType::Truncation;
197 }
198
199 let header_size = 16.min(block.data.len());
201 let parity_bs = if self.config.parity_block_size == 0 {
202 256
203 } else {
204 self.config.parity_block_size
205 };
206 let current_parity = Self::compute_parity(&block.data, parity_bs);
207 let mut header_damaged = false;
208 for i in 0..header_size {
209 if i < current_parity.len()
210 && i < block.parity.len()
211 && current_parity[i] != block.parity[i]
212 {
213 header_damaged = true;
214 break;
215 }
216 }
217 if header_damaged {
218 return CorruptionType::HeaderDamage;
219 }
220
221 let zero_run_threshold = 8;
223 let mut consecutive_zeros = 0usize;
224 for &b in &block.data {
225 if b == 0 {
226 consecutive_zeros += 1;
227 if consecutive_zeros >= zero_run_threshold {
228 return CorruptionType::ZeroFill;
229 }
230 } else {
231 consecutive_zeros = 0;
232 }
233 }
234
235 let differing = current_parity
237 .iter()
238 .zip(block.parity.iter())
239 .filter(|(a, b)| a != b)
240 .count();
241 if differing > 0 && differing <= parity_bs / 2 {
242 return CorruptionType::BitFlip;
243 }
244
245 CorruptionType::Unknown
246 }
247
248 fn estimate_original_length(&self, block: &BlockWithParity) -> usize {
251 block.data.len()
252 }
253
254 pub fn scan_all(&mut self) -> Vec<CorruptionReport> {
256 let cids: Vec<String> = self.blocks.keys().cloned().collect();
257 let mut new_reports = Vec::new();
258 for cid in &cids {
259 self.stats.total_scanned += 1;
260 if let Some(corruption_type) = self.detect_corruption_internal(cid) {
261 self.stats.corruptions_found += 1;
262 let (byte_offset, bytes_affected) = self.estimate_damage_extent(cid);
263 let report = CorruptionReport {
264 cid: cid.clone(),
265 corruption_type,
266 byte_offset,
267 bytes_affected,
268 detected_at: current_epoch_secs(),
269 repair_action: None,
270 };
271 new_reports.push(report);
272 }
273 }
274 self.reports.extend(new_reports.clone());
275 new_reports
276 }
277
278 fn detect_corruption_internal(&self, cid: &str) -> Option<CorruptionType> {
280 self.detect_corruption(cid)
281 }
282
283 fn estimate_damage_extent(&self, cid: &str) -> (Option<usize>, usize) {
285 let block = match self.blocks.get(cid) {
286 Some(b) => b,
287 None => return (None, 0),
288 };
289 let parity_bs = if self.config.parity_block_size == 0 {
290 256
291 } else {
292 self.config.parity_block_size
293 };
294 let current_parity = Self::compute_parity(&block.data, parity_bs);
295 let mut first_diff: Option<usize> = None;
296 let mut diff_count = 0usize;
297 for (i, (cur, orig)) in current_parity.iter().zip(block.parity.iter()).enumerate() {
298 if cur != orig {
299 if first_diff.is_none() {
300 first_diff = Some(i);
301 }
302 diff_count += 1;
303 }
304 }
305 (first_diff, diff_count)
306 }
307
308 pub fn repair_block(&mut self, cid: &str) -> Result<RepairAction, String> {
313 if !self.blocks.contains_key(cid) {
314 return Err(format!("block not found: {}", cid));
315 }
316
317 if self.detect_corruption(cid).is_none() {
319 return Ok(RepairAction::Restored);
320 }
321
322 if !self.config.enable_parity {
323 if self.config.auto_quarantine_on_fail {
324 self.quarantine(cid);
325 self.stats.repairs_failed += 1;
326 return Ok(RepairAction::Quarantined);
327 }
328 self.stats.repairs_failed += 1;
329 return Ok(RepairAction::Unrecoverable);
330 }
331
332 let mut repaired = false;
334 for _attempt in 0..self.config.max_repair_attempts {
335 if self.try_parity_repair(cid) {
336 repaired = true;
337 break;
338 }
339 }
340
341 if repaired {
342 self.stats.repairs_successful += 1;
343 if let Some(report) = self.reports.iter_mut().rev().find(|r| r.cid == cid) {
345 report.repair_action = Some(RepairAction::RebuiltFromParity);
346 }
347 Ok(RepairAction::RebuiltFromParity)
348 } else {
349 self.stats.repairs_failed += 1;
350 if self.config.auto_quarantine_on_fail {
351 self.quarantine(cid);
352 Ok(RepairAction::Quarantined)
353 } else {
354 Ok(RepairAction::Unrecoverable)
355 }
356 }
357 }
358
359 fn try_parity_repair(&mut self, cid: &str) -> bool {
366 let parity_bs = if self.config.parity_block_size == 0 {
367 256
368 } else {
369 self.config.parity_block_size
370 };
371
372 let block = match self.blocks.get(cid) {
374 Some(b) => b.clone(),
375 None => return false,
376 };
377
378 let current_parity = Self::compute_parity(&block.data, parity_bs);
379
380 let mut diff_positions: Vec<usize> = Vec::new();
382 for (i, (cur, orig)) in current_parity.iter().zip(block.parity.iter()).enumerate() {
383 if cur != orig {
384 diff_positions.push(i);
385 }
386 }
387
388 if diff_positions.is_empty() {
389 return true; }
391
392 let mut new_data = block.data.clone();
395 let mut any_fixed = false;
396
397 for &pos in &diff_positions {
398 let indices: Vec<usize> = (pos..block.data.len()).step_by(parity_bs).collect();
400
401 if indices.len() == 1 {
402 let idx = indices[0];
408 new_data[idx] ^= current_parity[pos] ^ block.parity[pos];
409 any_fixed = true;
410 } else {
411 for &idx in &indices {
413 let mut candidate = block.data.clone();
414 candidate[idx] ^= current_parity[pos] ^ block.parity[pos];
415 let candidate_checksum = Self::compute_checksum(&candidate);
416 if candidate_checksum == block.checksum {
417 new_data = candidate;
418 any_fixed = true;
419 break;
420 }
421 }
422 }
423 }
424
425 if any_fixed {
426 let new_checksum = Self::compute_checksum(&new_data);
427 if new_checksum == block.checksum {
428 if let Some(b) = self.blocks.get_mut(cid) {
429 b.data = new_data;
430 b.parity = Self::compute_parity(&b.data, parity_bs);
431 }
432 return true;
433 }
434 }
435
436 false
437 }
438
439 pub fn corrupt_block(&mut self, cid: &str, offset: usize, value: u8) -> Result<(), String> {
443 let block = self
444 .blocks
445 .get_mut(cid)
446 .ok_or_else(|| format!("block not found: {}", cid))?;
447 if offset >= block.data.len() {
448 return Err(format!(
449 "offset {} out of range for block of length {}",
450 offset,
451 block.data.len()
452 ));
453 }
454 block.data[offset] = value;
455 Ok(())
456 }
457
458 pub fn verify_block(&self, cid: &str) -> bool {
460 match self.blocks.get(cid) {
461 Some(block) => Self::compute_checksum(&block.data) == block.checksum,
462 None => false,
463 }
464 }
465
466 pub fn quarantine(&mut self, cid: &str) -> bool {
470 if self.blocks.contains_key(cid) {
471 let inserted = self.quarantined.insert(cid.to_string());
472 if inserted {
473 self.stats.quarantined += 1;
474 }
475 true
476 } else {
477 false
478 }
479 }
480
481 pub fn is_quarantined(&self, cid: &str) -> bool {
483 self.quarantined.contains(cid)
484 }
485
486 pub fn get_report(&self, cid: &str) -> Option<&CorruptionReport> {
488 self.reports.iter().rev().find(|r| r.cid == cid)
489 }
490
491 pub fn stats(&self) -> &RepairStats {
493 &self.stats
494 }
495}
496
497fn current_epoch_secs() -> u64 {
499 std::time::SystemTime::now()
500 .duration_since(std::time::UNIX_EPOCH)
501 .map(|d| d.as_secs())
502 .unwrap_or(0)
503}
504
505#[cfg(test)]
510mod tests {
511 use super::*;
512
513 fn default_config() -> RepairConfig {
514 RepairConfig::default()
515 }
516
517 fn sample_data() -> Vec<u8> {
518 (0..128).map(|i| (i * 7 + 13) as u8).collect()
519 }
520
521 #[test]
522 fn test_add_and_verify_clean_block() {
523 let mut repairer = CorruptionRepairer::new(default_config());
524 repairer.add_block("cid1", &sample_data());
525 assert!(repairer.verify_block("cid1"));
526 }
527
528 #[test]
529 fn test_verify_nonexistent_block() {
530 let repairer = CorruptionRepairer::new(default_config());
531 assert!(!repairer.verify_block("nonexistent"));
532 }
533
534 #[test]
535 fn test_detect_no_corruption_clean_block() {
536 let mut repairer = CorruptionRepairer::new(default_config());
537 repairer.add_block("cid1", &sample_data());
538 assert!(repairer.detect_corruption("cid1").is_none());
539 }
540
541 #[test]
542 fn test_detect_bit_flip() {
543 let mut repairer = CorruptionRepairer::new(default_config());
544 let data = sample_data();
545 repairer.add_block("cid1", &data);
546 let offset = 64;
548 let original = data[offset];
549 let flipped = original ^ 0x01;
550 repairer.corrupt_block("cid1", offset, flipped).ok();
551 let corruption = repairer.detect_corruption("cid1");
552 assert!(corruption.is_some());
553 assert!(!repairer.verify_block("cid1"));
554 }
555
556 #[test]
557 fn test_detect_truncation() {
558 let mut repairer = CorruptionRepairer::new(default_config());
559 let data = sample_data();
560 repairer.add_block("cid1", &data);
561 if let Some(block) = repairer.blocks.get_mut("cid1") {
563 block.data.truncate(64);
564 }
565 let corruption = repairer.detect_corruption("cid1");
566 assert!(corruption.is_some());
567 }
568
569 #[test]
570 fn test_detect_zero_fill() {
571 let mut repairer = CorruptionRepairer::new(default_config());
572 let mut data: Vec<u8> = (1..=128).collect();
573 for b in data.iter_mut() {
575 if *b == 0 {
576 *b = 1;
577 }
578 }
579 repairer.add_block("cid1", &data);
580 for i in 32..48 {
582 repairer.corrupt_block("cid1", i, 0).ok();
583 }
584 let corruption = repairer.detect_corruption("cid1");
585 assert!(corruption.is_some());
586 }
587
588 #[test]
589 fn test_parity_computation_deterministic() {
590 let data = sample_data();
591 let p1 = CorruptionRepairer::compute_parity(&data, 64);
592 let p2 = CorruptionRepairer::compute_parity(&data, 64);
593 assert_eq!(p1, p2);
594 }
595
596 #[test]
597 fn test_parity_empty_data() {
598 let parity = CorruptionRepairer::compute_parity(&[], 32);
599 assert_eq!(parity.len(), 32);
600 assert!(parity.iter().all(|&b| b == 0));
601 }
602
603 #[test]
604 fn test_parity_single_byte() {
605 let data = vec![0xAB];
606 let parity = CorruptionRepairer::compute_parity(&data, 4);
607 assert_eq!(parity[0], 0xAB);
608 assert_eq!(parity[1], 0);
609 }
610
611 #[test]
612 fn test_repair_via_parity_single_byte_flip() {
613 let mut repairer = CorruptionRepairer::new(RepairConfig {
614 enable_parity: true,
615 parity_block_size: 256,
616 max_repair_attempts: 3,
617 auto_quarantine_on_fail: true,
618 });
619 let data: Vec<u8> = (0..128).collect();
621 repairer.add_block("cid1", &data);
622
623 let original = data[50];
625 repairer.corrupt_block("cid1", 50, original ^ 0xFF).ok();
626 assert!(!repairer.verify_block("cid1"));
627
628 let result = repairer.repair_block("cid1");
629 assert!(result.is_ok());
630 let action = result.unwrap_or(RepairAction::Unrecoverable);
631 assert_eq!(action, RepairAction::RebuiltFromParity);
632 assert!(repairer.verify_block("cid1"));
633 }
634
635 #[test]
636 fn test_unrecoverable_damage() {
637 let mut repairer = CorruptionRepairer::new(RepairConfig {
638 enable_parity: false,
639 parity_block_size: 256,
640 max_repair_attempts: 1,
641 auto_quarantine_on_fail: false,
642 });
643 let data = sample_data();
644 repairer.add_block("cid1", &data);
645 repairer.corrupt_block("cid1", 10, 0xFF).ok();
646 let result = repairer.repair_block("cid1");
647 assert!(result.is_ok());
648 assert_eq!(
649 result.unwrap_or(RepairAction::Restored),
650 RepairAction::Unrecoverable
651 );
652 }
653
654 #[test]
655 fn test_auto_quarantine_on_fail() {
656 let mut repairer = CorruptionRepairer::new(RepairConfig {
657 enable_parity: false,
658 parity_block_size: 256,
659 max_repair_attempts: 1,
660 auto_quarantine_on_fail: true,
661 });
662 let data = sample_data();
663 repairer.add_block("cid1", &data);
664 repairer.corrupt_block("cid1", 10, 0xFF).ok();
665 let result = repairer.repair_block("cid1");
666 assert_eq!(
667 result.unwrap_or(RepairAction::Unrecoverable),
668 RepairAction::Quarantined
669 );
670 assert!(repairer.is_quarantined("cid1"));
671 }
672
673 #[test]
674 fn test_scan_all_clean() {
675 let mut repairer = CorruptionRepairer::new(default_config());
676 repairer.add_block("a", &[1, 2, 3]);
677 repairer.add_block("b", &[4, 5, 6]);
678 let reports = repairer.scan_all();
679 assert!(reports.is_empty());
680 assert_eq!(repairer.stats().total_scanned, 2);
681 }
682
683 #[test]
684 fn test_scan_all_with_corruption() {
685 let mut repairer = CorruptionRepairer::new(default_config());
686 repairer.add_block("a", &[1, 2, 3]);
687 repairer.add_block("b", &[4, 5, 6]);
688 repairer.corrupt_block("b", 0, 0xFF).ok();
689 let reports = repairer.scan_all();
690 assert_eq!(reports.len(), 1);
691 assert_eq!(reports[0].cid, "b");
692 assert_eq!(repairer.stats().corruptions_found, 1);
693 }
694
695 #[test]
696 fn test_quarantine_existing_block() {
697 let mut repairer = CorruptionRepairer::new(default_config());
698 repairer.add_block("cid1", &[10, 20]);
699 assert!(repairer.quarantine("cid1"));
700 assert!(repairer.is_quarantined("cid1"));
701 }
702
703 #[test]
704 fn test_quarantine_nonexistent_block() {
705 let mut repairer = CorruptionRepairer::new(default_config());
706 assert!(!repairer.quarantine("missing"));
707 assert!(!repairer.is_quarantined("missing"));
708 }
709
710 #[test]
711 fn test_quarantine_idempotent() {
712 let mut repairer = CorruptionRepairer::new(default_config());
713 repairer.add_block("cid1", &[1]);
714 repairer.quarantine("cid1");
715 let stats_after_first = repairer.stats().quarantined;
716 repairer.quarantine("cid1");
717 assert_eq!(repairer.stats().quarantined, stats_after_first);
718 }
719
720 #[test]
721 fn test_stats_initial() {
722 let repairer = CorruptionRepairer::new(default_config());
723 let s = repairer.stats();
724 assert_eq!(s.total_scanned, 0);
725 assert_eq!(s.corruptions_found, 0);
726 assert_eq!(s.repairs_successful, 0);
727 assert_eq!(s.repairs_failed, 0);
728 assert_eq!(s.quarantined, 0);
729 }
730
731 #[test]
732 fn test_stats_after_operations() {
733 let mut repairer = CorruptionRepairer::new(RepairConfig {
734 enable_parity: true,
735 parity_block_size: 256,
736 max_repair_attempts: 3,
737 auto_quarantine_on_fail: true,
738 });
739 let data: Vec<u8> = (0..64).collect();
740 repairer.add_block("cid1", &data);
741 repairer.corrupt_block("cid1", 10, 0xFF).ok();
742 let _ = repairer.repair_block("cid1");
743 let s = repairer.stats();
744 assert!(s.repairs_successful > 0 || s.repairs_failed > 0);
745 }
746
747 #[test]
748 fn test_inject_corruption() {
749 let mut repairer = CorruptionRepairer::new(default_config());
750 repairer.add_block("cid1", &[1, 2, 3, 4]);
751 assert!(repairer.corrupt_block("cid1", 2, 99).is_ok());
752 assert!(!repairer.verify_block("cid1"));
753 }
754
755 #[test]
756 fn test_inject_corruption_out_of_range() {
757 let mut repairer = CorruptionRepairer::new(default_config());
758 repairer.add_block("cid1", &[1, 2, 3]);
759 let result = repairer.corrupt_block("cid1", 100, 0);
760 assert!(result.is_err());
761 }
762
763 #[test]
764 fn test_inject_corruption_missing_block() {
765 let mut repairer = CorruptionRepairer::new(default_config());
766 let result = repairer.corrupt_block("missing", 0, 0);
767 assert!(result.is_err());
768 }
769
770 #[test]
771 fn test_checksum_consistency() {
772 let data = sample_data();
773 let c1 = CorruptionRepairer::compute_checksum(&data);
774 let c2 = CorruptionRepairer::compute_checksum(&data);
775 assert_eq!(c1, c2);
776 }
777
778 #[test]
779 fn test_checksum_differs_for_different_data() {
780 let c1 = CorruptionRepairer::compute_checksum(&[1, 2, 3]);
781 let c2 = CorruptionRepairer::compute_checksum(&[4, 5, 6]);
782 assert_ne!(c1, c2);
783 }
784
785 #[test]
786 fn test_checksum_empty() {
787 let c = CorruptionRepairer::compute_checksum(&[]);
788 assert_eq!(c, 0xcbf29ce484222325);
790 }
791
792 #[test]
793 fn test_multiple_blocks() {
794 let mut repairer = CorruptionRepairer::new(default_config());
795 for i in 0..10 {
796 let data: Vec<u8> = (0..32).map(|j| (i * 32 + j) as u8).collect();
797 repairer.add_block(&format!("cid{}", i), &data);
798 }
799 for i in 0..10 {
800 assert!(repairer.verify_block(&format!("cid{}", i)));
801 }
802 }
803
804 #[test]
805 fn test_empty_repairer() {
806 let mut repairer = CorruptionRepairer::new(default_config());
807 assert!(repairer.scan_all().is_empty());
808 assert_eq!(repairer.stats().total_scanned, 0);
809 assert!(repairer.detect_corruption("nonexistent").is_none());
810 assert!(!repairer.verify_block("nonexistent"));
811 }
812
813 #[test]
814 fn test_get_report_after_scan() {
815 let mut repairer = CorruptionRepairer::new(default_config());
816 repairer.add_block("cid1", &[10, 20, 30]);
817 repairer.corrupt_block("cid1", 1, 0xFF).ok();
818 repairer.scan_all();
819 let report = repairer.get_report("cid1");
820 assert!(report.is_some());
821 let r = report.unwrap_or_else(|| panic!("expected report"));
822 assert_eq!(r.cid, "cid1");
823 assert!(r.bytes_affected > 0);
824 }
825
826 #[test]
827 fn test_get_report_nonexistent() {
828 let repairer = CorruptionRepairer::new(default_config());
829 assert!(repairer.get_report("missing").is_none());
830 }
831
832 #[test]
833 fn test_repair_nonexistent_block() {
834 let mut repairer = CorruptionRepairer::new(default_config());
835 let result = repairer.repair_block("missing");
836 assert!(result.is_err());
837 }
838
839 #[test]
840 fn test_repair_already_clean_block() {
841 let mut repairer = CorruptionRepairer::new(default_config());
842 repairer.add_block("cid1", &[1, 2, 3]);
843 let result = repairer.repair_block("cid1");
844 assert_eq!(
845 result.unwrap_or(RepairAction::Unrecoverable),
846 RepairAction::Restored
847 );
848 }
849
850 #[test]
851 fn test_default_repair_config() {
852 let cfg = RepairConfig::default();
853 assert!(cfg.enable_parity);
854 assert_eq!(cfg.parity_block_size, 256);
855 assert_eq!(cfg.max_repair_attempts, 3);
856 assert!(cfg.auto_quarantine_on_fail);
857 }
858
859 #[test]
860 fn test_parity_block_size_zero_fallback() {
861 let parity = CorruptionRepairer::compute_parity(&[1, 2, 3], 0);
862 assert_eq!(parity.len(), 256);
863 }
864
865 #[test]
866 fn test_corruption_report_fields() {
867 let mut repairer = CorruptionRepairer::new(default_config());
868 repairer.add_block("cid1", &sample_data());
869 repairer.corrupt_block("cid1", 20, 0xAA).ok();
870 let reports = repairer.scan_all();
871 assert_eq!(reports.len(), 1);
872 assert!(reports[0].detected_at > 0);
873 assert_eq!(reports[0].repair_action, None);
874 }
875
876 #[test]
877 fn test_repair_updates_report() {
878 let mut repairer = CorruptionRepairer::new(RepairConfig {
879 enable_parity: true,
880 parity_block_size: 256,
881 max_repair_attempts: 3,
882 auto_quarantine_on_fail: true,
883 });
884 let data: Vec<u8> = (0..128).collect();
885 repairer.add_block("cid1", &data);
886 repairer.corrupt_block("cid1", 50, data[50] ^ 0xFF).ok();
887 repairer.scan_all();
888 let _ = repairer.repair_block("cid1");
889 let report = repairer.get_report("cid1");
890 assert!(report.is_some());
891 if let Some(r) = report {
892 if r.repair_action.is_some() {
893 assert_eq!(
894 r.repair_action
895 .as_ref()
896 .unwrap_or(&RepairAction::Unrecoverable),
897 &RepairAction::RebuiltFromParity
898 );
899 }
900 }
901 }
902}