alpm_types/
checksum.rs

1use std::{
2    fmt::{Debug, Display, Formatter},
3    marker::PhantomData,
4    ops::DerefMut,
5    str::FromStr,
6};
7
8use digest::{Digest, FixedOutput, HashMarker, Output, OutputSizeUser, Update};
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use strum::{Display, EnumString, VariantArray, VariantNames};
11use winnow::{
12    ModalResult,
13    Parser,
14    ascii::dec_uint,
15    combinator::{alt, cut_err, eof, repeat, terminated},
16    error::{StrContext, StrContextValue},
17    token::one_of,
18};
19
20use crate::{
21    Error,
22    digests::{Blake2b512, Md5, Sha1, Sha224, Sha256, Sha384, Sha512},
23};
24
25/// Defines the string representation format of a checksum digest.
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum DigestEncoding {
28    /// Checksum digest represented by a hexadecimal string.
29    Hex,
30    /// Checksum digest represented by a decimal string.
31    Dec,
32}
33
34/// [`Digest`] extension providing a [`Self::ENCODING`] constant defining the string representation
35/// of the digest used for parsing and formatting.
36pub trait DigestString: Digest {
37    /// The format used for string representation of the digest.
38    const ENCODING: DigestEncoding;
39}
40
41impl DigestString for Blake2b512 {
42    const ENCODING: DigestEncoding = DigestEncoding::Hex;
43}
44
45impl DigestString for Md5 {
46    const ENCODING: DigestEncoding = DigestEncoding::Hex;
47}
48
49impl DigestString for Sha1 {
50    const ENCODING: DigestEncoding = DigestEncoding::Hex;
51}
52
53impl DigestString for Sha224 {
54    const ENCODING: DigestEncoding = DigestEncoding::Hex;
55}
56
57impl DigestString for Sha256 {
58    const ENCODING: DigestEncoding = DigestEncoding::Hex;
59}
60
61impl DigestString for Sha384 {
62    const ENCODING: DigestEncoding = DigestEncoding::Hex;
63}
64
65impl DigestString for Sha512 {
66    const ENCODING: DigestEncoding = DigestEncoding::Hex;
67}
68
69impl DigestString for Crc32Cksum {
70    const ENCODING: DigestEncoding = DigestEncoding::Dec;
71}
72
73// Convenience type aliases for the supported checksums
74
75/// A checksum using the Blake2b512 algorithm
76pub type Blake2b512Checksum = Checksum<Blake2b512>;
77
78/// A checksum using the Md5 algorithm
79pub type Md5Checksum = Checksum<Md5>;
80
81/// A checksum using the Sha1 algorithm
82pub type Sha1Checksum = Checksum<Sha1>;
83
84/// A checksum using the Sha224 algorithm
85pub type Sha224Checksum = Checksum<Sha224>;
86
87/// A checksum using the Sha256 algorithm
88pub type Sha256Checksum = Checksum<Sha256>;
89
90/// A checksum using the Sha384 algorithm
91pub type Sha384Checksum = Checksum<Sha384>;
92
93/// A checksum using the Sha512 algorithm
94pub type Sha512Checksum = Checksum<Sha512>;
95
96/// A checksum using CRC-32/CKSUM algorithm
97pub type Crc32CksumChecksum = Checksum<Crc32Cksum>;
98
99/// This enum represents all accepted checksum algorithms used in the Arch Linux distribution.
100#[derive(
101    Clone,
102    Copy,
103    Debug,
104    Deserialize,
105    Display,
106    EnumString,
107    Eq,
108    Hash,
109    Ord,
110    PartialEq,
111    PartialOrd,
112    Serialize,
113    VariantNames,
114    VariantArray,
115)]
116pub enum ChecksumAlgorithm {
117    /// Blake2b-512 cryptographic hash algorithm
118    Blake2b512,
119    /// Md5 hash algorithm (deprecated)
120    Md5,
121    /// Sha1 hash algorithm (deprecated)
122    Sha1,
123    /// Sha224 hash algorithm
124    Sha224,
125    /// Sha256 hash algorithm
126    Sha256,
127    /// Sha384 hash algorithm
128    Sha384,
129    /// Sha512 hash algorithm
130    Sha512,
131    /// CRC-32/CKSUM hash algorithm
132    Crc32Cksum,
133}
134
135impl ChecksumAlgorithm {
136    /// Determines if a checksum algorithm is considered deprecated for security reasons.
137    ///
138    /// Returns `true` for cryptographically unsafe algorithms that should be avoided.
139    /// These algorithms are still supported for backwards compatibility but their use is strongly
140    /// discouraged.
141    ///
142    /// Currently deprecated algorithms:
143    ///
144    /// - [`ChecksumAlgorithm::Md5`]: Vulnerable to collision attacks
145    /// - [`ChecksumAlgorithm::Sha1`]: Vulnerable to collision attacks
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// use alpm_types::ChecksumAlgorithm;
151    ///
152    /// // Deprecated algorithms
153    /// assert!(ChecksumAlgorithm::Md5.is_deprecated());
154    /// assert!(ChecksumAlgorithm::Sha1.is_deprecated());
155    ///
156    /// // Safe algorithms
157    /// assert!(!ChecksumAlgorithm::Sha256.is_deprecated());
158    /// assert!(!ChecksumAlgorithm::Blake2b512.is_deprecated());
159    /// ```
160    pub fn is_deprecated(&self) -> bool {
161        match self {
162            ChecksumAlgorithm::Md5 | ChecksumAlgorithm::Sha1 | ChecksumAlgorithm::Crc32Cksum => {
163                true
164            }
165            ChecksumAlgorithm::Blake2b512
166            | ChecksumAlgorithm::Sha224
167            | ChecksumAlgorithm::Sha256
168            | ChecksumAlgorithm::Sha384
169            | ChecksumAlgorithm::Sha512 => false,
170        }
171    }
172
173    /// Returns a list of [`ChecksumAlgorithm`] variants that are not considered deprecated.
174    pub fn non_deprecated_checksums(&self) -> Vec<ChecksumAlgorithm> {
175        <ChecksumAlgorithm as VariantArray>::VARIANTS
176            .iter()
177            .filter(|algo| !algo.is_deprecated())
178            .copied()
179            .collect::<Vec<ChecksumAlgorithm>>()
180    }
181}
182
183/// A [checksum] using a supported algorithm
184///
185/// Checksums are created using one of the supported algorithms:
186///
187/// - `Blake2b512`
188/// - `Md5` (**WARNING**: Use of this algorithm is highly discouraged, because it is
189///   cryptographically unsafe)
190/// - `Sha1` (**WARNING**: Use of this algorithm is highly discouraged, because it is
191///   cryptographically unsafe)
192/// - `Sha224`
193/// - `Sha256`
194/// - `Sha384`
195/// - `Sha512`
196/// - `Crc32Cksum` (**WARNING**: Use of this algorithm is highly discouraged, because it is
197///   cryptographically unsafe)
198///
199/// ## Note
200///
201/// There are two ways to use a checksum:
202///
203/// 1. Generically over a digest (e.g. `Checksum::<Blake2b512>`)
204/// 2. Using the convenience type aliases (e.g. `Blake2b512Checksum`)
205///
206/// ## Examples
207///
208/// ```
209/// use std::str::FromStr;
210/// use alpm_types::{digests::Blake2b512, Checksum};
211///
212/// # fn main() -> Result<(), alpm_types::Error> {
213/// let checksum = Checksum::<Blake2b512>::calculate_from("foo\n");
214/// let digest = vec![
215///     210, 2, 215, 149, 29, 242, 196, 183, 17, 202, 68, 180, 188, 201, 215, 179, 99, 250, 66,
216///     82, 18, 126, 5, 140, 26, 145, 14, 192, 91, 108, 208, 56, 215, 28, 194, 18, 33, 192, 49,
217///     192, 53, 159, 153, 62, 116, 107, 7, 245, 150, 92, 248, 197, 195, 116, 106, 88, 51, 122,
218///     217, 171, 101, 39, 142, 119,
219/// ];
220/// assert_eq!(checksum.inner(), digest);
221/// assert_eq!(
222///     format!("{}", checksum),
223///     "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77",
224/// );
225///
226/// // create checksum from hex string
227/// let checksum = Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77")?;
228/// assert_eq!(checksum.inner(), digest);
229/// # Ok(())
230/// # }
231/// ```
232///
233/// # Developer Note
234///
235/// In case you want to wrap this type and make the parent `Serialize`able, please note the
236/// following:
237///
238/// Serde automatically adds a `Serialize` trait bound on top of it trait bounds in wrapper
239/// types. **However**, that's not needed as we use `D` simply as a phantom marker that
240/// isn't serialized in the first place.
241/// To fix this in your wrapper type, make use of the [bound container attribute], e.g.:
242///
243/// [checksum]: https://en.wikipedia.org/wiki/Checksum
244/// ```
245/// use alpm_types::{Checksum, digests::Digest};
246/// use serde::Serialize;
247///
248/// #[derive(Serialize)]
249/// struct Wrapper<D: Digest> {
250///     #[serde(bound = "D: Digest")]
251///     checksum: Checksum<D>,
252/// }
253/// ```
254#[derive(Clone)]
255pub struct Checksum<D: Digest> {
256    digest: Vec<u8>,
257    _marker: PhantomData<D>,
258}
259
260impl<D: DigestString> Serialize for Checksum<D> {
261    /// Serialize a [`Checksum`] into a hex `String` representation.
262    ///
263    /// We chose hex as byte vectors are imperformant and considered bad practice for non-binary
264    /// formats like `JSON` or `YAML`
265    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
266    where
267        S: Serializer,
268    {
269        serializer.serialize_str(&self.to_string())
270    }
271}
272
273impl<'de, D: DigestString> Deserialize<'de> for Checksum<D> {
274    fn deserialize<De>(deserializer: De) -> Result<Self, De::Error>
275    where
276        De: Deserializer<'de>,
277    {
278        let s = String::deserialize(deserializer)?;
279        Checksum::from_str(&s).map_err(serde::de::Error::custom)
280    }
281}
282
283impl<D: DigestString> Checksum<D> {
284    /// Calculate a new Checksum for data that may be represented as a list of bytes
285    ///
286    /// ## Examples
287    /// ```
288    /// use alpm_types::{digests::Blake2b512, Checksum};
289    ///
290    /// assert_eq!(
291    ///     format!("{}", Checksum::<Blake2b512>::calculate_from("foo\n")),
292    ///     "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77",
293    /// );
294    /// ```
295    pub fn calculate_from(input: impl AsRef<[u8]>) -> Self {
296        let mut hasher = D::new();
297        hasher.update(input);
298
299        Checksum {
300            digest: hasher.finalize()[..].to_vec(),
301            _marker: PhantomData,
302        }
303    }
304
305    /// Return a reference to the inner type
306    pub fn inner(&self) -> &[u8] {
307        &self.digest
308    }
309
310    /// Recognizes an ASCII hexadecimal [`Checksum`] from a string slice.
311    ///
312    /// Consumes all input.
313    /// See [`Checksum::from_str`].
314    ///
315    /// # Errors
316    ///
317    /// Returns an error if `input` is not the output of a _hash function_
318    /// in hexadecimal (or decimal in case of CRC-32/CKSUM) form.
319    pub fn parser(input: &mut &str) -> ModalResult<Self> {
320        /// Consume 1 hex digit and return its hex value.
321        ///
322        /// Accepts uppercase or lowercase.
323        #[inline]
324        fn hex_digit(input: &mut &str) -> ModalResult<u8> {
325            one_of(('0'..='9', 'a'..='f', 'A'..='F'))
326                .map(|d: char|
327                    // unwraps are unreachable: their invariants are always
328                    // upheld because the above character set can never
329                    // consume anything but a single valid hex digit
330                    d.to_digit(16).unwrap().try_into().unwrap())
331                .context(StrContext::Expected(StrContextValue::Description(
332                    "ASCII hex digit",
333                )))
334                .parse_next(input)
335        }
336
337        let hex_pair = (hex_digit, hex_digit).map(|(first, second)|
338            // shift is infallible because hex_digit cannot return >0b00001111
339            (first << 4) + second);
340
341        // output size in bytes
342        let digest_bytes = <D as Digest>::output_size();
343
344        let digest = match D::ENCODING {
345            DigestEncoding::Hex => {
346                // Consume exactly the number of hex pairs that our Digest type expects
347                let digest = cut_err(repeat(digest_bytes, hex_pair))
348                    .context(StrContext::Label("hash digest"))
349                    .context(StrContext::Expected(StrContextValue::Description(
350                        "a hex hash digest with the appropriate length for the given algorithm.",
351                    )))
352                    .parse_next(input)?;
353
354                cut_err(eof)
355                    .context(StrContext::Expected(StrContextValue::Description(
356                        "end of checksum. Checksum is too long.",
357                    )))
358                    .parse_next(input)?;
359
360                digest
361            }
362            DigestEncoding::Dec => {
363                // output size in bits
364                let digest_bits = digest_bytes * 8;
365
366                // The following logic parses a decimal integer for consumption by a digest.
367                // We chose to use a [`u128::MAX`] as this is the currently largest number type in
368                // the rust std library. In reality we only use this for CRC-32/CKSUM which is
369                // 4 bytes, but it's nice to keep this a bit more generic.
370
371                // Determine the maximum allowed value based on the number of allowed
372                // `digest_bytes`.
373                let max_value: u128 = if digest_bits >= 128 {
374                    // Since we're parsing into a `u128`, we don't allow digests that use more bytes
375                    // than that. If we ever were to add such a digest, this
376                    // logic needs to be adjusted.
377                    u128::MAX
378                } else {
379                    (1u128 << digest_bits) - 1
380                };
381
382                // Parse into the u128 decimal and verify that the resulting value fits into our
383                // requested digest length. E.g. CRC-32 is restricted to a u32.
384                cut_err(dec_uint::<_, u128, _>)
385                    .verify(move |&v| v <= max_value)
386                    // Convert the u128 into a big endian byte array.
387                    // Then cut the array at the highest significant byte we allow for this digest.
388                    .map(move |v | v.to_be_bytes()[16 - digest_bytes..].to_vec())
389                    .context(StrContext::Label("hash digest"))
390                    .context(StrContext::Expected(StrContextValue::Description(
391                        "a decimal hash digest with the appropriate length for the given algorithm.",
392                    )))
393                    .parse_next(input)?
394            }
395        };
396
397        Ok(Self {
398            digest,
399            _marker: PhantomData,
400        })
401    }
402}
403
404impl<D: DigestString> FromStr for Checksum<D> {
405    type Err = Error;
406    /// Create a new Checksum from a hex string and return it in a Result
407    ///
408    /// The input is processed as a lowercase string.
409    /// An Error is returned, if the input length does not match the output size for the given
410    /// supported algorithm, or if the provided hex string could not be converted to a list of
411    /// bytes.
412    ///
413    /// Delegates to [`Checksum::parser`].
414    ///
415    /// ## Examples
416    /// ```
417    /// use std::str::FromStr;
418    /// use alpm_types::{digests::Blake2b512, Checksum};
419    ///
420    /// assert!(Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77").is_ok());
421    /// assert!(Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e7").is_err());
422    /// assert!(Checksum::<Blake2b512>::from_str("d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e7x").is_err());
423    /// ```
424    fn from_str(s: &str) -> Result<Checksum<D>, Self::Err> {
425        Ok(Checksum::parser.parse(s)?)
426    }
427}
428
429impl<D: DigestString> Display for Checksum<D> {
430    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
431        match D::ENCODING {
432            DigestEncoding::Hex => {
433                write!(
434                    fmt,
435                    "{}",
436                    self.digest
437                        .iter()
438                        .map(|x| format!("{x:02x?}"))
439                        .collect::<Vec<String>>()
440                        .join("")
441                )
442            }
443            DigestEncoding::Dec => {
444                // Convert a big-endian byte array into an u128.
445                // The parser already assumes that the digest fits into a u128,
446                // so this should be infallible.
447                let value = self
448                    .digest
449                    .iter()
450                    .fold(0u128, |acc, &byte| (acc << 8) | byte as u128);
451                write!(fmt, "{}", value)
452            }
453        }
454    }
455}
456
457/// Use [Display] as [Debug] impl, since the byte representation and [PhantomData] field aren't
458/// relevant for debugging purposes.
459impl<D: DigestString> Debug for Checksum<D> {
460    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
461        Display::fmt(&self, f)
462    }
463}
464
465impl<D: Digest> PartialEq for Checksum<D> {
466    fn eq(&self, other: &Self) -> bool {
467        self.digest == other.digest
468    }
469}
470
471impl<D: Digest> Eq for Checksum<D> {}
472
473impl<D: Digest> Ord for Checksum<D> {
474    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
475        self.digest.cmp(&other.digest)
476    }
477}
478
479impl<D: Digest> PartialOrd for Checksum<D> {
480    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
481        Some(self.cmp(other))
482    }
483}
484
485/// A [`Checksum`] that may be skipped.
486///
487/// Strings representing checksums are used to verify the integrity of files.
488/// If the `"SKIP"` keyword is found, the integrity check is skipped.
489#[derive(Clone, Debug, Deserialize, Serialize)]
490#[serde(tag = "type")]
491pub enum SkippableChecksum<D: DigestString + Clone> {
492    /// Sourcefile checksum validation may be skipped, which is expressed with this variant.
493    Skip,
494    /// The related source file should be validated via the provided checksum.
495    #[serde(bound = "D: Digest + Clone")]
496    Checksum {
497        /// The checksum to be used for the validation.
498        digest: Checksum<D>,
499    },
500}
501
502impl<D: DigestString + Clone> SkippableChecksum<D> {
503    /// Determines whether the [`SkippableChecksum`] is skipped.
504    ///
505    /// Checksums are considered skipped if they are of the variant [`SkippableChecksum::Skip`].
506    pub fn is_skipped(&self) -> bool {
507        matches!(self, SkippableChecksum::Skip)
508    }
509
510    /// Recognizes a [`SkippableChecksum`] from a string slice.
511    ///
512    /// Consumes all its input.
513    /// See [`SkippableChecksum::from_str`], [`Checksum::parser`] and [`Checksum::from_str`].
514    ///
515    /// # Errors
516    ///
517    /// Returns an error if `input` is not the output of a _hash function_
518    /// in hexadecimal (or decimal in case of CRC32/CKSUM) form.
519    pub fn parser(input: &mut &str) -> ModalResult<Self> {
520        terminated(
521            alt((
522                "SKIP".value(Self::Skip),
523                Checksum::parser.map(|digest| Self::Checksum { digest }),
524            )),
525            cut_err(eof).context(StrContext::Expected(StrContextValue::Description(
526                "end of checksum.",
527            ))),
528        )
529        .parse_next(input)
530    }
531}
532
533impl<D: DigestString + Clone> FromStr for SkippableChecksum<D> {
534    type Err = Error;
535    /// Create a new [`SkippableChecksum`] from a string slice and return it in a Result.
536    ///
537    /// First checks for the special `SKIP` keyword, before trying [`Checksum::from_str`].
538    ///
539    /// Delegates to [`SkippableChecksum::parser`].
540    ///
541    /// ## Examples
542    /// ```
543    /// use std::str::FromStr;
544    ///
545    /// use alpm_types::{SkippableChecksum, digests::Sha256};
546    ///
547    /// assert!(SkippableChecksum::<Sha256>::from_str("SKIP").is_ok());
548    /// assert!(
549    ///     SkippableChecksum::<Sha256>::from_str(
550    ///         "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"
551    ///     )
552    ///     .is_ok()
553    /// );
554    /// ```
555    fn from_str(s: &str) -> Result<SkippableChecksum<D>, Self::Err> {
556        Ok(Self::parser.parse(s)?)
557    }
558}
559
560impl<D: DigestString + Clone> Display for SkippableChecksum<D> {
561    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
562        let output = match self {
563            SkippableChecksum::Skip => "SKIP".to_string(),
564            SkippableChecksum::Checksum { digest } => digest.to_string(),
565        };
566        write!(fmt, "{output}",)
567    }
568}
569
570impl<D: DigestString + Clone> PartialEq for SkippableChecksum<D> {
571    fn eq(&self, other: &Self) -> bool {
572        match (self, other) {
573            (SkippableChecksum::Skip, SkippableChecksum::Skip) => true,
574            (SkippableChecksum::Skip, SkippableChecksum::Checksum { .. }) => false,
575            (SkippableChecksum::Checksum { .. }, SkippableChecksum::Skip) => false,
576            (
577                SkippableChecksum::Checksum { digest },
578                SkippableChecksum::Checksum {
579                    digest: digest_other,
580                },
581            ) => digest == digest_other,
582        }
583    }
584}
585
586/// CRC-32/CKSUM hasher state.
587///
588/// This implementation tracks the length of the input data and appends it to the checksum
589/// calculation similarly to the Unix `cksum` utility.
590#[derive(Clone, Debug)]
591pub struct Crc32Cksum {
592    digest: crc_fast::Digest,
593    len: u64,
594}
595
596impl HashMarker for Crc32Cksum {}
597
598impl Default for Crc32Cksum {
599    fn default() -> Self {
600        Self {
601            digest: crc_fast::Digest::new(crc_fast::CrcAlgorithm::Crc32Cksum),
602            len: 0,
603        }
604    }
605}
606
607impl Update for Crc32Cksum {
608    fn update(&mut self, data: &[u8]) {
609        self.digest.update(data);
610        self.len += data.len() as u64;
611    }
612}
613
614impl OutputSizeUser for Crc32Cksum {
615    type OutputSize = digest::consts::U4;
616}
617
618impl FixedOutput for Crc32Cksum {
619    fn finalize_into(mut self, out: &mut Output<Self>) {
620        if self.len != 0 {
621            let len_bytes = self.len.to_be_bytes();
622
623            // Skip leading zero bytes and append the length to the digest...
624            let start = len_bytes.iter().position(|&b| b != 0).unwrap_or(7);
625            self.digest.update(&len_bytes[start..]);
626        }
627
628        let crc = self.digest.finalize() as u32;
629        out.deref_mut().clone_from_slice(&crc.to_be_bytes());
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use proptest::prelude::*;
636    use rstest::rstest;
637
638    use super::*;
639
640    proptest! {
641        #![proptest_config(ProptestConfig::with_cases(1000))]
642
643        #[test]
644        fn valid_checksum_blake2b512_from_string(string in r"[a-f0-9]{128}") {
645            prop_assert_eq!(&string, &format!("{}", Blake2b512Checksum::from_str(&string).unwrap()));
646        }
647
648        #[test]
649        fn invalid_checksum_blake2b512_bigger_size(string in r"[a-f0-9]{129}") {
650            assert!(Blake2b512Checksum::from_str(&string).is_err());
651        }
652
653        #[test]
654        fn invalid_checksum_blake2b512_smaller_size(string in r"[a-f0-9]{127}") {
655            assert!(Blake2b512Checksum::from_str(&string).is_err());
656        }
657
658        #[test]
659        fn invalid_checksum_blake2b512_wrong_chars(string in r"[e-z0-9]{128}") {
660            assert!(Blake2b512Checksum::from_str(&string).is_err());
661        }
662
663        #[test]
664        fn valid_checksum_sha1_from_string(string in r"[a-f0-9]{40}") {
665            prop_assert_eq!(&string, &format!("{}", Sha1Checksum::from_str(&string).unwrap()));
666        }
667
668        #[test]
669        fn invalid_checksum_sha1_from_string_bigger_size(string in r"[a-f0-9]{41}") {
670            assert!(Sha1Checksum::from_str(&string).is_err());
671        }
672
673        #[test]
674        fn invalid_checksum_sha1_from_string_smaller_size(string in r"[a-f0-9]{39}") {
675            assert!(Sha1Checksum::from_str(&string).is_err());
676        }
677
678        #[test]
679        fn invalid_checksum_sha1_from_string_wrong_chars(string in r"[e-z0-9]{40}") {
680            assert!(Sha1Checksum::from_str(&string).is_err());
681        }
682
683        #[test]
684        fn valid_checksum_sha224_from_string(string in r"[a-f0-9]{56}") {
685            prop_assert_eq!(&string, &format!("{}", Sha224Checksum::from_str(&string).unwrap()));
686        }
687
688        #[test]
689        fn invalid_checksum_sha224_from_string_bigger_size(string in r"[a-f0-9]{57}") {
690            assert!(Sha224Checksum::from_str(&string).is_err());
691        }
692
693        #[test]
694        fn invalid_checksum_sha224_from_string_smaller_size(string in r"[a-f0-9]{55}") {
695            assert!(Sha224Checksum::from_str(&string).is_err());
696        }
697
698        #[test]
699        fn invalid_checksum_sha224_from_string_wrong_chars(string in r"[e-z0-9]{56}") {
700            assert!(Sha224Checksum::from_str(&string).is_err());
701        }
702
703        #[test]
704        fn valid_checksum_sha256_from_string(string in r"[a-f0-9]{64}") {
705            prop_assert_eq!(&string, &format!("{}", Sha256Checksum::from_str(&string).unwrap()));
706        }
707
708        #[test]
709        fn invalid_checksum_sha256_from_string_bigger_size(string in r"[a-f0-9]{65}") {
710            assert!(Sha256Checksum::from_str(&string).is_err());
711        }
712
713        #[test]
714        fn invalid_checksum_sha256_from_string_smaller_size(string in r"[a-f0-9]{63}") {
715            assert!(Sha256Checksum::from_str(&string).is_err());
716        }
717
718        #[test]
719        fn invalid_checksum_sha256_from_string_wrong_chars(string in r"[e-z0-9]{64}") {
720            assert!(Sha256Checksum::from_str(&string).is_err());
721        }
722
723        #[test]
724        fn valid_checksum_sha384_from_string(string in r"[a-f0-9]{96}") {
725            prop_assert_eq!(&string, &format!("{}", Sha384Checksum::from_str(&string).unwrap()));
726        }
727
728        #[test]
729        fn invalid_checksum_sha384_from_string_bigger_size(string in r"[a-f0-9]{97}") {
730            assert!(Sha384Checksum::from_str(&string).is_err());
731        }
732
733        #[test]
734        fn invalid_checksum_sha384_from_string_smaller_size(string in r"[a-f0-9]{95}") {
735            assert!(Sha384Checksum::from_str(&string).is_err());
736        }
737
738        #[test]
739        fn invalid_checksum_sha384_from_string_wrong_chars(string in r"[e-z0-9]{96}") {
740            assert!(Sha384Checksum::from_str(&string).is_err());
741        }
742
743        #[test]
744        fn valid_checksum_sha512_from_string(string in r"[a-f0-9]{128}") {
745            prop_assert_eq!(&string, &format!("{}", Sha512Checksum::from_str(&string).unwrap()));
746        }
747
748        #[test]
749        fn invalid_checksum_sha512_from_string_bigger_size(string in r"[a-f0-9]{129}") {
750            assert!(Sha512Checksum::from_str(&string).is_err());
751        }
752
753        #[test]
754        fn invalid_checksum_sha512_from_string_smaller_size(string in r"[a-f0-9]{127}") {
755            assert!(Sha512Checksum::from_str(&string).is_err());
756        }
757
758        #[test]
759        fn invalid_checksum_sha512_from_string_wrong_chars(string in r"[e-z0-9]{128}") {
760            assert!(Sha512Checksum::from_str(&string).is_err());
761        }
762
763        #[test]
764        fn valid_checksum_crc32cksum(sum in 0u32..=u32::MAX) {
765            let decimal_str = format!("{sum}");
766            prop_assert_eq!(
767                &decimal_str,
768                &format!("{}", Crc32CksumChecksum::from_str(decimal_str.as_str()).unwrap())
769            );
770        }
771
772        #[test]
773        fn invalid_checksum_crc32cksum_bigger_size(sum in (u32::MAX as u128)..=u128::MAX) {
774            let decimal_str = format!("{sum}");
775            assert!(Crc32CksumChecksum::from_str(decimal_str.as_str()).is_err());
776        }
777
778        #[test]
779        fn invalid_checksum_crc32cksum_wrong_chars(string in r"[a-f]{9}") {
780            assert!(Crc32CksumChecksum::from_str(&string).is_err());
781        }
782
783        #[test]
784        fn invalid_checksum_crc32cksum_negative(string in r"-[1-9]{9}") {
785            assert!(Crc32CksumChecksum::from_str(&string).is_err());
786        }
787    }
788
789    #[rstest]
790    fn checksum_blake2b512() {
791        let data = "foo\n";
792        let digest = vec![
793            210, 2, 215, 149, 29, 242, 196, 183, 17, 202, 68, 180, 188, 201, 215, 179, 99, 250, 66,
794            82, 18, 126, 5, 140, 26, 145, 14, 192, 91, 108, 208, 56, 215, 28, 194, 18, 33, 192, 49,
795            192, 53, 159, 153, 62, 116, 107, 7, 245, 150, 92, 248, 197, 195, 116, 106, 88, 51, 122,
796            217, 171, 101, 39, 142, 119,
797        ];
798        let hex_digest = "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77";
799
800        let checksum = Blake2b512Checksum::calculate_from(data);
801        assert_eq!(digest, checksum.inner());
802        assert_eq!(format!("{}", &checksum), hex_digest,);
803
804        let checksum = Blake2b512Checksum::from_str(hex_digest).unwrap();
805        assert_eq!(digest, checksum.inner());
806        assert_eq!(format!("{}", &checksum), hex_digest,);
807    }
808
809    #[rstest]
810    fn checksum_sha1() {
811        let data = "foo\n";
812        let digest = vec![
813            241, 210, 210, 249, 36, 233, 134, 172, 134, 253, 247, 179, 108, 148, 188, 223, 50, 190,
814            236, 21,
815        ];
816        let hex_digest = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15";
817
818        let checksum = Sha1Checksum::calculate_from(data);
819        assert_eq!(digest, checksum.inner());
820        assert_eq!(format!("{}", &checksum), hex_digest,);
821
822        let checksum = Sha1Checksum::from_str(hex_digest).unwrap();
823        assert_eq!(digest, checksum.inner());
824        assert_eq!(format!("{}", &checksum), hex_digest,);
825    }
826
827    #[rstest]
828    fn checksum_sha224() {
829        let data = "foo\n";
830        let digest = vec![
831            231, 213, 227, 110, 141, 71, 12, 62, 81, 3, 254, 221, 46, 79, 42, 165, 195, 10, 178,
832            127, 102, 41, 189, 195, 40, 111, 157, 210,
833        ];
834        let hex_digest = "e7d5e36e8d470c3e5103fedd2e4f2aa5c30ab27f6629bdc3286f9dd2";
835
836        let checksum = Sha224Checksum::calculate_from(data);
837        assert_eq!(digest, checksum.inner());
838        assert_eq!(format!("{}", &checksum), hex_digest,);
839
840        let checksum = Sha224Checksum::from_str(hex_digest).unwrap();
841        assert_eq!(digest, checksum.inner());
842        assert_eq!(format!("{}", &checksum), hex_digest,);
843    }
844
845    #[rstest]
846    fn checksum_sha256() {
847        let data = "foo\n";
848        let digest = vec![
849            181, 187, 157, 128, 20, 160, 249, 177, 214, 30, 33, 231, 150, 215, 141, 204, 223, 19,
850            82, 242, 60, 211, 40, 18, 244, 133, 11, 135, 138, 228, 148, 76,
851        ];
852        let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
853
854        let checksum = Sha256Checksum::calculate_from(data);
855        assert_eq!(digest, checksum.inner());
856        assert_eq!(format!("{}", &checksum), hex_digest,);
857
858        let checksum = Sha256Checksum::from_str(hex_digest).unwrap();
859        assert_eq!(digest, checksum.inner());
860        assert_eq!(format!("{}", &checksum), hex_digest,);
861    }
862
863    #[rstest]
864    fn checksum_sha384() {
865        let data = "foo\n";
866        let digest = vec![
867            142, 255, 218, 191, 225, 68, 22, 33, 74, 37, 15, 147, 85, 5, 37, 11, 217, 145, 241, 6,
868            6, 93, 137, 157, 182, 225, 155, 220, 139, 246, 72, 243, 172, 15, 25, 53, 196, 246, 95,
869            232, 247, 152, 40, 155, 26, 13, 30, 6,
870        ];
871        let hex_digest = "8effdabfe14416214a250f935505250bd991f106065d899db6e19bdc8bf648f3ac0f1935c4f65fe8f798289b1a0d1e06";
872
873        let checksum = Sha384Checksum::calculate_from(data);
874        assert_eq!(digest, checksum.inner());
875        assert_eq!(format!("{}", &checksum), hex_digest,);
876
877        let checksum = Sha384Checksum::from_str(hex_digest).unwrap();
878        assert_eq!(digest, checksum.inner());
879        assert_eq!(format!("{}", &checksum), hex_digest,);
880    }
881
882    #[rstest]
883    fn checksum_sha512() {
884        let data = "foo\n";
885        let digest = vec![
886            12, 249, 24, 10, 118, 74, 186, 134, 58, 103, 182, 215, 47, 9, 24, 188, 19, 28, 103,
887            114, 100, 44, 178, 220, 229, 163, 79, 10, 112, 47, 148, 112, 221, 194, 191, 18, 92, 18,
888            25, 139, 25, 149, 194, 51, 195, 75, 74, 253, 52, 108, 84, 162, 51, 76, 53, 10, 148,
889            138, 81, 182, 232, 180, 230, 182,
890        ];
891        let hex_digest = "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6";
892
893        let checksum = Sha512Checksum::calculate_from(data);
894        assert_eq!(digest, checksum.inner());
895        assert_eq!(format!("{}", &checksum), hex_digest);
896
897        let checksum = Sha512Checksum::from_str(hex_digest).unwrap();
898        assert_eq!(digest, checksum.inner());
899        assert_eq!(format!("{}", &checksum), hex_digest);
900    }
901
902    #[rstest]
903    fn checksum_crc32cksum() {
904        let data = "foo\n";
905        let digest = 3915528286u32;
906        let digest_string = format!("{digest}");
907
908        let checksum = Crc32CksumChecksum::calculate_from(data);
909        assert_eq!(digest.to_be_bytes(), checksum.inner());
910        assert_eq!(format!("{}", &checksum), digest_string);
911
912        let checksum = Crc32CksumChecksum::from_str(digest_string.as_str()).unwrap();
913        assert_eq!(digest.to_be_bytes(), checksum.inner());
914        assert_eq!(format!("{}", &checksum), digest_string);
915    }
916
917    #[rstest]
918    #[case::non_hex_digits(
919        "0cf9180a764aba863a67b6d72f0918bc13gggggg642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6",
920        "expected ASCII hex digit"
921    )]
922    #[case::incomplete_pair(" b ", "expected ASCII hex digit")]
923    #[case::incomplete_digest("0cf9180a764aba863a67b6d72f0918bca", "expected ASCII hex digit")]
924    #[case::whitespace(
925        "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",
926        "expected ASCII hex digit"
927    )]
928    fn checksum_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
929        let Err(Error::ParseError(err_msg)) = Sha512Checksum::from_str(input) else {
930            panic!("'{input}' did not fail to parse as expected")
931        };
932        assert!(
933            err_msg.contains(err_snippet),
934            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
935        );
936    }
937
938    #[rstest]
939    fn skippable_checksum_sha256() {
940        let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
941        let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
942        assert_eq!(format!("{}", &checksum), hex_digest);
943    }
944
945    #[rstest]
946    fn skippable_checksum_skip() {
947        let hex_digest = "SKIP";
948        let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
949
950        assert_eq!(SkippableChecksum::Skip, checksum);
951        assert_eq!(format!("{}", &checksum), hex_digest);
952    }
953}