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
15use std::collections::HashMap;
16
17/// Minimum match length for targets >= 1024 bytes.
18const MIN_MATCH_LENGTH_LARGE: usize = 16;
19/// Minimum match length for small targets (< 1024 bytes).
20const MIN_MATCH_LENGTH_SMALL: usize = 8;
21/// Maximum offsets to inspect for a single 4-byte key.
22const MAX_MATCH_CANDIDATES: usize = 1024;
23/// Compare long common prefixes in chunks before locating the exact tail.
24const MATCH_CHUNK_SIZE: usize = 32;
25
26/// Delta encoder.
27#[derive(Debug)]
28pub struct DeltaEncoder;
29
30impl DeltaEncoder {
31    /// Create a new delta encoder.
32    pub fn new() -> Self {
33        Self
34    }
35
36    /// Encode a delta from base to target.
37    pub fn encode(base: &[u8], target: &[u8]) -> Vec<u8> {
38        if base.is_empty() {
39            return Self::encode_insert(target);
40        }
41
42        let index = Self::build_index(base);
43        Self::encode_with_index(&index, base, target)
44    }
45
46    /// Encode a delta using a pre-built index (avoids rebuilding for sliding window).
47    pub fn encode_with_index(
48        index: &HashMap<[u8; 4], Vec<usize>>,
49        base: &[u8],
50        target: &[u8],
51    ) -> Vec<u8> {
52        if base.is_empty() {
53            return Self::encode_insert(target);
54        }
55
56        let min_match = Self::min_match_for(target.len());
57        let mut delta = Vec::new();
58        let mut pos = 0;
59
60        while pos < target.len() {
61            if let Some((offset, length)) =
62                Self::find_best_match(index, base, target, pos, min_match)
63            {
64                Self::emit_copy(&mut delta, offset, length);
65                pos += length;
66            } else {
67                let start = pos;
68                while pos < target.len() && pos - start < 127 {
69                    if Self::find_best_match(index, base, target, pos, min_match).is_some() {
70                        break;
71                    }
72                    pos += 1;
73                }
74
75                let len = pos - start;
76                delta.push(len as u8 - 1);
77                delta.extend_from_slice(&target[start..pos]);
78            }
79        }
80
81        delta
82    }
83
84    /// Estimate the encoded delta size without allocating the output.
85    pub fn estimate_delta_size(base: &[u8], target: &[u8]) -> usize {
86        if base.is_empty() {
87            return target.len() + target.len().div_ceil(128);
88        }
89
90        let index = Self::build_index(base);
91        Self::estimate_delta_size_with_index(&index, base, target)
92    }
93
94    /// Estimate delta size using a pre-built index (avoids rebuilding for sliding window).
95    pub fn estimate_delta_size_with_index(
96        index: &HashMap<[u8; 4], Vec<usize>>,
97        base: &[u8],
98        target: &[u8],
99    ) -> usize {
100        if base.is_empty() {
101            return target.len() + target.len().div_ceil(128);
102        }
103
104        let min_match = Self::min_match_for(target.len());
105        let mut size = 0usize;
106        let mut pos = 0;
107
108        while pos < target.len() {
109            if let Some((offset, length)) =
110                Self::find_best_match(index, base, target, pos, min_match)
111            {
112                size += Self::copy_instruction_size(offset, length);
113                pos += length;
114            } else {
115                let start = pos;
116                while pos < target.len() && pos - start < 127 {
117                    if Self::find_best_match(index, base, target, pos, min_match).is_some() {
118                        break;
119                    }
120                    pos += 1;
121                }
122                size += 1 + (pos - start);
123            }
124        }
125
126        size
127    }
128
129    /// Build a 4-byte hash index over the base data.
130    pub fn build_index(base: &[u8]) -> HashMap<[u8; 4], Vec<usize>> {
131        let mut index: HashMap<[u8; 4], Vec<usize>> = HashMap::new();
132
133        for i in 0..base.len().saturating_sub(4) {
134            let key = [base[i], base[i + 1], base[i + 2], base[i + 3]];
135            index.entry(key).or_default().push(i);
136        }
137
138        index
139    }
140
141    /// Emit a Git-style copy instruction.
142    ///
143    /// Format: `1sssoooo [offset bytes] [size bytes]`
144    /// - Bit 7: copy flag (always 1)
145    /// - Bits 0-3 (o): which offset bytes (0-3) are present
146    /// - Bits 4-6 (s): which size bytes (0-2) are present
147    /// - If no s bits set, size = 0x10000
148    fn emit_copy(delta: &mut Vec<u8>, offset: usize, length: usize) {
149        let mut cmd: u8 = 0x80;
150        let offset = offset as u32;
151        let length = length as u32;
152
153        // Offset byte flags: bits 0-3
154        // Always emit at least offset byte 0 to avoid the reserved cmd=0x80
155        // (which occurs when offset=0 and length=0x10000).
156        cmd |= 0x01; // always include offset byte 0
157        if offset & 0xFF00 != 0 {
158            cmd |= 0x02;
159        }
160        if offset & 0xFF_0000 != 0 {
161            cmd |= 0x04;
162        }
163        if offset & 0xFF00_0000 != 0 {
164            cmd |= 0x08;
165        }
166
167        // Size byte flags: bits 4-6
168        // Special case: size == 0x10000 is encoded as no size bytes (all s bits zero)
169        if length != 0x10000 {
170            if length & 0xFF != 0 {
171                cmd |= 0x10;
172            }
173            if length & 0xFF00 != 0 {
174                cmd |= 0x20;
175            }
176            if length & 0xFF_0000 != 0 {
177                cmd |= 0x40;
178            }
179        }
180
181        delta.push(cmd);
182
183        // Emit offset bytes (low to high), only those flagged
184        delta.push(offset as u8); // always present (bit 0 always set)
185        if offset & 0xFF00 != 0 {
186            delta.push((offset >> 8) as u8);
187        }
188        if offset & 0xFF_0000 != 0 {
189            delta.push((offset >> 16) as u8);
190        }
191        if offset & 0xFF00_0000 != 0 {
192            delta.push((offset >> 24) as u8);
193        }
194
195        // Emit size bytes (low to high), only those flagged
196        if length != 0x10000 {
197            if length & 0xFF != 0 {
198                delta.push(length as u8);
199            }
200            if length & 0xFF00 != 0 {
201                delta.push((length >> 8) as u8);
202            }
203            if length & 0xFF_0000 != 0 {
204                delta.push((length >> 16) as u8);
205            }
206        }
207    }
208
209    /// Calculate the byte size of a Git-style copy instruction.
210    fn copy_instruction_size(offset: usize, length: usize) -> usize {
211        let offset = offset as u32;
212        let length = length as u32;
213        let mut n = 1 + 1; // flag byte + offset byte 0 (always present)
214
215        // Additional offset bytes (bits 1-3)
216        if offset & 0xFF00 != 0 {
217            n += 1;
218        }
219        if offset & 0xFF_0000 != 0 {
220            n += 1;
221        }
222        if offset & 0xFF00_0000 != 0 {
223            n += 1;
224        }
225
226        // Size bytes (bits 4-6); 0x10000 = no bytes
227        if length != 0x10000 {
228            if length & 0xFF != 0 {
229                n += 1;
230            }
231            if length & 0xFF00 != 0 {
232                n += 1;
233            }
234            if length & 0xFF_0000 != 0 {
235                n += 1;
236            }
237        }
238
239        n
240    }
241
242    /// Choose minimum match length based on target size.
243    fn min_match_for(target_len: usize) -> usize {
244        if target_len < 1024 {
245            MIN_MATCH_LENGTH_SMALL
246        } else {
247            MIN_MATCH_LENGTH_LARGE
248        }
249    }
250
251    fn encode_insert(data: &[u8]) -> Vec<u8> {
252        let mut delta = Vec::new();
253        for chunk in data.chunks(128) {
254            delta.push((chunk.len() - 1) as u8);
255            delta.extend_from_slice(chunk);
256        }
257        delta
258    }
259
260    fn find_best_match(
261        index: &HashMap<[u8; 4], Vec<usize>>,
262        base: &[u8],
263        target: &[u8],
264        pos: usize,
265        min_match: usize,
266    ) -> Option<(usize, usize)> {
267        if pos + 4 > target.len() {
268            return None;
269        }
270
271        let key = [
272            target[pos],
273            target[pos + 1],
274            target[pos + 2],
275            target[pos + 3],
276        ];
277        let offsets = index.get(&key)?;
278
279        let mut best_offset = 0;
280        let mut best_length = 0;
281
282        let target_remaining = target.len() - pos;
283        let recent_start = offsets.len().saturating_sub(MAX_MATCH_CANDIDATES);
284        let mut examined = 0usize;
285
286        if recent_start > 0 {
287            let offset = offsets[0];
288            let length = Self::match_length(base, offset, target, pos);
289            if length > best_length {
290                best_length = length;
291                best_offset = offset;
292            }
293            if length == target_remaining {
294                return Some((best_offset, best_length));
295            }
296            examined += 1;
297        }
298
299        let remaining_budget = MAX_MATCH_CANDIDATES - examined;
300        let start = offsets.len().saturating_sub(remaining_budget);
301        for &offset in &offsets[start..] {
302            let length = Self::match_length(base, offset, target, pos);
303            if length > best_length {
304                best_length = length;
305                best_offset = offset;
306            }
307            if length == target_remaining {
308                break;
309            }
310        }
311
312        if best_length >= min_match {
313            Some((best_offset, best_length))
314        } else {
315            None
316        }
317    }
318
319    fn match_length(base: &[u8], base_pos: usize, target: &[u8], target_pos: usize) -> usize {
320        let max_len = (base.len() - base_pos).min(target.len() - target_pos);
321        let mut len = 0;
322        while len + MATCH_CHUNK_SIZE <= max_len
323            && base[base_pos + len..base_pos + len + MATCH_CHUNK_SIZE]
324                == target[target_pos + len..target_pos + len + MATCH_CHUNK_SIZE]
325        {
326            len += MATCH_CHUNK_SIZE;
327        }
328        while len < max_len && base[base_pos + len] == target[target_pos + len] {
329            len += 1;
330        }
331        len
332    }
333}
334
335impl Default for DeltaEncoder {
336    fn default() -> Self {
337        Self::new()
338    }
339}