use crate::error::{CompressionError, CompressionResult};
use crate::smart_compressor::{ObjectCategory, ObjectType};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Instant;
use tracing::{debug, info, trace, warn};
pub const MAX_CHAIN_DEPTH: u32 = 10;
pub const SIMILARITY_THRESHOLD: f64 = 0.80;
pub const MIN_SPACE_SAVINGS: f64 = 0.10;
pub const MAX_RECONSTRUCTION_TIME_MS: u64 = 100;
#[derive(Debug, Clone, Copy)]
pub struct DeltaConfig {
pub similarity_threshold: f64,
pub min_space_savings: f64,
pub max_chain_depth: u32,
}
impl DeltaConfig {
pub fn for_object_type(obj_type: ObjectType) -> Self {
match obj_type.category() {
ObjectCategory::Image => DeltaConfig {
similarity_threshold: 0.85,
min_space_savings: 0.15,
max_chain_depth: 5,
},
ObjectCategory::Video => DeltaConfig {
similarity_threshold: 0.95,
min_space_savings: 0.05,
max_chain_depth: 3,
},
ObjectCategory::Audio => DeltaConfig {
similarity_threshold: 0.90,
min_space_savings: 0.10,
max_chain_depth: 5,
},
ObjectCategory::Text | ObjectCategory::Document => DeltaConfig {
similarity_threshold: 0.70,
min_space_savings: 0.10,
max_chain_depth: MAX_CHAIN_DEPTH,
},
ObjectCategory::CreativeProject => DeltaConfig {
similarity_threshold: 0.70, min_space_savings: 0.10,
max_chain_depth: 10, },
ObjectCategory::Office => DeltaConfig {
similarity_threshold: 0.75,
min_space_savings: 0.10,
max_chain_depth: MAX_CHAIN_DEPTH,
},
ObjectCategory::MlSpecialized => DeltaConfig {
similarity_threshold: 0.75, min_space_savings: 0.10,
max_chain_depth: 8, },
ObjectCategory::Database => DeltaConfig {
similarity_threshold: 0.80,
min_space_savings: 0.10,
max_chain_depth: 5,
},
ObjectCategory::GitObject => DeltaConfig {
similarity_threshold: 0.75,
min_space_savings: 0.10,
max_chain_depth: MAX_CHAIN_DEPTH,
},
ObjectCategory::Archive | ObjectCategory::Unknown => DeltaConfig {
similarity_threshold: SIMILARITY_THRESHOLD,
min_space_savings: MIN_SPACE_SAVINGS,
max_chain_depth: MAX_CHAIN_DEPTH,
},
}
}
pub fn default_config() -> Self {
DeltaConfig {
similarity_threshold: SIMILARITY_THRESHOLD,
min_space_savings: MIN_SPACE_SAVINGS,
max_chain_depth: MAX_CHAIN_DEPTH,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DeltaMetadata {
pub base_hash: String,
pub chain_depth: u32,
pub original_size: usize,
pub delta_size: usize,
pub is_delta: bool,
pub similarity: f64,
pub space_savings: f64,
}
impl DeltaMetadata {
pub fn full(hash: String, size: usize) -> Self {
DeltaMetadata {
base_hash: hash,
chain_depth: 0,
original_size: size,
delta_size: size,
is_delta: false,
similarity: 1.0,
space_savings: 0.0,
}
}
pub fn delta(
base_hash: String,
base_chain_depth: u32,
original_size: usize,
delta_size: usize,
similarity: f64,
) -> Self {
let space_savings = if original_size > 0 {
(original_size - delta_size) as f64 / original_size as f64
} else {
0.0
};
DeltaMetadata {
base_hash,
chain_depth: base_chain_depth + 1,
original_size,
delta_size,
is_delta: true,
similarity,
space_savings,
}
}
pub fn is_at_max_depth(&self) -> bool {
self.chain_depth >= MAX_CHAIN_DEPTH
}
pub fn compression_ratio(&self) -> f64 {
if self.original_size == 0 {
1.0
} else {
self.delta_size as f64 / self.original_size as f64
}
}
}
#[derive(Debug, Clone)]
pub struct DeltaChainTracker {
metadata: HashMap<String, DeltaMetadata>,
}
impl DeltaChainTracker {
pub fn new() -> Self {
DeltaChainTracker {
metadata: HashMap::new(),
}
}
pub fn register(&mut self, hash: String, metadata: DeltaMetadata) {
self.metadata.insert(hash, metadata);
}
pub fn get(&self, hash: &str) -> Option<&DeltaMetadata> {
self.metadata.get(hash)
}
pub fn find_best_base(&self, candidates: &[String]) -> Option<String> {
candidates
.iter()
.filter_map(|hash| {
self.metadata
.get(hash)
.map(|meta| (hash.clone(), meta.chain_depth))
})
.min_by_key(|(_, depth)| *depth)
.filter(|(_, depth)| *depth < MAX_CHAIN_DEPTH)
.map(|(hash, _)| hash)
}
pub fn get_chain(&self, hash: &str) -> CompressionResult<Vec<String>> {
let mut chain = Vec::new();
let mut current = hash.to_string();
loop {
chain.push(current.clone());
if let Some(meta) = self.metadata.get(¤t) {
if meta.chain_depth == 0 {
break;
}
current = meta.base_hash.clone();
} else {
return Err(CompressionError::invalid_input(format!(
"missing metadata for hash: {}",
current
)));
}
}
chain.reverse();
Ok(chain)
}
}
impl Default for DeltaChainTracker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DeltaEncoder {
similarity_threshold: f64,
min_space_savings: f64,
max_chain_depth: u32,
}
impl DeltaEncoder {
pub fn new() -> Self {
DeltaEncoder {
similarity_threshold: SIMILARITY_THRESHOLD,
min_space_savings: MIN_SPACE_SAVINGS,
max_chain_depth: MAX_CHAIN_DEPTH,
}
}
pub fn with_thresholds(similarity_threshold: f64, min_space_savings: f64) -> Self {
DeltaEncoder {
similarity_threshold,
min_space_savings,
max_chain_depth: MAX_CHAIN_DEPTH,
}
}
pub fn encode(&self, base: &[u8], target: &[u8]) -> CompressionResult<(Vec<u8>, bool)> {
self.encode_with_depth(base, target, 0)
}
pub fn encode_with_depth(
&self,
base: &[u8],
target: &[u8],
current_chain_depth: u32,
) -> CompressionResult<(Vec<u8>, bool)> {
let start = Instant::now();
if current_chain_depth >= self.max_chain_depth {
debug!(
"Chain depth {} reached max depth {}, using full compression",
current_chain_depth, self.max_chain_depth
);
return Ok((target.to_vec(), false));
}
let similarity = self.calculate_similarity(base, target);
trace!(
"Similarity between base and target: {:.2}%",
similarity * 100.0
);
if similarity < self.similarity_threshold {
debug!(
"Similarity {:.2}% below threshold {:.2}%, using full compression",
similarity * 100.0,
self.similarity_threshold * 100.0
);
return Ok((target.to_vec(), false));
}
let delta = {
let mut raw_patch = Vec::new();
match bsdiff::diff(base, target, &mut raw_patch) {
Ok(()) => zstd::bulk::compress(&raw_patch, 3).unwrap_or(raw_patch),
Err(e) => {
warn!("bsdiff encoding failed: {}, falling back to full", e);
return Ok((target.to_vec(), false));
}
}
};
let space_savings = if !target.is_empty() && delta.len() < target.len() {
(target.len() - delta.len()) as f64 / target.len() as f64
} else if !target.is_empty() && delta.len() >= target.len() {
-((delta.len() - target.len()) as f64 / target.len() as f64)
} else {
0.0
};
if space_savings < self.min_space_savings {
debug!(
"Space savings {:.2}% below threshold {:.2}%, using full compression",
space_savings * 100.0,
self.min_space_savings * 100.0
);
return Ok((target.to_vec(), false));
}
let elapsed = start.elapsed();
info!(
"Delta encoding: {} bytes -> {} bytes ({:.1}% savings) in {:?}",
target.len(),
delta.len(),
space_savings * 100.0,
elapsed
);
Ok((delta, true))
}
pub fn decode(&self, base: &[u8], delta: &[u8]) -> CompressionResult<Vec<u8>> {
let start = Instant::now();
let result = {
let raw_patch = zstd::bulk::decompress(delta, 128 * 1024 * 1024).map_err(|e| {
CompressionError::decompression_failed(format!(
"bsdiff zstd decompression failed: {}",
e
))
})?;
let mut output = Vec::new();
bsdiff::patch(base, &mut raw_patch.as_slice(), &mut output).map_err(|e| {
CompressionError::decompression_failed(format!("bsdiff patch failed: {}", e))
})?;
output
};
let elapsed = start.elapsed();
if elapsed.as_millis() > MAX_RECONSTRUCTION_TIME_MS as u128 {
warn!(
"Delta reconstruction took {:?}, exceeds target of {}ms",
elapsed, MAX_RECONSTRUCTION_TIME_MS
);
}
trace!(
"Delta decoding: {} bytes delta + {} bytes base -> {} bytes target in {:?}",
delta.len(),
base.len(),
result.len(),
elapsed
);
Ok(result)
}
pub fn decode_chain(&self, versions: &[(Vec<u8>, bool)]) -> CompressionResult<Vec<u8>> {
if versions.is_empty() {
return Err(CompressionError::invalid_input("empty version chain"));
}
let start = Instant::now();
let mut current = versions[0].0.clone();
for (i, (data, is_delta)) in versions.iter().enumerate().skip(1) {
if *is_delta {
trace!("Applying delta {} in chain", i);
current = self.decode(¤t, data)?;
} else {
current = data.clone();
}
}
let elapsed = start.elapsed();
if elapsed.as_millis() > MAX_RECONSTRUCTION_TIME_MS as u128 {
warn!(
"Delta chain reconstruction took {:?}, exceeds target of {}ms",
elapsed, MAX_RECONSTRUCTION_TIME_MS
);
}
info!(
"Delta chain reconstruction: {} versions -> {} bytes in {:?}",
versions.len(),
current.len(),
elapsed
);
Ok(current)
}
fn calculate_similarity(&self, base: &[u8], target: &[u8]) -> f64 {
if base.is_empty() && target.is_empty() {
return 1.0;
}
if base.is_empty() || target.is_empty() {
return 0.0;
}
let sample_size = 1024.min(base.len().min(target.len()));
let mut matches = 0;
let base_step = if base.len() > sample_size {
base.len() / sample_size
} else {
1
};
let target_step = if target.len() > sample_size {
target.len() / sample_size
} else {
1
};
for i in 0..sample_size {
let base_idx = (i * base_step).min(base.len() - 1);
let target_idx = (i * target_step).min(target.len() - 1);
if base[base_idx] == target[target_idx] {
matches += 1;
}
}
matches as f64 / sample_size as f64
}
pub fn hash_data(data: &[u8]) -> String {
let hash = blake3::hash(data);
hash.to_hex().to_string()
}
pub fn max_chain_depth(&self) -> u32 {
self.max_chain_depth
}
pub fn set_max_chain_depth(&mut self, depth: u32) {
self.max_chain_depth = depth;
}
pub fn with_max_chain_depth(max_chain_depth: u32) -> Self {
DeltaEncoder {
similarity_threshold: SIMILARITY_THRESHOLD,
min_space_savings: MIN_SPACE_SAVINGS,
max_chain_depth,
}
}
}
impl Default for DeltaEncoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_delta_metadata_full() {
let meta = DeltaMetadata::full("abc123".to_string(), 1000);
assert_eq!(meta.chain_depth, 0);
assert!(!meta.is_delta);
assert_eq!(meta.similarity, 1.0);
assert_eq!(meta.compression_ratio(), 1.0);
}
#[test]
fn test_delta_metadata_delta() {
let meta = DeltaMetadata::delta("abc123".to_string(), 0, 1000, 100, 0.95);
assert_eq!(meta.chain_depth, 1);
assert!(meta.is_delta);
assert_eq!(meta.similarity, 0.95);
assert_eq!(meta.space_savings, 0.9);
assert_eq!(meta.compression_ratio(), 0.1);
}
#[test]
fn test_delta_metadata_max_depth() {
let meta = DeltaMetadata::delta("abc".to_string(), MAX_CHAIN_DEPTH - 1, 1000, 100, 0.9);
assert!(meta.is_at_max_depth());
let meta2 = DeltaMetadata::delta("abc".to_string(), 5, 1000, 100, 0.9);
assert!(!meta2.is_at_max_depth());
}
#[test]
fn test_delta_chain_tracker() {
let mut tracker = DeltaChainTracker::new();
let meta1 = DeltaMetadata::full("base".to_string(), 1000);
tracker.register("hash1".to_string(), meta1.clone());
let meta2 = DeltaMetadata::delta("hash1".to_string(), 0, 900, 100, 0.95);
tracker.register("hash2".to_string(), meta2);
assert_eq!(tracker.get("hash1"), Some(&meta1));
assert!(tracker.get("hash2").is_some());
assert!(tracker.get("nonexistent").is_none());
}
#[test]
fn test_find_best_base() {
let mut tracker = DeltaChainTracker::new();
tracker.register(
"base".to_string(),
DeltaMetadata::full("".to_string(), 1000),
);
tracker.register(
"v1".to_string(),
DeltaMetadata::delta("base".to_string(), 0, 900, 100, 0.9),
);
tracker.register(
"v2".to_string(),
DeltaMetadata::delta("v1".to_string(), 1, 850, 50, 0.85),
);
let candidates = vec!["base".to_string(), "v1".to_string(), "v2".to_string()];
let best = tracker.find_best_base(&candidates);
assert_eq!(best, Some("base".to_string())); }
#[test]
fn test_get_chain() {
let mut tracker = DeltaChainTracker::new();
tracker.register(
"base".to_string(),
DeltaMetadata::full("".to_string(), 1000),
);
tracker.register(
"v1".to_string(),
DeltaMetadata::delta("base".to_string(), 0, 900, 100, 0.9),
);
tracker.register(
"v2".to_string(),
DeltaMetadata::delta("v1".to_string(), 1, 850, 50, 0.85),
);
let chain = tracker.get_chain("v2").unwrap();
assert_eq!(chain, vec!["base", "v1", "v2"]);
}
#[test]
fn test_delta_encode_decode_simple() {
let encoder = DeltaEncoder::new();
let base = b"Hello, World!";
let target = b"Hello, MediaGit World!";
let (delta, should_use_delta) = encoder.encode(base, target).unwrap();
if should_use_delta {
let reconstructed = encoder.decode(base, &delta).unwrap();
assert_eq!(target, &reconstructed[..]);
}
}
#[test]
fn test_delta_encode_high_similarity() {
let encoder = DeltaEncoder::new();
let base = b"a".repeat(1000);
let mut target = base.clone();
target[500] = b'b';
let (data, should_use_delta) = encoder.encode(&base, &target).unwrap();
if should_use_delta {
assert!(
data.len() < target.len() / 2,
"Delta should be much smaller"
);
let reconstructed = encoder.decode(&base, &data).unwrap();
assert_eq!(target, reconstructed);
} else {
assert_eq!(data, target);
}
}
#[test]
fn test_delta_encode_low_similarity() {
let encoder = DeltaEncoder::new();
let base: Vec<u8> = (0..1000).map(|i| (i % 256) as u8).collect();
let target: Vec<u8> = (0..1000).map(|i| ((i + 128) % 256) as u8).collect();
let (data, should_use_delta) = encoder.encode(&base, &target).unwrap();
if !should_use_delta {
assert_eq!(data, target);
}
}
#[test]
fn test_delta_encode_identical() {
let encoder = DeltaEncoder::new();
let base = b"Hello, World!";
let target = b"Hello, World!";
let (data, should_use_delta) = encoder.encode(base, target).unwrap();
if should_use_delta {
assert!(data.len() < 50, "Delta for identical files should be tiny");
let reconstructed = encoder.decode(base, &data).unwrap();
assert_eq!(target, &reconstructed[..]);
} else {
assert_eq!(data, target.to_vec());
}
}
#[test]
fn test_delta_decode_chain() {
let encoder = DeltaEncoder::new();
let v1 = b"Hello, World!";
let v2 = b"Hello, MediaGit World!";
let v3 = b"Hello, MediaGit Rust World!";
let (delta2, should_use_delta2) = encoder.encode(v1, v2).unwrap();
let (delta3, should_use_delta3) = encoder.encode(v2, v3).unwrap();
let versions = vec![
(v1.to_vec(), false),
(delta2, should_use_delta2),
(delta3, should_use_delta3),
];
let reconstructed = encoder.decode_chain(&versions).unwrap();
assert_eq!(v3, &reconstructed[..]);
}
#[test]
fn test_similarity_calculation() {
let encoder = DeltaEncoder::new();
let data = b"Hello, World!";
assert_eq!(encoder.calculate_similarity(data, data), 1.0);
assert_eq!(encoder.calculate_similarity(b"", b""), 1.0);
assert_eq!(encoder.calculate_similarity(b"test", b""), 0.0);
assert_eq!(encoder.calculate_similarity(b"", b"test"), 0.0);
let base = b"a".repeat(1000);
let mut similar = base.clone();
similar[500] = b'b';
let sim = encoder.calculate_similarity(&base, &similar);
assert!(sim > 0.99, "Similarity should be very high: {}", sim);
}
#[test]
fn test_hash_data() {
let data1 = b"Hello, World!";
let data2 = b"Hello, World!";
let data3 = b"Hello, MediaGit!";
let hash1 = DeltaEncoder::hash_data(data1);
let hash2 = DeltaEncoder::hash_data(data2);
let hash3 = DeltaEncoder::hash_data(data3);
assert_eq!(hash1, hash2); assert_ne!(hash1, hash3); assert_eq!(hash1.len(), 64); }
#[test]
fn test_custom_thresholds() {
let encoder = DeltaEncoder::with_thresholds(0.5, 0.05);
let base = b"Hello, World!";
let target = b"Goodbye, World!";
let (_, should_use_delta) = encoder.encode(base, target).unwrap();
let _ = should_use_delta; }
#[test]
fn test_large_data() {
let encoder = DeltaEncoder::new();
let base = vec![0x42u8; 1024 * 100]; let mut target = base.clone();
for i in (0..target.len()).step_by(100) {
target[i] = 0x43;
}
let (data, should_use_delta) = encoder.encode(&base, &target).unwrap();
if should_use_delta {
assert!(
data.len() < target.len() / 2,
"Delta should be much smaller than target"
);
let reconstructed = encoder.decode(&base, &data).unwrap();
assert_eq!(target, reconstructed);
} else {
assert_eq!(data, target);
}
}
#[test]
fn test_chain_depth_enforcement() {
let encoder = DeltaEncoder::with_max_chain_depth(3);
assert_eq!(encoder.max_chain_depth(), 3);
let base = vec![0x42u8; 1000];
let mut target = base.clone();
target[500] = 0x43;
let (data0, should_use0) = encoder.encode_with_depth(&base, &target, 0).unwrap();
if should_use0 {
assert!(
data0.len() < target.len(),
"Delta should be smaller than full data"
);
}
let (_data2, should_use2) = encoder.encode_with_depth(&base, &target, 2).unwrap();
assert_eq!(
should_use2, should_use0,
"Should make same decision at depth 2"
);
let (data3, should_use3) = encoder.encode_with_depth(&base, &target, 3).unwrap();
assert!(!should_use3, "Should NOT use delta at max depth");
assert_eq!(data3, target, "Should return full data at max depth");
let (data4, should_use4) = encoder.encode_with_depth(&base, &target, 4).unwrap();
assert!(!should_use4, "Should NOT use delta beyond max depth");
assert_eq!(data4, target, "Should return full data beyond max depth");
}
#[test]
fn test_max_chain_depth_setter() {
let mut encoder = DeltaEncoder::new();
assert_eq!(encoder.max_chain_depth(), MAX_CHAIN_DEPTH);
assert_eq!(encoder.max_chain_depth(), 10);
encoder.set_max_chain_depth(75);
assert_eq!(encoder.max_chain_depth(), 75);
encoder.set_max_chain_depth(25);
assert_eq!(encoder.max_chain_depth(), 25);
}
#[test]
fn test_chain_depth_with_constructor() {
let encoder = DeltaEncoder::with_max_chain_depth(100);
assert_eq!(encoder.max_chain_depth(), 100);
let pattern = b"The quick brown fox jumps over the lazy dog. ";
let base: Vec<u8> = pattern.iter().cycle().take(100_000).copied().collect();
let mut target = base.clone();
for b in &mut target[50_000..50_100] {
*b = b'X';
}
let (_, should_use99) = encoder.encode_with_depth(&base, &target, 99).unwrap();
assert!(should_use99, "Should use delta just below max depth");
let (_, should_use100) = encoder.encode_with_depth(&base, &target, 100).unwrap();
assert!(!should_use100, "Should NOT use delta at max depth");
}
#[test]
fn test_encode_delegates_to_encode_with_depth() {
let encoder = DeltaEncoder::new();
let base = b"Test data for delta encoding";
let target = b"Test data for delta encoding with changes";
let (data1, should1) = encoder.encode(base, target).unwrap();
let (data2, should2) = encoder.encode_with_depth(base, target, 0).unwrap();
assert_eq!(should1, should2, "Both methods should return same decision");
assert_eq!(data1, data2, "Both methods should return same data");
}
#[test]
fn test_new_category_delta_configurations() {
use crate::smart_compressor::{ObjectCategory, ObjectType};
let creative_config = DeltaConfig::for_object_type(ObjectType::AdobePhotoshop);
assert_eq!(
creative_config.similarity_threshold, 0.70,
"Creative projects should have 70% threshold"
);
assert_eq!(
creative_config.max_chain_depth, 10,
"Creative projects should allow depth 10"
);
assert_eq!(
creative_config.min_space_savings, 0.10,
"Creative projects should require 10% savings"
);
assert_eq!(
ObjectType::AdobePhotoshop.category(),
ObjectCategory::CreativeProject
);
assert_eq!(
ObjectType::Blender.category(),
ObjectCategory::CreativeProject
);
assert_eq!(
ObjectType::DavinciResolve.category(),
ObjectCategory::CreativeProject
);
let office_config = DeltaConfig::for_object_type(ObjectType::WordDocument);
assert_eq!(
office_config.similarity_threshold, 0.75,
"Office docs should have 75% threshold"
);
assert_eq!(
office_config.max_chain_depth, 10,
"Office docs should allow depth 10"
);
assert_eq!(ObjectType::WordDocument.category(), ObjectCategory::Office);
let ml_config = DeltaConfig::for_object_type(ObjectType::MlCheckpoint);
assert_eq!(
ml_config.similarity_threshold, 0.75,
"ML models should have 75% threshold"
);
assert_eq!(
ml_config.max_chain_depth, 8,
"ML models should allow depth 8"
);
assert_eq!(
ObjectType::MlCheckpoint.category(),
ObjectCategory::MlSpecialized
);
assert_eq!(
ObjectType::MlInference.category(),
ObjectCategory::MlSpecialized
);
let db_config = DeltaConfig::for_object_type(ObjectType::SqliteDatabase);
assert_eq!(
db_config.similarity_threshold, 0.80,
"Databases should have 80% threshold"
);
assert_eq!(
db_config.max_chain_depth, 5,
"Databases should allow depth 5"
);
assert_eq!(
ObjectType::SqliteDatabase.category(),
ObjectCategory::Database
);
}
#[test]
fn test_creative_project_delta_priority() {
use crate::smart_compressor::ObjectType;
let creative_config = DeltaConfig::for_object_type(ObjectType::AdobePhotoshop);
let image_config = DeltaConfig::for_object_type(ObjectType::Tiff);
let video_config = DeltaConfig::for_object_type(ObjectType::Mp4);
assert!(
creative_config.similarity_threshold < image_config.similarity_threshold,
"Creative projects (0.70) should have lower threshold than images (0.85)"
);
assert!(
creative_config.max_chain_depth >= image_config.max_chain_depth,
"Creative projects should allow equal or longer chains"
);
assert!(
video_config.similarity_threshold > creative_config.similarity_threshold,
"Video (0.95) should have higher threshold than creative projects (0.70)"
);
}
#[test]
fn test_delta_roundtrip_creative_project() {
use crate::smart_compressor::ObjectType;
let encoder = DeltaEncoder::new();
let base_content = b"<CreativeProjectLayer id='1' opacity='100%'>".repeat(100);
let base_data = base_content.clone();
let mut modified = base_data.clone();
for i in (0..modified.len()).step_by(500) {
if i < modified.len() {
modified[i] = b'X';
}
}
let similarity = encoder.calculate_similarity(&base_data, &modified);
let config = DeltaConfig::for_object_type(ObjectType::AdobePhotoshop);
assert!(
similarity >= config.similarity_threshold,
"Similarity {:.2} should meet creative project threshold {:.2}",
similarity,
config.similarity_threshold
);
assert_eq!(
config.max_chain_depth, 10,
"Creative projects should allow depth 10"
);
assert_eq!(
config.similarity_threshold, 0.70,
"Creative projects should have 70% threshold"
);
assert_eq!(
ObjectType::AdobePhotoshop.category(),
crate::smart_compressor::ObjectCategory::CreativeProject,
"AdobePhotoshop should be in CreativeProject category"
);
}
}