Skip to main content

argyph_fs/
hash.rs

1use camino::{Utf8Path, Utf8PathBuf};
2use rayon::prelude::*;
3use std::io;
4
5/// BLAKE3 hash output — 32 bytes (256 bits).
6///
7/// `Display` and `Debug` formats produce lowercase hexadecimal.
8#[derive(Clone, Copy, PartialEq, Eq, Hash)]
9pub struct Blake3Hash([u8; 32]);
10
11impl Blake3Hash {
12    /// Return the raw 32-byte hash.
13    #[inline]
14    pub fn as_bytes(&self) -> &[u8; 32] {
15        &self.0
16    }
17}
18
19impl std::fmt::Debug for Blake3Hash {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        write!(f, "Blake3Hash({self})")
22    }
23}
24
25impl std::fmt::Display for Blake3Hash {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        for byte in &self.0 {
28            write!(f, "{byte:02x}")?;
29        }
30        Ok(())
31    }
32}
33
34impl From<[u8; 32]> for Blake3Hash {
35    fn from(bytes: [u8; 32]) -> Self {
36        Self(bytes)
37    }
38}
39
40impl AsRef<[u8; 32]> for Blake3Hash {
41    fn as_ref(&self) -> &[u8; 32] {
42        &self.0
43    }
44}
45
46impl AsRef<[u8]> for Blake3Hash {
47    fn as_ref(&self) -> &[u8] {
48        &self.0
49    }
50}
51
52/// Compute the BLAKE3 hash of a file's contents, parallelized via rayon.
53///
54/// Reads the entire file into memory. Callers should enforce a size cap
55/// before calling this on untrusted file paths.
56pub fn hash_file(path: &Utf8Path) -> io::Result<Blake3Hash> {
57    let data = std::fs::read(path.as_std_path())?;
58    let hash = blake3::hash(&data);
59    Ok(Blake3Hash(*hash.as_bytes()))
60}
61
62/// Compute BLAKE3 hashes for multiple file-data pairs in parallel using rayon.
63pub fn hash_files_parallel(files: &[(Utf8PathBuf, Vec<u8>)]) -> Vec<(Utf8PathBuf, Blake3Hash)> {
64    files
65        .par_iter()
66        .map(|(path, data)| {
67            let hash = blake3::hash(data);
68            (path.clone(), Blake3Hash(*hash.as_bytes()))
69        })
70        .collect()
71}
72
73#[cfg(test)]
74#[allow(clippy::unwrap_used)]
75mod tests {
76    use super::*;
77    use camino::Utf8PathBuf;
78    use std::io::Write;
79
80    fn temp_utf8_path(name: &str) -> Utf8PathBuf {
81        let dir = std::env::temp_dir();
82        Utf8PathBuf::from_path_buf(dir.join(name)).unwrap()
83    }
84
85    #[test]
86    fn hash_deterministic() {
87        let path = temp_utf8_path("argyph_test_hash_det");
88        let mut f = std::fs::File::create(path.as_std_path()).unwrap();
89        f.write_all(b"hello arghash").unwrap();
90        drop(f);
91
92        let h1 = hash_file(&path).unwrap();
93        let h2 = hash_file(&path).unwrap();
94        assert_eq!(h1, h2);
95
96        std::fs::remove_file(path.as_std_path()).unwrap();
97    }
98
99    #[test]
100    fn hash_different_content() {
101        let p1 = temp_utf8_path("argyph_test_hash_a");
102        let p2 = temp_utf8_path("argyph_test_hash_b");
103        std::fs::write(p1.as_std_path(), b"aaa").unwrap();
104        std::fs::write(p2.as_std_path(), b"bbb").unwrap();
105
106        let h1 = hash_file(&p1).unwrap();
107        let h2 = hash_file(&p2).unwrap();
108        assert_ne!(h1, h2);
109
110        std::fs::remove_file(p1.as_std_path()).unwrap();
111        std::fs::remove_file(p2.as_std_path()).unwrap();
112    }
113
114    #[test]
115    fn display_is_hex() {
116        let hash = Blake3Hash::from([0u8; 32]);
117        let s = hash.to_string();
118        assert_eq!(s.len(), 64);
119        assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
120    }
121
122    #[test]
123    fn hash_missing_file() {
124        let path = temp_utf8_path("argyph_nonexistent_xyz");
125        let _ = std::fs::remove_file(path.as_std_path());
126        assert!(hash_file(&path).is_err());
127    }
128}