heroforge-core 0.2.2

Pure Rust core library for reading and writing Fossil SCM repositories
Documentation
use md5::Context as Md5Context;
use sha1::{Digest, Sha1};
use sha3::Sha3_256;

/// Compute SHA1 hash, return lowercase hex string
pub fn sha1_hex(data: &[u8]) -> String {
    let mut hasher = Sha1::new();
    hasher.update(data);
    hex::encode(hasher.finalize())
}

/// Compute SHA3-256 hash, return lowercase hex string
pub fn sha3_256_hex(data: &[u8]) -> String {
    let mut hasher = Sha3_256::new();
    Digest::update(&mut hasher, data);
    hex::encode(hasher.finalize())
}

/// Compute MD5 hash, return lowercase hex string
pub fn md5_hex(data: &[u8]) -> String {
    let mut ctx = Md5Context::new();
    ctx.consume(data);
    format!("{:x}", ctx.compute())
}

/// Heroforge checksum for manifest Z-card (MD5 of content before Z card)
pub fn manifest_checksum(content: &[u8]) -> String {
    md5_hex(content)
}

/// Heroforge delta checksum (sum of 32-bit words mod 2^32-1)
pub fn delta_checksum(data: &[u8]) -> u32 {
    let mut sum: u64 = 0;
    let mut chunks = data.chunks_exact(4);

    for chunk in &mut chunks {
        let word = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
        sum = sum.wrapping_add(word as u64);
    }

    // Handle remainder (pad with zeros)
    let remainder = chunks.remainder();
    if !remainder.is_empty() {
        let mut padded = [0u8; 4];
        padded[..remainder.len()].copy_from_slice(remainder);
        let word = u32::from_be_bytes(padded);
        sum = sum.wrapping_add(word as u64);
    }

    // Modulo 2^32 - 1
    while sum > 0xFFFFFFFF {
        sum = (sum & 0xFFFFFFFF) + (sum >> 32);
    }

    sum as u32
}

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

    #[test]
    fn test_sha1() {
        let hash = sha1_hex(b"hello world");
        assert_eq!(hash, "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed");
    }

    #[test]
    fn test_md5() {
        let hash = md5_hex(b"hello world");
        assert_eq!(hash, "5eb63bbbe01eeed093cb22bb8f5acdc3");
    }
}