lex_store/delta.rs
1//! Delta encoding for stage canonical bytes (#261 slice 3).
2//!
3//! Each implementation lives at
4//! `<root>/stages/<sig>/implementations/<stage_id>.ast.json` —
5//! the canonical bytes of the AST, content-addressed by `stage_id`.
6//! For most edits, two consecutive stages share a long byte prefix
7//! and a long byte suffix (e.g. body changes inside the same fn
8//! shape), so storing every stage as a full file is wasteful.
9//!
10//! Slice 3 ships an opt-in delta format: when a stage's diff
11//! against an existing parent stage is below
12//! [`DELTA_RATIO_THRESHOLD`], persist `<stage_id>.delta.json`
13//! holding `(base_stage_id, common_prefix_len, common_suffix_len,
14//! middle_bytes_hex)` instead of the full bytes. Reconstruction
15//! splices `base_bytes[..prefix] + middle + base_bytes[tail..]`.
16//!
17//! Chain length is capped at [`DELTA_CHAIN_CAP`]; once a chain
18//! reaches the cap, the next stage is materialized as a full
19//! snapshot so reconstruction stays O(1) per access in the limit.
20//!
21//! # Determinism
22//!
23//! The delta format is *not* canonical — multiple `(prefix,
24//! suffix, middle)` decompositions of the same byte change are
25//! valid. We pick the largest common prefix, then the largest
26//! suffix that doesn't overlap the prefix, so the format is
27//! single-valued for a given `(base_bytes, new_bytes)` pair.
28
29use serde::{Deserialize, Serialize};
30
31/// Maximum length of a delta chain. Past this, [`encode`] yields
32/// `None` so the caller writes a full snapshot. The cap is a
33/// pragmatic balance between disk savings and reconstruction cost
34/// — 32 keeps the worst-case `get_ast` at 32 file reads + 32
35/// splices, which dominates over filesystem latency.
36pub const DELTA_CHAIN_CAP: usize = 32;
37
38/// A new stage is delta-encoded when the *middle* (non-shared)
39/// bytes are at most this fraction of the new stage's size.
40/// Below the threshold the delta is a clear win; above it, the
41/// metadata overhead and the indirection cost of reconstruction
42/// usually outweigh the byte savings.
43pub const DELTA_RATIO_THRESHOLD: f64 = 0.5;
44
45/// On-disk format of a delta-encoded stage.
46///
47/// File path: `<sig>/implementations/<stage_id>.delta.json`.
48/// The pair `(common_prefix, common_suffix)` must satisfy
49/// `prefix + suffix <= base_bytes.len()` — they describe a
50/// non-overlapping splice into the base.
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct StageDelta {
53 /// `stage_id` of the previous stage these bytes are diff'd
54 /// against. Either holds full bytes (`.ast.json`) or its own
55 /// delta — reconstruction recurses.
56 pub base_stage_id: String,
57 /// Length of the chain ending at *this* stage. A delta against
58 /// a full snapshot has `chain_length: 1`; chained deltas
59 /// increment from there.
60 pub chain_length: usize,
61 /// Number of bytes shared at the start with `base`.
62 pub common_prefix: usize,
63 /// Number of bytes shared at the end with `base`.
64 pub common_suffix: usize,
65 /// Lowercase-hex of the replacement bytes for the middle.
66 /// Empty hex string means "delete the middle, splice prefix
67 /// directly to suffix" — happens when the new bytes are a
68 /// pure deletion against the base.
69 pub middle_hex: String,
70}
71
72/// Compute the splice between `base` and `new`. Always succeeds —
73/// degenerate inputs (identical bytes, total replacement) produce
74/// valid but trivial deltas. The caller decides whether the
75/// resulting middle is small enough to be worth storing as a
76/// delta versus a full snapshot.
77pub fn splice(base: &[u8], new: &[u8]) -> (usize, usize, Vec<u8>) {
78 let prefix_len = common_prefix_len(base, new);
79 // Suffix can't overlap the prefix in either side: cap at the
80 // remaining length of each.
81 let max_suffix = std::cmp::min(
82 base.len().saturating_sub(prefix_len),
83 new.len().saturating_sub(prefix_len),
84 );
85 let suffix_len = common_suffix_len(
86 &base[base.len() - max_suffix..],
87 &new[new.len() - max_suffix..],
88 );
89 let middle = new[prefix_len..new.len() - suffix_len].to_vec();
90 (prefix_len, suffix_len, middle)
91}
92
93/// Apply a splice to `base`, producing the reconstructed `new`
94/// bytes. Returns an error when the prefix+suffix lengths would
95/// overflow the base — guards against tampered or corrupt
96/// `.delta.json` files.
97pub fn apply(base: &[u8], delta: &StageDelta) -> Result<Vec<u8>, DeltaError> {
98 let middle = hex::decode(&delta.middle_hex)
99 .map_err(|e| DeltaError::InvalidHex(e.to_string()))?;
100 if delta.common_prefix + delta.common_suffix > base.len() {
101 return Err(DeltaError::OverlappingSplice {
102 base_len: base.len(),
103 prefix: delta.common_prefix,
104 suffix: delta.common_suffix,
105 });
106 }
107 let prefix = &base[..delta.common_prefix];
108 let suffix_start = base.len() - delta.common_suffix;
109 let suffix = &base[suffix_start..];
110 let mut out = Vec::with_capacity(prefix.len() + middle.len() + suffix.len());
111 out.extend_from_slice(prefix);
112 out.extend(middle);
113 out.extend_from_slice(suffix);
114 Ok(out)
115}
116
117/// Decide whether the delta is worth storing. `middle_len` is the
118/// length of the splice's replacement bytes; `new_len` is the
119/// total length of the new bytes. Returns `true` when the ratio
120/// is below [`DELTA_RATIO_THRESHOLD`] *and* the chain length isn't
121/// at the cap.
122pub fn is_worth_encoding(middle_len: usize, new_len: usize, chain_length: usize) -> bool {
123 if chain_length > DELTA_CHAIN_CAP {
124 return false;
125 }
126 if new_len == 0 {
127 return false;
128 }
129 (middle_len as f64) / (new_len as f64) < DELTA_RATIO_THRESHOLD
130}
131
132fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
133 a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
134}
135
136fn common_suffix_len(a: &[u8], b: &[u8]) -> usize {
137 a.iter()
138 .rev()
139 .zip(b.iter().rev())
140 .take_while(|(x, y)| x == y)
141 .count()
142}
143
144#[derive(Debug, thiserror::Error)]
145pub enum DeltaError {
146 #[error("delta middle is not valid hex: {0}")]
147 InvalidHex(String),
148 #[error("delta splice overflows base: base_len={base_len}, prefix={prefix}, suffix={suffix}")]
149 OverlappingSplice {
150 base_len: usize,
151 prefix: usize,
152 suffix: usize,
153 },
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn splice_then_apply_round_trips() {
162 let base = b"hello, the quick brown fox jumps over the lazy dog";
163 let new = b"hello, the quick green fox jumps over the lazy dog";
164 let (p, s, m) = splice(base, new);
165 let delta = StageDelta {
166 base_stage_id: "x".into(),
167 chain_length: 1,
168 common_prefix: p,
169 common_suffix: s,
170 middle_hex: hex::encode(&m),
171 };
172 let reconstructed = apply(base, &delta).unwrap();
173 assert_eq!(reconstructed, new);
174 }
175
176 #[test]
177 fn identical_bytes_yield_empty_middle() {
178 let base = b"unchanged";
179 let new = b"unchanged";
180 let (p, s, m) = splice(base, new);
181 assert_eq!(p, 9);
182 assert_eq!(s, 0);
183 assert!(m.is_empty(), "no middle when bytes are identical");
184 // Apply gives back base.
185 let delta = StageDelta {
186 base_stage_id: "x".into(),
187 chain_length: 1,
188 common_prefix: p,
189 common_suffix: s,
190 middle_hex: hex::encode(&m),
191 };
192 assert_eq!(apply(base, &delta).unwrap(), base);
193 }
194
195 #[test]
196 fn pure_insertion_is_pure_middle() {
197 let base = b"abXY";
198 let new = b"abZZZZXY";
199 let (p, s, m) = splice(base, new);
200 assert_eq!(p, 2);
201 assert_eq!(s, 2);
202 assert_eq!(m, b"ZZZZ");
203 }
204
205 #[test]
206 fn pure_deletion_yields_empty_middle() {
207 let base = b"abZZZZXY";
208 let new = b"abXY";
209 let (p, s, m) = splice(base, new);
210 assert_eq!(p, 2);
211 assert_eq!(s, 2);
212 assert!(m.is_empty());
213 let delta = StageDelta {
214 base_stage_id: "x".into(),
215 chain_length: 1,
216 common_prefix: p,
217 common_suffix: s,
218 middle_hex: hex::encode(&m),
219 };
220 assert_eq!(apply(base, &delta).unwrap(), new);
221 }
222
223 #[test]
224 fn is_worth_encoding_respects_threshold() {
225 // Middle is 30% of new — under 50% threshold.
226 assert!(is_worth_encoding(30, 100, 1));
227 // 60% — over threshold.
228 assert!(!is_worth_encoding(60, 100, 1));
229 // Chain length cap.
230 assert!(!is_worth_encoding(1, 100, DELTA_CHAIN_CAP + 1));
231 }
232
233 #[test]
234 fn apply_refuses_overlapping_splice() {
235 let base = b"short";
236 let delta = StageDelta {
237 base_stage_id: "x".into(),
238 chain_length: 1,
239 common_prefix: 4,
240 common_suffix: 4, // 4 + 4 > 5 — invalid
241 middle_hex: String::new(),
242 };
243 assert!(apply(base, &delta).is_err());
244 }
245}