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}