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 winnow::{
10    ModalResult,
11    Parser,
12    combinator::{alt, eof, repeat, terminated},
13    error::{StrContext, StrContextValue},
14    token::one_of,
15};
16
17use crate::{
18    Error,
19    digests::{Blake2b512, Md5, Sha1, Sha224, Sha256, Sha384, Sha512},
20};
21
22// Convenience type aliases for the supported checksums
23
24/// A checksum using the Blake2b512 algorithm
25pub type Blake2b512Checksum = Checksum<Blake2b512>;
26
27/// A checksum using the Md5 algorithm
28pub type Md5Checksum = Checksum<Md5>;
29
30/// A checksum using the Sha1 algorithm
31pub type Sha1Checksum = Checksum<Sha1>;
32
33/// A checksum using the Sha224 algorithm
34pub type Sha224Checksum = Checksum<Sha224>;
35
36/// A checksum using the Sha256 algorithm
37pub type Sha256Checksum = Checksum<Sha256>;
38
39/// A checksum using the Sha384 algorithm
40pub type Sha384Checksum = Checksum<Sha384>;
41
42/// A checksum using the Sha512 algorithm
43pub type Sha512Checksum = Checksum<Sha512>;
44
45/// A [checksum] using a supported algorithm
46///
47/// Checksums are created using one of the supported algorithms:
48///
49/// - `Blake2b512`
50/// - `Md5` (**WARNING**: Use of this algorithm is highly discouraged, because it is
51///   cryptographically unsafe)
52/// - `Sha1` (**WARNING**: Use of this algorithm is highly discouraged, because it is
53///   cryptographically unsafe)
54/// - `Sha224`
55/// - `Sha256`
56/// - `Sha384`
57/// - `Sha512`
58///
59/// Contrary to makepkg/pacman, this crate *does not* support using cksum-style CRC-32 as it
60/// is non-standard (different implementations throughout libraries) and cryptographically unsafe.
61///
62/// ## Note
63///
64/// There are two ways to use a checksum:
65///
66/// 1. Generically over a digest (e.g. `Checksum::<Blake2b512>`)
67/// 2. Using the convenience type aliases (e.g. `Blake2b512Checksum`)
68///
69/// ## Examples
70///
71/// ```
72/// use std::str::FromStr;
73/// use alpm_types::{digests::Blake2b512, Checksum};
74///
75/// # fn main() -> Result<(), alpm_types::Error> {
76/// let checksum = Checksum::<Blake2b512>::calculate_from("foo\n");
77/// let digest = vec![
78///     210, 2, 215, 149, 29, 242, 196, 183, 17, 202, 68, 180, 188, 201, 215, 179, 99, 250, 66,
79///     82, 18, 126, 5, 140, 26, 145, 14, 192, 91, 108, 208, 56, 215, 28, 194, 18, 33, 192, 49,
80///     192, 53, 159, 153, 62, 116, 107, 7, 245, 150, 92, 248, 197, 195, 116, 106, 88, 51, 122,
81///     217, 171, 101, 39, 142, 119,
82/// ];
83/// assert_eq!(checksum.inner(), digest);
84/// assert_eq!(
85///     format!("{}", checksum),
86///     "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77",
87/// );
88///
89/// // create checksum from hex string
90/// let checksum = Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77")?;
91/// assert_eq!(checksum.inner(), digest);
92/// # Ok(())
93/// # }
94/// ```
95///
96/// # Developer Note
97///
98/// In case you want to wrap this type and make the parent `Serialize`able, please note the
99/// following:
100///
101/// Serde automatically adds a `Serialize` trait bound on top of it trait bounds in wrapper
102/// types. **However**, that's not needed as we use `D` simply as a phantom marker that
103/// isn't serialized in the first place.
104/// To fix this in your wrapper type, make use of the [bound container attribute], e.g.:
105///
106/// [checksum]: https://en.wikipedia.org/wiki/Checksum
107/// ```
108/// use alpm_types::{Checksum, digests::Digest};
109/// use serde::Serialize;
110///
111/// #[derive(Serialize)]
112/// struct Wrapper<D: Digest> {
113///     #[serde(bound = "D: Digest")]
114///     checksum: Checksum<D>,
115/// }
116/// ```
117#[derive(Clone)]
118pub struct Checksum<D: Digest> {
119    digest: Vec<u8>,
120    _marker: PhantomData<D>,
121}
122
123impl<D: Digest> Serialize for Checksum<D> {
124    /// Serialize a [`Checksum`] into a hex `String` representation.
125    ///
126    /// We chose hex as byte vectors are imperformant and considered bad practice for non-binary
127    /// formats like `JSON` or `YAML`
128    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
129    where
130        S: Serializer,
131    {
132        serializer.serialize_str(&self.to_string())
133    }
134}
135
136impl<'de, D: Digest> Deserialize<'de> for Checksum<D> {
137    fn deserialize<De>(deserializer: De) -> Result<Self, De::Error>
138    where
139        De: Deserializer<'de>,
140    {
141        let s = String::deserialize(deserializer)?;
142        Checksum::from_str(&s).map_err(serde::de::Error::custom)
143    }
144}
145
146impl<D: Digest> Checksum<D> {
147    /// Calculate a new Checksum for data that may be represented as a list of bytes
148    ///
149    /// ## Examples
150    /// ```
151    /// use alpm_types::{digests::Blake2b512, Checksum};
152    ///
153    /// assert_eq!(
154    ///     format!("{}", Checksum::<Blake2b512>::calculate_from("foo\n")),
155    ///     "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77",
156    /// );
157    /// ```
158    pub fn calculate_from(input: impl AsRef<[u8]>) -> Self {
159        let mut hasher = D::new();
160        hasher.update(input);
161
162        Checksum {
163            digest: hasher.finalize()[..].to_vec(),
164            _marker: PhantomData,
165        }
166    }
167
168    /// Return a reference to the inner type
169    pub fn inner(&self) -> &[u8] {
170        &self.digest
171    }
172
173    /// Recognizes an ASCII hexadecimal [`Checksum`] from a string slice.
174    ///
175    /// Consumes all input.
176    /// See [`Checksum::from_str`].
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if `input` is not the output of a _hash function_
181    /// in hexadecimal form.
182    pub fn parser(input: &mut &str) -> ModalResult<Self> {
183        /// Consume 1 hex digit and return its hex value.
184        ///
185        /// Accepts uppercase or lowercase.
186        #[inline]
187        fn hex_digit(input: &mut &str) -> ModalResult<u8> {
188            one_of(('0'..='9', 'a'..='f', 'A'..='F'))
189                .map(|d: char|
190                    // unwraps are unreachable: their invariants are always
191                    // upheld because the above character set can never
192                    // consume anything but a single valid hex digit
193                    d.to_digit(16).unwrap().try_into().unwrap())
194                .context(StrContext::Expected(StrContextValue::Description(
195                    "ASCII hex digit",
196                )))
197                .parse_next(input)
198        }
199
200        let hex_pair = (hex_digit, hex_digit).map(|(first, second)|
201            // shift is infallible because hex_digit cannot return >0b00001111
202            (first << 4) + second);
203
204        Ok(Self {
205            digest: terminated(
206                repeat(
207                    // consume exactly the number of hex pairs that our Digest type expects
208                    <D as Digest>::output_size(),
209                    hex_pair,
210                )
211                .context(StrContext::Label("hash digest")),
212                eof.context(StrContext::Expected(StrContextValue::Description(
213                    "end of checksum",
214                ))),
215            )
216            .parse_next(input)?,
217            _marker: PhantomData,
218        })
219    }
220}
221
222impl<D: Digest> FromStr for Checksum<D> {
223    type Err = Error;
224    /// Create a new Checksum from a hex string and return it in a Result
225    ///
226    /// The input is processed as a lowercase string.
227    /// An Error is returned, if the input length does not match the output size for the given
228    /// supported algorithm, or if the provided hex string could not be converted to a list of
229    /// bytes.
230    ///
231    /// Delegates to [`Checksum::parser`].
232    ///
233    /// ## Examples
234    /// ```
235    /// use std::str::FromStr;
236    /// use alpm_types::{digests::Blake2b512, Checksum};
237    ///
238    /// assert!(Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77").is_ok());
239    /// assert!(Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e7").is_err());
240    /// assert!(Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e7x").is_err());
241    /// ```
242    fn from_str(s: &str) -> Result<Checksum<D>, Self::Err> {
243        Ok(Checksum::parser.parse(s)?)
244    }
245}
246
247impl<D: Digest> Display for Checksum<D> {
248    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
249        write!(
250            fmt,
251            "{}",
252            self.digest
253                .iter()
254                .map(|x| format!("{:02x?}", x))
255                .collect::<Vec<String>>()
256                .join("")
257        )
258    }
259}
260
261/// Use [Display] as [Debug] impl, since the byte representation and [PhantomData] field aren't
262/// relevant for debugging purposes.
263impl<D: Digest> Debug for Checksum<D> {
264    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
265        Display::fmt(&self, f)
266    }
267}
268
269impl<D: Digest> PartialEq for Checksum<D> {
270    fn eq(&self, other: &Self) -> bool {
271        self.digest == other.digest
272    }
273}
274
275impl<D: Digest> Eq for Checksum<D> {}
276
277impl<D: Digest> Ord for Checksum<D> {
278    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
279        self.digest.cmp(&other.digest)
280    }
281}
282
283impl<D: Digest> PartialOrd for Checksum<D> {
284    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
285        Some(self.cmp(other))
286    }
287}
288
289/// A [`Checksum`] that may be skipped.
290///
291/// Strings representing checksums are used to verify the integrity of files.
292/// If the `"SKIP"` keyword is found, the integrity check is skipped.
293#[derive(Debug, Clone, Deserialize, Serialize)]
294#[serde(tag = "type")]
295pub enum SkippableChecksum<D: Digest + Clone> {
296    /// Sourcefile checksum validation may be skipped, which is expressed with this variant.
297    Skip,
298    /// The related source file should be validated via the provided checksum.
299    #[serde(bound = "D: Digest + Clone")]
300    Checksum {
301        /// The checksum to be used for the validation.
302        digest: Checksum<D>,
303    },
304}
305
306impl<D: Digest + Clone> SkippableChecksum<D> {
307    /// Recognizes a [`SkippableChecksum`] from a string slice.
308    ///
309    /// Consumes all its input.
310    /// See [`SkippableChecksum::from_str`], [`Checksum::parser`] and [`Checksum::from_str`].
311    ///
312    /// # Errors
313    ///
314    /// Returns an error if `input` is not the output of a _hash function_
315    /// in hexadecimal form.
316    pub fn parser(input: &mut &str) -> ModalResult<Self> {
317        terminated(
318            alt((
319                "SKIP".value(Self::Skip),
320                Checksum::parser.map(|digest| Self::Checksum { digest }),
321            )),
322            eof.context(StrContext::Expected(StrContextValue::Description(
323                "end of checksum",
324            ))),
325        )
326        .parse_next(input)
327    }
328}
329
330impl<D: Digest + Clone> FromStr for SkippableChecksum<D> {
331    type Err = Error;
332    /// Create a new [`SkippableChecksum`] from a string slice and return it in a Result.
333    ///
334    /// First checks for the special `SKIP` keyword, before trying [`Checksum::from_str`].
335    ///
336    /// Delegates to [`SkippableChecksum::parser`].
337    ///
338    /// ## Examples
339    /// ```
340    /// use std::str::FromStr;
341    ///
342    /// use alpm_types::{SkippableChecksum, digests::Sha256};
343    ///
344    /// assert!(SkippableChecksum::<Sha256>::from_str("SKIP").is_ok());
345    /// assert!(
346    ///     SkippableChecksum::<Sha256>::from_str(
347    ///         "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"
348    ///     )
349    ///     .is_ok()
350    /// );
351    /// ```
352    fn from_str(s: &str) -> Result<SkippableChecksum<D>, Self::Err> {
353        Ok(Self::parser.parse(s)?)
354    }
355}
356
357impl<D: Digest + Clone> Display for SkippableChecksum<D> {
358    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
359        let output = match self {
360            SkippableChecksum::Skip => "SKIP".to_string(),
361            SkippableChecksum::Checksum { digest } => digest.to_string(),
362        };
363        write!(fmt, "{output}",)
364    }
365}
366
367impl<D: Digest + Clone> PartialEq for SkippableChecksum<D> {
368    fn eq(&self, other: &Self) -> bool {
369        match (self, other) {
370            (SkippableChecksum::Skip, SkippableChecksum::Skip) => true,
371            (SkippableChecksum::Skip, SkippableChecksum::Checksum { .. }) => false,
372            (SkippableChecksum::Checksum { .. }, SkippableChecksum::Skip) => false,
373            (
374                SkippableChecksum::Checksum { digest },
375                SkippableChecksum::Checksum {
376                    digest: digest_other,
377                },
378            ) => digest == digest_other,
379        }
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use proptest::prelude::*;
386    use rstest::rstest;
387
388    use super::*;
389
390    proptest! {
391        #![proptest_config(ProptestConfig::with_cases(1000))]
392
393        #[test]
394        fn valid_checksum_blake2b512_from_string(string in r"[a-f0-9]{128}") {
395            prop_assert_eq!(&string, &format!("{}", Blake2b512Checksum::from_str(&string).unwrap()));
396        }
397
398        #[test]
399        fn invalid_checksum_blake2b512_bigger_size(string in r"[a-f0-9]{129}") {
400            assert!(Blake2b512Checksum::from_str(&string).is_err());
401        }
402
403        #[test]
404        fn invalid_checksum_blake2b512_smaller_size(string in r"[a-f0-9]{127}") {
405            assert!(Blake2b512Checksum::from_str(&string).is_err());
406        }
407
408        #[test]
409        fn invalid_checksum_blake2b512_wrong_chars(string in r"[e-z0-9]{128}") {
410            assert!(Blake2b512Checksum::from_str(&string).is_err());
411        }
412
413        #[test]
414        fn valid_checksum_sha1_from_string(string in r"[a-f0-9]{40}") {
415            prop_assert_eq!(&string, &format!("{}", Sha1Checksum::from_str(&string).unwrap()));
416        }
417
418        #[test]
419        fn invalid_checksum_sha1_from_string_bigger_size(string in r"[a-f0-9]{41}") {
420            assert!(Sha1Checksum::from_str(&string).is_err());
421        }
422
423        #[test]
424        fn invalid_checksum_sha1_from_string_smaller_size(string in r"[a-f0-9]{39}") {
425            assert!(Sha1Checksum::from_str(&string).is_err());
426        }
427
428        #[test]
429        fn invalid_checksum_sha1_from_string_wrong_chars(string in r"[e-z0-9]{40}") {
430            assert!(Sha1Checksum::from_str(&string).is_err());
431        }
432
433        #[test]
434        fn valid_checksum_sha224_from_string(string in r"[a-f0-9]{56}") {
435            prop_assert_eq!(&string, &format!("{}", Sha224Checksum::from_str(&string).unwrap()));
436        }
437
438        #[test]
439        fn invalid_checksum_sha224_from_string_bigger_size(string in r"[a-f0-9]{57}") {
440            assert!(Sha224Checksum::from_str(&string).is_err());
441        }
442
443        #[test]
444        fn invalid_checksum_sha224_from_string_smaller_size(string in r"[a-f0-9]{55}") {
445            assert!(Sha224Checksum::from_str(&string).is_err());
446        }
447
448        #[test]
449        fn invalid_checksum_sha224_from_string_wrong_chars(string in r"[e-z0-9]{56}") {
450            assert!(Sha224Checksum::from_str(&string).is_err());
451        }
452
453        #[test]
454        fn valid_checksum_sha256_from_string(string in r"[a-f0-9]{64}") {
455            prop_assert_eq!(&string, &format!("{}", Sha256Checksum::from_str(&string).unwrap()));
456        }
457
458        #[test]
459        fn invalid_checksum_sha256_from_string_bigger_size(string in r"[a-f0-9]{65}") {
460            assert!(Sha256Checksum::from_str(&string).is_err());
461        }
462
463        #[test]
464        fn invalid_checksum_sha256_from_string_smaller_size(string in r"[a-f0-9]{63}") {
465            assert!(Sha256Checksum::from_str(&string).is_err());
466        }
467
468        #[test]
469        fn invalid_checksum_sha256_from_string_wrong_chars(string in r"[e-z0-9]{64}") {
470            assert!(Sha256Checksum::from_str(&string).is_err());
471        }
472
473        #[test]
474        fn valid_checksum_sha384_from_string(string in r"[a-f0-9]{96}") {
475            prop_assert_eq!(&string, &format!("{}", Sha384Checksum::from_str(&string).unwrap()));
476        }
477
478        #[test]
479        fn invalid_checksum_sha384_from_string_bigger_size(string in r"[a-f0-9]{97}") {
480            assert!(Sha384Checksum::from_str(&string).is_err());
481        }
482
483        #[test]
484        fn invalid_checksum_sha384_from_string_smaller_size(string in r"[a-f0-9]{95}") {
485            assert!(Sha384Checksum::from_str(&string).is_err());
486        }
487
488        #[test]
489        fn invalid_checksum_sha384_from_string_wrong_chars(string in r"[e-z0-9]{96}") {
490            assert!(Sha384Checksum::from_str(&string).is_err());
491        }
492
493        #[test]
494        fn valid_checksum_sha512_from_string(string in r"[a-f0-9]{128}") {
495            prop_assert_eq!(&string, &format!("{}", Sha512Checksum::from_str(&string).unwrap()));
496        }
497
498        #[test]
499        fn invalid_checksum_sha512_from_string_bigger_size(string in r"[a-f0-9]{129}") {
500            assert!(Sha512Checksum::from_str(&string).is_err());
501        }
502
503        #[test]
504        fn invalid_checksum_sha512_from_string_smaller_size(string in r"[a-f0-9]{127}") {
505            assert!(Sha512Checksum::from_str(&string).is_err());
506        }
507
508        #[test]
509        fn invalid_checksum_sha512_from_string_wrong_chars(string in r"[e-z0-9]{128}") {
510            assert!(Sha512Checksum::from_str(&string).is_err());
511        }
512    }
513
514    #[rstest]
515    fn checksum_blake2b512() {
516        let data = "foo\n";
517        let digest = vec![
518            210, 2, 215, 149, 29, 242, 196, 183, 17, 202, 68, 180, 188, 201, 215, 179, 99, 250, 66,
519            82, 18, 126, 5, 140, 26, 145, 14, 192, 91, 108, 208, 56, 215, 28, 194, 18, 33, 192, 49,
520            192, 53, 159, 153, 62, 116, 107, 7, 245, 150, 92, 248, 197, 195, 116, 106, 88, 51, 122,
521            217, 171, 101, 39, 142, 119,
522        ];
523        let hex_digest = "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77";
524
525        let checksum = Blake2b512Checksum::calculate_from(data);
526        assert_eq!(digest, checksum.inner());
527        assert_eq!(format!("{}", &checksum), hex_digest,);
528
529        let checksum = Blake2b512Checksum::from_str(hex_digest).unwrap();
530        assert_eq!(digest, checksum.inner());
531        assert_eq!(format!("{}", &checksum), hex_digest,);
532    }
533
534    #[rstest]
535    fn checksum_sha1() {
536        let data = "foo\n";
537        let digest = vec![
538            241, 210, 210, 249, 36, 233, 134, 172, 134, 253, 247, 179, 108, 148, 188, 223, 50, 190,
539            236, 21,
540        ];
541        let hex_digest = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15";
542
543        let checksum = Sha1Checksum::calculate_from(data);
544        assert_eq!(digest, checksum.inner());
545        assert_eq!(format!("{}", &checksum), hex_digest,);
546
547        let checksum = Sha1Checksum::from_str(hex_digest).unwrap();
548        assert_eq!(digest, checksum.inner());
549        assert_eq!(format!("{}", &checksum), hex_digest,);
550    }
551
552    #[rstest]
553    fn checksum_sha224() {
554        let data = "foo\n";
555        let digest = vec![
556            231, 213, 227, 110, 141, 71, 12, 62, 81, 3, 254, 221, 46, 79, 42, 165, 195, 10, 178,
557            127, 102, 41, 189, 195, 40, 111, 157, 210,
558        ];
559        let hex_digest = "e7d5e36e8d470c3e5103fedd2e4f2aa5c30ab27f6629bdc3286f9dd2";
560
561        let checksum = Sha224Checksum::calculate_from(data);
562        assert_eq!(digest, checksum.inner());
563        assert_eq!(format!("{}", &checksum), hex_digest,);
564
565        let checksum = Sha224Checksum::from_str(hex_digest).unwrap();
566        assert_eq!(digest, checksum.inner());
567        assert_eq!(format!("{}", &checksum), hex_digest,);
568    }
569
570    #[rstest]
571    fn checksum_sha256() {
572        let data = "foo\n";
573        let digest = vec![
574            181, 187, 157, 128, 20, 160, 249, 177, 214, 30, 33, 231, 150, 215, 141, 204, 223, 19,
575            82, 242, 60, 211, 40, 18, 244, 133, 11, 135, 138, 228, 148, 76,
576        ];
577        let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
578
579        let checksum = Sha256Checksum::calculate_from(data);
580        assert_eq!(digest, checksum.inner());
581        assert_eq!(format!("{}", &checksum), hex_digest,);
582
583        let checksum = Sha256Checksum::from_str(hex_digest).unwrap();
584        assert_eq!(digest, checksum.inner());
585        assert_eq!(format!("{}", &checksum), hex_digest,);
586    }
587
588    #[rstest]
589    fn checksum_sha384() {
590        let data = "foo\n";
591        let digest = vec![
592            142, 255, 218, 191, 225, 68, 22, 33, 74, 37, 15, 147, 85, 5, 37, 11, 217, 145, 241, 6,
593            6, 93, 137, 157, 182, 225, 155, 220, 139, 246, 72, 243, 172, 15, 25, 53, 196, 246, 95,
594            232, 247, 152, 40, 155, 26, 13, 30, 6,
595        ];
596        let hex_digest = "8effdabfe14416214a250f935505250bd991f106065d899db6e19bdc8bf648f3ac0f1935c4f65fe8f798289b1a0d1e06";
597
598        let checksum = Sha384Checksum::calculate_from(data);
599        assert_eq!(digest, checksum.inner());
600        assert_eq!(format!("{}", &checksum), hex_digest,);
601
602        let checksum = Sha384Checksum::from_str(hex_digest).unwrap();
603        assert_eq!(digest, checksum.inner());
604        assert_eq!(format!("{}", &checksum), hex_digest,);
605    }
606
607    #[rstest]
608    fn checksum_sha512() {
609        let data = "foo\n";
610        let digest = vec![
611            12, 249, 24, 10, 118, 74, 186, 134, 58, 103, 182, 215, 47, 9, 24, 188, 19, 28, 103,
612            114, 100, 44, 178, 220, 229, 163, 79, 10, 112, 47, 148, 112, 221, 194, 191, 18, 92, 18,
613            25, 139, 25, 149, 194, 51, 195, 75, 74, 253, 52, 108, 84, 162, 51, 76, 53, 10, 148,
614            138, 81, 182, 232, 180, 230, 182,
615        ];
616        let hex_digest = "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6";
617
618        let checksum = Sha512Checksum::calculate_from(data);
619        assert_eq!(digest, checksum.inner());
620        assert_eq!(format!("{}", &checksum), hex_digest);
621
622        let checksum = Sha512Checksum::from_str(hex_digest).unwrap();
623        assert_eq!(digest, checksum.inner());
624        assert_eq!(format!("{}", &checksum), hex_digest);
625    }
626
627    #[rstest]
628    #[case::non_hex_digits(
629        "0cf9180a764aba863a67b6d72f0918bc13gggggg642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6",
630        "expected ASCII hex digit"
631    )]
632    #[case::incomplete_pair(" b ", "expected ASCII hex digit")]
633    #[case::incomplete_digest("0cf9180a764aba863a67b6d72f0918bca", "expected ASCII hex digit")]
634    #[case::whitespace(
635        "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",
636        "expected ASCII hex digit"
637    )]
638    fn checksum_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
639        let Err(Error::ParseError(err_msg)) = Sha512Checksum::from_str(input) else {
640            panic!("'{input}' did not fail to parse as expected")
641        };
642        assert!(
643            err_msg.contains(err_snippet),
644            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
645        );
646    }
647
648    #[rstest]
649    fn skippable_checksum_sha256() {
650        let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
651        let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
652        assert_eq!(format!("{}", &checksum), hex_digest);
653    }
654
655    #[rstest]
656    fn skippable_checksum_skip() {
657        let hex_digest = "SKIP";
658        let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
659
660        assert_eq!(SkippableChecksum::Skip, checksum);
661        assert_eq!(format!("{}", &checksum), hex_digest);
662    }
663}