Skip to main content

heddle_format/delta/
delta_encoder.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Delta encoder using Git-style compact copy instructions.
3//!
4//! Copy instruction format (identical to Git):
5//! ```text
6//! Byte 0: 1oooosss
7//!   o bits (3-6): which offset bytes follow (up to 4 → 32-bit offset)
8//!   s bits (0-2): which size bytes follow (up to 3 → 24-bit size; all zero = 0x10000)
9//! [offset bytes, low to high, only present if corresponding o-bit is set]
10//! [size bytes, low to high, only present if corresponding s-bit is set]
11//! ```
12//!
13//! Insert instruction: `[length-1] [literal bytes]` (max 127 bytes per chunk).
14
15/// Minimum match length for targets >= 1024 bytes.
16const MIN_MATCH_LENGTH_LARGE: usize = 16;
17/// Minimum match length for small targets (< 1024 bytes).
18const MIN_MATCH_LENGTH_SMALL: usize = 8;
19/// Maximum offsets to inspect for a single 4-byte key.
20const MAX_MATCH_CANDIDATES: usize = 1024;
21/// Compare long common prefixes in chunks before locating the exact tail.
22const MATCH_CHUNK_SIZE: usize = 32;
23/// Largest copy length representable by the three size bytes.
24const MAX_COPY_LENGTH: usize = 0xFF_FFFF;
25/// Sample one base position per 16-byte block for normal-sized objects.
26const INDEX_BLOCK_SIZE: usize = 16;
27/// Keep each cached index at or below 4 MiB, even for very large bases.
28const MAX_INDEX_BYTES: usize = 4 * 1024 * 1024;
29/// Preserve short matches in small objects, where a dense flat index is cheap.
30const DENSE_INDEX_BELOW: usize = 1024;
31
32#[derive(Clone, Copy, Debug)]
33struct IndexEntry {
34    key: u32,
35    offset: u32,
36}
37
38/// Flat, bounded index of sampled positions in a delta base.
39#[derive(Debug)]
40pub struct DeltaIndex {
41    entries: Vec<IndexEntry>,
42}
43
44/// Delta encoder.
45#[derive(Debug)]
46pub struct DeltaEncoder;
47
48impl DeltaEncoder {
49    /// Create a new delta encoder.
50    pub fn new() -> Self {
51        Self
52    }
53
54    /// Encode a delta from base to target.
55    pub fn encode(base: &[u8], target: &[u8]) -> Vec<u8> {
56        if base.is_empty() {
57            return Self::encode_insert(target);
58        }
59
60        let index = Self::build_index(base);
61        Self::encode_with_index(&index, base, target)
62    }
63
64    /// Encode a delta using a pre-built index (avoids rebuilding for sliding window).
65    pub fn encode_with_index(index: &DeltaIndex, base: &[u8], target: &[u8]) -> Vec<u8> {
66        if base.is_empty() {
67            return Self::encode_insert(target);
68        }
69
70        let min_match = Self::min_match_for(target.len());
71        let mut delta = Vec::new();
72        let mut pos = 0;
73        let mut key = Self::target_key(target, pos);
74
75        while pos < target.len() {
76            if let Some((offset, length)) =
77                Self::find_best_match(index, base, target, pos, key, min_match)
78            {
79                Self::emit_copy(&mut delta, offset, length);
80                pos += length;
81                key = Self::target_key(target, pos);
82            } else {
83                let start = pos;
84                while pos < target.len() && pos - start < 127 {
85                    pos += 1;
86                    key = Self::roll_target_key(key, target, pos);
87                    if Self::find_best_match(index, base, target, pos, key, min_match).is_some() {
88                        break;
89                    }
90                }
91
92                let len = pos - start;
93                delta.push(len as u8 - 1);
94                delta.extend_from_slice(&target[start..pos]);
95            }
96        }
97
98        delta
99    }
100
101    /// Estimate the encoded delta size without allocating the output.
102    pub fn estimate_delta_size(base: &[u8], target: &[u8]) -> usize {
103        if base.is_empty() {
104            return target.len() + target.len().div_ceil(128);
105        }
106
107        let index = Self::build_index(base);
108        Self::estimate_delta_size_with_index(&index, base, target)
109    }
110
111    /// Estimate delta size using a pre-built index (avoids rebuilding for sliding window).
112    pub fn estimate_delta_size_with_index(index: &DeltaIndex, base: &[u8], target: &[u8]) -> usize {
113        if base.is_empty() {
114            return target.len() + target.len().div_ceil(128);
115        }
116
117        let min_match = Self::min_match_for(target.len());
118        let mut size = 0usize;
119        let mut pos = 0;
120        let mut key = Self::target_key(target, pos);
121
122        while pos < target.len() {
123            if let Some((offset, length)) =
124                Self::find_best_match(index, base, target, pos, key, min_match)
125            {
126                size += Self::copy_instruction_size(offset, length);
127                pos += length;
128                key = Self::target_key(target, pos);
129            } else {
130                let start = pos;
131                while pos < target.len() && pos - start < 127 {
132                    pos += 1;
133                    key = Self::roll_target_key(key, target, pos);
134                    if Self::find_best_match(index, base, target, pos, key, min_match).is_some() {
135                        break;
136                    }
137                }
138                size += 1 + (pos - start);
139            }
140        }
141
142        size
143    }
144
145    /// Build a flat index over sampled base positions, capped at 4 MiB.
146    pub fn build_index(base: &[u8]) -> DeltaIndex {
147        if base.len() < 4 {
148            return DeltaIndex {
149                entries: Vec::new(),
150            };
151        }
152
153        // Git copy offsets are 32-bit. Larger bases can still be represented by
154        // inserts, but only their addressable prefix may be indexed for copies.
155        let last_offset = (base.len() - 4).min(u32::MAX as usize);
156        let max_entries = MAX_INDEX_BYTES / size_of::<IndexEntry>();
157        // Sixteen bytes matches the large-object minimum match length. A
158        // shifted target may need up to 15 literal bytes before it reaches a
159        // sampled base position; larger objects use a wider stride to fit.
160        let stride = if base.len() < DENSE_INDEX_BELOW {
161            1
162        } else {
163            (last_offset + 1)
164                .div_ceil(max_entries)
165                .max(INDEX_BLOCK_SIZE)
166                .next_multiple_of(INDEX_BLOCK_SIZE)
167        };
168        let mut entries = Vec::with_capacity(last_offset / stride + 1);
169
170        for offset in (0..=last_offset).step_by(stride) {
171            let key = u32::from_be_bytes([
172                base[offset],
173                base[offset + 1],
174                base[offset + 2],
175                base[offset + 3],
176            ]);
177            entries.push(IndexEntry {
178                key,
179                offset: offset as u32,
180            });
181        }
182        entries.sort_unstable_by_key(|entry| (entry.key, entry.offset));
183        DeltaIndex { entries }
184    }
185
186    /// Emit a Git-style copy instruction.
187    ///
188    /// Format: `1sssoooo [offset bytes] [size bytes]`
189    /// - Bit 7: copy flag (always 1)
190    /// - Bits 0-3 (o): which offset bytes (0-3) are present
191    /// - Bits 4-6 (s): which size bytes (0-2) are present
192    /// - If no s bits set, size = 0x10000
193    fn emit_copy(delta: &mut Vec<u8>, offset: usize, length: usize) {
194        let mut remaining = length;
195        let mut offset = offset;
196        while remaining > 0 {
197            let chunk = remaining.min(MAX_COPY_LENGTH);
198            Self::emit_copy_instruction(delta, offset, chunk);
199            offset += chunk;
200            remaining -= chunk;
201        }
202    }
203
204    fn emit_copy_instruction(delta: &mut Vec<u8>, offset: usize, length: usize) {
205        let mut cmd: u8 = 0x80;
206        let offset = offset as u32;
207        let length = length as u32;
208
209        // Offset byte flags: bits 0-3
210        // Always emit at least offset byte 0 to avoid the reserved cmd=0x80
211        // (which occurs when offset=0 and length=0x10000).
212        cmd |= 0x01; // always include offset byte 0
213        if offset & 0xFF00 != 0 {
214            cmd |= 0x02;
215        }
216        if offset & 0xFF_0000 != 0 {
217            cmd |= 0x04;
218        }
219        if offset & 0xFF00_0000 != 0 {
220            cmd |= 0x08;
221        }
222
223        // Size byte flags: bits 4-6
224        // Special case: size == 0x10000 is encoded as no size bytes (all s bits zero)
225        if length != 0x10000 {
226            if length & 0xFF != 0 {
227                cmd |= 0x10;
228            }
229            if length & 0xFF00 != 0 {
230                cmd |= 0x20;
231            }
232            if length & 0xFF_0000 != 0 {
233                cmd |= 0x40;
234            }
235        }
236
237        delta.push(cmd);
238
239        // Emit offset bytes (low to high), only those flagged
240        delta.push(offset as u8); // always present (bit 0 always set)
241        if offset & 0xFF00 != 0 {
242            delta.push((offset >> 8) as u8);
243        }
244        if offset & 0xFF_0000 != 0 {
245            delta.push((offset >> 16) as u8);
246        }
247        if offset & 0xFF00_0000 != 0 {
248            delta.push((offset >> 24) as u8);
249        }
250
251        // Emit size bytes (low to high), only those flagged
252        if length != 0x10000 {
253            if length & 0xFF != 0 {
254                delta.push(length as u8);
255            }
256            if length & 0xFF00 != 0 {
257                delta.push((length >> 8) as u8);
258            }
259            if length & 0xFF_0000 != 0 {
260                delta.push((length >> 16) as u8);
261            }
262        }
263    }
264
265    /// Calculate the byte size of a Git-style copy instruction.
266    fn copy_instruction_size(offset: usize, length: usize) -> usize {
267        let mut remaining = length;
268        let mut offset = offset;
269        let mut size = 0;
270        while remaining > 0 {
271            let chunk = remaining.min(MAX_COPY_LENGTH);
272            size += Self::copy_instruction_size_one(offset, chunk);
273            offset += chunk;
274            remaining -= chunk;
275        }
276        size
277    }
278
279    fn copy_instruction_size_one(offset: usize, length: usize) -> usize {
280        let offset = offset as u32;
281        let length = length as u32;
282        let mut n = 1 + 1; // flag byte + offset byte 0 (always present)
283
284        // Additional offset bytes (bits 1-3)
285        if offset & 0xFF00 != 0 {
286            n += 1;
287        }
288        if offset & 0xFF_0000 != 0 {
289            n += 1;
290        }
291        if offset & 0xFF00_0000 != 0 {
292            n += 1;
293        }
294
295        // Size bytes (bits 4-6); 0x10000 = no bytes
296        if length != 0x10000 {
297            if length & 0xFF != 0 {
298                n += 1;
299            }
300            if length & 0xFF00 != 0 {
301                n += 1;
302            }
303            if length & 0xFF_0000 != 0 {
304                n += 1;
305            }
306        }
307
308        n
309    }
310
311    /// Choose minimum match length based on target size.
312    fn min_match_for(target_len: usize) -> usize {
313        if target_len < 1024 {
314            MIN_MATCH_LENGTH_SMALL
315        } else {
316            MIN_MATCH_LENGTH_LARGE
317        }
318    }
319
320    fn encode_insert(data: &[u8]) -> Vec<u8> {
321        let mut delta = Vec::new();
322        for chunk in data.chunks(128) {
323            delta.push((chunk.len() - 1) as u8);
324            delta.extend_from_slice(chunk);
325        }
326        delta
327    }
328
329    fn find_best_match(
330        index: &DeltaIndex,
331        base: &[u8],
332        target: &[u8],
333        pos: usize,
334        key: Option<u32>,
335        min_match: usize,
336    ) -> Option<(usize, usize)> {
337        let key = key?;
338        let found = index
339            .entries
340            .binary_search_by_key(&key, |entry| entry.key)
341            .ok()?;
342        let first = index.entries[..found].partition_point(|entry| entry.key < key);
343        let last = found + index.entries[found..].partition_point(|entry| entry.key == key);
344        let offsets = &index.entries[first..last];
345
346        let mut best_offset = 0;
347        let mut best_length = 0;
348
349        let target_remaining = target.len() - pos;
350        let recent_start = offsets.len().saturating_sub(MAX_MATCH_CANDIDATES);
351        let mut examined = 0usize;
352
353        if recent_start > 0 {
354            let offset = offsets[0].offset as usize;
355            let length = Self::match_length(base, offset, target, pos);
356            if length > best_length {
357                best_length = length;
358                best_offset = offset;
359            }
360            if length == target_remaining {
361                return Some((best_offset, best_length));
362            }
363            examined += 1;
364        }
365
366        let remaining_budget = MAX_MATCH_CANDIDATES - examined;
367        let start = offsets.len().saturating_sub(remaining_budget);
368        for entry in &offsets[start..] {
369            let offset = entry.offset as usize;
370            let length = Self::match_length(base, offset, target, pos);
371            if length > best_length {
372                best_length = length;
373                best_offset = offset;
374            }
375            if length == target_remaining {
376                break;
377            }
378        }
379
380        if best_length >= min_match {
381            Some((best_offset, best_length))
382        } else {
383            None
384        }
385    }
386
387    fn target_key(target: &[u8], pos: usize) -> Option<u32> {
388        let bytes = target.get(pos..pos.checked_add(4)?)?;
389        Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
390    }
391
392    fn roll_target_key(key: Option<u32>, target: &[u8], pos: usize) -> Option<u32> {
393        let next_byte = *target.get(pos.checked_add(3)?)?;
394        Some((key? << 8) | u32::from(next_byte))
395    }
396
397    fn match_length(base: &[u8], base_pos: usize, target: &[u8], target_pos: usize) -> usize {
398        // A long match may need several copy instructions. Keep every next
399        // instruction's starting offset within the 32-bit wire field.
400        let max_len = (base.len() - base_pos).min(target.len() - target_pos).min(
401            (u32::MAX as usize)
402                .saturating_sub(base_pos)
403                .saturating_add(1),
404        );
405        let mut len = 0;
406        while len + MATCH_CHUNK_SIZE <= max_len
407            && base[base_pos + len..base_pos + len + MATCH_CHUNK_SIZE]
408                == target[target_pos + len..target_pos + len + MATCH_CHUNK_SIZE]
409        {
410            len += MATCH_CHUNK_SIZE;
411        }
412        while len < max_len && base[base_pos + len] == target[target_pos + len] {
413            len += 1;
414        }
415        len
416    }
417}
418
419impl Default for DeltaEncoder {
420    fn default() -> Self {
421        Self::new()
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::{DeltaEncoder, IndexEntry, MAX_INDEX_BYTES};
428
429    #[test]
430    fn index_memory_is_bounded() {
431        let base = vec![0u8; 16 * 1024 * 1024];
432        let index = DeltaEncoder::build_index(&base);
433        assert!(index.entries.capacity() * size_of::<IndexEntry>() <= MAX_INDEX_BYTES);
434    }
435}