Skip to main content

delta/
apply.rs

1use crate::types::{Command, DeltaError, PlacedCommand};
2
3/// Compute the total output size of algorithm commands.
4pub fn output_size(commands: &[Command]) -> usize {
5    commands
6        .iter()
7        .map(|cmd| match cmd {
8            Command::Copy { length, .. } => *length,
9            Command::Add { data } => data.len(),
10        })
11        .sum()
12}
13
14/// Convert algorithm output to placed commands with sequential destinations.
15///
16/// Consumes the command list so that `Add` payloads are moved, not copied.
17pub fn place_commands(commands: Vec<Command>) -> Vec<PlacedCommand> {
18    let mut placed = Vec::with_capacity(commands.len());
19    let mut dst = 0;
20    for cmd in commands {
21        match cmd {
22            Command::Copy { offset, length } => {
23                placed.push(PlacedCommand::Copy { src: offset, dst, length });
24                dst += length;
25            }
26            Command::Add { data } => {
27                let len = data.len();
28                placed.push(PlacedCommand::Add { dst, data }); // move, no clone
29                dst += len;
30            }
31        }
32    }
33    placed
34}
35
36/// Convert placed commands back to algorithm commands (strip destinations).
37///
38/// Commands are sorted by destination offset to recover original sequential
39/// order, then each PlacedCopy/PlacedAdd is converted to Copy/Add.
40///
41/// Consumes the placed list so that `Add` payloads are moved, not copied.
42///
43/// # Panics
44///
45/// Panics if any `PlacedCommand::Move` is present.  Move commands are DLT\x04-only
46/// and have no algorithm-level equivalent; they must not be unplaced.
47pub fn unplace_commands(mut placed: Vec<PlacedCommand>) -> Vec<Command> {
48    placed.sort_by_key(|c| match c {
49        PlacedCommand::Copy { dst, .. } => *dst,
50        PlacedCommand::Add  { dst, .. } => *dst,
51        PlacedCommand::Move { dst, .. } => *dst,
52    });
53    placed
54        .into_iter()
55        .map(|c| match c {
56            PlacedCommand::Copy { src, length, .. } => Command::Copy { offset: src, length },
57            PlacedCommand::Add  { data, .. }        => Command::Add { data },
58            PlacedCommand::Move { .. } => panic!(
59                "unplace_commands: Move has no algorithm-level equivalent; \
60                 Move commands are DLT\\x04-only and cannot be unplaced"
61            ),
62        })
63        .collect()
64}
65
66/// Apply placed commands in standard mode: read from R, write to out.
67///
68/// Returns the number of bytes written.
69pub fn apply_placed_to(r: &[u8], commands: &[PlacedCommand], out: &mut [u8]) -> usize {
70    let mut max_written = 0;
71    for cmd in commands {
72        match cmd {
73            PlacedCommand::Copy { src, dst, length } => {
74                out[*dst..*dst + *length].copy_from_slice(&r[*src..*src + *length]);
75                let end = dst + length;
76                if end > max_written { max_written = end; }
77            }
78            PlacedCommand::Add { dst, data } => {
79                out[*dst..*dst + data.len()].copy_from_slice(data);
80                let end = dst + data.len();
81                if end > max_written { max_written = end; }
82            }
83            PlacedCommand::Move { src, dst, length } => {
84                // LZ77-style self-referential copy: reads from already-written output.
85                // Encoder invariant: this command must appear after all commands that
86                // write [src, src+length); validate_placed_commands checks src+length<=dst
87                // (necessary) but not execution ordering (encoder's responsibility).
88                out.copy_within(*src..*src + *length, *dst);
89                let end = dst + length;
90                if end > max_written { max_written = end; }
91            }
92        }
93    }
94    max_written
95}
96
97/// Apply placed commands in-place within a single buffer.
98///
99/// Uses `copy_within` (maps to libc `memmove`) so overlapping src/dst is safe.
100pub fn apply_placed_inplace_to(commands: &[PlacedCommand], buf: &mut [u8]) {
101    for cmd in commands {
102        match cmd {
103            PlacedCommand::Copy { src, dst, length } => {
104                buf.copy_within(*src..*src + *length, *dst);
105            }
106            PlacedCommand::Add { dst, data } => {
107                buf[*dst..*dst + data.len()].copy_from_slice(data);
108            }
109            PlacedCommand::Move { src, dst, length } => {
110                // Same as Copy in the inplace buffer; copy_within handles overlaps.
111                buf.copy_within(*src..*src + *length, *dst);
112            }
113        }
114    }
115}
116
117/// Validate placed commands before apply so malformed deltas fail cleanly.
118pub fn validate_placed_commands(
119    commands: &[PlacedCommand],
120    reference_size: usize,
121    version_size: usize,
122    inplace: bool,
123) -> Result<(), DeltaError> {
124    let source_limit = if inplace {
125        reference_size.max(version_size)
126    } else {
127        reference_size
128    };
129    for cmd in commands {
130        match cmd {
131            PlacedCommand::Copy { src, dst, length } => {
132                validate_apply_range(*dst, *length, version_size, "copy destination")?;
133                validate_apply_range(*src, *length, source_limit, "copy source")?;
134            }
135            PlacedCommand::Add { dst, data } => {
136                validate_apply_range(*dst, data.len(), version_size, "add destination")?;
137            }
138            PlacedCommand::Move { src, dst, length } => {
139                validate_apply_range(*dst, *length, version_size, "move destination")?;
140                validate_apply_range(*src, *length, version_size, "move source")?;
141                // Necessary geometric constraint: src + length <= dst.
142                // This prevents self-referential loops but does NOT prove the src
143                // region has been written; that is the encoder's responsibility
144                // (Move commands must follow all commands writing their src region).
145                if src + length > *dst {
146                    return Err(DeltaError::InvalidFormat(format!(
147                        "move src+len ({}+{}) > dst ({}): source not yet written",
148                        src, length, dst
149                    )));
150                }
151            }
152        }
153    }
154    Ok(())
155}
156
157// ── convenience wrappers (Command → output) ─────────────────────────────
158
159/// Apply algorithm commands, writing into a pre-allocated buffer.
160///
161/// Returns the number of bytes written.
162pub fn apply_delta_to(r: &[u8], commands: &[Command], out: &mut [u8]) -> usize {
163    let mut pos = 0;
164    for cmd in commands {
165        match cmd {
166            Command::Add { data } => {
167                out[pos..pos + data.len()].copy_from_slice(data);
168                pos += data.len();
169            }
170            Command::Copy { offset, length } => {
171                out[pos..pos + *length].copy_from_slice(&r[*offset..*offset + *length]);
172                pos += *length;
173            }
174        }
175    }
176    pos
177}
178
179/// Reconstruct the version from reference + algorithm commands.
180pub fn apply_delta(r: &[u8], commands: &[Command]) -> Vec<u8> {
181    let mut out = vec![0u8; output_size(commands)];
182    apply_delta_to(r, commands, &mut out);
183    out
184}
185
186/// Apply placed in-place commands to a buffer initialized with R.
187pub fn apply_delta_inplace(
188    r: &[u8],
189    commands: &[PlacedCommand],
190    version_size: usize,
191) -> Vec<u8> {
192    let buf_size = r.len().max(version_size);
193    let mut buf = vec![0u8; buf_size];
194    buf[..r.len()].copy_from_slice(r);
195    apply_placed_inplace_to(commands, &mut buf);
196    buf.truncate(version_size);
197    buf
198}
199
200fn validate_apply_range(
201    start: usize,
202    length: usize,
203    limit: usize,
204    name: &str,
205) -> Result<(), DeltaError> {
206    if start > limit || length > limit.saturating_sub(start) {
207        return Err(DeltaError::InvalidFormat(format!("{} out of range", name)));
208    }
209    Ok(())
210}