1use std::io::Read;
10
11use crate::hash::Hash;
12use crate::CasError;
13
14const CONTEXT_DOMAIN: &[u8] = b"suture-context-v1";
18
19#[inline]
24#[must_use]
25pub fn hash_bytes(data: &[u8]) -> Hash {
26 Hash::from_data(data)
27}
28
29pub fn hash_file(path: &std::path::Path) -> Result<Hash, std::io::Error> {
34 let mut hasher = blake3::Hasher::new();
35 let file = std::fs::File::open(path)?;
36 let mut reader = std::io::BufReader::new(file);
37 const BUFFER_SIZE: usize = 64 * 1024; let mut buffer = [0u8; BUFFER_SIZE];
39
40 loop {
41 let n = reader.read(&mut buffer)?;
42 if n == 0 {
43 break;
44 }
45 hasher.update(&buffer[..n]);
46 }
47
48 Ok(Hash::from(*hasher.finalize().as_bytes()))
49}
50
51#[must_use]
57pub fn hash_with_context(context: &str, data: &[u8]) -> Hash {
58 let context_key = blake3::derive_key(context, CONTEXT_DOMAIN);
59 let mut hasher = blake3::Hasher::new_keyed(&context_key);
60 hasher.update(data);
61 Hash::from(*hasher.finalize().as_bytes())
62}
63
64pub fn verify_hash(data: &[u8], expected: &Hash) -> Result<(), CasError> {
68 let actual = hash_bytes(data);
69 if actual == *expected {
70 Ok(())
71 } else {
72 Err(CasError::HashMismatch {
73 expected: expected.to_hex(),
74 actual: actual.to_hex(),
75 })
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn test_hash_bytes_deterministic() {
85 let h1 = hash_bytes(b"hello");
86 let h2 = hash_bytes(b"hello");
87 assert_eq!(h1, h2);
88 }
89
90 #[test]
91 fn test_hash_bytes_different() {
92 let h1 = hash_bytes(b"hello");
93 let h2 = hash_bytes(b"world");
94 assert_ne!(h1, h2);
95 }
96
97 #[test]
98 fn test_hash_empty() {
99 let h = hash_bytes(b"");
100 let hex = h.to_hex();
102 assert_eq!(hex.len(), 64);
103 }
104
105 #[test]
106 fn test_hash_with_context_differs() {
107 let data = b"same data";
108 let h1 = hash_with_context("blob", data);
109 let h2 = hash_with_context("patch", data);
110 assert_ne!(h1, h2, "Different contexts must produce different hashes");
111 }
112
113 #[test]
114 fn test_verify_hash_ok() {
115 let data = b"test data";
116 let hash = hash_bytes(data);
117 assert!(verify_hash(data, &hash).is_ok());
118 }
119
120 #[test]
121 fn test_verify_hash_mismatch() {
122 let hash = hash_bytes(b"original");
123 assert!(verify_hash(b"tampered", &hash).is_err());
124 }
125
126 #[test]
127 fn test_hash_file() -> Result<(), Box<dyn std::error::Error>> {
128 let dir = tempfile::tempdir()?;
129 let file_path = dir.path().join("test.txt");
130 std::fs::write(&file_path, b"file content for hashing")?;
131
132 let h1 = hash_file(&file_path)?;
133 let h2 = hash_file(&file_path)?;
134 assert_eq!(h1, h2);
135
136 let h_direct = hash_bytes(b"file content for hashing");
137 assert_eq!(h1, h_direct, "File hash must match direct hash");
138 Ok(())
139 }
140}