1use std::collections::HashMap;
35
36const FNV_OFFSET_BASIS_64: u64 = 14_695_981_039_346_656_037;
39const FNV_PRIME_64: u64 = 1_099_511_628_211;
40
41const PAGE_SIZE: u64 = 4_096;
43
44#[derive(Clone, Debug)]
48pub struct BlockScanEntry {
49 pub cid: String,
51 pub size_bytes: u64,
53 pub checksum: u32,
55 pub verified: bool,
57 pub metadata: HashMap<String, String>,
59}
60
61impl BlockScanEntry {
62 #[inline]
65 pub fn compute_checksum(data: &[u8]) -> u32 {
66 let mut hash = FNV_OFFSET_BASIS_64;
67 for &byte in data {
68 hash ^= u64::from(byte);
69 hash = hash.wrapping_mul(FNV_PRIME_64);
70 }
71 let hi = (hash >> 32) as u32;
72 let lo = hash as u32;
73 hi ^ lo
74 }
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct IndexEntry {
80 pub cid: String,
82 pub offset: u64,
84 pub size: u64,
86 pub shard: u8,
88 pub flags: u8,
90}
91
92impl IndexEntry {
93 #[inline]
95 pub fn is_pinned(&self) -> bool {
96 self.flags & 0x01 != 0
97 }
98
99 #[inline]
101 pub fn is_compressed(&self) -> bool {
102 self.flags & 0x02 != 0
103 }
104
105 #[inline]
107 pub fn is_encrypted(&self) -> bool {
108 self.flags & 0x04 != 0
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
114pub enum RebuildPhase {
115 Scanning,
117 Verifying,
119 Rebuilding,
121 Validating,
123 Complete,
125 Failed(String),
127}
128
129impl RebuildPhase {
130 pub fn label(&self) -> &str {
132 match self {
133 RebuildPhase::Scanning => "Scanning",
134 RebuildPhase::Verifying => "Verifying",
135 RebuildPhase::Rebuilding => "Rebuilding",
136 RebuildPhase::Validating => "Validating",
137 RebuildPhase::Complete => "Complete",
138 RebuildPhase::Failed(_) => "Failed",
139 }
140 }
141}
142
143#[derive(Clone, Debug)]
145pub struct RebuildConfig {
146 pub shard_count: u8,
148 pub verify_checksums: bool,
150 pub rebuild_missing_only: bool,
152 pub max_errors: usize,
154 pub batch_size: usize,
156}
157
158impl Default for RebuildConfig {
159 fn default() -> Self {
160 Self {
161 shard_count: 16,
162 verify_checksums: true,
163 rebuild_missing_only: false,
164 max_errors: 100,
165 batch_size: 1_000,
166 }
167 }
168}
169
170#[derive(Clone, Debug)]
172pub struct RebuildProgress {
173 pub phase: RebuildPhase,
175 pub blocks_scanned: u64,
177 pub blocks_verified: u64,
179 pub blocks_rebuilt: u64,
181 pub errors: Vec<String>,
183 pub started_at: u64,
185}
186
187impl RebuildProgress {
188 fn new(now: u64) -> Self {
189 Self {
190 phase: RebuildPhase::Scanning,
191 blocks_scanned: 0,
192 blocks_verified: 0,
193 blocks_rebuilt: 0,
194 errors: Vec::new(),
195 started_at: now,
196 }
197 }
198}
199
200#[derive(Clone, Debug)]
202pub struct RebuildStats {
203 pub blocks_scanned: u64,
205 pub blocks_verified: u64,
207 pub blocks_rebuilt: u64,
209 pub error_count: usize,
211 pub index_size: usize,
213 pub phase: String,
215}
216
217pub struct BlockIndexRebuild {
224 pub config: RebuildConfig,
226 pub scan_entries: Vec<BlockScanEntry>,
228 pub index: HashMap<String, IndexEntry>,
230 pub progress: RebuildProgress,
232 pub existing_index: HashMap<String, IndexEntry>,
234}
235
236impl BlockIndexRebuild {
237 pub fn new(config: RebuildConfig) -> Self {
244 Self {
245 config,
246 scan_entries: Vec::new(),
247 index: HashMap::new(),
248 progress: RebuildProgress::new(0),
249 existing_index: HashMap::new(),
250 }
251 }
252
253 pub fn assign_shard(&self, cid: &str) -> u8 {
260 let mut hash = FNV_OFFSET_BASIS_64;
261 for &byte in cid.as_bytes() {
262 hash ^= u64::from(byte);
263 hash = hash.wrapping_mul(FNV_PRIME_64);
264 }
265 let shard_count = if self.config.shard_count == 0 {
266 1u64
267 } else {
268 u64::from(self.config.shard_count)
269 };
270 (hash % shard_count) as u8
271 }
272
273 #[inline]
277 pub fn assign_offset(idx: usize) -> u64 {
278 idx as u64 * PAGE_SIZE
279 }
280
281 pub fn detect_flags(meta: &HashMap<String, String>) -> u8 {
287 let mut flags: u8 = 0;
288 if meta.get("pinned").map(|v| v.as_str()) == Some("true") {
289 flags |= 0x01;
290 }
291 if meta.get("compressed").map(|v| v.as_str()) == Some("true") {
292 flags |= 0x02;
293 }
294 if meta.get("encrypted").map(|v| v.as_str()) == Some("true") {
295 flags |= 0x04;
296 }
297 flags
298 }
299
300 pub fn load_blocks(
310 &mut self,
311 blocks: Vec<(String, Vec<u8>, HashMap<String, String>)>,
312 now: u64,
313 ) {
314 self.progress.phase = RebuildPhase::Scanning;
315 if self.progress.started_at == 0 {
316 self.progress.started_at = now;
317 }
318
319 for (cid, data, metadata) in blocks {
320 let checksum = BlockScanEntry::compute_checksum(&data);
321 let size_bytes = data.len() as u64;
322 let entry = BlockScanEntry {
323 cid,
324 size_bytes,
325 checksum,
326 verified: false,
327 metadata,
328 };
329 self.scan_entries.push(entry);
330 self.progress.blocks_scanned += 1;
331 }
332 }
333
334 pub fn load_existing_index(&mut self, entries: Vec<IndexEntry>) {
340 for entry in entries {
341 self.existing_index.insert(entry.cid.clone(), entry);
342 }
343 }
344
345 pub fn verify_phase(&mut self) {
357 self.progress.phase = RebuildPhase::Verifying;
358
359 for entry in &mut self.scan_entries {
360 if self.config.verify_checksums {
361 entry.verified = true;
368 } else {
369 entry.verified = true;
370 }
371 self.progress.blocks_verified += 1;
372 }
373 }
374
375 pub fn verify_with_data<I>(&mut self, data_iter: I)
381 where
382 I: IntoIterator<Item = (String, Vec<u8>)>,
383 {
384 self.progress.phase = RebuildPhase::Verifying;
385
386 let data_map: HashMap<String, Vec<u8>> = data_iter.into_iter().collect();
387
388 for entry in &mut self.scan_entries {
389 if let Some(raw) = data_map.get(&entry.cid) {
390 let computed = BlockScanEntry::compute_checksum(raw);
391 if computed == entry.checksum {
392 entry.verified = true;
393 self.progress.blocks_verified += 1;
394 } else if self.progress.errors.len() < self.config.max_errors {
395 self.progress.errors.push(format!(
396 "checksum mismatch for {}: stored={:#010x} computed={:#010x}",
397 entry.cid, entry.checksum, computed
398 ));
399 }
400 } else {
401 entry.verified = true;
403 self.progress.blocks_verified += 1;
404 }
405 }
406 }
407
408 pub fn rebuild_phase(&mut self, _now: u64) {
415 self.progress.phase = RebuildPhase::Rebuilding;
416
417 let verified_indices: Vec<usize> = self
419 .scan_entries
420 .iter()
421 .enumerate()
422 .filter(|(_, e)| e.verified)
423 .map(|(i, _)| i)
424 .collect();
425
426 for idx in verified_indices {
427 let entry = &self.scan_entries[idx];
428 let cid = entry.cid.clone();
429
430 if self.config.rebuild_missing_only && self.existing_index.contains_key(&cid) {
431 continue;
432 }
433
434 let shard = self.assign_shard(&cid);
435 let offset = Self::assign_offset(idx);
436 let flags = Self::detect_flags(&entry.metadata);
437 let size = entry.size_bytes;
438
439 let index_entry = IndexEntry {
440 cid: cid.clone(),
441 offset,
442 size,
443 shard,
444 flags,
445 };
446 self.index.insert(cid, index_entry);
447 self.progress.blocks_rebuilt += 1;
448 }
449 }
450
451 pub fn validate_phase(&mut self) {
459 self.progress.phase = RebuildPhase::Validating;
460
461 let mut validation_errors: Vec<String> = Vec::new();
462
463 for entry in &self.scan_entries {
464 if !self.index.contains_key(&entry.cid) {
465 let in_existing = self.existing_index.contains_key(&entry.cid);
467 if !(self.config.rebuild_missing_only && in_existing) {
468 validation_errors.push(format!(
469 "consistency error: scanned block '{}' not found in rebuilt index",
470 entry.cid
471 ));
472 }
473 }
474 }
475
476 for err in &validation_errors {
477 if self.progress.errors.len() < self.config.max_errors {
478 self.progress.errors.push(err.clone());
479 }
480 }
481
482 if validation_errors.is_empty() {
483 self.progress.phase = RebuildPhase::Complete;
484 } else {
485 self.progress.phase = RebuildPhase::Failed(format!(
486 "{} consistency error(s) detected",
487 validation_errors.len()
488 ));
489 }
490 }
491
492 pub fn run_full_rebuild(
504 &mut self,
505 blocks: Vec<(String, Vec<u8>, HashMap<String, String>)>,
506 existing: Vec<IndexEntry>,
507 now: u64,
508 ) -> &RebuildProgress {
509 self.load_blocks(blocks, now);
510 self.load_existing_index(existing);
511 self.verify_phase();
512 self.rebuild_phase(now);
513 self.validate_phase();
514 &self.progress
515 }
516
517 pub fn get_index_entry(&self, cid: &str) -> Option<&IndexEntry> {
523 self.index.get(cid)
524 }
525
526 pub fn index_size(&self) -> usize {
528 self.index.len()
529 }
530
531 pub fn rebuild_stats(&self) -> RebuildStats {
533 RebuildStats {
534 blocks_scanned: self.progress.blocks_scanned,
535 blocks_verified: self.progress.blocks_verified,
536 blocks_rebuilt: self.progress.blocks_rebuilt,
537 error_count: self.progress.errors.len(),
538 index_size: self.index.len(),
539 phase: self.progress.phase.label().to_string(),
540 }
541 }
542
543 pub fn export_index(&self) -> Vec<IndexEntry> {
545 self.index.values().cloned().collect()
546 }
547
548 pub fn scan_entries(&self) -> &[BlockScanEntry] {
550 &self.scan_entries
551 }
552
553 pub fn errors(&self) -> &[String] {
555 &self.progress.errors
556 }
557
558 pub fn progress(&self) -> &RebuildProgress {
560 &self.progress
561 }
562
563 pub fn existing_index(&self) -> &HashMap<String, IndexEntry> {
565 &self.existing_index
566 }
567
568 pub fn reset(&mut self) {
572 self.scan_entries.clear();
573 self.index.clear();
574 self.existing_index.clear();
575 self.progress = RebuildProgress::new(0);
576 }
577
578 pub fn is_finished(&self) -> bool {
580 matches!(
581 self.progress.phase,
582 RebuildPhase::Complete | RebuildPhase::Failed(_)
583 )
584 }
585
586 pub fn is_successful(&self) -> bool {
588 matches!(self.progress.phase, RebuildPhase::Complete) && self.progress.errors.is_empty()
589 }
590
591 pub fn merge_index(&mut self, entries: Vec<IndexEntry>) {
594 for entry in entries {
595 self.index.entry(entry.cid.clone()).or_insert(entry);
596 }
597 }
598
599 pub fn upsert_index_entry(&mut self, entry: IndexEntry) {
601 self.index.insert(entry.cid.clone(), entry);
602 }
603
604 pub fn remove_index_entry(&mut self, cid: &str) -> Option<IndexEntry> {
607 self.index.remove(cid)
608 }
609
610 pub fn unverified_entries(&self) -> Vec<&BlockScanEntry> {
613 self.scan_entries.iter().filter(|e| !e.verified).collect()
614 }
615
616 pub fn all_index_entries(&self) -> Vec<&IndexEntry> {
618 self.index.values().collect()
619 }
620
621 pub fn entries_in_shard(&self, shard: u8) -> Vec<&IndexEntry> {
623 self.index.values().filter(|e| e.shard == shard).collect()
624 }
625
626 pub fn pinned_entries(&self) -> Vec<&IndexEntry> {
628 self.index.values().filter(|e| e.is_pinned()).collect()
629 }
630
631 pub fn compressed_entries(&self) -> Vec<&IndexEntry> {
633 self.index.values().filter(|e| e.is_compressed()).collect()
634 }
635
636 pub fn encrypted_entries(&self) -> Vec<&IndexEntry> {
638 self.index.values().filter(|e| e.is_encrypted()).collect()
639 }
640}
641
642#[cfg(test)]
645mod tests {
646 use std::collections::HashMap;
647 use std::env::temp_dir;
648
649 use crate::block_index_rebuild::{
650 BlockIndexRebuild, BlockScanEntry, IndexEntry, RebuildConfig, RebuildPhase,
651 };
652
653 fn default_config() -> RebuildConfig {
656 RebuildConfig::default()
657 }
658
659 fn make_block(
660 cid: &str,
661 data: &[u8],
662 meta: &[(&str, &str)],
663 ) -> (String, Vec<u8>, HashMap<String, String>) {
664 let metadata = meta
665 .iter()
666 .map(|(k, v)| (k.to_string(), v.to_string()))
667 .collect();
668 (cid.to_string(), data.to_vec(), metadata)
669 }
670
671 fn simple_blocks() -> Vec<(String, Vec<u8>, HashMap<String, String>)> {
672 vec![
673 make_block("QmAlpha", b"hello world", &[]),
674 make_block("QmBeta", b"foo bar baz", &[("pinned", "true")]),
675 make_block("QmGamma", b"compressed content", &[("compressed", "true")]),
676 ]
677 }
678
679 #[test]
682 fn test_default_config_shard_count() {
683 let cfg = default_config();
684 assert_eq!(cfg.shard_count, 16);
685 }
686
687 #[test]
688 fn test_default_config_verify_checksums() {
689 let cfg = default_config();
690 assert!(cfg.verify_checksums);
691 }
692
693 #[test]
694 fn test_default_config_rebuild_missing_only() {
695 let cfg = default_config();
696 assert!(!cfg.rebuild_missing_only);
697 }
698
699 #[test]
700 fn test_default_config_max_errors() {
701 let cfg = default_config();
702 assert_eq!(cfg.max_errors, 100);
703 }
704
705 #[test]
706 fn test_default_config_batch_size() {
707 let cfg = default_config();
708 assert_eq!(cfg.batch_size, 1000);
709 }
710
711 #[test]
714 fn test_checksum_empty_slice() {
715 let cs = BlockScanEntry::compute_checksum(&[]);
716 let h: u64 = 14_695_981_039_346_656_037;
718 let expected = ((h >> 32) as u32) ^ (h as u32);
719 assert_eq!(cs, expected);
720 }
721
722 #[test]
723 fn test_checksum_single_byte() {
724 let cs = BlockScanEntry::compute_checksum(b"A");
725 assert_ne!(cs, 0);
726 }
727
728 #[test]
729 fn test_checksum_deterministic() {
730 let cs1 = BlockScanEntry::compute_checksum(b"hello world");
731 let cs2 = BlockScanEntry::compute_checksum(b"hello world");
732 assert_eq!(cs1, cs2);
733 }
734
735 #[test]
736 fn test_checksum_different_inputs_differ() {
737 let cs1 = BlockScanEntry::compute_checksum(b"foo");
738 let cs2 = BlockScanEntry::compute_checksum(b"bar");
739 assert_ne!(cs1, cs2);
740 }
741
742 #[test]
745 fn test_assign_shard_in_range() {
746 let engine = BlockIndexRebuild::new(default_config());
747 let s = engine.assign_shard("QmFoo");
748 assert!(s < 16);
749 }
750
751 #[test]
752 fn test_assign_shard_deterministic() {
753 let engine = BlockIndexRebuild::new(default_config());
754 assert_eq!(engine.assign_shard("QmBar"), engine.assign_shard("QmBar"));
755 }
756
757 #[test]
758 fn test_assign_shard_single_shard() {
759 let cfg = RebuildConfig {
760 shard_count: 1,
761 ..Default::default()
762 };
763 let engine = BlockIndexRebuild::new(cfg);
764 assert_eq!(engine.assign_shard("anything"), 0);
765 }
766
767 #[test]
770 fn test_assign_offset_zero() {
771 assert_eq!(BlockIndexRebuild::assign_offset(0), 0);
772 }
773
774 #[test]
775 fn test_assign_offset_page_aligned() {
776 assert_eq!(BlockIndexRebuild::assign_offset(1), 4_096);
777 assert_eq!(BlockIndexRebuild::assign_offset(2), 8_192);
778 }
779
780 #[test]
781 fn test_assign_offset_large_index() {
782 let offset = BlockIndexRebuild::assign_offset(1_000);
783 assert_eq!(offset, 1_000 * 4_096);
784 }
785
786 #[test]
789 fn test_detect_flags_empty_meta() {
790 let meta = HashMap::new();
791 assert_eq!(BlockIndexRebuild::detect_flags(&meta), 0x00);
792 }
793
794 #[test]
795 fn test_detect_flags_pinned() {
796 let mut meta = HashMap::new();
797 meta.insert("pinned".to_string(), "true".to_string());
798 assert_eq!(BlockIndexRebuild::detect_flags(&meta), 0x01);
799 }
800
801 #[test]
802 fn test_detect_flags_compressed() {
803 let mut meta = HashMap::new();
804 meta.insert("compressed".to_string(), "true".to_string());
805 assert_eq!(BlockIndexRebuild::detect_flags(&meta), 0x02);
806 }
807
808 #[test]
809 fn test_detect_flags_encrypted() {
810 let mut meta = HashMap::new();
811 meta.insert("encrypted".to_string(), "true".to_string());
812 assert_eq!(BlockIndexRebuild::detect_flags(&meta), 0x04);
813 }
814
815 #[test]
816 fn test_detect_flags_all_three() {
817 let mut meta = HashMap::new();
818 meta.insert("pinned".to_string(), "true".to_string());
819 meta.insert("compressed".to_string(), "true".to_string());
820 meta.insert("encrypted".to_string(), "true".to_string());
821 assert_eq!(BlockIndexRebuild::detect_flags(&meta), 0x07);
822 }
823
824 #[test]
825 fn test_detect_flags_false_values_ignored() {
826 let mut meta = HashMap::new();
827 meta.insert("pinned".to_string(), "false".to_string());
828 meta.insert("compressed".to_string(), "no".to_string());
829 assert_eq!(BlockIndexRebuild::detect_flags(&meta), 0x00);
830 }
831
832 #[test]
835 fn test_load_blocks_count() {
836 let mut engine = BlockIndexRebuild::new(default_config());
837 engine.load_blocks(simple_blocks(), 1_000_000);
838 assert_eq!(engine.scan_entries.len(), 3);
839 assert_eq!(engine.progress.blocks_scanned, 3);
840 }
841
842 #[test]
843 fn test_load_blocks_sets_phase() {
844 let mut engine = BlockIndexRebuild::new(default_config());
845 engine.load_blocks(simple_blocks(), 42);
846 assert_eq!(engine.progress.phase, RebuildPhase::Scanning);
847 }
848
849 #[test]
850 fn test_load_blocks_records_started_at() {
851 let mut engine = BlockIndexRebuild::new(default_config());
852 engine.load_blocks(simple_blocks(), 999);
853 assert_eq!(engine.progress.started_at, 999);
854 }
855
856 #[test]
857 fn test_load_blocks_checksum_stored() {
858 let mut engine = BlockIndexRebuild::new(default_config());
859 let data = b"test data";
860 let expected = BlockScanEntry::compute_checksum(data);
861 engine.load_blocks(vec![make_block("QmTest", data, &[])], 0);
862 assert_eq!(engine.scan_entries[0].checksum, expected);
863 }
864
865 #[test]
866 fn test_load_blocks_size_bytes() {
867 let mut engine = BlockIndexRebuild::new(default_config());
868 let data = b"twelve bytes";
869 engine.load_blocks(vec![make_block("QmSize", data, &[])], 0);
870 assert_eq!(engine.scan_entries[0].size_bytes, data.len() as u64);
871 }
872
873 #[test]
874 fn test_load_blocks_verified_false_initially() {
875 let mut engine = BlockIndexRebuild::new(default_config());
876 engine.load_blocks(vec![make_block("QmV", b"data", &[])], 0);
877 assert!(!engine.scan_entries[0].verified);
882 }
883
884 #[test]
887 fn test_load_existing_index_populates() {
888 let mut engine = BlockIndexRebuild::new(default_config());
889 let entry = IndexEntry {
890 cid: "QmExist".to_string(),
891 offset: 0,
892 size: 100,
893 shard: 3,
894 flags: 0,
895 };
896 engine.load_existing_index(vec![entry.clone()]);
897 assert_eq!(engine.existing_index.len(), 1);
898 assert_eq!(engine.existing_index.get("QmExist"), Some(&entry));
899 }
900
901 #[test]
904 fn test_verify_phase_marks_verified() {
905 let mut engine = BlockIndexRebuild::new(default_config());
906 engine.load_blocks(simple_blocks(), 0);
907 engine.verify_phase();
908 assert!(engine.scan_entries.iter().all(|e| e.verified));
909 }
910
911 #[test]
912 fn test_verify_phase_increments_counter() {
913 let mut engine = BlockIndexRebuild::new(default_config());
914 engine.load_blocks(simple_blocks(), 0);
915 engine.verify_phase();
916 assert_eq!(engine.progress.blocks_verified, 3);
917 }
918
919 #[test]
920 fn test_verify_phase_sets_phase() {
921 let mut engine = BlockIndexRebuild::new(default_config());
922 engine.load_blocks(simple_blocks(), 0);
923 engine.verify_phase();
924 assert_eq!(engine.progress.phase, RebuildPhase::Verifying);
925 }
926
927 #[test]
930 fn test_rebuild_phase_builds_index() {
931 let mut engine = BlockIndexRebuild::new(default_config());
932 engine.load_blocks(simple_blocks(), 0);
933 engine.verify_phase();
934 engine.rebuild_phase(0);
935 assert_eq!(engine.index.len(), 3);
936 assert_eq!(engine.progress.blocks_rebuilt, 3);
937 }
938
939 #[test]
940 fn test_rebuild_phase_shard_range() {
941 let mut engine = BlockIndexRebuild::new(default_config());
942 engine.load_blocks(simple_blocks(), 0);
943 engine.verify_phase();
944 engine.rebuild_phase(0);
945 for entry in engine.index.values() {
946 assert!(entry.shard < 16);
947 }
948 }
949
950 #[test]
951 fn test_rebuild_phase_offset_page_aligned() {
952 let mut engine = BlockIndexRebuild::new(default_config());
953 engine.load_blocks(simple_blocks(), 0);
954 engine.verify_phase();
955 engine.rebuild_phase(0);
956 for entry in engine.index.values() {
957 assert_eq!(entry.offset % 4_096, 0);
958 }
959 }
960
961 #[test]
962 fn test_rebuild_phase_flags_pinned() {
963 let mut engine = BlockIndexRebuild::new(default_config());
964 engine.load_blocks(
965 vec![make_block("QmPinned", b"data", &[("pinned", "true")])],
966 0,
967 );
968 engine.verify_phase();
969 engine.rebuild_phase(0);
970 let entry = engine.index.get("QmPinned").expect("entry missing");
971 assert!(entry.is_pinned());
972 }
973
974 #[test]
975 fn test_rebuild_phase_rebuild_missing_only_skips_existing() {
976 let cfg = RebuildConfig {
977 rebuild_missing_only: true,
978 ..Default::default()
979 };
980 let mut engine = BlockIndexRebuild::new(cfg);
981 let existing = IndexEntry {
982 cid: "QmAlpha".to_string(),
983 offset: 0,
984 size: 11,
985 shard: 0,
986 flags: 0,
987 };
988 engine.load_existing_index(vec![existing]);
989 engine.load_blocks(simple_blocks(), 0);
990 engine.verify_phase();
991 engine.rebuild_phase(0);
992 assert!(!engine.index.contains_key("QmAlpha"));
994 assert!(engine.index.contains_key("QmBeta"));
996 assert!(engine.index.contains_key("QmGamma"));
997 }
998
999 #[test]
1002 fn test_validate_phase_complete_on_success() {
1003 let mut engine = BlockIndexRebuild::new(default_config());
1004 engine.load_blocks(simple_blocks(), 0);
1005 engine.verify_phase();
1006 engine.rebuild_phase(0);
1007 engine.validate_phase();
1008 assert_eq!(engine.progress.phase, RebuildPhase::Complete);
1009 }
1010
1011 #[test]
1012 fn test_validate_phase_failed_when_missing() {
1013 let mut engine = BlockIndexRebuild::new(default_config());
1014 engine.load_blocks(simple_blocks(), 0);
1015 engine.validate_phase();
1017 assert!(matches!(engine.progress.phase, RebuildPhase::Failed(_)));
1018 }
1019
1020 #[test]
1021 fn test_validate_phase_errors_recorded() {
1022 let mut engine = BlockIndexRebuild::new(default_config());
1023 engine.load_blocks(simple_blocks(), 0);
1024 engine.validate_phase();
1025 assert!(!engine.progress.errors.is_empty());
1026 }
1027
1028 #[test]
1031 fn test_run_full_rebuild_complete() {
1032 let mut engine = BlockIndexRebuild::new(default_config());
1033 let progress = engine.run_full_rebuild(simple_blocks(), vec![], 12345);
1034 assert_eq!(progress.phase, RebuildPhase::Complete);
1035 }
1036
1037 #[test]
1038 fn test_run_full_rebuild_counts() {
1039 let mut engine = BlockIndexRebuild::new(default_config());
1040 let progress = engine.run_full_rebuild(simple_blocks(), vec![], 0);
1041 assert_eq!(progress.blocks_scanned, 3);
1042 assert_eq!(progress.blocks_verified, 3);
1043 assert_eq!(progress.blocks_rebuilt, 3);
1044 }
1045
1046 #[test]
1047 fn test_run_full_rebuild_no_errors() {
1048 let mut engine = BlockIndexRebuild::new(default_config());
1049 let progress = engine.run_full_rebuild(simple_blocks(), vec![], 0);
1050 assert!(progress.errors.is_empty());
1051 }
1052
1053 #[test]
1054 fn test_run_full_rebuild_with_existing() {
1055 let cfg = RebuildConfig {
1056 rebuild_missing_only: true,
1057 ..Default::default()
1058 };
1059 let mut engine = BlockIndexRebuild::new(cfg);
1060 let existing = IndexEntry {
1061 cid: "QmAlpha".to_string(),
1062 offset: 0,
1063 size: 11,
1064 shard: 5,
1065 flags: 0,
1066 };
1067 let progress = engine.run_full_rebuild(simple_blocks(), vec![existing], 0);
1068 assert_eq!(progress.phase, RebuildPhase::Complete);
1069 assert_eq!(engine.index_size(), 2);
1070 }
1071
1072 #[test]
1075 fn test_get_index_entry_found() {
1076 let mut engine = BlockIndexRebuild::new(default_config());
1077 engine.run_full_rebuild(simple_blocks(), vec![], 0);
1078 assert!(engine.get_index_entry("QmAlpha").is_some());
1079 }
1080
1081 #[test]
1082 fn test_get_index_entry_not_found() {
1083 let engine = BlockIndexRebuild::new(default_config());
1084 assert!(engine.get_index_entry("QmNonexistent").is_none());
1085 }
1086
1087 #[test]
1088 fn test_index_size_empty() {
1089 let engine = BlockIndexRebuild::new(default_config());
1090 assert_eq!(engine.index_size(), 0);
1091 }
1092
1093 #[test]
1094 fn test_index_size_after_rebuild() {
1095 let mut engine = BlockIndexRebuild::new(default_config());
1096 engine.run_full_rebuild(simple_blocks(), vec![], 0);
1097 assert_eq!(engine.index_size(), 3);
1098 }
1099
1100 #[test]
1103 fn test_rebuild_stats_phase_label() {
1104 let mut engine = BlockIndexRebuild::new(default_config());
1105 engine.run_full_rebuild(simple_blocks(), vec![], 0);
1106 let stats = engine.rebuild_stats();
1107 assert_eq!(stats.phase, "Complete");
1108 }
1109
1110 #[test]
1111 fn test_rebuild_stats_error_count() {
1112 let mut engine = BlockIndexRebuild::new(default_config());
1113 engine.run_full_rebuild(simple_blocks(), vec![], 0);
1114 let stats = engine.rebuild_stats();
1115 assert_eq!(stats.error_count, 0);
1116 }
1117
1118 #[test]
1121 fn test_phase_label_scanning() {
1122 assert_eq!(RebuildPhase::Scanning.label(), "Scanning");
1123 }
1124
1125 #[test]
1126 fn test_phase_label_failed() {
1127 assert_eq!(RebuildPhase::Failed("oops".to_string()).label(), "Failed");
1128 }
1129
1130 #[test]
1131 fn test_phase_label_complete() {
1132 assert_eq!(RebuildPhase::Complete.label(), "Complete");
1133 }
1134
1135 #[test]
1138 fn test_index_entry_is_pinned() {
1139 let e = IndexEntry {
1140 cid: "c".to_string(),
1141 offset: 0,
1142 size: 0,
1143 shard: 0,
1144 flags: 0x01,
1145 };
1146 assert!(e.is_pinned());
1147 assert!(!e.is_compressed());
1148 assert!(!e.is_encrypted());
1149 }
1150
1151 #[test]
1152 fn test_index_entry_is_compressed() {
1153 let e = IndexEntry {
1154 cid: "c".to_string(),
1155 offset: 0,
1156 size: 0,
1157 shard: 0,
1158 flags: 0x02,
1159 };
1160 assert!(e.is_compressed());
1161 }
1162
1163 #[test]
1164 fn test_index_entry_is_encrypted() {
1165 let e = IndexEntry {
1166 cid: "c".to_string(),
1167 offset: 0,
1168 size: 0,
1169 shard: 0,
1170 flags: 0x04,
1171 };
1172 assert!(e.is_encrypted());
1173 }
1174
1175 #[test]
1176 fn test_index_entry_all_flags() {
1177 let e = IndexEntry {
1178 cid: "c".to_string(),
1179 offset: 0,
1180 size: 0,
1181 shard: 0,
1182 flags: 0x07,
1183 };
1184 assert!(e.is_pinned());
1185 assert!(e.is_compressed());
1186 assert!(e.is_encrypted());
1187 }
1188
1189 #[test]
1192 fn test_reset_clears_state() {
1193 let mut engine = BlockIndexRebuild::new(default_config());
1194 engine.run_full_rebuild(simple_blocks(), vec![], 0);
1195 engine.reset();
1196 assert_eq!(engine.scan_entries.len(), 0);
1197 assert_eq!(engine.index.len(), 0);
1198 assert_eq!(engine.progress.blocks_scanned, 0);
1199 }
1200
1201 #[test]
1202 fn test_is_finished_true_after_complete() {
1203 let mut engine = BlockIndexRebuild::new(default_config());
1204 engine.run_full_rebuild(simple_blocks(), vec![], 0);
1205 assert!(engine.is_finished());
1206 }
1207
1208 #[test]
1209 fn test_is_finished_false_before_run() {
1210 let engine = BlockIndexRebuild::new(default_config());
1211 assert!(!engine.is_finished());
1212 }
1213
1214 #[test]
1215 fn test_is_successful_after_clean_run() {
1216 let mut engine = BlockIndexRebuild::new(default_config());
1217 engine.run_full_rebuild(simple_blocks(), vec![], 0);
1218 assert!(engine.is_successful());
1219 }
1220
1221 #[test]
1222 fn test_merge_index_does_not_overwrite() {
1223 let mut engine = BlockIndexRebuild::new(default_config());
1224 engine.run_full_rebuild(vec![make_block("QmAlpha", b"hello world", &[])], vec![], 0);
1225 let old_offset = engine.index.get("QmAlpha").expect("missing").offset;
1226 let incoming = IndexEntry {
1227 cid: "QmAlpha".to_string(),
1228 offset: 9_999_999,
1229 size: 0,
1230 shard: 0,
1231 flags: 0,
1232 };
1233 engine.merge_index(vec![incoming]);
1234 assert_eq!(
1236 engine.index.get("QmAlpha").expect("missing").offset,
1237 old_offset
1238 );
1239 }
1240
1241 #[test]
1242 fn test_upsert_index_entry_overwrites() {
1243 let mut engine = BlockIndexRebuild::new(default_config());
1244 engine.run_full_rebuild(vec![make_block("QmU", b"data", &[])], vec![], 0);
1245 let new_entry = IndexEntry {
1246 cid: "QmU".to_string(),
1247 offset: 77_000,
1248 size: 42,
1249 shard: 7,
1250 flags: 0x03,
1251 };
1252 engine.upsert_index_entry(new_entry.clone());
1253 assert_eq!(engine.index.get("QmU"), Some(&new_entry));
1254 }
1255
1256 #[test]
1257 fn test_remove_index_entry() {
1258 let mut engine = BlockIndexRebuild::new(default_config());
1259 engine.run_full_rebuild(simple_blocks(), vec![], 0);
1260 let removed = engine.remove_index_entry("QmAlpha");
1261 assert!(removed.is_some());
1262 assert!(engine.get_index_entry("QmAlpha").is_none());
1263 }
1264
1265 #[test]
1266 fn test_pinned_entries_filter() {
1267 let mut engine = BlockIndexRebuild::new(default_config());
1268 engine.run_full_rebuild(simple_blocks(), vec![], 0);
1269 let pinned = engine.pinned_entries();
1270 assert!(pinned.iter().any(|e| e.cid == "QmBeta"));
1272 }
1273
1274 #[test]
1275 fn test_compressed_entries_filter() {
1276 let mut engine = BlockIndexRebuild::new(default_config());
1277 engine.run_full_rebuild(simple_blocks(), vec![], 0);
1278 let compressed = engine.compressed_entries();
1279 assert!(compressed.iter().any(|e| e.cid == "QmGamma"));
1281 }
1282
1283 #[test]
1284 fn test_entries_in_shard_returns_subset() {
1285 let mut engine = BlockIndexRebuild::new(default_config());
1286 engine.run_full_rebuild(simple_blocks(), vec![], 0);
1287 let total: usize = (0..16_u8).map(|s| engine.entries_in_shard(s).len()).sum();
1289 assert_eq!(total, engine.index_size());
1290 }
1291
1292 #[test]
1293 fn test_export_index_length() {
1294 let mut engine = BlockIndexRebuild::new(default_config());
1295 engine.run_full_rebuild(simple_blocks(), vec![], 0);
1296 assert_eq!(engine.export_index().len(), 3);
1297 }
1298
1299 #[test]
1300 fn test_verify_with_data_detects_mismatch() {
1301 let mut engine = BlockIndexRebuild::new(default_config());
1302 engine.progress.started_at = 1;
1304 engine.progress.blocks_scanned = 1;
1305 engine
1307 .scan_entries
1308 .push(crate::block_index_rebuild::BlockScanEntry {
1309 cid: "QmTampered".to_string(),
1310 size_bytes: 5,
1311 checksum: 0xDEAD_BEEF, verified: false,
1313 metadata: HashMap::new(),
1314 });
1315 engine.verify_with_data(vec![("QmTampered".to_string(), b"hello".to_vec())]);
1317 let entry = &engine.scan_entries[0];
1319 assert!(!entry.verified);
1320 assert!(!engine.progress.errors.is_empty());
1321 }
1322
1323 #[test]
1324 fn test_unverified_entries_empty_after_verify() {
1325 let mut engine = BlockIndexRebuild::new(default_config());
1326 engine.load_blocks(simple_blocks(), 0);
1327 engine.verify_phase();
1328 assert_eq!(engine.unverified_entries().len(), 0);
1329 }
1330
1331 #[test]
1334 fn test_temp_dir_accessible() {
1335 let tmp = temp_dir();
1336 assert!(tmp.exists());
1337 }
1338
1339 #[test]
1340 fn test_large_batch_rebuild() {
1341 let mut engine = BlockIndexRebuild::new(default_config());
1342 let blocks: Vec<_> = (0..200)
1343 .map(|i| {
1344 make_block(
1345 &format!("QmBlock{i:04}"),
1346 format!("data for block {i}").as_bytes(),
1347 &[],
1348 )
1349 })
1350 .collect();
1351 let progress = engine.run_full_rebuild(blocks, vec![], 0);
1352 assert_eq!(progress.blocks_scanned, 200);
1353 assert_eq!(progress.blocks_rebuilt, 200);
1354 assert_eq!(progress.phase, RebuildPhase::Complete);
1355 }
1356
1357 #[test]
1358 fn test_max_errors_cap() {
1359 let cfg = RebuildConfig {
1360 max_errors: 5,
1361 ..Default::default()
1362 };
1363 let mut engine = BlockIndexRebuild::new(cfg);
1364 let blocks: Vec<_> = (0..20)
1366 .map(|i| make_block(&format!("QmX{i}"), b"d", &[]))
1367 .collect();
1368 engine.load_blocks(blocks, 0);
1369 engine.validate_phase();
1371 assert!(engine.progress.errors.len() <= 5);
1373 }
1374}