1use camino::{Utf8Path, Utf8PathBuf};
2use rayon::prelude::*;
3use std::io;
4
5#[derive(Clone, Copy, PartialEq, Eq, Hash)]
9pub struct Blake3Hash([u8; 32]);
10
11impl Blake3Hash {
12 #[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
52pub 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
62pub 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}