1use std::fmt;
25use std::fs::File;
26use std::io::{self, Read};
27use std::path::Path;
28
29use crate::config::buffers::parse_buffer_size;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub struct Blake3Hash([u8; 32]);
37
38impl Blake3Hash {
39 #[inline]
50 #[must_use]
51 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
52 Self(bytes)
53 }
54
55 #[inline]
67 #[must_use]
68 pub const fn as_bytes(&self) -> &[u8; 32] {
69 &self.0
70 }
71
72 #[must_use]
84 pub fn to_hex(&self) -> String {
85 hex::encode(self.0)
86 }
87
88 pub fn from_hex(hex_str: &str) -> Result<Self, hex::FromHexError> {
104 let mut bytes = [0u8; 32];
105 hex::decode_to_slice(hex_str, &mut bytes)?;
106 Ok(Self(bytes))
107 }
108}
109
110impl fmt::Display for Blake3Hash {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 write!(f, "{}", self.to_hex())
113 }
114}
115
116impl serde::Serialize for Blake3Hash {
117 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
118 where
119 S: serde::Serializer,
120 {
121 if serializer.is_human_readable() {
122 serializer.serialize_str(&self.to_hex())
123 } else {
124 self.0.serialize(serializer)
126 }
127 }
128}
129
130impl<'de> serde::Deserialize<'de> for Blake3Hash {
131 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132 where
133 D: serde::Deserializer<'de>,
134 {
135 if deserializer.is_human_readable() {
136 let hex_str = String::deserialize(deserializer)?;
137 Self::from_hex(&hex_str).map_err(serde::de::Error::custom)
138 } else {
139 let bytes = <[u8; 32]>::deserialize(deserializer)?;
141 Ok(Self(bytes))
142 }
143 }
144}
145
146pub fn hash_file(path: &Path) -> io::Result<Blake3Hash> {
166 let mut file = File::open(path)?;
167 let mut hasher = blake3::Hasher::new();
168
169 let mut buffer = vec![0u8; parse_buffer_size()];
171 loop {
172 let bytes_read = file.read(&mut buffer)?;
173 if bytes_read == 0 {
174 break;
175 }
176 hasher.update(&buffer[..bytes_read]);
177 }
178
179 let hash = hasher.finalize();
180 Ok(Blake3Hash::from_bytes(*hash.as_bytes()))
181}
182
183#[inline]
196#[must_use]
197pub fn hash_bytes(content: &[u8]) -> Blake3Hash {
198 let hash = blake3::hash(content);
199 Blake3Hash::from_bytes(*hash.as_bytes())
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use std::io::Write;
206 use tempfile::NamedTempFile;
207
208 #[test]
209 fn test_hash_bytes_deterministic() {
210 let content = b"hello world";
211 let hash1 = hash_bytes(content);
212 let hash2 = hash_bytes(content);
213
214 assert_eq!(
215 hash1, hash2,
216 "Hashing same content should produce same hash"
217 );
218 }
219
220 #[test]
221 fn test_hash_bytes_different_content() {
222 let hash1 = hash_bytes(b"hello world");
223 let hash2 = hash_bytes(b"hello sqry");
224
225 assert_ne!(
226 hash1, hash2,
227 "Different content should produce different hashes"
228 );
229 }
230
231 #[test]
232 fn test_hash_empty_content() {
233 let hash = hash_bytes(b"");
234
235 let expected_hex = "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262";
237 assert_eq!(hash.to_hex(), expected_hex);
238 }
239
240 #[test]
241 fn test_hash_file() -> io::Result<()> {
242 let mut temp_file = NamedTempFile::new()?;
244 temp_file.write_all(b"test content for hashing")?;
245 temp_file.flush()?;
246
247 let hash = hash_file(temp_file.path())?;
248
249 let expected = hash_bytes(b"test content for hashing");
251 assert_eq!(hash, expected);
252
253 Ok(())
254 }
255
256 #[test]
257 fn test_hash_file_large() -> io::Result<()> {
258 let mut temp_file = NamedTempFile::new()?;
260 let large_content = vec![b'x'; 100_000]; temp_file.write_all(&large_content)?;
262 temp_file.flush()?;
263
264 let hash1 = hash_file(temp_file.path())?;
265 let hash2 = hash_bytes(&large_content);
266
267 assert_eq!(hash1, hash2, "Large file hash should match bytes hash");
268
269 Ok(())
270 }
271
272 #[test]
273 fn test_hash_file_nonexistent() {
274 let result = hash_file(Path::new("/nonexistent/file.txt"));
275
276 assert!(
277 result.is_err(),
278 "Hashing nonexistent file should return error"
279 );
280 }
281
282 #[test]
283 fn test_blake3hash_hex_roundtrip() {
284 let original = hash_bytes(b"test");
285 let hex = original.to_hex();
286 let parsed = Blake3Hash::from_hex(&hex).unwrap();
287
288 assert_eq!(original, parsed, "Hex encoding/decoding should roundtrip");
289 }
290
291 #[test]
292 fn test_blake3hash_display() {
293 let hash = hash_bytes(b"test");
294 let display = format!("{hash}");
295 let to_hex = hash.to_hex();
296
297 assert_eq!(display, to_hex, "Display should match to_hex()");
298 }
299
300 #[test]
301 fn test_blake3hash_from_hex_invalid() {
302 assert!(Blake3Hash::from_hex("abc").is_err());
304
305 assert!(
307 Blake3Hash::from_hex(
308 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
309 )
310 .is_err()
311 );
312
313 assert!(
315 Blake3Hash::from_hex("00000000000000000000000000000000000000000000000000000000000000")
316 .is_err()
317 );
318 }
319
320 #[test]
321 fn test_blake3hash_serde_json() {
322 let hash = hash_bytes(b"test");
323
324 let json = serde_json::to_string(&hash).unwrap();
326 assert!(json.contains(&hash.to_hex()));
327
328 let parsed: Blake3Hash = serde_json::from_str(&json).unwrap();
330 assert_eq!(hash, parsed);
331 }
332
333 #[test]
334 fn test_blake3hash_serde_postcard() {
335 let hash = hash_bytes(b"test");
336
337 let binary = postcard::to_allocvec(&hash).unwrap();
339 assert!(
342 binary.len() >= 32,
343 "Postcard should serialize at least the 32 hash bytes"
344 );
345
346 let parsed: Blake3Hash = postcard::from_bytes(&binary).unwrap();
348 assert_eq!(hash, parsed, "Roundtrip serialization should preserve hash");
349 }
350
351 #[test]
352 fn test_known_blake3_vectors() {
353 let hash = hash_bytes(b"");
358 assert_eq!(
359 hash.to_hex(),
360 "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
361 );
362
363 let hash = hash_bytes(&[0]);
365 assert_eq!(
366 hash.to_hex(),
367 "2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213"
368 );
369 }
370}