msy 0.4.2

Modern musl rsync alternative - Fast, parallel file synchronization
Documentation
use crate::error::Result;
use std::path::Path;

mod blake3;
mod xxhash3;

pub use self::blake3::Blake3Hasher;
pub use self::xxhash3::XxHash3Hasher;

/// Type of checksum to compute
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChecksumType {
    /// No checksum verification (trust TCP)
    None,

    /// Fast non-cryptographic checksum (xxHash3)
    /// Good for detecting corruption but not malicious tampering
    Fast,

    /// Cryptographic checksum (BLAKE3)
    /// Slower but provides cryptographic guarantees
    #[allow(dead_code)]
    Cryptographic,
}

/// A computed checksum value
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Checksum {
    None,
    Fast(Vec<u8>),
    Cryptographic(Vec<u8>),
}

#[allow(dead_code)] // Public API for checksum operations
impl Checksum {
    /// Create a None checksum
    pub fn none() -> Self {
        Self::None
    }

    /// Create a Fast (xxHash3) checksum
    pub fn fast(hash: Vec<u8>) -> Self {
        Self::Fast(hash)
    }

    /// Create a Cryptographic (BLAKE3) checksum
    pub fn cryptographic(hash: Vec<u8>) -> Self {
        Self::Cryptographic(hash)
    }

    /// Check if this is a None checksum
    pub fn is_none(&self) -> bool {
        matches!(self, Self::None)
    }

    /// Get the checksum bytes, or None if this is a None checksum
    pub fn bytes(&self) -> Option<&[u8]> {
        match self {
            Self::None => None,
            Self::Fast(bytes) | Self::Cryptographic(bytes) => Some(bytes),
        }
    }

    /// Convert to hex string for display
    pub fn to_hex(&self) -> String {
        match self.bytes() {
            Some(bytes) => hex::encode(bytes),
            None => "none".to_string(),
        }
    }
}

/// Integrity verifier for file transfers
#[derive(Clone)]
pub struct IntegrityVerifier {
    checksum_type: ChecksumType,
    verify_on_write: bool,
}

#[allow(dead_code)] // Public API for integrity verification
impl IntegrityVerifier {
    /// Create a new integrity verifier
    pub fn new(checksum_type: ChecksumType, verify_on_write: bool) -> Self {
        Self {
            checksum_type,
            verify_on_write,
        }
    }

    /// Get the checksum type
    pub fn checksum_type(&self) -> ChecksumType {
        self.checksum_type
    }

    /// Check if paranoid mode is enabled (verify on write)
    pub fn verify_on_write(&self) -> bool {
        self.verify_on_write
    }

    /// Compute checksum for a file
    pub fn compute_file_checksum(&self, path: &Path) -> Result<Checksum> {
        match self.checksum_type {
            ChecksumType::None => Ok(Checksum::None),
            ChecksumType::Fast => {
                let hash = XxHash3Hasher::hash_file(path)?;
                Ok(Checksum::Fast(hash.to_le_bytes().to_vec()))
            }
            ChecksumType::Cryptographic => {
                let hash = Blake3Hasher::hash_file(path)?;
                Ok(Checksum::Cryptographic(hash.as_bytes().to_vec()))
            }
        }
    }

    /// Compute checksum for data in memory
    pub fn compute_data_checksum(&self, data: &[u8]) -> Result<Checksum> {
        match self.checksum_type {
            ChecksumType::None => Ok(Checksum::None),
            ChecksumType::Fast => {
                let hash = XxHash3Hasher::hash_data(data);
                Ok(Checksum::Fast(hash.to_le_bytes().to_vec()))
            }
            ChecksumType::Cryptographic => {
                let hash = Blake3Hasher::hash_data(data);
                Ok(Checksum::Cryptographic(hash.as_bytes().to_vec()))
            }
        }
    }

    /// Verify that source and destination files match
    pub fn verify_transfer(&self, source: &Path, dest: &Path) -> Result<bool> {
        let source_sum = self.compute_file_checksum(source)?;
        let dest_sum = self.compute_file_checksum(dest)?;
        Ok(source_sum == dest_sum)
    }

