use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use anyhow::{Result, anyhow};
use crate::config::DeltaAlgorithm;
#[derive(Debug)]
pub struct DeltaStorage {
base_files: HashMap<String, Vec<u8>>,
similarity_threshold: f32,
delta_algorithm: DeltaAlgorithm,
base_file_info: HashMap<String, BaseFileInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaseFileInfo {
pub size: u64,
pub file_type: String,
pub created_at: u64,
pub reference_count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeltaInfo {
pub is_delta: bool,
pub base_storage_id: Option<String>,
pub similarity_score: Option<f32>,
pub delta_algorithm: DeltaAlgorithm,
pub original_size: u64,
pub delta_size: u64,
}
#[derive(Debug, Clone)]
pub struct SimilarityMatch {
pub base_storage_id: String,
pub similarity_score: f32,
pub estimated_compression: f32,
}
impl DeltaStorage {
pub fn new(similarity_threshold: f32, delta_algorithm: DeltaAlgorithm) -> Self {
Self {
base_files: HashMap::new(),
similarity_threshold,
delta_algorithm,
base_file_info: HashMap::new(),
}
}
pub fn calculate_similarity(&self, data1: &[u8], data2: &[u8]) -> f32 {
if data1.is_empty() && data2.is_empty() {
return 1.0;
}
if data1.is_empty() || data2.is_empty() {
return 0.0;
}
if data1.len() <= 16 || data2.len() <= 16 {
return self.calculate_byte_similarity(data1, data2);
}
let window_size = std::cmp::min(8, std::cmp::min(data1.len(), data2.len()) / 4);
if window_size == 0 {
return self.calculate_byte_similarity(data1, data2);
}
let mut matches = 0;
let mut total_windows = 0;
for i in 0..=data1.len().saturating_sub(window_size) {
total_windows += 1;
let window1 = &data1[i..i + window_size];
let mut found_match = false;
for j in 0..=data2.len().saturating_sub(window_size) {
let window2 = &data2[j..j + window_size];
if window1 == window2 {
matches += 1;
found_match = true;
break;
}
}
if !found_match {
let mut best_partial_match = 0;
for j in 0..=data2.len().saturating_sub(window_size) {
let window2 = &data2[j..j + window_size];
let partial_matches = window1.iter()
.zip(window2.iter())
.filter(|(a, b)| a == b)
.count();
best_partial_match = best_partial_match.max(partial_matches);
}
if best_partial_match > window_size / 2 {
matches += best_partial_match / window_size;
}
}
}
if total_windows == 0 {
0.0
} else {
matches as f32 / total_windows as f32
}
}
fn calculate_byte_similarity(&self, data1: &[u8], data2: &[u8]) -> f32 {
let max_len = std::cmp::max(data1.len(), data2.len());
if max_len == 0 {
return 1.0;
}
let min_len = std::cmp::min(data1.len(), data2.len());
let matches = data1.iter()
.take(min_len)
.zip(data2.iter().take(min_len))
.filter(|(a, b)| a == b)
.count();
matches as f32 / max_len as f32
}
pub fn find_best_base(&self, data: &[u8], file_type: &str) -> Option<SimilarityMatch> {
let mut best_match = None;
let mut best_similarity = 0.0;
for (base_id, base_data) in &self.base_files {
if let Some(base_info) = self.base_file_info.get(base_id) {
let type_bonus = if base_info.file_type == file_type { 0.1 } else { 0.0 };
let similarity = self.calculate_similarity(data, base_data) + type_bonus;
if similarity > best_similarity && similarity >= self.similarity_threshold {
best_similarity = similarity;
let estimated_compression = 1.0 - (1.0 - similarity) * 0.8;
best_match = Some(SimilarityMatch {
base_storage_id: base_id.clone(),
similarity_score: similarity,
estimated_compression,
});
}
}
}
best_match
}
pub fn create_delta(&self, base_data: &[u8], target_data: &[u8]) -> Result<Vec<u8>> {
match self.delta_algorithm {
DeltaAlgorithm::Simple => self.create_simple_delta(base_data, target_data),
DeltaAlgorithm::XDelta => {
Err(anyhow!("XDelta algorithm not implemented yet"))
}
DeltaAlgorithm::BsDiff => {
Err(anyhow!("BsDiff algorithm not implemented yet"))
}
}
}
fn create_simple_delta(&self, base_data: &[u8], target_data: &[u8]) -> Result<Vec<u8>> {
let mut delta = Vec::new();
delta.extend_from_slice(b"STOWR_DELTA_V1");
delta.extend_from_slice(&(base_data.len() as u64).to_le_bytes());
delta.extend_from_slice(&(target_data.len() as u64).to_le_bytes());
let mut i = 0;
while i < target_data.len() {
if i < base_data.len() && target_data[i] == base_data[i] {
let mut same_count = 0;
while i + same_count < target_data.len()
&& i + same_count < base_data.len()
&& target_data[i + same_count] == base_data[i + same_count] {
same_count += 1;
}
delta.push(0x01); delta.extend_from_slice(&(same_count as u32).to_le_bytes());
i += same_count;
} else {
let diff_start = i;
while i < target_data.len()
&& (i >= base_data.len() || target_data[i] != base_data[i]) {
i += 1;
}
let diff_len = i - diff_start;
delta.push(0x02); delta.extend_from_slice(&(diff_len as u32).to_le_bytes());
delta.extend_from_slice(&target_data[diff_start..i]);
}
}
Ok(delta)
}
pub fn apply_delta(&self, base_data: &[u8], delta_data: &[u8]) -> Result<Vec<u8>> {
if delta_data.len() < 22 { return Err(anyhow!("Invalid delta data: too short"));
}
if &delta_data[0..14] != b"STOWR_DELTA_V1" {
return Err(anyhow!("Invalid delta data: wrong header"));
}
let base_len = u64::from_le_bytes(
delta_data[14..22].try_into().map_err(|_| anyhow!("Invalid base length"))?
) as usize;
let target_len = u64::from_le_bytes(
delta_data[22..30].try_into().map_err(|_| anyhow!("Invalid target length"))?
) as usize;
if base_data.len() != base_len {
return Err(anyhow!("Base data length mismatch"));
}
let mut result = Vec::with_capacity(target_len);
let mut delta_pos = 30;
let mut base_pos = 0;
while delta_pos < delta_data.len() {
let command = delta_data[delta_pos];
delta_pos += 1;
match command {
0x01 => { if delta_pos + 4 > delta_data.len() {
return Err(anyhow!("Invalid COPY command"));
}
let copy_len = u32::from_le_bytes(
delta_data[delta_pos..delta_pos + 4].try_into().unwrap()
) as usize;
delta_pos += 4;
if base_pos + copy_len > base_data.len() {
return Err(anyhow!("COPY command out of bounds"));
}
result.extend_from_slice(&base_data[base_pos..base_pos + copy_len]);
base_pos += copy_len;
}
0x02 => { if delta_pos + 4 > delta_data.len() {
return Err(anyhow!("Invalid INSERT command"));
}
let insert_len = u32::from_le_bytes(
delta_data[delta_pos..delta_pos + 4].try_into().unwrap()
) as usize;
delta_pos += 4;
if delta_pos + insert_len > delta_data.len() {
return Err(anyhow!("INSERT command out of bounds"));
}
result.extend_from_slice(&delta_data[delta_pos..delta_pos + insert_len]);
delta_pos += insert_len;
}
_ => return Err(anyhow!("Unknown delta command: {}", command)),
}
}
if result.len() != target_len {
return Err(anyhow!("Reconstructed file size mismatch"));
}
Ok(result)
}
pub fn add_base_file(&mut self, storage_id: String, data: Vec<u8>, file_type: String) {
let info = BaseFileInfo {
size: data.len() as u64,
file_type,
created_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
reference_count: 0,
};
self.base_files.insert(storage_id.clone(), data);
self.base_file_info.insert(storage_id, info);
}
pub fn remove_base_file(&mut self, storage_id: &str) -> bool {
if let Some(info) = self.base_file_info.get(storage_id) {
if info.reference_count == 0 {
self.base_files.remove(storage_id);
self.base_file_info.remove(storage_id);
true
} else {
false }
} else {
self.base_files.remove(storage_id);
true
}
}
pub fn increment_reference(&mut self, storage_id: &str) {
if let Some(info) = self.base_file_info.get_mut(storage_id) {
info.reference_count += 1;
}
}
pub fn decrement_reference(&mut self, storage_id: &str) -> bool {
if let Some(info) = self.base_file_info.get_mut(storage_id) {
if info.reference_count > 0 {
info.reference_count -= 1;
}
info.reference_count == 0
} else {
true }
}
pub fn get_base_file_data(&self, storage_id: &str) -> Option<&[u8]> {
self.base_files.get(storage_id).map(|v| v.as_slice())
}
pub fn get_stats(&self) -> DeltaStats {
let total_base_files = self.base_files.len() as u32;
let total_references = self.base_file_info.values()
.map(|info| info.reference_count)
.sum::<u32>();
DeltaStats {
total_base_files,
total_delta_files: total_references,
average_similarity: 0.0, storage_savings: 0.0, }
}
pub fn infer_file_type(file_path: &std::path::Path) -> String {
file_path.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("unknown")
.to_lowercase()
}
}
#[derive(Debug, Clone)]
pub struct DeltaStats {
pub total_base_files: u32,
pub total_delta_files: u32,
pub average_similarity: f32,
pub storage_savings: f32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_similarity_calculation() {
let delta_storage = DeltaStorage::new(0.7, DeltaAlgorithm::Simple);
let data1 = b"Hello World";
let data2 = b"Hello World";
let data3 = b"Hello Rust";
let identical_similarity = delta_storage.calculate_similarity(data1, data2);
assert!((identical_similarity - 1.0).abs() < 0.1, "Identical files should have similarity close to 1.0, got: {}", identical_similarity);
let partial_similarity = delta_storage.calculate_similarity(data1, data3);
assert!(partial_similarity >= 0.0 && partial_similarity <= 1.0, "Similarity should be between 0.0 and 1.0, got: {}", partial_similarity);
let similar_data1 = b"Hello World Test";
let similar_data2 = b"Hello World Best";
let similar_similarity = delta_storage.calculate_similarity(similar_data1, similar_data2);
assert!(similar_similarity > 0.0, "Similar texts should have similarity > 0.0, got: {}", similar_similarity);
let diff_data1 = b"AAAAAAAAAA";
let diff_data2 = b"BBBBBBBBBB";
let diff_similarity = delta_storage.calculate_similarity(diff_data1, diff_data2);
assert!(diff_similarity == 0.0, "Completely different data should have similarity 0.0, got: {}", diff_similarity);
}
#[test]
fn test_simple_delta() {
let delta_storage = DeltaStorage::new(0.7, DeltaAlgorithm::Simple);
let base_data = b"Hello World";
let target_data = b"Hello Rust World";
let delta = delta_storage.create_delta(base_data, target_data).unwrap();
let reconstructed = delta_storage.apply_delta(base_data, &delta).unwrap();
assert_eq!(reconstructed, target_data);
}
#[test]
fn test_file_type_inference() {
use std::path::Path;
assert_eq!(DeltaStorage::infer_file_type(Path::new("test.txt")), "txt");
assert_eq!(DeltaStorage::infer_file_type(Path::new("image.png")), "png");
assert_eq!(DeltaStorage::infer_file_type(Path::new("noext")), "unknown");
}
}