Skip to main content

zoi_core/
hash.rs

1use anyhow::{Result, anyhow};
2use sha2::{Digest, Sha256, Sha512};
3use std::fs;
4use std::io::Read;
5use std::path::Path;
6use walkdir::WalkDir;
7
8/// Supported hashing algorithms for integrity verification.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum HashAlgorithm {
11    /// SHA-512 (128 character hex string).
12    Sha512,
13    /// SHA-256 (64 character hex string).
14    Sha256,
15}
16
17impl HashAlgorithm {
18    pub fn from_len(len: usize) -> Option<Self> {
19        match len {
20            128 => Some(HashAlgorithm::Sha512),
21            64 => Some(HashAlgorithm::Sha256),
22            _ => None,
23        }
24    }
25
26    pub fn from_name(name: &str) -> Option<Self> {
27        match name.to_lowercase().as_str() {
28            "sha512" => Some(HashAlgorithm::Sha512),
29            "sha256" => Some(HashAlgorithm::Sha256),
30            _ => None,
31        }
32    }
33}
34
35/// Calculates the cryptographic hash of a single file.
36pub fn calculate_file_hash(path: &Path, algo: HashAlgorithm) -> Result<String> {
37    let mut file =
38        fs::File::open(path).map_err(|e| anyhow!("Failed to open file {:?}: {}", path, e))?;
39
40    match algo {
41        HashAlgorithm::Sha512 => {
42            let mut hasher = Sha512::new();
43            let mut buffer = [0; 8192];
44            loop {
45                let bytes_read = file.read(&mut buffer)?;
46                if bytes_read == 0 {
47                    break;
48                }
49                hasher.update(&buffer[..bytes_read]);
50            }
51            Ok(hex::encode(hasher.finalize()))
52        }
53        HashAlgorithm::Sha256 => {
54            let mut hasher = Sha256::new();
55            let mut buffer = [0; 8192];
56            loop {
57                let bytes_read = file.read(&mut buffer)?;
58                if bytes_read == 0 {
59                    break;
60                }
61                hasher.update(&buffer[..bytes_read]);
62            }
63            Ok(hex::encode(hasher.finalize()))
64        }
65    }
66}
67
68/// Calculates a recursive hash of an entire directory's contents.
69///
70/// Deterministic Algorithm:
71/// - Collects all files in the directory.
72/// - Sorts files by their relative paths to ensure consistency.
73/// - Hashes each file's relative path, its metadata length, and finally its raw content.
74///
75/// This provides a single SHA-512 "Snapshot" hash that represents the exact state
76/// of the directory, used for bit-for-bit reproducibility checks in Specification v2.
77pub fn calculate_dir_hash(path: &Path) -> Result<String> {
78    if !path.is_dir() {
79        return Err(anyhow!("Path is not a directory"));
80    }
81
82    let mut hasher = Sha512::new();
83    let mut paths = Vec::new();
84
85    for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
86        if entry.file_type().is_file() {
87            paths.push(entry.path().to_path_buf());
88        }
89    }
90
91    paths.sort();
92
93    for file_path in paths {
94        if let Ok(rel_path) = file_path.strip_prefix(path) {
95            let rel_path_str = rel_path.to_string_lossy().to_string().replace('\\', "/");
96            let path_bytes = rel_path_str.as_bytes();
97            hasher.update((path_bytes.len() as u64).to_le_bytes());
98            hasher.update(path_bytes);
99        }
100
101        let mut file = fs::File::open(&file_path)?;
102        let metadata = file.metadata()?;
103        hasher.update(metadata.len().to_le_bytes());
104
105        let mut buffer = [0; 8192];
106        loop {
107            let bytes_read = file.read(&mut buffer)?;
108            if bytes_read == 0 {
109                break;
110            }
111            hasher.update(&buffer[..bytes_read]);
112        }
113    }
114
115    Ok(hex::encode(hasher.finalize()))
116}