use super::{Delta, DeltaOp};
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)] pub struct DeltaStats {
pub operations_count: usize,
pub literal_bytes: u64,
pub bytes_written: u64,
}
#[allow(dead_code)] pub fn apply_delta(old_file: &Path, delta: &Delta, new_file: &Path) -> io::Result<DeltaStats> {
let mut old = File::open(old_file)?;
let mut new = File::create(new_file)?;
let mut literal_bytes = 0u64;
let mut bytes_written = 0u64;
for op in &delta.ops {
match op {
DeltaOp::Copy { offset, size } => {
old.seek(SeekFrom::Start(*offset))?;
let mut buffer = vec![0u8; *size];
old.read_exact(&mut buffer)?;
new.write_all(&buffer)?;
bytes_written += *size as u64;
}
DeltaOp::Data(data) => {
new.write_all(data)?;
literal_bytes += data.len() as u64;
bytes_written += data.len() as u64;
}
}
}
new.flush()?;
Ok(DeltaStats { operations_count: delta.ops.len(), literal_bytes, bytes_written })
}
#[allow(dead_code)]
pub fn apply_delta_no_base(delta: &Delta, new_file: &Path) -> io::Result<()> {
let mut new = File::create(new_file)?;
for op in &delta.ops {
match op {
DeltaOp::Copy { .. } => {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Cannot apply Copy operation without base file"));
}
DeltaOp::Data(data) => {
new.write_all(data)?;
}
}
}
new.flush()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::delta::{compute_checksums, generate_delta};
use tempfile::{NamedTempFile, TempDir};
#[test]
fn test_apply_delta_identical() {
let mut original = NamedTempFile::new().unwrap();
let data = b"Hello, World! This is a test of delta sync.";
original.write_all(data).unwrap();
original.flush().unwrap();
let mut modified = NamedTempFile::new().unwrap();
modified.write_all(data).unwrap();
modified.flush().unwrap();
let checksums = compute_checksums(original.path(), 8).unwrap();
let delta = generate_delta(modified.path(), &checksums, 8).unwrap();
let temp_dir = TempDir::new().unwrap();
let reconstructed = temp_dir.path().join("reconstructed");
let _stats = apply_delta(original.path(), &delta, &reconstructed).unwrap();
let original_data = std::fs::read(modified.path()).unwrap();
let reconstructed_data = std::fs::read(&reconstructed).unwrap();
assert_eq!(original_data, reconstructed_data);
}
#[test]
fn test_apply_delta_modified() {
let mut original = NamedTempFile::new().unwrap();
original.write_all(b"AAAABBBBCCCCDDDD").unwrap();
original.flush().unwrap();
let mut modified = NamedTempFile::new().unwrap();
modified.write_all(b"AAAAXXXXYYYYDDDD").unwrap();
modified.flush().unwrap();
let block_size = 4;
let checksums = compute_checksums(original.path(), block_size).unwrap();
let delta = generate_delta(modified.path(), &checksums, block_size).unwrap();
let temp_dir = TempDir::new().unwrap();
let reconstructed = temp_dir.path().join("reconstructed");
let stats = apply_delta(original.path(), &delta, &reconstructed).unwrap();
let expected = std::fs::read(modified.path()).unwrap();
let actual = std::fs::read(&reconstructed).unwrap();
assert_eq!(expected, actual);
assert_eq!(stats.bytes_written, 16); assert_eq!(stats.literal_bytes, 8); }
#[test]
fn test_apply_delta_completely_new() {
let mut original = NamedTempFile::new().unwrap();
original.write_all(b"old data here").unwrap();
original.flush().unwrap();
let mut modified = NamedTempFile::new().unwrap();
modified.write_all(b"completely new content!").unwrap();
modified.flush().unwrap();
let checksums = compute_checksums(original.path(), 4).unwrap();
let delta = generate_delta(modified.path(), &checksums, 4).unwrap();
let temp_dir = TempDir::new().unwrap();
let reconstructed = temp_dir.path().join("reconstructed");
let _stats = apply_delta(original.path(), &delta, &reconstructed).unwrap();
let expected = std::fs::read(modified.path()).unwrap();
let actual = std::fs::read(&reconstructed).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn test_apply_delta_no_base() {
let mut source = NamedTempFile::new().unwrap();
let data = b"new file content";
source.write_all(data).unwrap();
source.flush().unwrap();
let delta = generate_delta(source.path(), &[], 4).unwrap();
let temp_dir = TempDir::new().unwrap();
let reconstructed = temp_dir.path().join("reconstructed");
apply_delta_no_base(&delta, &reconstructed).unwrap();
let reconstructed_data = std::fs::read(&reconstructed).unwrap();
assert_eq!(reconstructed_data, data);
}
#[test]
fn test_roundtrip_large_file() {
let mut original = NamedTempFile::new().unwrap();
let original_data: Vec<u8> = (0..10000).map(|i| (i % 256) as u8).collect();
original.write_all(&original_data).unwrap();
original.flush().unwrap();
let mut modified_data = original_data.clone();
for byte in &mut modified_data[2000..3000] {
*byte = 0xFF;
}
let mut modified = NamedTempFile::new().unwrap();
modified.write_all(&modified_data).unwrap();
modified.flush().unwrap();
let block_size = 512;
let checksums = compute_checksums(original.path(), block_size).unwrap();
let delta = generate_delta(modified.path(), &checksums, block_size).unwrap();
let temp_dir = TempDir::new().unwrap();
let reconstructed = temp_dir.path().join("reconstructed");
let stats = apply_delta(original.path(), &delta, &reconstructed).unwrap();
let reconstructed_data = std::fs::read(&reconstructed).unwrap();
assert_eq!(reconstructed_data, modified_data);
let ratio = delta.compression_ratio();
println!("Compression ratio: {:.2}%", ratio * 100.0);
assert!(ratio < 0.2);
assert_eq!(stats.bytes_written, 10000);
let stats_ratio = stats.literal_bytes as f64 / stats.bytes_written as f64;
assert!(stats_ratio < 0.2);
}
}