rix/
hashes.rs

1#[derive(Copy, Clone, Debug, PartialEq)]
2pub enum HashType {
3    Md5,
4    Sha1,
5    Sha256,
6    Sha512,
7}
8
9#[derive(Debug, PartialEq)]
10pub struct Hash {
11    pub hash_type: HashType,
12    pub bytes: Vec<u8>,
13}
14
15impl HashType {
16    pub fn size(&self) -> usize {
17        match self {
18            HashType::Md5 => 16,
19            HashType::Sha1 => 20,
20            HashType::Sha256 => 32,
21            HashType::Sha512 => 64,
22        }
23    }
24
25    pub fn from_str(hash_type: &str) -> Option<HashType> {
26        match hash_type {
27            "md5" => Some(HashType::Md5),
28            "sha1" => Some(HashType::Sha1),
29            "sha256" => Some(HashType::Sha256),
30            "sha512" => Some(HashType::Sha512),
31            _ => None,
32        }
33    }
34}
35
36impl std::fmt::Display for HashType {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        let hash_type = match self {
39            HashType::Md5 => "md5",
40            HashType::Sha1 => "sha1",
41            HashType::Sha256 => "sha256",
42            HashType::Sha512 => "sha512",
43        };
44        write!(f, "{}", hash_type)
45    }
46}
47
48pub fn parse(hash_str: &str, hash_type: HashType) -> Result<Hash, String> {
49    let hash_str_len = hash_str.as_bytes().len();
50    let hash_size = hash_type.size();
51    if hash_str_len == 2 * hash_size {
52        from_base16(hash_str, hash_type)
53    } else if hash_str_len == to_base32_len(hash_size) {
54        from_base32(hash_str, hash_type)
55    } else if hash_str_len == to_base64_len(hash_size) {
56        from_base64(hash_str, hash_type)
57    } else {
58        Err(format!("hash '{}' with unexpected length.", hash_str))
59    }
60}
61
62pub fn sri_hash_components<'a>(hash_str: &'a str) -> Result<(&'a str, &'a str), String> {
63    hash_str
64        .split_once('-')
65        .or_else(|| hash_str.split_once(':'))
66        .ok_or(format!("Failed to parse '{}'. Not an SRI hash.", hash_str))
67}
68
69pub fn to_base16(hash: &Hash) -> String {
70    let bytes = &hash.bytes;
71    let mut out_string = String::with_capacity(2 * bytes.len());
72    for i in 0..bytes.len() {
73        out_string.push(nibble_to_base16(bytes[i] >> 4));
74        out_string.push(nibble_to_base16(bytes[i] & 0x0f));
75    }
76    return out_string;
77}
78
79pub fn from_base16(base16_str: &str, hash_type: HashType) -> Result<Hash, String> {
80    let base16_str_bytes = base16_str.as_bytes();
81    let mut bytes = vec![0; hash_type.size()];
82    for idx in 0..bytes.len() {
83        bytes[idx] = parse_base16_digit(base16_str_bytes[idx * 2])? << 4
84            | parse_base16_digit(base16_str_bytes[idx * 2 + 1])?;
85    }
86    return Ok(Hash { hash_type, bytes });
87}
88
89pub fn to_base32(hash: &Hash) -> String {
90    let bytes = &hash.bytes;
91    let bytes_len = bytes.len();
92    let len = to_base32_len(bytes_len);
93    let mut out_string = String::with_capacity(len);
94
95    for idx in (0..len).rev() {
96        let b = idx * 5;
97        let i = b / 8;
98        let j = b % 8;
99        let carry = if i >= bytes_len - 1 {
100            0
101        } else {
102            bytes[i + 1].checked_shl(8 - j as u32).unwrap_or(0)
103        };
104        let c = (bytes[i] >> j) | carry;
105        out_string.push(nibble_to_base32(c & 0x1f));
106    }
107
108    return out_string;
109}
110
111pub fn from_base32(base32_str: &str, hash_type: HashType) -> Result<Hash, String> {
112    let mut bytes = vec![0; hash_type.size()];
113    let base32_str_bytes = base32_str.as_bytes();
114    let str_len = base32_str_bytes.len();
115    for idx in 0..to_base32_len(bytes.len()) {
116        let digit = parse_base32_digit(base32_str_bytes[str_len - idx - 1])?;
117        let b = idx * 5;
118        let i = b / 8;
119        let j = b % 8;
120        bytes[i] |= digit << j;
121
122        let carry = digit.checked_shr(8 - j as u32).unwrap_or(0);
123        if i < bytes.len() - 1 {
124            bytes[i + 1] |= carry;
125        } else if carry != 0 {
126            return Err(format!("Invalid base-32 string '{}'", base32_str));
127        }
128    }
129    return Ok(Hash { hash_type, bytes });
130}
131
132pub fn to_base64(hash: &Hash) -> String {
133    let bytes = &hash.bytes;
134    let mut out_string = String::with_capacity(to_base64_len(bytes.len()));
135    let mut data: usize = 0;
136    let mut nbits: usize = 0;
137
138    for byte in bytes {
139        data = data << 8 | (*byte as usize);
140        nbits += 8;
141        while nbits >= 6 {
142            nbits -= 6;
143            out_string.push(BASE_64_CHARS[data >> nbits & 0x3f] as char);
144        }
145    }
146
147    if nbits > 0 {
148        out_string.push(BASE_64_CHARS[data << (6 - nbits) & 0x3f] as char);
149    }
150
151    while out_string.len() % 4 > 0 {
152        out_string.push('=');
153    }
154
155    return out_string;
156}
157
158pub fn from_base64(base64_str: &str, hash_type: HashType) -> Result<Hash, String> {
159    let mut bytes = vec![0; hash_type.size()];
160    let base64_str_bytes = base64_str.as_bytes();
161    let mut d: u32 = 0;
162    let mut bits: u32 = 0;
163    let mut byte = 0;
164
165    for chr in base64_str_bytes {
166        if *chr == b'=' {
167            break;
168        }
169        let digit = BASE_64_CHAR_VALUES[*chr as usize];
170        if digit == INVALID_CHAR_VALUE {
171            return Err(format!(
172                "Character '{}' is not a valid base-64 character.",
173                *chr as char
174            ));
175        }
176        bits += 6;
177        d = d << 6 | digit as u32;
178        if bits >= 8 {
179            bytes[byte] = (d >> (bits - 8) & 0xff) as u8;
180            bits -= 8;
181            byte += 1;
182        }
183    }
184    return Ok(Hash { hash_type, bytes });
185}
186
187pub fn to_sri(hash: &Hash) -> String {
188    format!("{}-{}", hash.hash_type, to_base64(&hash))
189}
190
191fn nibble_to_base16(nibble: u8) -> char {
192    if nibble < 10 {
193        return (b'0' + nibble) as char;
194    }
195    return (b'a' + nibble - 10) as char;
196}
197
198fn parse_base16_digit(chr: u8) -> Result<u8, String> {
199    match chr {
200        b'0'..=b'9' => Ok(chr - b'0'),
201        b'A'..=b'F' => Ok(chr - b'A' + 10),
202        b'a'..=b'f' => Ok(chr - b'a' + 10),
203        _ => Err("Not a hex numeral.".to_owned()),
204    }
205}
206
207fn to_base32_len(bytes_count: usize) -> usize {
208    (bytes_count * 8 - 1) / 5 + 1
209}
210
211fn nibble_to_base32(nibble: u8) -> char {
212    if nibble < 10 {
213        return (b'0' + nibble) as char;
214    } else if nibble < 14 {
215        return (b'a' + nibble - 10) as char;
216    } else if nibble < 23 {
217        return (b'f' + nibble - 14) as char;
218    } else if nibble < 27 {
219        return (b'p' + nibble - 23) as char;
220    }
221    return (b'v' + nibble - 27) as char;
222}
223
224fn parse_base32_digit(chr: u8) -> Result<u8, String> {
225    match chr {
226        b'0'..=b'9' => Ok(chr - b'0'),
227        b'a'..=b'd' => Ok(chr - b'a' + 10),
228        b'f'..=b'n' => Ok(chr - b'f' + 14),
229        b'p'..=b's' => Ok(chr - b'p' + 23),
230        b'v'..=b'z' => Ok(chr - b'v' + 27),
231        _ => {
232            return Err(format!(
233                "Character '{}' is not a valid base-32 character.",
234                chr as char
235            ))
236        }
237    }
238}
239
240fn to_base64_len(bytes_count: usize) -> usize {
241    ((4 * bytes_count / 3) + 3) & !3
242}
243
244const BASE_64_CHARS: &[u8] =
245    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".as_bytes();
246const BASE_64_CHAR_VALUES: [u8; 256] = compute_base64_char_values();
247const INVALID_CHAR_VALUE: u8 = 255;
248
249const fn compute_base64_char_values() -> [u8; 256] {
250    let mut char_values: [u8; 256] = [INVALID_CHAR_VALUE; 256];
251    let mut idx = 0;
252    while idx < 64 {
253        char_values[BASE_64_CHARS[idx] as usize] = idx as u8;
254        idx += 1;
255    }
256    return char_values;
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    fn sha256_sample() -> Hash {
264        Hash {
265            hash_type: HashType::Sha256,
266            bytes: vec![
267                0xd5, 0x31, 0x38, 0x62, 0x85, 0x6f, 0x77, 0x70, 0xbd, 0xff, 0xed, 0x2d, 0xfe, 0x8c,
268                0x41, 0x7a, 0x84, 0xf3, 0xf6, 0xd5, 0xe1, 0x1c, 0x3b, 0x5c, 0x19, 0x42, 0x0f, 0x21,
269                0x30, 0x76, 0x6f, 0x81,
270            ],
271        }
272    }
273
274    fn sha512_sample() -> Hash {
275        Hash {
276            hash_type: HashType::Sha512,
277            bytes: vec![
278                0xfb, 0x2e, 0x19, 0x9d, 0xe3, 0xe9, 0xbd, 0x6b, 0x35, 0x7d, 0xcf, 0xcb, 0x85, 0x94,
279                0x53, 0x1e, 0x44, 0xde, 0xb1, 0xb5, 0xe4, 0xc8, 0x16, 0x2e, 0x38, 0x1f, 0xb9, 0x0b,
280                0x2a, 0x1d, 0x66, 0xaa, 0xc4, 0xb8, 0x44, 0xd7, 0x8b, 0x7c, 0xce, 0x55, 0xfa, 0x40,
281                0x40, 0x87, 0x60, 0x0b, 0x79, 0x57, 0x6c, 0x72, 0xd3, 0x0c, 0x6f, 0x5d, 0x42, 0x8b,
282                0x31, 0x47, 0xd0, 0x61, 0xbc, 0xb2, 0x83, 0x2d,
283            ],
284        }
285    }
286
287    #[test]
288    fn test_hash_type_size() {
289        assert_eq!(HashType::Md5.size(), 16);
290        assert_eq!(HashType::Sha1.size(), 20);
291        assert_eq!(HashType::Sha256.size(), 32);
292        assert_eq!(HashType::Sha512.size(), 64);
293    }
294
295    #[test]
296    fn test_hash_type_from_str() {
297        assert_eq!(HashType::from_str("md5"), Some(HashType::Md5));
298        assert_eq!(HashType::from_str("sha1"), Some(HashType::Sha1));
299        assert_eq!(HashType::from_str("sha256"), Some(HashType::Sha256));
300        assert_eq!(HashType::from_str("sha512"), Some(HashType::Sha512));
301        assert_eq!(HashType::from_str("foobar"), None);
302    }
303
304    #[test]
305    fn test_parse_sha256_base16() {
306        assert_eq!(
307            parse(
308                "d5313862856f7770bdffed2dfe8c417a84f3f6d5e11c3b5c19420f2130766f81",
309                HashType::Sha256,
310            ),
311            Ok(sha256_sample()),
312        );
313    }
314
315    #[test]
316    fn test_parse_sha256_base32() {
317        assert_eq!(
318            parse(
319                "10bgfqq223s235f3n771spvg713s866gwbgdzyyp0xvghmi3hcfm",
320                HashType::Sha256,
321            ),
322            Ok(sha256_sample()),
323        );
324    }
325
326    #[test]
327    fn test_parse_sha256_base64() {
328        assert_eq!(
329            parse(
330                "1TE4YoVvd3C9/+0t/oxBeoTz9tXhHDtcGUIPITB2b4E=",
331                HashType::Sha256,
332            ),
333            Ok(sha256_sample()),
334        );
335    }
336
337    #[test]
338    fn test_parse_sha256_invalid() {
339        assert_eq!(
340            parse("foobar", HashType::Sha256),
341            Err("hash 'foobar' with unexpected length.".to_owned()),
342        );
343    }
344
345    #[test]
346    fn test_parse_sha512_base64() {
347        assert_eq!(
348            parse("+y4ZnePpvWs1fc/LhZRTHkTesbXkyBYuOB+5CyodZqrEuETXi3zOVfpAQIdgC3lXbHLTDG9dQosxR9BhvLKDLQ==", HashType::Sha512),
349            Ok(sha512_sample()),
350        );
351    }
352
353    #[test]
354    fn test_to_base16() {
355        assert_eq!(
356            to_base16(&sha256_sample()),
357            "d5313862856f7770bdffed2dfe8c417a84f3f6d5e11c3b5c19420f2130766f81"
358        );
359    }
360
361    #[test]
362    fn test_to_base3() {
363        assert_eq!(
364            to_base32(&sha256_sample()),
365            "10bgfqq223s235f3n771spvg713s866gwbgdzyyp0xvghmi3hcfm"
366        );
367    }
368
369    #[test]
370    fn test_to_base64() {
371        assert_eq!(
372            to_base64(&sha256_sample()),
373            "1TE4YoVvd3C9/+0t/oxBeoTz9tXhHDtcGUIPITB2b4E="
374        );
375    }
376
377    #[test]
378    fn test_from_base32_invalid_char() {
379        assert_eq!(
380            from_base32(")", HashType::Sha256),
381            Err("Character ')' is not a valid base-32 character.".to_owned()),
382        );
383    }
384
385    #[test]
386    fn test_from_base64_invalid_char() {
387        assert_eq!(
388            from_base64(")", HashType::Sha256),
389            Err("Character ')' is not a valid base-64 character.".to_owned()),
390        );
391    }
392
393    #[test]
394    fn test_sri_hash_components() {
395        assert_eq!(sri_hash_components("md5-foobar"), Ok(("md5", "foobar")));
396        assert_eq!(sri_hash_components("sha256:abc"), Ok(("sha256", "abc")),);
397    }
398
399    #[test]
400    fn test_sri_hash_components_fail() {
401        assert_eq!(
402            sri_hash_components("md5foobar"),
403            Err("Failed to parse 'md5foobar'. Not an SRI hash.".to_owned())
404        );
405    }
406}