Skip to main content

rust_par2/
slice.rs

1//! Per-slice checksums for in-flight verification.
2//!
3//! The IFSC packet stores an MD5 and a CRC32 for every input slice, computed
4//! over the slice zero-padded to the full slice size. [`SliceHasher`] produces
5//! the same pair from bytes in memory, so a caller can check a slice before
6//! feeding it to a [`Repair`](crate::repair::Repair) builder: a corrupt slice
7//! that is fed poisons every recovered slice and cannot be taken back out.
8
9use md5::{Digest, Md5};
10
11use crate::types::{Par2File, SliceChecksum};
12
13/// Incremental MD5 + CRC32 over one input slice, zero-padded to `slice_size`.
14///
15/// ```
16/// use rust_par2::slice::SliceHasher;
17///
18/// let mut h = SliceHasher::new(8);
19/// h.update(b"abc");
20/// h.update(b"de");
21/// let sum = h.finish(); // checksums of b"abcde\0\0\0"
22/// assert_eq!(sum, SliceHasher::checksum(b"abcde", 8));
23/// ```
24pub struct SliceHasher {
25    md5: Md5,
26    crc: crc32fast::Hasher,
27    slice_size: u64,
28    len: u64,
29}
30
31impl SliceHasher {
32    /// Start hashing a slice of a set whose slice size is `slice_size`.
33    pub fn new(slice_size: u64) -> Self {
34        Self {
35            md5: Md5::new(),
36            crc: crc32fast::Hasher::new(),
37            slice_size,
38            len: 0,
39        }
40    }
41
42    /// Feed the next bytes of the slice.
43    ///
44    /// Bytes beyond `slice_size` are hashed too, so the result will not match
45    /// any IFSC entry; callers should not pass more than one slice.
46    pub fn update(&mut self, data: &[u8]) {
47        self.md5.update(data);
48        self.crc.update(data);
49        self.len += data.len() as u64;
50    }
51
52    /// Bytes fed so far, before padding.
53    pub fn len(&self) -> u64 {
54        self.len
55    }
56
57    /// Whether no bytes have been fed yet.
58    pub fn is_empty(&self) -> bool {
59        self.len == 0
60    }
61
62    /// Zero-pad to `slice_size` and return the checksum pair.
63    pub fn finish(mut self) -> SliceChecksum {
64        const ZEROS: [u8; 4096] = [0; 4096];
65        let mut pad = self.slice_size.saturating_sub(self.len);
66        while pad > 0 {
67            let n = pad.min(ZEROS.len() as u64) as usize;
68            self.md5.update(&ZEROS[..n]);
69            self.crc.update(&ZEROS[..n]);
70            pad -= n as u64;
71        }
72        SliceChecksum {
73            md5: self.md5.finalize().into(),
74            crc32: self.crc.finalize(),
75        }
76    }
77
78    /// Checksum a whole slice in one call.
79    pub fn checksum(data: &[u8], slice_size: u64) -> SliceChecksum {
80        let mut h = Self::new(slice_size);
81        h.update(data);
82        h.finish()
83    }
84}
85
86impl Par2File {
87    /// Whether `data` matches the IFSC checksum of this file's slice
88    /// `local_index` (0-based within the file).
89    ///
90    /// `data` is the slice as it appears in the file: `slice_size` bytes, or
91    /// fewer for the file's last slice. Returns `false` if the index is out of
92    /// range or the length is wrong for that slice.
93    pub fn slice_matches(&self, local_index: usize, data: &[u8], slice_size: u64) -> bool {
94        let Some(expected) = self.slices.get(local_index) else {
95            return false;
96        };
97        let offset = local_index as u64 * slice_size;
98        let expected_len = self.size.saturating_sub(offset).min(slice_size);
99        if data.len() as u64 != expected_len {
100            return false;
101        }
102        SliceHasher::checksum(data, slice_size) == *expected
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn padding_matches_explicit_zeroes() {
112        let data = vec![0xABu8; 10_000];
113        let mut padded = data.clone();
114        padded.resize(16_384, 0);
115        let a = SliceHasher::checksum(&data, 16_384);
116        let b = SliceHasher::checksum(&padded, 16_384);
117        assert_eq!(a, b);
118        assert_eq!(a.crc32, crc32fast::hash(&padded));
119        let md5: [u8; 16] = Md5::digest(&padded).into();
120        assert_eq!(a.md5, md5);
121    }
122
123    #[test]
124    fn incremental_matches_one_shot() {
125        let data: Vec<u8> = (0..5000u32).map(|i| (i * 7) as u8).collect();
126        let mut h = SliceHasher::new(8192);
127        for chunk in data.chunks(333) {
128            h.update(chunk);
129        }
130        assert_eq!(h.len(), 5000);
131        assert_eq!(h.finish(), SliceHasher::checksum(&data, 8192));
132    }
133}