Skip to main content

vole_document/dra/
op.rs

1//! DRA instruction set.
2//!
3//! The algebra is deliberately tiny, versioned, and non-Turing-complete. Every
4//! instruction has a deterministic, statically boundable output length. There
5//! is no I/O, no clock, no RNG, and no external authority.
6
7use crate::error::{Error, Result};
8use crate::limits::Limits;
9
10/// A single reconstruction instruction.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Op {
13    /// Emit the exact bytes of the referenced object.
14    EmitObject {
15        /// Index into the descriptor's object table.
16        object_id: u32,
17    },
18    /// Emit literal bytes carried inline in the graph record.
19    Inline {
20        /// The literal bytes.
21        bytes: Vec<u8>,
22    },
23    /// Repeat the byte output of the immediately preceding instruction
24    /// `count` additional times.
25    RepeatLast {
26        /// Number of extra copies.
27        count: u32,
28    },
29    /// Emit the decoded bytes of the referenced entropy channel.
30    DecodeChannel {
31        /// Index into the descriptor's entropy channel table.
32        channel_id: u32,
33    },
34    /// Reconstruct bytes by interleaving a contiguous range of per-kind payload
35    /// channels. A *kind* channel holds one kind byte per token; a *lengths*
36    /// channel holds one little-endian `u32` per token; payload channel
37    /// `first_payload_channel + k` carries the token spans whose kind is `k`.
38    InterleaveChannels {
39        /// Channel holding one kind byte per token.
40        kinds_channel: u32,
41        /// Channel holding four little-endian length bytes per token.
42        lengths_channel: u32,
43        /// First channel of the contiguous per-kind payload channel range.
44        first_payload_channel: u32,
45        /// Number of payload channels; valid kinds are `0..payload_channel_count`.
46        payload_channel_count: u8,
47    },
48    /// Record the current output position (a u64) into the named slot. Emits no
49    /// output bytes; the recorded value is available to later [`Op::EmitOffset`]
50    /// instructions.
51    MarkOffset {
52        /// Slot index; must be below [`crate::dra::program::MAX_OFFSET_SLOTS`].
53        slot: u8,
54    },
55    /// Emit the decimal form of a previously marked output position, left
56    /// zero-padded to exactly `width` bytes.
57    EmitOffset {
58        /// Slot index marked by an earlier [`Op::MarkOffset`].
59        slot: u8,
60        /// Exact output width in bytes.
61        width: u8,
62    },
63    /// Reconstruct output from a compact item table over one data object,
64    /// amortizing per-segment framing: literal runs copy contiguous data-object
65    /// bytes, marks record output positions, and emits render marked positions as
66    /// fixed-width decimals. The data object must be consumed exactly.
67    PackSegments {
68        /// Index into the descriptor's object table.
69        data_object: u32,
70        /// Ordered items describing the reconstruction.
71        items: Vec<PackItem>,
72    },
73    /// Reconstruct output from a *data* entropy channel interpreted by a
74    /// serialized item table carried in a *plan* entropy channel. This lets one
75    /// layout plan travel through rANS-coded channels instead of a literal data
76    /// object, so the plan and its data pay entropy-coding cost together. The
77    /// plan channel holds exactly [`encode_items`] of the item table, and the
78    /// data channel must be consumed exactly.
79    PackedChannels {
80        /// Index into the descriptor's entropy channel table for the data.
81        data_channel: u32,
82        /// Index into the descriptor's entropy channel table for the plan.
83        plan_channel: u32,
84        /// Exact reconstructed output length; must equal the produced length.
85        declared_output_len: u64,
86    },
87    /// Reconstruct the *exact original raw DEFLATE bitstream* from a plaintext
88    /// source plus an opaque correction object, replaying the producer's DEFLATE
89    /// coding decisions. Emits raw DEFLATE (RFC 1951) bytes only; any zlib framing
90    /// is composed by surrounding instructions. `declared_output_len` is the exact
91    /// expected raw payload length, validated at evaluation.
92    ///
93    /// The plaintext may come from either the object table (`source_kind =
94    /// `[`DEFLATE_SOURCE_OBJECT`]) or an entropy channel (`source_kind =
95    /// `[`DEFLATE_SOURCE_CHANNEL`]); the channel form lets several streams share
96    /// one stored plaintext capsule. Reconstruction can panic internally on
97    /// hostile correction data, so it is isolated and never panics the decoder
98    /// (see `crate::codec::deflate`).
99    DeflateReplay {
100        /// Replay-codec identity: which exact reconstruction semantics the
101        /// `corrections` blob is bound to. Must be [`REPLAY_DEFLATE_PREFLATE_0_7_6`].
102        /// The correction state is an opaque, version-coupled `preflate` blob; this
103        /// tag makes that explicit on the wire so the format can never silently
104        /// treat the current preflate internal representation as a stable standard.
105        replay_codec: u8,
106        /// Plaintext source kind ([`DEFLATE_SOURCE_OBJECT`] / [`DEFLATE_SOURCE_CHANNEL`]).
107        source_kind: u8,
108        /// Index into the object or channel table selected by `source_kind`.
109        source_id: u32,
110        /// Index into the descriptor's object table holding the corrections.
111        corrections_object: u32,
112        /// Exact length (bytes) of the raw DEFLATE payload the op reproduces.
113        declared_output_len: u32,
114    },
115}
116
117/// One item in a [`Op::PackSegments`] item table.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum PackItem {
120    /// Copy `len` contiguous bytes from the data object, advancing its cursor.
121    Literal {
122        /// Number of bytes to copy.
123        len: u32,
124    },
125    /// Record the current output position into `slot`.
126    Mark {
127        /// Slot index; must be below [`crate::dra::program::MAX_OFFSET_SLOTS`].
128        slot: u8,
129    },
130    /// Emit the marked value of `slot`, zero-padded to `width` decimal bytes.
131    Emit {
132        /// Slot index marked by an earlier [`PackItem::Mark`].
133        slot: u8,
134        /// Exact output width in bytes; `1..=20`.
135        width: u8,
136    },
137}
138
139/// Opcode byte for [`Op::EmitObject`].
140pub const OP_EMIT_OBJECT: u8 = 0x01;
141/// Opcode byte for [`Op::Inline`].
142pub const OP_INLINE: u8 = 0x02;
143/// Opcode byte for [`Op::RepeatLast`].
144pub const OP_REPEAT_LAST: u8 = 0x03;
145/// Opcode byte for [`Op::DecodeChannel`].
146pub const OP_DECODE_CHANNEL: u8 = 0x04;
147/// Opcode byte for [`Op::InterleaveChannels`].
148pub const OP_INTERLEAVE_CHANNELS: u8 = 0x05;
149/// Opcode byte for [`Op::MarkOffset`].
150pub const OP_MARK_OFFSET: u8 = 0x06;
151/// Opcode byte for [`Op::EmitOffset`].
152pub const OP_EMIT_OFFSET: u8 = 0x07;
153/// Opcode byte for [`Op::PackSegments`].
154pub const OP_PACK_SEGMENTS: u8 = 0x08;
155/// Opcode byte for [`Op::PackedChannels`].
156pub const OP_PACKED_CHANNELS: u8 = 0x09;
157/// Opcode byte for [`Op::DeflateReplay`].
158pub const OP_DEFLATE_REPLAY: u8 = 0x0A;
159/// [`Op::DeflateReplay`] plaintext source: the object table.
160pub const DEFLATE_SOURCE_OBJECT: u8 = 0;
161/// [`Op::DeflateReplay`] plaintext source: the entropy channel table.
162pub const DEFLATE_SOURCE_CHANNEL: u8 = 1;
163/// Replay-codec identity for exact DEFLATE replay: `preflate` 0.7.6 semantics.
164///
165/// This names an **experimental**, version-coupled decoder contract, not a frozen
166/// archival standard: the correction blob is `preflate`'s private
167/// bitcode+CABAC layout. A future VOLE-owned implementation would be a new codec
168/// id (and a new universe).
169pub const REPLAY_DEFLATE_PREFLATE_0_7_6: u8 = 1;
170
171/// Item tag for [`PackItem::Literal`].
172const PACK_ITEM_LITERAL: u8 = 0x01;
173/// Item tag for [`PackItem::Mark`].
174const PACK_ITEM_MARK: u8 = 0x02;
175/// Item tag for [`PackItem::Emit`].
176const PACK_ITEM_EMIT: u8 = 0x03;
177
178impl Op {
179    /// Encode this instruction into `out`.
180    pub fn encode(&self, out: &mut Vec<u8>) -> Result<()> {
181        match self {
182            Op::EmitObject { object_id } => {
183                out.push(OP_EMIT_OBJECT);
184                out.extend_from_slice(&object_id.to_le_bytes());
185            }
186            Op::Inline { bytes } => {
187                let len = u32::try_from(bytes.len())
188                    .map_err(|_| Error::resource_limit("inline literal exceeds 4 GiB"))?;
189                out.push(OP_INLINE);
190                out.extend_from_slice(&len.to_le_bytes());
191                out.extend_from_slice(bytes);
192            }
193            Op::RepeatLast { count } => {
194                out.push(OP_REPEAT_LAST);
195                out.extend_from_slice(&count.to_le_bytes());
196            }
197            Op::DecodeChannel { channel_id } => {
198                out.push(OP_DECODE_CHANNEL);
199                out.extend_from_slice(&channel_id.to_le_bytes());
200            }
201            Op::InterleaveChannels {
202                kinds_channel,
203                lengths_channel,
204                first_payload_channel,
205                payload_channel_count,
206            } => {
207                out.push(OP_INTERLEAVE_CHANNELS);
208                out.extend_from_slice(&kinds_channel.to_le_bytes());
209                out.extend_from_slice(&lengths_channel.to_le_bytes());
210                out.extend_from_slice(&first_payload_channel.to_le_bytes());
211                out.push(*payload_channel_count);
212            }
213            Op::MarkOffset { slot } => {
214                out.push(OP_MARK_OFFSET);
215                out.push(*slot);
216            }
217            Op::EmitOffset { slot, width } => {
218                out.push(OP_EMIT_OFFSET);
219                out.push(*slot);
220                out.push(*width);
221            }
222            Op::PackSegments { data_object, items } => {
223                out.push(OP_PACK_SEGMENTS);
224                out.extend_from_slice(&data_object.to_le_bytes());
225                write_item_table(items, out)?;
226            }
227            Op::PackedChannels {
228                data_channel,
229                plan_channel,
230                declared_output_len,
231            } => {
232                out.push(OP_PACKED_CHANNELS);
233                out.extend_from_slice(&data_channel.to_le_bytes());
234                out.extend_from_slice(&plan_channel.to_le_bytes());
235                out.extend_from_slice(&declared_output_len.to_le_bytes());
236            }
237            Op::DeflateReplay {
238                replay_codec,
239                source_kind,
240                source_id,
241                corrections_object,
242                declared_output_len,
243            } => {
244                out.push(OP_DEFLATE_REPLAY);
245                out.push(*replay_codec);
246                out.push(*source_kind);
247                out.extend_from_slice(&source_id.to_le_bytes());
248                out.extend_from_slice(&corrections_object.to_le_bytes());
249                out.extend_from_slice(&declared_output_len.to_le_bytes());
250            }
251        }
252        Ok(())
253    }
254
255    /// Decode one instruction from `data[*pos..]`, advancing `*pos`.
256    pub fn decode(data: &[u8], pos: &mut usize, limits: Limits) -> Result<Op> {
257        let tag = *data
258            .get(*pos)
259            .ok_or_else(|| Error::invalid_graph("truncated instruction opcode"))?;
260        *pos += 1;
261        match tag {
262            OP_EMIT_OBJECT => {
263                let id = read_u32(data, pos)?;
264                Ok(Op::EmitObject { object_id: id })
265            }
266            OP_INLINE => {
267                let len = read_u32(data, pos)?;
268                if len > limits.max_record_len {
269                    return Err(Error::resource_limit(
270                        "inline literal length exceeds record limit",
271                    ));
272                }
273                let end = pos
274                    .checked_add(len as usize)
275                    .ok_or_else(|| Error::invalid_graph("inline length overflow"))?;
276                if end > data.len() {
277                    return Err(Error::invalid_graph("truncated inline literal"));
278                }
279                let bytes = data[*pos..end].to_vec();
280                *pos = end;
281                Ok(Op::Inline { bytes })
282            }
283            OP_REPEAT_LAST => {
284                let count = read_u32(data, pos)?;
285                Ok(Op::RepeatLast { count })
286            }
287            OP_DECODE_CHANNEL => {
288                let id = read_u32(data, pos)?;
289                Ok(Op::DecodeChannel { channel_id: id })
290            }
291            OP_INTERLEAVE_CHANNELS => {
292                let kinds_channel = read_u32(data, pos)?;
293                let lengths_channel = read_u32(data, pos)?;
294                let first_payload_channel = read_u32(data, pos)?;
295                let payload_channel_count = *data
296                    .get(*pos)
297                    .ok_or_else(|| Error::invalid_graph("truncated instruction operand"))?;
298                *pos += 1;
299                Ok(Op::InterleaveChannels {
300                    kinds_channel,
301                    lengths_channel,
302                    first_payload_channel,
303                    payload_channel_count,
304                })
305            }
306            OP_MARK_OFFSET => {
307                let slot = read_u8(data, pos)?;
308                Ok(Op::MarkOffset { slot })
309            }
310            OP_EMIT_OFFSET => {
311                let slot = read_u8(data, pos)?;
312                let width = read_u8(data, pos)?;
313                Ok(Op::EmitOffset { slot, width })
314            }
315            OP_PACK_SEGMENTS => {
316                let data_object = read_u32(data, pos)?;
317                let item_count = read_u32(data, pos)?;
318                let items = read_items(data, pos, item_count, limits)?;
319                Ok(Op::PackSegments { data_object, items })
320            }
321            OP_PACKED_CHANNELS => {
322                let data_channel = read_u32(data, pos)?;
323                let plan_channel = read_u32(data, pos)?;
324                let declared_output_len = read_u64(data, pos)?;
325                Ok(Op::PackedChannels {
326                    data_channel,
327                    plan_channel,
328                    declared_output_len,
329                })
330            }
331            OP_DEFLATE_REPLAY => {
332                let replay_codec = read_u8(data, pos)?;
333                if replay_codec != REPLAY_DEFLATE_PREFLATE_0_7_6 {
334                    return Err(Error::unsupported_feature(format!(
335                        "DEFLATE_REPLAY codec {replay_codec} is not the supported preflate-0.7.6 semantics"
336                    )));
337                }
338                let source_kind = read_u8(data, pos)?;
339                if source_kind != DEFLATE_SOURCE_OBJECT && source_kind != DEFLATE_SOURCE_CHANNEL {
340                    return Err(Error::invalid_graph(format!(
341                        "DEFLATE_REPLAY source kind {source_kind} is not 0 (object) or 1 (channel)"
342                    )));
343                }
344                let source_id = read_u32(data, pos)?;
345                let corrections_object = read_u32(data, pos)?;
346                let declared_output_len = read_u32(data, pos)?;
347                Ok(Op::DeflateReplay {
348                    replay_codec,
349                    source_kind,
350                    source_id,
351                    corrections_object,
352                    declared_output_len,
353                })
354            }
355            other => Err(Error::invalid_graph(format!(
356                "unknown DRA opcode {other:#04x}"
357            ))),
358        }
359    }
360}
361
362/// Encode `items` into the shared packed-item wire form
363/// `[item_count u32 LE][items...]`.
364///
365/// This is the canonical codec used by both [`Op::PackSegments`] (inline in the
366/// graph record) and [`Op::PackedChannels`] (carried in a plan entropy channel),
367/// so the two never diverge.
368pub fn encode_items(items: &[PackItem]) -> Result<Vec<u8>> {
369    let mut out = Vec::with_capacity(4 + items.len() * 3);
370    write_item_table(items, &mut out)?;
371    Ok(out)
372}
373
374/// Decode a complete `[item_count u32 LE][items...]` table from `bytes`.
375///
376/// Bounded: rejects truncation, unknown item tags, a count above
377/// [`Limits::max_graph_ops`], and trailing bytes past the table.
378pub fn decode_items(bytes: &[u8], limits: Limits) -> Result<Vec<PackItem>> {
379    if bytes.len() < 4 {
380        return Err(Error::invalid_graph("truncated packed item table header"));
381    }
382    let count = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
383    let mut pos = 4usize;
384    let items = read_items(bytes, &mut pos, count, limits)?;
385    if pos != bytes.len() {
386        return Err(Error::invalid_graph(format!(
387            "packed item table has {} trailing bytes",
388            bytes.len() - pos
389        )));
390    }
391    Ok(items)
392}
393
394/// Append the full item table (`[item_count u32 LE][items...]`) to `out`.
395fn write_item_table(items: &[PackItem], out: &mut Vec<u8>) -> Result<()> {
396    let count = u32::try_from(items.len())
397        .map_err(|_| Error::resource_limit("packed item table exceeds 4 GiB"))?;
398    out.extend_from_slice(&count.to_le_bytes());
399    write_items_into(items, out)
400}
401
402/// Append the item list (without the count prefix) to `out`.
403fn write_items_into(items: &[PackItem], out: &mut Vec<u8>) -> Result<()> {
404    for item in items {
405        match item {
406            PackItem::Literal { len } => {
407                out.push(PACK_ITEM_LITERAL);
408                write_leb128_u32(*len, out);
409            }
410            PackItem::Mark { slot } => {
411                out.push(PACK_ITEM_MARK);
412                out.push(*slot);
413            }
414            PackItem::Emit { slot, width } => {
415                out.push(PACK_ITEM_EMIT);
416                out.push(*slot);
417                out.push(*width);
418            }
419        }
420    }
421    Ok(())
422}
423
424/// Decode `count` items from `data[*pos..]`, advancing `*pos`. Shared by the
425/// inline [`Op::PackSegments`] table and [`decode_items`].
426fn read_items(data: &[u8], pos: &mut usize, count: u32, limits: Limits) -> Result<Vec<PackItem>> {
427    if count > limits.max_graph_ops {
428        return Err(Error::resource_limit(format!(
429            "packed item count {count} exceeds limit {}",
430            limits.max_graph_ops
431        )));
432    }
433    let mut items = Vec::with_capacity(count.min(4096) as usize);
434    for _ in 0..count {
435        let tag = *data
436            .get(*pos)
437            .ok_or_else(|| Error::invalid_graph("truncated packed item"))?;
438        *pos += 1;
439        match tag {
440            PACK_ITEM_LITERAL => {
441                let len = read_leb128_u32(data, pos)?;
442                items.push(PackItem::Literal { len });
443            }
444            PACK_ITEM_MARK => {
445                let slot = read_u8(data, pos)?;
446                items.push(PackItem::Mark { slot });
447            }
448            PACK_ITEM_EMIT => {
449                let slot = read_u8(data, pos)?;
450                let width = read_u8(data, pos)?;
451                items.push(PackItem::Emit { slot, width });
452            }
453            other => {
454                return Err(Error::invalid_graph(format!(
455                    "unknown packed item tag {other:#04x}"
456                )));
457            }
458        }
459    }
460    Ok(items)
461}
462
463fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8> {
464    let v = *data
465        .get(*pos)
466        .ok_or_else(|| Error::invalid_graph("truncated instruction operand"))?;
467    *pos += 1;
468    Ok(v)
469}
470
471/// Append `value` to `out` as a minimal unsigned LEB128 varint (at most 5 bytes).
472fn write_leb128_u32(value: u32, out: &mut Vec<u8>) {
473    let mut v = value;
474    loop {
475        let byte = (v & 0x7f) as u8;
476        v >>= 7;
477        if v == 0 {
478            out.push(byte);
479            return;
480        }
481        out.push(byte | 0x80);
482    }
483}
484
485/// Read an unsigned LEB128 varint bounded to `u32` from `data[*pos..]`, advancing
486/// `*pos`. Rejects truncation, non-minimal overflow, and any encoding that would
487/// exceed `u32` (more than five bytes).
488fn read_leb128_u32(data: &[u8], pos: &mut usize) -> Result<u32> {
489    let mut result: u32 = 0;
490    for shift in [0u32, 7, 14, 21, 28] {
491        let byte = *data
492            .get(*pos)
493            .ok_or_else(|| Error::invalid_graph("truncated LEB128 operand"))?;
494        *pos += 1;
495        let low = u32::from(byte & 0x7f);
496        if shift == 28 && low > 0x0f {
497            return Err(Error::invalid_graph("LEB128 varint overflows u32"));
498        }
499        result |= low << shift;
500        if byte & 0x80 == 0 {
501            return Ok(result);
502        }
503    }
504    Err(Error::invalid_graph("LEB128 varint overflows u32"))
505}
506
507fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64> {
508    let end = pos
509        .checked_add(8)
510        .ok_or_else(|| Error::invalid_graph("operand offset overflow"))?;
511    if end > data.len() {
512        return Err(Error::invalid_graph("truncated instruction operand"));
513    }
514    let mut buf = [0u8; 8];
515    buf.copy_from_slice(&data[*pos..end]);
516    *pos = end;
517    Ok(u64::from_le_bytes(buf))
518}
519
520fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32> {
521    let end = pos
522        .checked_add(4)
523        .ok_or_else(|| Error::invalid_graph("operand offset overflow"))?;
524    if end > data.len() {
525        return Err(Error::invalid_graph("truncated instruction operand"));
526    }
527    let v = u32::from_le_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]);
528    *pos = end;
529    Ok(v)
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535
536    fn roundtrip(op: Op) {
537        let mut buf = Vec::new();
538        op.encode(&mut buf).unwrap();
539        let mut pos = 0;
540        let back = Op::decode(&buf, &mut pos, Limits::DEFAULT).unwrap();
541        assert_eq!(op, back);
542        assert_eq!(pos, buf.len());
543    }
544
545    #[test]
546    fn op_roundtrips() {
547        roundtrip(Op::EmitObject { object_id: 7 });
548        roundtrip(Op::Inline {
549            bytes: vec![1, 2, 3, 4, 5],
550        });
551        roundtrip(Op::RepeatLast { count: 1_000_000 });
552        roundtrip(Op::Inline { bytes: Vec::new() });
553        roundtrip(Op::DecodeChannel { channel_id: 3 });
554        roundtrip(Op::InterleaveChannels {
555            kinds_channel: 0,
556            lengths_channel: 1,
557            first_payload_channel: 2,
558            payload_channel_count: 3,
559        });
560        roundtrip(Op::MarkOffset { slot: 0 });
561        roundtrip(Op::MarkOffset { slot: 15 });
562        roundtrip(Op::EmitOffset { slot: 0, width: 1 });
563        roundtrip(Op::EmitOffset { slot: 7, width: 20 });
564        roundtrip(Op::PackSegments {
565            data_object: 2,
566            items: vec![
567                PackItem::Literal { len: 3 },
568                PackItem::Mark { slot: 0 },
569                PackItem::Literal { len: 300 },
570                PackItem::Emit { slot: 0, width: 3 },
571            ],
572        });
573        roundtrip(Op::PackedChannels {
574            data_channel: 0,
575            plan_channel: 1,
576            declared_output_len: 9,
577        });
578        roundtrip(Op::PackedChannels {
579            data_channel: 3,
580            plan_channel: 4,
581            declared_output_len: u64::MAX,
582        });
583        roundtrip(Op::DeflateReplay {
584            replay_codec: REPLAY_DEFLATE_PREFLATE_0_7_6,
585            source_kind: DEFLATE_SOURCE_OBJECT,
586            source_id: 0,
587            corrections_object: 1,
588            declared_output_len: 1234,
589        });
590        roundtrip(Op::DeflateReplay {
591            replay_codec: REPLAY_DEFLATE_PREFLATE_0_7_6,
592            source_kind: DEFLATE_SOURCE_CHANNEL,
593            source_id: u32::MAX,
594            corrections_object: u32::MAX,
595            declared_output_len: u32::MAX,
596        });
597    }
598
599    #[test]
600    fn rejects_unknown_replay_codec_as_unsupported_feature() {
601        // A graph record whose DEFLATE_REPLAY op names a replay codec this build
602        // does not implement must fail as `UnsupportedFeature`, never be
603        // silently rerun with the wrong (preflate 0.7.6) semantics (FIX2).
604        let data = [OP_DEFLATE_REPLAY, 2];
605        let mut pos = 0;
606        let e = Op::decode(&data, &mut pos, Limits::DEFAULT).unwrap_err();
607        assert_eq!(e.class(), crate::ErrorClass::UnsupportedFeature);
608    }
609
610    #[test]
611    fn rejects_unknown_opcode() {
612        let data = [0xEEu8];
613        let mut pos = 0;
614        let e = Op::decode(&data, &mut pos, Limits::DEFAULT).unwrap_err();
615        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
616    }
617
618    #[test]
619    fn rejects_truncated_operand() {
620        let data = [OP_EMIT_OBJECT, 0x01, 0x02];
621        let mut pos = 0;
622        let e = Op::decode(&data, &mut pos, Limits::DEFAULT).unwrap_err();
623        assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
624    }
625}