Skip to main content

aria2_core/checksum/
message_digest.rs

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}