1use std::collections::HashMap;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum DedupStage {
22 ExactHash,
24 ChunkHash,
26 Similarity,
28}
29
30#[derive(Debug, Clone)]
32pub struct DedupResult {
33 pub cid: String,
35 pub is_duplicate: bool,
37 pub duplicate_of: Option<String>,
39 pub stage_detected: Option<DedupStage>,
41 pub space_saved: u64,
43}
44
45#[derive(Debug, Clone)]
47pub struct PipelineConfig {
48 pub stages: Vec<DedupStage>,
50 pub chunk_size: usize,
52 pub min_chunk_size: usize,
55 pub similarity_threshold: f64,
59 pub fingerprint_bits: u32,
62}
63
64impl Default for PipelineConfig {
65 fn default() -> Self {
66 Self {
67 stages: vec![
68 DedupStage::ExactHash,
69 DedupStage::ChunkHash,
70 DedupStage::Similarity,
71 ],
72 chunk_size: 4096,
73 min_chunk_size: 512,
74 similarity_threshold: 0.9,
75 fingerprint_bits: 64,
76 }
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct BlockEntry {
83 pub cid: String,
85 pub data: Vec<u8>,
87 pub full_hash: u64,
89 pub chunks: Vec<u64>,
91 pub fingerprint: u64,
93 pub size: u64,
95}
96
97#[derive(Debug, Clone, Default)]
99pub struct DedupPipelineStats {
100 pub total_processed: u64,
102 pub exact_duplicates: u64,
104 pub chunk_duplicates: u64,
106 pub similarity_duplicates: u64,
108 pub bytes_saved: u64,
110 pub unique_blocks: u64,
112}
113
114pub struct DeduplicationPipeline {
134 config: PipelineConfig,
135 index: HashMap<u64, String>,
137 chunk_index: HashMap<u64, String>,
139 fingerprints: Vec<(u64, String)>,
141 stats: DedupPipelineStats,
142}
143
144impl DeduplicationPipeline {
145 pub fn new(config: PipelineConfig) -> Self {
151 Self {
152 config,
153 index: HashMap::new(),
154 chunk_index: HashMap::new(),
155 fingerprints: Vec::new(),
156 stats: DedupPipelineStats::default(),
157 }
158 }
159
160 pub fn process_block(&mut self, cid: &str, data: &[u8]) -> DedupResult {
171 self.stats.total_processed += 1;
172
173 let full_hash = Self::compute_full_hash(data);
174 let bits = self.config.fingerprint_bits.min(64);
175
176 for stage in self.config.stages.clone() {
178 match stage {
179 DedupStage::ExactHash => {
180 if let Some(original_cid) = self.index.get(&full_hash).cloned() {
181 let saved = data.len() as u64;
182 self.stats.exact_duplicates += 1;
183 self.stats.bytes_saved += saved;
184 return DedupResult {
185 cid: cid.to_string(),
186 is_duplicate: true,
187 duplicate_of: Some(original_cid),
188 stage_detected: Some(DedupStage::ExactHash),
189 space_saved: saved,
190 };
191 }
192 }
193
194 DedupStage::ChunkHash => {
195 let chunks = Self::compute_chunks(
196 data,
197 self.config.chunk_size,
198 self.config.min_chunk_size,
199 );
200 if let Some(original_cid) = self.find_chunk_duplicate(&chunks) {
201 let saved = data.len() as u64;
202 self.stats.chunk_duplicates += 1;
203 self.stats.bytes_saved += saved;
204 self.index_chunks(&chunks, cid);
206 return DedupResult {
207 cid: cid.to_string(),
208 is_duplicate: true,
209 duplicate_of: Some(original_cid),
210 stage_detected: Some(DedupStage::ChunkHash),
211 space_saved: saved,
212 };
213 }
214 self.index_chunks(&chunks, cid);
216 }
217
218 DedupStage::Similarity => {
219 let fingerprint = Self::compute_simhash(data, bits);
220 let threshold = self.config.similarity_threshold;
221 if let Some(original_cid) = self
222 .check_similarity(fingerprint, threshold)
223 .map(str::to_string)
224 {
225 let saved = data.len() as u64;
226 self.stats.similarity_duplicates += 1;
227 self.stats.bytes_saved += saved;
228 self.fingerprints.push((fingerprint, cid.to_string()));
230 return DedupResult {
231 cid: cid.to_string(),
232 is_duplicate: true,
233 duplicate_of: Some(original_cid),
234 stage_detected: Some(DedupStage::Similarity),
235 space_saved: saved,
236 };
237 }
238 self.fingerprints.push((fingerprint, cid.to_string()));
239 }
240 }
241 }
242
243 self.index.insert(full_hash, cid.to_string());
245 self.stats.unique_blocks += 1;
246
247 DedupResult {
248 cid: cid.to_string(),
249 is_duplicate: false,
250 duplicate_of: None,
251 stage_detected: None,
252 space_saved: 0,
253 }
254 }
255
256 pub fn compute_full_hash(data: &[u8]) -> u64 {
262 fnv1a_64(data)
263 }
264
265 pub fn compute_chunks(data: &[u8], chunk_size: usize, min_size: usize) -> Vec<u64> {
270 if data.is_empty() {
271 return Vec::new();
272 }
273
274 let effective_chunk = chunk_size.max(1);
276 let effective_min = min_size.min(effective_chunk);
277
278 let mut hashes: Vec<u64> = Vec::new();
279 let mut offset = 0usize;
280
281 while offset < data.len() {
282 let remaining = data.len() - offset;
283
284 if remaining < effective_min && !hashes.is_empty() {
287 let tail_start = offset.saturating_sub(effective_chunk);
289 let merged_hash = fnv1a_64(&data[tail_start..]);
290 if let Some(last) = hashes.last_mut() {
291 *last = merged_hash;
292 }
293 break;
294 }
295
296 let end = (offset + effective_chunk).min(data.len());
297 hashes.push(fnv1a_64(&data[offset..end]));
298 offset = end;
299 }
300
301 hashes
302 }
303
304 pub fn compute_simhash(data: &[u8], bits: u32) -> u64 {
310 let bits = bits.min(64) as usize;
311 if data.is_empty() || bits == 0 {
312 return 0;
313 }
314
315 let mut bit_weights = vec![0i64; bits];
317 let window = 8usize;
318
319 let num_windows = data.len().saturating_sub(window).saturating_add(1);
321 let actual_windows = num_windows.max(1);
322
323 for start in 0..actual_windows {
324 let end = (start + window).min(data.len());
325 let h = fnv1a_64(&data[start..end]);
326
327 for (bit, weight) in bit_weights.iter_mut().enumerate().take(bits) {
330 if (h >> bit) & 1 == 1 {
331 *weight += 1;
332 } else {
333 *weight -= 1;
334 }
335 }
336 }
337
338 let mut fingerprint = 0u64;
340 for (bit, &weight) in bit_weights.iter().enumerate().take(bits) {
341 if weight > 0 {
342 fingerprint |= 1u64 << bit;
343 }
344 }
345
346 fingerprint
347 }
348
349 #[inline]
352 pub fn hamming_distance(a: u64, b: u64) -> u32 {
353 (a ^ b).count_ones()
354 }
355
356 #[inline]
362 pub fn similarity_from_hamming(distance: u32, bits: u32) -> f64 {
363 if bits == 0 {
364 return 0.0;
365 }
366 1.0 - (distance as f64 / bits as f64)
367 }
368
369 pub fn check_similarity(&self, fingerprint: u64, threshold: f64) -> Option<&str> {
374 let bits = self.config.fingerprint_bits.min(64);
375 for (stored_fp, stored_cid) in &self.fingerprints {
376 let distance = Self::hamming_distance(fingerprint, *stored_fp);
377 let similarity = Self::similarity_from_hamming(distance, bits);
378 if similarity >= threshold {
379 return Some(stored_cid.as_str());
380 }
381 }
382 None
383 }
384
385 pub fn remove_block(&mut self, cid: &str) -> bool {
393 let mut removed = false;
394
395 self.index.retain(|_, v| {
397 if v.as_str() == cid {
398 removed = true;
399 false
400 } else {
401 true
402 }
403 });
404
405 self.chunk_index.retain(|_, v| {
407 if v.as_str() == cid {
408 removed = true;
409 false
410 } else {
411 true
412 }
413 });
414
415 let before = self.fingerprints.len();
417 self.fingerprints.retain(|(_, c)| c.as_str() != cid);
418 if self.fingerprints.len() < before {
419 removed = true;
420 }
421
422 removed
423 }
424
425 pub fn index_size(&self) -> usize {
427 self.index.len()
428 }
429
430 pub fn stats(&self) -> &DedupPipelineStats {
432 &self.stats
433 }
434
435 fn find_chunk_duplicate(&self, chunks: &[u64]) -> Option<String> {
442 for &ch in chunks {
443 if let Some(original_cid) = self.chunk_index.get(&ch) {
444 return Some(original_cid.clone());
445 }
446 }
447 None
448 }
449
450 fn index_chunks(&mut self, chunks: &[u64], cid: &str) {
454 for &ch in chunks {
455 self.chunk_index
456 .entry(ch)
457 .or_insert_with(|| cid.to_string());
458 }
459 }
460}
461
462#[inline]
468fn fnv1a_64(data: &[u8]) -> u64 {
469 const OFFSET_BASIS: u64 = 14695981039346656037;
470 const PRIME: u64 = 1099511628211;
471 let mut hash = OFFSET_BASIS;
472 for &byte in data {
473 hash ^= byte as u64;
474 hash = hash.wrapping_mul(PRIME);
475 }
476 hash
477}
478
479#[cfg(test)]
484mod tests {
485 use super::*;
486
487 fn default_pipeline() -> DeduplicationPipeline {
490 DeduplicationPipeline::new(PipelineConfig::default())
491 }
492
493 fn exact_only_pipeline() -> DeduplicationPipeline {
494 DeduplicationPipeline::new(PipelineConfig {
495 stages: vec![DedupStage::ExactHash],
496 ..PipelineConfig::default()
497 })
498 }
499
500 fn chunk_only_pipeline() -> DeduplicationPipeline {
501 DeduplicationPipeline::new(PipelineConfig {
502 stages: vec![DedupStage::ChunkHash],
503 ..PipelineConfig::default()
504 })
505 }
506
507 fn similarity_only_pipeline() -> DeduplicationPipeline {
508 DeduplicationPipeline::new(PipelineConfig {
509 stages: vec![DedupStage::Similarity],
510 similarity_threshold: 0.8,
511 ..PipelineConfig::default()
512 })
513 }
514
515 #[test]
518 fn test_unique_block_accepted() {
519 let mut p = default_pipeline();
520 let r = p.process_block("cid-1", b"unique data block");
521 assert!(!r.is_duplicate);
522 assert!(r.duplicate_of.is_none());
523 assert!(r.stage_detected.is_none());
524 assert_eq!(r.space_saved, 0);
525 assert_eq!(r.cid, "cid-1");
526 }
527
528 #[test]
531 fn test_exact_duplicate_detected() {
532 let mut p = exact_only_pipeline();
533 p.process_block("cid-1", b"identical block data");
534 let r = p.process_block("cid-2", b"identical block data");
535 assert!(r.is_duplicate);
536 assert_eq!(r.duplicate_of.as_deref(), Some("cid-1"));
537 assert_eq!(r.stage_detected, Some(DedupStage::ExactHash));
538 assert_eq!(r.space_saved, b"identical block data".len() as u64);
539 }
540
541 #[test]
544 fn test_different_blocks_not_exact_duplicate() {
545 let mut p = exact_only_pipeline();
546 p.process_block("cid-1", b"block A");
547 let r = p.process_block("cid-2", b"block B");
548 assert!(!r.is_duplicate);
549 }
550
551 #[test]
554 fn test_three_way_exact_duplicate() {
555 let mut p = exact_only_pipeline();
556 p.process_block("cid-1", b"shared data");
557 let r2 = p.process_block("cid-2", b"shared data");
558 let r3 = p.process_block("cid-3", b"shared data");
559 assert!(r2.is_duplicate);
560 assert!(r3.is_duplicate);
561 assert_eq!(r2.duplicate_of.as_deref(), Some("cid-1"));
563 assert_eq!(r3.duplicate_of.as_deref(), Some("cid-1"));
564 }
565
566 #[test]
569 fn test_chunk_duplicate_detected() {
570 let mut p = chunk_only_pipeline();
571 let block: Vec<u8> = (0u8..=255).cycle().take(8192).collect();
573 p.process_block("cid-1", &block);
574 let r = p.process_block("cid-2", &block);
576 assert!(r.is_duplicate);
577 assert_eq!(r.stage_detected, Some(DedupStage::ChunkHash));
578 }
579
580 #[test]
583 fn test_chunk_partial_overlap() {
584 let mut p = chunk_only_pipeline();
585 let block_a: Vec<u8> = vec![0xABu8; 8192];
586 p.process_block("cid-1", &block_a);
587
588 let mut block_b = block_a[..4096].to_vec();
590 block_b.extend_from_slice(&[0xCDu8; 4096]);
591 let r = p.process_block("cid-2", &block_b);
592 assert!(r.is_duplicate);
593 assert_eq!(r.stage_detected, Some(DedupStage::ChunkHash));
594 assert_eq!(r.duplicate_of.as_deref(), Some("cid-1"));
595 }
596
597 #[test]
600 fn test_compute_full_hash_deterministic() {
601 let data = b"deterministic hash input";
602 let h1 = DeduplicationPipeline::compute_full_hash(data);
603 let h2 = DeduplicationPipeline::compute_full_hash(data);
604 assert_eq!(h1, h2);
605 }
606
607 #[test]
610 fn test_compute_full_hash_differs() {
611 let h1 = DeduplicationPipeline::compute_full_hash(b"aaa");
612 let h2 = DeduplicationPipeline::compute_full_hash(b"bbb");
613 assert_ne!(h1, h2);
614 }
615
616 #[test]
619 fn test_compute_full_hash_empty() {
620 let h = DeduplicationPipeline::compute_full_hash(b"");
622 assert_eq!(h, 14695981039346656037u64);
624 }
625
626 #[test]
629 fn test_compute_chunks_basic() {
630 let data: Vec<u8> = (0u8..255).collect();
631 let chunks = DeduplicationPipeline::compute_chunks(&data, 64, 16);
632 assert!(!chunks.is_empty());
634 assert!(chunks.len() <= 4);
635 }
636
637 #[test]
640 fn test_compute_chunks_empty() {
641 let chunks = DeduplicationPipeline::compute_chunks(&[], 64, 16);
642 assert!(chunks.is_empty());
643 }
644
645 #[test]
648 fn test_compute_chunks_single_chunk() {
649 let data = vec![0x42u8; 100];
650 let chunks = DeduplicationPipeline::compute_chunks(&data, 4096, 512);
651 assert_eq!(chunks.len(), 1);
652 }
653
654 #[test]
657 fn test_hamming_distance_identical() {
658 assert_eq!(
659 DeduplicationPipeline::hamming_distance(
660 0xDEAD_BEEF_CAFE_BABEu64,
661 0xDEAD_BEEF_CAFE_BABEu64
662 ),
663 0
664 );
665 }
666
667 #[test]
670 fn test_hamming_distance_all_bits() {
671 assert_eq!(DeduplicationPipeline::hamming_distance(0u64, u64::MAX), 64);
672 }
673
674 #[test]
677 fn test_hamming_distance_single_bit() {
678 assert_eq!(DeduplicationPipeline::hamming_distance(0u64, 1u64), 1);
679 assert_eq!(DeduplicationPipeline::hamming_distance(0u64, 1u64 << 63), 1);
680 }
681
682 #[test]
685 fn test_similarity_from_hamming_exact() {
686 let s = DeduplicationPipeline::similarity_from_hamming(0, 64);
688 assert!((s - 1.0).abs() < 1e-10);
689
690 let s = DeduplicationPipeline::similarity_from_hamming(64, 64);
692 assert!((s - 0.0).abs() < 1e-10);
693
694 let s = DeduplicationPipeline::similarity_from_hamming(32, 64);
696 assert!((s - 0.5).abs() < 1e-10);
697 }
698
699 #[test]
702 fn test_similarity_from_hamming_zero_bits() {
703 let s = DeduplicationPipeline::similarity_from_hamming(0, 0);
704 assert_eq!(s, 0.0);
705 }
706
707 #[test]
710 fn test_compute_simhash_deterministic() {
711 let data = b"simhash test data";
712 let f1 = DeduplicationPipeline::compute_simhash(data, 64);
713 let f2 = DeduplicationPipeline::compute_simhash(data, 64);
714 assert_eq!(f1, f2);
715 }
716
717 #[test]
720 fn test_compute_simhash_empty() {
721 let f = DeduplicationPipeline::compute_simhash(b"", 64);
723 assert_eq!(f, 0);
724 }
725
726 #[test]
729 fn test_similarity_duplicate_detected() {
730 let mut p = similarity_only_pipeline();
731 let base: Vec<u8> = (0u8..=200).cycle().take(500).collect();
732 p.process_block("cid-1", &base);
733
734 let r = p.process_block("cid-2", &base);
736 assert!(r.is_duplicate);
737 assert_eq!(r.stage_detected, Some(DedupStage::Similarity));
738 assert_eq!(r.duplicate_of.as_deref(), Some("cid-1"));
739 }
740
741 #[test]
744 fn test_similarity_threshold_below_not_duplicate() {
745 let mut p = DeduplicationPipeline::new(PipelineConfig {
746 stages: vec![DedupStage::Similarity],
747 similarity_threshold: 1.0,
749 ..PipelineConfig::default()
750 });
751 let data_a = b"alpha alpha alpha alpha alpha alpha alpha alpha alpha alpha";
752 let data_b = b"beta beta beta beta beta beta beta beta beta beta beta beta";
753 p.process_block("cid-1", data_a);
754 let r = p.process_block("cid-2", data_b);
755 let _ = r.is_duplicate;
759 }
760
761 #[test]
764 fn test_multi_stage_exact_precedence() {
765 let mut p = default_pipeline();
766 let data = b"multi-stage block";
767 p.process_block("cid-1", data);
768 let r = p.process_block("cid-2", data);
769 assert_eq!(r.stage_detected, Some(DedupStage::ExactHash));
771 }
772
773 #[test]
776 fn test_stats_total_processed() {
777 let mut p = default_pipeline();
778 p.process_block("c1", b"a");
779 p.process_block("c2", b"b");
780 p.process_block("c3", b"c");
781 assert_eq!(p.stats().total_processed, 3);
782 }
783
784 #[test]
787 fn test_stats_exact_duplicates() {
788 let mut p = exact_only_pipeline();
789 p.process_block("c1", b"same");
790 p.process_block("c2", b"same");
791 p.process_block("c3", b"same");
792 assert_eq!(p.stats().exact_duplicates, 2);
793 assert_eq!(p.stats().unique_blocks, 1);
794 }
795
796 #[test]
799 fn test_stats_bytes_saved() {
800 let data = b"12345678901234567890"; let mut p = exact_only_pipeline();
802 p.process_block("c1", data);
803 p.process_block("c2", data);
804 p.process_block("c3", data);
805 assert_eq!(p.stats().bytes_saved, 40); }
807
808 #[test]
811 fn test_stats_unique_blocks() {
812 let mut p = exact_only_pipeline();
813 p.process_block("c1", b"alpha");
814 p.process_block("c2", b"beta");
815 p.process_block("c3", b"gamma");
816 assert_eq!(p.stats().unique_blocks, 3);
817 assert_eq!(p.stats().exact_duplicates, 0);
818 }
819
820 #[test]
823 fn test_index_size() {
824 let mut p = exact_only_pipeline();
825 assert_eq!(p.index_size(), 0);
826 p.process_block("c1", b"a");
827 p.process_block("c2", b"b");
828 assert_eq!(p.index_size(), 2);
829 p.process_block("c3", b"a");
831 assert_eq!(p.index_size(), 2);
832 }
833
834 #[test]
837 fn test_remove_block_primary_index() {
838 let mut p = exact_only_pipeline();
839 p.process_block("c1", b"removable data");
840 assert_eq!(p.index_size(), 1);
841 let removed = p.remove_block("c1");
842 assert!(removed);
843 assert_eq!(p.index_size(), 0);
844 }
845
846 #[test]
849 fn test_remove_block_not_found() {
850 let mut p = default_pipeline();
851 p.process_block("c1", b"existing block");
852 let removed = p.remove_block("c-nonexistent");
853 assert!(!removed);
854 }
855
856 #[test]
859 fn test_remove_block_allows_reinsertion() {
860 let mut p = exact_only_pipeline();
861 let data = b"reinsert me";
862 p.process_block("c1", data);
863 p.remove_block("c1");
864
865 let r = p.process_block("c2", data);
867 assert!(!r.is_duplicate);
868 }
869
870 #[test]
873 fn test_all_stages_enabled() {
874 let mut p = default_pipeline();
875
876 let r1 = p.process_block("cid-a", b"hello from stage one");
878 assert!(!r1.is_duplicate);
879
880 let r2 = p.process_block("cid-b", b"hello from stage one");
882 assert!(r2.is_duplicate);
883 assert_eq!(r2.stage_detected, Some(DedupStage::ExactHash));
884
885 let r3 = p.process_block("cid-c", b"completely different data");
887 assert!(!r3.is_duplicate);
888
889 assert_eq!(p.stats().total_processed, 3);
890 assert_eq!(p.stats().unique_blocks, 2);
891 assert_eq!(p.stats().exact_duplicates, 1);
892 }
893
894 #[test]
897 fn test_chunk_index_populated() {
898 let mut p = chunk_only_pipeline();
899 let block: Vec<u8> = vec![0x77u8; 8192];
900 let r1 = p.process_block("c1", &block);
901 assert!(!r1.is_duplicate);
902 let r2 = p.process_block("c2", &block);
904 assert!(r2.is_duplicate);
905 }
906
907 #[test]
910 fn test_empty_block_no_panic() {
911 let mut p = default_pipeline();
912 let r = p.process_block("empty", b"");
913 assert_eq!(r.cid, "empty");
915 }
916
917 #[test]
920 fn test_stats_similarity_duplicates() {
921 let mut p = similarity_only_pipeline();
922 let data: Vec<u8> = (0u8..=127).cycle().take(256).collect();
923 p.process_block("c1", &data);
924 p.process_block("c2", &data); assert_eq!(p.stats().similarity_duplicates, 1);
926 }
927
928 #[test]
931 fn test_remove_block_removes_fingerprint() {
932 let mut p = similarity_only_pipeline();
933 let data: Vec<u8> = (0u8..=200).cycle().take(400).collect();
934 p.process_block("c1", &data);
935 let removed = p.remove_block("c1");
936 assert!(removed);
937
938 let r = p.process_block("c2", &data);
940 assert!(!r.is_duplicate);
941 }
942}