Skip to main content

grit_lib/
delta_encode.rs

1//! Encode Git pack binary deltas (format decoded by [`crate::unpack_objects::apply_delta`]).
2//!
3//! [`DeltaIndex`] indexes a base buffer with a rolling fingerprint so many targets can be
4//! encoded against the same base efficiently (pack-objects style windows).
5
6use crate::error::{Error, Result};
7
8/// Bytes in each indexed window (Git `RABIN_WINDOW` in diff-delta).
9pub(crate) const WINDOW: usize = 16;
10/// Minimum COPY length worth emitting instead of literals.
11pub(crate) const MIN_COPY: usize = 4;
12/// Largest single COPY opcode in Git pack deltas.
13pub(crate) const COPY_CHUNK: usize = 0x10000;
14
15pub(crate) fn write_delta_varint(out: &mut Vec<u8>, mut n: usize) {
16    loop {
17        let mut b = (n & 0x7f) as u8;
18        n >>= 7;
19        if n != 0 {
20            b |= 0x80;
21        }
22        out.push(b);
23        if n == 0 {
24            break;
25        }
26    }
27}
28
29/// Emit one `COPY` opcode (copy `size` bytes from `offset` in the base object).
30pub(crate) fn push_copy(out: &mut Vec<u8>, mut offset: usize, mut size: usize) -> Result<()> {
31    if size == 0 {
32        return Ok(());
33    }
34    while size > 0 {
35        let chunk = size.min(COPY_CHUNK);
36        let moff = offset;
37        let msize = chunk;
38
39        let mut op = 0x80u8;
40        if moff & 0x0000_00ff != 0 {
41            op |= 0x01;
42        }
43        if moff & 0x0000_ff00 != 0 {
44            op |= 0x02;
45        }
46        if moff & 0x00ff_0000 != 0 {
47            op |= 0x04;
48        }
49        if moff & 0xff00_0000 != 0 {
50            op |= 0x08;
51        }
52        if msize != COPY_CHUNK {
53            if msize & 0x00ff != 0 {
54                op |= 0x10;
55            }
56            if msize & 0xff00 != 0 {
57                op |= 0x20;
58            }
59        }
60
61        out.push(op);
62        if op & 0x01 != 0 {
63            out.push(moff as u8);
64        }
65        if op & 0x02 != 0 {
66            out.push((moff >> 8) as u8);
67        }
68        if op & 0x04 != 0 {
69            out.push((moff >> 16) as u8);
70        }
71        if op & 0x08 != 0 {
72            out.push((moff >> 24) as u8);
73        }
74        if msize != COPY_CHUNK {
75            if op & 0x10 != 0 {
76                out.push(msize as u8);
77            }
78            if op & 0x20 != 0 {
79                out.push((msize >> 8) as u8);
80            }
81        }
82
83        offset = offset.saturating_add(chunk);
84        size -= chunk;
85    }
86    Ok(())
87}
88
89pub(crate) fn push_insert(out: &mut Vec<u8>, mut data: &[u8]) {
90    while !data.is_empty() {
91        let n = data.len().min(127);
92        out.push(n as u8);
93        out.extend_from_slice(&data[..n]);
94        data = &data[n..];
95    }
96}
97
98/// Rolling-hash index over a base buffer for reuse when encoding many targets.
99pub struct DeltaIndex {
100    inner: crate::pack_roll_delta::BaseMatchCatalog,
101}
102
103impl DeltaIndex {
104    /// Build an index over `base` for subsequent [`Self::encode`] calls.
105    #[must_use]
106    pub fn new(base: &[u8]) -> Self {
107        Self {
108            inner: crate::pack_roll_delta::BaseMatchCatalog::build(base),
109        }
110    }
111
112    /// Like [`Self::new`], indexing a buffer the caller already shares instead of copying it.
113    #[must_use]
114    pub(crate) fn from_shared(base: std::sync::Arc<[u8]>) -> Self {
115        Self {
116            inner: crate::pack_roll_delta::BaseMatchCatalog::build_shared(base),
117        }
118    }
119
120    /// Indexed stride used when subsampling large bases (Git indexes one entry per 16-byte rolling-hash window).
121    #[must_use]
122    pub fn index_stride(&self) -> usize {
123        WINDOW
124    }
125
126    /// Encode `target` as a binary delta against this index's base.
127    pub fn encode(&self, target: &[u8], max_size: usize) -> Option<Vec<u8>> {
128        self.inner.encode(target, max_size)
129    }
130}
131
132/// Encode a delta from `base` to `target` using a rolling-hash match finder.
133pub fn encode_delta(base: &[u8], target: &[u8], max_size: usize) -> Option<Vec<u8>> {
134    DeltaIndex::new(base).encode(target, max_size)
135}
136
137/// Build a delta when `target` begins with the entire `base` buffer (strict extension).
138pub fn encode_prefix_extension_delta(base: &[u8], target: &[u8]) -> Result<Vec<u8>> {
139    if !target.starts_with(base) || target.len() <= base.len() {
140        return Err(Error::CorruptObject(
141            "encode_prefix_extension_delta: target must strictly extend base".into(),
142        ));
143    }
144    let mut out = Vec::new();
145    write_delta_varint(&mut out, base.len());
146    write_delta_varint(&mut out, target.len());
147    push_copy(&mut out, 0, base.len())?;
148    push_insert(&mut out, &target[base.len()..]);
149    Ok(out)
150}
151
152/// Encode a binary delta from `base` to `target`.
153pub fn encode_lcp_delta(base: &[u8], target: &[u8]) -> Result<Vec<u8>> {
154    encode_delta(base, target, 0)
155        .ok_or_else(|| Error::CorruptObject("encode_lcp_delta: delta encoding failed".into()))
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::unpack_objects::apply_delta;
162
163    fn assert_roundtrip(base: &[u8], target: &[u8]) {
164        let delta = encode_delta(base, target, 0).expect("encode");
165        let got = apply_delta(base, &delta).expect("apply");
166        assert_eq!(
167            got,
168            target,
169            "base_len={} target_len={}",
170            base.len(),
171            target.len()
172        );
173    }
174
175    #[test]
176    fn empty_target_produces_valid_delta() {
177        let base = b"non-empty base";
178        let delta = DeltaIndex::new(base).encode(b"", 0).expect("encode");
179        assert_eq!(apply_delta(base, &delta).expect("apply"), b"" as &[u8]);
180    }
181
182    #[test]
183    fn empty_target_respects_max_size_before_return() {
184        let base = b"non-empty base";
185        assert!(DeltaIndex::new(base).encode(b"", 1).is_none());
186    }
187
188    #[test]
189    fn empty_base_and_target_roundtrip() {
190        assert_roundtrip(b"", b"");
191    }
192
193    #[test]
194    fn roundtrip_prefix_delta() {
195        let base = b"hello world".repeat(100);
196        let mut target = base.clone();
197        target.extend_from_slice(b"\nextra suffix\n");
198        assert_roundtrip(&base, &target);
199    }
200
201    #[test]
202    fn roundtrip_lcp_delta() {
203        assert_roundtrip(b"padding\n9", b"padding\n10");
204    }
205
206    #[test]
207    fn roundtrip_mid_file_insertion() {
208        let mut lines: Vec<String> = (1..=200)
209            .map(|i| format!("The quick brown fox jumps over the lazy dog line {i}\n"))
210            .collect();
211        let base = lines.join("");
212        lines.insert(100, "inserted: extra line for delta test\n".to_owned());
213        let target = lines.join("");
214        let delta = encode_delta(base.as_bytes(), target.as_bytes(), 0).expect("encode");
215        assert!(
216            delta.len() * 100 <= target.len() * 105,
217            "delta {} bytes vs target {} (want <=5%)",
218            delta.len(),
219            target.len()
220        );
221        assert_roundtrip(base.as_bytes(), target.as_bytes());
222    }
223
224    #[test]
225    fn roundtrip_mid_file_line_replace() {
226        let mut lines: Vec<String> = (1..=200)
227            .map(|i| format!("The quick brown fox jumps over the lazy dog line {i}\n"))
228            .collect();
229        let base = lines.join("");
230        lines[100] = "edit: replaced line content for delta test\n".to_owned();
231        let target = lines.join("");
232        let delta = encode_delta(base.as_bytes(), target.as_bytes(), 0).expect("encode");
233        assert!(
234            delta.len() * 100 <= target.len() * 105,
235            "delta {} bytes vs target {} (want <=5%)",
236            delta.len(),
237            target.len()
238        );
239        assert_roundtrip(base.as_bytes(), target.as_bytes());
240    }
241
242    #[test]
243    fn roundtrip_random_edits() {
244        let mut seed = 0x767u64;
245        for _ in 0..250 {
246            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
247            let base_len = (seed % 8000 + 32) as usize;
248            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
249            let target_len = (seed % 8000) as usize;
250            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
251
252            let mut base = vec![0u8; base_len];
253            for b in &mut base {
254                seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
255                *b = (seed >> 32) as u8;
256            }
257
258            let mut target = if target_len == 0 {
259                Vec::new()
260            } else {
261                vec![0u8; target_len]
262            };
263            let shared = base_len.min(target_len);
264            target[..shared].copy_from_slice(&base[..shared]);
265
266            let op = (seed % 5) as u8;
267            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
268            match op {
269                0 if target_len > shared => {
270                    for t in &mut target[shared..] {
271                        seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
272                        *t = (seed >> 40) as u8;
273                    }
274                }
275                1 if target_len > 0 && base_len > 0 => {
276                    let i = (seed as usize) % target_len;
277                    seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
278                    target[i] = (seed >> 33) as u8;
279                }
280                2 if target_len > 4 => {
281                    let i = (seed as usize) % (target_len - 1);
282                    seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
283                    target.remove(i);
284                }
285                3 if target_len > 0 => {
286                    let i = (seed as usize) % (target_len + 1);
287                    seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
288                    target.insert(i, (seed >> 34) as u8);
289                }
290                _ => {}
291            }
292
293            assert_roundtrip(&base, &target);
294        }
295    }
296
297    #[test]
298    fn real_world_source_edit() {
299        let base = include_str!("delta_encode.rs");
300        let mut target = base.to_owned();
301        target.insert_str(base.len() / 3, "\npub fn new_api() {}\n");
302        assert_roundtrip(base.as_bytes(), target.as_bytes());
303    }
304
305    #[test]
306    fn max_size_rejects_large_delta() {
307        let base = vec![0u8; 10_000];
308        let target = vec![1u8; 10_000];
309        assert!(encode_delta(&base, &target, 50).is_none());
310    }
311
312    #[test]
313    fn prefix_extension_requires_strict_suffix() {
314        let base = b"same";
315        let err = encode_prefix_extension_delta(base, base).unwrap_err();
316        assert!(matches!(err, Error::CorruptObject(_)));
317    }
318
319    #[test]
320    fn prefix_extension_splits_large_copy_runs() {
321        let base = vec![0xABu8; 70_000];
322        let mut target = base.clone();
323        target.push(b'!');
324        let delta = encode_prefix_extension_delta(&base, &target).unwrap();
325        let got = apply_delta(&base, &delta).unwrap();
326        assert_eq!(got, target);
327    }
328
329    #[test]
330    fn delta_index_reuse_across_targets() {
331        let base = b"abcdef".repeat(500);
332        let index = DeltaIndex::new(&base);
333        for n in 1..20 {
334            let mut target = base.clone();
335            target.extend_from_slice(&b"X".repeat(n));
336            let d1 = index.encode(&target, 0).unwrap();
337            let d2 = encode_delta(&base, &target, 0).unwrap();
338            assert_eq!(d1, d2);
339            assert_eq!(apply_delta(&base, &d1).unwrap(), target);
340        }
341    }
342
343    #[test]
344    fn index_stride_matches_git_window() {
345        let index = DeltaIndex::new(b"0123456789abcdef");
346        assert_eq!(index.index_stride(), WINDOW);
347    }
348
349    #[test]
350    fn large_base_roundtrip_extension() {
351        let base = vec![0u8; 512 * 1024];
352        let mut target = base.clone();
353        target.push(1);
354        assert_roundtrip(&base, &target);
355    }
356
357    #[test]
358    fn single_byte_mid_edit_encodes_small() {
359        let base = vec![b'a'; 50_000];
360        let mut target = base.clone();
361        target[25_000] = b'X';
362        let delta = encode_delta(&base, &target, 0).expect("delta");
363        assert!(delta.len() < 200, "delta {} bytes", delta.len());
364    }
365
366    #[test]
367    fn repetitive_lines_one_byte_edit_encodes_small() {
368        let lines: Vec<String> = (1..=500)
369            .map(|i| format!("line {i:05} lorem ipsum dolor sit amet"))
370            .collect();
371        let base = lines.join("\n");
372        let mut target = base.as_bytes().to_vec();
373        target[9000] ^= 0x01;
374        let delta = encode_delta(base.as_bytes(), &target, 0).expect("delta");
375        assert!(
376            delta.len() < 200,
377            "delta {} base {}",
378            delta.len(),
379            base.len()
380        );
381    }
382
383    #[test]
384    fn issue_1011_consecutive_blob_pair_from_repo_fixture() {
385        let base = include_bytes!("../tests/fixtures/issue_1011_base.bin");
386        let target = include_bytes!("../tests/fixtures/issue_1011_target.bin");
387        let delta = encode_delta(base, target, 0).expect("encode");
388        assert_eq!(apply_delta(base, &delta).expect("apply"), target.as_slice());
389        assert!(
390            delta.len() < target.len() / 10,
391            "delta {} vs target {} (expect Git-scale compression)",
392            delta.len(),
393            target.len()
394        );
395    }
396
397    #[test]
398    fn multi_line_edit_step_encodes_small() {
399        let line = " some moderately long line of text to make blob bigger abcdefghijklmnop\n";
400        let mut body = String::new();
401        while body.len() < 90 * 1024 {
402            let n = body.lines().count() + 1;
403            body.push_str(&format!("{n}{line}"));
404        }
405        let base = body.as_bytes();
406        let mut lines: Vec<String> = body.lines().map(str::to_owned).collect();
407        let stride = lines.len() / 16;
408        for k in 0..15 {
409            let idx = (3 + k * stride).min(lines.len().saturating_sub(1));
410            lines[idx] = format!("edit 1-{k}");
411        }
412        let target = lines.join("\n");
413        let delta = encode_delta(base, target.as_bytes(), 0).expect("encode");
414        assert!(
415            delta.len() < target.len() / 10,
416            "delta {} vs target {}",
417            delta.len(),
418            target.len()
419        );
420    }
421
422    #[test]
423    fn issue_1011_single_line_edit_in_large_blob() {
424        let lines: Vec<String> = (1..=3000)
425            .map(|i| {
426                format!(
427                    "{i} some moderately long line of text to make blob bigger abcdefghijklmnop"
428                )
429            })
430            .collect();
431        let base = lines.join("\n");
432        let mut target = base.as_bytes().to_vec();
433        // One-byte edit mid-file (same size as base; tail stays aligned).
434        target[base.len() / 3] ^= 0x01;
435        let target = target;
436        let max = target.len() / 2 - 20;
437        let delta_unlim = encode_delta(base.as_bytes(), &target, 0).expect("unlim");
438        let delta_max = encode_delta(base.as_bytes(), &target, max);
439        assert!(
440            delta_max.is_some(),
441            "encode_delta None with max_size {max} (unlim len {})",
442            delta_unlim.len()
443        );
444        let delta = delta_max.unwrap();
445        assert!(
446            delta.len() < target.len() / 10,
447            "delta {} vs target {}",
448            delta.len(),
449            target.len()
450        );
451    }
452}