Skip to main content

vole_document/container/
checkpoint.rs

1//! The optional `CHECKPOINT` record: a bounded, advisory per-op output table.
2//!
3//! A checkpoint is a self-describing, little-endian, versioned payload written
4//! with [`crate::container::record::FLAG_OPTIONAL`] into a seekable descriptor
5//! (one that also carries an `OBSERVATION_INDEX` and a seek `DIRECTORY`, which
6//! locates it). It records, for every reconstruction op, that op's absolute
7//! output start offset and length — a random-access boundary table a planner can
8//! consume to select an op window **without** reading the `OBSERVATION_INDEX`
9//! record.
10//!
11//! ```text
12//! checkpoint_v1 :=
13//!     version:u8 = 1
14//!     kind:u8 = 1                # OP_BOUNDARIES
15//!     reserved:u16 = 0
16//!     entry_count:u32 LE
17//!     source_len:u64 LE
18//!     graph_crc32c:u32 LE        # CRC-32C of the GRAPH record payload
19//!     entry[entry_count]: out_start:u64 LE | out_len:u64 LE
20//! ```
21//!
22//! The checkpoint is **advisory, never authority** (mirroring the
23//! `DIRECTORY`/`OBSERVATION_INDEX` discipline of ADR-0019). [`Self::validate`]
24//! re-derives every length from the authoritative reconstruction program with
25//! [`crate::dra::Program::analyze_ops`] and rejects any contradiction, so a
26//! checkpoint can never change a served byte. A lying, corrupt, oversized, or
27//! out-of-closure checkpoint is rejected: the full parser fails closed, and the
28//! seek reader ignores it and falls back to the Phase-8 index lane.
29//!
30//! Unknown version, kind, or a non-zero reserved field fail closed. All
31//! arithmetic is checked.
32
33use crate::dra::Program;
34use crate::error::{Error, Result};
35use crate::integrity::crc32c;
36use crate::limits::Limits;
37
38/// Wire version of the checkpoint payload implemented by this build.
39pub const CHECKPOINT_VERSION: u8 = 1;
40
41/// Checkpoint kind: a per-op output boundary table.
42pub const CHECKPOINT_KIND_OP_BOUNDARIES: u8 = 1;
43
44/// The only checkpoint kind this build understands; any other kind fails closed.
45pub const CHECKPOINT_KNOWN_KINDS: u8 = CHECKPOINT_KIND_OP_BOUNDARIES;
46
47/// Encoded size of the fixed checkpoint header.
48pub const CHECKPOINT_HEADER_LEN: usize = 1 + 1 + 2 + 4 + 8 + 4;
49/// Encoded size of one boundary entry: `out_start u64 | out_len u64`.
50pub const CHECKPOINT_ENTRY_LEN: usize = 16;
51
52/// One op's absolute output boundary: where the op's output begins and how many
53/// bytes it produces.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct CheckpointEntry {
56    /// Absolute output offset at which this op's bytes begin.
57    pub out_start: u64,
58    /// Bytes this op contributes to the output.
59    pub out_len: u64,
60}
61
62/// A decoded `CHECKPOINT` record payload.
63#[derive(Debug, Clone, PartialEq, Eq, Default)]
64pub struct CheckpointTable {
65    /// Checkpoint kind (only [`CHECKPOINT_KIND_OP_BOUNDARIES`] is valid here).
66    pub kind: u8,
67    /// Declared total output length (must equal the source length).
68    pub source_len: u64,
69    /// CRC-32C of the `GRAPH` payload this checkpoint describes.
70    pub graph_crc32c: u32,
71    /// Per-op boundaries, in program order.
72    pub entries: Vec<CheckpointEntry>,
73}
74
75impl CheckpointTable {
76    /// Build an OP_BOUNDARIES checkpoint from the authoritative program.
77    ///
78    /// Per-op lengths come from [`Program::analyze_ops`]; the boundaries are their
79    /// running sum. `graph_crc32c` is the CRC-32C of `program.encode()` — the
80    /// exact `GRAPH` payload the checkpoint is bound to. No allocation is sized by
81    /// an untrusted count: the vector is bounded by [`Limits::max_graph_ops`].
82    pub fn from_program(
83        program: &Program,
84        object_lens: &[u64],
85        channel_lens: &[u64],
86        source_len: u64,
87        limits: Limits,
88    ) -> Result<CheckpointTable> {
89        if program.ops.len() as u64 > u64::from(limits.max_graph_ops) {
90            return Err(Error::resource_limit(
91                "checkpoint entry count exceeds the graph-op limit",
92            ));
93        }
94        let per_op = program.analyze_ops(object_lens, channel_lens, limits)?;
95        if per_op.len() != program.ops.len() {
96            return Err(Error::internal_invariant(
97                "analyze_ops returned a different op count",
98            ));
99        }
100        let mut entries = Vec::with_capacity(per_op.len());
101        let mut acc: u64 = 0;
102        for &len in &per_op {
103            entries.push(CheckpointEntry {
104                out_start: acc,
105                out_len: len,
106            });
107            acc = acc
108                .checked_add(len)
109                .ok_or_else(|| Error::invalid_graph("checkpoint output length overflow"))?;
110        }
111        if acc != source_len {
112            return Err(Error::coverage_violation(format!(
113                "checkpoint boundaries sum to {acc} but the source is {source_len} bytes"
114            )));
115        }
116        let graph_crc32c = crc32c(&program.encode()?);
117        Ok(CheckpointTable {
118            kind: CHECKPOINT_KIND_OP_BOUNDARIES,
119            source_len,
120            graph_crc32c,
121            entries,
122        })
123    }
124
125    /// The per-op output lengths, in program order.
126    pub fn lengths(&self) -> Vec<u64> {
127        self.entries.iter().map(|e| e.out_len).collect()
128    }
129
130    /// Encode the payload to canonical bytes.
131    pub fn encode(&self) -> Result<Vec<u8>> {
132        if self.kind != CHECKPOINT_KIND_OP_BOUNDARIES {
133            return Err(Error::invalid_container("checkpoint has an unknown kind"));
134        }
135        let count = u32::try_from(self.entries.len())
136            .map_err(|_| Error::resource_limit("checkpoint entry_count exceeds u32"))?;
137        let mut out =
138            Vec::with_capacity(CHECKPOINT_HEADER_LEN + self.entries.len() * CHECKPOINT_ENTRY_LEN);
139        out.push(CHECKPOINT_VERSION);
140        out.push(self.kind);
141        out.extend_from_slice(&0u16.to_le_bytes());
142        out.extend_from_slice(&count.to_le_bytes());
143        out.extend_from_slice(&self.source_len.to_le_bytes());
144        out.extend_from_slice(&self.graph_crc32c.to_le_bytes());
145        for e in &self.entries {
146            out.extend_from_slice(&e.out_start.to_le_bytes());
147            out.extend_from_slice(&e.out_len.to_le_bytes());
148        }
149        Ok(out)
150    }
151
152    /// Decode and structurally bound a payload.
153    ///
154    /// This checks the size bound, the version, the kind, the reserved field, and
155    /// that the declared entry count fits the payload. It does **not** check
156    /// agreement with a program; call [`CheckpointTable::validate`] for that.
157    pub fn decode(bytes: &[u8], limits: Limits) -> Result<CheckpointTable> {
158        if bytes.len() as u64 > u64::from(limits.max_checkpoint_bytes) {
159            return Err(Error::resource_limit(format!(
160                "checkpoint payload {} exceeds limit {}",
161                bytes.len(),
162                limits.max_checkpoint_bytes
163            )));
164        }
165        if bytes.len() < CHECKPOINT_HEADER_LEN {
166            return Err(Error::invalid_container("truncated CHECKPOINT header"));
167        }
168        let version = bytes[0];
169        if version != CHECKPOINT_VERSION {
170            return Err(Error::unsupported_version(format!(
171                "checkpoint version {version} is not supported"
172            )));
173        }
174        let kind = bytes[1];
175        if kind & !CHECKPOINT_KNOWN_KINDS != 0 {
176            return Err(Error::invalid_container("CHECKPOINT has an unknown kind"));
177        }
178        let reserved = u16::from_le_bytes([bytes[2], bytes[3]]);
179        if reserved != 0 {
180            return Err(Error::invalid_container(
181                "CHECKPOINT reserved field must be zero",
182            ));
183        }
184        let count = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
185        if count > limits.max_graph_ops {
186            return Err(Error::resource_limit(format!(
187                "checkpoint entry_count {count} exceeds limit {}",
188                limits.max_graph_ops
189            )));
190        }
191        let source_len = u64::from_le_bytes([
192            bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
193        ]);
194        let graph_crc32c = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
195
196        let count = count as usize;
197        let need = count
198            .checked_mul(CHECKPOINT_ENTRY_LEN)
199            .ok_or_else(|| Error::invalid_container("CHECKPOINT section length overflow"))?;
200        let available = bytes.len() - CHECKPOINT_HEADER_LEN;
201        if need != available {
202            return Err(Error::invalid_container(format!(
203                "CHECKPOINT declares {count} entries ({need} bytes) but {} bytes follow",
204                available
205            )));
206        }
207        let mut entries = Vec::with_capacity(count);
208        let mut p = CHECKPOINT_HEADER_LEN;
209        for _ in 0..count {
210            let out_start = u64::from_le_bytes([
211                bytes[p],
212                bytes[p + 1],
213                bytes[p + 2],
214                bytes[p + 3],
215                bytes[p + 4],
216                bytes[p + 5],
217                bytes[p + 6],
218                bytes[p + 7],
219            ]);
220            let out_len = u64::from_le_bytes([
221                bytes[p + 8],
222                bytes[p + 9],
223                bytes[p + 10],
224                bytes[p + 11],
225                bytes[p + 12],
226                bytes[p + 13],
227                bytes[p + 14],
228                bytes[p + 15],
229            ]);
230            p += CHECKPOINT_ENTRY_LEN;
231            entries.push(CheckpointEntry { out_start, out_len });
232        }
233        Ok(CheckpointTable {
234            kind,
235            source_len,
236            graph_crc32c,
237            entries,
238        })
239    }
240
241    /// Validate the checkpoint against the authoritative program and graph.
242    ///
243    /// Requires: the declared kind is OP_BOUNDARIES; `graph_payload`'s CRC-32C
244    /// equals the checkpoint's binding; the declared `source_len` matches; the
245    /// entry count matches the program; the entries are contiguous and cover the
246    /// source exactly; and every entry's `out_len` equals the program's own
247    /// [`Program::analyze_ops`] view. Any disagreement is a typed rejection — the
248    /// checkpoint is never trusted.
249    pub fn validate(
250        &self,
251        program: &Program,
252        graph_payload: &[u8],
253        source_len: u64,
254        object_lens: &[u64],
255        channel_lens: &[u64],
256        limits: Limits,
257    ) -> Result<()> {
258        if self.kind != CHECKPOINT_KIND_OP_BOUNDARIES {
259            return Err(Error::invalid_container("checkpoint has an unknown kind"));
260        }
261        if self.graph_crc32c != crc32c(graph_payload) {
262            return Err(Error::invalid_container(
263                "checkpoint GRAPH-CRC binding disagrees with the GRAPH record",
264            ));
265        }
266        if self.source_len != source_len {
267            return Err(Error::integrity_mismatch(format!(
268                "checkpoint declares source length {} but the descriptor declares {source_len}",
269                self.source_len
270            )));
271        }
272        if self.entries.len() != program.ops.len() {
273            return Err(Error::invalid_container(format!(
274                "checkpoint lists {} boundaries but the program has {} ops",
275                self.entries.len(),
276                program.ops.len()
277            )));
278        }
279        // Contiguity and exact coverage of the source.
280        let mut acc: u64 = 0;
281        for (i, e) in self.entries.iter().enumerate() {
282            if e.out_start != acc {
283                return Err(Error::invalid_container(format!(
284                    "checkpoint boundary {i} starts at {} but {acc} was expected",
285                    e.out_start
286                )));
287            }
288            acc = acc
289                .checked_add(e.out_len)
290                .ok_or_else(|| Error::invalid_container("checkpoint output length overflow"))?;
291        }
292        if acc != source_len {
293            return Err(Error::coverage_violation(format!(
294                "checkpoint boundaries sum to {acc} but the source is {source_len} bytes"
295            )));
296        }
297        // The program is authority: re-derive every op length and reject any lie.
298        let per_op = program.analyze_ops(object_lens, channel_lens, limits)?;
299        if per_op.len() != self.entries.len() {
300            return Err(Error::invalid_container(
301                "checkpoint entry count disagrees with analyze_ops",
302            ));
303        }
304        for (i, (e, want)) in self.entries.iter().zip(&per_op).enumerate() {
305            if e.out_len != *want {
306                return Err(Error::coverage_violation(format!(
307                    "checkpoint boundary {i} claims {} bytes but the program produces {want}",
308                    e.out_len
309                )));
310            }
311        }
312        Ok(())
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use crate::dra::Op;
320
321    fn tiny_program() -> Program {
322        Program::new(vec![
323            Op::EmitObject { object_id: 0 },
324            Op::Inline {
325                bytes: b"de".to_vec(),
326            },
327        ])
328    }
329
330    fn tiny_table() -> CheckpointTable {
331        CheckpointTable::from_program(&tiny_program(), &[3], &[], 5, Limits::DEFAULT).unwrap()
332    }
333
334    #[test]
335    fn from_program_builds_contiguous_boundaries() {
336        let t = tiny_table();
337        assert_eq!(t.kind, CHECKPOINT_KIND_OP_BOUNDARIES);
338        assert_eq!(t.source_len, 5);
339        assert_eq!(t.lengths(), vec![3, 2]);
340        assert_eq!(t.entries[0].out_start, 0);
341        assert_eq!(t.entries[1].out_start, 3);
342    }
343
344    #[test]
345    fn roundtrip() {
346        let t = tiny_table();
347        let bytes = t.encode().unwrap();
348        assert_eq!(
349            bytes.len(),
350            CHECKPOINT_HEADER_LEN + 2 * CHECKPOINT_ENTRY_LEN
351        );
352        let back = CheckpointTable::decode(&bytes, Limits::DEFAULT).unwrap();
353        assert_eq!(back, t);
354    }
355
356    #[test]
357    fn validate_accepts_an_honest_checkpoint() {
358        let program = tiny_program();
359        let t = tiny_table();
360        t.validate(
361            &program,
362            &program.encode().unwrap(),
363            5,
364            &[3],
365            &[],
366            Limits::DEFAULT,
367        )
368        .unwrap();
369    }
370
371    #[test]
372    fn validate_rejects_a_wrong_length() {
373        let program = tiny_program();
374        let mut t = tiny_table();
375        t.entries[1].out_len = 3;
376        t.entries[1].out_start = 3;
377        t.source_len = 6;
378        // Contiguity/coverage hold internally, but the program disagrees.
379        assert_eq!(
380            t.validate(
381                &program,
382                &program.encode().unwrap(),
383                6,
384                &[3],
385                &[],
386                Limits::DEFAULT
387            )
388            .unwrap_err()
389            .class(),
390            crate::ErrorClass::CoverageViolation
391        );
392    }
393
394    #[test]
395    fn validate_rejects_a_foreign_graph_binding() {
396        let program = tiny_program();
397        let t = tiny_table();
398        assert_eq!(
399            t.validate(&program, b"not the graph", 5, &[3], &[], Limits::DEFAULT)
400                .unwrap_err()
401                .class(),
402            crate::ErrorClass::InvalidContainer
403        );
404    }
405
406    #[test]
407    fn unknown_version_and_kind_fail_closed() {
408        let mut bytes = tiny_table().encode().unwrap();
409        bytes[0] = 9;
410        assert_eq!(
411            CheckpointTable::decode(&bytes, Limits::DEFAULT)
412                .unwrap_err()
413                .class(),
414            crate::ErrorClass::UnsupportedVersion
415        );
416        let mut bytes = tiny_table().encode().unwrap();
417        bytes[1] = 0x80;
418        assert_eq!(
419            CheckpointTable::decode(&bytes, Limits::DEFAULT)
420                .unwrap_err()
421                .class(),
422            crate::ErrorClass::InvalidContainer
423        );
424    }
425
426    #[test]
427    fn truncation_and_over_length_are_rejected() {
428        let bytes = tiny_table().encode().unwrap();
429        assert_eq!(
430            CheckpointTable::decode(&bytes[..bytes.len() - 1], Limits::DEFAULT)
431                .unwrap_err()
432                .class(),
433            crate::ErrorClass::InvalidContainer
434        );
435        let limits = Limits {
436            max_checkpoint_bytes: 8,
437            ..Limits::DEFAULT
438        };
439        assert_eq!(
440            CheckpointTable::decode(&bytes, limits).unwrap_err().class(),
441            crate::ErrorClass::ResourceLimit
442        );
443    }
444}