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 any reader, along with the total
84/// number of bytes consumed. Useful for hashing entries while streaming an
85/// archive without touching the filesystem.
86///
87/// # Errors
88///
89/// Returns an error if reading from the reader fails.
90pub fn calculate_reader_hash(
91    reader: &mut impl Read,
92    algo: HashAlgorithm
93) -> Result<(u64, String)> {
94    let mut buffer = [0u8; 8192];
95    let mut len: u64 = 0;
96
97    macro_rules! stream {
98        ($hasher:expr) => {{
99            loop {
100                let bytes_read = reader.read(&mut buffer)?;
101                if bytes_read == 0 {
102                    break;
103                }
104                len += bytes_read as u64;
105                $hasher.update(
106                    buffer
107                        .get(..bytes_read)
108                        .ok_or_else(|| anyhow!("Buffer overflow"))?
109                );
110            }
111            hex::encode($hasher.finalize())
112        }};
113    }
114
115    match algo {
116        HashAlgorithm::Sha512 => {
117            let mut hasher = Sha512::new();
118            let result = stream!(hasher);
119            Ok((len, result))
120        }
121        HashAlgorithm::Sha256 => {
122            let mut hasher = Sha256::new();
123            let result = stream!(hasher);
124            Ok((len, result))
125        }
126    }
127}
128
129/// Calculates the cryptographic hash of a string.
130pub fn calculate_string_hash(input: &str, algo: HashAlgorithm) -> String {
131    match algo {
132        HashAlgorithm::Sha512 => {
133            let mut hasher = Sha512::new();
134            hasher.update(input.as_bytes());
135            hex::encode(hasher.finalize())
136        }
137        HashAlgorithm::Sha256 => {
138            let mut hasher = Sha256::new();
139            hasher.update(input.as_bytes());
140            hex::encode(hasher.finalize())
141        }
142    }
143}
144
145/// Calculates a recursive hash of an entire directory's contents.
146///
147/// Deterministic Algorithm:
148/// - Collects all files in the directory.
149/// - Sorts files by their relative paths to ensure consistency.
150/// - Hashes each file's relative path, its metadata length, and finally its raw
151///   content.
152///
153/// This provides a single SHA-512 "Snapshot" hash that represents the exact
154/// state of the directory, used for bit-for-bit reproducibility checks in
155/// Specification v2.
156///
157/// # Errors
158///
159/// Returns an error if:
160/// - The path is not a directory.
161/// - Any file within the directory cannot be read.
162pub fn calculate_dir_hash(path: &Path) -> Result<String> {
163    if !path.is_dir() {
164        return Err(anyhow!("Path is not a directory"));
165    }
166
167    let mut hasher = Sha512::new();
168    let mut paths = Vec::new();
169
170    for entry in WalkDir::new(path)
171        .into_iter()
172        .filter_map(std::result::Result::ok)
173    {
174        if entry.file_type().is_file() {
175            paths.push(entry.path().to_path_buf());
176        }
177    }
178
179    paths.sort();
180
181    for file_path in paths {
182        if let Ok(rel_path) = file_path.strip_prefix(path) {
183            let rel_path_str =
184                rel_path.to_string_lossy().to_string().replace('\\', "/");
185            let path_bytes = rel_path_str.as_bytes();
186            hasher.update((path_bytes.len() as u64).to_le_bytes());
187            hasher.update(path_bytes);
188        }
189
190        let mut file = fs::File::open(&file_path)?;
191        let metadata = file.metadata()?;
192        hasher.update(metadata.len().to_le_bytes());
193
194        let mut buffer = [0; 8192];
195        loop {
196            let bytes_read = file.read(&mut buffer)?;
197            if bytes_read == 0 {
198                break;
199            }
200            hasher.update(
201                buffer
202                    .get(..bytes_read)
203                    .ok_or_else(|| anyhow!("Buffer overflow"))?
204            );
205        }
206    }
207
208    Ok(hex::encode(hasher.finalize()))
209}