Skip to main content

vole_document/dra/
program.rs

1//! Reconstruction program, coverage certificate, and bounded evaluation.
2
3use crate::dra::op::{Op, PackItem, decode_items};
4use crate::error::{Error, Result};
5use crate::limits::Limits;
6
7/// DRA version carried in the graph record.
8pub const DRA_VERSION: u8 = 6;
9
10/// Number of positional-offset slots addressable by [`Op::MarkOffset`] and
11/// [`Op::EmitOffset`]. Slot indices must be strictly below this bound.
12///
13/// The bound is 256 so the PDF layout candidate can mark one slot per indirect
14/// object (indices `0..=254`) while reserving slot `255` for the most recent
15/// classic `xref` section start. Slot indices remain `u8` on the wire.
16pub const MAX_OFFSET_SLOTS: usize = 256;
17
18/// Who is the reconstruction authority for an output interval.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Authority {
21    /// Bytes reproduced verbatim from a literal object or inline literal.
22    Literal,
23    /// Bytes deterministically generated (currently only by `REPEAT_LAST`).
24    Generated,
25    /// Bytes decoded from a typed entropy channel.
26    EntropyChannel,
27}
28
29/// One output interval and its authority.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct Span {
32    /// Start offset in the reconstructed output.
33    pub start: u64,
34    /// Length in bytes.
35    pub len: u64,
36    /// Reconstruction authority.
37    pub authority: Authority,
38}
39
40/// A coverage certificate: the map from output intervals to authorities.
41///
42/// The invariant is that the spans are contiguous and cover exactly
43/// `[0, total_len)` with no gaps and no overlapping authorities.
44#[derive(Debug, Clone, PartialEq, Eq, Default)]
45pub struct CoverageMap {
46    /// Ordered, contiguous spans.
47    pub spans: Vec<Span>,
48}
49
50impl CoverageMap {
51    /// Total covered length.
52    pub fn total_len(&self) -> u64 {
53        self.spans.last().map(|s| s.start + s.len).unwrap_or(0)
54    }
55
56    /// Assert contiguity/gap-freedom and equality with a declared length.
57    pub fn validate(&self, declared_len: u64) -> Result<()> {
58        let mut expected = 0u64;
59        for s in &self.spans {
60            if s.start != expected {
61                return Err(Error::coverage_violation(format!(
62                    "coverage gap/overlap: expected span at {expected}, found {}",
63                    s.start
64                )));
65            }
66            expected = expected
67                .checked_add(s.len)
68                .ok_or_else(|| Error::coverage_violation("coverage length overflow"))?;
69        }
70        if expected != declared_len {
71            return Err(Error::coverage_violation(format!(
72                "coverage covers {expected} bytes but {declared_len} were declared"
73            )));
74        }
75        Ok(())
76    }
77}
78
79/// A bounded, ordered list of reconstruction instructions.
80#[derive(Debug, Clone, PartialEq, Eq, Default)]
81pub struct Program {
82    /// Instructions, evaluated in order.
83    pub ops: Vec<Op>,
84}
85
86impl Program {
87    /// Wrap an instruction list.
88    pub fn new(ops: Vec<Op>) -> Self {
89        Program { ops }
90    }
91
92    /// Encode the program to graph-record payload bytes.
93    pub fn encode(&self) -> Result<Vec<u8>> {
94        let count = u32::try_from(self.ops.len())
95            .map_err(|_| Error::resource_limit("too many DRA instructions"))?;
96        let mut out = Vec::with_capacity(5 + self.ops.len() * 5);
97        out.push(DRA_VERSION);
98        out.extend_from_slice(&count.to_le_bytes());
99        for op in &self.ops {
100            op.encode(&mut out)?;
101        }
102        Ok(out)
103    }
104
105    /// Decode a graph-record payload.
106    pub fn decode(data: &[u8], limits: Limits) -> Result<Program> {
107        if data.is_empty() {
108            return Err(Error::invalid_graph("empty graph record"));
109        }
110        if data[0] != DRA_VERSION {
111            return Err(Error::unsupported_version(format!(
112                "DRA version {} is not supported",
113                data[0]
114            )));
115        }
116        if data.len() < 5 {
117            return Err(Error::invalid_graph("truncated graph header"));
118        }
119        let count = u32::from_le_bytes([data[1], data[2], data[3], data[4]]);
120        if count > limits.max_graph_ops {
121            return Err(Error::resource_limit(format!(
122                "graph has {count} instructions, limit {}",
123                limits.max_graph_ops
124            )));
125        }
126        let mut pos = 5usize;
127        let mut ops = Vec::with_capacity(count as usize);
128        for _ in 0..count {
129            ops.push(Op::decode(data, &mut pos, limits)?);
130        }
131        if pos != data.len() {
132            return Err(Error::invalid_graph(format!(
133                "graph record has {} trailing bytes",
134                data.len() - pos
135            )));
136        }
137        Ok(Program { ops })
138    }
139
140    /// Walk the program, validating instruction semantics and returning both
141    /// the predicted output length and the coverage certificate, using only
142    /// object and channel *lengths* (no byte materialization, no large
143    /// allocation).
144    pub fn analyze(
145        &self,
146        object_lens: &[u64],
147        channel_lens: &[u64],
148        limits: Limits,
149    ) -> Result<(u64, CoverageMap)> {
150        if self.ops.len() as u64 > limits.max_graph_ops as u64 {
151            return Err(Error::resource_limit("graph instruction limit exceeded"));
152        }
153        let mut spans: Vec<Span> = Vec::new();
154        let mut total: u64 = 0;
155        let mut last_len: u64 = 0;
156        let mut have_last = false;
157        // Which positional slots have been marked earlier in program order.
158        let mut marked = [false; MAX_OFFSET_SLOTS];
159
160        for op in &self.ops {
161            match op {
162                Op::EmitObject { object_id } => {
163                    let len = *object_lens.get(*object_id as usize).ok_or_else(|| {
164                        Error::invalid_graph(format!("graph references missing object {object_id}"))
165                    })?;
166                    total = total
167                        .checked_add(len)
168                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
169                    if len > 0 {
170                        spans.push(Span {
171                            start: total - len,
172                            len,
173                            authority: Authority::Literal,
174                        });
175                    }
176                    last_len = len;
177                    have_last = true;
178                }
179                Op::Inline { bytes } => {
180                    let len = bytes.len() as u64;
181                    total = total
182                        .checked_add(len)
183                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
184                    if len > 0 {
185                        spans.push(Span {
186                            start: total - len,
187                            len,
188                            authority: Authority::Literal,
189                        });
190                    }
191                    last_len = len;
192                    have_last = true;
193                }
194                Op::DecodeChannel { channel_id } => {
195                    let len = *channel_lens.get(*channel_id as usize).ok_or_else(|| {
196                        Error::invalid_graph(format!(
197                            "graph references missing entropy channel {channel_id}"
198                        ))
199                    })?;
200                    total = total
201                        .checked_add(len)
202                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
203                    if len > 0 {
204                        spans.push(Span {
205                            start: total - len,
206                            len,
207                            authority: Authority::EntropyChannel,
208                        });
209                    }
210                    last_len = len;
211                    have_last = true;
212                }
213                Op::InterleaveChannels {
214                    first_payload_channel,
215                    payload_channel_count,
216                    ..
217                } => {
218                    let first = *first_payload_channel as usize;
219                    let count = *payload_channel_count as usize;
220                    let end = first
221                        .checked_add(count)
222                        .ok_or_else(|| Error::invalid_graph("interleave channel range overflow"))?;
223                    if end > channel_lens.len() {
224                        return Err(Error::invalid_graph(format!(
225                            "interleave payload channel range {first}..{end} exceeds {} channels",
226                            channel_lens.len()
227                        )));
228                    }
229                    let mut len: u64 = 0;
230                    for &seg in &channel_lens[first..end] {
231                        len = len
232                            .checked_add(seg)
233                            .ok_or_else(|| Error::resource_limit("interleave length overflow"))?;
234                    }
235                    total = total
236                        .checked_add(len)
237                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
238                    if len > 0 {
239                        spans.push(Span {
240                            start: total - len,
241                            len,
242                            authority: Authority::Generated,
243                        });
244                    }
245                    last_len = len;
246                    have_last = true;
247                }
248                Op::MarkOffset { slot } => {
249                    let idx = *slot as usize;
250                    if idx >= MAX_OFFSET_SLOTS {
251                        return Err(Error::invalid_graph(format!(
252                            "MARK_OFFSET slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
253                        )));
254                    }
255                    // Bookkeeping only: contributes no output bytes and does not
256                    // disturb the pending "last block" for REPEAT_LAST.
257                    marked[idx] = true;
258                }
259                Op::EmitOffset { slot, width } => {
260                    let idx = *slot as usize;
261                    if idx >= MAX_OFFSET_SLOTS {
262                        return Err(Error::invalid_graph(format!(
263                            "EMIT_OFFSET slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
264                        )));
265                    }
266                    if *width == 0 || *width > 20 {
267                        return Err(Error::invalid_graph(format!(
268                            "EMIT_OFFSET width {width} is outside 1..=20"
269                        )));
270                    }
271                    if !marked[idx] {
272                        return Err(Error::invalid_graph(format!(
273                            "EMIT_OFFSET references unmarked slot {slot}"
274                        )));
275                    }
276                    // The value is a decimal number left-zero-padded to `width`,
277                    // so the predicted contribution is exactly `width` bytes.
278                    let len = *width as u64;
279                    total = total
280                        .checked_add(len)
281                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
282                    spans.push(Span {
283                        start: total - len,
284                        len,
285                        authority: Authority::Generated,
286                    });
287                    last_len = len;
288                    have_last = true;
289                }
290                Op::RepeatLast { count } => {
291                    if !have_last {
292                        return Err(Error::invalid_graph(
293                            "REPEAT_LAST has no preceding literal instruction",
294                        ));
295                    }
296                    if (*count as u64) > limits.max_repeat_count {
297                        return Err(Error::resource_limit(format!(
298                            "REPEAT_LAST count {count} exceeds limit {}",
299                            limits.max_repeat_count
300                        )));
301                    }
302                    let extra = last_len
303                        .checked_mul(*count as u64)
304                        .ok_or_else(|| Error::resource_limit("repeat length overflow"))?;
305                    total = total
306                        .checked_add(extra)
307                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
308                    if extra > 0 {
309                        spans.push(Span {
310                            start: total - extra,
311                            len: extra,
312                            authority: Authority::Generated,
313                        });
314                    }
315                    // Consecutive REPEAT_LAST is rejected to keep expansion
316                    // statically bounded and unambiguous.
317                    have_last = false;
318                    last_len = 0;
319                }
320                Op::PackSegments { data_object, items } => {
321                    let data_len = *object_lens.get(*data_object as usize).ok_or_else(|| {
322                        Error::invalid_graph(format!(
323                            "graph references missing object {data_object}"
324                        ))
325                    })?;
326                    let mut literal_total: u64 = 0;
327                    let mut produced: u64 = 0;
328                    // Which slots have been marked earlier in item order.
329                    let mut marked = [false; MAX_OFFSET_SLOTS];
330                    for item in items {
331                        match item {
332                            PackItem::Literal { len } => {
333                                literal_total =
334                                    literal_total.checked_add(*len as u64).ok_or_else(|| {
335                                        Error::resource_limit("packed literal length overflow")
336                                    })?;
337                                produced = produced.checked_add(*len as u64).ok_or_else(|| {
338                                    Error::resource_limit("output length overflow")
339                                })?;
340                            }
341                            PackItem::Mark { slot } => {
342                                let idx = *slot as usize;
343                                if idx >= MAX_OFFSET_SLOTS {
344                                    return Err(Error::invalid_graph(format!(
345                                        "PACK_SEGMENTS mark slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
346                                    )));
347                                }
348                                marked[idx] = true;
349                            }
350                            PackItem::Emit { slot, width } => {
351                                let idx = *slot as usize;
352                                if idx >= MAX_OFFSET_SLOTS {
353                                    return Err(Error::invalid_graph(format!(
354                                        "PACK_SEGMENTS emit slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
355                                    )));
356                                }
357                                if *width == 0 || *width > 20 {
358                                    return Err(Error::invalid_graph(format!(
359                                        "PACK_SEGMENTS emit width {width} is outside 1..=20"
360                                    )));
361                                }
362                                if !marked[idx] {
363                                    return Err(Error::invalid_graph(format!(
364                                        "PACK_SEGMENTS emit references unmarked slot {slot}"
365                                    )));
366                                }
367                                produced =
368                                    produced.checked_add(*width as u64).ok_or_else(|| {
369                                        Error::resource_limit("output length overflow")
370                                    })?;
371                            }
372                        }
373                    }
374                    if literal_total > data_len {
375                        return Err(Error::invalid_graph(format!(
376                            "PACK_SEGMENTS literal runs total {literal_total} bytes but data object {data_object} holds {data_len}"
377                        )));
378                    }
379                    total = total
380                        .checked_add(produced)
381                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
382                    if produced > 0 {
383                        spans.push(Span {
384                            start: total - produced,
385                            len: produced,
386                            authority: Authority::Generated,
387                        });
388                    }
389                    // A following REPEAT_LAST repeats the whole produced block.
390                    last_len = produced;
391                    have_last = true;
392                }
393                Op::PackedChannels {
394                    data_channel,
395                    plan_channel,
396                    declared_output_len,
397                } => {
398                    if *data_channel as usize >= channel_lens.len() {
399                        return Err(Error::invalid_graph(format!(
400                            "graph references missing entropy channel {data_channel}"
401                        )));
402                    }
403                    if *plan_channel as usize >= channel_lens.len() {
404                        return Err(Error::invalid_graph(format!(
405                            "graph references missing entropy channel {plan_channel}"
406                        )));
407                    }
408                    if *declared_output_len > limits.max_output_bytes {
409                        return Err(Error::resource_limit(format!(
410                            "PACKED_CHANNELS declared output {declared_output_len} exceeds limit {}",
411                            limits.max_output_bytes
412                        )));
413                    }
414                    // The plan channel's byte length is knowable structurally,
415                    // but the produced length is only proved by evaluation; the
416                    // declared length is the static prediction.
417                    let len = *declared_output_len;
418                    total = total
419                        .checked_add(len)
420                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
421                    if len > 0 {
422                        spans.push(Span {
423                            start: total - len,
424                            len,
425                            authority: Authority::Generated,
426                        });
427                    }
428                    last_len = len;
429                    have_last = true;
430                }
431            }
432            if total > limits.max_output_bytes {
433                return Err(Error::resource_limit(format!(
434                    "predicted output {total} exceeds limit {}",
435                    limits.max_output_bytes
436                )));
437            }
438        }
439
440        Ok((total, CoverageMap { spans }))
441    }
442
443    /// Analyze using concrete object and channel tables.
444    pub fn analyze_inputs(
445        &self,
446        objects: &[Vec<u8>],
447        channels: &[Vec<u8>],
448        limits: Limits,
449    ) -> Result<(u64, CoverageMap)> {
450        let object_lens: Vec<u64> = objects.iter().map(|o| o.len() as u64).collect();
451        let channel_lens: Vec<u64> = channels.iter().map(|c| c.len() as u64).collect();
452        self.analyze(&object_lens, &channel_lens, limits)
453    }
454
455    /// Convenience wrapper over [`Program::analyze`] for a program with no
456    /// entropy channels.
457    pub fn analyze_objects(
458        &self,
459        objects: &[Vec<u8>],
460        limits: Limits,
461    ) -> Result<(u64, CoverageMap)> {
462        let lens: Vec<u64> = objects.iter().map(|o| o.len() as u64).collect();
463        self.analyze(&lens, &[], limits)
464    }
465
466    /// Materialize the program's output, enforcing all bounds.
467    pub fn eval(
468        &self,
469        objects: &[Vec<u8>],
470        channels: &[Vec<u8>],
471        limits: Limits,
472    ) -> Result<Vec<u8>> {
473        let (predicted, _coverage) = self.analyze_inputs(objects, channels, limits)?;
474        let cap = predicted.min(64 * 1024 * 1024) as usize;
475        let mut out: Vec<u8> = Vec::with_capacity(cap);
476        let mut have_last = false;
477        let mut block_len: usize = 0;
478        // Recorded output positions and their marked state.
479        let mut slots = [0u64; MAX_OFFSET_SLOTS];
480        let mut marked = [false; MAX_OFFSET_SLOTS];
481
482        for op in &self.ops {
483            match op {
484                Op::EmitObject { object_id } => {
485                    let obj = objects.get(*object_id as usize).ok_or_else(|| {
486                        Error::invalid_graph(format!("graph references missing object {object_id}"))
487                    })?;
488                    block_len = obj.len();
489                    out.extend_from_slice(obj);
490                    have_last = true;
491                }
492                Op::Inline { bytes } => {
493                    block_len = bytes.len();
494                    out.extend_from_slice(bytes);
495                    have_last = true;
496                }
497                Op::DecodeChannel { channel_id } => {
498                    let ch = channels.get(*channel_id as usize).ok_or_else(|| {
499                        Error::invalid_graph(format!(
500                            "graph references missing entropy channel {channel_id}"
501                        ))
502                    })?;
503                    block_len = ch.len();
504                    out.extend_from_slice(ch);
505                    have_last = true;
506                }
507                Op::InterleaveChannels {
508                    kinds_channel,
509                    lengths_channel,
510                    first_payload_channel,
511                    payload_channel_count,
512                } => {
513                    let kinds = channels.get(*kinds_channel as usize).ok_or_else(|| {
514                        Error::invalid_graph(format!(
515                            "graph references missing entropy channel {kinds_channel}"
516                        ))
517                    })?;
518                    let lengths = channels.get(*lengths_channel as usize).ok_or_else(|| {
519                        Error::invalid_graph(format!(
520                            "graph references missing entropy channel {lengths_channel}"
521                        ))
522                    })?;
523                    let token_count = kinds.len();
524                    let required = token_count
525                        .checked_mul(4)
526                        .ok_or_else(|| Error::invalid_graph("interleave lengths overflow"))?;
527                    if lengths.len() != required {
528                        return Err(Error::invalid_graph(format!(
529                            "interleave length channel has {} bytes but {required} are required",
530                            lengths.len()
531                        )));
532                    }
533                    let first = *first_payload_channel as usize;
534                    let count = *payload_channel_count as usize;
535                    let end = first
536                        .checked_add(count)
537                        .ok_or_else(|| Error::invalid_graph("interleave channel range overflow"))?;
538                    if end > channels.len() {
539                        return Err(Error::invalid_graph(format!(
540                            "interleave payload channel range {first}..{end} exceeds {} channels",
541                            channels.len()
542                        )));
543                    }
544                    let start = out.len();
545                    let mut cursors = vec![0usize; count];
546                    for (i, chunk) in lengths.as_chunks::<4>().0.iter().enumerate() {
547                        let k = kinds[i] as usize;
548                        if k >= count {
549                            return Err(Error::invalid_graph(format!(
550                                "interleave token {i} names kind {k} outside 0..{count}"
551                            )));
552                        }
553                        let l = u32::from_le_bytes(*chunk) as usize;
554                        let ch = &channels[first + k];
555                        let cursor = cursors[k];
556                        let seg_end = cursor.checked_add(l).ok_or_else(|| {
557                            Error::invalid_graph("interleave payload cursor overflow")
558                        })?;
559                        if seg_end > ch.len() {
560                            return Err(Error::invalid_graph(format!(
561                                "interleave token {i} reads {l} bytes past channel {} ({cursor}..{seg_end} of {})",
562                                first + k,
563                                ch.len()
564                            )));
565                        }
566                        out.extend_from_slice(&ch[cursor..seg_end]);
567                        cursors[k] = seg_end;
568                        if out.len() as u64 > limits.max_output_bytes {
569                            return Err(Error::resource_limit(
570                                "output exceeds materialization limit",
571                            ));
572                        }
573                    }
574                    for (k, &cursor) in cursors.iter().enumerate() {
575                        let ch_len = channels[first + k].len();
576                        if cursor != ch_len {
577                            return Err(Error::invalid_graph(format!(
578                                "interleave payload channel {} was not fully consumed ({cursor} of {ch_len})",
579                                first + k
580                            )));
581                        }
582                    }
583                    block_len = out.len() - start;
584                    have_last = true;
585                }
586                Op::MarkOffset { slot } => {
587                    let idx = *slot as usize;
588                    if idx >= MAX_OFFSET_SLOTS {
589                        return Err(Error::invalid_graph(format!(
590                            "MARK_OFFSET slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
591                        )));
592                    }
593                    slots[idx] = out.len() as u64;
594                    marked[idx] = true;
595                }
596                Op::EmitOffset { slot, width } => {
597                    let idx = *slot as usize;
598                    if idx >= MAX_OFFSET_SLOTS {
599                        return Err(Error::invalid_graph(format!(
600                            "EMIT_OFFSET slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
601                        )));
602                    }
603                    if *width == 0 || *width > 20 {
604                        return Err(Error::invalid_graph(format!(
605                            "EMIT_OFFSET width {width} is outside 1..=20"
606                        )));
607                    }
608                    if !marked[idx] {
609                        return Err(Error::invalid_graph(format!(
610                            "EMIT_OFFSET references unmarked slot {slot}"
611                        )));
612                    }
613                    let digits = slots[idx].to_string();
614                    if digits.len() > *width as usize {
615                        return Err(Error::invalid_graph(format!(
616                            "EMIT_OFFSET slot {slot} value {} needs {} bytes but width is {width}",
617                            slots[idx],
618                            digits.len()
619                        )));
620                    }
621                    let start = out.len();
622                    out.extend(std::iter::repeat_n(b'0', *width as usize - digits.len()));
623                    out.extend_from_slice(digits.as_bytes());
624                    debug_assert_eq!(out.len() - start, *width as usize);
625                    block_len = out.len() - start;
626                    have_last = true;
627                }
628                Op::RepeatLast { count } => {
629                    if !have_last {
630                        return Err(Error::invalid_graph(
631                            "REPEAT_LAST has no preceding literal instruction",
632                        ));
633                    }
634                    // The preceding literal instruction produced exactly the
635                    // trailing `block_len` bytes; repeat that block `count`
636                    // more times. Consecutive repeats were rejected during
637                    // analysis, so `have_last` is now cleared.
638                    let start = out.len() - block_len;
639                    for _ in 0..*count {
640                        out.extend_from_within(start..start + block_len);
641                    }
642                    have_last = false;
643                    block_len = 0;
644                }
645                Op::PackSegments { data_object, items } => {
646                    let data = objects.get(*data_object as usize).ok_or_else(|| {
647                        Error::invalid_graph(format!(
648                            "graph references missing object {data_object}"
649                        ))
650                    })?;
651                    let start = out.len();
652                    run_pack_items("PACK_SEGMENTS", items, data, &mut out, limits)?;
653                    block_len = out.len() - start;
654                    have_last = true;
655                }
656                Op::PackedChannels {
657                    data_channel,
658                    plan_channel,
659                    declared_output_len,
660                } => {
661                    let data = channels.get(*data_channel as usize).ok_or_else(|| {
662                        Error::invalid_graph(format!(
663                            "graph references missing entropy channel {data_channel}"
664                        ))
665                    })?;
666                    let plan = channels.get(*plan_channel as usize).ok_or_else(|| {
667                        Error::invalid_graph(format!(
668                            "graph references missing entropy channel {plan_channel}"
669                        ))
670                    })?;
671                    let items = decode_items(plan, limits)?;
672                    let start = out.len();
673                    run_pack_items("PACKED_CHANNELS", &items, data, &mut out, limits)?;
674                    let produced = (out.len() - start) as u64;
675                    if produced != *declared_output_len {
676                        return Err(Error::invalid_graph(format!(
677                            "PACKED_CHANNELS produced {produced} bytes but {declared_output_len} were declared"
678                        )));
679                    }
680                    block_len = out.len() - start;
681                    have_last = true;
682                }
683            }
684            if out.len() as u64 > limits.max_output_bytes {
685                return Err(Error::resource_limit(
686                    "output exceeds materialization limit",
687                ));
688            }
689        }
690        Ok(out)
691    }
692}
693
694/// Interpret a packed item table over `data`, appending produced bytes to `out`.
695///
696/// Shared by [`Op::PackSegments`] (data from an object) and
697/// [`Op::PackedChannels`] (data from an entropy channel). The item semantics are
698/// identical: `Literal` copies contiguous data bytes, `Mark` records the current
699/// output position, and `Emit` renders a marked position as a fixed-width
700/// decimal. The data must be consumed exactly; all reads are bounds-checked and
701/// the running output is checked against [`Limits::max_output_bytes`].
702fn run_pack_items(
703    label: &str,
704    items: &[PackItem],
705    data: &[u8],
706    out: &mut Vec<u8>,
707    limits: Limits,
708) -> Result<()> {
709    let mut cursor: usize = 0;
710    let mut slots: [Option<u64>; MAX_OFFSET_SLOTS] = [None; MAX_OFFSET_SLOTS];
711    for item in items {
712        match item {
713            PackItem::Literal { len } => {
714                let len = *len as usize;
715                let end = cursor
716                    .checked_add(len)
717                    .ok_or_else(|| Error::invalid_graph("packed data cursor overflow"))?;
718                if end > data.len() {
719                    return Err(Error::invalid_graph(format!(
720                        "{label} literal reads {len} bytes past data ({cursor}..{end} of {})",
721                        data.len()
722                    )));
723                }
724                out.extend_from_slice(&data[cursor..end]);
725                cursor = end;
726            }
727            PackItem::Mark { slot } => {
728                let idx = *slot as usize;
729                if idx >= MAX_OFFSET_SLOTS {
730                    return Err(Error::invalid_graph(format!(
731                        "{label} mark slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
732                    )));
733                }
734                slots[idx] = Some(out.len() as u64);
735            }
736            PackItem::Emit { slot, width } => {
737                let idx = *slot as usize;
738                if idx >= MAX_OFFSET_SLOTS {
739                    return Err(Error::invalid_graph(format!(
740                        "{label} emit slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
741                    )));
742                }
743                if *width == 0 || *width > 20 {
744                    return Err(Error::invalid_graph(format!(
745                        "{label} emit width {width} is outside 1..=20"
746                    )));
747                }
748                let value = slots[idx].ok_or_else(|| {
749                    Error::invalid_graph(format!("{label} emit references unmarked slot {slot}"))
750                })?;
751                let digits = value.to_string();
752                if digits.len() > *width as usize {
753                    return Err(Error::invalid_graph(format!(
754                        "{label} slot {slot} value {value} needs {} bytes but width is {width}",
755                        digits.len()
756                    )));
757                }
758                out.extend(std::iter::repeat_n(b'0', *width as usize - digits.len()));
759                out.extend_from_slice(digits.as_bytes());
760            }
761        }
762        if out.len() as u64 > limits.max_output_bytes {
763            return Err(Error::resource_limit(
764                "output exceeds materialization limit",
765            ));
766        }
767    }
768    if cursor != data.len() {
769        return Err(Error::invalid_graph(format!(
770            "{label} did not fully consume data ({cursor} of {})",
771            data.len()
772        )));
773    }
774    Ok(())
775}
776
777#[cfg(test)]
778mod tests {
779    use super::*;
780
781    fn objs(xs: &[&[u8]]) -> Vec<Vec<u8>> {
782        xs.iter().map(|x| x.to_vec()).collect()
783    }
784
785    #[test]
786    fn literal_concat_and_coverage() {
787        let objects = objs(&[b"hello ", b"world"]);
788        let p = Program::new(vec![
789            Op::EmitObject { object_id: 0 },
790            Op::EmitObject { object_id: 1 },
791        ]);
792        let (len, cov) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
793        assert_eq!(len, 11);
794        cov.validate(11).unwrap();
795        assert_eq!(
796            p.eval(&objects, &[], Limits::DEFAULT).unwrap(),
797            b"hello world"
798        );
799    }
800
801    #[test]
802    fn inline_and_repeat() {
803        let objects = objs(&[]);
804        let p = Program::new(vec![
805            Op::Inline {
806                bytes: b"ab".to_vec(),
807            },
808            Op::RepeatLast { count: 2 },
809        ]);
810        let (len, cov) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
811        assert_eq!(len, 6);
812        cov.validate(6).unwrap();
813        assert_eq!(p.eval(&objects, &[], Limits::DEFAULT).unwrap(), b"ababab");
814        // Authority split: first "ab" literal, remaining "abab" generated.
815        assert_eq!(
816            cov.spans[0],
817            Span {
818                start: 0,
819                len: 2,
820                authority: Authority::Literal
821            }
822        );
823        assert_eq!(
824            cov.spans[1],
825            Span {
826                start: 2,
827                len: 4,
828                authority: Authority::Generated
829            }
830        );
831    }
832
833    #[test]
834    fn decode_channel_and_repeat() {
835        let objects = objs(&[]);
836        let channels = objs(&[b"abc"]);
837        let p = Program::new(vec![
838            Op::DecodeChannel { channel_id: 0 },
839            Op::RepeatLast { count: 1 },
840        ]);
841        let (len, cov) = p
842            .analyze_inputs(&objects, &channels, Limits::DEFAULT)
843            .unwrap();
844        assert_eq!(len, 6);
845        cov.validate(6).unwrap();
846        assert_eq!(
847            cov.spans[0],
848            Span {
849                start: 0,
850                len: 3,
851                authority: Authority::EntropyChannel,
852            }
853        );
854        assert_eq!(
855            cov.spans[1],
856            Span {
857                start: 3,
858                len: 3,
859                authority: Authority::Generated,
860            }
861        );
862        assert_eq!(
863            p.eval(&objects, &channels, Limits::DEFAULT).unwrap(),
864            b"abcabc"
865        );
866    }
867
868    #[test]
869    fn rejects_missing_channel() {
870        let p = Program::new(vec![Op::DecodeChannel { channel_id: 5 }]);
871        let e = p.analyze_inputs(&[], &[], Limits::DEFAULT).unwrap_err();
872        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
873    }
874
875    #[test]
876    fn rejects_missing_object() {
877        let objects = objs(&[]);
878        let p = Program::new(vec![Op::EmitObject { object_id: 3 }]);
879        let e = p.analyze_objects(&objects, Limits::DEFAULT).unwrap_err();
880        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
881    }
882
883    #[test]
884    fn rejects_leading_repeat() {
885        let objects = objs(&[]);
886        let p = Program::new(vec![Op::RepeatLast { count: 1 }]);
887        let e = p.analyze_objects(&objects, Limits::DEFAULT).unwrap_err();
888        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
889    }
890
891    #[test]
892    fn rejects_consecutive_repeats() {
893        let objects = objs(&[]);
894        let p = Program::new(vec![
895            Op::Inline {
896                bytes: b"x".to_vec(),
897            },
898            Op::RepeatLast { count: 1 },
899            Op::RepeatLast { count: 1 },
900        ]);
901        let e = p.analyze_objects(&objects, Limits::DEFAULT).unwrap_err();
902        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
903    }
904
905    #[test]
906    fn enforces_output_limit() {
907        let objects = objs(&[b"abcdefgh"]);
908        let p = Program::new(vec![
909            Op::EmitObject { object_id: 0 },
910            Op::RepeatLast { count: 1000 },
911        ]);
912        let limits = Limits {
913            max_output_bytes: 64,
914            ..Limits::DEFAULT
915        };
916        let e = p.analyze_objects(&objects, limits).unwrap_err();
917        assert_eq!(e.class(), crate::ErrorClass::ResourceLimit);
918    }
919
920    #[test]
921    fn coverage_gap_is_rejected() {
922        let cov = CoverageMap {
923            spans: vec![
924                Span {
925                    start: 0,
926                    len: 2,
927                    authority: Authority::Literal,
928                },
929                Span {
930                    start: 3,
931                    len: 2,
932                    authority: Authority::Literal,
933                },
934            ],
935        };
936        let e = cov.validate(4).unwrap_err();
937        assert_eq!(e.class(), crate::ErrorClass::CoverageViolation);
938    }
939
940    #[test]
941    fn program_roundtrips_via_bytes() {
942        let p = Program::new(vec![
943            Op::EmitObject { object_id: 1 },
944            Op::Inline {
945                bytes: b"hi".to_vec(),
946            },
947            Op::RepeatLast { count: 3 },
948        ]);
949        let enc = p.encode().unwrap();
950        let back = Program::decode(&enc, Limits::DEFAULT).unwrap();
951        assert_eq!(p, back);
952    }
953
954    fn le_lengths(lens: &[u32]) -> Vec<u8> {
955        let mut v = Vec::with_capacity(lens.len() * 4);
956        for &l in lens {
957            v.extend_from_slice(&l.to_le_bytes());
958        }
959        v
960    }
961
962    fn interleave_op() -> Op {
963        Op::InterleaveChannels {
964            kinds_channel: 0,
965            lengths_channel: 1,
966            first_payload_channel: 2,
967            payload_channel_count: 2,
968        }
969    }
970
971    /// Channels: kinds `[0,1,0,1]`, lengths `[2,3,1,2]`, and two payload
972    /// channels. Token order interleaves to `ab` `def` `c` `gh` = "abdefcgh".
973    fn interleave_channels() -> Vec<Vec<u8>> {
974        vec![
975            vec![0, 1, 0, 1],
976            le_lengths(&[2, 3, 1, 2]),
977            b"abc".to_vec(),
978            b"defgh".to_vec(),
979        ]
980    }
981
982    #[test]
983    fn interleave_roundtrip() {
984        let channels = interleave_channels();
985        let p = Program::new(vec![interleave_op()]);
986        let (len, cov) = p.analyze_inputs(&[], &channels, Limits::DEFAULT).unwrap();
987        assert_eq!(len, 8);
988        cov.validate(8).unwrap();
989        assert_eq!(
990            cov.spans,
991            vec![Span {
992                start: 0,
993                len: 8,
994                authority: Authority::Generated,
995            }]
996        );
997        assert_eq!(
998            p.eval(&[], &channels, Limits::DEFAULT).unwrap(),
999            b"abdefcgh"
1000        );
1001
1002        // Byte round-trip of the instruction through the graph record.
1003        let enc = p.encode().unwrap();
1004        assert_eq!(Program::decode(&enc, Limits::DEFAULT).unwrap(), p);
1005
1006        // The op is also the "last block" for a following REPEAT_LAST.
1007        let p2 = Program::new(vec![interleave_op(), Op::RepeatLast { count: 1 }]);
1008        let (len2, cov2) = p2.analyze_inputs(&[], &channels, Limits::DEFAULT).unwrap();
1009        assert_eq!(len2, 16);
1010        cov2.validate(16).unwrap();
1011        assert_eq!(
1012            p2.eval(&[], &channels, Limits::DEFAULT).unwrap(),
1013            b"abdefcghabdefcgh"
1014        );
1015    }
1016
1017    #[test]
1018    fn interleave_rejects_bad_channel_index() {
1019        // Payload range past the end of the channel table: rejected by analyze.
1020        let p = Program::new(vec![Op::InterleaveChannels {
1021            kinds_channel: 0,
1022            lengths_channel: 1,
1023            first_payload_channel: 2,
1024            payload_channel_count: 3,
1025        }]);
1026        let short = vec![vec![0u8], le_lengths(&[0]), vec![0u8]];
1027        let e = p.analyze_inputs(&[], &short, Limits::DEFAULT).unwrap_err();
1028        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1029
1030        // A token kind that names a channel outside the payload range: rejected
1031        // by eval (analyze only reasons about channel lengths).
1032        let channels = vec![vec![2u8], le_lengths(&[1]), b"x".to_vec(), b"y".to_vec()];
1033        let p = Program::new(vec![interleave_op()]);
1034        let e = p.eval(&[], &channels, Limits::DEFAULT).unwrap_err();
1035        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1036    }
1037
1038    #[test]
1039    fn interleave_rejects_unconsumed_payload() {
1040        // kind 0 length 1 consumes only one byte of a two-byte payload channel.
1041        let channels = vec![vec![0u8], le_lengths(&[1]), b"ab".to_vec(), b"z".to_vec()];
1042        let p = Program::new(vec![interleave_op()]);
1043        let e = p.eval(&[], &channels, Limits::DEFAULT).unwrap_err();
1044        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1045    }
1046
1047    #[test]
1048    fn interleave_lengths_must_be_4x_tokens() {
1049        // Two kind tokens but only one length (4 bytes instead of 8).
1050        let channels = vec![vec![0u8, 0u8], le_lengths(&[1]), b"ab".to_vec()];
1051        let p = Program::new(vec![Op::InterleaveChannels {
1052            kinds_channel: 0,
1053            lengths_channel: 1,
1054            first_payload_channel: 2,
1055            payload_channel_count: 1,
1056        }]);
1057        let e = p.eval(&[], &channels, Limits::DEFAULT).unwrap_err();
1058        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1059    }
1060
1061    #[test]
1062    fn interleave_predicts_from_channel_lens() {
1063        let p = Program::new(vec![interleave_op()]);
1064        // Only payload channel lengths (3 + 5) contribute; the kinds (4) and
1065        // lengths (16) channels are inputs, not output.
1066        let channel_lens = [4u64, 16, 3, 5];
1067        let (len, cov) = p.analyze(&[], &channel_lens, Limits::DEFAULT).unwrap();
1068        assert_eq!(len, 8);
1069        assert_eq!(
1070            cov.spans,
1071            vec![Span {
1072                start: 0,
1073                len: 8,
1074                authority: Authority::Generated,
1075            }]
1076        );
1077    }
1078
1079    #[test]
1080    fn mark_emit_roundtrip() {
1081        // MarkOffset after "abc" records position 3; the later EmitOffset
1082        // renders it as a zero-padded 3-byte decimal, so the digits are "003".
1083        let p = Program::new(vec![
1084            Op::Inline {
1085                bytes: b"abc".to_vec(),
1086            },
1087            Op::MarkOffset { slot: 0 },
1088            Op::Inline {
1089                bytes: b"def".to_vec(),
1090            },
1091            Op::EmitOffset { slot: 0, width: 3 },
1092        ]);
1093        let (len, cov) = p.analyze_objects(&[], Limits::DEFAULT).unwrap();
1094        assert_eq!(len, 9);
1095        cov.validate(9).unwrap();
1096        assert_eq!(p.eval(&[], &[], Limits::DEFAULT).unwrap(), b"abcdef003");
1097
1098        // Byte round-trip of both new instructions through the graph record.
1099        let enc = p.encode().unwrap();
1100        assert_eq!(Program::decode(&enc, Limits::DEFAULT).unwrap(), p);
1101
1102        // EmitOffset is a valid "last block" for a following REPEAT_LAST, with
1103        // block length equal to `width`.
1104        let p2 = Program::new(vec![
1105            Op::MarkOffset { slot: 0 },
1106            Op::EmitOffset { slot: 0, width: 2 },
1107            Op::RepeatLast { count: 1 },
1108        ]);
1109        let (len2, cov2) = p2.analyze_objects(&[], Limits::DEFAULT).unwrap();
1110        assert_eq!(len2, 4);
1111        cov2.validate(4).unwrap();
1112        assert_eq!(p2.eval(&[], &[], Limits::DEFAULT).unwrap(), b"0000");
1113    }
1114
1115    #[test]
1116    fn emit_zero_pads() {
1117        // A position of 0 rendered at width 3 is entirely zero-padded.
1118        let p = Program::new(vec![
1119            Op::MarkOffset { slot: 1 },
1120            Op::EmitOffset { slot: 1, width: 3 },
1121        ]);
1122        let (len, cov) = p.analyze_objects(&[], Limits::DEFAULT).unwrap();
1123        assert_eq!(len, 3);
1124        cov.validate(3).unwrap();
1125        assert_eq!(p.eval(&[], &[], Limits::DEFAULT).unwrap(), b"000");
1126    }
1127
1128    #[test]
1129    fn emit_width_too_small_errors() {
1130        // Analysis predicts the bounded width, but the marked position 10 needs
1131        // two digits; materialization must refuse rather than emit a truncated
1132        // (wrong-width) value.
1133        let p = Program::new(vec![
1134            Op::Inline {
1135                bytes: b"0123456789".to_vec(),
1136            },
1137            Op::MarkOffset { slot: 0 },
1138            Op::EmitOffset { slot: 0, width: 1 },
1139        ]);
1140        let (len, _) = p.analyze_objects(&[], Limits::DEFAULT).unwrap();
1141        assert_eq!(len, 11);
1142        let e = p.eval(&[], &[], Limits::DEFAULT).unwrap_err();
1143        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1144    }
1145
1146    #[test]
1147    fn emit_unmarked_slot_errors() {
1148        // Emitting a slot that was never marked earlier in program order is
1149        // rejected statically by analysis.
1150        let p = Program::new(vec![Op::EmitOffset { slot: 0, width: 4 }]);
1151        let e = p.analyze_objects(&[], Limits::DEFAULT).unwrap_err();
1152        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1153    }
1154
1155    #[test]
1156    fn mark_slot_bound_covers_every_u8_slot() {
1157        // `MAX_OFFSET_SLOTS == 256`, so every `u8` slot (0..=255) is in range.
1158        // The old out-of-range case (slot 16) is now valid, and the reserved
1159        // top slot `255` analyzes cleanly.
1160        let p = Program::new(vec![Op::MarkOffset { slot: 16 }]);
1161        let (len, _) = p.analyze_objects(&[], Limits::DEFAULT).unwrap();
1162        assert_eq!(len, 0);
1163
1164        let p = Program::new(vec![
1165            Op::MarkOffset { slot: 255 },
1166            Op::EmitOffset {
1167                slot: 255,
1168                width: 1,
1169            },
1170        ]);
1171        let (len, _) = p.analyze_objects(&[], Limits::DEFAULT).unwrap();
1172        assert_eq!(len, 1);
1173    }
1174
1175    #[test]
1176    fn analyze_predicts_width_bytes() {
1177        // The predicted contribution is exactly `width`, regardless of the
1178        // eventual digit count, and the authority is Generated.
1179        let p = Program::new(vec![
1180            Op::MarkOffset { slot: 3 },
1181            Op::EmitOffset { slot: 3, width: 7 },
1182        ]);
1183        let (len, cov) = p.analyze_objects(&[], Limits::DEFAULT).unwrap();
1184        assert_eq!(len, 7);
1185        cov.validate(7).unwrap();
1186        assert_eq!(
1187            cov.spans,
1188            vec![Span {
1189                start: 0,
1190                len: 7,
1191                authority: Authority::Generated,
1192            }]
1193        );
1194    }
1195
1196    #[test]
1197    fn pack_roundtrip() {
1198        let objects = objs(&[b"abcdef"]);
1199        let p = Program::new(vec![Op::PackSegments {
1200            data_object: 0,
1201            items: vec![
1202                PackItem::Literal { len: 3 },
1203                PackItem::Mark { slot: 0 },
1204                PackItem::Literal { len: 3 },
1205                PackItem::Emit { slot: 0, width: 3 },
1206            ],
1207        }]);
1208        let (len, cov) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1209        assert_eq!(len, 9);
1210        cov.validate(9).unwrap();
1211        assert_eq!(
1212            cov.spans,
1213            vec![Span {
1214                start: 0,
1215                len: 9,
1216                authority: Authority::Generated,
1217            }]
1218        );
1219        assert_eq!(
1220            p.eval(&objects, &[], Limits::DEFAULT).unwrap(),
1221            b"abcdef003"
1222        );
1223
1224        // Round-trip through the graph record (exercises the varint item wire).
1225        let enc = p.encode().unwrap();
1226        assert_eq!(Program::decode(&enc, Limits::DEFAULT).unwrap(), p);
1227    }
1228
1229    #[test]
1230    fn pack_varint_long_len() {
1231        // A literal length above the one-byte LEB128 range must round-trip.
1232        let data: Vec<u8> = (0..300u32).map(|i| i as u8).collect();
1233        let objects = vec![data.clone()];
1234        let p = Program::new(vec![Op::PackSegments {
1235            data_object: 0,
1236            items: vec![PackItem::Literal { len: 300 }],
1237        }]);
1238        let (len, _) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1239        assert_eq!(len, 300);
1240        assert_eq!(p.eval(&objects, &[], Limits::DEFAULT).unwrap(), data);
1241        let enc = p.encode().unwrap();
1242        assert_eq!(Program::decode(&enc, Limits::DEFAULT).unwrap(), p);
1243    }
1244
1245    #[test]
1246    fn pack_rejects_unconsumed_data() {
1247        let objects = objs(&[b"abcdef"]);
1248        let p = Program::new(vec![Op::PackSegments {
1249            data_object: 0,
1250            items: vec![PackItem::Literal { len: 3 }],
1251        }]);
1252        // Analysis only charges the literals; the shortfall is caught in eval.
1253        let (len, _) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1254        assert_eq!(len, 3);
1255        let e = p.eval(&objects, &[], Limits::DEFAULT).unwrap_err();
1256        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1257    }
1258
1259    #[test]
1260    fn pack_rejects_emit_before_mark() {
1261        let objects = objs(&[b"abc"]);
1262        let p = Program::new(vec![Op::PackSegments {
1263            data_object: 0,
1264            items: vec![PackItem::Emit { slot: 0, width: 2 }],
1265        }]);
1266        let e = p.analyze_objects(&objects, Limits::DEFAULT).unwrap_err();
1267        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1268    }
1269
1270    #[test]
1271    fn pack_rejects_bad_slot_width() {
1272        let objects = objs(&[b"abc"]);
1273        let too_wide = Program::new(vec![Op::PackSegments {
1274            data_object: 0,
1275            items: vec![
1276                PackItem::Mark { slot: 0 },
1277                PackItem::Emit { slot: 0, width: 21 },
1278            ],
1279        }]);
1280        let e = too_wide
1281            .analyze_objects(&objects, Limits::DEFAULT)
1282            .unwrap_err();
1283        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1284
1285        let zero_width = Program::new(vec![Op::PackSegments {
1286            data_object: 0,
1287            items: vec![
1288                PackItem::Mark { slot: 0 },
1289                PackItem::Emit { slot: 0, width: 0 },
1290            ],
1291        }]);
1292        let e = zero_width
1293            .analyze_objects(&objects, Limits::DEFAULT)
1294            .unwrap_err();
1295        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1296    }
1297
1298    #[test]
1299    fn analyze_predicts_pack_length() {
1300        let objects = objs(&[b"abcdef"]);
1301        let items = vec![
1302            PackItem::Literal { len: 2 },
1303            PackItem::Mark { slot: 1 },
1304            PackItem::Literal { len: 4 },
1305            PackItem::Emit { slot: 1, width: 5 },
1306        ];
1307        let p = Program::new(vec![Op::PackSegments {
1308            data_object: 0,
1309            items: items.clone(),
1310        }]);
1311        // 2 + 4 literal bytes plus a width-5 emitted field = 11, all Generated.
1312        let (len, cov) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1313        assert_eq!(len, 11);
1314        assert_eq!(
1315            cov.spans,
1316            vec![Span {
1317                start: 0,
1318                len: 11,
1319                authority: Authority::Generated,
1320            }]
1321        );
1322        // The whole packed block is the unit of a following REPEAT_LAST.
1323        let p2 = Program::new(vec![
1324            Op::PackSegments {
1325                data_object: 0,
1326                items,
1327            },
1328            Op::RepeatLast { count: 1 },
1329        ]);
1330        let (len2, cov2) = p2.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1331        assert_eq!(len2, 22);
1332        cov2.validate(22).unwrap();
1333        // "ab" + (mark=2) "cdef" + "00002", repeated once.
1334        assert_eq!(
1335            p2.eval(&objects, &[], Limits::DEFAULT).unwrap(),
1336            b"abcdef00002abcdef00002"
1337        );
1338    }
1339
1340    #[test]
1341    fn dra_version_is_six() {
1342        assert_eq!(DRA_VERSION, 6);
1343        let p = Program::new(vec![Op::Inline {
1344            bytes: b"x".to_vec(),
1345        }]);
1346        let enc = p.encode().unwrap();
1347        assert_eq!(enc[0], DRA_VERSION);
1348        // A stale prior-version graph record is rejected, not misparsed.
1349        let mut stale = enc.clone();
1350        stale[0] = DRA_VERSION - 1;
1351        let e = Program::decode(&stale, Limits::DEFAULT).unwrap_err();
1352        assert_eq!(e.class(), crate::ErrorClass::UnsupportedVersion);
1353    }
1354
1355    /// Data `b"abcdef"`, plan items `Literal{3}, Mark{0}, Literal{3}, Emit{0,3}`
1356    /// reconstruct `abcdef003`.
1357    fn packed_channels_program() -> Program {
1358        Program::new(vec![Op::PackedChannels {
1359            data_channel: 0,
1360            plan_channel: 1,
1361            declared_output_len: 9,
1362        }])
1363    }
1364
1365    fn packed_plan() -> Vec<u8> {
1366        crate::dra::op::encode_items(&[
1367            PackItem::Literal { len: 3 },
1368            PackItem::Mark { slot: 0 },
1369            PackItem::Literal { len: 3 },
1370            PackItem::Emit { slot: 0, width: 3 },
1371        ])
1372        .unwrap()
1373    }
1374
1375    #[test]
1376    fn packed_channels_roundtrip() {
1377        let channels = objs(&[b"abcdef", &packed_plan()]);
1378        let p = packed_channels_program();
1379        let (len, cov) = p.analyze_inputs(&[], &channels, Limits::DEFAULT).unwrap();
1380        assert_eq!(len, 9);
1381        cov.validate(9).unwrap();
1382        assert_eq!(
1383            cov.spans,
1384            vec![Span {
1385                start: 0,
1386                len: 9,
1387                authority: Authority::Generated,
1388            }]
1389        );
1390        assert_eq!(
1391            p.eval(&[], &channels, Limits::DEFAULT).unwrap(),
1392            b"abcdef003"
1393        );
1394
1395        // The plan codec and the op both round-trip through their wire forms.
1396        assert_eq!(
1397            crate::dra::op::decode_items(&packed_plan(), Limits::DEFAULT).unwrap(),
1398            vec![
1399                PackItem::Literal { len: 3 },
1400                PackItem::Mark { slot: 0 },
1401                PackItem::Literal { len: 3 },
1402                PackItem::Emit { slot: 0, width: 3 },
1403            ]
1404        );
1405        let enc = p.encode().unwrap();
1406        assert_eq!(Program::decode(&enc, Limits::DEFAULT).unwrap(), p);
1407    }
1408
1409    #[test]
1410    fn packed_channels_declared_len_mismatch_errors() {
1411        let channels = objs(&[b"abcdef", &packed_plan()]);
1412        // Analysis only predicts the declared length; evaluation proves it.
1413        let p = Program::new(vec![Op::PackedChannels {
1414            data_channel: 0,
1415            plan_channel: 1,
1416            declared_output_len: 8,
1417        }]);
1418        let (len, _) = p.analyze_inputs(&[], &channels, Limits::DEFAULT).unwrap();
1419        assert_eq!(len, 8);
1420        let e = p.eval(&[], &channels, Limits::DEFAULT).unwrap_err();
1421        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1422    }
1423
1424    #[test]
1425    fn packed_channels_missing_channel_errors() {
1426        let channels = objs(&[b"abcdef", &packed_plan()]);
1427        let missing_data = Program::new(vec![Op::PackedChannels {
1428            data_channel: 2,
1429            plan_channel: 1,
1430            declared_output_len: 9,
1431        }]);
1432        let e = missing_data
1433            .analyze_inputs(&[], &channels, Limits::DEFAULT)
1434            .unwrap_err();
1435        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1436
1437        let missing_plan = Program::new(vec![Op::PackedChannels {
1438            data_channel: 0,
1439            plan_channel: 2,
1440            declared_output_len: 9,
1441        }]);
1442        let e = missing_plan
1443            .analyze_inputs(&[], &channels, Limits::DEFAULT)
1444            .unwrap_err();
1445        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1446    }
1447
1448    #[test]
1449    fn packed_channels_truncated_plan_errors() {
1450        let plan = packed_plan();
1451        let truncated = &plan[..plan.len() - 1];
1452        let channels = objs(&[b"abcdef", truncated]);
1453        let p = packed_channels_program();
1454        // Analysis cannot see inside the plan; evaluation rejects truncation.
1455        p.analyze_inputs(&[], &channels, Limits::DEFAULT).unwrap();
1456        let e = p.eval(&[], &channels, Limits::DEFAULT).unwrap_err();
1457        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1458    }
1459
1460    #[test]
1461    fn packed_channels_rejects_unconsumed_data() {
1462        // Plan consumes only three of the six data bytes.
1463        let plan = crate::dra::op::encode_items(&[PackItem::Literal { len: 3 }]).unwrap();
1464        let channels = objs(&[b"abcdef", &plan]);
1465        let p = Program::new(vec![Op::PackedChannels {
1466            data_channel: 0,
1467            plan_channel: 1,
1468            declared_output_len: 3,
1469        }]);
1470        let (len, _) = p.analyze_inputs(&[], &channels, Limits::DEFAULT).unwrap();
1471        assert_eq!(len, 3);
1472        let e = p.eval(&[], &channels, Limits::DEFAULT).unwrap_err();
1473        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1474    }
1475}