Skip to main content

delta/
encoding.rs

1use crate::types::{
2    DeltaError, PlacedCommand,
3    DELTA_ADD_HEADER, DELTA_BIGADD_HEADER, DELTA_BIGCOPY_PAYLOAD,
4    DELTA_CMD_ADD, DELTA_CMD_BIGADD, DELTA_CMD_BIGCOPY, DELTA_CMD_BIGMOVE,
5    DELTA_CMD_COPY, DELTA_CMD_END, DELTA_CMD_MOVE,
6    DELTA_COPY_PAYLOAD, DELTA_CRC_SIZE, DELTA_FLAG_INPLACE,
7    DELTA_HEADER_SIZE, DELTA_HEADER_SIZE_LARGE,
8    DELTA_MAGIC, DELTA_MAGIC_LARGE,
9    DELTA_U32_SIZE, DELTA_U64_SIZE,
10};
11
12/// Maximum value that fits in a u32, as usize, for per-command size selection.
13const U32_MAX: usize = u32::MAX as usize;
14
15fn check_u32(val: usize, field: &str) -> Result<u32, DeltaError> {
16    u32::try_from(val)
17        .map_err(|_| DeltaError::InvalidFormat(format!("{field} exceeds 4 GiB (32-bit format limit)")))
18}
19
20// ── Encoding ─────────────────────────────────────────────────────────────────
21
22/// Encode placed commands to the DLT\x03 binary delta format.
23///
24/// DLT\x03 header: magic(4) + flags(1) + version_size(u32 BE) + crcs(16) = 25 bytes.
25/// Commands: END(0), COPY(1, u32×3), ADD(2, u32×2 + data).
26/// All offsets must fit in u32; returns Err if any exceed 4 GiB.
27pub fn encode_delta(
28    commands: &[PlacedCommand],
29    inplace: bool,
30    version_size: usize,
31    src_crc: &[u8; DELTA_CRC_SIZE],
32    dst_crc: &[u8; DELTA_CRC_SIZE],
33) -> Result<Vec<u8>, DeltaError> {
34    let mut out = Vec::new();
35    out.extend_from_slice(DELTA_MAGIC);
36    out.push(if inplace { DELTA_FLAG_INPLACE } else { 0 });
37    out.extend_from_slice(&check_u32(version_size, "version_size")?.to_be_bytes());
38    out.extend_from_slice(src_crc);
39    out.extend_from_slice(dst_crc);
40
41    for cmd in commands {
42        match cmd {
43            PlacedCommand::Copy { src, dst, length } => {
44                out.push(DELTA_CMD_COPY);
45                out.extend_from_slice(&check_u32(*src,    "copy src offset")?.to_be_bytes());
46                out.extend_from_slice(&check_u32(*dst,    "copy dst offset")?.to_be_bytes());
47                out.extend_from_slice(&check_u32(*length, "copy length")?.to_be_bytes());
48            }
49            PlacedCommand::Add { dst, data } => {
50                out.push(DELTA_CMD_ADD);
51                out.extend_from_slice(&check_u32(*dst,       "add dst offset")?.to_be_bytes());
52                out.extend_from_slice(&check_u32(data.len(), "add length")?.to_be_bytes());
53                out.extend_from_slice(data);
54            }
55            PlacedCommand::Move { .. } => {
56                return Err(DeltaError::InvalidFormat(
57                    "MOVE commands require DLT\\x04 format; use encode_delta_large".into(),
58                ));
59            }
60        }
61    }
62
63    out.push(DELTA_CMD_END);
64    Ok(out)
65}
66
67/// Encode placed commands to the DLT\x04 binary delta format.
68///
69/// DLT\x04 header: magic(4) + flags(1) + version_size(u64 BE) + crcs(16) = 29 bytes.
70/// Commands: per-command size selection — COPY(1)/BIGCOPY(3), ADD(2)/BIGADD(4),
71/// MOVE(5)/BIGMOVE(6).  Small variants use u32 fields; big variants use u64 fields.
72/// Fields ≤ U32_MAX get the small variant; larger fields get the big variant.
73///
74/// This function is **infallible**: `usize → u64` is a widening conversion on all
75/// platforms Rust supports (Rust guarantees `usize` ≤ 64 bits), so no field can
76/// overflow a u64.  Contrast with `encode_delta` (DLT\x03), which returns `Result`
77/// because `usize → u32` truncates on 64-bit platforms.
78pub fn encode_delta_large(
79    commands: &[PlacedCommand],
80    inplace: bool,
81    version_size: usize,
82    src_crc: &[u8; DELTA_CRC_SIZE],
83    dst_crc: &[u8; DELTA_CRC_SIZE],
84    force_large: bool,
85) -> Vec<u8> {
86    let mut out = Vec::new();
87    out.extend_from_slice(DELTA_MAGIC_LARGE);
88    out.push(if inplace { DELTA_FLAG_INPLACE } else { 0 });
89    out.extend_from_slice(&(version_size as u64).to_be_bytes());
90    out.extend_from_slice(src_crc);
91    out.extend_from_slice(dst_crc);
92
93    for cmd in commands {
94        match cmd {
95            PlacedCommand::Copy { src, dst, length } => {
96                encode_3field(&mut out, DELTA_CMD_COPY, DELTA_CMD_BIGCOPY, *src, *dst, *length, force_large);
97            }
98            PlacedCommand::Add { dst, data } => {
99                if !force_large && *dst <= U32_MAX && data.len() <= U32_MAX {
100                    out.push(DELTA_CMD_ADD);
101                    out.extend_from_slice(&(*dst       as u32).to_be_bytes());
102                    out.extend_from_slice(&(data.len() as u32).to_be_bytes());
103                } else {
104                    out.push(DELTA_CMD_BIGADD);
105                    out.extend_from_slice(&(*dst       as u64).to_be_bytes());
106                    out.extend_from_slice(&(data.len() as u64).to_be_bytes());
107                }
108                out.extend_from_slice(data);
109            }
110            PlacedCommand::Move { src, dst, length } => {
111                encode_3field(&mut out, DELTA_CMD_MOVE, DELTA_CMD_BIGMOVE, *src, *dst, *length, force_large);
112            }
113        }
114    }
115
116    out.push(DELTA_CMD_END);
117    out
118}
119
120/// Emit a 3-field command (src, dst, length) using small (u32) or big (u64) variant.
121///
122/// Used for COPY/BIGCOPY and MOVE/BIGMOVE, which share identical wire shapes.
123/// When force_large is true the big variant is always emitted.
124#[inline]
125fn encode_3field(out: &mut Vec<u8>, small: u8, big: u8, a: usize, b: usize, c: usize, force_large: bool) {
126    if !force_large && a <= U32_MAX && b <= U32_MAX && c <= U32_MAX {
127        out.push(small);
128        out.extend_from_slice(&(a as u32).to_be_bytes());
129        out.extend_from_slice(&(b as u32).to_be_bytes());
130        out.extend_from_slice(&(c as u32).to_be_bytes());
131    } else {
132        out.push(big);
133        out.extend_from_slice(&(a as u64).to_be_bytes());
134        out.extend_from_slice(&(b as u64).to_be_bytes());
135        out.extend_from_slice(&(c as u64).to_be_bytes());
136    }
137}
138
139// ── Decoding ─────────────────────────────────────────────────────────────────
140
141/// Decode a binary delta (DLT\x03 or DLT\x04).
142///
143/// Returns (commands, inplace, version_size, src_crc, dst_crc).
144/// CRC validation is the caller's responsibility.
145pub fn decode_delta(
146    data: &[u8],
147) -> Result<(Vec<PlacedCommand>, bool, usize, [u8; DELTA_CRC_SIZE], [u8; DELTA_CRC_SIZE]), DeltaError> {
148    if data.len() < 4 {
149        return Err(DeltaError::InvalidFormat("not a delta file".into()));
150    }
151    match &data[..4] {
152        m if m == DELTA_MAGIC    => decode_small(data),
153        m if m == DELTA_MAGIC_LARGE => decode_large(data),
154        _ => Err(DeltaError::InvalidFormat("not a delta file".into())),
155    }
156}
157
158fn decode_small(
159    data: &[u8],
160) -> Result<(Vec<PlacedCommand>, bool, usize, [u8; DELTA_CRC_SIZE], [u8; DELTA_CRC_SIZE]), DeltaError> {
161    if data.len() < DELTA_HEADER_SIZE {
162        return Err(DeltaError::InvalidFormat("not a delta file".into()));
163    }
164    let inplace = data[4] & DELTA_FLAG_INPLACE != 0;
165    let version_size = u32::from_be_bytes([data[5], data[6], data[7], data[8]]) as usize;
166    let crc_offset = 9; // 4 + 1 + 4
167    let mut src_crc = [0u8; DELTA_CRC_SIZE];
168    let mut dst_crc = [0u8; DELTA_CRC_SIZE];
169    src_crc.copy_from_slice(&data[crc_offset..crc_offset + DELTA_CRC_SIZE]);
170    dst_crc.copy_from_slice(&data[crc_offset + DELTA_CRC_SIZE..crc_offset + 2 * DELTA_CRC_SIZE]);
171
172    let mut pos = DELTA_HEADER_SIZE;
173    let mut commands = Vec::new();
174    let mut saw_end = false;
175
176    while pos < data.len() {
177        let t = data[pos];
178        pos += 1;
179        match t {
180            DELTA_CMD_END => { saw_end = true; break; }
181
182            DELTA_CMD_COPY => {
183                if pos + DELTA_COPY_PAYLOAD > data.len() { return Err(DeltaError::UnexpectedEof); }
184                let src    = read_u32(data, pos); pos += DELTA_U32_SIZE;
185                let dst    = read_u32(data, pos); pos += DELTA_U32_SIZE;
186                let length = read_u32(data, pos); pos += DELTA_U32_SIZE;
187                validate_placed_range(dst, length, version_size, "copy")?;
188                commands.push(PlacedCommand::Copy { src, dst, length });
189            }
190
191            DELTA_CMD_ADD => {
192                if pos + DELTA_ADD_HEADER > data.len() { return Err(DeltaError::UnexpectedEof); }
193                let dst    = read_u32(data, pos); pos += DELTA_U32_SIZE;
194                let length = read_u32(data, pos); pos += DELTA_U32_SIZE;
195                if length > data.len() - pos { return Err(DeltaError::UnexpectedEof); }
196                validate_placed_range(dst, length, version_size, "add")?;
197                commands.push(PlacedCommand::Add { dst, data: data[pos..pos + length].to_vec() });
198                pos += length;
199            }
200
201            // V4-only commands arriving in a V3 stream: specific error.
202            DELTA_CMD_BIGCOPY | DELTA_CMD_BIGADD | DELTA_CMD_MOVE | DELTA_CMD_BIGMOVE => {
203                return Err(DeltaError::InvalidFormat(format!(
204                    "command type {} requires DLT\\x04 format", t
205                )));
206            }
207
208            _ => {
209                return Err(DeltaError::InvalidFormat(format!(
210                    "unknown command type: {}", t
211                )));
212            }
213        }
214    }
215
216    finish_decode(commands, saw_end, pos, data.len(), inplace, version_size, src_crc, dst_crc)
217}
218
219fn decode_large(
220    data: &[u8],
221) -> Result<(Vec<PlacedCommand>, bool, usize, [u8; DELTA_CRC_SIZE], [u8; DELTA_CRC_SIZE]), DeltaError> {
222    if data.len() < DELTA_HEADER_SIZE_LARGE {
223        return Err(DeltaError::InvalidFormat("not a delta file".into()));
224    }
225    let inplace = data[4] & DELTA_FLAG_INPLACE != 0;
226    let version_size = u64::from_be_bytes([
227        data[5], data[6], data[7],  data[8],
228        data[9], data[10], data[11], data[12],
229    ]) as usize;
230    let crc_offset = 13; // 4 + 1 + 8
231    let mut src_crc = [0u8; DELTA_CRC_SIZE];
232    let mut dst_crc = [0u8; DELTA_CRC_SIZE];
233    src_crc.copy_from_slice(&data[crc_offset..crc_offset + DELTA_CRC_SIZE]);
234    dst_crc.copy_from_slice(&data[crc_offset + DELTA_CRC_SIZE..crc_offset + 2 * DELTA_CRC_SIZE]);
235
236    let mut pos = DELTA_HEADER_SIZE_LARGE;
237    let mut commands = Vec::new();
238    let mut saw_end = false;
239
240    while pos < data.len() {
241        let t = data[pos];
242        pos += 1;
243        match t {
244            DELTA_CMD_END => { saw_end = true; break; }
245
246            DELTA_CMD_COPY => {
247                if pos + DELTA_COPY_PAYLOAD > data.len() { return Err(DeltaError::UnexpectedEof); }
248                let src    = read_u32(data, pos); pos += DELTA_U32_SIZE;
249                let dst    = read_u32(data, pos); pos += DELTA_U32_SIZE;
250                let length = read_u32(data, pos); pos += DELTA_U32_SIZE;
251                validate_placed_range(dst, length, version_size, "copy")?;
252                commands.push(PlacedCommand::Copy { src, dst, length });
253            }
254
255            DELTA_CMD_ADD => {
256                if pos + DELTA_ADD_HEADER > data.len() { return Err(DeltaError::UnexpectedEof); }
257                let dst    = read_u32(data, pos); pos += DELTA_U32_SIZE;
258                let length = read_u32(data, pos); pos += DELTA_U32_SIZE;
259                if length > data.len() - pos { return Err(DeltaError::UnexpectedEof); }
260                validate_placed_range(dst, length, version_size, "add")?;
261                commands.push(PlacedCommand::Add { dst, data: data[pos..pos + length].to_vec() });
262                pos += length;
263            }
264
265            DELTA_CMD_BIGCOPY => {
266                if pos + DELTA_BIGCOPY_PAYLOAD > data.len() { return Err(DeltaError::UnexpectedEof); }
267                let src    = read_u64(data, pos); pos += DELTA_U64_SIZE;
268                let dst    = read_u64(data, pos); pos += DELTA_U64_SIZE;
269                let length = read_u64(data, pos); pos += DELTA_U64_SIZE;
270                validate_placed_range(dst, length, version_size, "bigcopy")?;
271                commands.push(PlacedCommand::Copy { src, dst, length });
272            }
273
274            DELTA_CMD_BIGADD => {
275                if pos + DELTA_BIGADD_HEADER > data.len() { return Err(DeltaError::UnexpectedEof); }
276                let dst    = read_u64(data, pos); pos += DELTA_U64_SIZE;
277                let length = read_u64(data, pos); pos += DELTA_U64_SIZE;
278                if length > data.len() - pos { return Err(DeltaError::UnexpectedEof); }
279                validate_placed_range(dst, length, version_size, "bigadd")?;
280                commands.push(PlacedCommand::Add { dst, data: data[pos..pos + length].to_vec() });
281                pos += length;
282            }
283
284            DELTA_CMD_MOVE => {
285                if pos + DELTA_COPY_PAYLOAD > data.len() { return Err(DeltaError::UnexpectedEof); }
286                let src    = read_u32(data, pos); pos += DELTA_U32_SIZE;
287                let dst    = read_u32(data, pos); pos += DELTA_U32_SIZE;
288                let length = read_u32(data, pos); pos += DELTA_U32_SIZE;
289                validate_placed_range(dst, length, version_size, "move")?;
290                commands.push(PlacedCommand::Move { src, dst, length });
291            }
292
293            DELTA_CMD_BIGMOVE => {
294                if pos + DELTA_BIGCOPY_PAYLOAD > data.len() { return Err(DeltaError::UnexpectedEof); }
295                let src    = read_u64(data, pos); pos += DELTA_U64_SIZE;
296                let dst    = read_u64(data, pos); pos += DELTA_U64_SIZE;
297                let length = read_u64(data, pos); pos += DELTA_U64_SIZE;
298                validate_placed_range(dst, length, version_size, "bigmove")?;
299                commands.push(PlacedCommand::Move { src, dst, length });
300            }
301
302            _ => {
303                return Err(DeltaError::InvalidFormat(format!(
304                    "unknown command type: {}", t
305                )));
306            }
307        }
308    }
309
310    finish_decode(commands, saw_end, pos, data.len(), inplace, version_size, src_crc, dst_crc)
311}
312
313#[inline]
314fn read_u32(data: &[u8], pos: usize) -> usize {
315    u32::from_be_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]) as usize
316}
317
318#[inline]
319fn read_u64(data: &[u8], pos: usize) -> usize {
320    u64::from_be_bytes([
321        data[pos], data[pos+1], data[pos+2], data[pos+3],
322        data[pos+4], data[pos+5], data[pos+6], data[pos+7],
323    ]) as usize
324}
325
326fn finish_decode(
327    commands: Vec<PlacedCommand>,
328    saw_end: bool,
329    pos: usize,
330    data_len: usize,
331    inplace: bool,
332    version_size: usize,
333    src_crc: [u8; DELTA_CRC_SIZE],
334    dst_crc: [u8; DELTA_CRC_SIZE],
335) -> Result<(Vec<PlacedCommand>, bool, usize, [u8; DELTA_CRC_SIZE], [u8; DELTA_CRC_SIZE]), DeltaError> {
336    if !saw_end {
337        return Err(DeltaError::InvalidFormat("missing END command".into()));
338    }
339    if pos != data_len {
340        return Err(DeltaError::InvalidFormat("trailing data after END".into()));
341    }
342    Ok((commands, inplace, version_size, src_crc, dst_crc))
343}
344
345// ── Utilities ─────────────────────────────────────────────────────────────────
346
347/// Check if binary data is an in-place delta (DLT\x03 or DLT\x04).
348pub fn is_inplace_delta(data: &[u8]) -> bool {
349    if data.len() < 5 {
350        return false;
351    }
352    let magic = &data[..4];
353    (magic == DELTA_MAGIC || magic == DELTA_MAGIC_LARGE)
354        && data[4] & DELTA_FLAG_INPLACE != 0
355}
356
357fn validate_placed_range(dst: usize, length: usize, version_size: usize, kind: &str) -> Result<(), DeltaError> {
358    if dst > version_size || length > version_size.saturating_sub(dst) {
359        return Err(DeltaError::InvalidFormat(format!(
360            "{} command exceeds version size",
361            kind
362        )));
363    }
364    Ok(())
365}