lcpfs 2026.1.102

LCP File System - A ZFS-inspired copy-on-write filesystem for Rust
// Copyright 2025 LunaOS Contributors
// SPDX-License-Identifier: Apache-2.0

//! Delta Synchronization for LCPFS.
//!
//! This module provides efficient delta-based file synchronization,
//! similar to rsync's algorithm. It transfers only the changed portions
//! of files, minimizing bandwidth usage for large file updates.
//!
//! # Algorithm Overview
//!
//! 1. **Signature Generation**: Break source file into blocks, compute
//!    rolling (Adler-32) and strong (BLAKE3-style) checksums for each.
//!
//! 2. **Delta Computation**: Scan target file with rolling checksum,
//!    looking for blocks that match source. Emit copy operations for
//!    matches, insert operations for new data.
//!
//! 3. **Delta Application**: Use source file and delta operations to
//!    reconstruct the target file.
//!
//! # Example
//!
//! ```ignore
//! use lcpfs::delta::{generate_signatures, compute_delta, apply_delta};
//!
//! // Source file (what we have)
//! let source = b"original file content here";
//!
//! // Generate signatures from source
//! let sigs = generate_signatures(source, 512)?;
//!
//! // Target file (what we want)
//! let target = b"modified file content here";
//!
//! // Compute delta
//! let delta = compute_delta(&sigs, target)?;
//!
//! // Apply delta to get target from source
//! let result = apply_delta(source, &delta)?;
//! assert_eq!(&result, target);
//! ```
//!
//! # Sync Operations
//!
//! For full directory synchronization:
//!
//! ```ignore
//! use lcpfs::delta::{plan_sync, execute_sync, FileInfo};
//!
//! // List files in source and dest
//! let source_files = list_files("/source")?;
//! let dest_files = list_files("/dest")?;
//!
//! // Create sync plan
//! let plan = plan_sync("/source", "/dest", &source_files, &dest_files, timestamp);
//!
//! // Execute sync
//! execute_sync(&plan, &source_fs, &mut dest_fs, Some(progress_cb), timestamp)?;
//! ```
//!
//! # Performance
//!
//! - Block size adapts to file size for optimal matching
//! - Rolling checksum enables O(1) per-byte scanning
//! - Contiguous operations are merged to reduce overhead
//! - Memory usage bounded by periodic flushing

pub mod compute;
pub mod sync;
pub mod types;

// Re-exports
pub use compute::{
    apply_delta, compute_delta, create_full_insert_delta, create_identity_delta,
    generate_signatures, generate_signatures_default, optimal_block_size,
};
pub use sync::{
    FileInfo, MemoryFs, SyncFilesystem, apply_file_delta, compute_file_delta,
    compute_new_file_delta, estimate_transfer_size, execute_sync, plan_sync,
};
pub use types::{
    BlockSignature, DEFAULT_BLOCK_SIZE, Delta, DeltaError, DeltaOp, DeltaResult, MAX_BLOCK_SIZE,
    MIN_BLOCK_SIZE, RollingChecksum, SignatureSet, SyncEntry, SyncPlan, SyncProgress, SyncStatus,
};

