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, 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        Ok(Self {
286            digest: terminated(
287                repeat(
288                    // consume exactly the number of hex pairs that our Digest type expects
289                    <D as Digest>::output_size(),
290                    hex_pair,
291                )
292                .context(StrContext::Label("hash digest")),
293                eof.context(StrContext::Expected(StrContextValue::Description(
294                    "end of checksum",
295                ))),
296            )
297            .parse_next(input)?,
298            _marker: PhantomData,
299        })
300    }
301}
302
303impl<D: Digest> FromStr for Checksum<D> {
304    type Err = Error;
305    /// Create a new Checksum from a hex string and return it in a Result
306    ///
307    /// The input is processed as a lowercase string.
308    /// An Error is returned, if the input length does not match the output size for the given
309    /// supported algorithm, or if the provided hex string could not be converted to a list of
310    /// bytes.
311    ///
312    /// Delegates to [`Checksum::parser`].
313    ///
314    /// ## Examples
315    /// ```
316    /// use std::str::FromStr;
317    /// use alpm_types::{digests::Blake2b512, Checksum};
318    ///
319    /// assert!(Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77").is_ok());
320    /// assert!(Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e7").is_err());
321    /// assert!(Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e7x").is_err());
322    /// ```
323    fn from_str(s: &str) -> Result<Checksum<D>, Self::Err> {
324        Ok(Checksum::parser.parse(s)?)
325    }
326}
327
328impl<D: Digest> Display for Checksum<D> {
329    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
330        write!(
331            fmt,
332            "{}",
333            self.digest
334                .iter()
335                .map(|x| format!("{x:02x?}"))
336                .collect::<Vec<String>>()
337                .join("")
338        )
339    }
340}
341
342/// Use [Display] as [Debug] impl, since the byte representation and [PhantomData] field aren't
343/// relevant for debugging purposes.
344impl<D: Digest> Debug for Checksum<D> {
345    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
346        Display::fmt(&self, f)
347    }
348}
349
350impl<D: Digest> PartialEq for Checksum<D> {
351    fn eq(&self, other: &Self) -> bool {
352        self.digest == other.digest
353    }
354}
355
356impl<D: Digest> Eq for Checksum<D> {}
357
358impl<D: Digest> Ord for Checksum<D> {
359    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
360        self.digest.cmp(&other.digest)
361    }
362}
363
364impl<D: Digest> PartialOrd for Checksum<D> {
365    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
366        Some(self.cmp(other))
367    }
368}
369
370/// A [`Checksum`] that may be skipped.
371///
372/// Strings representing checksums are used to verify the integrity of files.
373/// If the `"SKIP"` keyword is found, the integrity check is skipped.
374#[derive(Clone, Debug, Deserialize, Serialize)]
375#[serde(tag = "type")]
376pub enum SkippableChecksum<D: Digest + Clone> {
377    /// Sourcefile checksum validation may be skipped, which is expressed with this variant.
378    Skip,
379    /// The related source file should be validated via the provided checksum.
380    #[serde(bound = "D: Digest + Clone")]
381    Checksum {
382        /// The checksum to be used for the validation.
383        digest: Checksum<D>,
384    },
385}
386
387impl<D: Digest + Clone> SkippableChecksum<D> {
388    /// Determines whether the [`SkippableChecksum`] is skipped.
389    ///
390    /// Checksums are considered skipped if they are of the variant [`SkippableChecksum::Skip`].
391    pub fn is_skipped(&self) -> bool {
392        matches!(self, SkippableChecksum::Skip)
393    }
394
395    /// Recognizes a [`SkippableChecksum`] from a string slice.
396    ///
397    /// Consumes all its input.
398    /// See [`SkippableChecksum::from_str`], [`Checksum::parser`] and [`Checksum::from_str`].
399    ///
400    /// # Errors
401    ///
402    /// Returns an error if `input` is not the output of a _hash function_
403    /// in hexadecimal form.
404    pub fn parser(input: &mut &str) -> ModalResult<Self> {
405        terminated(
406            alt((
407                "SKIP".value(Self::Skip),
408                Checksum::parser.map(|digest| Self::Checksum { digest }),
409            )),
410            eof.context(StrContext::Expected(StrContextValue::Description(
411                "end of checksum",
412            ))),
413        )
414        .parse_next(input)
415    }
416}
417
418impl<D: Digest + Clone> FromStr for SkippableChecksum<D> {
419    type Err = Error;
420    /// Create a new [`SkippableChecksum`] from a string slice and return it in a Result.
421    ///
422    /// First checks for the special `SKIP` keyword, before trying [`Checksum::from_str`].
423    ///
424    /// Delegates to [`SkippableChecksum::parser`].
425    ///
426    /// ## Examples
427    /// ```
428    /// use std::str::FromStr;
429    ///
430    /// use alpm_types::{SkippableChecksum, digests::Sha256};
431    ///
432    /// assert!(SkippableChecksum::<Sha256>::from_str("SKIP").is_ok());
433    /// assert!(
434    ///     SkippableChecksum::<Sha256>::from_str(
435    ///         "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"
436    ///     )
437    ///     .is_ok()
438    /// );
439    /// ```
440    fn from_str(s: &str) -> Result<SkippableChecksum<D>, Self::Err> {
441        Ok(Self::parser.parse(s)?)
442    }
443}
444
445impl<D: Digest + Clone> Display for SkippableChecksum<D> {
446    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
447        let output = match self {
448            SkippableChecksum::Skip => "SKIP".to_string(),
449            SkippableChecksum::Checksum { digest } => digest.to_string(),
450        };
451        write!(fmt, "{output}",)
452    }
453}
454
455impl<D: Digest + Clone> PartialEq for SkippableChecksum<D> {
456    fn eq(&self, other: &Self) -> bool {
457        match (self, other) {
458            (SkippableChecksum::Skip, SkippableChecksum::Skip) => true,
459            (SkippableChecksum::Skip, SkippableChecksum::Checksum { .. }) => false,
460            (SkippableChecksum::Checksum { .. }, SkippableChecksum::Skip) => false,
461            (
462                SkippableChecksum::Checksum { digest },
463                SkippableChecksum::Checksum {
464                    digest: digest_other,
465                },
466            ) => digest == digest_other,
467        }
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use proptest::prelude::*;
474    use rstest::rstest;
475
476    use super::*;
477
478    proptest! {
479        #![proptest_config(ProptestConfig::with_cases(1000))]
480
481        #[test]
482        fn valid_checksum_blake2b512_from_string(string in r"[a-f0-9]{128}") {
483            prop_assert_eq!(&string, &format!("{}", Blake2b512Checksum::from_str(&string).unwrap()));
484        }
485
486        #[test]
487        fn invalid_checksum_blake2b512_bigger_size(string in r"[a-f0-9]{129}") {
488            assert!(Blake2b512Checksum::from_str(&string).is_err());
489        }
490
491        #[test]
492        fn invalid_checksum_blake2b512_smaller_size(string in r"[a-f0-9]{127}") {
493            assert!(Blake2b512Checksum::from_str(&string).is_err());
494        }
495
496        #[test]
497        fn invalid_checksum_blake2b512_wrong_chars(string in r"[e-z0-9]{128}") {
498            assert!(Blake2b512Checksum::from_str(&string).is_err());
499        }
500
501        #[test]
502        fn valid_checksum_sha1_from_string(string in r"[a-f0-9]{40}") {
503            prop_assert_eq!(&string, &format!("{}", Sha1Checksum::from_str(&string).unwrap()));
504        }
505
506        #[test]
507        fn invalid_checksum_sha1_from_string_bigger_size(string in r"[a-f0-9]{41}") {
508            assert!(Sha1Checksum::from_str(&string).is_err());
509        }
510
511        #[test]
512        fn invalid_checksum_sha1_from_string_smaller_size(string in r"[a-f0-9]{39}") {
513            assert!(Sha1Checksum::from_str(&string).is_err());
514        }
515
516        #[test]
517        fn invalid_checksum_sha1_from_string_wrong_chars(string in r"[e-z0-9]{40}") {
518            assert!(Sha1Checksum::from_str(&string).is_err());
519        }
520
521        #[test]
522        fn valid_checksum_sha224_from_string(string in r"[a-f0-9]{56}") {
523            prop_assert_eq!(&string, &format!("{}", Sha224Checksum::from_str(&string).unwrap()));
524        }
525
526        #[test]
527        fn invalid_checksum_sha224_from_string_bigger_size(string in r"[a-f0-9]{57}") {
528            assert!(Sha224Checksum::from_str(&string).is_err());
529        }
530
531        #[test]
532        fn invalid_checksum_sha224_from_string_smaller_size(string in r"[a-f0-9]{55}") {
533            assert!(Sha224Checksum::from_str(&string).is_err());
534        }
535
536        #[test]
537        fn invalid_checksum_sha224_from_string_wrong_chars(string in r"[e-z0-9]{56}") {
538            assert!(Sha224Checksum::from_str(&string).is_err());
539        }
540
541        #[test]
542        fn valid_checksum_sha256_from_string(string in r"[a-f0-9]{64}") {
543            prop_assert_eq!(&string, &format!("{}", Sha256Checksum::from_str(&string).unwrap()));
544        }
545
546        #[test]
547        fn invalid_checksum_sha256_from_string_bigger_size(string in r"[a-f0-9]{65}") {
548            assert!(Sha256Checksum::from_str(&string).is_err());
549        }
550
551        #[test]
552        fn invalid_checksum_sha256_from_string_smaller_size(string in r"[a-f0-9]{63}") {
553            assert!(Sha256Checksum::from_str(&string).is_err());
554        }
555
556        #[test]
557        fn invalid_checksum_sha256_from_string_wrong_chars(string in r"[e-z0-9]{64}") {
558            assert!(Sha256Checksum::from_str(&string).is_err());
559        }
560
561        #[test]
562        fn valid_checksum_sha384_from_string(string in r"[a-f0-9]{96}") {
563            prop_assert_eq!(&string, &format!("{}", Sha384Checksum::from_str(&string).unwrap()));
564        }
565
566        #[test]
567        fn invalid_checksum_sha384_from_string_bigger_size(string in r"[a-f0-9]{97}") {
568            assert!(Sha384Checksum::from_str(&string).is_err());
569        }
570
571        #[test]
572        fn invalid_checksum_sha384_from_string_smaller_size(string in r"[a-f0-9]{95}") {
573            assert!(Sha384Checksum::from_str(&string).is_err());
574        }
575
576        #[test]
577        fn invalid_checksum_sha384_from_string_wrong_chars(string in r"[e-z0-9]{96}") {
578            assert!(Sha384Checksum::from_str(&string).is_err());
579        }
580
581        #[test]
582        fn valid_checksum_sha512_from_string(string in r"[a-f0-9]{128}") {
583            prop_assert_eq!(&string, &format!("{}", Sha512Checksum::from_str(&string).unwrap()));
584        }
585
586        #[test]
587        fn invalid_checksum_sha512_from_string_bigger_size(string in r"[a-f0-9]{129}") {
588            assert!(Sha512Checksum::from_str(&string).is_err());
589        }
590
591        #[test]
592        fn invalid_checksum_sha512_from_string_smaller_size(string in r"[a-f0-9]{127}") {
593            assert!(Sha512Checksum::from_str(&string).is_err());
594        }
595
596        #[test]
597        fn invalid_checksum_sha512_from_string_wrong_chars(string in r"[e-z0-9]{128}") {
598            assert!(Sha512Checksum::from_str(&string).is_err());
599        }
600    }
601
602    #[rstest]
603    fn checksum_blake2b512() {
604        let data = "foo\n";
605        let digest = vec![
606            210, 2, 215, 149, 29, 242, 196, 183, 17, 202, 68, 180, 188, 201, 215, 179, 99, 250, 66,
607            82, 18, 126, 5, 140, 26, 145, 14, 192, 91, 108, 208, 56, 215, 28, 194, 18, 33, 192, 49,
608            192, 53, 159, 153, 62, 116, 107, 7, 245, 150, 92, 248, 197, 195, 116, 106, 88, 51, 122,
609            217, 171, 101, 39, 142, 119,
610        ];
611        let hex_digest = "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77";
612
613        let checksum = Blake2b512Checksum::calculate_from(data);
614        assert_eq!(digest, checksum.inner());
615        assert_eq!(format!("{}", &checksum), hex_digest,);
616
617        let checksum = Blake2b512Checksum::from_str(hex_digest).unwrap();
618        assert_eq!(digest, checksum.inner());
619        assert_eq!(format!("{}", &checksum), hex_digest,);
620    }
621
622    #[rstest]
623    fn checksum_sha1() {
624        let data = "foo\n";
625        let digest = vec![
626            241, 210, 210, 249, 36, 233, 134, 172, 134, 253, 247, 179, 108, 148, 188, 223, 50, 190,
627            236, 21,
628        ];
629        let hex_digest = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15";
630
631        let checksum = Sha1Checksum::calculate_from(data);
632        assert_eq!(digest, checksum.inner());
633        assert_eq!(format!("{}", &checksum), hex_digest,);
634
635        let checksum = Sha1Checksum::from_str(hex_digest).unwrap();
636        assert_eq!(digest, checksum.inner());
637        assert_eq!(format!("{}", &checksum), hex_digest,);
638    }
639
640    #[rstest]
641    fn checksum_sha224() {
642        let data = "foo\n";
643        let digest = vec![
644            231, 213, 227, 110, 141, 71, 12, 62, 81, 3, 254, 221, 46, 79, 42, 165, 195, 10, 178,
645            127, 102, 41, 189, 195, 40, 111, 157, 210,
646        ];
647        let hex_digest = "e7d5e36e8d470c3e5103fedd2e4f2aa5c30ab27f6629bdc3286f9dd2";
648
649        let checksum = Sha224Checksum::calculate_from(data);
650        assert_eq!(digest, checksum.inner());
651        assert_eq!(format!("{}", &checksum), hex_digest,);
652
653        let checksum = Sha224Checksum::from_str(hex_digest).unwrap();
654        assert_eq!(digest, checksum.inner());
655        assert_eq!(format!("{}", &checksum), hex_digest,);
656    }
657
658    #[rstest]
659    fn checksum_sha256() {
660        let data = "foo\n";
661        let digest = vec![
662            181, 187, 157, 128, 20, 160, 249, 177, 214, 30, 33, 231, 150, 215, 141, 204, 223, 19,
663            82, 242, 60, 211, 40, 18, 244, 133, 11, 135, 138, 228, 148, 76,
664        ];
665        let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
666
667        let checksum = Sha256Checksum::calculate_from(data);
668        assert_eq!(digest, checksum.inner());
669        assert_eq!(format!("{}", &checksum), hex_digest,);
670
671        let checksum = Sha256Checksum::from_str(hex_digest).unwrap();
672        assert_eq!(digest, checksum.inner());
673        assert_eq!(format!("{}", &checksum), hex_digest,);
674    }
675
676    #[rstest]
677    fn checksum_sha384() {
678        let data = "foo\n";
679        let digest = vec![
680            142, 255, 218, 191, 225, 68, 22, 33, 74, 37, 15, 147, 85, 5, 37, 11, 217, 145, 241, 6,
681            6, 93, 137, 157, 182, 225, 155, 220, 139, 246, 72, 243, 172, 15, 25, 53, 196, 246, 95,
682            232, 247, 152, 40, 155, 26, 13, 30, 6,
683        ];
684        let hex_digest = "8effdabfe14416214a250f935505250bd991f106065d899db6e19bdc8bf648f3ac0f1935c4f65fe8f798289b1a0d1e06";
685
686        let checksum = Sha384Checksum::calculate_from(data);
687        assert_eq!(digest, checksum.inner());
688        assert_eq!(format!("{}", &checksum), hex_digest,);
689
690        let checksum = Sha384Checksum::from_str(hex_digest).unwrap();
691        assert_eq!(digest, checksum.inner());
692        assert_eq!(format!("{}", &checksum), hex_digest,);
693    }
694
695    #[rstest]
696    fn checksum_sha512() {
697        let data = "foo\n";
698        let digest = vec![
699            12, 249, 24, 10, 118, 74, 186, 134, 58, 103, 182, 215, 47, 9, 24, 188, 19, 28, 103,
700            114, 100, 44, 178, 220, 229, 163, 79, 10, 112, 47, 148, 112, 221, 194, 191, 18, 92, 18,
701            25, 139, 25, 149, 194, 51, 195, 75, 74, 253, 52, 108, 84, 162, 51, 76, 53, 10, 148,
702            138, 81, 182, 232, 180, 230, 182,
703        ];
704        let hex_digest = "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6";
705
706        let checksum = Sha512Checksum::calculate_from(data);
707        assert_eq!(digest, checksum.inner());
708        assert_eq!(format!("{}", &checksum), hex_digest);
709
710        let checksum = Sha512Checksum::from_str(hex_digest).unwrap();
711        assert_eq!(digest, checksum.inner());
712        assert_eq!(format!("{}", &checksum), hex_digest);
713    }
714
715    #[rstest]
716    #[case::non_hex_digits(
717        "0cf9180a764aba863a67b6d72f0918bc13gggggg642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6",
718        "expected ASCII hex digit"
719    )]
720    #[case::incomplete_pair(" b ", "expected ASCII hex digit")]
721    #[case::incomplete_digest("0cf9180a764aba863a67b6d72f0918bca", "expected ASCII hex digit")]
722    #[case::whitespace(
723        "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",
724        "expected ASCII hex digit"
725    )]
726    fn checksum_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
727        let Err(Error::ParseError(err_msg)) = Sha512Checksum::from_str(input) else {
728            panic!("'{input}' did not fail to parse as expected")
729        };
730        assert!(
731            err_msg.contains(err_snippet),
732            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
733        );
734    }
735
736    #[rstest]
737    fn skippable_checksum_sha256() {
738        let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
739        let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
740        assert_eq!(format!("{}", &checksum), hex_digest);
741    }
742
743    #[rstest]
744    fn skippable_checksum_skip() {
745        let hex_digest = "SKIP";
746        let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
747
748        assert_eq!(SkippableChecksum::Skip, checksum);
749        assert_eq!(format!("{}", &checksum), hex_digest);
750    }
751}