1use crate::error::{CompressionError, CompressionResult};
51use crate::smart_compressor::{ObjectCategory, ObjectType};
52use serde::{Deserialize, Serialize};
53use std::collections::HashMap;
54use std::time::Instant;
55use tracing::{debug, info, trace, warn};
56
57pub const MAX_CHAIN_DEPTH: u32 = 10;
59
60pub const SIMILARITY_THRESHOLD: f64 = 0.80;
62
63pub const MIN_SPACE_SAVINGS: f64 = 0.10;
65
66pub const MAX_RECONSTRUCTION_TIME_MS: u64 = 100;
68
69#[derive(Debug, Clone, Copy)]
71pub struct DeltaConfig {
72 pub similarity_threshold: f64,
74 pub min_space_savings: f64,
76 pub max_chain_depth: u32,
78}
79
80impl DeltaConfig {
81 pub fn for_object_type(obj_type: ObjectType) -> Self {
83 match obj_type.category() {
84 ObjectCategory::Image => DeltaConfig {
86 similarity_threshold: 0.85,
87 min_space_savings: 0.15,
88 max_chain_depth: 5,
89 },
90 ObjectCategory::Video => DeltaConfig {
92 similarity_threshold: 0.95,
93 min_space_savings: 0.05,
94 max_chain_depth: 3,
95 },
96 ObjectCategory::Audio => DeltaConfig {
98 similarity_threshold: 0.90,
99 min_space_savings: 0.10,
100 max_chain_depth: 5,
101 },
102 ObjectCategory::Text | ObjectCategory::Document => DeltaConfig {
104 similarity_threshold: 0.70,
105 min_space_savings: 0.10,
106 max_chain_depth: MAX_CHAIN_DEPTH,
107 },
108 ObjectCategory::CreativeProject => DeltaConfig {
112 similarity_threshold: 0.70, min_space_savings: 0.10,
114 max_chain_depth: 10, },
116 ObjectCategory::Office => DeltaConfig {
118 similarity_threshold: 0.75,
119 min_space_savings: 0.10,
120 max_chain_depth: MAX_CHAIN_DEPTH,
121 },
122 ObjectCategory::MlSpecialized => DeltaConfig {
125 similarity_threshold: 0.75, min_space_savings: 0.10,
127 max_chain_depth: 8, },
129 ObjectCategory::Database => DeltaConfig {
131 similarity_threshold: 0.80,
132 min_space_savings: 0.10,
133 max_chain_depth: 5,
134 },
135 ObjectCategory::GitObject => DeltaConfig {
137 similarity_threshold: 0.75,
138 min_space_savings: 0.10,
139 max_chain_depth: MAX_CHAIN_DEPTH,
140 },
141 ObjectCategory::Archive | ObjectCategory::Unknown => DeltaConfig {
143 similarity_threshold: SIMILARITY_THRESHOLD,
144 min_space_savings: MIN_SPACE_SAVINGS,
145 max_chain_depth: MAX_CHAIN_DEPTH,
146 },
147 }
148 }
149
150 pub fn default_config() -> Self {
152 DeltaConfig {
153 similarity_threshold: SIMILARITY_THRESHOLD,
154 min_space_savings: MIN_SPACE_SAVINGS,
155 max_chain_depth: MAX_CHAIN_DEPTH,
156 }
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
164pub struct DeltaMetadata {
165 pub base_hash: String,
167 pub chain_depth: u32,
169 pub original_size: usize,
171 pub delta_size: usize,
173 pub is_delta: bool,
175 pub similarity: f64,
177 pub space_savings: f64,
179}
180
181impl DeltaMetadata {
182 pub fn full(hash: String, size: usize) -> Self {
184 DeltaMetadata {
185 base_hash: hash,
186 chain_depth: 0,
187 original_size: size,
188 delta_size: size,
189 is_delta: false,
190 similarity: 1.0,
191 space_savings: 0.0,
192 }
193 }
194
195 pub fn delta(
197 base_hash: String,
198 base_chain_depth: u32,
199 original_size: usize,
200 delta_size: usize,
201 similarity: f64,
202 ) -> Self {
203 let space_savings = if original_size > 0 {
204 (original_size - delta_size) as f64 / original_size as f64
205 } else {
206 0.0
207 };
208
209 DeltaMetadata {
210 base_hash,
211 chain_depth: base_chain_depth + 1,
212 original_size,
213 delta_size,
214 is_delta: true,
215 similarity,
216 space_savings,
217 }
218 }
219
220 pub fn is_at_max_depth(&self) -> bool {
222 self.chain_depth >= MAX_CHAIN_DEPTH
223 }
224
225 pub fn compression_ratio(&self) -> f64 {
227 if self.original_size == 0 {
228 1.0
229 } else {
230 self.delta_size as f64 / self.original_size as f64
231 }
232 }
233}
234
235#[derive(Debug, Clone)]
239pub struct DeltaChainTracker {
240 metadata: HashMap<String, DeltaMetadata>,
242}
243
244impl DeltaChainTracker {
245 pub fn new() -> Self {
247 DeltaChainTracker {
248 metadata: HashMap::new(),
249 }
250 }
251
252 pub fn register(&mut self, hash: String, metadata: DeltaMetadata) {
254 self.metadata.insert(hash, metadata);
255 }
256
257 pub fn get(&self, hash: &str) -> Option<&DeltaMetadata> {
259 self.metadata.get(hash)
260 }
261
262 pub fn find_best_base(&self, candidates: &[String]) -> Option<String> {
266 candidates
267 .iter()
268 .filter_map(|hash| {
269 self.metadata
270 .get(hash)
271 .map(|meta| (hash.clone(), meta.chain_depth))
272 })
273 .min_by_key(|(_, depth)| *depth)
274 .filter(|(_, depth)| *depth < MAX_CHAIN_DEPTH)
275 .map(|(hash, _)| hash)
276 }
277
278 pub fn get_chain(&self, hash: &str) -> CompressionResult<Vec<String>> {
280 let mut chain = Vec::new();
281 let mut current = hash.to_string();
282
283 loop {
284 chain.push(current.clone());
285
286 if let Some(meta) = self.metadata.get(¤t) {
287 if meta.chain_depth == 0 {
288 break;
290 }
291 current = meta.base_hash.clone();
292 } else {
293 return Err(CompressionError::invalid_input(format!(
294 "missing metadata for hash: {}",
295 current
296 )));
297 }
298 }
299
300 chain.reverse();
302 Ok(chain)
303 }
304}
305
306impl Default for DeltaChainTracker {
307 fn default() -> Self {
308 Self::new()
309 }
310}
311
312#[derive(Debug, Clone)]
317pub struct DeltaEncoder {
318 similarity_threshold: f64,
320 min_space_savings: f64,
322 max_chain_depth: u32,
324}
325
326impl DeltaEncoder {
327 pub fn new() -> Self {
329 DeltaEncoder {
330 similarity_threshold: SIMILARITY_THRESHOLD,
331 min_space_savings: MIN_SPACE_SAVINGS,
332 max_chain_depth: MAX_CHAIN_DEPTH,
333 }
334 }
335
336 pub fn with_thresholds(similarity_threshold: f64, min_space_savings: f64) -> Self {
338 DeltaEncoder {
339 similarity_threshold,
340 min_space_savings,
341 max_chain_depth: MAX_CHAIN_DEPTH,
342 }
343 }
344
345 pub fn encode(&self, base: &[u8], target: &[u8]) -> CompressionResult<(Vec<u8>, bool)> {
359 self.encode_with_depth(base, target, 0)
360 }
361
362 pub fn encode_with_depth(
383 &self,
384 base: &[u8],
385 target: &[u8],
386 current_chain_depth: u32,
387 ) -> CompressionResult<(Vec<u8>, bool)> {
388 let start = Instant::now();
389
390 if current_chain_depth >= self.max_chain_depth {
392 debug!(
393 "Chain depth {} reached max depth {}, using full compression",
394 current_chain_depth, self.max_chain_depth
395 );
396 return Ok((target.to_vec(), false));
397 }
398
399 let similarity = self.calculate_similarity(base, target);
401 trace!(
402 "Similarity between base and target: {:.2}%",
403 similarity * 100.0
404 );
405
406 if similarity < self.similarity_threshold {
408 debug!(
409 "Similarity {:.2}% below threshold {:.2}%, using full compression",
410 similarity * 100.0,
411 self.similarity_threshold * 100.0
412 );
413 return Ok((target.to_vec(), false));
414 }
415
416 let delta = {
420 let mut raw_patch = Vec::new();
421 match bsdiff::diff(base, target, &mut raw_patch) {
422 Ok(()) => zstd::bulk::compress(&raw_patch, 3).unwrap_or(raw_patch),
423 Err(e) => {
424 warn!("bsdiff encoding failed: {}, falling back to full", e);
425 return Ok((target.to_vec(), false));
426 }
427 }
428 };
429
430 let space_savings = if !target.is_empty() && delta.len() < target.len() {
432 (target.len() - delta.len()) as f64 / target.len() as f64
433 } else if !target.is_empty() && delta.len() >= target.len() {
434 -((delta.len() - target.len()) as f64 / target.len() as f64)
436 } else {
437 0.0
438 };
439
440 if space_savings < self.min_space_savings {
442 debug!(
443 "Space savings {:.2}% below threshold {:.2}%, using full compression",
444 space_savings * 100.0,
445 self.min_space_savings * 100.0
446 );
447 return Ok((target.to_vec(), false));
448 }
449
450 let elapsed = start.elapsed();
451 info!(
452 "Delta encoding: {} bytes -> {} bytes ({:.1}% savings) in {:?}",
453 target.len(),
454 delta.len(),
455 space_savings * 100.0,
456 elapsed
457 );
458
459 Ok((delta, true))
460 }
461
462 pub fn decode(&self, base: &[u8], delta: &[u8]) -> CompressionResult<Vec<u8>> {
473 let start = Instant::now();
474
475 let result = {
476 let raw_patch = zstd::bulk::decompress(delta, 128 * 1024 * 1024).map_err(|e| {
478 CompressionError::decompression_failed(format!(
479 "bsdiff zstd decompression failed: {}",
480 e
481 ))
482 })?;
483 let mut output = Vec::new();
484 bsdiff::patch(base, &mut raw_patch.as_slice(), &mut output).map_err(|e| {
485 CompressionError::decompression_failed(format!("bsdiff patch failed: {}", e))
486 })?;
487 output
488 };
489
490 let elapsed = start.elapsed();
491 if elapsed.as_millis() > MAX_RECONSTRUCTION_TIME_MS as u128 {
492 warn!(
493 "Delta reconstruction took {:?}, exceeds target of {}ms",
494 elapsed, MAX_RECONSTRUCTION_TIME_MS
495 );
496 }
497
498 trace!(
499 "Delta decoding: {} bytes delta + {} bytes base -> {} bytes target in {:?}",
500 delta.len(),
501 base.len(),
502 result.len(),
503 elapsed
504 );
505
506 Ok(result)
507 }
508
509 pub fn decode_chain(&self, versions: &[(Vec<u8>, bool)]) -> CompressionResult<Vec<u8>> {
519 if versions.is_empty() {
520 return Err(CompressionError::invalid_input("empty version chain"));
521 }
522
523 let start = Instant::now();
524 let mut current = versions[0].0.clone();
525
526 for (i, (data, is_delta)) in versions.iter().enumerate().skip(1) {
527 if *is_delta {
528 trace!("Applying delta {} in chain", i);
529 current = self.decode(¤t, data)?;
530 } else {
531 current = data.clone();
533 }
534 }
535
536 let elapsed = start.elapsed();
537 if elapsed.as_millis() > MAX_RECONSTRUCTION_TIME_MS as u128 {
538 warn!(
539 "Delta chain reconstruction took {:?}, exceeds target of {}ms",
540 elapsed, MAX_RECONSTRUCTION_TIME_MS
541 );
542 }
543
544 info!(
545 "Delta chain reconstruction: {} versions -> {} bytes in {:?}",
546 versions.len(),
547 current.len(),
548 elapsed
549 );
550
551 Ok(current)
552 }
553
554 fn calculate_similarity(&self, base: &[u8], target: &[u8]) -> f64 {
559 if base.is_empty() && target.is_empty() {
560 return 1.0;
561 }
562
563 if base.is_empty() || target.is_empty() {
564 return 0.0;
565 }
566
567 let sample_size = 1024.min(base.len().min(target.len()));
570 let mut matches = 0;
571
572 let base_step = if base.len() > sample_size {
574 base.len() / sample_size
575 } else {
576 1
577 };
578 let target_step = if target.len() > sample_size {
579 target.len() / sample_size
580 } else {
581 1
582 };
583
584 for i in 0..sample_size {
585 let base_idx = (i * base_step).min(base.len() - 1);
586 let target_idx = (i * target_step).min(target.len() - 1);
587
588 if base[base_idx] == target[target_idx] {
589 matches += 1;
590 }
591 }
592
593 matches as f64 / sample_size as f64
594 }
595
596 pub fn hash_data(data: &[u8]) -> String {
598 let hash = blake3::hash(data);
599 hash.to_hex().to_string()
600 }
601
602 pub fn max_chain_depth(&self) -> u32 {
604 self.max_chain_depth
605 }
606
607 pub fn set_max_chain_depth(&mut self, depth: u32) {
626 self.max_chain_depth = depth;
627 }
628
629 pub fn with_max_chain_depth(max_chain_depth: u32) -> Self {
644 DeltaEncoder {
645 similarity_threshold: SIMILARITY_THRESHOLD,
646 min_space_savings: MIN_SPACE_SAVINGS,
647 max_chain_depth,
648 }
649 }
650}
651
652impl Default for DeltaEncoder {
653 fn default() -> Self {
654 Self::new()
655 }
656}
657
658#[cfg(test)]
659#[allow(clippy::unwrap_used)]
660mod tests {
661 use super::*;
662
663 #[test]
664 fn test_delta_metadata_full() {
665 let meta = DeltaMetadata::full("abc123".to_string(), 1000);
666 assert_eq!(meta.chain_depth, 0);
667 assert!(!meta.is_delta);
668 assert_eq!(meta.similarity, 1.0);
669 assert_eq!(meta.compression_ratio(), 1.0);
670 }
671
672 #[test]
673 fn test_delta_metadata_delta() {
674 let meta = DeltaMetadata::delta("abc123".to_string(), 0, 1000, 100, 0.95);
675 assert_eq!(meta.chain_depth, 1);
676 assert!(meta.is_delta);
677 assert_eq!(meta.similarity, 0.95);
678 assert_eq!(meta.space_savings, 0.9);
679 assert_eq!(meta.compression_ratio(), 0.1);
680 }
681
682 #[test]
683 fn test_delta_metadata_max_depth() {
684 let meta = DeltaMetadata::delta("abc".to_string(), MAX_CHAIN_DEPTH - 1, 1000, 100, 0.9);
685 assert!(meta.is_at_max_depth());
686
687 let meta2 = DeltaMetadata::delta("abc".to_string(), 5, 1000, 100, 0.9);
688 assert!(!meta2.is_at_max_depth());
689 }
690
691 #[test]
692 fn test_delta_chain_tracker() {
693 let mut tracker = DeltaChainTracker::new();
694
695 let meta1 = DeltaMetadata::full("base".to_string(), 1000);
696 tracker.register("hash1".to_string(), meta1.clone());
697
698 let meta2 = DeltaMetadata::delta("hash1".to_string(), 0, 900, 100, 0.95);
699 tracker.register("hash2".to_string(), meta2);
700
701 assert_eq!(tracker.get("hash1"), Some(&meta1));
702 assert!(tracker.get("hash2").is_some());
703 assert!(tracker.get("nonexistent").is_none());
704 }
705
706 #[test]
707 fn test_find_best_base() {
708 let mut tracker = DeltaChainTracker::new();
709
710 tracker.register(
711 "base".to_string(),
712 DeltaMetadata::full("".to_string(), 1000),
713 );
714 tracker.register(
715 "v1".to_string(),
716 DeltaMetadata::delta("base".to_string(), 0, 900, 100, 0.9),
717 );
718 tracker.register(
719 "v2".to_string(),
720 DeltaMetadata::delta("v1".to_string(), 1, 850, 50, 0.85),
721 );
722
723 let candidates = vec!["base".to_string(), "v1".to_string(), "v2".to_string()];
724 let best = tracker.find_best_base(&candidates);
725
726 assert_eq!(best, Some("base".to_string())); }
728
729 #[test]
730 fn test_get_chain() {
731 let mut tracker = DeltaChainTracker::new();
732
733 tracker.register(
734 "base".to_string(),
735 DeltaMetadata::full("".to_string(), 1000),
736 );
737 tracker.register(
738 "v1".to_string(),
739 DeltaMetadata::delta("base".to_string(), 0, 900, 100, 0.9),
740 );
741 tracker.register(
742 "v2".to_string(),
743 DeltaMetadata::delta("v1".to_string(), 1, 850, 50, 0.85),
744 );
745
746 let chain = tracker.get_chain("v2").unwrap();
747 assert_eq!(chain, vec!["base", "v1", "v2"]);
748 }
749
750 #[test]
751 fn test_delta_encode_decode_simple() {
752 let encoder = DeltaEncoder::new();
753
754 let base = b"Hello, World!";
755 let target = b"Hello, MediaGit World!";
756
757 let (delta, should_use_delta) = encoder.encode(base, target).unwrap();
758
759 if should_use_delta {
760 let reconstructed = encoder.decode(base, &delta).unwrap();
761 assert_eq!(target, &reconstructed[..]);
762 }
763 }
764
765 #[test]
766 fn test_delta_encode_high_similarity() {
767 let encoder = DeltaEncoder::new();
768
769 let base = b"a".repeat(1000);
770 let mut target = base.clone();
771 target[500] = b'b'; let (data, should_use_delta) = encoder.encode(&base, &target).unwrap();
774
775 if should_use_delta {
777 assert!(
778 data.len() < target.len() / 2,
779 "Delta should be much smaller"
780 );
781 let reconstructed = encoder.decode(&base, &data).unwrap();
782 assert_eq!(target, reconstructed);
783 } else {
784 assert_eq!(data, target);
786 }
787 }
788
789 #[test]
790 fn test_delta_encode_low_similarity() {
791 let encoder = DeltaEncoder::new();
792
793 let base: Vec<u8> = (0..1000).map(|i| (i % 256) as u8).collect();
794 let target: Vec<u8> = (0..1000).map(|i| ((i + 128) % 256) as u8).collect();
795
796 let (data, should_use_delta) = encoder.encode(&base, &target).unwrap();
797
798 if !should_use_delta {
799 assert_eq!(data, target);
801 }
802 }
803
804 #[test]
805 fn test_delta_encode_identical() {
806 let encoder = DeltaEncoder::new();
807
808 let base = b"Hello, World!";
809 let target = b"Hello, World!";
810
811 let (data, should_use_delta) = encoder.encode(base, target).unwrap();
812
813 if should_use_delta {
816 assert!(data.len() < 50, "Delta for identical files should be tiny");
818 let reconstructed = encoder.decode(base, &data).unwrap();
819 assert_eq!(target, &reconstructed[..]);
820 } else {
821 assert_eq!(data, target.to_vec());
823 }
824 }
825
826 #[test]
827 fn test_delta_decode_chain() {
828 let encoder = DeltaEncoder::new();
829
830 let v1 = b"Hello, World!";
831 let v2 = b"Hello, MediaGit World!";
832 let v3 = b"Hello, MediaGit Rust World!";
833
834 let (delta2, should_use_delta2) = encoder.encode(v1, v2).unwrap();
835 let (delta3, should_use_delta3) = encoder.encode(v2, v3).unwrap();
836
837 let versions = vec![
839 (v1.to_vec(), false),
840 (delta2, should_use_delta2),
841 (delta3, should_use_delta3),
842 ];
843
844 let reconstructed = encoder.decode_chain(&versions).unwrap();
845 assert_eq!(v3, &reconstructed[..]);
846 }
847
848 #[test]
849 fn test_similarity_calculation() {
850 let encoder = DeltaEncoder::new();
851
852 let data = b"Hello, World!";
854 assert_eq!(encoder.calculate_similarity(data, data), 1.0);
855
856 assert_eq!(encoder.calculate_similarity(b"", b""), 1.0);
858 assert_eq!(encoder.calculate_similarity(b"test", b""), 0.0);
859 assert_eq!(encoder.calculate_similarity(b"", b"test"), 0.0);
860
861 let base = b"a".repeat(1000);
863 let mut similar = base.clone();
864 similar[500] = b'b';
865 let sim = encoder.calculate_similarity(&base, &similar);
866 assert!(sim > 0.99, "Similarity should be very high: {}", sim);
867 }
868
869 #[test]
870 fn test_hash_data() {
871 let data1 = b"Hello, World!";
872 let data2 = b"Hello, World!";
873 let data3 = b"Hello, MediaGit!";
874
875 let hash1 = DeltaEncoder::hash_data(data1);
876 let hash2 = DeltaEncoder::hash_data(data2);
877 let hash3 = DeltaEncoder::hash_data(data3);
878
879 assert_eq!(hash1, hash2); assert_ne!(hash1, hash3); assert_eq!(hash1.len(), 64); }
883
884 #[test]
885 fn test_custom_thresholds() {
886 let encoder = DeltaEncoder::with_thresholds(0.5, 0.05);
887
888 let base = b"Hello, World!";
889 let target = b"Goodbye, World!";
890
891 let (_, should_use_delta) = encoder.encode(base, target).unwrap();
892
893 let _ = should_use_delta; }
897
898 #[test]
899 fn test_large_data() {
900 let encoder = DeltaEncoder::new();
901
902 let base = vec![0x42u8; 1024 * 100]; let mut target = base.clone();
904 for i in (0..target.len()).step_by(100) {
906 target[i] = 0x43;
907 }
908
909 let (data, should_use_delta) = encoder.encode(&base, &target).unwrap();
910
911 if should_use_delta {
913 assert!(
914 data.len() < target.len() / 2,
915 "Delta should be much smaller than target"
916 );
917 let reconstructed = encoder.decode(&base, &data).unwrap();
918 assert_eq!(target, reconstructed);
919 } else {
920 assert_eq!(data, target);
922 }
923 }
924
925 #[test]
926 fn test_chain_depth_enforcement() {
927 let encoder = DeltaEncoder::with_max_chain_depth(3);
928 assert_eq!(encoder.max_chain_depth(), 3);
929
930 let base = vec![0x42u8; 1000];
932 let mut target = base.clone();
933 target[500] = 0x43; let (data0, should_use0) = encoder.encode_with_depth(&base, &target, 0).unwrap();
937 if should_use0 {
938 assert!(
939 data0.len() < target.len(),
940 "Delta should be smaller than full data"
941 );
942 }
943
944 let (_data2, should_use2) = encoder.encode_with_depth(&base, &target, 2).unwrap();
946 assert_eq!(
948 should_use2, should_use0,
949 "Should make same decision at depth 2"
950 );
951
952 let (data3, should_use3) = encoder.encode_with_depth(&base, &target, 3).unwrap();
954 assert!(!should_use3, "Should NOT use delta at max depth");
955 assert_eq!(data3, target, "Should return full data at max depth");
956
957 let (data4, should_use4) = encoder.encode_with_depth(&base, &target, 4).unwrap();
959 assert!(!should_use4, "Should NOT use delta beyond max depth");
960 assert_eq!(data4, target, "Should return full data beyond max depth");
961 }
962
963 #[test]
964 fn test_max_chain_depth_setter() {
965 let mut encoder = DeltaEncoder::new();
966
967 assert_eq!(encoder.max_chain_depth(), MAX_CHAIN_DEPTH);
969 assert_eq!(encoder.max_chain_depth(), 10);
970
971 encoder.set_max_chain_depth(75);
973 assert_eq!(encoder.max_chain_depth(), 75);
974
975 encoder.set_max_chain_depth(25);
977 assert_eq!(encoder.max_chain_depth(), 25);
978 }
979
980 #[test]
981 fn test_chain_depth_with_constructor() {
982 let encoder = DeltaEncoder::with_max_chain_depth(100);
983 assert_eq!(encoder.max_chain_depth(), 100);
984
985 let pattern = b"The quick brown fox jumps over the lazy dog. ";
988 let base: Vec<u8> = pattern.iter().cycle().take(100_000).copied().collect();
989 let mut target = base.clone();
990 for b in &mut target[50_000..50_100] {
992 *b = b'X';
993 }
994
995 let (_, should_use99) = encoder.encode_with_depth(&base, &target, 99).unwrap();
997 assert!(should_use99, "Should use delta just below max depth");
998
999 let (_, should_use100) = encoder.encode_with_depth(&base, &target, 100).unwrap();
1001 assert!(!should_use100, "Should NOT use delta at max depth");
1002 }
1003
1004 #[test]
1005 fn test_encode_delegates_to_encode_with_depth() {
1006 let encoder = DeltaEncoder::new();
1007
1008 let base = b"Test data for delta encoding";
1009 let target = b"Test data for delta encoding with changes";
1010
1011 let (data1, should1) = encoder.encode(base, target).unwrap();
1013 let (data2, should2) = encoder.encode_with_depth(base, target, 0).unwrap();
1014
1015 assert_eq!(should1, should2, "Both methods should return same decision");
1016 assert_eq!(data1, data2, "Both methods should return same data");
1017 }
1018
1019 #[test]
1020 fn test_new_category_delta_configurations() {
1021 use crate::smart_compressor::{ObjectCategory, ObjectType};
1022
1023 let creative_config = DeltaConfig::for_object_type(ObjectType::AdobePhotoshop);
1025 assert_eq!(
1026 creative_config.similarity_threshold, 0.70,
1027 "Creative projects should have 70% threshold"
1028 );
1029 assert_eq!(
1030 creative_config.max_chain_depth, 10,
1031 "Creative projects should allow depth 10"
1032 );
1033 assert_eq!(
1034 creative_config.min_space_savings, 0.10,
1035 "Creative projects should require 10% savings"
1036 );
1037
1038 assert_eq!(
1040 ObjectType::AdobePhotoshop.category(),
1041 ObjectCategory::CreativeProject
1042 );
1043 assert_eq!(
1044 ObjectType::Blender.category(),
1045 ObjectCategory::CreativeProject
1046 );
1047 assert_eq!(
1048 ObjectType::DavinciResolve.category(),
1049 ObjectCategory::CreativeProject
1050 );
1051
1052 let office_config = DeltaConfig::for_object_type(ObjectType::WordDocument);
1054 assert_eq!(
1055 office_config.similarity_threshold, 0.75,
1056 "Office docs should have 75% threshold"
1057 );
1058 assert_eq!(
1059 office_config.max_chain_depth, 10,
1060 "Office docs should allow depth 10"
1061 );
1062 assert_eq!(ObjectType::WordDocument.category(), ObjectCategory::Office);
1063
1064 let ml_config = DeltaConfig::for_object_type(ObjectType::MlCheckpoint);
1066 assert_eq!(
1067 ml_config.similarity_threshold, 0.75,
1068 "ML models should have 75% threshold"
1069 );
1070 assert_eq!(
1071 ml_config.max_chain_depth, 8,
1072 "ML models should allow depth 8"
1073 );
1074 assert_eq!(
1075 ObjectType::MlCheckpoint.category(),
1076 ObjectCategory::MlSpecialized
1077 );
1078 assert_eq!(
1079 ObjectType::MlInference.category(),
1080 ObjectCategory::MlSpecialized
1081 );
1082
1083 let db_config = DeltaConfig::for_object_type(ObjectType::SqliteDatabase);
1085 assert_eq!(
1086 db_config.similarity_threshold, 0.80,
1087 "Databases should have 80% threshold"
1088 );
1089 assert_eq!(
1090 db_config.max_chain_depth, 5,
1091 "Databases should allow depth 5"
1092 );
1093 assert_eq!(
1094 ObjectType::SqliteDatabase.category(),
1095 ObjectCategory::Database
1096 );
1097 }
1098
1099 #[test]
1100 fn test_creative_project_delta_priority() {
1101 use crate::smart_compressor::ObjectType;
1102
1103 let creative_config = DeltaConfig::for_object_type(ObjectType::AdobePhotoshop);
1105 let image_config = DeltaConfig::for_object_type(ObjectType::Tiff);
1106 let video_config = DeltaConfig::for_object_type(ObjectType::Mp4);
1107
1108 assert!(
1110 creative_config.similarity_threshold < image_config.similarity_threshold,
1111 "Creative projects (0.70) should have lower threshold than images (0.85)"
1112 );
1113
1114 assert!(
1116 creative_config.max_chain_depth >= image_config.max_chain_depth,
1117 "Creative projects should allow equal or longer chains"
1118 );
1119
1120 assert!(
1122 video_config.similarity_threshold > creative_config.similarity_threshold,
1123 "Video (0.95) should have higher threshold than creative projects (0.70)"
1124 );
1125 }
1126
1127 #[test]
1128 fn test_delta_roundtrip_creative_project() {
1129 use crate::smart_compressor::ObjectType;
1130
1131 let encoder = DeltaEncoder::new();
1132
1133 let base_content = b"<CreativeProjectLayer id='1' opacity='100%'>".repeat(100);
1136 let base_data = base_content.clone();
1137
1138 let mut modified = base_data.clone();
1140 for i in (0..modified.len()).step_by(500) {
1142 if i < modified.len() {
1143 modified[i] = b'X';
1144 }
1145 }
1146
1147 let similarity = encoder.calculate_similarity(&base_data, &modified);
1149
1150 let config = DeltaConfig::for_object_type(ObjectType::AdobePhotoshop);
1152
1153 assert!(
1156 similarity >= config.similarity_threshold,
1157 "Similarity {:.2} should meet creative project threshold {:.2}",
1158 similarity,
1159 config.similarity_threshold
1160 );
1161
1162 assert_eq!(
1164 config.max_chain_depth, 10,
1165 "Creative projects should allow depth 10"
1166 );
1167 assert_eq!(
1168 config.similarity_threshold, 0.70,
1169 "Creative projects should have 70% threshold"
1170 );
1171
1172 assert_eq!(
1174 ObjectType::AdobePhotoshop.category(),
1175 crate::smart_compressor::ObjectCategory::CreativeProject,
1176 "AdobePhotoshop should be in CreativeProject category"
1177 );
1178 }
1179}