alpm_types/
checksum.rs

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
23// Convenience type aliases for the supported checksums
24
25/// A checksum using the Blake2b512 algorithm
26pub type Blake2b512Checksum = Checksum<Blake2b512>;
27
28/// A checksum using the Md5 algorithm
29pub type Md5Checksum = Checksum<Md5>;
30
31/// A checksum using the Sha1 algorithm
32pub type Sha1Checksum = Checksum<Sha1>;
33
34/// A checksum using the Sha224 algorithm
35pub type Sha224Checksum = Checksum<Sha224>;
36
37/// A checksum using the Sha256 algorithm
38pub type Sha256Checksum = Checksum<Sha256>;
39
40/// A checksum using the Sha384 algorithm
41pub type Sha384Checksum = Checksum<Sha384>;
42
43/// A checksum using the Sha512 algorithm
44pub type Sha512Checksum = Checksum<Sha512>;
45
46/// This enum represents all accepted checksum algorithms used in the Arch Linux distribution.
47#[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    /// Blake2b-512 cryptographic hash algorithm
65    Blake2b512,
66    /// Md5 hash algorithm (deprecated)
67    Md5,
68    /// Sha1 hash algorithm (deprecated)
69    Sha1,
70    /// Sha224 hash algorithm
71    Sha224,
72    /// Sha256 hash algorithm
73    Sha256,
74    /// Sha384 hash algorithm
75    Sha384,
76    /// Sha512 hash algorithm
77    Sha512,
78}
79
80impl ChecksumAlgorithm {
81    /// Determines if a checksum algorithm is considered deprecated for security reasons.
82    ///
83    /// Returns `true` for cryptographically unsafe algorithms that should be avoided.
84    /// These algorithms are still supported for backwards compatibility but their use is strongly
85    /// discouraged.
86    ///
87    /// Currently deprecated algorithms:
88    ///
89    /// - [`ChecksumAlgorithm::Md5`]: Vulnerable to collision attacks
90    /// - [`ChecksumAlgorithm::Sha1`]: Vulnerable to collision attacks
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use alpm_types::ChecksumAlgorithm;
96    ///
97    /// // Deprecated algorithms
98    /// assert!(ChecksumAlgorithm::Md5.is_deprecated());
99    /// assert!(ChecksumAlgorithm::Sha1.is_deprecated());
100    ///
101    /// // Safe algorithms
102    /// assert!(!ChecksumAlgorithm::Sha256.is_deprecated());
103    /// assert!(!ChecksumAlgorithm::Blake2b512.is_deprecated());
104    /// ```
105    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    /// Returns a list of [`ChecksumAlgorithm`] variants that are not considered deprecated.
117    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/// A [checksum] using a supported algorithm
127///
128/// Checksums are created using one of the supported algorithms:
129///
130/// - `Blake2b512`
131/// - `Md5` (**WARNING**: Use of this algorithm is highly discouraged, because it is
132///   cryptographically unsafe)
133/// - `Sha1` (**WARNING**: Use of this algorithm is highly discouraged, because it is
134///   cryptographically unsafe)
135/// - `Sha224`
136/// - `Sha256`
137/// - `Sha384`
138/// - `Sha512`
139///
140/// Contrary to makepkg/pacman, this crate *does not* support using cksum-style CRC-32 as it
141/// is non-standard (different implementations throughout libraries) and cryptographically unsafe.
142///
143/// ## Note
144///
145/// There are two ways to use a checksum:
146///
147/// 1. Generically over a digest (e.g. `Checksum::<Blake2b512>`)
148/// 2. Using the convenience type aliases (e.g. `Blake2b512Checksum`)
149///
150/// ## Examples
151///
152/// ```
153/// use std::str::FromStr;
154/// use alpm_types::{digests::Blake2b512, Checksum};
155///
156/// # fn main() -> Result<(), alpm_types::Error> {
157/// let checksum = Checksum::<Blake2b512>::calculate_from("foo\n");
158/// let digest = vec![
159///     210, 2, 215, 149, 29, 242, 196, 183, 17, 202, 68, 180, 188, 201, 215, 179, 99, 250, 66,
160///     82, 18, 126, 5, 140, 26, 145, 14, 192, 91, 108, 208, 56, 215, 28, 194, 18, 33, 192, 49,
161///     192, 53, 159, 153, 62, 116, 107, 7, 245, 150, 92, 248, 197, 195, 116, 106, 88, 51, 122,
162///     217, 171, 101, 39, 142, 119,
163/// ];
164/// assert_eq!(checksum.inner(), digest);
165/// assert_eq!(
166///     format!("{}", checksum),
167///     "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77",
168/// );
169///
170/// // create checksum from hex string
171/// let checksum = Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77")?;
172/// assert_eq!(checksum.inner(), digest);
173/// # Ok(())
174/// # }
175/// ```
176///
177/// # Developer Note
178///
179/// In case you want to wrap this type and make the parent `Serialize`able, please note the
180/// following:
181///
182/// Serde automatically adds a `Serialize` trait bound on top of it trait bounds in wrapper
183/// types. **However**, that's not needed as we use `D` simply as a phantom marker that
184/// isn't serialized in the first place.
185/// To fix this in your wrapper type, make use of the [bound container attribute], e.g.:
186///
187/// [checksum]: https://en.wikipedia.org/wiki/Checksum
188/// ```
189/// use alpm_types::{Checksum, digests::Digest};
190/// use serde::Serialize;
191///
192/// #[derive(Serialize)]
193/// struct Wrapper<D: Digest> {
194///     #[serde(bound = "D: Digest")]
195///     checksum: Checksum<D>,
196/// }
197/// ```
198#[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    /// Serialize a [`Checksum`] into a hex `String` representation.
206    ///
207    /// We chose hex as byte vectors are imperformant and considered bad practice for non-binary
208    /// formats like `JSON` or `YAML`
209    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    /// Calculate a new Checksum for data that may be represented as a list of bytes
229    ///
230    /// ## Examples
231    /// ```
232    /// use alpm_types::{digests::Blake2b512, Checksum};
233    ///
234    /// assert_eq!(
235    ///     format!("{}", Checksum::<Blake2b512>::calculate_from("foo\n")),
236    ///     "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77",
237    /// );
238    /// ```
239    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    /// Return a reference to the inner type
250    pub fn inner(&self) -> &[u8] {
251        &self.digest
252    }
253
254    /// Recognizes an ASCII hexadecimal [`Checksum`] from a string slice.
255    ///
256    /// Consumes all input.
257    /// See [`Checksum::from_str`].
258    ///
259    /// # Errors
260    ///
261    /// Returns an error if `input` is not the output of a _hash function_
262    /// in hexadecimal form.
263    pub fn parser(input: &mut &str) -> ModalResult<Self> {
264        /// Consume 1 hex digit and return its hex value.
265        ///
266        /// Accepts uppercase or lowercase.
267        #[inline]
268        fn hex_digit(input: &mut &str) -> ModalResult<u8> {
269            one_of(('0'..='9', 'a'..='f', 'A'..='F'))
270                .map(|d: char|
271                    // unwraps are unreachable: their invariants are always
272                    // upheld because the above character set can never
273                    // consume anything but a single valid hex digit
274                    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            // shift is infallible because hex_digit cannot return >0b00001111
283            (first << 4) + second);
284
285        // Consume exactly the number of hex pairs that our Digest type expects
286        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    /// Create a new Checksum from a hex string and return it in a Result
309    ///
310    /// The input is processed as a lowercase string.
311    /// An Error is returned, if the input length does not match the output size for the given
312    /// supported algorithm, or if the provided hex string could not be converted to a list of
313    /// bytes.
314    ///
315    /// Delegates to [`Checksum::parser`].
316    ///
317    /// ## Examples
318    /// ```
319    /// use std::str::FromStr;
320    /// use alpm_types::{digests::Blake2b512, Checksum};
321    ///
322    /// assert!(Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77").is_ok());
323    /// assert!(Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e7").is_err());
324    /// assert!(Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e7x").is_err());
325    /// ```
326    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
345/// Use [Display] as [Debug] impl, since the byte representation and [PhantomData] field aren't
346/// relevant for debugging purposes.
347impl<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/// A [`Checksum`] that may be skipped.
374///
375/// Strings representing checksums are used to verify the integrity of files.
376/// If the `"SKIP"` keyword is found, the integrity check is skipped.
377#[derive(Clone, Debug, Deserialize, Serialize)]
378#[serde(tag = "type")]
379pub enum SkippableChecksum<D: Digest + Clone> {
380    /// Sourcefile checksum validation may be skipped, which is expressed with this variant.
381    Skip,
382    /// The related source file should be validated via the provided checksum.
383    #[serde(bound = "D: Digest + Clone")]
384    Checksum {
385        /// The checksum to be used for the validation.
386        digest: Checksum<D>,
387    },
388}
389
390impl<D: Digest + Clone> SkippableChecksum<D> {
391    /// Determines whether the [`SkippableChecksum`] is skipped.
392    ///
393    /// Checksums are considered skipped if they are of the variant [`SkippableChecksum::Skip`].
394    pub fn is_skipped(&self) -> bool {
395        matches!(self, SkippableChecksum::Skip)
396    }
397
398    /// Recognizes a [`SkippableChecksum`] from a string slice.
399    ///
400    /// Consumes all its input.
401    /// See [`SkippableChecksum::from_str`], [`Checksum::parser`] and [`Checksum::from_str`].
402    ///
403    /// # Errors
404    ///
405    /// Returns an error if `input` is not the output of a _hash function_
406    /// in hexadecimal form.
407    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    /// Create a new [`SkippableChecksum`] from a string slice and return it in a Result.
424    ///
425    /// First checks for the special `SKIP` keyword, before trying [`Checksum::from_str`].
426    ///
427    /// Delegates to [`SkippableChecksum::parser`].
428    ///
429    /// ## Examples
430    /// ```
431    /// use std::str::FromStr;
432    ///
433    /// use alpm_types::{SkippableChecksum, digests::Sha256};
434    ///
435    /// assert!(SkippableChecksum::<Sha256>::from_str("SKIP").is_ok());
436    /// assert!(
437    ///     SkippableChecksum::<Sha256>::from_str(
438    ///         "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"
439    ///     )
440    ///     .is_ok()
441    /// );
442    /// ```
443    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}