1use std::path::Path;
2
3use sha2::{Digest, Sha256};
4use tokio::io::AsyncReadExt;
5use xxhash_rust::xxh3::xxh3_64;
6use xxhash_rust::xxh64::xxh64;
7
8use crate::error::Result;
9use crate::CoreError;
10
11const BUF_SIZE: usize = 64 * 1024;
12
13fn hash_mismatch(path: &Path, expected: u64, actual: u64) -> CoreError {
15 CoreError::HashMismatch {
16 path: path.to_path_buf(),
17 expected: format!("{expected:016x}"),
18 actual: format!("{actual:016x}"),
19 }
20}
21
22pub async fn hash_file_xxhash(path: &Path) -> Result<u64> {
24 let data = tokio::fs::read(path).await?;
25 Ok(xxh3_64(&data))
26}
27
28pub async fn verify_xxhash(path: &Path, expected: u64) -> Result<()> {
30 let actual = hash_file_xxhash(path).await?;
31 if actual != expected {
32 return Err(hash_mismatch(path, expected, actual));
33 }
34 Ok(())
35}
36
37pub async fn verify_xxh64(path: &Path, expected: u64) -> Result<()> {
39 let data = tokio::fs::read(path).await?;
40 let actual = xxh64(&data, 0);
41 if actual != expected {
42 return Err(hash_mismatch(path, expected, actual));
43 }
44 Ok(())
45}
46
47pub async fn verify_xxhash_compat(path: &Path, expected: u64) -> Result<u64> {
51 let data = tokio::fs::read(path).await?;
52 let h64 = xxh64(&data, 0);
53 if h64 == expected || xxh3_64(&data) == expected {
54 return Ok(expected);
55 }
56 Err(hash_mismatch(path, expected, h64))
57}
58
59pub async fn hash_file_sha256(path: &Path) -> Result<String> {
61 let mut file = tokio::fs::File::open(path).await?;
62 let mut hasher = Sha256::new();
63 let mut buf = vec![0u8; BUF_SIZE];
64
65 loop {
66 let n = file.read(&mut buf).await?;
67 if n == 0 {
68 break;
69 }
70 hasher.update(&buf[..n]);
71 }
72
73 Ok(format!("{:x}", hasher.finalize()))
74}
75
76pub async fn verify_sha256(path: &Path, expected: &str) -> Result<()> {
78 let actual = hash_file_sha256(path).await?;
79 if actual != expected {
80 return Err(CoreError::HashMismatch {
81 path: path.to_path_buf(),
82 expected: expected.to_string(),
83 actual,
84 });
85 }
86 Ok(())
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use std::io::Write;
93 use tempfile::NamedTempFile;
94
95 fn create_temp_file(content: &[u8]) -> NamedTempFile {
96 let mut f = NamedTempFile::new().unwrap();
97 f.write_all(content).unwrap();
98 f
99 }
100
101 #[tokio::test]
102 async fn test_xxhash_roundtrip() {
103 let f = create_temp_file(b"hello world");
104 let hash = hash_file_xxhash(f.path()).await.unwrap();
105 verify_xxhash(f.path(), hash).await.unwrap();
106 }
107
108 #[tokio::test]
109 async fn test_xxhash_mismatch() {
110 let f = create_temp_file(b"hello world");
111 let result = verify_xxhash(f.path(), 0).await;
112 assert!(result.is_err());
113 }
114
115 #[tokio::test]
116 async fn test_sha256_known_value() {
117 let f = create_temp_file(b"hello world");
118 let hash = hash_file_sha256(f.path()).await.unwrap();
119 assert_eq!(
121 hash,
122 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
123 );
124 }
125
126 #[tokio::test]
127 async fn test_sha256_verify() {
128 let f = create_temp_file(b"test data");
129 let hash = hash_file_sha256(f.path()).await.unwrap();
130 verify_sha256(f.path(), &hash).await.unwrap();
131 }
132
133 #[tokio::test]
134 async fn test_sha256_mismatch() {
135 let f = create_temp_file(b"test data");
136 let result = verify_sha256(f.path(), "0000").await;
137 assert!(result.is_err());
138 }
139
140 #[tokio::test]
141 async fn test_xxhash_empty_file() {
142 let f = create_temp_file(b"");
143 let hash = hash_file_xxhash(f.path()).await.unwrap();
144 let expected = xxh3_64(b"");
145 assert_eq!(hash, expected);
146 }
147
148 #[tokio::test]
149 async fn test_xxhash_large_file() {
150 let data = vec![0xABu8; 1024 * 1024]; let f = create_temp_file(&data);
152 let hash = hash_file_xxhash(f.path()).await.unwrap();
153 let expected = xxh3_64(&data);
154 assert_eq!(hash, expected);
155 }
156
157 #[tokio::test]
158 async fn test_sha256_empty_file() {
159 let f = create_temp_file(b"");
160 let hash = hash_file_sha256(f.path()).await.unwrap();
161 assert_eq!(
162 hash,
163 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
164 );
165 }
166
167 #[tokio::test]
168 async fn test_xxhash_nonexistent_file() {
169 let result = hash_file_xxhash(Path::new("/tmp/nonexistent_file_xxhash_test")).await;
170 assert!(result.is_err());
171 }
172
173 #[tokio::test]
174 async fn test_sha256_nonexistent_file() {
175 let result = hash_file_sha256(Path::new("/tmp/nonexistent_file_sha256_test")).await;
176 assert!(result.is_err());
177 }
178
179 #[tokio::test]
180 async fn test_xxhash_different_content_different_hash() {
181 let f1 = create_temp_file(b"content alpha");
182 let f2 = create_temp_file(b"content beta");
183 let h1 = hash_file_xxhash(f1.path()).await.unwrap();
184 let h2 = hash_file_xxhash(f2.path()).await.unwrap();
185 assert_ne!(h1, h2);
186 }
187
188 #[tokio::test]
189 async fn test_xxhash_same_content_same_hash() {
190 let f1 = create_temp_file(b"identical content");
191 let f2 = create_temp_file(b"identical content");
192 let h1 = hash_file_xxhash(f1.path()).await.unwrap();
193 let h2 = hash_file_xxhash(f2.path()).await.unwrap();
194 assert_eq!(h1, h2);
195 }
196}