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}