1use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum HashError {
7 #[error("unsupported hash algorithm: {0}")]
8 UnsupportedAlgorithm(String),
9 #[error("invalid hash encoding")]
10 InvalidEncoding,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum HashAlgorithm {
17 Sha256,
18 Sha512,
19 Sha1,
20 Md5,
21}
22
23impl HashAlgorithm {
24 pub fn from_nix_str(s: &str) -> Result<Self, HashError> {
26 s.parse()
27 }
28
29 #[must_use]
31 pub fn as_nix_str(&self) -> &'static str {
32 match self {
33 Self::Sha256 => "sha256",
34 Self::Sha512 => "sha512",
35 Self::Sha1 => "sha1",
36 Self::Md5 => "md5",
37 }
38 }
39
40 #[must_use]
42 pub fn digest_len(&self) -> usize {
43 match self {
44 Self::Sha256 => 32,
45 Self::Sha512 => 64,
46 Self::Sha1 => 20,
47 Self::Md5 => 16,
48 }
49 }
50}
51
52impl std::fmt::Display for HashAlgorithm {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.write_str(self.as_nix_str())
55 }
56}
57
58impl std::str::FromStr for HashAlgorithm {
59 type Err = HashError;
60
61 fn from_str(s: &str) -> Result<Self, Self::Err> {
62 match s {
63 "sha256" => Ok(Self::Sha256),
64 "sha512" => Ok(Self::Sha512),
65 "sha1" => Ok(Self::Sha1),
66 "md5" => Ok(Self::Md5),
67 _ => Err(HashError::UnsupportedAlgorithm(s.to_string())),
68 }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct NixHash {
75 pub algorithm: HashAlgorithm,
76 pub digest: Vec<u8>,
77}
78
79impl NixHash {
80 pub fn new(algorithm: HashAlgorithm, digest: Vec<u8>) -> Self {
82 Self { algorithm, digest }
83 }
84
85 #[must_use]
87 pub fn to_nix_string(&self) -> String {
88 format!("{}:{}", self.algorithm, hex::encode(&self.digest))
89 }
90
91 #[must_use]
93 pub fn to_sri(&self) -> String {
94 format!("{}-{}", self.algorithm, base64_encode(&self.digest))
95 }
96
97 #[must_use]
102 pub fn to_hex(&self) -> String {
103 hex::encode(&self.digest)
104 }
105
106 pub fn parse_any(
136 algorithm: HashAlgorithm,
137 raw: &str,
138 ) -> Result<Self, HashError> {
139 let digest = decode_hash_any(algorithm, raw)?;
140 Ok(Self::new(algorithm, digest))
141 }
142}
143
144pub fn decode_hash_any(
154 algo: HashAlgorithm,
155 raw: &str,
156) -> Result<Vec<u8>, HashError> {
157 let want = algo.digest_len();
158 let hex_len = want * 2;
159 let base32_len = (want * 8 + 4) / 5;
160 let colon_prefix = format!("{}:", algo.as_nix_str());
169 let raw = raw.strip_prefix(&colon_prefix).unwrap_or(raw);
170 let sri_prefix = format!("{}-", algo.as_nix_str());
173
174 if let Some(b64) = raw.strip_prefix(&sri_prefix) {
175 let bytes = base64_decode_padtolerant(b64)?;
180 if bytes.len() != want {
181 return Err(HashError::InvalidEncoding);
182 }
183 return Ok(bytes);
184 }
185
186 if raw.len() == hex_len
187 && raw.chars().all(|c| c.is_ascii_hexdigit() && !c.is_uppercase())
188 {
189 let bytes = hex::decode(raw)?;
190 return Ok(bytes);
191 }
192
193 if raw.len() == base32_len {
194 let alphabet = "0123456789abcdfghijklmnpqrsvwxyz";
197 if raw.chars().all(|c| alphabet.contains(c)) {
198 let bytes = decode_nix_base32(raw, want)?;
199 return Ok(bytes);
200 }
201 }
202
203 let base64_len = (4 * want / 3 + 3) & !3usize;
214 let base64_len_unpadded = (want * 8).div_ceil(6);
218 if raw.len() == base64_len || raw.len() == base64_len_unpadded {
219 if let Ok(bytes) = base64_decode_padtolerant(raw) {
220 if bytes.len() == want {
221 return Ok(bytes);
222 }
223 }
224 }
225
226 Err(HashError::InvalidEncoding)
227}
228
229fn decode_nix_base32(input: &str, want: usize) -> Result<Vec<u8>, HashError> {
238 let alphabet = b"0123456789abcdfghijklmnpqrsvwxyz";
239 let mut digit_value = [0u8; 128];
240 for (i, &c) in alphabet.iter().enumerate() {
241 digit_value[c as usize] = i as u8;
242 }
243 let mut out = vec![0u8; want];
244 let bytes = input.as_bytes();
245 for (n, &c) in bytes.iter().rev().enumerate() {
246 let d = digit_value[c as usize] as usize;
247 let b = n * 5;
248 let i = b / 8;
249 let j = b % 8;
250 out[i] |= ((d << j) & 0xff) as u8;
251 if i + 1 < want {
252 out[i + 1] |= (d >> (8 - j)) as u8;
253 } else if (d >> (8 - j)) != 0 {
254 return Err(HashError::InvalidEncoding);
257 }
258 }
259 Ok(out)
260}
261
262impl std::fmt::Display for NixHash {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 write!(f, "{}:{}", self.algorithm, hex::encode(&self.digest))
266 }
267}
268
269#[must_use]
271pub fn base64_encode(input: &[u8]) -> String {
272 use base64::Engine;
273 base64::engine::general_purpose::STANDARD.encode(input)
274}
275
276pub fn base64_decode(input: &str) -> Result<Vec<u8>, HashError> {
278 use base64::Engine;
279 base64::engine::general_purpose::STANDARD
280 .decode(input)
281 .map_err(|_| HashError::InvalidEncoding)
282}
283
284pub fn base64_decode_padtolerant(input: &str) -> Result<Vec<u8>, HashError> {
291 let rem = input.len() % 4;
292 if rem == 0 {
293 return base64_decode(input);
294 }
295 let mut padded = String::with_capacity(input.len() + (4 - rem));
297 padded.push_str(input);
298 for _ in 0..(4 - rem) {
299 padded.push('=');
300 }
301 base64_decode(&padded)
302}
303
304#[must_use]
306pub fn minimal_base64_encode(input: &[u8]) -> String {
307 base64_encode(input)
308}
309
310pub(crate) mod hex {
312 #[must_use]
314 pub fn encode(bytes: &[u8]) -> String {
315 let mut s = String::with_capacity(bytes.len() * 2);
316 for b in bytes {
317 s.push_str(&format!("{b:02x}"));
318 }
319 s
320 }
321
322 pub fn decode(s: &str) -> Result<Vec<u8>, super::HashError> {
324 if !s.len().is_multiple_of(2) {
325 return Err(super::HashError::InvalidEncoding);
326 }
327 (0..s.len())
328 .step_by(2)
329 .map(|i| {
330 u8::from_str_radix(&s[i..i + 2], 16)
331 .map_err(|_| super::HashError::InvalidEncoding)
332 })
333 .collect()
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use proptest::prelude::*;
341
342 #[test]
343 fn parse_any_accepts_64_char_hex_sha256() {
344 let raw = "0".repeat(64);
345 let h = NixHash::parse_any(HashAlgorithm::Sha256, &raw).unwrap();
346 assert_eq!(h.digest, vec![0u8; 32]);
347 }
348
349 #[test]
350 fn parse_any_accepts_52_char_base32_sha256() {
351 let raw = "0".repeat(52);
352 let h = NixHash::parse_any(HashAlgorithm::Sha256, &raw).unwrap();
353 assert_eq!(h.digest, vec![0u8; 32]);
354 }
355
356 #[test]
357 fn parse_any_accepts_sri_sha256() {
358 let sri = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
360 let h = NixHash::parse_any(HashAlgorithm::Sha256, sri).unwrap();
361 assert_eq!(h.digest, vec![0u8; 32]);
362 }
363
364 #[test]
365 fn parse_any_accepts_colon_prefixed_hex_sha256() {
366 let raw = format!("sha256:{}", "0".repeat(64));
370 let h = NixHash::parse_any(HashAlgorithm::Sha256, &raw).unwrap();
371 assert_eq!(h.digest, vec![0u8; 32]);
372 }
373
374 #[test]
375 fn parse_any_accepts_colon_prefixed_base32_sha256() {
376 let raw = format!("sha256:{}", "0".repeat(52));
377 let h = NixHash::parse_any(HashAlgorithm::Sha256, &raw).unwrap();
378 assert_eq!(h.digest, vec![0u8; 32]);
379 }
380
381 #[test]
382 fn parse_any_rejects_wrong_length() {
383 let bad = "0".repeat(50);
385 assert!(NixHash::parse_any(HashAlgorithm::Sha256, &bad).is_err());
386 }
387
388 #[test]
389 fn parse_any_rejects_invalid_alphabet() {
390 let bad = format!("Z{}", "0".repeat(51));
394 assert!(NixHash::parse_any(HashAlgorithm::Sha256, &bad).is_err());
395 }
396
397 proptest! {
398 #[test]
400 fn parse_any_all_formats_agree(digest in proptest::collection::vec(any::<u8>(), 32..=32)) {
401 let nh = NixHash::new(HashAlgorithm::Sha256, digest.clone());
402 let hex_str = hex::encode(&digest);
403 let sri = nh.to_sri();
404 let mut b32 = String::with_capacity(52);
407 let alphabet = b"0123456789abcdfghijklmnpqrsvwxyz";
410 for n in (0..52usize).rev() {
411 let b = n * 5;
412 let i = b / 8;
413 let j = b % 8;
414 let mut v = (digest[i] as usize) >> j;
415 if i + 1 < 32 {
416 v |= (digest[i + 1] as usize) << (8 - j);
417 }
418 b32.push(alphabet[v & 0x1f] as char);
419 }
420 let parsed_hex = NixHash::parse_any(HashAlgorithm::Sha256, &hex_str).unwrap();
421 let parsed_b32 = NixHash::parse_any(HashAlgorithm::Sha256, &b32).unwrap();
422 let parsed_sri = NixHash::parse_any(HashAlgorithm::Sha256, &sri).unwrap();
423 prop_assert_eq!(&parsed_hex.digest, &digest);
424 prop_assert_eq!(&parsed_b32.digest, &digest);
425 prop_assert_eq!(&parsed_sri.digest, &digest);
426 }
427 }
428
429 #[test]
430 fn algorithm_roundtrip() {
431 for algo in [HashAlgorithm::Sha256, HashAlgorithm::Sha512, HashAlgorithm::Sha1, HashAlgorithm::Md5] {
432 let parsed = HashAlgorithm::from_nix_str(algo.as_nix_str()).unwrap();
433 assert_eq!(parsed, algo);
434 }
435 }
436
437 #[test]
438 fn nix_string_format() {
439 let hash = NixHash::new(HashAlgorithm::Sha256, vec![0xab; 32]);
440 let s = hash.to_nix_string();
441 assert!(s.starts_with("sha256:"));
442 assert_eq!(s.len(), 7 + 64); }
444
445 #[test]
446 fn sri_roundtrip() {
447 let digest = vec![0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
449 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
450 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
451 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef];
452 let hash = NixHash::new(HashAlgorithm::Sha256, digest.clone());
453 let sri = hash.to_sri();
454 assert!(sri.starts_with("sha256-"));
455 let b64_part = sri.strip_prefix("sha256-").unwrap();
457 assert!(!b64_part.is_empty());
459 assert_eq!(b64_part.len(), 44);
461 }
462
463 #[test]
464 fn invalid_algorithm_string() {
465 assert!(HashAlgorithm::from_nix_str("blake2b").is_err());
466 assert!(HashAlgorithm::from_nix_str("SHA256").is_err());
467 assert!(HashAlgorithm::from_nix_str("").is_err());
468 assert!(HashAlgorithm::from_nix_str("sha-256").is_err());
469
470 match HashAlgorithm::from_nix_str("unknown") {
471 Err(HashError::UnsupportedAlgorithm(s)) => assert_eq!(s, "unknown"),
472 other => panic!("expected UnsupportedAlgorithm, got {other:?}"),
473 }
474 }
475
476 #[test]
477 fn hash_digest_length_per_algorithm() {
478 assert_eq!(HashAlgorithm::Sha256.digest_len(), 32);
479 assert_eq!(HashAlgorithm::Sha512.digest_len(), 64);
480 assert_eq!(HashAlgorithm::Sha1.digest_len(), 20);
481 assert_eq!(HashAlgorithm::Md5.digest_len(), 16);
482 }
483
484 #[test]
485 fn hex_encode_decode_roundtrip() {
486 let original = vec![0x00, 0x11, 0x22, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
487 let encoded = hex::encode(&original);
488 assert_eq!(encoded, "001122aabbccddeeff");
489 let decoded = hex::decode(&encoded).unwrap();
490 assert_eq!(decoded, original);
491 }
492
493 #[test]
494 fn hex_decode_odd_length() {
495 assert!(hex::decode("abc").is_err());
496 }
497
498 #[test]
499 fn hex_decode_invalid_chars() {
500 assert!(hex::decode("zzzz").is_err());
501 assert!(hex::decode("gg").is_err());
502 }
503
504 #[test]
505 fn hex_roundtrip_all_byte_values() {
506 let all_bytes: Vec<u8> = (0..=255).collect();
507 let encoded = hex::encode(&all_bytes);
508 let decoded = hex::decode(&encoded).unwrap();
509 assert_eq!(decoded, all_bytes);
510 }
511
512 #[test]
513 fn empty_digest_handling() {
514 let hash = NixHash::new(HashAlgorithm::Sha256, vec![]);
515 let nix_str = hash.to_nix_string();
516 assert_eq!(nix_str, "sha256:");
517
518 let sri = hash.to_sri();
519 assert_eq!(sri, "sha256-");
520 }
521
522 #[test]
523 fn base64_encode_known_vectors() {
524 assert_eq!(minimal_base64_encode(b""), "");
526 assert_eq!(minimal_base64_encode(b"f"), "Zg==");
527 assert_eq!(minimal_base64_encode(b"fo"), "Zm8=");
528 assert_eq!(minimal_base64_encode(b"foo"), "Zm9v");
529 assert_eq!(minimal_base64_encode(b"foob"), "Zm9vYg==");
530 assert_eq!(minimal_base64_encode(b"fooba"), "Zm9vYmE=");
531 assert_eq!(minimal_base64_encode(b"foobar"), "Zm9vYmFy");
532 }
533
534 #[test]
535 fn nix_string_with_all_algorithms() {
536 for algo in [HashAlgorithm::Sha256, HashAlgorithm::Sha512, HashAlgorithm::Sha1, HashAlgorithm::Md5] {
537 let digest = vec![0xab; algo.digest_len()];
538 let hash = NixHash::new(algo, digest);
539 let s = hash.to_nix_string();
540 let expected_prefix = format!("{}:", algo.as_nix_str());
541 assert!(s.starts_with(&expected_prefix), "failed for {algo:?}");
542 let hex_part = s.strip_prefix(&expected_prefix).unwrap();
543 assert_eq!(hex_part.len(), algo.digest_len() * 2);
544 }
545 }
546
547 #[test]
550 fn base64_decode_known_vectors() {
551 assert_eq!(base64_decode("").unwrap(), b"");
552 assert_eq!(base64_decode("Zg==").unwrap(), b"f");
553 assert_eq!(base64_decode("Zm8=").unwrap(), b"fo");
554 assert_eq!(base64_decode("Zm9v").unwrap(), b"foo");
555 assert_eq!(base64_decode("Zm9vYmFy").unwrap(), b"foobar");
556 }
557
558 #[test]
559 fn base64_decode_invalid_input() {
560 assert!(base64_decode("!!!invalid!!!").is_err());
561 }
562
563 #[test]
564 fn base64_roundtrip_binary() {
565 let data: Vec<u8> = (0..=255).collect();
566 let encoded = base64_encode(&data);
567 let decoded = base64_decode(&encoded).unwrap();
568 assert_eq!(decoded, data);
569 }
570
571 #[test]
574 fn sri_format_all_algorithms() {
575 for algo in [HashAlgorithm::Sha256, HashAlgorithm::Sha512, HashAlgorithm::Sha1, HashAlgorithm::Md5] {
576 let digest = vec![0x42; algo.digest_len()];
577 let hash = NixHash::new(algo, digest);
578 let sri = hash.to_sri();
579 let prefix = format!("{}-", algo.as_nix_str());
580 assert!(sri.starts_with(&prefix), "SRI for {algo:?} should start with {prefix}");
581 }
582 }
583
584 #[test]
585 fn sri_base64_decode_matches_digest() {
586 let digest = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04,
587 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
588 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
589 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C];
590 let hash = NixHash::new(HashAlgorithm::Sha256, digest.clone());
591 let sri = hash.to_sri();
592 let b64_part = sri.strip_prefix("sha256-").unwrap();
593 let decoded = base64_decode(b64_part).unwrap();
594 assert_eq!(decoded, digest);
595 }
596
597 #[test]
600 fn hex_encode_empty() {
601 assert_eq!(hex::encode(&[]), "");
602 }
603
604 #[test]
605 fn hex_decode_empty() {
606 assert_eq!(hex::decode("").unwrap(), Vec::<u8>::new());
607 }
608
609 #[test]
610 fn hex_decode_accepts_uppercase() {
611 assert_eq!(hex::decode("AABB").unwrap(), vec![0xAA, 0xBB]);
612 }
613
614 #[test]
617 fn nix_hash_equality() {
618 let h1 = NixHash::new(HashAlgorithm::Sha256, vec![1; 32]);
619 let h2 = NixHash::new(HashAlgorithm::Sha256, vec![1; 32]);
620 let h3 = NixHash::new(HashAlgorithm::Sha256, vec![2; 32]);
621 let h4 = NixHash::new(HashAlgorithm::Sha1, vec![1; 20]);
622 assert_eq!(h1, h2);
623 assert_ne!(h1, h3);
624 assert_ne!(h1, h4);
625 }
626
627 #[test]
630 fn hash_algorithm_is_copy() {
631 let a = HashAlgorithm::Sha256;
632 let b = a;
633 assert_eq!(a, b);
634 }
635
636 #[test]
639 fn hash_algorithm_display_strings() {
640 assert_eq!(format!("{}", HashAlgorithm::Sha256), "sha256");
641 assert_eq!(format!("{}", HashAlgorithm::Sha512), "sha512");
642 assert_eq!(format!("{}", HashAlgorithm::Sha1), "sha1");
643 assert_eq!(format!("{}", HashAlgorithm::Md5), "md5");
644 }
645
646 #[test]
647 fn hash_algorithm_from_str_via_parse() {
648 let algo: HashAlgorithm = "sha256".parse().unwrap();
650 assert_eq!(algo, HashAlgorithm::Sha256);
651
652 let algo: HashAlgorithm = "sha512".parse().unwrap();
653 assert_eq!(algo, HashAlgorithm::Sha512);
654
655 let algo: HashAlgorithm = "sha1".parse().unwrap();
656 assert_eq!(algo, HashAlgorithm::Sha1);
657
658 let algo: HashAlgorithm = "md5".parse().unwrap();
659 assert_eq!(algo, HashAlgorithm::Md5);
660
661 let result: Result<HashAlgorithm, _> = "blake2b".parse();
662 assert!(result.is_err());
663 }
664
665 #[test]
668 fn nix_hash_display_format_matches_to_nix_string() {
669 let hash = NixHash::new(HashAlgorithm::Sha256, vec![0xab, 0xcd, 0xef]);
670 let displayed = format!("{hash}");
671 let manual = hash.to_nix_string();
672 assert_eq!(displayed, manual);
673 assert_eq!(displayed, "sha256:abcdef");
674 }
675
676 #[test]
679 fn nix_hash_new_stores_fields() {
680 let h = NixHash::new(HashAlgorithm::Sha512, vec![1, 2, 3]);
681 assert_eq!(h.algorithm, HashAlgorithm::Sha512);
682 assert_eq!(h.digest, vec![1, 2, 3]);
683 }
684
685 #[test]
688 fn minimal_base64_encode_matches_base64_encode() {
689 let data = b"hello world";
690 assert_eq!(minimal_base64_encode(data), base64_encode(data));
691 }
692
693 #[test]
696 fn base64_encode_no_padding_needed() {
697 assert_eq!(base64_encode(b"abc"), "YWJj");
699 }
700
701 #[test]
702 fn base64_encode_one_padding() {
703 assert_eq!(base64_encode(b"f"), "Zg==");
706 }
707
708 #[test]
709 fn base64_encode_two_padding() {
710 assert_eq!(base64_encode(b"fo"), "Zm8=");
712 }
713
714 #[test]
717 fn hex_decode_odd_length_error_variant() {
718 match hex::decode("a") {
719 Err(HashError::InvalidEncoding) => {}
720 other => panic!("expected InvalidEncoding, got {other:?}"),
721 }
722 }
723
724 #[test]
725 fn hex_decode_invalid_chars_error_variant() {
726 match hex::decode("zz") {
727 Err(HashError::InvalidEncoding) => {}
728 other => panic!("expected InvalidEncoding, got {other:?}"),
729 }
730 }
731
732 #[test]
735 fn hash_error_display_strings() {
736 let err = HashError::UnsupportedAlgorithm("blake3".to_string());
737 let s = format!("{err}");
738 assert!(s.contains("blake3"));
739
740 let err = HashError::InvalidEncoding;
741 let s = format!("{err}");
742 assert!(s.contains("invalid"));
743 }
744
745 #[test]
748 fn digest_len_for_all_algorithms() {
749 let cases = [
750 (HashAlgorithm::Md5, 16),
751 (HashAlgorithm::Sha1, 20),
752 (HashAlgorithm::Sha256, 32),
753 (HashAlgorithm::Sha512, 64),
754 ];
755 for (algo, expected_len) in cases {
756 assert_eq!(algo.digest_len(), expected_len);
757 }
758 }
759
760 #[test]
763 fn algorithm_display_roundtrip_through_from_nix_str() {
764 for algo in [
765 HashAlgorithm::Sha256,
766 HashAlgorithm::Sha512,
767 HashAlgorithm::Sha1,
768 HashAlgorithm::Md5,
769 ] {
770 let s = format!("{algo}");
771 let parsed = HashAlgorithm::from_nix_str(&s).unwrap();
772 assert_eq!(parsed, algo);
773 }
774 }
775
776 #[test]
779 fn hex_roundtrip_lengths_5_10_20_32_64() {
780 for len in [5, 10, 20, 32, 64] {
781 let data: Vec<u8> = (0..len).map(|i| i as u8).collect();
782 let encoded = hex::encode(&data);
783 assert_eq!(encoded.len(), len * 2);
784 let decoded = hex::decode(&encoded).unwrap();
785 assert_eq!(decoded, data);
786 }
787 }
788
789 #[test]
792 fn base64_roundtrip_lengths_1_through_10() {
793 for len in 1..=10 {
794 let data: Vec<u8> = (0..len).map(|i| i as u8).collect();
795 let encoded = base64_encode(&data);
796 let decoded = base64_decode(&encoded).unwrap();
797 assert_eq!(decoded, data, "failed for length {len}");
798 }
799 }
800
801 #[test]
804 fn hash_algorithm_in_hashset() {
805 use std::collections::HashSet;
806 let mut set = HashSet::new();
807 set.insert(HashAlgorithm::Sha256);
808 set.insert(HashAlgorithm::Sha256);
809 set.insert(HashAlgorithm::Md5);
810 assert_eq!(set.len(), 2);
811 assert!(set.contains(&HashAlgorithm::Sha256));
812 assert!(set.contains(&HashAlgorithm::Md5));
813 assert!(!set.contains(&HashAlgorithm::Sha512));
814 }
815
816 #[test]
819 fn nix_hash_empty_digest_to_sri() {
820 let hash = NixHash::new(HashAlgorithm::Md5, vec![]);
821 let sri = hash.to_sri();
822 assert_eq!(sri, "md5-");
823 }
824
825 #[test]
826 fn nix_hash_empty_digest_display() {
827 let hash = NixHash::new(HashAlgorithm::Sha1, vec![]);
828 assert_eq!(format!("{hash}"), "sha1:");
829 }
830
831 #[test]
834 fn hex_encode_known_vectors() {
835 assert_eq!(hex::encode(b"\x00"), "00");
836 assert_eq!(hex::encode(b"\xff"), "ff");
837 assert_eq!(hex::encode(b"\x01\x02\x03\x04"), "01020304");
838 assert_eq!(hex::encode(b"\xde\xad\xbe\xef"), "deadbeef");
839 }
840
841 #[test]
842 fn hex_encode_lowercase_only() {
843 let encoded = hex::encode(&[0xAB, 0xCD, 0xEF]);
844 assert_eq!(encoded, "abcdef");
846 assert!(encoded.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
847 }
848
849 #[test]
852 fn base64_decode_invalid_returns_invalid_encoding() {
853 match base64_decode("@@@") {
854 Err(HashError::InvalidEncoding) => {}
855 other => panic!("expected InvalidEncoding, got {other:?}"),
856 }
857 }
858
859 #[test]
862 fn sha512_full_digest_sri_length() {
863 let hash = NixHash::new(HashAlgorithm::Sha512, vec![0xAA; 64]);
864 let sri = hash.to_sri();
865 let b64 = sri.strip_prefix("sha512-").unwrap();
866 assert_eq!(b64.len(), 88);
868 }
869
870 #[test]
871 fn sha1_full_digest_hex_length() {
872 let hash = NixHash::new(HashAlgorithm::Sha1, vec![0x55; 20]);
873 let hex_part = hash.to_nix_string();
874 let h = hex_part.strip_prefix("sha1:").unwrap();
875 assert_eq!(h.len(), 40); }
877
878 #[test]
887 fn bare_base64_sha256_round_trips_to_hex() {
888 let raw = "zvncoRkQx3AwPx52ehjA2vcFroF+yDC2MQR5uS6DATs=";
890 let h = NixHash::parse_any(HashAlgorithm::Sha256, raw)
891 .expect("bare base64 sha256 must parse");
892 assert_eq!(h.digest.len(), 32);
893 let sri = format!("sha256-{raw}");
895 let h2 = NixHash::parse_any(HashAlgorithm::Sha256, &sri).unwrap();
896 assert_eq!(h.digest, h2.digest);
897 }
898
899 #[test]
900 fn bare_base64_length_per_algo_disambiguates() {
901 for (algo, n) in [
904 (HashAlgorithm::Sha256, 32usize),
905 (HashAlgorithm::Sha512, 64),
906 (HashAlgorithm::Sha1, 20),
907 (HashAlgorithm::Md5, 16),
908 ] {
909 let digest = vec![0x3C; n];
910 let b64 = base64_encode(&digest);
911 let parsed = NixHash::parse_any(algo, &b64)
912 .expect("bare base64 must parse for every algo");
913 assert_eq!(parsed.digest, digest);
914 }
915 }
916
917 #[test]
918 fn bare_base64_wrong_length_rejected() {
919 let short = base64_encode(&[0u8; 16]); assert!(matches!(
923 NixHash::parse_any(HashAlgorithm::Sha256, &short),
924 Err(HashError::InvalidEncoding)
925 ));
926 }
927
928 #[test]
935 fn sri_unpadded_base64_parses() {
936 let raw = "sha256-UlI+6OMUj5F6uVAw+Mg2wOZrjfdRq73d1qufaXVI/go";
939 let h = NixHash::parse_any(HashAlgorithm::Sha256, raw)
940 .expect("unpadded SRI sha256 must parse");
941 assert_eq!(h.digest.len(), 32);
942 let padded = format!("{raw}=");
944 let h2 = NixHash::parse_any(HashAlgorithm::Sha256, &padded).unwrap();
945 assert_eq!(h.digest, h2.digest);
946 }
947
948 #[test]
949 fn bare_unpadded_base64_parses_every_algo() {
950 for (algo, n) in [
951 (HashAlgorithm::Sha256, 32usize),
952 (HashAlgorithm::Sha512, 64),
953 (HashAlgorithm::Sha1, 20),
954 (HashAlgorithm::Md5, 16),
955 ] {
956 let digest = vec![0x5Au8; n];
957 let padded = base64_encode(&digest);
958 let unpadded = padded.trim_end_matches('=');
959 let parsed = NixHash::parse_any(algo, unpadded)
960 .expect("unpadded bare base64 must parse");
961 assert_eq!(parsed.digest, digest);
962 }
963 }
964
965 #[test]
966 fn pad_tolerant_matches_padded() {
967 let digest = vec![0x11u8; 32];
968 let padded = base64_encode(&digest);
969 let unpadded = padded.trim_end_matches('=');
970 assert_eq!(
971 base64_decode_padtolerant(unpadded).unwrap(),
972 base64_decode(&padded).unwrap()
973 );
974 }
975}