1use std::fmt::Write;
2use std::path::Path;
3
4use sha2::{Digest, Sha256};
5use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
6use xxhash_rust::xxh3::Xxh3;
7use xxhash_rust::xxh64::Xxh64;
8
9use crate::CoreError;
10use crate::error::Result;
11
12const BUF_SIZE: usize = 64 * 1024;
13
14fn hash_mismatch(path: &Path, expected: u64, actual: u64) -> CoreError {
16 CoreError::HashMismatch {
17 path: path.to_path_buf(),
18 expected: format!("{expected:016x}"),
19 actual: format!("{actual:016x}"),
20 }
21}
22
23pub async fn hash_file_xxhash(path: &Path) -> Result<u64> {
25 let mut file = tokio::fs::File::open(path).await?;
26 let mut hasher = Xxh3::new();
27 let mut buf = vec![0u8; BUF_SIZE];
28
29 loop {
30 let n = file.read(&mut buf).await?;
31 if n == 0 {
32 break;
33 }
34 hasher.update(&buf[..n]);
35 }
36
37 Ok(hasher.digest())
38}
39
40pub async fn verify_xxhash(path: &Path, expected: u64) -> Result<()> {
42 let actual = hash_file_xxhash(path).await?;
43 if actual != expected {
44 return Err(hash_mismatch(path, expected, actual));
45 }
46 Ok(())
47}
48
49pub async fn verify_xxh64(path: &Path, expected: u64) -> Result<()> {
51 let actual = hash_file_xxh64(path).await?;
52 if actual != expected {
53 return Err(hash_mismatch(path, expected, actual));
54 }
55 Ok(())
56}
57
58pub async fn hash_file_xxh64(path: &Path) -> Result<u64> {
60 let mut file = tokio::fs::File::open(path).await?;
61 let mut hasher = Xxh64::new(0);
62 let mut buf = vec![0u8; BUF_SIZE];
63
64 loop {
65 let n = file.read(&mut buf).await?;
66 if n == 0 {
67 break;
68 }
69 hasher.update(&buf[..n]);
70 }
71
72 Ok(hasher.digest())
73}
74
75pub async fn verify_xxhash_compat(path: &Path, expected: u64) -> Result<u64> {
79 let mut file = tokio::fs::File::open(path).await?;
80 let mut xxh64_hasher = Xxh64::new(0);
81 let mut xxh3_hasher = Xxh3::new();
82 let mut buf = vec![0u8; BUF_SIZE];
83
84 loop {
85 let n = file.read(&mut buf).await?;
86 if n == 0 {
87 break;
88 }
89 xxh64_hasher.update(&buf[..n]);
90 xxh3_hasher.update(&buf[..n]);
91 }
92
93 let h64 = xxh64_hasher.digest();
94 if h64 == expected || xxh3_hasher.digest() == expected {
95 return Ok(expected);
96 }
97 Err(hash_mismatch(path, expected, h64))
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum CompatHashMatch {
102 Xxh64,
103 Xxh3,
104}
105
106pub async fn copy_and_hash_compat<R, W>(
108 reader: &mut R,
109 writer: &mut W,
110 expected: u64,
111) -> Result<(u64, CompatHashMatch)>
112where
113 R: tokio::io::AsyncRead + Unpin,
114 W: AsyncWrite + Unpin,
115{
116 let mut xxh64_hasher = Xxh64::new(0);
117 let mut xxh3_hasher = Xxh3::new();
118 let mut buf = vec![0u8; BUF_SIZE];
119
120 loop {
121 let n = reader.read(&mut buf).await?;
122 if n == 0 {
123 break;
124 }
125 xxh64_hasher.update(&buf[..n]);
126 xxh3_hasher.update(&buf[..n]);
127 writer.write_all(&buf[..n]).await?;
128 }
129 writer.flush().await?;
130
131 let h64 = xxh64_hasher.digest();
132 if h64 == expected {
133 return Ok((h64, CompatHashMatch::Xxh64));
134 }
135 let h3 = xxh3_hasher.digest();
136 if h3 == expected {
137 return Ok((h3, CompatHashMatch::Xxh3));
138 }
139 Err(hash_mismatch(Path::new("<stream>"), expected, h64))
140}
141
142pub async fn hash_file_sha256(path: &Path) -> Result<String> {
144 let mut file = tokio::fs::File::open(path).await?;
145 let mut hasher = Sha256::new();
146 let mut buf = vec![0u8; BUF_SIZE];
147
148 loop {
149 let n = file.read(&mut buf).await?;
150 if n == 0 {
151 break;
152 }
153 hasher.update(&buf[..n]);
154 }
155
156 let mut hex = String::with_capacity(64);
157 for byte in hasher.finalize() {
158 write!(&mut hex, "{byte:02x}").expect("writing to String cannot fail");
159 }
160 Ok(hex)
161}
162
163pub async fn verify_sha256(path: &Path, expected: &str) -> Result<()> {
165 let actual = hash_file_sha256(path).await?;
166 if actual != expected {
167 return Err(CoreError::HashMismatch {
168 path: path.to_path_buf(),
169 expected: expected.to_string(),
170 actual,
171 });
172 }
173 Ok(())
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use std::io::Write;
180 use tempfile::NamedTempFile;
181 use xxhash_rust::xxh3::xxh3_64;
182 use xxhash_rust::xxh64::xxh64;
183
184 fn create_temp_file(content: &[u8]) -> NamedTempFile {
185 let mut f = NamedTempFile::new().unwrap();
186 f.write_all(content).unwrap();
187 f
188 }
189
190 #[tokio::test]
191 async fn test_xxhash_roundtrip() {
192 let f = create_temp_file(b"hello world");
193 let hash = hash_file_xxhash(f.path()).await.unwrap();
194 verify_xxhash(f.path(), hash).await.unwrap();
195 }
196
197 #[tokio::test]
198 async fn test_xxhash_mismatch() {
199 let f = create_temp_file(b"hello world");
200 let result = verify_xxhash(f.path(), 0).await;
201 assert!(result.is_err());
202 }
203
204 #[tokio::test]
205 async fn test_sha256_known_value() {
206 let f = create_temp_file(b"hello world");
207 let hash = hash_file_sha256(f.path()).await.unwrap();
208 assert_eq!(
210 hash,
211 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
212 );
213 }
214
215 #[tokio::test]
216 async fn test_sha256_verify() {
217 let f = create_temp_file(b"test data");
218 let hash = hash_file_sha256(f.path()).await.unwrap();
219 verify_sha256(f.path(), &hash).await.unwrap();
220 }
221
222 #[tokio::test]
223 async fn test_sha256_mismatch() {
224 let f = create_temp_file(b"test data");
225 let result = verify_sha256(f.path(), "0000").await;
226 assert!(result.is_err());
227 }
228
229 #[tokio::test]
230 async fn test_xxhash_empty_file() {
231 let f = create_temp_file(b"");
232 let hash = hash_file_xxhash(f.path()).await.unwrap();
233 let expected = xxh3_64(b"");
234 assert_eq!(hash, expected);
235 }
236
237 #[tokio::test]
238 async fn test_xxhash_large_file() {
239 let data = vec![0xABu8; 1024 * 1024]; let f = create_temp_file(&data);
241 let hash = hash_file_xxhash(f.path()).await.unwrap();
242 let expected = xxh3_64(&data);
243 assert_eq!(hash, expected);
244 }
245
246 #[tokio::test]
247 async fn test_sha256_empty_file() {
248 let f = create_temp_file(b"");
249 let hash = hash_file_sha256(f.path()).await.unwrap();
250 assert_eq!(
251 hash,
252 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
253 );
254 }
255
256 #[tokio::test]
257 async fn test_xxhash_nonexistent_file() {
258 let result = hash_file_xxhash(Path::new("/tmp/nonexistent_file_xxhash_test")).await;
259 assert!(result.is_err());
260 }
261
262 #[tokio::test]
263 async fn test_sha256_nonexistent_file() {
264 let result = hash_file_sha256(Path::new("/tmp/nonexistent_file_sha256_test")).await;
265 assert!(result.is_err());
266 }
267
268 #[tokio::test]
269 async fn test_xxhash_different_content_different_hash() {
270 let f1 = create_temp_file(b"content alpha");
271 let f2 = create_temp_file(b"content beta");
272 let h1 = hash_file_xxhash(f1.path()).await.unwrap();
273 let h2 = hash_file_xxhash(f2.path()).await.unwrap();
274 assert_ne!(h1, h2);
275 }
276
277 #[tokio::test]
278 async fn test_xxhash_same_content_same_hash() {
279 let f1 = create_temp_file(b"identical content");
280 let f2 = create_temp_file(b"identical content");
281 let h1 = hash_file_xxhash(f1.path()).await.unwrap();
282 let h2 = hash_file_xxhash(f2.path()).await.unwrap();
283 assert_eq!(h1, h2);
284 }
285
286 #[tokio::test]
287 async fn copy_and_hash_matches_existing_helpers() {
288 let content = b"streamed hash content";
289 let expected = xxh64(content, 0);
290 let mut reader = tokio::io::BufReader::new(&content[..]);
291 let mut writer = Vec::new();
292
293 let (hash, matched) = copy_and_hash_compat(&mut reader, &mut writer, expected)
294 .await
295 .unwrap();
296
297 assert_eq!(writer, content);
298 assert_eq!(hash, expected);
299 assert_eq!(matched, CompatHashMatch::Xxh64);
300 }
301
302 #[tokio::test]
303 async fn copy_and_hash_detects_mismatch() {
304 let content = b"streamed hash content";
305 let mut reader = tokio::io::BufReader::new(&content[..]);
306 let mut writer = Vec::new();
307
308 let err = copy_and_hash_compat(&mut reader, &mut writer, 0)
309 .await
310 .unwrap_err();
311
312 assert!(matches!(err, CoreError::HashMismatch { .. }));
313 assert_eq!(writer, content);
314 }
315}