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