1use crate::dra::Program;
34use crate::error::{Error, Result};
35use crate::integrity::crc32c;
36use crate::limits::Limits;
37
38pub const CHECKPOINT_VERSION: u8 = 1;
40
41pub const CHECKPOINT_KIND_OP_BOUNDARIES: u8 = 1;
43
44pub const CHECKPOINT_KNOWN_KINDS: u8 = CHECKPOINT_KIND_OP_BOUNDARIES;
46
47pub const CHECKPOINT_HEADER_LEN: usize = 1 + 1 + 2 + 4 + 8 + 4;
49pub const CHECKPOINT_ENTRY_LEN: usize = 16;
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct CheckpointEntry {
56 pub out_start: u64,
58 pub out_len: u64,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Default)]
64pub struct CheckpointTable {
65 pub kind: u8,
67 pub source_len: u64,
69 pub graph_crc32c: u32,
71 pub entries: Vec<CheckpointEntry>,
73}
74
75impl CheckpointTable {
76 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 pub fn lengths(&self) -> Vec<u64> {
127 self.entries.iter().map(|e| e.out_len).collect()
128 }
129
130 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 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 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 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 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 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}