// ═══════════════════════════════════════════════════════════════════════════════
// INTEGRATION TESTS
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::vec;
    use alloc::vec::Vec;

    #[test]
    fn test_exports_accessible() {
        // Verify all re-exports are accessible
        let _ = RollingChecksum::new(512);
        let _ = DeltaOp::copy(0, 100);
        let _ = SyncStatus::New;
        let _ = DEFAULT_BLOCK_SIZE;
    }

    #[test]
    fn test_full_workflow() {
        // Create larger original file (multiple blocks)
        let mut original = Vec::with_capacity(8192);
        for i in 0..8192 {
            original.push((i % 256) as u8);
        }

        // Generate signatures with smaller block size
        let sigs = generate_signatures(&original, 512).unwrap();

        // Modify a small portion in the middle
        let mut modified = original.clone();
        modified[4000..4100].fill(0xFF);

        // Compute delta
        let delta = compute_delta(&sigs, &modified).unwrap();

        // Delta should have some copy operations
        assert!(!delta.is_full_replace());

        // Apply delta
        let result = apply_delta(&original, &delta).unwrap();
        assert_eq!(result, modified);
    }

    #[test]
    fn test_large_file_workflow() {
        // Create larger test data
        let mut original = Vec::with_capacity(100_000);
        for i in 0..100_000 {
            original.push((i % 256) as u8);
        }

        // Modify a small portion
        let mut modified = original.clone();
        modified[50000..50100].fill(0xFF);

        // Generate signatures
        let block_size = optimal_block_size(original.len() as u64);
        let sigs = generate_signatures(&original, block_size).unwrap();

        // Compute delta
        let delta = compute_delta(&sigs, &modified).unwrap();

        // Should have significant copy operations
        assert!(delta.copy_percentage() > 90.0);

        // Apply delta
        let result = apply_delta(&original, &delta).unwrap();
        assert_eq!(result, modified);
    }

    #[test]
    fn test_sync_workflow() {
        let mut source = MemoryFs::new();
        let mut dest = MemoryFs::new();

        // Setup source
        source.add_file("/doc1.txt", b"Document one content", 1000);
        source.add_file("/doc2.txt", b"Document two content", 1000);
        source.add_file("/new.txt", b"New document", 2000);

        // Setup dest (slightly different)
        dest.add_file("/doc1.txt", b"Document one content", 1000);
        dest.add_file("/doc2.txt", b"Old document two", 500);
        dest.add_file("/obsolete.txt", b"Delete this", 100);

        // Create and execute sync plan
        let source_files = source.list_files("/").unwrap();
        let dest_files = dest.list_files("/").unwrap();
        let plan = plan_sync("/src", "/dst", &source_files, &dest_files, 0);

        // Should have:
        // - 1 unchanged (doc1.txt)
        // - 1 modified (doc2.txt)
        // - 1 new (new.txt)
        // - 1 deleted (obsolete.txt)
        assert_eq!(plan.count_by_status(SyncStatus::Unchanged), 1);
        assert_eq!(plan.count_by_status(SyncStatus::Modified), 1);
        assert_eq!(plan.count_by_status(SyncStatus::New), 1);
        assert_eq!(plan.count_by_status(SyncStatus::Deleted), 1);

        // Execute sync
        let progress =
            execute_sync(&plan, &source, &mut dest, None::<fn(&SyncProgress)>, 0).unwrap();

        // Verify
        assert!(dest.exists("/doc1.txt"));
        assert!(dest.exists("/doc2.txt"));
        assert!(dest.exists("/new.txt"));
        assert!(!dest.exists("/obsolete.txt"));

        assert_eq!(
            dest.read_file("/doc2.txt").unwrap(),
            b"Document two content"
        );
    }

    #[test]
    fn test_incremental_updates() {
        // Simulate incremental backup scenario
        let mut source = MemoryFs::new();
        let mut dest = MemoryFs::new();

        // Initial sync
        source.add_file("/data.bin", &[0u8; 10000], 1000);
        let src1 = source.list_files("/").unwrap();
        let dst1 = dest.list_files("/").unwrap();
        let plan1 = plan_sync("/s", "/d", &src1, &dst1, 0);
        execute_sync(&plan1, &source, &mut dest, None::<fn(&SyncProgress)>, 0).unwrap();

        // Small update
        let mut new_data = [0u8; 10000];
        new_data[5000..5100].fill(0xFF);
        source.add_file("/data.bin", &new_data, 2000);

        let src2 = source.list_files("/").unwrap();
        let dst2 = dest.list_files("/").unwrap();
        let plan2 = plan_sync("/s", "/d", &src2, &dst2, 0);

        // Should be a modification, not a new file
        assert_eq!(plan2.count_by_status(SyncStatus::Modified), 1);

        // Transfer should be small (just the delta)
        let est = estimate_transfer_size(&plan2);
        assert!(est < 5000); // Much less than full file

        execute_sync(&plan2, &source, &mut dest, None::<fn(&SyncProgress)>, 0).unwrap();

        // Verify content
        let result = dest.read_file("/data.bin").unwrap();
        assert_eq!(&result, &new_data);
    }

    #[test]
    fn test_empty_sync() {
        let source = MemoryFs::new();
        let dest = MemoryFs::new();

        let src = source.list_files("/").unwrap();
        let dst = dest.list_files("/").unwrap();
        let plan = plan_sync("/s", "/d", &src, &dst, 0);

        assert!(plan.is_empty());
    }

    #[test]
    fn test_rolling_checksum_api() {
        let mut rc = RollingChecksum::new(8);

        // Push initial data
        for &b in b"abcdefgh" {
            rc.push(b);
        }

        let checksum1 = rc.checksum();

        // Roll to bcdefghi
        rc.roll(b'i');
        let checksum2 = rc.checksum();

        assert_ne!(checksum1, checksum2);

        // Roll to cdefghij
        rc.roll(b'j');
        let checksum3 = rc.checksum();

        assert_ne!(checksum2, checksum3);
    }

    #[test]
    fn test_delta_statistics() {
        // Create varied data (not all same byte)
        let source: Vec<u8> = (0..10000).map(|i| (i % 256) as u8).collect();
        let mut target = source.clone();
        // Modify a small portion
        target[5000..5100].fill(0xFF);

        let sigs = generate_signatures(&source, 512).unwrap();
        let delta = compute_delta(&sigs, &target).unwrap();

        // Check statistics
        assert!(!delta.is_identity());
        assert!(!delta.is_full_replace());
        // Should have significant copy percentage
        assert!(delta.copy_percentage() > 80.0);
    }

    #[test]
    fn test_sync_progress() {
        let mut progress = SyncProgress::new(10, 10000, 0);

        progress.update("/file1.txt", 1000, 100);
        assert_eq!(progress.percentage(), 10.0);

        progress.update("/file2.txt", 2000, 200);
        assert_eq!(progress.percentage(), 30.0);

        assert!(progress.rate(200) > 0.0);
    }
}