msy 0.4.2

Modern musl rsync alternative - Fast, parallel file synchronization
Documentation
use super::{Delta, DeltaOp};
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;

/// Statistics about delta application
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)] // Reserved for future remote sync implementation
pub struct DeltaStats {
    pub operations_count: usize,
    pub literal_bytes: u64,
    pub bytes_written: u64,
}

/// Apply delta operations to reconstruct a file
///
/// Reads from old_file (using Copy ops) and delta data (using Data ops)
/// to create new_file.
///
/// Returns statistics about the delta application.
#[allow(dead_code)] // Reserved for future remote sync implementation
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 } => {
                // Seek to position in old file
                old.seek(SeekFrom::Start(*offset))?;

                // Copy block
                let mut buffer = vec![0u8; *size];
                old.read_exact(&mut buffer)?;
                new.write_all(&buffer)?;
                bytes_written += *size as u64;
            }
            DeltaOp::Data(data) => {
                // Write literal 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,
    })
}

/// Apply delta when there's no old file (full reconstruction from literals)
#[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() {
        // Create original file
        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();

        // Create "modified" file (actually identical)
        let mut modified = NamedTempFile::new().unwrap();
        modified.write_all(data).unwrap();
        modified.flush().unwrap();

        // Generate delta
        let checksums = compute_checksums(original.path(), 8).unwrap();
        let delta = generate_delta(modified.path(), &checksums, 8).unwrap();

        // Apply delta
        let temp_dir = TempDir::new().unwrap();
        let reconstructed = temp_dir.path().join("reconstructed");
        let _stats = apply_delta(original.path(), &delta, &reconstructed).unwrap();

        // Verify reconstructed file matches modified
        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() {
        // Create original file
        let mut original = NamedTempFile::new().unwrap();
        original.write_all(b"AAAABBBBCCCCDDDD").unwrap();
        original.flush().unwrap();

        // Create modified file (change middle blocks)
        let mut modified = NamedTempFile::new().unwrap();
        modified.write_all(b"AAAAXXXXYYYYDDDD").unwrap();
        modified.flush().unwrap();

        // Generate delta
        let block_size = 4;
        let checksums = compute_checksums(original.path(), block_size).unwrap();
        let delta = generate_delta(modified.path(), &checksums, block_size).unwrap();

        // Apply delta
        let temp_dir = TempDir::new().unwrap();
        let reconstructed = temp_dir.path().join("reconstructed");
        let stats = apply_delta(original.path(), &delta, &reconstructed).unwrap();

        // Verify
        let expected = std::fs::read(modified.path()).unwrap();
        let actual = std::fs::read(&reconstructed).unwrap();
        assert_eq!(expected, actual);

        // Verify delta stats
        assert_eq!(stats.bytes_written, 16); // Total file size
        assert_eq!(stats.literal_bytes, 8); // XXXX + YYYY
    }

    #[test]
    fn test_apply_delta_completely_new() {
        // Create original file
        let mut original = NamedTempFile::new().unwrap();
        original.write_all(b"old data here").unwrap();
        original.flush().unwrap();

        // Create completely different file
        let mut modified = NamedTempFile::new().unwrap();
        modified.write_all(b"completely new content!").unwrap();
        modified.flush().unwrap();

        // Generate delta
        let checksums = compute_checksums(original.path(), 4).unwrap();
        let delta = generate_delta(modified.path(), &checksums, 4).unwrap();

        // Apply delta
        let temp_dir = TempDir::new().unwrap();
        let reconstructed = temp_dir.path().join("reconstructed");
        let _stats = apply_delta(original.path(), &delta, &reconstructed).unwrap();

        // Verify
        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() {
        // Create file with only literal data
        let mut source = NamedTempFile::new().unwrap();
        let data = b"new file content";
        source.write_all(data).unwrap();
        source.flush().unwrap();

        // Generate delta with no base checksums
        let delta = generate_delta(source.path(), &[], 4).unwrap();

        // Apply without base file
        let temp_dir = TempDir::new().unwrap();
        let reconstructed = temp_dir.path().join("reconstructed");
        apply_delta_no_base(&delta, &reconstructed).unwrap();

        // Verify
        let reconstructed_data = std::fs::read(&reconstructed).unwrap();
        assert_eq!(reconstructed_data, data);
    }

    #[test]
    fn test_roundtrip_large_file() {
        // Create larger test 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();

        // Modify some blocks
        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();

        // Generate and apply delta
        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();

        // Verify
        let reconstructed_data = std::fs::read(&reconstructed).unwrap();
        assert_eq!(reconstructed_data, modified_data);

        // Check delta efficiency
        let ratio = delta.compression_ratio();
        println!("Compression ratio: {:.2}%", ratio * 100.0);
        assert!(ratio < 0.2); // Should transfer less than 20% (1000 bytes out of 10000)

        // Verify stats match
        assert_eq!(stats.bytes_written, 10000);
        let stats_ratio = stats.literal_bytes as f64 / stats.bytes_written as f64;
        assert!(stats_ratio < 0.2);
    }
}