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    // TODO(cleanup): Investigate the arithmetic_side_effects
59    #[expect(clippy::expect_used, clippy::arithmetic_side_effects)]
60    fn update(&mut self, data: &[u8]) {
61        self.digest.update(data);
62        self.len += u64::try_from(data.len())
63            .expect("the number of bytes in the input slice fit into a u64");
64    }
65}
66
67impl OutputSizeUser for Crc32Cksum {
68    type OutputSize = digest::consts::U4;
69}
70
71impl FixedOutput for Crc32Cksum {
72    fn finalize_into(mut self, out: &mut Output<Self>) {
73        // Feed the length of the input as octets into the digest, with its least significant octet
74        // first. The smallest amount of non-zero octets is to be used.
75        // Apparently this is used to differentiate between some outputs that would otherwise result
76        // in the same checksum.
77        //
78        // See the `cksum` specification for details, specifically the end of the `1.` paragraph:
79        // https://man.archlinux.org/man/cksum.1p#DESCRIPTION
80        let mut len = self.len;
81        while len != 0 {
82            self.digest.update(&[len as u8]);
83            len >>= 8;
84        }
85
86        let crc = self.digest.finalize();
87        out.deref_mut().clone_from_slice(&crc.to_be_bytes());
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use std::{
94        io::Write,
95        process::{Command, Stdio},
96    };
97
98    use rstest::rstest;
99    use testresult::TestResult;
100    use which::which;
101
102    use crate::Crc32CksumChecksum;
103
104    /// Ensures that our [`Crc32CksumChecksum`] implementation produces the same digests as the
105    /// `cksum` binary from coreutils.
106    #[rstest]
107    #[case::empty(Vec::new())]
108    #[case::single_byte(b"a".to_vec())]
109    #[case::utf8("ÄÖÜ äöü ß 🦆:3".as_bytes().to_vec())]
110    #[case::really_long_input(vec![b'a'; 5000])]
111    fn checksum_matches_cksum(#[case] data: Vec<u8>) -> TestResult {
112        let cksum = which("cksum").unwrap_or_else(|_| {
113            panic!("cksum: command not found");
114        });
115
116        let output = {
117            let mut child = Command::new(cksum)
118                .stdin(Stdio::piped())
119                .stdout(Stdio::piped())
120                .spawn()?;
121
122            // Send the input to the `cksum`'s stdin.
123            child
124                .stdin
125                .take()
126                .expect("the stdin of cksum to be piped")
127                .write_all(&data)?;
128
129            child.wait_with_output()?
130        };
131        assert!(
132            output.status.success(),
133            "cksum exited with {}",
134            output.status
135        );
136
137        // `cksum` returns "{digest} {input_bytes}" when reading from stdin.
138        // We simply split by space to get the digest
139        let stdout = String::from_utf8_lossy(&output.stdout);
140        let digest: &str = stdout
141            .split_whitespace()
142            .next()
143            .expect("cksum to print a digest");
144
145        // Make sure both implementations create the same checksum.
146        let checksum = Crc32CksumChecksum::calculate_from(&data);
147        assert_eq!(&format!("{checksum}"), digest);
148
149        Ok(())
150    }
151}