1use std::{
2 fmt::{Debug, Display, Formatter},
3 marker::PhantomData,
4 str::FromStr,
5};
6
7pub use digest::Digest;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use strum::{Display, EnumString, VariantArray, VariantNames};
10use winnow::{
11 ModalResult,
12 Parser,
13 combinator::{alt, cut_err, eof, repeat, terminated},
14 error::{StrContext, StrContextValue},
15 token::one_of,
16};
17
18use crate::{
19 Error,
20 digests::{Blake2b512, Md5, Sha1, Sha224, Sha256, Sha384, Sha512},
21};
22
23pub type Blake2b512Checksum = Checksum<Blake2b512>;
27
28pub type Md5Checksum = Checksum<Md5>;
30
31pub type Sha1Checksum = Checksum<Sha1>;
33
34pub type Sha224Checksum = Checksum<Sha224>;
36
37pub type Sha256Checksum = Checksum<Sha256>;
39
40pub type Sha384Checksum = Checksum<Sha384>;
42
43pub type Sha512Checksum = Checksum<Sha512>;
45
46#[derive(
48 Clone,
49 Copy,
50 Debug,
51 Deserialize,
52 Display,
53 EnumString,
54 Eq,
55 Hash,
56 Ord,
57 PartialEq,
58 PartialOrd,
59 Serialize,
60 VariantNames,
61 VariantArray,
62)]
63pub enum ChecksumAlgorithm {
64 Blake2b512,
66 Md5,
68 Sha1,
70 Sha224,
72 Sha256,
74 Sha384,
76 Sha512,
78}
79
80impl ChecksumAlgorithm {
81 pub fn is_deprecated(&self) -> bool {
106 match self {
107 ChecksumAlgorithm::Md5 | ChecksumAlgorithm::Sha1 => true,
108 ChecksumAlgorithm::Blake2b512
109 | ChecksumAlgorithm::Sha224
110 | ChecksumAlgorithm::Sha256
111 | ChecksumAlgorithm::Sha384
112 | ChecksumAlgorithm::Sha512 => false,
113 }
114 }
115
116 pub fn non_deprecated_checksums(&self) -> Vec<ChecksumAlgorithm> {
118 <ChecksumAlgorithm as VariantArray>::VARIANTS
119 .iter()
120 .filter(|algo| !algo.is_deprecated())
121 .copied()
122 .collect::<Vec<ChecksumAlgorithm>>()
123 }
124}
125
126#[derive(Clone)]
199pub struct Checksum<D: Digest> {
200 digest: Vec<u8>,
201 _marker: PhantomData<D>,
202}
203
204impl<D: Digest> Serialize for Checksum<D> {
205 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
210 where
211 S: Serializer,
212 {
213 serializer.serialize_str(&self.to_string())
214 }
215}
216
217impl<'de, D: Digest> Deserialize<'de> for Checksum<D> {
218 fn deserialize<De>(deserializer: De) -> Result<Self, De::Error>
219 where
220 De: Deserializer<'de>,
221 {
222 let s = String::deserialize(deserializer)?;
223 Checksum::from_str(&s).map_err(serde::de::Error::custom)
224 }
225}
226
227impl<D: Digest> Checksum<D> {
228 pub fn calculate_from(input: impl AsRef<[u8]>) -> Self {
240 let mut hasher = D::new();
241 hasher.update(input);
242
243 Checksum {
244 digest: hasher.finalize()[..].to_vec(),
245 _marker: PhantomData,
246 }
247 }
248
249 pub fn inner(&self) -> &[u8] {
251 &self.digest
252 }
253
254 pub fn parser(input: &mut &str) -> ModalResult<Self> {
264 #[inline]
268 fn hex_digit(input: &mut &str) -> ModalResult<u8> {
269 one_of(('0'..='9', 'a'..='f', 'A'..='F'))
270 .map(|d: char|
271 d.to_digit(16).unwrap().try_into().unwrap())
275 .context(StrContext::Expected(StrContextValue::Description(
276 "ASCII hex digit",
277 )))
278 .parse_next(input)
279 }
280
281 let hex_pair = (hex_digit, hex_digit).map(|(first, second)|
282 (first << 4) + second);
284
285 let digest = cut_err(repeat(<D as Digest>::output_size(), hex_pair))
287 .context(StrContext::Label("hash digest"))
288 .context(StrContext::Expected(StrContextValue::Description(
289 "a hex hash digest with the appropriate length for the given algorithm.",
290 )))
291 .parse_next(input)?;
292
293 cut_err(eof)
294 .context(StrContext::Expected(StrContextValue::Description(
295 "end of checksum. Checksum is too long.",
296 )))
297 .parse_next(input)?;
298
299 Ok(Self {
300 digest,
301 _marker: PhantomData,
302 })
303 }
304}
305
306impl<D: Digest> FromStr for Checksum<D> {
307 type Err = Error;
308 fn from_str(s: &str) -> Result<Checksum<D>, Self::Err> {
327 Ok(Checksum::parser.parse(s)?)
328 }
329}
330
331impl<D: Digest> Display for Checksum<D> {
332 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
333 write!(
334 fmt,
335 "{}",
336 self.digest
337 .iter()
338 .map(|x| format!("{x:02x?}"))
339 .collect::<Vec<String>>()
340 .join("")
341 )
342 }
343}
344
345impl<D: Digest> Debug for Checksum<D> {
348 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
349 Display::fmt(&self, f)
350 }
351}
352
353impl<D: Digest> PartialEq for Checksum<D> {
354 fn eq(&self, other: &Self) -> bool {
355 self.digest == other.digest
356 }
357}
358
359impl<D: Digest> Eq for Checksum<D> {}
360
361impl<D: Digest> Ord for Checksum<D> {
362 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
363 self.digest.cmp(&other.digest)
364 }
365}
366
367impl<D: Digest> PartialOrd for Checksum<D> {
368 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
369 Some(self.cmp(other))
370 }
371}
372
373#[derive(Clone, Debug, Deserialize, Serialize)]
378#[serde(tag = "type")]
379pub enum SkippableChecksum<D: Digest + Clone> {
380 Skip,
382 #[serde(bound = "D: Digest + Clone")]
384 Checksum {
385 digest: Checksum<D>,
387 },
388}
389
390impl<D: Digest + Clone> SkippableChecksum<D> {
391 pub fn is_skipped(&self) -> bool {
395 matches!(self, SkippableChecksum::Skip)
396 }
397
398 pub fn parser(input: &mut &str) -> ModalResult<Self> {
408 terminated(
409 alt((
410 "SKIP".value(Self::Skip),
411 Checksum::parser.map(|digest| Self::Checksum { digest }),
412 )),
413 cut_err(eof).context(StrContext::Expected(StrContextValue::Description(
414 "end of checksum.",
415 ))),
416 )
417 .parse_next(input)
418 }
419}
420
421impl<D: Digest + Clone> FromStr for SkippableChecksum<D> {
422 type Err = Error;
423 fn from_str(s: &str) -> Result<SkippableChecksum<D>, Self::Err> {
444 Ok(Self::parser.parse(s)?)
445 }
446}
447
448impl<D: Digest + Clone> Display for SkippableChecksum<D> {
449 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
450 let output = match self {
451 SkippableChecksum::Skip => "SKIP".to_string(),
452 SkippableChecksum::Checksum { digest } => digest.to_string(),
453 };
454 write!(fmt, "{output}",)
455 }
456}
457
458impl<D: Digest + Clone> PartialEq for SkippableChecksum<D> {
459 fn eq(&self, other: &Self) -> bool {
460 match (self, other) {
461 (SkippableChecksum::Skip, SkippableChecksum::Skip) => true,
462 (SkippableChecksum::Skip, SkippableChecksum::Checksum { .. }) => false,
463 (SkippableChecksum::Checksum { .. }, SkippableChecksum::Skip) => false,
464 (
465 SkippableChecksum::Checksum { digest },
466 SkippableChecksum::Checksum {
467 digest: digest_other,
468 },
469 ) => digest == digest_other,
470 }
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use proptest::prelude::*;
477 use rstest::rstest;
478
479 use super::*;
480
481 proptest! {
482 #![proptest_config(ProptestConfig::with_cases(1000))]
483
484 #[test]
485 fn valid_checksum_blake2b512_from_string(string in r"[a-f0-9]{128}") {
486 prop_assert_eq!(&string, &format!("{}", Blake2b512Checksum::from_str(&string).unwrap()));
487 }
488
489 #[test]
490 fn invalid_checksum_blake2b512_bigger_size(string in r"[a-f0-9]{129}") {
491 assert!(Blake2b512Checksum::from_str(&string).is_err());
492 }
493
494 #[test]
495 fn invalid_checksum_blake2b512_smaller_size(string in r"[a-f0-9]{127}") {
496 assert!(Blake2b512Checksum::from_str(&string).is_err());
497 }
498
499 #[test]
500 fn invalid_checksum_blake2b512_wrong_chars(string in r"[e-z0-9]{128}") {
501 assert!(Blake2b512Checksum::from_str(&string).is_err());
502 }
503
504 #[test]
505 fn valid_checksum_sha1_from_string(string in r"[a-f0-9]{40}") {
506 prop_assert_eq!(&string, &format!("{}", Sha1Checksum::from_str(&string).unwrap()));
507 }
508
509 #[test]
510 fn invalid_checksum_sha1_from_string_bigger_size(string in r"[a-f0-9]{41}") {
511 assert!(Sha1Checksum::from_str(&string).is_err());
512 }
513
514 #[test]
515 fn invalid_checksum_sha1_from_string_smaller_size(string in r"[a-f0-9]{39}") {
516 assert!(Sha1Checksum::from_str(&string).is_err());
517 }
518
519 #[test]
520 fn invalid_checksum_sha1_from_string_wrong_chars(string in r"[e-z0-9]{40}") {
521 assert!(Sha1Checksum::from_str(&string).is_err());
522 }
523
524 #[test]
525 fn valid_checksum_sha224_from_string(string in r"[a-f0-9]{56}") {
526 prop_assert_eq!(&string, &format!("{}", Sha224Checksum::from_str(&string).unwrap()));
527 }
528
529 #[test]
530 fn invalid_checksum_sha224_from_string_bigger_size(string in r"[a-f0-9]{57}") {
531 assert!(Sha224Checksum::from_str(&string).is_err());
532 }
533
534 #[test]
535 fn invalid_checksum_sha224_from_string_smaller_size(string in r"[a-f0-9]{55}") {
536 assert!(Sha224Checksum::from_str(&string).is_err());
537 }
538
539 #[test]
540 fn invalid_checksum_sha224_from_string_wrong_chars(string in r"[e-z0-9]{56}") {
541 assert!(Sha224Checksum::from_str(&string).is_err());
542 }
543
544 #[test]
545 fn valid_checksum_sha256_from_string(string in r"[a-f0-9]{64}") {
546 prop_assert_eq!(&string, &format!("{}", Sha256Checksum::from_str(&string).unwrap()));
547 }
548
549 #[test]
550 fn invalid_checksum_sha256_from_string_bigger_size(string in r"[a-f0-9]{65}") {
551 assert!(Sha256Checksum::from_str(&string).is_err());
552 }
553
554 #[test]
555 fn invalid_checksum_sha256_from_string_smaller_size(string in r"[a-f0-9]{63}") {
556 assert!(Sha256Checksum::from_str(&string).is_err());
557 }
558
559 #[test]
560 fn invalid_checksum_sha256_from_string_wrong_chars(string in r"[e-z0-9]{64}") {
561 assert!(Sha256Checksum::from_str(&string).is_err());
562 }
563
564 #[test]
565 fn valid_checksum_sha384_from_string(string in r"[a-f0-9]{96}") {
566 prop_assert_eq!(&string, &format!("{}", Sha384Checksum::from_str(&string).unwrap()));
567 }
568
569 #[test]
570 fn invalid_checksum_sha384_from_string_bigger_size(string in r"[a-f0-9]{97}") {
571 assert!(Sha384Checksum::from_str(&string).is_err());
572 }
573
574 #[test]
575 fn invalid_checksum_sha384_from_string_smaller_size(string in r"[a-f0-9]{95}") {
576 assert!(Sha384Checksum::from_str(&string).is_err());
577 }
578
579 #[test]
580 fn invalid_checksum_sha384_from_string_wrong_chars(string in r"[e-z0-9]{96}") {
581 assert!(Sha384Checksum::from_str(&string).is_err());
582 }
583
584 #[test]
585 fn valid_checksum_sha512_from_string(string in r"[a-f0-9]{128}") {
586 prop_assert_eq!(&string, &format!("{}", Sha512Checksum::from_str(&string).unwrap()));
587 }
588
589 #[test]
590 fn invalid_checksum_sha512_from_string_bigger_size(string in r"[a-f0-9]{129}") {
591 assert!(Sha512Checksum::from_str(&string).is_err());
592 }
593
594 #[test]
595 fn invalid_checksum_sha512_from_string_smaller_size(string in r"[a-f0-9]{127}") {
596 assert!(Sha512Checksum::from_str(&string).is_err());
597 }
598
599 #[test]
600 fn invalid_checksum_sha512_from_string_wrong_chars(string in r"[e-z0-9]{128}") {
601 assert!(Sha512Checksum::from_str(&string).is_err());
602 }
603 }
604
605 #[rstest]
606 fn checksum_blake2b512() {
607 let data = "foo\n";
608 let digest = vec![
609 210, 2, 215, 149, 29, 242, 196, 183, 17, 202, 68, 180, 188, 201, 215, 179, 99, 250, 66,
610 82, 18, 126, 5, 140, 26, 145, 14, 192, 91, 108, 208, 56, 215, 28, 194, 18, 33, 192, 49,
611 192, 53, 159, 153, 62, 116, 107, 7, 245, 150, 92, 248, 197, 195, 116, 106, 88, 51, 122,
612 217, 171, 101, 39, 142, 119,
613 ];
614 let hex_digest = "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77";
615
616 let checksum = Blake2b512Checksum::calculate_from(data);
617 assert_eq!(digest, checksum.inner());
618 assert_eq!(format!("{}", &checksum), hex_digest,);
619
620 let checksum = Blake2b512Checksum::from_str(hex_digest).unwrap();
621 assert_eq!(digest, checksum.inner());
622 assert_eq!(format!("{}", &checksum), hex_digest,);
623 }
624
625 #[rstest]
626 fn checksum_sha1() {
627 let data = "foo\n";
628 let digest = vec![
629 241, 210, 210, 249, 36, 233, 134, 172, 134, 253, 247, 179, 108, 148, 188, 223, 50, 190,
630 236, 21,
631 ];
632 let hex_digest = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15";
633
634 let checksum = Sha1Checksum::calculate_from(data);
635 assert_eq!(digest, checksum.inner());
636 assert_eq!(format!("{}", &checksum), hex_digest,);
637
638 let checksum = Sha1Checksum::from_str(hex_digest).unwrap();
639 assert_eq!(digest, checksum.inner());
640 assert_eq!(format!("{}", &checksum), hex_digest,);
641 }
642
643 #[rstest]
644 fn checksum_sha224() {
645 let data = "foo\n";
646 let digest = vec![
647 231, 213, 227, 110, 141, 71, 12, 62, 81, 3, 254, 221, 46, 79, 42, 165, 195, 10, 178,
648 127, 102, 41, 189, 195, 40, 111, 157, 210,
649 ];
650 let hex_digest = "e7d5e36e8d470c3e5103fedd2e4f2aa5c30ab27f6629bdc3286f9dd2";
651
652 let checksum = Sha224Checksum::calculate_from(data);
653 assert_eq!(digest, checksum.inner());
654 assert_eq!(format!("{}", &checksum), hex_digest,);
655
656 let checksum = Sha224Checksum::from_str(hex_digest).unwrap();
657 assert_eq!(digest, checksum.inner());
658 assert_eq!(format!("{}", &checksum), hex_digest,);
659 }
660
661 #[rstest]
662 fn checksum_sha256() {
663 let data = "foo\n";
664 let digest = vec![
665 181, 187, 157, 128, 20, 160, 249, 177, 214, 30, 33, 231, 150, 215, 141, 204, 223, 19,
666 82, 242, 60, 211, 40, 18, 244, 133, 11, 135, 138, 228, 148, 76,
667 ];
668 let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
669
670 let checksum = Sha256Checksum::calculate_from(data);
671 assert_eq!(digest, checksum.inner());
672 assert_eq!(format!("{}", &checksum), hex_digest,);
673
674 let checksum = Sha256Checksum::from_str(hex_digest).unwrap();
675 assert_eq!(digest, checksum.inner());
676 assert_eq!(format!("{}", &checksum), hex_digest,);
677 }
678
679 #[rstest]
680 fn checksum_sha384() {
681 let data = "foo\n";
682 let digest = vec![
683 142, 255, 218, 191, 225, 68, 22, 33, 74, 37, 15, 147, 85, 5, 37, 11, 217, 145, 241, 6,
684 6, 93, 137, 157, 182, 225, 155, 220, 139, 246, 72, 243, 172, 15, 25, 53, 196, 246, 95,
685 232, 247, 152, 40, 155, 26, 13, 30, 6,
686 ];
687 let hex_digest = "8effdabfe14416214a250f935505250bd991f106065d899db6e19bdc8bf648f3ac0f1935c4f65fe8f798289b1a0d1e06";
688
689 let checksum = Sha384Checksum::calculate_from(data);
690 assert_eq!(digest, checksum.inner());
691 assert_eq!(format!("{}", &checksum), hex_digest,);
692
693 let checksum = Sha384Checksum::from_str(hex_digest).unwrap();
694 assert_eq!(digest, checksum.inner());
695 assert_eq!(format!("{}", &checksum), hex_digest,);
696 }
697
698 #[rstest]
699 fn checksum_sha512() {
700 let data = "foo\n";
701 let digest = vec![
702 12, 249, 24, 10, 118, 74, 186, 134, 58, 103, 182, 215, 47, 9, 24, 188, 19, 28, 103,
703 114, 100, 44, 178, 220, 229, 163, 79, 10, 112, 47, 148, 112, 221, 194, 191, 18, 92, 18,
704 25, 139, 25, 149, 194, 51, 195, 75, 74, 253, 52, 108, 84, 162, 51, 76, 53, 10, 148,
705 138, 81, 182, 232, 180, 230, 182,
706 ];
707 let hex_digest = "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6";
708
709 let checksum = Sha512Checksum::calculate_from(data);
710 assert_eq!(digest, checksum.inner());
711 assert_eq!(format!("{}", &checksum), hex_digest);
712
713 let checksum = Sha512Checksum::from_str(hex_digest).unwrap();
714 assert_eq!(digest, checksum.inner());
715 assert_eq!(format!("{}", &checksum), hex_digest);
716 }
717
718 #[rstest]
719 #[case::non_hex_digits(
720 "0cf9180a764aba863a67b6d72f0918bc13gggggg642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6",
721 "expected ASCII hex digit"
722 )]
723 #[case::incomplete_pair(" b ", "expected ASCII hex digit")]
724 #[case::incomplete_digest("0cf9180a764aba863a67b6d72f0918bca", "expected ASCII hex digit")]
725 #[case::whitespace(
726 "d2 02 d7 95 1d f2 c4 b7 11 ca 44 b4 bc c9 d7 b3 63 fa 42 52 12 7e 05 8c 1a 91 0e c0 5b 6c d0 38 d7 1c c2 12 21 c0 31 c0 35 9f 99 3e 74 6b 07 f5 96 5c f8 c5 c3 74 6a 58 33 7a d9 ab 65 27 8e 77",
727 "expected ASCII hex digit"
728 )]
729 fn checksum_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
730 let Err(Error::ParseError(err_msg)) = Sha512Checksum::from_str(input) else {
731 panic!("'{input}' did not fail to parse as expected")
732 };
733 assert!(
734 err_msg.contains(err_snippet),
735 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
736 );
737 }
738
739 #[rstest]
740 fn skippable_checksum_sha256() {
741 let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
742 let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
743 assert_eq!(format!("{}", &checksum), hex_digest);
744 }
745
746 #[rstest]
747 fn skippable_checksum_skip() {
748 let hex_digest = "SKIP";
749 let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
750
751 assert_eq!(SkippableChecksum::Skip, checksum);
752 assert_eq!(format!("{}", &checksum), hex_digest);
753 }
754}