Skip to main content

ripsync_core/
checksum.rs

1//! Checksums for the delta engine.
2//!
3//! Two layers, exactly as in the rsync algorithm:
4//!
5//! * a **weak rolling checksum** ([`RollingChecksum`]) that is cheap to compute
6//!   and `O(1)` to slide one byte along a buffer, used to *cheaply reject*
7//!   non-matching windows; and
8//! * a **strong hash** ([`strong_hash`]) — the first 16 bytes of a BLAKE3 digest —
9//!   used to *confirm* a candidate match found via the weak checksum.
10//!
11//! # Rolling checksum definition
12//!
13//! With modulus `M = 65536` and a window of length `L`:
14//!
15//! ```text
16//! a = (Σ_k          bytes[k]) mod M
17//! b = (Σ_k (L - k) * bytes[k]) mod M       // position-weighted, k = 0..L
18//! checksum = a + b * M
19//! ```
20//!
21//! Because `M = 2^16`, reducing modulo `M` is the same as masking the low 16
22//! bits, and ordinary wrapping (`mod 2^32`) arithmetic is congruent modulo `M`.
23//! We therefore accumulate with wrapping `u32` ops and mask only when reading the
24//! value out — this keeps rolling branch-free and exact.
25
26/// Modulus used by the weak checksum (`2^16`).
27const M: u32 = 1 << 16;
28
29/// Length of the strong hash prefix kept per block, in bytes.
30pub const STRONG_LEN: usize = 16;
31
32/// A 16-byte strong hash (BLAKE3 prefix).
33pub type StrongHash = [u8; STRONG_LEN];
34
35/// Strong hash of `data`: the first [`STRONG_LEN`] bytes of its BLAKE3 digest.
36#[must_use]
37pub fn strong_hash(data: &[u8]) -> StrongHash {
38    let digest = blake3::hash(data);
39    let mut out = [0u8; STRONG_LEN];
40    out.copy_from_slice(&digest.as_bytes()[..STRONG_LEN]);
41    out
42}
43
44/// Rolling weak checksum over a fixed-length window.
45///
46/// Construct from a window with [`RollingChecksum::new`], read the current value
47/// with [`RollingChecksum::value`], and slide forward one byte at a time with
48/// [`RollingChecksum::roll`].
49#[derive(Debug, Clone, Copy)]
50pub struct RollingChecksum {
51    a: u32,
52    b: u32,
53    window_len: u32,
54}
55
56impl RollingChecksum {
57    /// Compute the checksum freshly over `window`.
58    ///
59    /// The window length is fixed for the life of this value; [`roll`] keeps it
60    /// constant.
61    ///
62    /// [`roll`]: RollingChecksum::roll
63    #[must_use]
64    pub fn new(window: &[u8]) -> Self {
65        let len = window.len();
66        let window_len = u32::try_from(len).unwrap_or(u32::MAX);
67        let mut a: u32 = 0;
68        let mut b: u32 = 0;
69
70        // Scalar unrolling: process 8 bytes per iteration.
71        // This reduces loop overhead and lets the CPU schedule independent
72        // adds/muls in parallel.  For a 128 KiB window this is ~16× fewer
73        // loop iterations than the naive byte-by-byte loop.
74        let chunks = len / 8;
75        for chunk in 0..chunks {
76            let base = chunk * 8;
77            let w = window_len.wrapping_sub(base as u32);
78
79            let b0 = u32::from(window[base]);
80            let b1 = u32::from(window[base + 1]);
81            let b2 = u32::from(window[base + 2]);
82            let b3 = u32::from(window[base + 3]);
83            let b4 = u32::from(window[base + 4]);
84            let b5 = u32::from(window[base + 5]);
85            let b6 = u32::from(window[base + 6]);
86            let b7 = u32::from(window[base + 7]);
87
88            a = a.wrapping_add(
89                b0.wrapping_add(b1)
90                    .wrapping_add(b2)
91                    .wrapping_add(b3)
92                    .wrapping_add(b4)
93                    .wrapping_add(b5)
94                    .wrapping_add(b6)
95                    .wrapping_add(b7),
96            );
97
98            // b += Σ (w - i) * byte[i]  for i = 0..7
99            // Rewrite as: b += w * Σ bytes - Σ (i * byte[i])
100            let sum_bytes = b0
101                .wrapping_add(b1)
102                .wrapping_add(b2)
103                .wrapping_add(b3)
104                .wrapping_add(b4)
105                .wrapping_add(b5)
106                .wrapping_add(b6)
107                .wrapping_add(b7);
108
109            let weighted = b1
110                .wrapping_add(b2.wrapping_mul(2))
111                .wrapping_add(b3.wrapping_mul(3))
112                .wrapping_add(b4.wrapping_mul(4))
113                .wrapping_add(b5.wrapping_mul(5))
114                .wrapping_add(b6.wrapping_mul(6))
115                .wrapping_add(b7.wrapping_mul(7));
116
117            b = b.wrapping_add(w.wrapping_mul(sum_bytes).wrapping_sub(weighted));
118        }
119
120        // Handle the remaining 0–7 bytes.
121        let remainder_start = chunks * 8;
122        for i in remainder_start..len {
123            let k = i;
124            let weight = window_len.wrapping_sub(u32::try_from(k).unwrap_or(0));
125            let byte = u32::from(window[i]);
126            a = a.wrapping_add(byte);
127            b = b.wrapping_add(weight.wrapping_mul(byte));
128        }
129
130        Self { a, b, window_len }
131    }
132
133    /// Slide the window one byte to the right.
134    ///
135    /// `out_byte` is the byte leaving the window on the left; `in_byte` is the
136    /// byte entering on the right. The window length is preserved.
137    pub fn roll(&mut self, out_byte: u8, in_byte: u8) {
138        let out = u32::from(out_byte);
139        let in_ = u32::from(in_byte);
140        self.a = self.a.wrapping_sub(out).wrapping_add(in_);
141        self.b = self
142            .b
143            .wrapping_sub(self.window_len.wrapping_mul(out))
144            .wrapping_add(self.a);
145    }
146
147    /// The current checksum value, `a + b * M`, with `a` and `b` reduced mod `M`.
148    #[must_use]
149    pub fn value(&self) -> u32 {
150        (self.a & (M - 1)) | ((self.b & (M - 1)) << 16)
151    }
152
153    /// The fixed window length this checksum tracks.
154    #[must_use]
155    pub fn window_len(&self) -> u32 {
156        self.window_len
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn fresh_matches_manual() {
166        // a = 1+2+3 = 6 ; b = 3*1 + 2*2 + 1*3 = 10 ; checksum = 6 + 10*65536.
167        let rc = RollingChecksum::new(&[1, 2, 3]);
168        assert_eq!(rc.value(), 6 + 10 * M);
169    }
170
171    #[test]
172    fn roll_equals_recompute() {
173        let data = b"the quick brown fox jumps over the lazy dog";
174        let w = 7usize;
175        let mut rc = RollingChecksum::new(&data[..w]);
176        for i in 1..=(data.len() - w) {
177            rc.roll(data[i - 1], data[i + w - 1]);
178            let fresh = RollingChecksum::new(&data[i..i + w]);
179            assert_eq!(rc.value(), fresh.value(), "mismatch at offset {i}");
180        }
181    }
182
183    #[test]
184    fn strong_hash_is_stable_and_16_bytes() {
185        let h1 = strong_hash(b"hello");
186        let h2 = strong_hash(b"hello");
187        let h3 = strong_hash(b"world");
188        assert_eq!(h1, h2);
189        assert_ne!(h1, h3);
190        assert_eq!(h1.len(), 16);
191    }
192}