Skip to main content

par2_rs/
checksum.rs

1use crate::crc_simd;
2#[cfg(feature = "crypto-aws-lc")]
3use aws_lc_sys::{MD5_CTX, MD5_Final, MD5_Init, MD5_Update};
4use crc_fast::{CrcAlgorithm, Digest as FastCrcDigest};
5#[cfg(any(feature = "crypto-rust", target_family = "wasm"))]
6use md5::{Digest as Md5Digest, Md5 as RustCryptoMd5};
7#[cfg(feature = "crypto-aws-lc")]
8use std::mem::MaybeUninit;
9
10const ZERO_PAD_CHUNK: [u8; 8192] = [0u8; 8192];
11
12/// Streaming CRC-32/ISO-HDLC.
13///
14/// Wraps [`crc_fast::Digest`], with one detour: on hosts where
15/// [`crate::crc_simd`] has a tier `crc-fast` does not (VPCLMULQDQ without
16/// AVX-512VL — see that module for why the hole exists), updates at or above
17/// [`crc_simd::MIN_UPDATE`] are folded by the local kernel instead.
18///
19/// While the kernel is carrying the stream the authoritative value is the plain
20/// `u32` in `accel` — which is in the finalized (post-xor) domain — and `inner`
21/// is stale. `inner` is re-seeded from `accel` only when a below-threshold
22/// update arrives. PAR2 slices are hundreds of kilobytes, so a slice pass
23/// normally touches the digest exactly once, at construction.
24///
25/// Because `accel` is not a digest, [`crc_fast::Digest::get_amount`] and
26/// [`crc_fast::Digest::combine`] would see a byte counter that stopped
27/// advancing the moment the kernel engaged. Neither is surfaced through this
28/// wrapper, and neither may be added without tracking the folded byte count
29/// here first. [`Crc32CombineOp`] is unaffected — it takes an explicit `len2`.
30#[derive(Clone)]
31pub(crate) struct Crc32Hasher {
32    inner: FastCrcDigest,
33    /// Carried CRC in the finalized domain. `Some` means `inner` is stale.
34    accel: Option<u32>,
35    /// Resolved once per hasher rather than per update, so the hot path is a
36    /// register test and not a `OnceLock` load. Always `false` on targets and
37    /// hosts with no tier, where it constant-folds the branch away entirely.
38    use_accel: bool,
39}
40
41impl Crc32Hasher {
42    pub(crate) fn new() -> Self {
43        Self {
44            inner: FastCrcDigest::new(CrcAlgorithm::Crc32IsoHdlc),
45            accel: None,
46            use_accel: crc_simd::available(),
47        }
48    }
49
50    pub(crate) fn update(&mut self, data: &[u8]) {
51        if self.use_accel && data.len() >= crc_simd::MIN_UPDATE {
52            // Both the kernel's input and its output are the finalized domain,
53            // so consecutive folded updates just carry a `u32`.
54            let initial = match self.accel {
55                Some(crc) => crc,
56                None => self.inner.finalize() as u32,
57            };
58            self.accel = Some(crc_simd::update(initial, data));
59            return;
60        }
61
62        // Leaving the folding path: materialize the carried value back into the
63        // resident digest exactly once, not once per update.
64        if let Some(crc) = self.accel.take() {
65            self.inner =
66                FastCrcDigest::new_with_init_state(CrcAlgorithm::Crc32IsoHdlc, u64::from(!crc));
67        }
68
69        self.inner.update(data);
70    }
71
72    pub(crate) fn finalize(self) -> u32 {
73        match self.accel {
74            Some(crc) => crc,
75            None => self.inner.finalize() as u32,
76        }
77    }
78}
79
80// With both backends compiled, only AWS-LC is ever selected outside the tests,
81// so the RustCrypto arm is dead in a non-test build of that configuration.
82#[cfg_attr(
83    all(feature = "crypto-aws-lc", feature = "crypto-rust"),
84    allow(dead_code)
85)]
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87enum Par2Md5Backend {
88    #[cfg(any(feature = "crypto-rust", target_family = "wasm"))]
89    RustCrypto,
90    #[cfg(feature = "crypto-aws-lc")]
91    NativeAwsLc,
92}
93
94const fn default_md5_backend() -> Par2Md5Backend {
95    #[cfg(feature = "crypto-aws-lc")]
96    {
97        Par2Md5Backend::NativeAwsLc
98    }
99    #[cfg(not(feature = "crypto-aws-lc"))]
100    {
101        Par2Md5Backend::RustCrypto
102    }
103}
104
105#[cfg(feature = "crypto-aws-lc")]
106#[derive(Clone)]
107struct AwsLcMd5State {
108    ctx: MD5_CTX,
109}
110
111#[cfg(feature = "crypto-aws-lc")]
112impl AwsLcMd5State {
113    fn new() -> Self {
114        let mut ctx = MaybeUninit::<MD5_CTX>::uninit();
115        let result = unsafe { MD5_Init(ctx.as_mut_ptr()) };
116        assert_eq!(result, 1, "aws-lc MD5_Init must succeed");
117        Self {
118            ctx: unsafe { ctx.assume_init() },
119        }
120    }
121
122    fn update(&mut self, data: &[u8]) {
123        let result = unsafe { MD5_Update(&mut self.ctx, data.as_ptr().cast(), data.len()) };
124        assert_eq!(result, 1, "aws-lc MD5_Update must succeed");
125    }
126
127    fn finalize(mut self) -> [u8; 16] {
128        let mut out = [0u8; 16];
129        let result = unsafe { MD5_Final(out.as_mut_ptr(), &mut self.ctx) };
130        assert_eq!(result, 1, "aws-lc MD5_Final must succeed");
131        out
132    }
133}
134
135#[derive(Clone)]
136enum Md5StateInner {
137    #[cfg(any(feature = "crypto-rust", target_family = "wasm"))]
138    RustCrypto(RustCryptoMd5),
139    #[cfg(feature = "crypto-aws-lc")]
140    NativeAwsLc(AwsLcMd5State),
141}
142
143#[derive(Clone)]
144pub(crate) struct Md5State {
145    inner: Md5StateInner,
146}
147
148impl Md5State {
149    pub(crate) fn new() -> Self {
150        Self::new_with_backend(default_md5_backend())
151    }
152
153    fn new_with_backend(backend: Par2Md5Backend) -> Self {
154        let inner = match backend {
155            #[cfg(any(feature = "crypto-rust", target_family = "wasm"))]
156            Par2Md5Backend::RustCrypto => Md5StateInner::RustCrypto(RustCryptoMd5::new()),
157            #[cfg(feature = "crypto-aws-lc")]
158            Par2Md5Backend::NativeAwsLc => Md5StateInner::NativeAwsLc(AwsLcMd5State::new()),
159        };
160        Self { inner }
161    }
162
163    pub(crate) fn update(&mut self, data: &[u8]) {
164        match &mut self.inner {
165            #[cfg(any(feature = "crypto-rust", target_family = "wasm"))]
166            Md5StateInner::RustCrypto(state) => state.update(data),
167            #[cfg(feature = "crypto-aws-lc")]
168            Md5StateInner::NativeAwsLc(state) => state.update(data),
169        }
170    }
171
172    pub(crate) fn finalize(self) -> [u8; 16] {
173        match self.inner {
174            #[cfg(any(feature = "crypto-rust", target_family = "wasm"))]
175            Md5StateInner::RustCrypto(state) => state.finalize().into(),
176            #[cfg(feature = "crypto-aws-lc")]
177            Md5StateInner::NativeAwsLc(state) => state.finalize(),
178        }
179    }
180}
181
182fn md5_with_backend(backend: Par2Md5Backend, data: &[u8]) -> [u8; 16] {
183    let mut hasher = Md5State::new_with_backend(backend);
184    hasher.update(data);
185    hasher.finalize()
186}
187
188/// Streaming CRC32 + MD5 checksum state for a single file slice.
189///
190/// Feeds data incrementally and produces the final (CRC32, MD5) pair that can
191/// be compared against PAR2 IFSC checksum entries.
192///
193/// For the last slice of a file (which may be shorter than slice_size), the
194/// PAR2 spec requires the remaining bytes to be zero-padded for checksum
195/// computation. Call [`finalize`](SliceChecksumState::finalize) with the
196/// `pad_to` parameter to handle this.
197#[derive(Clone)]
198pub struct SliceChecksumState {
199    crc32: Crc32Hasher,
200    md5: Md5State,
201    bytes_fed: u64,
202}
203
204impl SliceChecksumState {
205    /// Create a new checksum state.
206    pub fn new() -> Self {
207        Self {
208            crc32: Crc32Hasher::new(),
209            md5: Md5State::new(),
210            bytes_fed: 0,
211        }
212    }
213
214    /// Feed data into the checksum accumulators.
215    pub fn update(&mut self, data: &[u8]) {
216        self.crc32.update(data);
217        self.md5.update(data);
218        self.bytes_fed += data.len() as u64;
219    }
220
221    /// How many bytes have been fed so far.
222    pub fn bytes_fed(&self) -> u64 {
223        self.bytes_fed
224    }
225
226    /// Finalize and return (CRC32, MD5).
227    ///
228    /// If `pad_to` is specified and greater than `bytes_fed`, zero bytes are
229    /// fed to reach that length (for the last slice of a file).
230    pub fn finalize(mut self, pad_to: Option<u64>) -> (u32, [u8; 16]) {
231        if let Some(target) = pad_to
232            && target > self.bytes_fed
233        {
234            let mut remaining = target - self.bytes_fed;
235            while remaining > 0 {
236                let take = remaining.min(ZERO_PAD_CHUNK.len() as u64) as usize;
237                self.crc32.update(&ZERO_PAD_CHUNK[..take]);
238                self.md5.update(&ZERO_PAD_CHUNK[..take]);
239                remaining -= take as u64;
240            }
241        }
242        let crc = self.crc32.finalize();
243        let md5 = self.md5.finalize();
244        (crc, md5)
245    }
246}
247
248impl Default for SliceChecksumState {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254/// Streaming full-file MD5 hash state.
255#[derive(Clone)]
256pub struct FileHashState {
257    md5: Md5State,
258    bytes_fed: u64,
259}
260
261impl FileHashState {
262    pub fn new() -> Self {
263        Self {
264            md5: Md5State::new(),
265            bytes_fed: 0,
266        }
267    }
268
269    pub fn update(&mut self, data: &[u8]) {
270        self.md5.update(data);
271        self.bytes_fed += data.len() as u64;
272    }
273
274    pub fn bytes_fed(&self) -> u64 {
275        self.bytes_fed
276    }
277
278    pub fn finalize(self) -> [u8; 16] {
279        self.md5.finalize()
280    }
281}
282
283impl Default for FileHashState {
284    fn default() -> Self {
285        Self::new()
286    }
287}
288
289/// Combine two CRC32 values as if the underlying data were concatenated.
290const CRC32_COMBINE_POLY: u32 = 0xEDB8_8320;
291
292#[derive(Debug, Clone)]
293pub struct Crc32CombineOp {
294    op: Option<[u32; 32]>,
295}
296
297impl Crc32CombineOp {
298    pub fn new(len2: u64) -> Self {
299        if len2 == 0 {
300            return Self { op: None };
301        }
302
303        let mut op = [0u32; 32];
304        op[0] = CRC32_COMBINE_POLY;
305        for (n, item) in op.iter_mut().enumerate().skip(1) {
306            *item = 1 << (n - 1);
307        }
308
309        let mut tmp = [0u32; 32];
310        for _ in 0..3 {
311            crc32_mat_square(&mut tmp, &op);
312            op = tmp;
313        }
314
315        let mut remaining = len2;
316        let mut combined = [0u32; 32];
317        for (n, item) in combined.iter_mut().enumerate() {
318            *item = 1 << n;
319        }
320        while remaining > 0 {
321            if remaining & 1 != 0 {
322                let previous = combined;
323                for n in 0..32 {
324                    combined[n] = crc32_mat_vec(&op, previous[n]);
325                }
326            }
327            remaining >>= 1;
328            if remaining > 0 {
329                crc32_mat_square(&mut tmp, &op);
330                op = tmp;
331            }
332        }
333
334        Self { op: Some(combined) }
335    }
336
337    pub fn combine(&self, crc1: u32, crc2: u32) -> u32 {
338        match &self.op {
339            Some(op) => crc32_mat_vec(op, crc1) ^ crc2,
340            None => crc1,
341        }
342    }
343}
344
345/// Reverses a CRC32 concatenation operation for a fixed suffix length.
346///
347/// Given the CRC32 of `prefix || suffix` and the CRC32 of `suffix`, this
348/// operator recovers the CRC32 of `prefix`. This is useful for PAR2's final
349/// input slice, whose IFSC CRC includes zero-padding that is not part of the
350/// file itself.
351#[derive(Debug, Clone)]
352pub struct Crc32UncombineOp {
353    inverse: Option<[u32; 32]>,
354}
355
356impl Crc32UncombineOp {
357    /// Create an inverse operator for a suffix with `len2` bytes.
358    pub fn new(len2: u64) -> Self {
359        let inverse = Crc32CombineOp::new(len2)
360            .op
361            .as_ref()
362            .and_then(crc32_mat_invert);
363        Self { inverse }
364    }
365
366    /// Recover the CRC32 before a suffix from the concatenated CRC32.
367    ///
368    /// `combined` must be the CRC32 of the concatenated data and `crc2` the
369    /// CRC32 of its `len2`-byte suffix. For a zero-byte suffix this returns
370    /// `combined` unchanged.
371    pub fn uncombine(&self, combined: u32, crc2: u32) -> u32 {
372        match &self.inverse {
373            Some(inverse) => crc32_mat_vec(inverse, combined ^ crc2),
374            None => combined,
375        }
376    }
377}
378
379#[inline]
380fn crc32_mat_vec(mat: &[u32; 32], vec: u32) -> u32 {
381    let mut result = 0u32;
382    let mut v = vec;
383    let mut i = 0;
384    while v != 0 {
385        if v & 1 != 0 {
386            result ^= mat[i];
387        }
388        v >>= 1;
389        i += 1;
390    }
391    result
392}
393
394fn crc32_mat_square(square: &mut [u32; 32], mat: &[u32; 32]) {
395    for n in 0..32 {
396        square[n] = crc32_mat_vec(mat, mat[n]);
397    }
398}
399
400/// Invert a GF(2) matrix stored as the output vector for each input bit.
401fn crc32_mat_invert(mat: &[u32; 32]) -> Option<[u32; 32]> {
402    // Gaussian elimination is more convenient on rows, while the CRC helper
403    // above stores columns. Transpose into rows first.
404    let mut left = [0u32; 32];
405    for (column, &column_bits) in mat.iter().enumerate() {
406        for (row, row_bits) in left.iter_mut().enumerate() {
407            if column_bits & (1 << row) != 0 {
408                *row_bits |= 1 << column;
409            }
410        }
411    }
412    let mut right = [0u32; 32];
413    for (row, item) in right.iter_mut().enumerate() {
414        *item = 1 << row;
415    }
416
417    for column in 0..32 {
418        let pivot = (column..32).find(|&row| left[row] & (1 << column) != 0)?;
419        left.swap(column, pivot);
420        right.swap(column, pivot);
421
422        for row in 0..32 {
423            if row != column && left[row] & (1 << column) != 0 {
424                left[row] ^= left[column];
425                right[row] ^= right[column];
426            }
427        }
428    }
429
430    debug_assert!(left.iter().enumerate().all(|(row, &bits)| bits == 1 << row));
431
432    // Transpose the inverse back to the CRC helper's column representation.
433    let mut inverse = [0u32; 32];
434    for (input, inverse_column) in inverse.iter_mut().enumerate() {
435        for (output, &row_bits) in right.iter().enumerate() {
436            if row_bits & (1 << input) != 0 {
437                *inverse_column |= 1 << output;
438            }
439        }
440    }
441    Some(inverse)
442}
443
444pub fn crc32_combine(crc1: u32, crc2: u32, len2: u64) -> u32 {
445    Crc32CombineOp::new(len2).combine(crc1, crc2)
446}
447
448/// Undo [`crc32_combine`] for a known suffix CRC and length.
449pub fn crc32_uncombine(combined: u32, crc2: u32, len2: u64) -> u32 {
450    Crc32UncombineOp::new(len2).uncombine(combined, crc2)
451}
452
453/// Compute CRC32 of a byte slice.
454pub fn crc32(data: &[u8]) -> u32 {
455    let mut hasher = Crc32Hasher::new();
456    hasher.update(data);
457    hasher.finalize()
458}
459
460/// Compute CRC32 of a byte slice, zero-padding to `pad_to` bytes.
461pub(crate) fn crc32_padded(data: &[u8], pad_to: u64) -> u32 {
462    let mut hasher = Crc32Hasher::new();
463    hasher.update(data);
464    let mut remaining = pad_to.saturating_sub(data.len() as u64);
465    while remaining > 0 {
466        let chunk = remaining.min(ZERO_PAD_CHUNK.len() as u64) as usize;
467        hasher.update(&ZERO_PAD_CHUNK[..chunk]);
468        remaining -= chunk as u64;
469    }
470    hasher.finalize()
471}
472
473/// Compute MD5 of a byte slice.
474pub fn md5(data: &[u8]) -> [u8; 16] {
475    md5_with_backend(default_md5_backend(), data)
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    fn hex(bytes: &[u8]) -> String {
483        bytes.iter().map(|byte| format!("{byte:02x}")).collect()
484    }
485
486    #[test]
487    fn md5_default_backend_matches_reference_vector() {
488        assert_eq!(hex(&md5(b"abc")), "900150983cd24fb0d6963f7d28e17f72");
489    }
490
491    /// The AWS-LC binding against an MD5 the build did not select: the
492    /// reference comes from the dev-dependency, so this runs on a default
493    /// build that links no RustCrypto backend at all.
494    #[cfg(feature = "crypto-aws-lc")]
495    #[test]
496    fn md5_native_backend_matches_independent_reference() {
497        use md5::Digest as _;
498        let sample = b"par2-md5-native-vs-reference";
499        let native = md5_with_backend(Par2Md5Backend::NativeAwsLc, sample);
500        let reference: [u8; 16] = md5::Md5::digest(sample).into();
501        assert_eq!(native, reference);
502    }
503
504    #[cfg(all(feature = "crypto-aws-lc", feature = "crypto-rust"))]
505    #[test]
506    fn md5_native_backend_matches_rustcrypto_backend() {
507        let sample = b"par2-md5-native-vs-rustcrypto";
508        let rustcrypto = md5_with_backend(Par2Md5Backend::RustCrypto, sample);
509        let native = md5_with_backend(Par2Md5Backend::NativeAwsLc, sample);
510        assert_eq!(native, rustcrypto);
511    }
512
513    #[test]
514    fn file_hash_state_matches_one_shot_md5() {
515        let mut state = FileHashState::new();
516        state.update(b"par2");
517        state.update(b"-state");
518        assert_eq!(state.finalize(), md5(b"par2-state"));
519    }
520
521    #[test]
522    fn crc32_combine_op_matches_one_shot_combine() {
523        let first = crc32(b"short-tail");
524        let padding = [0u8; 17];
525        let second = crc32(&padding);
526        let mut concatenated = b"short-tail".to_vec();
527        concatenated.extend_from_slice(&padding);
528
529        let op = Crc32CombineOp::new(padding.len() as u64);
530        assert_eq!(op.combine(first, second), crc32_combine(first, second, 17));
531        assert_eq!(op.combine(first, second), crc32(&concatenated));
532    }
533
534    #[test]
535    fn crc32_uncombine_recovers_randomized_prefixes() {
536        let mut seed = 0x5eed_cafe_dead_beefu64;
537
538        for _ in 0..128 {
539            let prefix_len = (next_pseudorandom(&mut seed) % 513) as usize;
540            let suffix_len = (next_pseudorandom(&mut seed) % 513) as usize;
541            let mut prefix = vec![0u8; prefix_len];
542            let mut suffix = vec![0u8; suffix_len];
543            fill_pseudorandom(&mut prefix, &mut seed);
544            fill_pseudorandom(&mut suffix, &mut seed);
545
546            let mut joined = prefix.clone();
547            joined.extend_from_slice(&suffix);
548
549            let prefix_crc = crc32(&prefix);
550            let suffix_crc = crc32(&suffix);
551            let joined_crc = crc32(&joined);
552            let uncombine = Crc32UncombineOp::new(suffix_len as u64);
553
554            assert_eq!(uncombine.uncombine(joined_crc, suffix_crc), prefix_crc);
555            assert_eq!(
556                crc32_uncombine(joined_crc, suffix_crc, suffix_len as u64),
557                prefix_crc
558            );
559        }
560    }
561
562    #[test]
563    fn crc32_uncombine_removes_randomized_zero_padding() {
564        let mut seed = 0x8bad_f00d_0123_4567u64;
565
566        for _ in 0..128 {
567            let data_len = (next_pseudorandom(&mut seed) % 1025) as usize;
568            let padding_len = (next_pseudorandom(&mut seed) % 1025) as usize;
569            let mut data = vec![0u8; data_len];
570            fill_pseudorandom(&mut data, &mut seed);
571
572            let mut padded = data.clone();
573            padded.resize(data_len + padding_len, 0);
574
575            assert_eq!(
576                crc32_uncombine(
577                    crc32(&padded),
578                    crc32(&vec![0u8; padding_len]),
579                    padding_len as u64,
580                ),
581                crc32(&data)
582            );
583        }
584    }
585
586    // =======================================================================
587    // The accelerated CRC tier's seam. Every assertion below uses
588    // `crc_fast::crc32_iso_hdlc` as the oracle rather than this module's own
589    // `crc32`, so the tier is pinned against an external implementation and
590    // never against itself.
591    // =======================================================================
592
593    /// A randomized sequence of chunk sizes (each >= 1) summing to `total`,
594    /// stressing the seam's update chaining across many boundaries.
595    fn random_splits(total: usize, seed: &mut u64) -> Vec<usize> {
596        let mut remaining = total;
597        let mut sizes = Vec::new();
598        while remaining > 0 {
599            let take = 1 + (next_pseudorandom(seed) % remaining.min(4096) as u64) as usize;
600            sizes.push(take);
601            remaining -= take;
602        }
603        sizes
604    }
605
606    /// Feed `data` through [`Crc32Hasher`] in the given `splits`.
607    fn hasher_over_splits(data: &[u8], splits: &[usize]) -> u32 {
608        let mut hasher = Crc32Hasher::new();
609        let mut offset = 0usize;
610        for &size in splits {
611            hasher.update(&data[offset..offset + size]);
612            offset += size;
613        }
614        assert_eq!(offset, data.len(), "splits must cover the whole buffer");
615        hasher.finalize()
616    }
617
618    /// The hasher over randomized chunk splits (and the all-1-byte and
619    /// whole-buffer extremes) must equal the one-shot checksum at every length.
620    #[test]
621    fn crc32_hasher_matches_one_shot_over_random_splits() {
622        let mut seed = 0x00C3_2000_ABCD_EF01u64;
623        let mut cases = 0usize;
624
625        for &len in &[
626            0usize, 1, 2, 15, 16, 17, 63, 64, 255, 256, 1023, 1024, 4095, 4096, 4097, 65_535,
627            65_536, 65_537, 1_000_003,
628        ] {
629            let mut data = vec![0u8; len];
630            fill_pseudorandom(&mut data, &mut seed);
631            let reference = crc_fast::crc32_iso_hdlc(&data);
632
633            let all_1: Vec<usize> = vec![1usize; len];
634            let random = random_splits(len, &mut seed);
635            let whole = if len == 0 { vec![] } else { vec![len] };
636
637            for (label, splits) in [("all-1", &all_1), ("random", &random), ("whole", &whole)] {
638                assert_eq!(
639                    hasher_over_splits(&data, splits),
640                    reference,
641                    "Crc32Hasher diverged from one-shot CRC: len={len}, split={label}"
642                );
643            }
644
645            cases += 1;
646        }
647
648        assert!(cases >= 10, "expected >= 10 CRC cases, ran {cases}");
649    }
650
651    /// Update sequences that bounce across [`crc_simd::MIN_UPDATE`] in both
652    /// directions, checked at *every prefix* rather than only at the end, so a
653    /// bad hand-off is attributed to the update that introduced it.
654    ///
655    /// This is the load-bearing test for the tier's interaction with the
656    /// digest: entering the fold, leaving it, re-entering it, and the exact
657    /// threshold boundary (255 vs 256). On a host with no tier it still runs
658    /// and proves the seam unchanged, so it is never a silent skip.
659    #[test]
660    fn crc32_hasher_survives_updates_straddling_the_tier_threshold() {
661        const LEN: usize = 64 * 1024;
662        let mut seed = 0x00C3_2000_5111_D001u64;
663        let mut data = vec![0u8; LEN];
664        fill_pseudorandom(&mut data, &mut seed);
665
666        let min = crc_simd::MIN_UPDATE;
667        let sequences: [&[usize]; 8] = [
668            &[1, 64, 255, 256, 300, 4096, 7],
669            &[4096, 7, 256, 1, 300, 255, 64],
670            &[256, 256, 256, 1, 1, 1, 4096],
671            &[7, 7, 7, 300, 7, 4096, 255, 256],
672            &[300, 1, 4096, 64, 256, 255, 7, 256],
673            &[4096, 4096, 1, 4096, 255, 300, 256],
674            &[8192, 8192, 8192, 1],
675            &[1, 8192, 1, 8192, 1],
676        ];
677
678        let mut prefixes = 0usize;
679        for seq in sequences {
680            assert!(
681                seq.iter().any(|&len| len >= min) && seq.iter().any(|&len| len < min),
682                "sequence {seq:?} must straddle MIN_UPDATE ({min}) to be useful"
683            );
684            let total: usize = seq.iter().sum();
685            assert!(total <= LEN, "sequence {seq:?} exceeds the fixture");
686
687            let mut hasher = Crc32Hasher::new();
688            let mut offset = 0usize;
689            for &len in seq {
690                hasher.update(&data[offset..offset + len]);
691                offset += len;
692                assert_eq!(
693                    hasher.clone().finalize(),
694                    crc_fast::crc32_iso_hdlc(&data[..offset]),
695                    "sequence {seq:?} diverged at prefix {offset}"
696                );
697                prefixes += 1;
698            }
699        }
700
701        assert!(
702            prefixes >= 50,
703            "expected >= 50 prefix checks, ran {prefixes}"
704        );
705    }
706
707    /// The public one-shot [`crc32`] takes the tier for buffers at or above the
708    /// threshold and `crc-fast` below it; both arms must agree with `crc-fast`.
709    #[test]
710    fn crc32_one_shot_matches_crc_fast_across_the_threshold() {
711        let mut seed = 0x00C3_2000_5111_D002u64;
712        let mut data = vec![0u8; 8192];
713        fill_pseudorandom(&mut data, &mut seed);
714
715        let min = crc_simd::MIN_UPDATE;
716        for len in [
717            0,
718            1,
719            min.saturating_sub(2),
720            min.saturating_sub(1),
721            min,
722            min + 1,
723            min + 2,
724            1024,
725            8192,
726        ] {
727            assert_eq!(
728                crc32(&data[..len]),
729                crc_fast::crc32_iso_hdlc(&data[..len]),
730                "len {len}"
731            );
732        }
733    }
734
735    /// The zero-padding loops in [`crc32_padded`] and
736    /// [`SliceChecksumState::finalize`] feed 8 KiB chunks, which fold through
737    /// the tier. Both must match the CRC of an explicitly padded buffer, with
738    /// the data portion sized either side of the threshold.
739    #[test]
740    fn padded_crc_paths_fold_zero_padding_through_the_tier() {
741        let mut seed = 0x00C3_2000_5111_D003u64;
742        let min = crc_simd::MIN_UPDATE as u64;
743
744        let mut cases = 0usize;
745        for data_len in [0u64, 1, min - 1, min, min + 1, 4096, 20_000] {
746            for pad_to in [data_len, data_len + 1, data_len + 8192, data_len + 20_001] {
747                let mut data = vec![0u8; data_len as usize];
748                fill_pseudorandom(&mut data, &mut seed);
749
750                let mut expanded = data.clone();
751                expanded.resize(pad_to as usize, 0);
752                let reference = crc_fast::crc32_iso_hdlc(&expanded);
753
754                assert_eq!(
755                    crc32_padded(&data, pad_to),
756                    reference,
757                    "crc32_padded: data_len {data_len} pad_to {pad_to}"
758                );
759
760                // The same padding, reached through the streaming state, fed in
761                // chunks that straddle the threshold in both directions.
762                let mut state = SliceChecksumState::new();
763                let mut offset = 0usize;
764                for chunk in [1usize, 300, 7, 4096] {
765                    let take = chunk.min(data.len() - offset);
766                    state.update(&data[offset..offset + take]);
767                    offset += take;
768                }
769                state.update(&data[offset..]);
770                let (crc, md5_digest) = state.finalize(Some(pad_to));
771                assert_eq!(
772                    crc, reference,
773                    "SliceChecksumState: data_len {data_len} pad_to {pad_to}"
774                );
775                assert_eq!(
776                    md5_digest,
777                    md5(&expanded),
778                    "SliceChecksumState MD5: data_len {data_len} pad_to {pad_to}"
779                );
780
781                cases += 1;
782            }
783        }
784
785        assert!(cases >= 28, "expected >= 28 padding cases, ran {cases}");
786    }
787
788    fn next_pseudorandom(seed: &mut u64) -> u64 {
789        *seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
790        *seed
791    }
792
793    fn fill_pseudorandom(bytes: &mut [u8], seed: &mut u64) {
794        for byte in bytes {
795            *byte = next_pseudorandom(seed) as u8;
796        }
797    }
798}