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#[derive(Clone)]
31pub(crate) struct Crc32Hasher {
32 inner: FastCrcDigest,
33 accel: Option<u32>,
35 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 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 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#[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#[derive(Clone)]
198pub struct SliceChecksumState {
199 crc32: Crc32Hasher,
200 md5: Md5State,
201 bytes_fed: u64,
202}
203
204impl SliceChecksumState {
205 pub fn new() -> Self {
207 Self {
208 crc32: Crc32Hasher::new(),
209 md5: Md5State::new(),
210 bytes_fed: 0,
211 }
212 }
213
214 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 pub fn bytes_fed(&self) -> u64 {
223 self.bytes_fed
224 }
225
226 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#[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
289const 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#[derive(Debug, Clone)]
352pub struct Crc32UncombineOp {
353 inverse: Option<[u32; 32]>,
354}
355
356impl Crc32UncombineOp {
357 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 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
400fn crc32_mat_invert(mat: &[u32; 32]) -> Option<[u32; 32]> {
402 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 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
448pub fn crc32_uncombine(combined: u32, crc2: u32, len2: u64) -> u32 {
450 Crc32UncombineOp::new(len2).uncombine(combined, crc2)
451}
452
453pub fn crc32(data: &[u8]) -> u32 {
455 let mut hasher = Crc32Hasher::new();
456 hasher.update(data);
457 hasher.finalize()
458}
459
460pub(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
473pub 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 #[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 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 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 #[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 #[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 #[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 #[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 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}