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