zoi-core 1.25.3

Advanced Package Manager & Environment Orchestrator
Documentation
use std::fs;
use std::io::Read;
use std::path::Path;

use anyhow::{Result, anyhow};
use sha2::{Digest, Sha256, Sha512};
use walkdir::WalkDir;

/// Supported hashing algorithms for integrity verification.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashAlgorithm {
    /// SHA-512 (128 character hex string).
    Sha512,
    /// SHA-256 (64 character hex string).
    Sha256
}

impl HashAlgorithm {
    /// Returns the hash algorithm based on the length of the hex string.
    pub fn from_len(len: usize) -> Option<Self> {
        match len {
            128 => Some(HashAlgorithm::Sha512),
            64 => Some(HashAlgorithm::Sha256),
            _ => None
        }
    }

    /// Returns the hash algorithm based on its name.
    pub fn from_name(name: &str) -> Option<Self> {
        match name.to_lowercase().as_str() {
            "sha512" => Some(HashAlgorithm::Sha512),
            "sha256" => Some(HashAlgorithm::Sha256),
            _ => None
        }
    }
}

/// Calculates the cryptographic hash of a single file.
///
/// # Errors
///
/// Returns an error if the file cannot be opened or read.
pub fn calculate_file_hash(path: &Path, algo: HashAlgorithm) -> Result<String> {
    let mut file = fs::File::open(path)
        .map_err(|e| anyhow!("Failed to open file {}: {e}", path.display()))?;

    match algo {
        HashAlgorithm::Sha512 => {
            let mut hasher = Sha512::new();
            let mut buffer = [0; 8192];
            loop {
                let bytes_read = file.read(&mut buffer)?;
                if bytes_read == 0 {
                    break;
                }
                hasher.update(
                    buffer
                        .get(..bytes_read)
                        .ok_or_else(|| anyhow!("Buffer overflow"))?
                );
            }
            Ok(hex::encode(hasher.finalize()))
        }
        HashAlgorithm::Sha256 => {
            let mut hasher = Sha256::new();
            let mut buffer = [0; 8192];
            loop {
                let bytes_read = file.read(&mut buffer)?;
                if bytes_read == 0 {
                    break;
                }
                hasher.update(
                    buffer
                        .get(..bytes_read)
                        .ok_or_else(|| anyhow!("Buffer overflow"))?
                );
            }
            Ok(hex::encode(hasher.finalize()))
        }
    }
}

/// Calculates the cryptographic hash of a string.
pub fn calculate_string_hash(input: &str, algo: HashAlgorithm) -> String {
    match algo {
        HashAlgorithm::Sha512 => {
            let mut hasher = Sha512::new();
            hasher.update(input.as_bytes());
            hex::encode(hasher.finalize())
        }
        HashAlgorithm::Sha256 => {
            let mut hasher = Sha256::new();
            hasher.update(input.as_bytes());
            hex::encode(hasher.finalize())
        }
    }
}

/// Calculates a recursive hash of an entire directory's contents.
///
/// Deterministic Algorithm:
/// - Collects all files in the directory.
/// - Sorts files by their relative paths to ensure consistency.
/// - Hashes each file's relative path, its metadata length, and finally its raw
///   content.
///
/// This provides a single SHA-512 "Snapshot" hash that represents the exact
/// state of the directory, used for bit-for-bit reproducibility checks in
/// Specification v2.
///
/// # Errors
///
/// Returns an error if:
/// - The path is not a directory.
/// - Any file within the directory cannot be read.
pub fn calculate_dir_hash(path: &Path) -> Result<String> {
    if !path.is_dir() {
        return Err(anyhow!("Path is not a directory"));
    }

    let mut hasher = Sha512::new();
    let mut paths = Vec::new();

    for entry in WalkDir::new(path)
        .into_iter()
        .filter_map(std::result::Result::ok)
    {
        if entry.file_type().is_file() {
            paths.push(entry.path().to_path_buf());
        }
    }

    paths.sort();

    for file_path in paths {
        if let Ok(rel_path) = file_path.strip_prefix(path) {
            let rel_path_str =
                rel_path.to_string_lossy().to_string().replace('\\', "/");
            let path_bytes = rel_path_str.as_bytes();
            hasher.update((path_bytes.len() as u64).to_le_bytes());
            hasher.update(path_bytes);
        }

        let mut file = fs::File::open(&file_path)?;
        let metadata = file.metadata()?;
        hasher.update(metadata.len().to_le_bytes());

        let mut buffer = [0; 8192];
        loop {
            let bytes_read = file.read(&mut buffer)?;
            if bytes_read == 0 {
                break;
            }
            hasher.update(
                buffer
                    .get(..bytes_read)
                    .ok_or_else(|| anyhow!("Buffer overflow"))?
            );
        }
    }

    Ok(hex::encode(hasher.finalize()))
}