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 = 8;
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        let (total, coverage, _per_op) = self.analyze_inner(object_lens, channel_lens, limits)?;
151        Ok((total, coverage))
152    }
153
154    /// Walk the program and return the exact output length contributed by each
155    /// instruction, in program order.
156    ///
157    /// This is the op-indexed view of [`Program::analyze`]: `per_op[i]` is the
158    /// number of bytes instruction `i` produces (`0` for bookkeeping ops such as
159    /// `MARK_OFFSET`), so `per_op.iter().sum() == analyze(...).0`. It applies the
160    /// *same* validation and overflow checks as `analyze`; no rejection is
161    /// relaxed. The optional observation index is checked against this view.
162    pub fn analyze_ops(
163        &self,
164        object_lens: &[u64],
165        channel_lens: &[u64],
166        limits: Limits,
167    ) -> Result<Vec<u64>> {
168        let (_total, _coverage, per_op) = self.analyze_inner(object_lens, channel_lens, limits)?;
169        Ok(per_op)
170    }
171
172    /// Shared program walk: predicted total, coverage certificate, and per-op
173    /// output lengths.
174    fn analyze_inner(
175        &self,
176        object_lens: &[u64],
177        channel_lens: &[u64],
178        limits: Limits,
179    ) -> Result<(u64, CoverageMap, Vec<u64>)> {
180        if self.ops.len() as u64 > limits.max_graph_ops as u64 {
181            return Err(Error::resource_limit("graph instruction limit exceeded"));
182        }
183        let mut spans: Vec<Span> = Vec::new();
184        let mut per_op: Vec<u64> = Vec::with_capacity(self.ops.len());
185        let mut total: u64 = 0;
186        let mut last_len: u64 = 0;
187        let mut have_last = false;
188        // Which positional slots have been marked earlier in program order.
189        let mut marked = [false; MAX_OFFSET_SLOTS];
190
191        for op in &self.ops {
192            let before = total;
193            match op {
194                Op::EmitObject { object_id } => {
195                    let len = *object_lens.get(*object_id as usize).ok_or_else(|| {
196                        Error::invalid_graph(format!("graph references missing object {object_id}"))
197                    })?;
198                    total = total
199                        .checked_add(len)
200                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
201                    if len > 0 {
202                        spans.push(Span {
203                            start: total - len,
204                            len,
205                            authority: Authority::Literal,
206                        });
207                    }
208                    last_len = len;
209                    have_last = true;
210                }
211                Op::Inline { bytes } => {
212                    let len = bytes.len() as u64;
213                    total = total
214                        .checked_add(len)
215                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
216                    if len > 0 {
217                        spans.push(Span {
218                            start: total - len,
219                            len,
220                            authority: Authority::Literal,
221                        });
222                    }
223                    last_len = len;
224                    have_last = true;
225                }
226                Op::DecodeChannel { channel_id } => {
227                    let len = *channel_lens.get(*channel_id as usize).ok_or_else(|| {
228                        Error::invalid_graph(format!(
229                            "graph references missing entropy channel {channel_id}"
230                        ))
231                    })?;
232                    total = total
233                        .checked_add(len)
234                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
235                    if len > 0 {
236                        spans.push(Span {
237                            start: total - len,
238                            len,
239                            authority: Authority::EntropyChannel,
240                        });
241                    }
242                    last_len = len;
243                    have_last = true;
244                }
245                Op::InterleaveChannels {
246                    first_payload_channel,
247                    payload_channel_count,
248                    ..
249                } => {
250                    let first = *first_payload_channel as usize;
251                    let count = *payload_channel_count as usize;
252                    let end = first
253                        .checked_add(count)
254                        .ok_or_else(|| Error::invalid_graph("interleave channel range overflow"))?;
255                    if end > channel_lens.len() {
256                        return Err(Error::invalid_graph(format!(
257                            "interleave payload channel range {first}..{end} exceeds {} channels",
258                            channel_lens.len()
259                        )));
260                    }
261                    let mut len: u64 = 0;
262                    for &seg in &channel_lens[first..end] {
263                        len = len
264                            .checked_add(seg)
265                            .ok_or_else(|| Error::resource_limit("interleave length overflow"))?;
266                    }
267                    total = total
268                        .checked_add(len)
269                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
270                    if len > 0 {
271                        spans.push(Span {
272                            start: total - len,
273                            len,
274                            authority: Authority::Generated,
275                        });
276                    }
277                    last_len = len;
278                    have_last = true;
279                }
280                Op::MarkOffset { slot } => {
281                    let idx = *slot as usize;
282                    if idx >= MAX_OFFSET_SLOTS {
283                        return Err(Error::invalid_graph(format!(
284                            "MARK_OFFSET slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
285                        )));
286                    }
287                    // Bookkeeping only: contributes no output bytes and does not
288                    // disturb the pending "last block" for REPEAT_LAST.
289                    marked[idx] = true;
290                }
291                Op::EmitOffset { slot, width } => {
292                    let idx = *slot as usize;
293                    if idx >= MAX_OFFSET_SLOTS {
294                        return Err(Error::invalid_graph(format!(
295                            "EMIT_OFFSET slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
296                        )));
297                    }
298                    if *width == 0 || *width > 20 {
299                        return Err(Error::invalid_graph(format!(
300                            "EMIT_OFFSET width {width} is outside 1..=20"
301                        )));
302                    }
303                    if !marked[idx] {
304                        return Err(Error::invalid_graph(format!(
305                            "EMIT_OFFSET references unmarked slot {slot}"
306                        )));
307                    }
308                    // The value is a decimal number left-zero-padded to `width`,
309                    // so the predicted contribution is exactly `width` bytes.
310                    let len = *width as u64;
311                    total = total
312                        .checked_add(len)
313                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
314                    spans.push(Span {
315                        start: total - len,
316                        len,
317                        authority: Authority::Generated,
318                    });
319                    last_len = len;
320                    have_last = true;
321                }
322                Op::RepeatLast { count } => {
323                    if !have_last {
324                        return Err(Error::invalid_graph(
325                            "REPEAT_LAST has no preceding literal instruction",
326                        ));
327                    }
328                    if (*count as u64) > limits.max_repeat_count {
329                        return Err(Error::resource_limit(format!(
330                            "REPEAT_LAST count {count} exceeds limit {}",
331                            limits.max_repeat_count
332                        )));
333                    }
334                    let extra = last_len
335                        .checked_mul(*count as u64)
336                        .ok_or_else(|| Error::resource_limit("repeat length overflow"))?;
337                    total = total
338                        .checked_add(extra)
339                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
340                    if extra > 0 {
341                        spans.push(Span {
342                            start: total - extra,
343                            len: extra,
344                            authority: Authority::Generated,
345                        });
346                    }
347                    // Consecutive REPEAT_LAST is rejected to keep expansion
348                    // statically bounded and unambiguous.
349                    have_last = false;
350                    last_len = 0;
351                }
352                Op::PackSegments { data_object, items } => {
353                    let data_len = *object_lens.get(*data_object as usize).ok_or_else(|| {
354                        Error::invalid_graph(format!(
355                            "graph references missing object {data_object}"
356                        ))
357                    })?;
358                    let mut literal_total: u64 = 0;
359                    let mut produced: u64 = 0;
360                    // Which slots have been marked earlier in item order.
361                    let mut marked = [false; MAX_OFFSET_SLOTS];
362                    for item in items {
363                        match item {
364                            PackItem::Literal { len } => {
365                                literal_total =
366                                    literal_total.checked_add(*len as u64).ok_or_else(|| {
367                                        Error::resource_limit("packed literal length overflow")
368                                    })?;
369                                produced = produced.checked_add(*len as u64).ok_or_else(|| {
370                                    Error::resource_limit("output length overflow")
371                                })?;
372                            }
373                            PackItem::Mark { slot } => {
374                                let idx = *slot as usize;
375                                if idx >= MAX_OFFSET_SLOTS {
376                                    return Err(Error::invalid_graph(format!(
377                                        "PACK_SEGMENTS mark slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
378                                    )));
379                                }
380                                marked[idx] = true;
381                            }
382                            PackItem::Emit { slot, width } => {
383                                let idx = *slot as usize;
384                                if idx >= MAX_OFFSET_SLOTS {
385                                    return Err(Error::invalid_graph(format!(
386                                        "PACK_SEGMENTS emit slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
387                                    )));
388                                }
389                                if *width == 0 || *width > 20 {
390                                    return Err(Error::invalid_graph(format!(
391                                        "PACK_SEGMENTS emit width {width} is outside 1..=20"
392                                    )));
393                                }
394                                if !marked[idx] {
395                                    return Err(Error::invalid_graph(format!(
396                                        "PACK_SEGMENTS emit references unmarked slot {slot}"
397                                    )));
398                                }
399                                produced =
400                                    produced.checked_add(*width as u64).ok_or_else(|| {
401                                        Error::resource_limit("output length overflow")
402                                    })?;
403                            }
404                        }
405                    }
406                    if literal_total > data_len {
407                        return Err(Error::invalid_graph(format!(
408                            "PACK_SEGMENTS literal runs total {literal_total} bytes but data object {data_object} holds {data_len}"
409                        )));
410                    }
411                    total = total
412                        .checked_add(produced)
413                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
414                    if produced > 0 {
415                        spans.push(Span {
416                            start: total - produced,
417                            len: produced,
418                            authority: Authority::Generated,
419                        });
420                    }
421                    // A following REPEAT_LAST repeats the whole produced block.
422                    last_len = produced;
423                    have_last = true;
424                }
425                Op::PackedChannels {
426                    data_channel,
427                    plan_channel,
428                    declared_output_len,
429                } => {
430                    if *data_channel as usize >= channel_lens.len() {
431                        return Err(Error::invalid_graph(format!(
432                            "graph references missing entropy channel {data_channel}"
433                        )));
434                    }
435                    if *plan_channel as usize >= channel_lens.len() {
436                        return Err(Error::invalid_graph(format!(
437                            "graph references missing entropy channel {plan_channel}"
438                        )));
439                    }
440                    if *declared_output_len > limits.max_output_bytes {
441                        return Err(Error::resource_limit(format!(
442                            "PACKED_CHANNELS declared output {declared_output_len} exceeds limit {}",
443                            limits.max_output_bytes
444                        )));
445                    }
446                    // The plan channel's byte length is knowable structurally,
447                    // but the produced length is only proved by evaluation; the
448                    // declared length is the static prediction.
449                    let len = *declared_output_len;
450                    total = total
451                        .checked_add(len)
452                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
453                    if len > 0 {
454                        spans.push(Span {
455                            start: total - len,
456                            len,
457                            authority: Authority::Generated,
458                        });
459                    }
460                    last_len = len;
461                    have_last = true;
462                }
463                Op::DeflateReplay {
464                    replay_codec: _,
465                    source_kind,
466                    source_id,
467                    corrections_object,
468                    declared_output_len,
469                } => {
470                    let src_len = match *source_kind {
471                        crate::dra::op::DEFLATE_SOURCE_OBJECT => {
472                            *object_lens.get(*source_id as usize).ok_or_else(|| {
473                                Error::invalid_graph(format!(
474                                    "graph references missing object {source_id}"
475                                ))
476                            })?
477                        }
478                        crate::dra::op::DEFLATE_SOURCE_CHANNEL => {
479                            *channel_lens.get(*source_id as usize).ok_or_else(|| {
480                                Error::invalid_graph(format!(
481                                    "graph references missing entropy channel {source_id}"
482                                ))
483                            })?
484                        }
485                        other => {
486                            return Err(Error::invalid_graph(format!(
487                                "DEFLATE_REPLAY source kind {other} is invalid"
488                            )));
489                        }
490                    };
491                    if *corrections_object as usize >= object_lens.len() {
492                        return Err(Error::invalid_graph(format!(
493                            "graph references missing object {corrections_object}"
494                        )));
495                    }
496                    // Outer budget: the declared output may never exceed the
497                    // materialization cap.
498                    if u64::from(*declared_output_len) > limits.max_output_bytes {
499                        return Err(Error::resource_limit(format!(
500                            "DEFLATE_REPLAY declared output {declared_output_len} exceeds limit {}",
501                            limits.max_output_bytes
502                        )));
503                    }
504                    // Admission: VOLE declines to replay a descriptor whose
505                    // declared output falls outside its bounded replay profile.
506                    // This is a *policy* bound, not an RFC 1951 maximum: RFC 1951
507                    // admits arbitrarily many empty non-final blocks, so no
508                    // finite `f(decompressed_size)` bound exists. The check runs
509                    // *before* preflate so an out-of-profile claim never reaches
510                    // the engine.
511                    let replay_limit = replay_profile_limit(src_len, limits);
512                    if u64::from(*declared_output_len) > replay_limit {
513                        return Err(Error::invalid_graph(format!(
514                            "DEFLATE_REPLAY declared output {declared_output_len} exceeds the VOLE replay-profile admission limit {replay_limit} for a {src_len}-byte plaintext (a policy bound: RFC 1951 permits unbounded empty non-final blocks; VOLE declines replay outside this profile)"
515                        )));
516                    }
517                    let len = u64::from(*declared_output_len);
518                    total = total
519                        .checked_add(len)
520                        .ok_or_else(|| Error::resource_limit("output length overflow"))?;
521                    if len > 0 {
522                        spans.push(Span {
523                            start: total - len,
524                            len,
525                            authority: Authority::Generated,
526                        });
527                    }
528                    last_len = len;
529                    have_last = true;
530                }
531            }
532            per_op.push(total - before);
533            if total > limits.max_output_bytes {
534                return Err(Error::resource_limit(format!(
535                    "predicted output {total} exceeds limit {}",
536                    limits.max_output_bytes
537                )));
538            }
539        }
540
541        Ok((total, CoverageMap { spans }, per_op))
542    }
543
544    /// Analyze using concrete object and channel tables.
545    pub fn analyze_inputs(
546        &self,
547        objects: &[Vec<u8>],
548        channels: &[Vec<u8>],
549        limits: Limits,
550    ) -> Result<(u64, CoverageMap)> {
551        let object_lens: Vec<u64> = objects.iter().map(|o| o.len() as u64).collect();
552        let channel_lens: Vec<u64> = channels.iter().map(|c| c.len() as u64).collect();
553        self.analyze(&object_lens, &channel_lens, limits)
554    }
555
556    /// Convenience wrapper over [`Program::analyze`] for a program with no
557    /// entropy channels.
558    pub fn analyze_objects(
559        &self,
560        objects: &[Vec<u8>],
561        limits: Limits,
562    ) -> Result<(u64, CoverageMap)> {
563        let lens: Vec<u64> = objects.iter().map(|o| o.len() as u64).collect();
564        self.analyze(&lens, &[], limits)
565    }
566
567    /// Materialize the program's output, enforcing all bounds.
568    pub fn eval(
569        &self,
570        objects: &[Vec<u8>],
571        channels: &[Vec<u8>],
572        limits: Limits,
573    ) -> Result<Vec<u8>> {
574        let (predicted, _coverage) = self.analyze_inputs(objects, channels, limits)?;
575        let cap = predicted.min(64 * 1024 * 1024) as usize;
576        let mut out: Vec<u8> = Vec::with_capacity(cap);
577        let mut have_last = false;
578        let mut block_len: usize = 0;
579        // Recorded output positions and their marked state.
580        let mut slots = [0u64; MAX_OFFSET_SLOTS];
581        let mut marked = [false; MAX_OFFSET_SLOTS];
582
583        for op in &self.ops {
584            match op {
585                Op::EmitObject { object_id } => {
586                    let obj = objects.get(*object_id as usize).ok_or_else(|| {
587                        Error::invalid_graph(format!("graph references missing object {object_id}"))
588                    })?;
589                    block_len = obj.len();
590                    out.extend_from_slice(obj);
591                    have_last = true;
592                }
593                Op::Inline { bytes } => {
594                    block_len = bytes.len();
595                    out.extend_from_slice(bytes);
596                    have_last = true;
597                }
598                Op::DecodeChannel { channel_id } => {
599                    let ch = channels.get(*channel_id as usize).ok_or_else(|| {
600                        Error::invalid_graph(format!(
601                            "graph references missing entropy channel {channel_id}"
602                        ))
603                    })?;
604                    block_len = ch.len();
605                    out.extend_from_slice(ch);
606                    have_last = true;
607                }
608                Op::InterleaveChannels {
609                    kinds_channel,
610                    lengths_channel,
611                    first_payload_channel,
612                    payload_channel_count,
613                } => {
614                    let kinds = channels.get(*kinds_channel as usize).ok_or_else(|| {
615                        Error::invalid_graph(format!(
616                            "graph references missing entropy channel {kinds_channel}"
617                        ))
618                    })?;
619                    let lengths = channels.get(*lengths_channel as usize).ok_or_else(|| {
620                        Error::invalid_graph(format!(
621                            "graph references missing entropy channel {lengths_channel}"
622                        ))
623                    })?;
624                    let token_count = kinds.len();
625                    let required = token_count
626                        .checked_mul(4)
627                        .ok_or_else(|| Error::invalid_graph("interleave lengths overflow"))?;
628                    if lengths.len() != required {
629                        return Err(Error::invalid_graph(format!(
630                            "interleave length channel has {} bytes but {required} are required",
631                            lengths.len()
632                        )));
633                    }
634                    let first = *first_payload_channel as usize;
635                    let count = *payload_channel_count as usize;
636                    let end = first
637                        .checked_add(count)
638                        .ok_or_else(|| Error::invalid_graph("interleave channel range overflow"))?;
639                    if end > channels.len() {
640                        return Err(Error::invalid_graph(format!(
641                            "interleave payload channel range {first}..{end} exceeds {} channels",
642                            channels.len()
643                        )));
644                    }
645                    let start = out.len();
646                    let mut cursors = vec![0usize; count];
647                    for (i, chunk) in lengths.as_chunks::<4>().0.iter().enumerate() {
648                        let k = kinds[i] as usize;
649                        if k >= count {
650                            return Err(Error::invalid_graph(format!(
651                                "interleave token {i} names kind {k} outside 0..{count}"
652                            )));
653                        }
654                        let l = u32::from_le_bytes(*chunk) as usize;
655                        let ch = &channels[first + k];
656                        let cursor = cursors[k];
657                        let seg_end = cursor.checked_add(l).ok_or_else(|| {
658                            Error::invalid_graph("interleave payload cursor overflow")
659                        })?;
660                        if seg_end > ch.len() {
661                            return Err(Error::invalid_graph(format!(
662                                "interleave token {i} reads {l} bytes past channel {} ({cursor}..{seg_end} of {})",
663                                first + k,
664                                ch.len()
665                            )));
666                        }
667                        out.extend_from_slice(&ch[cursor..seg_end]);
668                        cursors[k] = seg_end;
669                        if out.len() as u64 > limits.max_output_bytes {
670                            return Err(Error::resource_limit(
671                                "output exceeds materialization limit",
672                            ));
673                        }
674                    }
675                    for (k, &cursor) in cursors.iter().enumerate() {
676                        let ch_len = channels[first + k].len();
677                        if cursor != ch_len {
678                            return Err(Error::invalid_graph(format!(
679                                "interleave payload channel {} was not fully consumed ({cursor} of {ch_len})",
680                                first + k
681                            )));
682                        }
683                    }
684                    block_len = out.len() - start;
685                    have_last = true;
686                }
687                Op::MarkOffset { slot } => {
688                    let idx = *slot as usize;
689                    if idx >= MAX_OFFSET_SLOTS {
690                        return Err(Error::invalid_graph(format!(
691                            "MARK_OFFSET slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
692                        )));
693                    }
694                    slots[idx] = out.len() as u64;
695                    marked[idx] = true;
696                }
697                Op::EmitOffset { slot, width } => {
698                    let idx = *slot as usize;
699                    if idx >= MAX_OFFSET_SLOTS {
700                        return Err(Error::invalid_graph(format!(
701                            "EMIT_OFFSET slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
702                        )));
703                    }
704                    if *width == 0 || *width > 20 {
705                        return Err(Error::invalid_graph(format!(
706                            "EMIT_OFFSET width {width} is outside 1..=20"
707                        )));
708                    }
709                    if !marked[idx] {
710                        return Err(Error::invalid_graph(format!(
711                            "EMIT_OFFSET references unmarked slot {slot}"
712                        )));
713                    }
714                    let digits = slots[idx].to_string();
715                    if digits.len() > *width as usize {
716                        return Err(Error::invalid_graph(format!(
717                            "EMIT_OFFSET slot {slot} value {} needs {} bytes but width is {width}",
718                            slots[idx],
719                            digits.len()
720                        )));
721                    }
722                    let start = out.len();
723                    out.extend(std::iter::repeat_n(b'0', *width as usize - digits.len()));
724                    out.extend_from_slice(digits.as_bytes());
725                    debug_assert_eq!(out.len() - start, *width as usize);
726                    block_len = out.len() - start;
727                    have_last = true;
728                }
729                Op::RepeatLast { count } => {
730                    if !have_last {
731                        return Err(Error::invalid_graph(
732                            "REPEAT_LAST has no preceding literal instruction",
733                        ));
734                    }
735                    // The preceding literal instruction produced exactly the
736                    // trailing `block_len` bytes; repeat that block `count`
737                    // more times. Consecutive repeats were rejected during
738                    // analysis, so `have_last` is now cleared.
739                    let start = out.len() - block_len;
740                    for _ in 0..*count {
741                        out.extend_from_within(start..start + block_len);
742                    }
743                    have_last = false;
744                    block_len = 0;
745                }
746                Op::PackSegments { data_object, items } => {
747                    let data = objects.get(*data_object as usize).ok_or_else(|| {
748                        Error::invalid_graph(format!(
749                            "graph references missing object {data_object}"
750                        ))
751                    })?;
752                    let start = out.len();
753                    run_pack_items("PACK_SEGMENTS", items, data, &mut out, limits)?;
754                    block_len = out.len() - start;
755                    have_last = true;
756                }
757                Op::PackedChannels {
758                    data_channel,
759                    plan_channel,
760                    declared_output_len,
761                } => {
762                    let data = channels.get(*data_channel as usize).ok_or_else(|| {
763                        Error::invalid_graph(format!(
764                            "graph references missing entropy channel {data_channel}"
765                        ))
766                    })?;
767                    let plan = channels.get(*plan_channel as usize).ok_or_else(|| {
768                        Error::invalid_graph(format!(
769                            "graph references missing entropy channel {plan_channel}"
770                        ))
771                    })?;
772                    let items = decode_items(plan, limits)?;
773                    let start = out.len();
774                    run_pack_items("PACKED_CHANNELS", &items, data, &mut out, limits)?;
775                    let produced = (out.len() - start) as u64;
776                    if produced != *declared_output_len {
777                        return Err(Error::invalid_graph(format!(
778                            "PACKED_CHANNELS produced {produced} bytes but {declared_output_len} were declared"
779                        )));
780                    }
781                    block_len = out.len() - start;
782                    have_last = true;
783                }
784                Op::DeflateReplay {
785                    replay_codec: _,
786                    source_kind,
787                    source_id,
788                    corrections_object,
789                    declared_output_len,
790                } => {
791                    let plaintext: &[u8] = match *source_kind {
792                        crate::dra::op::DEFLATE_SOURCE_OBJECT => {
793                            objects.get(*source_id as usize).ok_or_else(|| {
794                                Error::invalid_graph(format!(
795                                    "graph references missing object {source_id}"
796                                ))
797                            })?
798                        }
799                        crate::dra::op::DEFLATE_SOURCE_CHANNEL => {
800                            channels.get(*source_id as usize).ok_or_else(|| {
801                                Error::invalid_graph(format!(
802                                    "graph references missing entropy channel {source_id}"
803                                ))
804                            })?
805                        }
806                        other => {
807                            return Err(Error::invalid_graph(format!(
808                                "DEFLATE_REPLAY source kind {other} is invalid"
809                            )));
810                        }
811                    };
812                    let corrections =
813                        objects.get(*corrections_object as usize).ok_or_else(|| {
814                            Error::invalid_graph(format!(
815                                "graph references missing object {corrections_object}"
816                            ))
817                        })?;
818                    // Explicit input bounds (in addition to the static output
819                    // bound proven by `analyze`): never hand oversized inputs to
820                    // the replay engine.
821                    if plaintext.len() as u64 > u64::from(limits.max_record_len) {
822                        return Err(Error::resource_limit(
823                            "DEFLATE_REPLAY plaintext exceeds the record limit",
824                        ));
825                    }
826                    if corrections.len() as u64 > u64::from(limits.max_record_len) {
827                        return Err(Error::resource_limit(
828                            "DEFLATE_REPLAY corrections exceed the record limit",
829                        ));
830                    }
831                    let raw = replay_deflate_bounded(
832                        plaintext,
833                        corrections,
834                        *declared_output_len,
835                        limits,
836                    )?;
837                    if raw.len() as u64 != u64::from(*declared_output_len) {
838                        return Err(Error::invalid_graph(format!(
839                            "DEFLATE_REPLAY produced {} bytes but {} were declared",
840                            raw.len(),
841                            declared_output_len
842                        )));
843                    }
844                    block_len = raw.len();
845                    out.extend_from_slice(&raw);
846                    have_last = true;
847                }
848            }
849            if out.len() as u64 > limits.max_output_bytes {
850                return Err(Error::resource_limit(
851                    "output exceeds materialization limit",
852                ));
853            }
854        }
855        Ok(out)
856    }
857}
858
859/// VOLE replay-profile admission ratio, as a percentage of the plaintext length.
860///
861/// The nominal profile figure is `REPLAY_OUTPUT_RATIO_PERCENT` percent of the
862/// plaintext length plus [`REPLAY_OUTPUT_SLACK`]. This is a **VOLE policy**
863/// choice, not a theorem about DEFLATE: RFC 1951 admits unbounded empty
864/// non-final blocks, so no finite `f(decompressed_size)` bound exists. The
865/// percentage is that of the old algorithmic expansion figure (`2 * P`).
866pub const REPLAY_OUTPUT_RATIO_PERCENT: u64 = 200;
867
868/// Additive slack in the VOLE replay-profile admission limit.
869///
870/// Covers block headers, end-of-block codes, stored-block overhead, and small
871/// plaintexts. Like the ratio, this is a policy choice rather than a property of
872/// RFC 1951.
873pub const REPLAY_OUTPUT_SLACK: u64 = 1024;
874
875/// VOLE replay-profile admission limit on the declared output of a single
876/// `DEFLATE_REPLAY` for a `plaintext_len`-byte plaintext.
877///
878/// Returns `min(max_output_bytes, max_replay_bytes, plaintext_len * 200% + 1024)`
879/// (saturating). This is a **policy** bound: RFC 1951 gives no finite
880/// `compressed_size <= f(decompressed_size)` bound (arbitrarily many empty
881/// non-final stored blocks are legal), so VOLE declines to replay a descriptor
882/// whose declared output falls outside a bounded profile. A stronger guarantee
883/// would require a bound specific to `REPLAY_DEFLATE_PREFLATE_0_7_6`, or a future
884/// VOLE-owned streaming replay that enforces the limit on output as it is
885/// produced. This is the primary resource bound for `DEFLATE_REPLAY` because
886/// `preflate-rs` 0.7.6 offers no bounded streaming reconstruction sink.
887pub(crate) fn replay_profile_limit(plaintext_len: u64, limits: Limits) -> u64 {
888    let profile = plaintext_len
889        .saturating_mul(REPLAY_OUTPUT_RATIO_PERCENT)
890        .saturating_div(100)
891        .saturating_add(REPLAY_OUTPUT_SLACK);
892    limits
893        .max_output_bytes
894        .min(limits.max_replay_bytes)
895        .min(profile)
896}
897
898/// Replay a raw DEFLATE bitstream from plaintext and correction state on the
899/// **decode path**.
900///
901/// With the `deflate-replay` feature this delegates to the process-isolated,
902/// memory- and time-capped [`crate::codec::deflate::replay_bounded`] (which falls
903/// back to the in-process [`crate::codec::deflate::replay_raw`] when no worker is
904/// configured). The encoder-side `try_replay` verification and the fuzz targets
905/// keep using `replay_raw` directly. Without the feature the op is still
906/// recognized on the wire but cannot be evaluated, so it fails closed.
907#[cfg(feature = "deflate-replay")]
908fn replay_deflate_bounded(
909    plaintext: &[u8],
910    corrections: &[u8],
911    declared_output_len: u32,
912    limits: Limits,
913) -> Result<Vec<u8>> {
914    crate::codec::deflate::replay_bounded(plaintext, corrections, declared_output_len, limits)
915}
916
917/// Fail-closed stub for builds without the `deflate-replay` feature.
918#[cfg(not(feature = "deflate-replay"))]
919fn replay_deflate_bounded(
920    _plaintext: &[u8],
921    _corrections: &[u8],
922    _declared_output_len: u32,
923    _limits: Limits,
924) -> Result<Vec<u8>> {
925    Err(Error::unsupported_feature(
926        "DEFLATE_REPLAY requires the `deflate-replay` feature",
927    ))
928}
929
930/// Interpret a packed item table over `data`, appending produced bytes to `out`.
931///
932/// Shared by [`Op::PackSegments`] (data from an object) and
933/// [`Op::PackedChannels`] (data from an entropy channel). The item semantics are
934/// identical: `Literal` copies contiguous data bytes, `Mark` records the current
935/// output position, and `Emit` renders a marked position as a fixed-width
936/// decimal. The data must be consumed exactly; all reads are bounds-checked and
937/// the running output is checked against [`Limits::max_output_bytes`].
938fn run_pack_items(
939    label: &str,
940    items: &[PackItem],
941    data: &[u8],
942    out: &mut Vec<u8>,
943    limits: Limits,
944) -> Result<()> {
945    let mut cursor: usize = 0;
946    let mut slots: [Option<u64>; MAX_OFFSET_SLOTS] = [None; MAX_OFFSET_SLOTS];
947    for item in items {
948        match item {
949            PackItem::Literal { len } => {
950                let len = *len as usize;
951                let end = cursor
952                    .checked_add(len)
953                    .ok_or_else(|| Error::invalid_graph("packed data cursor overflow"))?;
954                if end > data.len() {
955                    return Err(Error::invalid_graph(format!(
956                        "{label} literal reads {len} bytes past data ({cursor}..{end} of {})",
957                        data.len()
958                    )));
959                }
960                out.extend_from_slice(&data[cursor..end]);
961                cursor = end;
962            }
963            PackItem::Mark { slot } => {
964                let idx = *slot as usize;
965                if idx >= MAX_OFFSET_SLOTS {
966                    return Err(Error::invalid_graph(format!(
967                        "{label} mark slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
968                    )));
969                }
970                slots[idx] = Some(out.len() as u64);
971            }
972            PackItem::Emit { slot, width } => {
973                let idx = *slot as usize;
974                if idx >= MAX_OFFSET_SLOTS {
975                    return Err(Error::invalid_graph(format!(
976                        "{label} emit slot {slot} exceeds {MAX_OFFSET_SLOTS} slots"
977                    )));
978                }
979                if *width == 0 || *width > 20 {
980                    return Err(Error::invalid_graph(format!(
981                        "{label} emit width {width} is outside 1..=20"
982                    )));
983                }
984                let value = slots[idx].ok_or_else(|| {
985                    Error::invalid_graph(format!("{label} emit references unmarked slot {slot}"))
986                })?;
987                let digits = value.to_string();
988                if digits.len() > *width as usize {
989                    return Err(Error::invalid_graph(format!(
990                        "{label} slot {slot} value {value} needs {} bytes but width is {width}",
991                        digits.len()
992                    )));
993                }
994                out.extend(std::iter::repeat_n(b'0', *width as usize - digits.len()));
995                out.extend_from_slice(digits.as_bytes());
996            }
997        }
998        if out.len() as u64 > limits.max_output_bytes {
999            return Err(Error::resource_limit(
1000                "output exceeds materialization limit",
1001            ));
1002        }
1003    }
1004    if cursor != data.len() {
1005        return Err(Error::invalid_graph(format!(
1006            "{label} did not fully consume data ({cursor} of {})",
1007            data.len()
1008        )));
1009    }
1010    Ok(())
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015    use super::*;
1016
1017    fn objs(xs: &[&[u8]]) -> Vec<Vec<u8>> {
1018        xs.iter().map(|x| x.to_vec()).collect()
1019    }
1020
1021    #[test]
1022    fn literal_concat_and_coverage() {
1023        let objects = objs(&[b"hello ", b"world"]);
1024        let p = Program::new(vec![
1025            Op::EmitObject { object_id: 0 },
1026            Op::EmitObject { object_id: 1 },
1027        ]);
1028        let (len, cov) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1029        assert_eq!(len, 11);
1030        cov.validate(11).unwrap();
1031        assert_eq!(
1032            p.eval(&objects, &[], Limits::DEFAULT).unwrap(),
1033            b"hello world"
1034        );
1035    }
1036
1037    #[test]
1038    fn inline_and_repeat() {
1039        let objects = objs(&[]);
1040        let p = Program::new(vec![
1041            Op::Inline {
1042                bytes: b"ab".to_vec(),
1043            },
1044            Op::RepeatLast { count: 2 },
1045        ]);
1046        let (len, cov) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1047        assert_eq!(len, 6);
1048        cov.validate(6).unwrap();
1049        assert_eq!(p.eval(&objects, &[], Limits::DEFAULT).unwrap(), b"ababab");
1050        // Authority split: first "ab" literal, remaining "abab" generated.
1051        assert_eq!(
1052            cov.spans[0],
1053            Span {
1054                start: 0,
1055                len: 2,
1056                authority: Authority::Literal
1057            }
1058        );
1059        assert_eq!(
1060            cov.spans[1],
1061            Span {
1062                start: 2,
1063                len: 4,
1064                authority: Authority::Generated
1065            }
1066        );
1067    }
1068
1069    #[test]
1070    fn decode_channel_and_repeat() {
1071        let objects = objs(&[]);
1072        let channels = objs(&[b"abc"]);
1073        let p = Program::new(vec![
1074            Op::DecodeChannel { channel_id: 0 },
1075            Op::RepeatLast { count: 1 },
1076        ]);
1077        let (len, cov) = p
1078            .analyze_inputs(&objects, &channels, Limits::DEFAULT)
1079            .unwrap();
1080        assert_eq!(len, 6);
1081        cov.validate(6).unwrap();
1082        assert_eq!(
1083            cov.spans[0],
1084            Span {
1085                start: 0,
1086                len: 3,
1087                authority: Authority::EntropyChannel,
1088            }
1089        );
1090        assert_eq!(
1091            cov.spans[1],
1092            Span {
1093                start: 3,
1094                len: 3,
1095                authority: Authority::Generated,
1096            }
1097        );
1098        assert_eq!(
1099            p.eval(&objects, &channels, Limits::DEFAULT).unwrap(),
1100            b"abcabc"
1101        );
1102    }
1103
1104    #[test]
1105    fn rejects_missing_channel() {
1106        let p = Program::new(vec![Op::DecodeChannel { channel_id: 5 }]);
1107        let e = p.analyze_inputs(&[], &[], Limits::DEFAULT).unwrap_err();
1108        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1109    }
1110
1111    #[test]
1112    fn rejects_missing_object() {
1113        let objects = objs(&[]);
1114        let p = Program::new(vec![Op::EmitObject { object_id: 3 }]);
1115        let e = p.analyze_objects(&objects, Limits::DEFAULT).unwrap_err();
1116        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1117    }
1118
1119    #[test]
1120    fn rejects_leading_repeat() {
1121        let objects = objs(&[]);
1122        let p = Program::new(vec![Op::RepeatLast { count: 1 }]);
1123        let e = p.analyze_objects(&objects, Limits::DEFAULT).unwrap_err();
1124        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1125    }
1126
1127    #[test]
1128    fn rejects_consecutive_repeats() {
1129        let objects = objs(&[]);
1130        let p = Program::new(vec![
1131            Op::Inline {
1132                bytes: b"x".to_vec(),
1133            },
1134            Op::RepeatLast { count: 1 },
1135            Op::RepeatLast { count: 1 },
1136        ]);
1137        let e = p.analyze_objects(&objects, Limits::DEFAULT).unwrap_err();
1138        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1139    }
1140
1141    #[test]
1142    fn enforces_output_limit() {
1143        let objects = objs(&[b"abcdefgh"]);
1144        let p = Program::new(vec![
1145            Op::EmitObject { object_id: 0 },
1146            Op::RepeatLast { count: 1000 },
1147        ]);
1148        let limits = Limits {
1149            max_output_bytes: 64,
1150            ..Limits::DEFAULT
1151        };
1152        let e = p.analyze_objects(&objects, limits).unwrap_err();
1153        assert_eq!(e.class(), crate::ErrorClass::ResourceLimit);
1154    }
1155
1156    #[test]
1157    fn coverage_gap_is_rejected() {
1158        let cov = CoverageMap {
1159            spans: vec![
1160                Span {
1161                    start: 0,
1162                    len: 2,
1163                    authority: Authority::Literal,
1164                },
1165                Span {
1166                    start: 3,
1167                    len: 2,
1168                    authority: Authority::Literal,
1169                },
1170            ],
1171        };
1172        let e = cov.validate(4).unwrap_err();
1173        assert_eq!(e.class(), crate::ErrorClass::CoverageViolation);
1174    }
1175
1176    #[test]
1177    fn program_roundtrips_via_bytes() {
1178        let p = Program::new(vec![
1179            Op::EmitObject { object_id: 1 },
1180            Op::Inline {
1181                bytes: b"hi".to_vec(),
1182            },
1183            Op::RepeatLast { count: 3 },
1184        ]);
1185        let enc = p.encode().unwrap();
1186        let back = Program::decode(&enc, Limits::DEFAULT).unwrap();
1187        assert_eq!(p, back);
1188    }
1189
1190    fn le_lengths(lens: &[u32]) -> Vec<u8> {
1191        let mut v = Vec::with_capacity(lens.len() * 4);
1192        for &l in lens {
1193            v.extend_from_slice(&l.to_le_bytes());
1194        }
1195        v
1196    }
1197
1198    fn interleave_op() -> Op {
1199        Op::InterleaveChannels {
1200            kinds_channel: 0,
1201            lengths_channel: 1,
1202            first_payload_channel: 2,
1203            payload_channel_count: 2,
1204        }
1205    }
1206
1207    /// Channels: kinds `[0,1,0,1]`, lengths `[2,3,1,2]`, and two payload
1208    /// channels. Token order interleaves to `ab` `def` `c` `gh` = "abdefcgh".
1209    fn interleave_channels() -> Vec<Vec<u8>> {
1210        vec![
1211            vec![0, 1, 0, 1],
1212            le_lengths(&[2, 3, 1, 2]),
1213            b"abc".to_vec(),
1214            b"defgh".to_vec(),
1215        ]
1216    }
1217
1218    #[test]
1219    fn interleave_roundtrip() {
1220        let channels = interleave_channels();
1221        let p = Program::new(vec![interleave_op()]);
1222        let (len, cov) = p.analyze_inputs(&[], &channels, Limits::DEFAULT).unwrap();
1223        assert_eq!(len, 8);
1224        cov.validate(8).unwrap();
1225        assert_eq!(
1226            cov.spans,
1227            vec![Span {
1228                start: 0,
1229                len: 8,
1230                authority: Authority::Generated,
1231            }]
1232        );
1233        assert_eq!(
1234            p.eval(&[], &channels, Limits::DEFAULT).unwrap(),
1235            b"abdefcgh"
1236        );
1237
1238        // Byte round-trip of the instruction through the graph record.
1239        let enc = p.encode().unwrap();
1240        assert_eq!(Program::decode(&enc, Limits::DEFAULT).unwrap(), p);
1241
1242        // The op is also the "last block" for a following REPEAT_LAST.
1243        let p2 = Program::new(vec![interleave_op(), Op::RepeatLast { count: 1 }]);
1244        let (len2, cov2) = p2.analyze_inputs(&[], &channels, Limits::DEFAULT).unwrap();
1245        assert_eq!(len2, 16);
1246        cov2.validate(16).unwrap();
1247        assert_eq!(
1248            p2.eval(&[], &channels, Limits::DEFAULT).unwrap(),
1249            b"abdefcghabdefcgh"
1250        );
1251    }
1252
1253    #[test]
1254    fn interleave_rejects_bad_channel_index() {
1255        // Payload range past the end of the channel table: rejected by analyze.
1256        let p = Program::new(vec![Op::InterleaveChannels {
1257            kinds_channel: 0,
1258            lengths_channel: 1,
1259            first_payload_channel: 2,
1260            payload_channel_count: 3,
1261        }]);
1262        let short = vec![vec![0u8], le_lengths(&[0]), vec![0u8]];
1263        let e = p.analyze_inputs(&[], &short, Limits::DEFAULT).unwrap_err();
1264        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1265
1266        // A token kind that names a channel outside the payload range: rejected
1267        // by eval (analyze only reasons about channel lengths).
1268        let channels = vec![vec![2u8], le_lengths(&[1]), b"x".to_vec(), b"y".to_vec()];
1269        let p = Program::new(vec![interleave_op()]);
1270        let e = p.eval(&[], &channels, Limits::DEFAULT).unwrap_err();
1271        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1272    }
1273
1274    #[test]
1275    fn interleave_rejects_unconsumed_payload() {
1276        // kind 0 length 1 consumes only one byte of a two-byte payload channel.
1277        let channels = vec![vec![0u8], le_lengths(&[1]), b"ab".to_vec(), b"z".to_vec()];
1278        let p = Program::new(vec![interleave_op()]);
1279        let e = p.eval(&[], &channels, Limits::DEFAULT).unwrap_err();
1280        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1281    }
1282
1283    #[test]
1284    fn interleave_lengths_must_be_4x_tokens() {
1285        // Two kind tokens but only one length (4 bytes instead of 8).
1286        let channels = vec![vec![0u8, 0u8], le_lengths(&[1]), b"ab".to_vec()];
1287        let p = Program::new(vec![Op::InterleaveChannels {
1288            kinds_channel: 0,
1289            lengths_channel: 1,
1290            first_payload_channel: 2,
1291            payload_channel_count: 1,
1292        }]);
1293        let e = p.eval(&[], &channels, Limits::DEFAULT).unwrap_err();
1294        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1295    }
1296
1297    #[test]
1298    fn interleave_predicts_from_channel_lens() {
1299        let p = Program::new(vec![interleave_op()]);
1300        // Only payload channel lengths (3 + 5) contribute; the kinds (4) and
1301        // lengths (16) channels are inputs, not output.
1302        let channel_lens = [4u64, 16, 3, 5];
1303        let (len, cov) = p.analyze(&[], &channel_lens, Limits::DEFAULT).unwrap();
1304        assert_eq!(len, 8);
1305        assert_eq!(
1306            cov.spans,
1307            vec![Span {
1308                start: 0,
1309                len: 8,
1310                authority: Authority::Generated,
1311            }]
1312        );
1313    }
1314
1315    #[test]
1316    fn mark_emit_roundtrip() {
1317        // MarkOffset after "abc" records position 3; the later EmitOffset
1318        // renders it as a zero-padded 3-byte decimal, so the digits are "003".
1319        let p = Program::new(vec![
1320            Op::Inline {
1321                bytes: b"abc".to_vec(),
1322            },
1323            Op::MarkOffset { slot: 0 },
1324            Op::Inline {
1325                bytes: b"def".to_vec(),
1326            },
1327            Op::EmitOffset { slot: 0, width: 3 },
1328        ]);
1329        let (len, cov) = p.analyze_objects(&[], Limits::DEFAULT).unwrap();
1330        assert_eq!(len, 9);
1331        cov.validate(9).unwrap();
1332        assert_eq!(p.eval(&[], &[], Limits::DEFAULT).unwrap(), b"abcdef003");
1333
1334        // Byte round-trip of both new instructions through the graph record.
1335        let enc = p.encode().unwrap();
1336        assert_eq!(Program::decode(&enc, Limits::DEFAULT).unwrap(), p);
1337
1338        // EmitOffset is a valid "last block" for a following REPEAT_LAST, with
1339        // block length equal to `width`.
1340        let p2 = Program::new(vec![
1341            Op::MarkOffset { slot: 0 },
1342            Op::EmitOffset { slot: 0, width: 2 },
1343            Op::RepeatLast { count: 1 },
1344        ]);
1345        let (len2, cov2) = p2.analyze_objects(&[], Limits::DEFAULT).unwrap();
1346        assert_eq!(len2, 4);
1347        cov2.validate(4).unwrap();
1348        assert_eq!(p2.eval(&[], &[], Limits::DEFAULT).unwrap(), b"0000");
1349    }
1350
1351    #[test]
1352    fn emit_zero_pads() {
1353        // A position of 0 rendered at width 3 is entirely zero-padded.
1354        let p = Program::new(vec![
1355            Op::MarkOffset { slot: 1 },
1356            Op::EmitOffset { slot: 1, width: 3 },
1357        ]);
1358        let (len, cov) = p.analyze_objects(&[], Limits::DEFAULT).unwrap();
1359        assert_eq!(len, 3);
1360        cov.validate(3).unwrap();
1361        assert_eq!(p.eval(&[], &[], Limits::DEFAULT).unwrap(), b"000");
1362    }
1363
1364    #[test]
1365    fn emit_width_too_small_errors() {
1366        // Analysis predicts the bounded width, but the marked position 10 needs
1367        // two digits; materialization must refuse rather than emit a truncated
1368        // (wrong-width) value.
1369        let p = Program::new(vec![
1370            Op::Inline {
1371                bytes: b"0123456789".to_vec(),
1372            },
1373            Op::MarkOffset { slot: 0 },
1374            Op::EmitOffset { slot: 0, width: 1 },
1375        ]);
1376        let (len, _) = p.analyze_objects(&[], Limits::DEFAULT).unwrap();
1377        assert_eq!(len, 11);
1378        let e = p.eval(&[], &[], Limits::DEFAULT).unwrap_err();
1379        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1380    }
1381
1382    #[test]
1383    fn emit_unmarked_slot_errors() {
1384        // Emitting a slot that was never marked earlier in program order is
1385        // rejected statically by analysis.
1386        let p = Program::new(vec![Op::EmitOffset { slot: 0, width: 4 }]);
1387        let e = p.analyze_objects(&[], Limits::DEFAULT).unwrap_err();
1388        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1389    }
1390
1391    #[test]
1392    fn mark_slot_bound_covers_every_u8_slot() {
1393        // `MAX_OFFSET_SLOTS == 256`, so every `u8` slot (0..=255) is in range.
1394        // The old out-of-range case (slot 16) is now valid, and the reserved
1395        // top slot `255` analyzes cleanly.
1396        let p = Program::new(vec![Op::MarkOffset { slot: 16 }]);
1397        let (len, _) = p.analyze_objects(&[], Limits::DEFAULT).unwrap();
1398        assert_eq!(len, 0);
1399
1400        let p = Program::new(vec![
1401            Op::MarkOffset { slot: 255 },
1402            Op::EmitOffset {
1403                slot: 255,
1404                width: 1,
1405            },
1406        ]);
1407        let (len, _) = p.analyze_objects(&[], Limits::DEFAULT).unwrap();
1408        assert_eq!(len, 1);
1409    }
1410
1411    #[test]
1412    fn analyze_predicts_width_bytes() {
1413        // The predicted contribution is exactly `width`, regardless of the
1414        // eventual digit count, and the authority is Generated.
1415        let p = Program::new(vec![
1416            Op::MarkOffset { slot: 3 },
1417            Op::EmitOffset { slot: 3, width: 7 },
1418        ]);
1419        let (len, cov) = p.analyze_objects(&[], Limits::DEFAULT).unwrap();
1420        assert_eq!(len, 7);
1421        cov.validate(7).unwrap();
1422        assert_eq!(
1423            cov.spans,
1424            vec![Span {
1425                start: 0,
1426                len: 7,
1427                authority: Authority::Generated,
1428            }]
1429        );
1430    }
1431
1432    #[test]
1433    fn pack_roundtrip() {
1434        let objects = objs(&[b"abcdef"]);
1435        let p = Program::new(vec![Op::PackSegments {
1436            data_object: 0,
1437            items: vec![
1438                PackItem::Literal { len: 3 },
1439                PackItem::Mark { slot: 0 },
1440                PackItem::Literal { len: 3 },
1441                PackItem::Emit { slot: 0, width: 3 },
1442            ],
1443        }]);
1444        let (len, cov) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1445        assert_eq!(len, 9);
1446        cov.validate(9).unwrap();
1447        assert_eq!(
1448            cov.spans,
1449            vec![Span {
1450                start: 0,
1451                len: 9,
1452                authority: Authority::Generated,
1453            }]
1454        );
1455        assert_eq!(
1456            p.eval(&objects, &[], Limits::DEFAULT).unwrap(),
1457            b"abcdef003"
1458        );
1459
1460        // Round-trip through the graph record (exercises the varint item wire).
1461        let enc = p.encode().unwrap();
1462        assert_eq!(Program::decode(&enc, Limits::DEFAULT).unwrap(), p);
1463    }
1464
1465    #[test]
1466    fn pack_varint_long_len() {
1467        // A literal length above the one-byte LEB128 range must round-trip.
1468        let data: Vec<u8> = (0..300u32).map(|i| i as u8).collect();
1469        let objects = vec![data.clone()];
1470        let p = Program::new(vec![Op::PackSegments {
1471            data_object: 0,
1472            items: vec![PackItem::Literal { len: 300 }],
1473        }]);
1474        let (len, _) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1475        assert_eq!(len, 300);
1476        assert_eq!(p.eval(&objects, &[], Limits::DEFAULT).unwrap(), data);
1477        let enc = p.encode().unwrap();
1478        assert_eq!(Program::decode(&enc, Limits::DEFAULT).unwrap(), p);
1479    }
1480
1481    #[test]
1482    fn pack_rejects_unconsumed_data() {
1483        let objects = objs(&[b"abcdef"]);
1484        let p = Program::new(vec![Op::PackSegments {
1485            data_object: 0,
1486            items: vec![PackItem::Literal { len: 3 }],
1487        }]);
1488        // Analysis only charges the literals; the shortfall is caught in eval.
1489        let (len, _) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1490        assert_eq!(len, 3);
1491        let e = p.eval(&objects, &[], Limits::DEFAULT).unwrap_err();
1492        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1493    }
1494
1495    #[test]
1496    fn pack_rejects_emit_before_mark() {
1497        let objects = objs(&[b"abc"]);
1498        let p = Program::new(vec![Op::PackSegments {
1499            data_object: 0,
1500            items: vec![PackItem::Emit { slot: 0, width: 2 }],
1501        }]);
1502        let e = p.analyze_objects(&objects, Limits::DEFAULT).unwrap_err();
1503        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1504    }
1505
1506    #[test]
1507    fn pack_rejects_bad_slot_width() {
1508        let objects = objs(&[b"abc"]);
1509        let too_wide = Program::new(vec![Op::PackSegments {
1510            data_object: 0,
1511            items: vec![
1512                PackItem::Mark { slot: 0 },
1513                PackItem::Emit { slot: 0, width: 21 },
1514            ],
1515        }]);
1516        let e = too_wide
1517            .analyze_objects(&objects, Limits::DEFAULT)
1518            .unwrap_err();
1519        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1520
1521        let zero_width = Program::new(vec![Op::PackSegments {
1522            data_object: 0,
1523            items: vec![
1524                PackItem::Mark { slot: 0 },
1525                PackItem::Emit { slot: 0, width: 0 },
1526            ],
1527        }]);
1528        let e = zero_width
1529            .analyze_objects(&objects, Limits::DEFAULT)
1530            .unwrap_err();
1531        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1532    }
1533
1534    #[test]
1535    fn analyze_predicts_pack_length() {
1536        let objects = objs(&[b"abcdef"]);
1537        let items = vec![
1538            PackItem::Literal { len: 2 },
1539            PackItem::Mark { slot: 1 },
1540            PackItem::Literal { len: 4 },
1541            PackItem::Emit { slot: 1, width: 5 },
1542        ];
1543        let p = Program::new(vec![Op::PackSegments {
1544            data_object: 0,
1545            items: items.clone(),
1546        }]);
1547        // 2 + 4 literal bytes plus a width-5 emitted field = 11, all Generated.
1548        let (len, cov) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1549        assert_eq!(len, 11);
1550        assert_eq!(
1551            cov.spans,
1552            vec![Span {
1553                start: 0,
1554                len: 11,
1555                authority: Authority::Generated,
1556            }]
1557        );
1558        // The whole packed block is the unit of a following REPEAT_LAST.
1559        let p2 = Program::new(vec![
1560            Op::PackSegments {
1561                data_object: 0,
1562                items,
1563            },
1564            Op::RepeatLast { count: 1 },
1565        ]);
1566        let (len2, cov2) = p2.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1567        assert_eq!(len2, 22);
1568        cov2.validate(22).unwrap();
1569        // "ab" + (mark=2) "cdef" + "00002", repeated once.
1570        assert_eq!(
1571            p2.eval(&objects, &[], Limits::DEFAULT).unwrap(),
1572            b"abcdef00002abcdef00002"
1573        );
1574    }
1575
1576    #[test]
1577    fn dra_version_is_eight() {
1578        assert_eq!(DRA_VERSION, 8);
1579        let p = Program::new(vec![Op::Inline {
1580            bytes: b"x".to_vec(),
1581        }]);
1582        let enc = p.encode().unwrap();
1583        assert_eq!(enc[0], DRA_VERSION);
1584        // A stale prior-version graph record is rejected, not misparsed.
1585        let mut stale = enc.clone();
1586        stale[0] = DRA_VERSION - 1;
1587        let e = Program::decode(&stale, Limits::DEFAULT).unwrap_err();
1588        assert_eq!(e.class(), crate::ErrorClass::UnsupportedVersion);
1589    }
1590
1591    /// Data `b"abcdef"`, plan items `Literal{3}, Mark{0}, Literal{3}, Emit{0,3}`
1592    /// reconstruct `abcdef003`.
1593    fn packed_channels_program() -> Program {
1594        Program::new(vec![Op::PackedChannels {
1595            data_channel: 0,
1596            plan_channel: 1,
1597            declared_output_len: 9,
1598        }])
1599    }
1600
1601    fn packed_plan() -> Vec<u8> {
1602        crate::dra::op::encode_items(&[
1603            PackItem::Literal { len: 3 },
1604            PackItem::Mark { slot: 0 },
1605            PackItem::Literal { len: 3 },
1606            PackItem::Emit { slot: 0, width: 3 },
1607        ])
1608        .unwrap()
1609    }
1610
1611    #[test]
1612    fn packed_channels_roundtrip() {
1613        let channels = objs(&[b"abcdef", &packed_plan()]);
1614        let p = packed_channels_program();
1615        let (len, cov) = p.analyze_inputs(&[], &channels, Limits::DEFAULT).unwrap();
1616        assert_eq!(len, 9);
1617        cov.validate(9).unwrap();
1618        assert_eq!(
1619            cov.spans,
1620            vec![Span {
1621                start: 0,
1622                len: 9,
1623                authority: Authority::Generated,
1624            }]
1625        );
1626        assert_eq!(
1627            p.eval(&[], &channels, Limits::DEFAULT).unwrap(),
1628            b"abcdef003"
1629        );
1630
1631        // The plan codec and the op both round-trip through their wire forms.
1632        assert_eq!(
1633            crate::dra::op::decode_items(&packed_plan(), Limits::DEFAULT).unwrap(),
1634            vec![
1635                PackItem::Literal { len: 3 },
1636                PackItem::Mark { slot: 0 },
1637                PackItem::Literal { len: 3 },
1638                PackItem::Emit { slot: 0, width: 3 },
1639            ]
1640        );
1641        let enc = p.encode().unwrap();
1642        assert_eq!(Program::decode(&enc, Limits::DEFAULT).unwrap(), p);
1643    }
1644
1645    #[test]
1646    fn packed_channels_declared_len_mismatch_errors() {
1647        let channels = objs(&[b"abcdef", &packed_plan()]);
1648        // Analysis only predicts the declared length; evaluation proves it.
1649        let p = Program::new(vec![Op::PackedChannels {
1650            data_channel: 0,
1651            plan_channel: 1,
1652            declared_output_len: 8,
1653        }]);
1654        let (len, _) = p.analyze_inputs(&[], &channels, Limits::DEFAULT).unwrap();
1655        assert_eq!(len, 8);
1656        let e = p.eval(&[], &channels, Limits::DEFAULT).unwrap_err();
1657        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1658    }
1659
1660    #[test]
1661    fn packed_channels_missing_channel_errors() {
1662        let channels = objs(&[b"abcdef", &packed_plan()]);
1663        let missing_data = Program::new(vec![Op::PackedChannels {
1664            data_channel: 2,
1665            plan_channel: 1,
1666            declared_output_len: 9,
1667        }]);
1668        let e = missing_data
1669            .analyze_inputs(&[], &channels, Limits::DEFAULT)
1670            .unwrap_err();
1671        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1672
1673        let missing_plan = Program::new(vec![Op::PackedChannels {
1674            data_channel: 0,
1675            plan_channel: 2,
1676            declared_output_len: 9,
1677        }]);
1678        let e = missing_plan
1679            .analyze_inputs(&[], &channels, Limits::DEFAULT)
1680            .unwrap_err();
1681        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1682    }
1683
1684    #[test]
1685    fn packed_channels_truncated_plan_errors() {
1686        let plan = packed_plan();
1687        let truncated = &plan[..plan.len() - 1];
1688        let channels = objs(&[b"abcdef", truncated]);
1689        let p = packed_channels_program();
1690        // Analysis cannot see inside the plan; evaluation rejects truncation.
1691        p.analyze_inputs(&[], &channels, Limits::DEFAULT).unwrap();
1692        let e = p.eval(&[], &channels, Limits::DEFAULT).unwrap_err();
1693        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1694    }
1695
1696    #[test]
1697    fn packed_channels_rejects_unconsumed_data() {
1698        // Plan consumes only three of the six data bytes.
1699        let plan = crate::dra::op::encode_items(&[PackItem::Literal { len: 3 }]).unwrap();
1700        let channels = objs(&[b"abcdef", &plan]);
1701        let p = Program::new(vec![Op::PackedChannels {
1702            data_channel: 0,
1703            plan_channel: 1,
1704            declared_output_len: 3,
1705        }]);
1706        let (len, _) = p.analyze_inputs(&[], &channels, Limits::DEFAULT).unwrap();
1707        assert_eq!(len, 3);
1708        let e = p.eval(&[], &channels, Limits::DEFAULT).unwrap_err();
1709        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1710    }
1711
1712    #[cfg(feature = "deflate-replay")]
1713    fn zlib_stream(data: &[u8]) -> Vec<u8> {
1714        use flate2::Compression;
1715        use flate2::write::ZlibEncoder;
1716        use std::io::Write;
1717        let mut e = ZlibEncoder::new(Vec::new(), Compression::new(6));
1718        e.write_all(data).unwrap();
1719        e.finish().unwrap()
1720    }
1721
1722    #[cfg(feature = "deflate-replay")]
1723    #[test]
1724    fn deflate_replay_roundtrip_is_byte_exact() {
1725        let data = b"BT /F1 12 Tf (replay me) Tj ET\n".repeat(300);
1726        let z = zlib_stream(&data);
1727        let plan = crate::codec::deflate::try_replay(&z, Limits::DEFAULT).expect("replay plan");
1728        let objects = objs(&[&plan.plaintext, &plan.corrections]);
1729        // header -> replay(raw deflate) -> adler: the full zlib stream.
1730        let p = Program::new(vec![
1731            Op::Inline {
1732                bytes: plan.header.to_vec(),
1733            },
1734            Op::DeflateReplay {
1735                replay_codec: crate::dra::op::REPLAY_DEFLATE_PREFLATE_0_7_6,
1736                source_kind: crate::dra::op::DEFLATE_SOURCE_OBJECT,
1737                source_id: 0,
1738                corrections_object: 1,
1739                declared_output_len: plan.raw_len,
1740            },
1741            Op::Inline {
1742                bytes: plan.adler.to_vec(),
1743            },
1744        ]);
1745        let (len, cov) = p.analyze_objects(&objects, Limits::DEFAULT).unwrap();
1746        assert_eq!(len, z.len() as u64);
1747        cov.validate(z.len() as u64).unwrap();
1748        assert_eq!(
1749            p.eval(&objects, &[], Limits::DEFAULT).unwrap(),
1750            z,
1751            "DEFLATE_REPLAY must reproduce the original zlib stream byte-for-byte"
1752        );
1753        // The op survives its wire form.
1754        let enc = p.encode().unwrap();
1755        assert_eq!(Program::decode(&enc, Limits::DEFAULT).unwrap(), p);
1756    }
1757
1758    #[cfg(feature = "deflate-replay")]
1759    #[test]
1760    fn deflate_replay_declared_len_mismatch_errors() {
1761        let data = b"replay mismatch vector".repeat(50);
1762        let z = zlib_stream(&data);
1763        let plan = crate::codec::deflate::try_replay(&z, Limits::DEFAULT).unwrap();
1764        let objects = objs(&[&plan.plaintext, &plan.corrections]);
1765        let p = Program::new(vec![Op::DeflateReplay {
1766            replay_codec: crate::dra::op::REPLAY_DEFLATE_PREFLATE_0_7_6,
1767            source_kind: crate::dra::op::DEFLATE_SOURCE_OBJECT,
1768            source_id: 0,
1769            corrections_object: 1,
1770            declared_output_len: plan.raw_len + 1,
1771        }]);
1772        let e = p.eval(&objects, &[], Limits::DEFAULT).unwrap_err();
1773        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1774    }
1775
1776    #[test]
1777    fn deflate_replay_missing_object_errors() {
1778        let objects = objs(&[b"plain"]);
1779        let p = Program::new(vec![Op::DeflateReplay {
1780            replay_codec: crate::dra::op::REPLAY_DEFLATE_PREFLATE_0_7_6,
1781            source_kind: crate::dra::op::DEFLATE_SOURCE_OBJECT,
1782            source_id: 0,
1783            corrections_object: 9,
1784            declared_output_len: 3,
1785        }]);
1786        let e = p.analyze_objects(&objects, Limits::DEFAULT).unwrap_err();
1787        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1788    }
1789
1790    #[cfg(feature = "deflate-replay")]
1791    #[test]
1792    fn deflate_replay_hostile_corrections_is_typed_error() {
1793        let mut blob = vec![0u8; 64];
1794        for (i, b) in blob.iter_mut().enumerate() {
1795            *b = (i as u8).wrapping_mul(37).wrapping_add(11);
1796        }
1797        let objects = objs(&[b"some plaintext bytes here", &blob]);
1798        let p = Program::new(vec![Op::DeflateReplay {
1799            replay_codec: crate::dra::op::REPLAY_DEFLATE_PREFLATE_0_7_6,
1800            source_kind: crate::dra::op::DEFLATE_SOURCE_OBJECT,
1801            source_id: 0,
1802            corrections_object: 1,
1803            declared_output_len: 8,
1804        }]);
1805        let e = p.eval(&objects, &[], Limits::DEFAULT).unwrap_err();
1806        assert!(
1807            matches!(
1808                e.class(),
1809                crate::ErrorClass::CodecReplay | crate::ErrorClass::InvalidGraph
1810            ),
1811            "unexpected class: {:?}",
1812            e.class()
1813        );
1814    }
1815
1816    #[test]
1817    fn replay_profile_limit_arithmetic() {
1818        let l = Limits::DEFAULT;
1819        // Profile figure: 200% of the plaintext plus 1024 bytes of slack.
1820        assert_eq!(replay_profile_limit(0, l), REPLAY_OUTPUT_SLACK);
1821        assert_eq!(replay_profile_limit(100, l), 200 + REPLAY_OUTPUT_SLACK);
1822        assert_eq!(replay_profile_limit(1000, l), 2000 + REPLAY_OUTPUT_SLACK);
1823        // A tiny replay cap lowers the limit below the profile figure.
1824        let tiny_replay = Limits {
1825            max_replay_bytes: 500,
1826            ..Limits::DEFAULT
1827        };
1828        assert_eq!(replay_profile_limit(1000, tiny_replay), 500);
1829        // The materialization cap participates too, and the minimum wins.
1830        let tiny_output = Limits {
1831            max_output_bytes: 300,
1832            ..Limits::DEFAULT
1833        };
1834        assert_eq!(replay_profile_limit(1000, tiny_output), 300);
1835        let both = Limits {
1836            max_output_bytes: 400,
1837            max_replay_bytes: 500,
1838            ..Limits::DEFAULT
1839        };
1840        assert_eq!(replay_profile_limit(1000, both), 400);
1841    }
1842
1843    #[test]
1844    fn replay_declared_len_above_profile_limit_is_rejected() {
1845        // A 10-byte plaintext has profile limit 10*200/100 + 1024 = 1044, far
1846        // below DEFAULT's max_output_bytes/max_replay_bytes caps, so the policy
1847        // limit is what rejects the claim.
1848        let objects = objs(&[b"plaintext!", &[0u8; 4]]);
1849        let limit = replay_profile_limit(10, Limits::DEFAULT);
1850        assert_eq!(limit, 1044);
1851        let p = Program::new(vec![Op::DeflateReplay {
1852            replay_codec: crate::dra::op::REPLAY_DEFLATE_PREFLATE_0_7_6,
1853            source_kind: crate::dra::op::DEFLATE_SOURCE_OBJECT,
1854            source_id: 0,
1855            corrections_object: 1,
1856            declared_output_len: (limit + 1) as u32,
1857        }]);
1858        let e = p.analyze_objects(&objects, Limits::DEFAULT).unwrap_err();
1859        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1860        // Evaluation analyzes first, so it rejects the same claim identically.
1861        let e = p.eval(&objects, &[], Limits::DEFAULT).unwrap_err();
1862        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
1863    }
1864
1865    #[test]
1866    fn analyze_ops_lengths_match_total_and_spans() {
1867        // Exercises literal, inline, generated, entropy-channel, and bookkeeping
1868        // ops so the per-op view is not trivially one span.
1869        let p = Program::new(vec![
1870            Op::EmitObject { object_id: 0 }, // 6 bytes
1871            Op::Inline {
1872                bytes: b"12".to_vec(),
1873            }, // 2 bytes
1874            Op::RepeatLast { count: 2 },     // 4 bytes (generated)
1875            Op::DecodeChannel { channel_id: 0 }, // 3 bytes
1876            Op::MarkOffset { slot: 5 },      // 0 bytes (bookkeeping)
1877            Op::EmitOffset { slot: 5, width: 4 }, // 4 bytes (generated)
1878        ]);
1879        let (total, cov) = p.analyze(&[6], &[3], Limits::DEFAULT).unwrap();
1880        let per_op = p.analyze_ops(&[6], &[3], Limits::DEFAULT).unwrap();
1881
1882        assert_eq!(per_op.len(), p.ops.len());
1883        assert_eq!(per_op, vec![6, 2, 4, 3, 0, 4]);
1884        assert_eq!(per_op.iter().sum::<u64>(), total);
1885        assert_eq!(total, 19);
1886
1887        // Each positive-length op owns exactly one coverage span, in order, with
1888        // matching start and length; zero-length ops emit no span.
1889        let positive: Vec<u64> = per_op.iter().copied().filter(|&l| l > 0).collect();
1890        assert_eq!(positive.len(), cov.spans.len());
1891        let mut start = 0u64;
1892        for (len, span) in positive.iter().zip(cov.spans.iter()) {
1893            assert_eq!(span.start, start);
1894            assert_eq!(span.len, *len);
1895            start += *len;
1896        }
1897        assert_eq!(start, total);
1898    }
1899}