Skip to main content

zoi_core/
hash.rs

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