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