1use digest::Digest;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4pub enum HashType {
5 Md5,
6 Sha1,
7 Sha256,
8 Sha512,
9 Adler32,
10}
11
12impl HashType {
13 #[allow(clippy::should_implement_trait)]
14 pub fn from_str(s: &str) -> Option<HashType> {
15 match s.to_lowercase().as_str() {
16 "md5" => Some(HashType::Md5),
17 "sha-1" | "sha1" => Some(HashType::Sha1),
18 "sha-256" | "sha256" => Some(HashType::Sha256),
19 "sha-512" | "sha512" => Some(HashType::Sha512),
20 "adler32" => Some(HashType::Adler32),
21 _ => None,
22 }
23 }
24
25 pub fn as_str(&self) -> &'static str {
26 match self {
27 HashType::Md5 => "md5",
28 HashType::Sha1 => "sha1",
29 HashType::Sha256 => "sha256",
30 HashType::Sha512 => "sha512",
31 HashType::Adler32 => "adler32",
32 }
33 }
34
35 pub fn digest_length(&self) -> usize {
36 match self {
37 HashType::Md5 => 16,
38 HashType::Sha1 => 20,
39 HashType::Sha256 => 32,
40 HashType::Sha512 => 64,
41 HashType::Adler32 => 4,
42 }
43 }
44
45 pub fn all_supported() -> Vec<HashType> {
46 vec![
47 HashType::Md5,
48 HashType::Sha1,
49 HashType::Sha256,
50 HashType::Sha512,
51 HashType::Adler32,
52 ]
53 }
54}
55
56enum DigestInner {
57 Md5(md5::Context),
58 Sha1(sha1::Sha1),
59 Sha256(sha2::Sha256),
60 Sha512(sha2::Sha512),
61 Adler32(Vec<u8>),
62}
63
64pub struct MessageDigest {
65 inner: DigestInner,
66}
67
68impl MessageDigest {
69 pub fn new(algo: HashType) -> Self {
70 let inner = match algo {
71 HashType::Md5 => DigestInner::Md5(md5::Context::new()),
72 HashType::Sha1 => DigestInner::Sha1(sha1::Sha1::new()),
73 HashType::Sha256 => DigestInner::Sha256(sha2::Sha256::new()),
74 HashType::Sha512 => DigestInner::Sha512(sha2::Sha512::new()),
75 HashType::Adler32 => DigestInner::Adler32(Vec::new()),
76 };
77 MessageDigest { inner }
78 }
79
80 pub fn update(&mut self, data: &[u8]) {
81 match &mut self.inner {
82 DigestInner::Md5(d) => d.consume(data),
83 DigestInner::Sha1(d) => d.update(data),
84 DigestInner::Sha256(d) => d.update(data),
85 DigestInner::Sha512(d) => d.update(data),
86 DigestInner::Adler32(buf) => buf.extend_from_slice(data),
87 }
88 }
89
90 pub fn finalize(self) -> Vec<u8> {
91 match self.inner {
92 DigestInner::Md5(d) => d.compute().to_vec(),
93 DigestInner::Sha1(d) => d.finalize().to_vec(),
94 DigestInner::Sha256(d) => d.finalize().to_vec(),
95 DigestInner::Sha512(d) => d.finalize().to_vec(),
96 DigestInner::Adler32(buf) => {
97 let checksum = adler32::adler32(&buf[..]).unwrap_or(1);
98 checksum.to_le_bytes().to_vec()
99 }
100 }
101 }
102
103 pub fn finalize_hex(self) -> String {
104 let bytes = self.finalize();
105 bytes.iter().map(|b| format!("{:02x}", b)).collect()
106 }
107
108 pub fn digest_length(&self) -> usize {
109 match &self.inner {
110 DigestInner::Md5(_) => 16,
111 DigestInner::Sha1(_) => 20,
112 DigestInner::Sha256(_) => 32,
113 DigestInner::Sha512(_) => 64,
114 DigestInner::Adler32(_) => 4,
115 }
116 }
117
118 pub fn reset(&mut self) {
119 match &mut self.inner {
120 DigestInner::Md5(d) => *d = md5::Context::new(),
121 DigestInner::Sha1(d) => *d = sha1::Sha1::new(),
122 DigestInner::Sha256(d) => *d = sha2::Sha256::new(),
123 DigestInner::Sha512(d) => *d = sha2::Sha512::new(),
124 DigestInner::Adler32(s) => *s = Vec::new(),
125 }
126 }
127
128 pub fn hash_data(algo: HashType, data: &[u8]) -> Vec<u8> {
129 let mut digest = Self::new(algo);
130 digest.update(data);
131 digest.finalize()
132 }
133
134 pub fn hash_hex(algo: HashType, data: &[u8]) -> String {
135 let mut digest = Self::new(algo);
136 digest.update(data);
137 digest.finalize_hex()
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_hash_type_from_str() {
147 assert_eq!(HashType::from_str("md5"), Some(HashType::Md5));
148 assert_eq!(HashType::from_str("MD5"), Some(HashType::Md5));
149 assert_eq!(HashType::from_str("sha-1"), Some(HashType::Sha1));
150 assert_eq!(HashType::from_str("SHA1"), Some(HashType::Sha1));
151 assert_eq!(HashType::from_str("sha-256"), Some(HashType::Sha256));
152 assert_eq!(HashType::from_str("sha256"), Some(HashType::Sha256));
153 assert_eq!(HashType::from_str("sha-512"), Some(HashType::Sha512));
154 assert_eq!(HashType::from_str("adler32"), Some(HashType::Adler32));
155 assert_eq!(HashType::from_str("unknown"), None);
156 }
157
158 #[test]
159 fn test_md5_known_vector() {
160 let hex = MessageDigest::hash_hex(HashType::Md5, b"");
161 assert_eq!(hex, "d41d8cd98f00b204e9800998ecf8427e");
162
163 let hex = MessageDigest::hash_hex(HashType::Md5, b"hello world");
164 assert_eq!(hex, "5eb63bbbe01eeed093cb22bb8f5acdc3");
165 }
166
167 #[test]
168 fn test_sha1_known_vector() {
169 let hex = MessageDigest::hash_hex(HashType::Sha1, b"");
170 assert_eq!(hex, "da39a3ee5e6b4b0d3255bfef95601890afd80709");
171
172 let hex = MessageDigest::hash_hex(
173 HashType::Sha1,
174 b"The quick brown fox jumps over the lazy dog",
175 );
176 assert_eq!(hex, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12");
177 }
178
179 #[test]
180 fn test_sha256_known_vector() {
181 let hex = MessageDigest::hash_hex(HashType::Sha256, b"");
182 assert_eq!(
183 hex,
184 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
185 );
186
187 let hex = MessageDigest::hash_hex(HashType::Sha256, b"abc");
188 assert_eq!(
189 hex,
190 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
191 );
192 }
193
194 #[test]
195 fn test_adler32_basic() {
196 let bytes = MessageDigest::hash_data(HashType::Adler32, b"hello world");
197 assert_eq!(bytes.len(), 4);
198
199 let empty = MessageDigest::hash_data(HashType::Adler32, b"");
200 assert_eq!(empty.len(), 4);
201 }
202
203 #[test]
204 fn test_finalize_hex_format_lowercase() {
205 let hex = MessageDigest::hash_hex(HashType::Md5, b"test");
206 for ch in hex.chars() {
207 assert!(
208 ch.is_ascii_digit() || ('a'..='f').contains(&ch),
209 "hex should be lowercase: {}",
210 hex
211 );
212 }
213 }
214
215 #[test]
216 fn test_digest_length_matches() {
217 for ht in HashType::all_supported() {
218 let md = MessageDigest::new(ht);
219 assert_eq!(md.digest_length(), ht.digest_length());
220 }
221 }
222
223 #[test]
224 fn test_streaming_vs_one_shot() {
225 let one_shot = MessageDigest::hash_hex(HashType::Sha256, b"hello world");
226
227 let mut streaming = MessageDigest::new(HashType::Sha256);
228 streaming.update(b"hello ");
229 streaming.update(b"world");
230 let streaming_hex = streaming.finalize_hex();
231
232 assert_eq!(one_shot, streaming_hex);
233 }
234
235 #[test]
236 fn test_different_data_different_hash() {
237 let h1 = MessageDigest::hash_hex(HashType::Md5, b"first data");
238 let h2 = MessageDigest::hash_hex(HashType::Md5, b"second data");
239 assert_ne!(h1, h2, "different data should produce different hash value");
240 }
241}