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 sri_prefix = format!("{}-", algo.as_nix_str());
163
164 if let Some(b64) = raw.strip_prefix(&sri_prefix) {
165 let bytes = base64_decode(b64)?;
166 if bytes.len() != want {
167 return Err(HashError::InvalidEncoding);
168 }
169 return Ok(bytes);
170 }
171
172 if raw.len() == hex_len
173 && raw.chars().all(|c| c.is_ascii_hexdigit() && !c.is_uppercase())
174 {
175 let bytes = hex::decode(raw)?;
176 return Ok(bytes);
177 }
178
179 if raw.len() == base32_len {
180 let alphabet = "0123456789abcdfghijklmnpqrsvwxyz";
183 if raw.chars().all(|c| alphabet.contains(c)) {
184 let bytes = decode_nix_base32(raw, want)?;
185 return Ok(bytes);
186 }
187 }
188
189 Err(HashError::InvalidEncoding)
190}
191
192fn decode_nix_base32(input: &str, want: usize) -> Result<Vec<u8>, HashError> {
201 let alphabet = b"0123456789abcdfghijklmnpqrsvwxyz";
202 let mut digit_value = [0u8; 128];
203 for (i, &c) in alphabet.iter().enumerate() {
204 digit_value[c as usize] = i as u8;
205 }
206 let mut out = vec![0u8; want];
207 let bytes = input.as_bytes();
208 for (n, &c) in bytes.iter().rev().enumerate() {
209 let d = digit_value[c as usize] as usize;
210 let b = n * 5;
211 let i = b / 8;
212 let j = b % 8;
213 out[i] |= ((d << j) & 0xff) as u8;
214 if i + 1 < want {
215 out[i + 1] |= (d >> (8 - j)) as u8;
216 } else if (d >> (8 - j)) != 0 {
217 return Err(HashError::InvalidEncoding);
220 }
221 }
222 Ok(out)
223}
224
225impl std::fmt::Display for NixHash {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 write!(f, "{}:{}", self.algorithm, hex::encode(&self.digest))
229 }
230}
231
232#[must_use]
234pub fn base64_encode(input: &[u8]) -> String {
235 use base64::Engine;
236 base64::engine::general_purpose::STANDARD.encode(input)
237}
238
239pub fn base64_decode(input: &str) -> Result<Vec<u8>, HashError> {
241 use base64::Engine;
242 base64::engine::general_purpose::STANDARD
243 .decode(input)
244 .map_err(|_| HashError::InvalidEncoding)
245}
246
247#[must_use]
249pub fn minimal_base64_encode(input: &[u8]) -> String {
250 base64_encode(input)
251}
252
253pub(crate) mod hex {
255 #[must_use]
257 pub fn encode(bytes: &[u8]) -> String {
258 let mut s = String::with_capacity(bytes.len() * 2);
259 for b in bytes {
260 s.push_str(&format!("{b:02x}"));
261 }
262 s
263 }
264
265 pub fn decode(s: &str) -> Result<Vec<u8>, super::HashError> {
267 if !s.len().is_multiple_of(2) {
268 return Err(super::HashError::InvalidEncoding);
269 }
270 (0..s.len())
271 .step_by(2)
272 .map(|i| {
273 u8::from_str_radix(&s[i..i + 2], 16)
274 .map_err(|_| super::HashError::InvalidEncoding)
275 })
276 .collect()
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use proptest::prelude::*;
284
285 #[test]
286 fn parse_any_accepts_64_char_hex_sha256() {
287 let raw = "0".repeat(64);
288 let h = NixHash::parse_any(HashAlgorithm::Sha256, &raw).unwrap();
289 assert_eq!(h.digest, vec![0u8; 32]);
290 }
291
292 #[test]
293 fn parse_any_accepts_52_char_base32_sha256() {
294 let raw = "0".repeat(52);
295 let h = NixHash::parse_any(HashAlgorithm::Sha256, &raw).unwrap();
296 assert_eq!(h.digest, vec![0u8; 32]);
297 }
298
299 #[test]
300 fn parse_any_accepts_sri_sha256() {
301 let sri = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
303 let h = NixHash::parse_any(HashAlgorithm::Sha256, sri).unwrap();
304 assert_eq!(h.digest, vec![0u8; 32]);
305 }
306
307 #[test]
308 fn parse_any_rejects_wrong_length() {
309 let bad = "0".repeat(50);
311 assert!(NixHash::parse_any(HashAlgorithm::Sha256, &bad).is_err());
312 }
313
314 #[test]
315 fn parse_any_rejects_invalid_alphabet() {
316 let bad = format!("Z{}", "0".repeat(51));
320 assert!(NixHash::parse_any(HashAlgorithm::Sha256, &bad).is_err());
321 }
322
323 proptest! {
324 #[test]
326 fn parse_any_all_formats_agree(digest in proptest::collection::vec(any::<u8>(), 32..=32)) {
327 let nh = NixHash::new(HashAlgorithm::Sha256, digest.clone());
328 let hex_str = hex::encode(&digest);
329 let sri = nh.to_sri();
330 let mut b32 = String::with_capacity(52);
333 let alphabet = b"0123456789abcdfghijklmnpqrsvwxyz";
336 for n in (0..52usize).rev() {
337 let b = n * 5;
338 let i = b / 8;
339 let j = b % 8;
340 let mut v = (digest[i] as usize) >> j;
341 if i + 1 < 32 {
342 v |= (digest[i + 1] as usize) << (8 - j);
343 }
344 b32.push(alphabet[v & 0x1f] as char);
345 }
346 let parsed_hex = NixHash::parse_any(HashAlgorithm::Sha256, &hex_str).unwrap();
347 let parsed_b32 = NixHash::parse_any(HashAlgorithm::Sha256, &b32).unwrap();
348 let parsed_sri = NixHash::parse_any(HashAlgorithm::Sha256, &sri).unwrap();
349 prop_assert_eq!(&parsed_hex.digest, &digest);
350 prop_assert_eq!(&parsed_b32.digest, &digest);
351 prop_assert_eq!(&parsed_sri.digest, &digest);
352 }
353 }
354
355 #[test]
356 fn algorithm_roundtrip() {
357 for algo in [HashAlgorithm::Sha256, HashAlgorithm::Sha512, HashAlgorithm::Sha1, HashAlgorithm::Md5] {
358 let parsed = HashAlgorithm::from_nix_str(algo.as_nix_str()).unwrap();
359 assert_eq!(parsed, algo);
360 }
361 }
362
363 #[test]
364 fn nix_string_format() {
365 let hash = NixHash::new(HashAlgorithm::Sha256, vec![0xab; 32]);
366 let s = hash.to_nix_string();
367 assert!(s.starts_with("sha256:"));
368 assert_eq!(s.len(), 7 + 64); }
370
371 #[test]
372 fn sri_roundtrip() {
373 let digest = vec![0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
375 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
376 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
377 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef];
378 let hash = NixHash::new(HashAlgorithm::Sha256, digest.clone());
379 let sri = hash.to_sri();
380 assert!(sri.starts_with("sha256-"));
381 let b64_part = sri.strip_prefix("sha256-").unwrap();
383 assert!(!b64_part.is_empty());
385 assert_eq!(b64_part.len(), 44);
387 }
388
389 #[test]
390 fn invalid_algorithm_string() {
391 assert!(HashAlgorithm::from_nix_str("blake2b").is_err());
392 assert!(HashAlgorithm::from_nix_str("SHA256").is_err());
393 assert!(HashAlgorithm::from_nix_str("").is_err());
394 assert!(HashAlgorithm::from_nix_str("sha-256").is_err());
395
396 match HashAlgorithm::from_nix_str("unknown") {
397 Err(HashError::UnsupportedAlgorithm(s)) => assert_eq!(s, "unknown"),
398 other => panic!("expected UnsupportedAlgorithm, got {other:?}"),
399 }
400 }
401
402 #[test]
403 fn hash_digest_length_per_algorithm() {
404 assert_eq!(HashAlgorithm::Sha256.digest_len(), 32);
405 assert_eq!(HashAlgorithm::Sha512.digest_len(), 64);
406 assert_eq!(HashAlgorithm::Sha1.digest_len(), 20);
407 assert_eq!(HashAlgorithm::Md5.digest_len(), 16);
408 }
409
410 #[test]
411 fn hex_encode_decode_roundtrip() {
412 let original = vec![0x00, 0x11, 0x22, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
413 let encoded = hex::encode(&original);
414 assert_eq!(encoded, "001122aabbccddeeff");
415 let decoded = hex::decode(&encoded).unwrap();
416 assert_eq!(decoded, original);
417 }
418
419 #[test]
420 fn hex_decode_odd_length() {
421 assert!(hex::decode("abc").is_err());
422 }
423
424 #[test]
425 fn hex_decode_invalid_chars() {
426 assert!(hex::decode("zzzz").is_err());
427 assert!(hex::decode("gg").is_err());
428 }
429
430 #[test]
431 fn hex_roundtrip_all_byte_values() {
432 let all_bytes: Vec<u8> = (0..=255).collect();
433 let encoded = hex::encode(&all_bytes);
434 let decoded = hex::decode(&encoded).unwrap();
435 assert_eq!(decoded, all_bytes);
436 }
437
438 #[test]
439 fn empty_digest_handling() {
440 let hash = NixHash::new(HashAlgorithm::Sha256, vec![]);
441 let nix_str = hash.to_nix_string();
442 assert_eq!(nix_str, "sha256:");
443
444 let sri = hash.to_sri();
445 assert_eq!(sri, "sha256-");
446 }
447
448 #[test]
449 fn base64_encode_known_vectors() {
450 assert_eq!(minimal_base64_encode(b""), "");
452 assert_eq!(minimal_base64_encode(b"f"), "Zg==");
453 assert_eq!(minimal_base64_encode(b"fo"), "Zm8=");
454 assert_eq!(minimal_base64_encode(b"foo"), "Zm9v");
455 assert_eq!(minimal_base64_encode(b"foob"), "Zm9vYg==");
456 assert_eq!(minimal_base64_encode(b"fooba"), "Zm9vYmE=");
457 assert_eq!(minimal_base64_encode(b"foobar"), "Zm9vYmFy");
458 }
459
460 #[test]
461 fn nix_string_with_all_algorithms() {
462 for algo in [HashAlgorithm::Sha256, HashAlgorithm::Sha512, HashAlgorithm::Sha1, HashAlgorithm::Md5] {
463 let digest = vec![0xab; algo.digest_len()];
464 let hash = NixHash::new(algo, digest);
465 let s = hash.to_nix_string();
466 let expected_prefix = format!("{}:", algo.as_nix_str());
467 assert!(s.starts_with(&expected_prefix), "failed for {algo:?}");
468 let hex_part = s.strip_prefix(&expected_prefix).unwrap();
469 assert_eq!(hex_part.len(), algo.digest_len() * 2);
470 }
471 }
472
473 #[test]
476 fn base64_decode_known_vectors() {
477 assert_eq!(base64_decode("").unwrap(), b"");
478 assert_eq!(base64_decode("Zg==").unwrap(), b"f");
479 assert_eq!(base64_decode("Zm8=").unwrap(), b"fo");
480 assert_eq!(base64_decode("Zm9v").unwrap(), b"foo");
481 assert_eq!(base64_decode("Zm9vYmFy").unwrap(), b"foobar");
482 }
483
484 #[test]
485 fn base64_decode_invalid_input() {
486 assert!(base64_decode("!!!invalid!!!").is_err());
487 }
488
489 #[test]
490 fn base64_roundtrip_binary() {
491 let data: Vec<u8> = (0..=255).collect();
492 let encoded = base64_encode(&data);
493 let decoded = base64_decode(&encoded).unwrap();
494 assert_eq!(decoded, data);
495 }
496
497 #[test]
500 fn sri_format_all_algorithms() {
501 for algo in [HashAlgorithm::Sha256, HashAlgorithm::Sha512, HashAlgorithm::Sha1, HashAlgorithm::Md5] {
502 let digest = vec![0x42; algo.digest_len()];
503 let hash = NixHash::new(algo, digest);
504 let sri = hash.to_sri();
505 let prefix = format!("{}-", algo.as_nix_str());
506 assert!(sri.starts_with(&prefix), "SRI for {algo:?} should start with {prefix}");
507 }
508 }
509
510 #[test]
511 fn sri_base64_decode_matches_digest() {
512 let digest = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04,
513 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
514 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
515 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C];
516 let hash = NixHash::new(HashAlgorithm::Sha256, digest.clone());
517 let sri = hash.to_sri();
518 let b64_part = sri.strip_prefix("sha256-").unwrap();
519 let decoded = base64_decode(b64_part).unwrap();
520 assert_eq!(decoded, digest);
521 }
522
523 #[test]
526 fn hex_encode_empty() {
527 assert_eq!(hex::encode(&[]), "");
528 }
529
530 #[test]
531 fn hex_decode_empty() {
532 assert_eq!(hex::decode("").unwrap(), Vec::<u8>::new());
533 }
534
535 #[test]
536 fn hex_decode_accepts_uppercase() {
537 assert_eq!(hex::decode("AABB").unwrap(), vec![0xAA, 0xBB]);
538 }
539
540 #[test]
543 fn nix_hash_equality() {
544 let h1 = NixHash::new(HashAlgorithm::Sha256, vec![1; 32]);
545 let h2 = NixHash::new(HashAlgorithm::Sha256, vec![1; 32]);
546 let h3 = NixHash::new(HashAlgorithm::Sha256, vec![2; 32]);
547 let h4 = NixHash::new(HashAlgorithm::Sha1, vec![1; 20]);
548 assert_eq!(h1, h2);
549 assert_ne!(h1, h3);
550 assert_ne!(h1, h4);
551 }
552
553 #[test]
556 fn hash_algorithm_is_copy() {
557 let a = HashAlgorithm::Sha256;
558 let b = a;
559 assert_eq!(a, b);
560 }
561
562 #[test]
565 fn hash_algorithm_display_strings() {
566 assert_eq!(format!("{}", HashAlgorithm::Sha256), "sha256");
567 assert_eq!(format!("{}", HashAlgorithm::Sha512), "sha512");
568 assert_eq!(format!("{}", HashAlgorithm::Sha1), "sha1");
569 assert_eq!(format!("{}", HashAlgorithm::Md5), "md5");
570 }
571
572 #[test]
573 fn hash_algorithm_from_str_via_parse() {
574 let algo: HashAlgorithm = "sha256".parse().unwrap();
576 assert_eq!(algo, HashAlgorithm::Sha256);
577
578 let algo: HashAlgorithm = "sha512".parse().unwrap();
579 assert_eq!(algo, HashAlgorithm::Sha512);
580
581 let algo: HashAlgorithm = "sha1".parse().unwrap();
582 assert_eq!(algo, HashAlgorithm::Sha1);
583
584 let algo: HashAlgorithm = "md5".parse().unwrap();
585 assert_eq!(algo, HashAlgorithm::Md5);
586
587 let result: Result<HashAlgorithm, _> = "blake2b".parse();
588 assert!(result.is_err());
589 }
590
591 #[test]
594 fn nix_hash_display_format_matches_to_nix_string() {
595 let hash = NixHash::new(HashAlgorithm::Sha256, vec![0xab, 0xcd, 0xef]);
596 let displayed = format!("{hash}");
597 let manual = hash.to_nix_string();
598 assert_eq!(displayed, manual);
599 assert_eq!(displayed, "sha256:abcdef");
600 }
601
602 #[test]
605 fn nix_hash_new_stores_fields() {
606 let h = NixHash::new(HashAlgorithm::Sha512, vec![1, 2, 3]);
607 assert_eq!(h.algorithm, HashAlgorithm::Sha512);
608 assert_eq!(h.digest, vec![1, 2, 3]);
609 }
610
611 #[test]
614 fn minimal_base64_encode_matches_base64_encode() {
615 let data = b"hello world";
616 assert_eq!(minimal_base64_encode(data), base64_encode(data));
617 }
618
619 #[test]
622 fn base64_encode_no_padding_needed() {
623 assert_eq!(base64_encode(b"abc"), "YWJj");
625 }
626
627 #[test]
628 fn base64_encode_one_padding() {
629 assert_eq!(base64_encode(b"f"), "Zg==");
632 }
633
634 #[test]
635 fn base64_encode_two_padding() {
636 assert_eq!(base64_encode(b"fo"), "Zm8=");
638 }
639
640 #[test]
643 fn hex_decode_odd_length_error_variant() {
644 match hex::decode("a") {
645 Err(HashError::InvalidEncoding) => {}
646 other => panic!("expected InvalidEncoding, got {other:?}"),
647 }
648 }
649
650 #[test]
651 fn hex_decode_invalid_chars_error_variant() {
652 match hex::decode("zz") {
653 Err(HashError::InvalidEncoding) => {}
654 other => panic!("expected InvalidEncoding, got {other:?}"),
655 }
656 }
657
658 #[test]
661 fn hash_error_display_strings() {
662 let err = HashError::UnsupportedAlgorithm("blake3".to_string());
663 let s = format!("{err}");
664 assert!(s.contains("blake3"));
665
666 let err = HashError::InvalidEncoding;
667 let s = format!("{err}");
668 assert!(s.contains("invalid"));
669 }
670
671 #[test]
674 fn digest_len_for_all_algorithms() {
675 let cases = [
676 (HashAlgorithm::Md5, 16),
677 (HashAlgorithm::Sha1, 20),
678 (HashAlgorithm::Sha256, 32),
679 (HashAlgorithm::Sha512, 64),
680 ];
681 for (algo, expected_len) in cases {
682 assert_eq!(algo.digest_len(), expected_len);
683 }
684 }
685
686 #[test]
689 fn algorithm_display_roundtrip_through_from_nix_str() {
690 for algo in [
691 HashAlgorithm::Sha256,
692 HashAlgorithm::Sha512,
693 HashAlgorithm::Sha1,
694 HashAlgorithm::Md5,
695 ] {
696 let s = format!("{algo}");
697 let parsed = HashAlgorithm::from_nix_str(&s).unwrap();
698 assert_eq!(parsed, algo);
699 }
700 }
701
702 #[test]
705 fn hex_roundtrip_lengths_5_10_20_32_64() {
706 for len in [5, 10, 20, 32, 64] {
707 let data: Vec<u8> = (0..len).map(|i| i as u8).collect();
708 let encoded = hex::encode(&data);
709 assert_eq!(encoded.len(), len * 2);
710 let decoded = hex::decode(&encoded).unwrap();
711 assert_eq!(decoded, data);
712 }
713 }
714
715 #[test]
718 fn base64_roundtrip_lengths_1_through_10() {
719 for len in 1..=10 {
720 let data: Vec<u8> = (0..len).map(|i| i as u8).collect();
721 let encoded = base64_encode(&data);
722 let decoded = base64_decode(&encoded).unwrap();
723 assert_eq!(decoded, data, "failed for length {len}");
724 }
725 }
726
727 #[test]
730 fn hash_algorithm_in_hashset() {
731 use std::collections::HashSet;
732 let mut set = HashSet::new();
733 set.insert(HashAlgorithm::Sha256);
734 set.insert(HashAlgorithm::Sha256);
735 set.insert(HashAlgorithm::Md5);
736 assert_eq!(set.len(), 2);
737 assert!(set.contains(&HashAlgorithm::Sha256));
738 assert!(set.contains(&HashAlgorithm::Md5));
739 assert!(!set.contains(&HashAlgorithm::Sha512));
740 }
741
742 #[test]
745 fn nix_hash_empty_digest_to_sri() {
746 let hash = NixHash::new(HashAlgorithm::Md5, vec![]);
747 let sri = hash.to_sri();
748 assert_eq!(sri, "md5-");
749 }
750
751 #[test]
752 fn nix_hash_empty_digest_display() {
753 let hash = NixHash::new(HashAlgorithm::Sha1, vec![]);
754 assert_eq!(format!("{hash}"), "sha1:");
755 }
756
757 #[test]
760 fn hex_encode_known_vectors() {
761 assert_eq!(hex::encode(b"\x00"), "00");
762 assert_eq!(hex::encode(b"\xff"), "ff");
763 assert_eq!(hex::encode(b"\x01\x02\x03\x04"), "01020304");
764 assert_eq!(hex::encode(b"\xde\xad\xbe\xef"), "deadbeef");
765 }
766
767 #[test]
768 fn hex_encode_lowercase_only() {
769 let encoded = hex::encode(&[0xAB, 0xCD, 0xEF]);
770 assert_eq!(encoded, "abcdef");
772 assert!(encoded.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
773 }
774
775 #[test]
778 fn base64_decode_invalid_returns_invalid_encoding() {
779 match base64_decode("@@@") {
780 Err(HashError::InvalidEncoding) => {}
781 other => panic!("expected InvalidEncoding, got {other:?}"),
782 }
783 }
784
785 #[test]
788 fn sha512_full_digest_sri_length() {
789 let hash = NixHash::new(HashAlgorithm::Sha512, vec![0xAA; 64]);
790 let sri = hash.to_sri();
791 let b64 = sri.strip_prefix("sha512-").unwrap();
792 assert_eq!(b64.len(), 88);
794 }
795
796 #[test]
797 fn sha1_full_digest_hex_length() {
798 let hash = NixHash::new(HashAlgorithm::Sha1, vec![0x55; 20]);
799 let hex_part = hash.to_nix_string();
800 let h = hex_part.strip_prefix("sha1:").unwrap();
801 assert_eq!(h.len(), 40); }
803}