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}