    /// Verify that a written block matches expected data
    ///
    /// This is used in paranoid mode to verify each block immediately after writing.
    /// Returns true if block matches, false if corrupted.
    pub fn verify_block(&self, expected_data: &[u8], actual_data: &[u8]) -> Result<bool> {
        if !self.verify_on_write {
            // Not in paranoid mode, skip verification
            return Ok(true);
        }

        let expected_sum = self.compute_data_checksum(expected_data)?;
        let actual_sum = self.compute_data_checksum(actual_data)?;
        Ok(expected_sum == actual_sum)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    #[test]
    fn test_checksum_none() {
        let checksum = Checksum::none();
        assert!(checksum.is_none());
        assert_eq!(checksum.bytes(), None);
        assert_eq!(checksum.to_hex(), "none");
    }

    #[test]
    fn test_checksum_fast() {
        let checksum = Checksum::fast(vec![1, 2, 3, 4]);
        assert!(!checksum.is_none());
        assert_eq!(checksum.bytes(), Some(&[1, 2, 3, 4][..]));
        assert_eq!(checksum.to_hex(), "01020304");
    }

    #[test]
    fn test_checksum_cryptographic() {
        let checksum = Checksum::cryptographic(vec![0xde, 0xad, 0xbe, 0xef]);
        assert!(!checksum.is_none());
        assert_eq!(checksum.bytes(), Some(&[0xde, 0xad, 0xbe, 0xef][..]));
        assert_eq!(checksum.to_hex(), "deadbeef");
    }

    #[test]
    fn test_checksum_equality() {
        let cs1 = Checksum::fast(vec![1, 2, 3]);
        let cs2 = Checksum::fast(vec![1, 2, 3]);
        let cs3 = Checksum::fast(vec![4, 5, 6]);

        assert_eq!(cs1, cs2);
        assert_ne!(cs1, cs3);
    }

    #[test]
    fn test_verifier_none() {
        let verifier = IntegrityVerifier::new(ChecksumType::None, false);
        assert_eq!(verifier.checksum_type(), ChecksumType::None);
        assert!(!verifier.verify_on_write());

        let checksum = verifier.compute_data_checksum(b"test data").unwrap();
        assert!(checksum.is_none());
    }

    #[test]
    fn test_verifier_cryptographic() {
        let verifier = IntegrityVerifier::new(ChecksumType::Cryptographic, false);
        assert_eq!(verifier.checksum_type(), ChecksumType::Cryptographic);

        let data = b"Hello, world!";
        let checksum = verifier.compute_data_checksum(data).unwrap();
        assert!(!checksum.is_none());

        // Same data should produce same checksum
        let checksum2 = verifier.compute_data_checksum(data).unwrap();
        assert_eq!(checksum, checksum2);

        // Different data should produce different checksum
        let checksum3 = verifier.compute_data_checksum(b"Different data").unwrap();
        assert_ne!(checksum, checksum3);
    }

    #[test]
    fn test_verifier_file_checksum() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.txt");
        fs::write(&file_path, b"Test file content").unwrap();

        let verifier = IntegrityVerifier::new(ChecksumType::Cryptographic, false);
        let checksum = verifier.compute_file_checksum(&file_path).unwrap();
        assert!(!checksum.is_none());
    }

    #[test]
    fn test_verify_transfer() {
        let temp_dir = TempDir::new().unwrap();
        let source_path = temp_dir.path().join("source.txt");
        let dest_path = temp_dir.path().join("dest.txt");

        fs::write(&source_path, b"File content").unwrap();
        fs::write(&dest_path, b"File content").unwrap();

        let verifier = IntegrityVerifier::new(ChecksumType::Cryptographic, false);
        assert!(verifier.verify_transfer(&source_path, &dest_path).unwrap());

        // Change destination content
        fs::write(&dest_path, b"Different content").unwrap();
        assert!(!verifier.verify_transfer(&source_path, &dest_path).unwrap());
    }

    #[test]
    fn test_verifier_fast() {
        let verifier = IntegrityVerifier::new(ChecksumType::Fast, false);
        assert_eq!(verifier.checksum_type(), ChecksumType::Fast);

        let data = b"Hello, xxHash3!";
        let checksum = verifier.compute_data_checksum(data).unwrap();
        assert!(!checksum.is_none());

        // Same data should produce same checksum
        let checksum2 = verifier.compute_data_checksum(data).unwrap();
        assert_eq!(checksum, checksum2);

        // Different data should produce different checksum
        let checksum3 = verifier.compute_data_checksum(b"Different data").unwrap();
        assert_ne!(checksum, checksum3);
    }

    #[test]
    fn test_verifier_fast_file() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.txt");
        fs::write(&file_path, b"Test file content").unwrap();

        let verifier = IntegrityVerifier::new(ChecksumType::Fast, false);
        let checksum = verifier.compute_file_checksum(&file_path).unwrap();
        assert!(!checksum.is_none());

        // Same file should produce same checksum
        let checksum2 = verifier.compute_file_checksum(&file_path).unwrap();
        assert_eq!(checksum, checksum2);
    }

    #[test]
    fn test_verify_transfer_fast() {
        let temp_dir = TempDir::new().unwrap();
        let source_path = temp_dir.path().join("source.txt");
        let dest_path = temp_dir.path().join("dest.txt");

        fs::write(&source_path, b"File content").unwrap();
        fs::write(&dest_path, b"File content").unwrap();

        let verifier = IntegrityVerifier::new(ChecksumType::Fast, false);
        assert!(verifier.verify_transfer(&source_path, &dest_path).unwrap());

        // Change destination content
        fs::write(&dest_path, b"Different content").unwrap();
        assert!(!verifier.verify_transfer(&source_path, &dest_path).unwrap());
    }

    #[test]
    fn test_verify_block_disabled() {
        let verifier = IntegrityVerifier::new(ChecksumType::Cryptographic, false);
        let data1 = b"Hello, world!";
        let data2 = b"Goodbye, world!";

        // With verify_on_write = false, should always return true
        assert!(verifier.verify_block(data1, data2).unwrap());
    }

    #[test]
    fn test_verify_block_matching() {
        let verifier = IntegrityVerifier::new(ChecksumType::Cryptographic, true);
        let data = b"Test data for block verification";

        // Same data should verify successfully
        assert!(verifier.verify_block(data, data).unwrap());
    }

    #[test]
    fn test_verify_block_mismatched() {
        let verifier = IntegrityVerifier::new(ChecksumType::Cryptographic, true);
        let expected = b"Expected data";
        let actual = b"Different data";

        // Different data should fail verification
        assert!(!verifier.verify_block(expected, actual).unwrap());
    }

    #[test]
    fn test_verify_block_fast_checksum() {
        let verifier = IntegrityVerifier::new(ChecksumType::Fast, true);
        let data = b"Fast checksum test data";

        // Should work with fast checksums too
        assert!(verifier.verify_block(data, data).unwrap());

        let corrupted = b"Corrupted checksum data";
        assert!(!verifier.verify_block(data, corrupted).unwrap());
    }
}