Skip to main content

alpm_types/checksum/
crc32.rs

1//! A `cksum` compatible CRC-32 Hasher.
2//!
3//! The actual CRC calculation is done by the [crc] crate. [`Crc32Cksum`] adds special handling used
4//! by `cksum`. On top of it, this module provides all traits necessary for `Digest`, which is
5//! needed for compatibility with the other hashing algorithms.
6//!
7//! `makepkg` still supports `cksum`'s as a legacy checksum algorithm, which we sadly have to
8//! support for backwards compatibility. In practice, nobody except a few packages in the AUR use
9//! this any longer.
10
11use std::{fmt::Formatter, ops::DerefMut};
12
13use crc::{CRC_32_CKSUM, Crc, Digest};
14use digest::{FixedOutput, HashMarker, Output, OutputSizeUser, Update};
15
16/// The CRC-32/CKSUM algorithm
17static CRC: Crc<u32> = Crc::<u32>::new(&CRC_32_CKSUM);
18
19/// A [`cksum`] compatible CRC-32 hasher.
20///
21/// This tracks the length of the input data and appends it to the checksum calculation, just like
22/// the Unix `cksum` utility does.
23///
24/// [`cksum`]: https://man.archlinux.org/man/cksum.1
25#[derive(Clone)]
26pub struct Crc32Cksum {
27    /// The ongoing CRC-32/CKSUM calculation.
28    digest: Digest<'static, u32>,
29    /// The number of bytes that have been fed into `digest` so far.
30    len: u64,
31}
32
33impl Default for Crc32Cksum {
34    fn default() -> Self {
35        Self {
36            digest: CRC.digest(),
37            len: 0,
38        }
39    }
40}
41
42impl std::fmt::Debug for Crc32Cksum {
43    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
44        f.debug_struct("Crc32Cksum")
45            .field("len", &self.len)
46            .finish()
47    }
48}
49
50impl HashMarker for Crc32Cksum {}
51
52impl Update for Crc32Cksum {
53    /// Update the digest with a new betch of bytes.
54    ///
55    /// # Panics
56    ///
57    /// Panics if the input data exceeds ~18.44 exabytes on systems with `usize > 64bits`.
58    fn update(&mut self, data: &[u8]) {
59        self.digest.update(data);
60        self.len += u64::try_from(data.len())
61            .expect("the number of bytes in the input slice fit into a u64");
62    }
63}
64
65impl OutputSizeUser for Crc32Cksum {
66    type OutputSize = digest::consts::U4;
67}
68
69impl FixedOutput for Crc32Cksum {
70    fn finalize_into(mut self, out: &mut Output<Self>) {
71        // Feed the length of the input as octets into the digest, with its least significant octet
72        // first. The smallest amount of non-zero octets is to be used.
73        // Apparently this is used to differentiate between some outputs that would otherwise result
74        // in the same checksum.
75        //
76        // See the `cksum` specification for details, specifically the end of the `1.` paragraph:
77        // https://man.archlinux.org/man/cksum.1p#DESCRIPTION
78        let mut len = self.len;
79        while len != 0 {
80            self.digest.update(&[len as u8]);
81            len >>= 8;
82        }
83
84        let crc = self.digest.finalize();
85        out.deref_mut().clone_from_slice(&crc.to_be_bytes());
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use std::{
92        io::Write,
93        process::{Command, Stdio},
94    };
95
96    use rstest::rstest;
97    use testresult::TestResult;
98    use which::which;
99
100    use crate::Crc32CksumChecksum;
101
102    /// Ensures that our [`Crc32CksumChecksum`] implementation produces the same digests as the
103    /// `cksum` binary from coreutils.
104    #[rstest]
105    #[case::empty(Vec::new())]
106    #[case::single_byte(b"a".to_vec())]
107    #[case::utf8("ÄÖÜ äöü ß 🦆:3".as_bytes().to_vec())]
108    #[case::really_long_input(vec![b'a'; 5000])]
109    fn checksum_matches_cksum(#[case] data: Vec<u8>) -> TestResult {
110        let cksum = which("cksum").unwrap_or_else(|_| {
111            panic!("cksum: command not found");
112        });
113
114        let output = {
115            let mut child = Command::new(cksum)
116                .stdin(Stdio::piped())
117                .stdout(Stdio::piped())
118                .spawn()?;
119
120            // Send the input to the `cksum`'s stdin.
121            child
122                .stdin
123                .take()
124                .expect("the stdin of cksum to be piped")
125                .write_all(&data)?;
126
127            child.wait_with_output()?
128        };
129        assert!(
130            output.status.success(),
131            "cksum exited with {}",
132            output.status
133        );
134
135        // `cksum` returns "{digest} {input_bytes}" when reading from stdin.
136        // We simply split by space to get the digest
137        let stdout = String::from_utf8_lossy(&output.stdout);
138        let digest: &str = stdout
139            .split_whitespace()
140            .next()
141            .expect("cksum to print a digest");
142
143        // Make sure both implementations create the same checksum.
144        let checksum = Crc32CksumChecksum::calculate_from(&data);
145        assert_eq!(&format!("{checksum}"), digest);
146
147        Ok(())
148    }
149}