Skip to main content

alpm_types/
checksum.rs

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