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}