Skip to main content

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 insta::assert_snapshot;
636    use proptest::prelude::*;
637    use rstest::rstest;
638
639    use super::*;
640    use crate::configure_insta;
641
642    proptest! {
643        #![proptest_config(ProptestConfig::with_cases(1000))]
644
645        #[test]
646        fn valid_checksum_blake2b512_from_string(string in r"[a-f0-9]{128}") {
647            prop_assert_eq!(&string, &format!("{}", Blake2b512Checksum::from_str(&string).unwrap()));
648        }
649
650        #[test]
651        fn invalid_checksum_blake2b512_bigger_size(string in r"[a-f0-9]{129}") {
652            assert!(Blake2b512Checksum::from_str(&string).is_err());
653        }
654
655        #[test]
656        fn invalid_checksum_blake2b512_smaller_size(string in r"[a-f0-9]{127}") {
657            assert!(Blake2b512Checksum::from_str(&string).is_err());
658        }
659
660        #[test]
661        fn invalid_checksum_blake2b512_wrong_chars(string in r"[e-z0-9]{128}") {
662            assert!(Blake2b512Checksum::from_str(&string).is_err());
663        }
664
665        #[test]
666        fn valid_checksum_sha1_from_string(string in r"[a-f0-9]{40}") {
667            prop_assert_eq!(&string, &format!("{}", Sha1Checksum::from_str(&string).unwrap()));
668        }
669
670        #[test]
671        fn invalid_checksum_sha1_from_string_bigger_size(string in r"[a-f0-9]{41}") {
672            assert!(Sha1Checksum::from_str(&string).is_err());
673        }
674
675        #[test]
676        fn invalid_checksum_sha1_from_string_smaller_size(string in r"[a-f0-9]{39}") {
677            assert!(Sha1Checksum::from_str(&string).is_err());
678        }
679
680        #[test]
681        fn invalid_checksum_sha1_from_string_wrong_chars(string in r"[e-z0-9]{40}") {
682            assert!(Sha1Checksum::from_str(&string).is_err());
683        }
684
685        #[test]
686        fn valid_checksum_sha224_from_string(string in r"[a-f0-9]{56}") {
687            prop_assert_eq!(&string, &format!("{}", Sha224Checksum::from_str(&string).unwrap()));
688        }
689
690        #[test]
691        fn invalid_checksum_sha224_from_string_bigger_size(string in r"[a-f0-9]{57}") {
692            assert!(Sha224Checksum::from_str(&string).is_err());
693        }
694
695        #[test]
696        fn invalid_checksum_sha224_from_string_smaller_size(string in r"[a-f0-9]{55}") {
697            assert!(Sha224Checksum::from_str(&string).is_err());
698        }
699
700        #[test]
701        fn invalid_checksum_sha224_from_string_wrong_chars(string in r"[e-z0-9]{56}") {
702            assert!(Sha224Checksum::from_str(&string).is_err());
703        }
704
705        #[test]
706        fn valid_checksum_sha256_from_string(string in r"[a-f0-9]{64}") {
707            prop_assert_eq!(&string, &format!("{}", Sha256Checksum::from_str(&string).unwrap()));
708        }
709
710        #[test]
711        fn invalid_checksum_sha256_from_string_bigger_size(string in r"[a-f0-9]{65}") {
712            assert!(Sha256Checksum::from_str(&string).is_err());
713        }
714
715        #[test]
716        fn invalid_checksum_sha256_from_string_smaller_size(string in r"[a-f0-9]{63}") {
717            assert!(Sha256Checksum::from_str(&string).is_err());
718        }
719
720        #[test]
721        fn invalid_checksum_sha256_from_string_wrong_chars(string in r"[e-z0-9]{64}") {
722            assert!(Sha256Checksum::from_str(&string).is_err());
723        }
724
725        #[test]
726        fn valid_checksum_sha384_from_string(string in r"[a-f0-9]{96}") {
727            prop_assert_eq!(&string, &format!("{}", Sha384Checksum::from_str(&string).unwrap()));
728        }
729
730        #[test]
731        fn invalid_checksum_sha384_from_string_bigger_size(string in r"[a-f0-9]{97}") {
732            assert!(Sha384Checksum::from_str(&string).is_err());
733        }
734
735        #[test]
736        fn invalid_checksum_sha384_from_string_smaller_size(string in r"[a-f0-9]{95}") {
737            assert!(Sha384Checksum::from_str(&string).is_err());
738        }
739
740        #[test]
741        fn invalid_checksum_sha384_from_string_wrong_chars(string in r"[e-z0-9]{96}") {
742            assert!(Sha384Checksum::from_str(&string).is_err());
743        }
744
745        #[test]
746        fn valid_checksum_sha512_from_string(string in r"[a-f0-9]{128}") {
747            prop_assert_eq!(&string, &format!("{}", Sha512Checksum::from_str(&string).unwrap()));
748        }
749
750        #[test]
751        fn invalid_checksum_sha512_from_string_bigger_size(string in r"[a-f0-9]{129}") {
752            assert!(Sha512Checksum::from_str(&string).is_err());
753        }
754
755        #[test]
756        fn invalid_checksum_sha512_from_string_smaller_size(string in r"[a-f0-9]{127}") {
757            assert!(Sha512Checksum::from_str(&string).is_err());
758        }
759
760        #[test]
761        fn invalid_checksum_sha512_from_string_wrong_chars(string in r"[e-z0-9]{128}") {
762            assert!(Sha512Checksum::from_str(&string).is_err());
763        }
764
765        #[test]
766        fn valid_checksum_crc32cksum(sum in 0u32..=u32::MAX) {
767            let decimal_str = format!("{sum}");
768            prop_assert_eq!(
769                &decimal_str,
770                &format!("{}", Crc32CksumChecksum::from_str(decimal_str.as_str()).unwrap())
771            );
772        }
773
774        #[test]
775        fn invalid_checksum_crc32cksum_bigger_size(sum in (u32::MAX as u128)..=u128::MAX) {
776            let decimal_str = format!("{sum}");
777            assert!(Crc32CksumChecksum::from_str(decimal_str.as_str()).is_err());
778        }
779
780        #[test]
781        fn invalid_checksum_crc32cksum_wrong_chars(string in r"[a-f]{9}") {
782            assert!(Crc32CksumChecksum::from_str(&string).is_err());
783        }
784
785        #[test]
786        fn invalid_checksum_crc32cksum_negative(string in r"-[1-9]{9}") {
787            assert!(Crc32CksumChecksum::from_str(&string).is_err());
788        }
789    }
790
791    #[rstest]
792    fn checksum_blake2b512() {
793        let data = "foo\n";
794        let digest = vec![
795            210, 2, 215, 149, 29, 242, 196, 183, 17, 202, 68, 180, 188, 201, 215, 179, 99, 250, 66,
796            82, 18, 126, 5, 140, 26, 145, 14, 192, 91, 108, 208, 56, 215, 28, 194, 18, 33, 192, 49,
797            192, 53, 159, 153, 62, 116, 107, 7, 245, 150, 92, 248, 197, 195, 116, 106, 88, 51, 122,
798            217, 171, 101, 39, 142, 119,
799        ];
800        let hex_digest = "d202d7951df2c4b711ca44b4bcc9d7b363fa4252127e058c1a910ec05b6cd038d71cc21221c031c0359f993e746b07f5965cf8c5c3746a58337ad9ab65278e77";
801
802        let checksum = Blake2b512Checksum::calculate_from(data);
803        assert_eq!(digest, checksum.inner());
804        assert_eq!(format!("{}", &checksum), hex_digest,);
805
806        let checksum = Blake2b512Checksum::from_str(hex_digest).unwrap();
807        assert_eq!(digest, checksum.inner());
808        assert_eq!(format!("{}", &checksum), hex_digest,);
809    }
810
811    #[rstest]
812    fn checksum_sha1() {
813        let data = "foo\n";
814        let digest = vec![
815            241, 210, 210, 249, 36, 233, 134, 172, 134, 253, 247, 179, 108, 148, 188, 223, 50, 190,
816            236, 21,
817        ];
818        let hex_digest = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15";
819
820        let checksum = Sha1Checksum::calculate_from(data);
821        assert_eq!(digest, checksum.inner());
822        assert_eq!(format!("{}", &checksum), hex_digest,);
823
824        let checksum = Sha1Checksum::from_str(hex_digest).unwrap();
825        assert_eq!(digest, checksum.inner());
826        assert_eq!(format!("{}", &checksum), hex_digest,);
827    }
828
829    #[rstest]
830    fn checksum_sha224() {
831        let data = "foo\n";
832        let digest = vec![
833            231, 213, 227, 110, 141, 71, 12, 62, 81, 3, 254, 221, 46, 79, 42, 165, 195, 10, 178,
834            127, 102, 41, 189, 195, 40, 111, 157, 210,
835        ];
836        let hex_digest = "e7d5e36e8d470c3e5103fedd2e4f2aa5c30ab27f6629bdc3286f9dd2";
837
838        let checksum = Sha224Checksum::calculate_from(data);
839        assert_eq!(digest, checksum.inner());
840        assert_eq!(format!("{}", &checksum), hex_digest,);
841
842        let checksum = Sha224Checksum::from_str(hex_digest).unwrap();
843        assert_eq!(digest, checksum.inner());
844        assert_eq!(format!("{}", &checksum), hex_digest,);
845    }
846
847    #[rstest]
848    fn checksum_sha256() {
849        let data = "foo\n";
850        let digest = vec![
851            181, 187, 157, 128, 20, 160, 249, 177, 214, 30, 33, 231, 150, 215, 141, 204, 223, 19,
852            82, 242, 60, 211, 40, 18, 244, 133, 11, 135, 138, 228, 148, 76,
853        ];
854        let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
855
856        let checksum = Sha256Checksum::calculate_from(data);
857        assert_eq!(digest, checksum.inner());
858        assert_eq!(format!("{}", &checksum), hex_digest,);
859
860        let checksum = Sha256Checksum::from_str(hex_digest).unwrap();
861        assert_eq!(digest, checksum.inner());
862        assert_eq!(format!("{}", &checksum), hex_digest,);
863    }
864
865    #[rstest]
866    fn checksum_sha384() {
867        let data = "foo\n";
868        let digest = vec![
869            142, 255, 218, 191, 225, 68, 22, 33, 74, 37, 15, 147, 85, 5, 37, 11, 217, 145, 241, 6,
870            6, 93, 137, 157, 182, 225, 155, 220, 139, 246, 72, 243, 172, 15, 25, 53, 196, 246, 95,
871            232, 247, 152, 40, 155, 26, 13, 30, 6,
872        ];
873        let hex_digest = "8effdabfe14416214a250f935505250bd991f106065d899db6e19bdc8bf648f3ac0f1935c4f65fe8f798289b1a0d1e06";
874
875        let checksum = Sha384Checksum::calculate_from(data);
876        assert_eq!(digest, checksum.inner());
877        assert_eq!(format!("{}", &checksum), hex_digest,);
878
879        let checksum = Sha384Checksum::from_str(hex_digest).unwrap();
880        assert_eq!(digest, checksum.inner());
881        assert_eq!(format!("{}", &checksum), hex_digest,);
882    }
883
884    #[rstest]
885    fn checksum_sha512() {
886        let data = "foo\n";
887        let digest = vec![
888            12, 249, 24, 10, 118, 74, 186, 134, 58, 103, 182, 215, 47, 9, 24, 188, 19, 28, 103,
889            114, 100, 44, 178, 220, 229, 163, 79, 10, 112, 47, 148, 112, 221, 194, 191, 18, 92, 18,
890            25, 139, 25, 149, 194, 51, 195, 75, 74, 253, 52, 108, 84, 162, 51, 76, 53, 10, 148,
891            138, 81, 182, 232, 180, 230, 182,
892        ];
893        let hex_digest = "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6";
894
895        let checksum = Sha512Checksum::calculate_from(data);
896        assert_eq!(digest, checksum.inner());
897        assert_eq!(format!("{}", &checksum), hex_digest);
898
899        let checksum = Sha512Checksum::from_str(hex_digest).unwrap();
900        assert_eq!(digest, checksum.inner());
901        assert_eq!(format!("{}", &checksum), hex_digest);
902    }
903
904    #[rstest]
905    fn checksum_crc32cksum() {
906        let data = "foo\n";
907        let digest = 3915528286u32;
908        let digest_string = format!("{digest}");
909
910        let checksum = Crc32CksumChecksum::calculate_from(data);
911        assert_eq!(digest.to_be_bytes(), checksum.inner());
912        assert_eq!(format!("{}", &checksum), digest_string);
913
914        let checksum = Crc32CksumChecksum::from_str(digest_string.as_str()).unwrap();
915        assert_eq!(digest.to_be_bytes(), checksum.inner());
916        assert_eq!(format!("{}", &checksum), digest_string);
917    }
918
919    #[rstest]
920    #[case::non_hex_digits(
921        "0cf9180a764aba863a67b6d72f0918bc13gggggg642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6"
922    )]
923    #[case::incomplete_pair(" b ")]
924    #[case::incomplete_digest("0cf9180a764aba863a67b6d72f0918bca")]
925    #[case::whitespace(
926        "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"
927    )]
928    fn checksum_parse_error(#[case] input: &str) {
929        let Err(Error::ParseError(err_msg)) = Sha512Checksum::from_str(input) else {
930            panic!("'{input}' erroneously parsed as Sha512Checksum")
931        };
932
933        let (test_name, _guard) = configure_insta();
934        assert_snapshot!(test_name, err_msg.to_string());
935    }
936
937    #[rstest]
938    fn skippable_checksum_sha256() {
939        let hex_digest = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
940        let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
941        assert_eq!(format!("{}", &checksum), hex_digest);
942    }
943
944    #[rstest]
945    fn skippable_checksum_skip() {
946        let hex_digest = "SKIP";
947        let checksum = SkippableChecksum::<Sha256>::from_str(hex_digest).unwrap();
948
949        assert_eq!(SkippableChecksum::Skip, checksum);
950        assert_eq!(format!("{}", &checksum), hex_digest);
951    }
952}