Skip to main content

ripsync_core/
delta.rs

1//! The delta engine — ripsync's crown jewel.
2//!
3//! Given an `old` buffer and a `new` buffer, [`encode`] produces a compact
4//! [`Delta`]: a sequence of [`Op::Copy`] (reuse a block of `old`) and
5//! [`Op::Literal`] (raw bytes that aren't in `old`) operations. [`apply`] stitches
6//! those back together so that, for all inputs,
7//!
8//! ```text
9//! apply(old, encode(old, new, _)) == new
10//! ```
11//!
12//! ## Algorithm
13//!
14//! 1. Choose a block size `B = clamp(round(sqrt(old.len())), 700, 131072)` (or an
15//!    explicit override).
16//! 2. Split `old` into blocks of `B` bytes (the last may be short). For each block
17//!    record its weak rolling checksum and strong hash, keyed weak → block indices.
18//! 3. Roll a `B`-byte window across `new`. When the weak checksum hits a known
19//!    block *and* the strong hash confirms it, flush any pending literal run, emit
20//!    a [`Op::Copy`], and jump the window forward by `B`. Otherwise push the
21//!    leftmost byte into the pending literal buffer and slide the window by one.
22//! 4. A short trailing block of `old` is matched against the remaining tail of
23//!    `new`, so an unchanged file re-encodes to pure copies with zero literals.
24
25use std::collections::HashMap;
26
27use rayon::prelude::*;
28use serde::{Deserialize, Serialize};
29
30use crate::checksum::{RollingChecksum, StrongHash, strong_hash};
31
32/// Minimum auto-selected block size, in bytes.
33pub const MIN_BLOCK: usize = 700;
34/// Maximum auto-selected block size, in bytes.
35pub const MAX_BLOCK: usize = 131_072;
36
37/// A single delta operation.
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub enum Op {
40    /// Reuse block `index` of `old` (its byte range is `index*B .. index*B + len`).
41    Copy(u32),
42    /// Insert these raw bytes verbatim.
43    Literal(Vec<u8>),
44}
45
46/// A delta that reconstructs `new` from `old`.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct Delta {
49    /// Block size used when this delta was produced.
50    pub block_size: u32,
51    /// The operations, in order.
52    pub ops: Vec<Op>,
53}
54
55impl Delta {
56    /// Number of bytes carried as literals (i.e. not reused from `old`).
57    #[must_use]
58    pub fn literal_bytes(&self) -> usize {
59        self.ops
60            .iter()
61            .map(|op| match op {
62                Op::Literal(b) => b.len(),
63                Op::Copy(_) => 0,
64            })
65            .sum()
66    }
67
68    /// Number of [`Op::Copy`] operations.
69    #[must_use]
70    pub fn copy_ops(&self) -> usize {
71        self.ops.iter().filter(|o| matches!(o, Op::Copy(_))).count()
72    }
73}
74
75/// Choose a block size for `old_len`: `clamp(round(sqrt(old_len)), 700, 131072)`.
76#[must_use]
77#[allow(
78    clippy::cast_precision_loss,
79    clippy::cast_sign_loss,
80    clippy::cast_possible_truncation
81)]
82pub fn block_size_for(old_len: usize) -> usize {
83    let approx = (old_len as f64).sqrt().round() as usize;
84    approx.clamp(MIN_BLOCK, MAX_BLOCK)
85}
86
87/// One block's fingerprint inside a [`Signature`].
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct BlockSig {
90    /// Weak rolling checksum of the block.
91    pub weak: u32,
92    /// Strong hash (BLAKE3 prefix) of the block.
93    pub strong: StrongHash,
94    /// Block length in bytes (all `block_size` except possibly the last).
95    pub len: u32,
96}
97
98/// A compact fingerprint of an `old` buffer: everything the delta encoder needs
99/// to find reusable blocks, and nothing else.
100///
101/// This is the wire-friendly half of the rsync split. The machine that holds the
102/// *old* file ("receiver") computes a [`Signature`] with [`Signature::compute`]
103/// and sends it across; the machine that holds the *new* file ("sender") feeds it
104/// to [`encode_with_signature`] to produce a [`Delta`] — without ever shipping the
105/// old bytes.
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct Signature {
108    /// Block size the blocks were cut at.
109    pub block_size: u32,
110    /// Per-block fingerprints, in `old` order.
111    pub blocks: Vec<BlockSig>,
112}
113
114impl Signature {
115    /// Compute the signature of `old`.
116    ///
117    /// `block_override` forces a block size (must be ≥ 1); otherwise
118    /// [`block_size_for`] picks one from `old.len()`.
119    #[must_use]
120    pub fn compute(old: &[u8], block_override: Option<usize>) -> Self {
121        let block = block_override.map_or_else(|| block_size_for(old.len()), |b| b.max(1));
122        let block_size = u32::try_from(block).unwrap_or(u32::MAX);
123        let blocks: Vec<BlockSig> = old
124            .par_chunks(block)
125            .map(|chunk| BlockSig {
126                weak: RollingChecksum::new(chunk).value(),
127                strong: strong_hash(chunk),
128                len: u32::try_from(chunk.len()).unwrap_or(u32::MAX),
129            })
130            .collect();
131        Self { block_size, blocks }
132    }
133
134    /// Number of blocks in the signature.
135    #[must_use]
136    pub fn len(&self) -> usize {
137        self.blocks.len()
138    }
139
140    /// Whether the signature is empty (the `old` buffer had no bytes).
141    #[must_use]
142    pub fn is_empty(&self) -> bool {
143        self.blocks.is_empty()
144    }
145}
146
147/// Acceleration structure built from a [`Signature`] at encode time: weak
148/// checksum → indices of blocks sharing it.
149struct Lookup<'a> {
150    sig: &'a Signature,
151    by_weak: HashMap<u32, Vec<u32>>,
152}
153
154impl<'a> Lookup<'a> {
155    fn new(sig: &'a Signature) -> Self {
156        let mut by_weak: HashMap<u32, Vec<u32>> = HashMap::new();
157        for (i, block) in sig.blocks.iter().enumerate() {
158            let idx = u32::try_from(i).unwrap_or(u32::MAX);
159            by_weak.entry(block.weak).or_default().push(idx);
160        }
161        Self { sig, by_weak }
162    }
163
164    /// Find a block matching `weak` and the window contents.
165    fn find(&self, weak: u32, window: &[u8]) -> Option<u32> {
166        let candidates = self.by_weak.get(&weak)?;
167        let mut strong: Option<StrongHash> = None;
168        for &bi in candidates {
169            let block = &self.sig.blocks[bi as usize];
170            if block.len as usize != window.len() {
171                continue;
172            }
173            let sh = *strong.get_or_insert_with(|| strong_hash(window));
174            if block.strong == sh {
175                return Some(bi);
176            }
177        }
178        None
179    }
180}
181
182/// Encode `new` against a previously computed [`Signature`] of `old`.
183///
184/// This is the half of the delta engine that runs on the machine holding `new`
185/// (the rsync "sender"): it needs only the signature, never `old` itself.
186#[must_use]
187pub fn encode_with_signature(sig: &Signature, new: &[u8]) -> Delta {
188    let block = sig.block_size as usize;
189    let block_size = sig.block_size;
190
191    // Degenerate: nothing to copy from.
192    if sig.blocks.is_empty() {
193        let ops = if new.is_empty() {
194            Vec::new()
195        } else {
196            vec![Op::Literal(new.to_vec())]
197        };
198        return Delta { block_size, ops };
199    }
200
201    let index = Lookup::new(sig);
202    let mut ops: Vec<Op> = Vec::new();
203    let n = new.len();
204    let mut pos = 0usize; // window start
205    let mut lit_start = 0usize; // start of pending literal run
206
207    let flush = |ops: &mut Vec<Op>, from: usize, to: usize| {
208        if to > from {
209            ops.push(Op::Literal(new[from..to].to_vec()));
210        }
211    };
212
213    if n >= block {
214        let mut rc = RollingChecksum::new(&new[..block]);
215        loop {
216            let window = &new[pos..pos + block];
217            if let Some(bi) = index.find(rc.value(), window) {
218                flush(&mut ops, lit_start, pos);
219                ops.push(Op::Copy(bi));
220                pos += block;
221                lit_start = pos;
222                if pos + block <= n {
223                    rc = RollingChecksum::new(&new[pos..pos + block]);
224                    continue;
225                }
226                break;
227            }
228            if pos + block < n {
229                rc.roll(new[pos], new[pos + block]);
230                pos += 1;
231            } else {
232                // Window sits at the very end; can't slide further.
233                break;
234            }
235        }
236    }
237
238    // Tail: try to match a short trailing block of `old` against the remainder,
239    // then emit whatever is left as a final literal.
240    let remaining = &new[lit_start..n];
241    if !remaining.is_empty() {
242        let weak = RollingChecksum::new(remaining).value();
243        if let Some(bi) = index.find(weak, remaining) {
244            ops.push(Op::Copy(bi));
245        } else {
246            ops.push(Op::Literal(remaining.to_vec()));
247        }
248    }
249
250    Delta { block_size, ops }
251}
252
253/// Encode the difference from `old` to `new`.
254///
255/// Convenience wrapper for callers that hold both buffers locally: computes the
256/// signature of `old` and encodes `new` against it. `block_override` forces a
257/// block size (must be ≥ 1); otherwise [`block_size_for`] picks one.
258#[must_use]
259pub fn encode(old: &[u8], new: &[u8], block_override: Option<usize>) -> Delta {
260    encode_with_signature(&Signature::compute(old, block_override), new)
261}
262
263/// Reconstruct `new` by applying `delta` to `old`.
264///
265/// # Errors
266///
267/// Returns [`crate::Error::DeltaApply`] if a [`Op::Copy`] references a block that
268/// does not exist in `old` (e.g. a corrupt or mismatched delta).
269pub fn apply(old: &[u8], delta: &Delta) -> crate::Result<Vec<u8>> {
270    let block = delta.block_size as usize;
271    let mut out: Vec<u8> = Vec::new();
272    for op in &delta.ops {
273        match op {
274            Op::Literal(bytes) => out.extend_from_slice(bytes),
275            Op::Copy(bi) => {
276                let start = (*bi as usize)
277                    .checked_mul(block)
278                    .ok_or_else(|| crate::Error::DeltaApply("block offset overflow".into()))?;
279                if start >= old.len() && !(start == 0 && old.is_empty()) {
280                    return Err(crate::Error::DeltaApply(format!(
281                        "copy block {bi} out of range"
282                    )));
283                }
284                let end = (start + block).min(old.len());
285                out.extend_from_slice(&old[start..end]);
286            }
287        }
288    }
289    Ok(out)
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    fn roundtrip(old: &[u8], new: &[u8], block: Option<usize>) -> Delta {
297        let d = encode(old, new, block);
298        let rebuilt = apply(old, &d).expect("apply");
299        assert_eq!(rebuilt, new, "roundtrip mismatch");
300        d
301    }
302
303    #[test]
304    fn empty_old_empty_new() {
305        let d = roundtrip(b"", b"", None);
306        assert!(d.ops.is_empty());
307    }
308
309    #[test]
310    fn empty_old_some_new() {
311        let d = roundtrip(b"", b"hello world", None);
312        assert_eq!(d.ops, vec![Op::Literal(b"hello world".to_vec())]);
313    }
314
315    #[test]
316    fn some_old_empty_new() {
317        let d = roundtrip(b"abcdef", b"", None);
318        assert!(d.ops.is_empty());
319    }
320
321    #[test]
322    fn identical_is_all_copy_zero_literals() {
323        let old: Vec<u8> = (0..5000u32).map(|i| (i % 251) as u8).collect();
324        let d = roundtrip(&old, &old, Some(700));
325        assert_eq!(d.literal_bytes(), 0, "expected zero literals");
326        assert!(d.copy_ops() > 0);
327        assert!(d.ops.iter().all(|o| matches!(o, Op::Copy(_))));
328    }
329
330    #[test]
331    fn completely_different_is_all_literal() {
332        let old = vec![0u8; 4096];
333        let new = vec![0xFFu8; 4096];
334        let d = roundtrip(&old, &new, Some(700));
335        assert_eq!(d.copy_ops(), 0, "expected no copies");
336        assert_eq!(d.literal_bytes(), new.len());
337    }
338
339    #[test]
340    fn new_shorter_than_old() {
341        let old: Vec<u8> = (0..3000u32).map(|i| (i % 97) as u8).collect();
342        let new = &old[..1234];
343        roundtrip(&old, new, Some(700));
344    }
345
346    #[test]
347    fn new_longer_than_old() {
348        let old: Vec<u8> = (0..3000u32).map(|i| (i % 97) as u8).collect();
349        let mut new = old.clone();
350        new.extend(std::iter::repeat_n(0xAB, 2000));
351        let d = roundtrip(&old, &new, Some(700));
352        assert!(d.copy_ops() > 0, "prefix should be copied");
353    }
354
355    #[test]
356    fn single_byte_change_middle() {
357        let old: Vec<u8> = (0..8000u32).map(|i| (i % 211) as u8).collect();
358        let mut new = old.clone();
359        new[4000] ^= 0xFF;
360        let d = roundtrip(&old, &new, Some(700));
361        // Most blocks unchanged → copies dominate, few literal bytes.
362        assert!(d.copy_ops() > 0);
363        assert!(
364            d.literal_bytes() < 2 * 700,
365            "only the changed block should differ"
366        );
367    }
368
369    #[test]
370    fn insertion_at_front_shifts_everything() {
371        // Proves the rolling checksum re-syncs after a shift.
372        let old: Vec<u8> = (0..8000u32).map(|i| (i % 197) as u8).collect();
373        let mut new = vec![1u8, 2, 3, 4, 5];
374        new.extend_from_slice(&old);
375        let d = roundtrip(&old, &new, Some(700));
376        assert!(d.copy_ops() > 0, "shifted blocks must still be found");
377        // The 5 inserted bytes plus at most one partial block become literals.
378        assert!(d.literal_bytes() <= 5 + 700);
379    }
380
381    #[test]
382    fn block_size_clamps() {
383        assert_eq!(block_size_for(0), MIN_BLOCK);
384        assert_eq!(block_size_for(100), MIN_BLOCK);
385        assert_eq!(block_size_for(1_000_000), 1000);
386        assert_eq!(block_size_for(usize::MAX), MAX_BLOCK);
387    }
388
389    #[test]
390    fn apply_rejects_out_of_range_copy() {
391        let bad = Delta {
392            block_size: 4,
393            ops: vec![Op::Copy(999)],
394        };
395        assert!(apply(b"tiny", &bad).is_err());
396    }
397
398    #[test]
399    fn strong_len_is_16() {
400        assert_eq!(crate::checksum::STRONG_LEN, 16);
401    }
402}