1use std::fs::File;
2use std::io::{self, Read};
3use std::path::{Path, PathBuf};
4
5use walkdir::WalkDir;
6
7use hmac::{Hmac, Mac};
8use md5::Md5;
9use sha1::Sha1;
10use sha2::Digest;
11use sha2::Sha256;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum HashAlgorithm {
16 Md5,
17 Sha1,
18 Sha256,
19 HmacMd5,
20 HmacSha1,
21 HmacSha256,
22}
23
24pub fn hash_bytes(data: &[u8], algorithm: HashAlgorithm, key: Option<&[u8]>) -> io::Result<String> {
26 let result = match algorithm {
27 HashAlgorithm::Md5 => format!("{:x}", Md5::digest(data)),
28 HashAlgorithm::Sha1 => format!("{:x}", Sha1::digest(data)),
29 HashAlgorithm::Sha256 => format!("{:x}", Sha256::digest(data)),
30 HashAlgorithm::HmacMd5 => {
31 let key =
32 key.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing HMAC key"))?;
33 type HmacMd5 = Hmac<Md5>;
34 let mut mac = HmacMd5::new_from_slice(key)
35 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?;
36 mac.update(data);
37 to_hex(&mac.finalize().into_bytes())
38 }
39 HashAlgorithm::HmacSha1 => {
40 let key =
41 key.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing HMAC key"))?;
42 type HmacSha1 = Hmac<Sha1>;
43 let mut mac = HmacSha1::new_from_slice(key)
44 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?;
45 mac.update(data);
46 to_hex(&mac.finalize().into_bytes())
47 }
48 HashAlgorithm::HmacSha256 => {
49 let key =
50 key.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing HMAC key"))?;
51 type HmacSha256 = Hmac<Sha256>;
52 let mut mac = HmacSha256::new_from_slice(key)
53 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?;
54 mac.update(data);
55 to_hex(&mac.finalize().into_bytes())
56 }
57 };
58
59 Ok(result)
60}
61
62pub fn hash_reader<R: Read>(
64 mut reader: R,
65 algorithm: HashAlgorithm,
66 key: Option<&[u8]>,
67) -> io::Result<String> {
68 let mut buffer = Vec::new();
69 reader.read_to_end(&mut buffer)?;
70 hash_bytes(&buffer, algorithm, key)
71}
72
73pub fn hash_file(path: &Path, algorithm: HashAlgorithm, key: Option<&[u8]>) -> io::Result<String> {
76 let file = File::open(path)?;
77 hash_reader(file, algorithm, key)
78}
79
80pub fn hash_directory(
84 path: &Path,
85 algorithm: HashAlgorithm,
86 key: Option<&[u8]>,
87 max_depth: usize,
88) -> io::Result<String> {
89 let mut files: Vec<PathBuf> = WalkDir::new(path)
90 .max_depth(max_depth)
91 .into_iter()
92 .filter_map(|e| e.ok())
93 .filter(|e| e.file_type().is_file())
94 .map(|e| e.into_path())
95 .collect();
96
97 files.sort();
98
99 let mut combined = String::new();
100 for f in files {
101 let h = hash_file(&f, algorithm, key)?;
102 combined.push_str(&h);
103 }
104
105 hash_bytes(combined.as_bytes(), algorithm, key)
106}
107
108fn to_hex(bytes: &[u8]) -> String {
109 let mut out = String::with_capacity(bytes.len() * 2);
110 for b in bytes {
111 use std::fmt::Write as _;
112 write!(&mut out, "{:02x}", b).unwrap();
113 }
114 out
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use std::fs::{self, File};
121 use std::io::Write;
122 use tempfile::tempdir;
123
124 fn create_temp_file(content: &[u8]) -> std::path::PathBuf {
125 use std::time::{SystemTime, UNIX_EPOCH};
126 let mut path = std::env::temp_dir();
127 let ts = SystemTime::now()
128 .duration_since(UNIX_EPOCH)
129 .unwrap()
130 .as_nanos();
131 path.push(format!("anti_sec_test_{}", ts));
132 let mut f = File::create(&path).unwrap();
133 f.write_all(content).unwrap();
134 path
135 }
136
137 #[test]
138 fn test_md5_hash() {
139 let path = create_temp_file(b"hello");
140 let digest = hash_file(&path, HashAlgorithm::Md5, None).unwrap();
141 fs::remove_file(&path).unwrap();
142 assert_eq!(digest, "5d41402abc4b2a76b9719d911017c592");
143 }
144
145 #[test]
146 fn test_sha1_hash() {
147 let path = create_temp_file(b"hello");
148 let digest = hash_file(&path, HashAlgorithm::Sha1, None).unwrap();
149 fs::remove_file(&path).unwrap();
150 assert_eq!(digest, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
151 }
152
153 #[test]
154 fn test_sha256_hash() {
155 let path = create_temp_file(b"hello");
156 let digest = hash_file(&path, HashAlgorithm::Sha256, None).unwrap();
157 fs::remove_file(&path).unwrap();
158 assert_eq!(
159 digest,
160 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
161 );
162 }
163
164 #[test]
165 fn test_hmac_sha256_hash() {
166 let path = create_temp_file(b"hello");
167 let digest = hash_file(&path, HashAlgorithm::HmacSha256, Some(b"secret")).unwrap();
168 fs::remove_file(&path).unwrap();
169 assert_eq!(
170 digest,
171 "88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b"
172 );
173 }
174
175 #[test]
176 fn test_hash_reader() {
177 let data = b"reader";
178 let digest = hash_reader(&data[..], HashAlgorithm::Sha256, None).unwrap();
179 assert_eq!(
180 digest,
181 "3d0941964aa3ebdcb00ccef58b1bb399f9f898465e9886d5aec7f31090a0fb30"
182 );
183 }
184
185 #[test]
186 fn test_hash_directory() {
187 let dir = tempdir().unwrap();
188 let file_a = dir.path().join("a.txt");
189 let file_b = dir.path().join("b.txt");
190 fs::write(&file_a, b"foo").unwrap();
191 fs::write(&file_b, b"bar").unwrap();
192 let digest = hash_directory(dir.path(), HashAlgorithm::Sha256, None, 1).unwrap();
193 let h1 = hash_file(&file_a, HashAlgorithm::Sha256, None).unwrap();
195 let h2 = hash_file(&file_b, HashAlgorithm::Sha256, None).unwrap();
196 let mut concat = String::new();
197 let mut names = vec![(&file_a, h1), (&file_b, h2)];
198 names.sort_by_key(|(p, _)| p.to_path_buf());
199 for (_, h) in names {
200 concat.push_str(&h);
201 }
202 let expected = hash_bytes(concat.as_bytes(), HashAlgorithm::Sha256, None).unwrap();
203 assert_eq!(digest, expected);
204 }
205}