use crate::error::{Error, Result};
use crate::limits::Limits;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Op {
EmitObject {
object_id: u32,
},
Inline {
bytes: Vec<u8>,
},
RepeatLast {
count: u32,
},
DecodeChannel {
channel_id: u32,
},
InterleaveChannels {
kinds_channel: u32,
lengths_channel: u32,
first_payload_channel: u32,
payload_channel_count: u8,
},
MarkOffset {
slot: u8,
},
EmitOffset {
slot: u8,
width: u8,
},
PackSegments {
data_object: u32,
items: Vec<PackItem>,
},
PackedChannels {
data_channel: u32,
plan_channel: u32,
declared_output_len: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PackItem {
Literal {
len: u32,
},
Mark {
slot: u8,
},
Emit {
slot: u8,
width: u8,
},
}
pub const OP_EMIT_OBJECT: u8 = 0x01;
pub const OP_INLINE: u8 = 0x02;
pub const OP_REPEAT_LAST: u8 = 0x03;
pub const OP_DECODE_CHANNEL: u8 = 0x04;
pub const OP_INTERLEAVE_CHANNELS: u8 = 0x05;
pub const OP_MARK_OFFSET: u8 = 0x06;
pub const OP_EMIT_OFFSET: u8 = 0x07;
pub const OP_PACK_SEGMENTS: u8 = 0x08;
pub const OP_PACKED_CHANNELS: u8 = 0x09;
const PACK_ITEM_LITERAL: u8 = 0x01;
const PACK_ITEM_MARK: u8 = 0x02;
const PACK_ITEM_EMIT: u8 = 0x03;
impl Op {
pub fn encode(&self, out: &mut Vec<u8>) -> Result<()> {
match self {
Op::EmitObject { object_id } => {
out.push(OP_EMIT_OBJECT);
out.extend_from_slice(&object_id.to_le_bytes());
}
Op::Inline { bytes } => {
let len = u32::try_from(bytes.len())
.map_err(|_| Error::resource_limit("inline literal exceeds 4 GiB"))?;
out.push(OP_INLINE);
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(bytes);
}
Op::RepeatLast { count } => {
out.push(OP_REPEAT_LAST);
out.extend_from_slice(&count.to_le_bytes());
}
Op::DecodeChannel { channel_id } => {
out.push(OP_DECODE_CHANNEL);
out.extend_from_slice(&channel_id.to_le_bytes());
}
Op::InterleaveChannels {
kinds_channel,
lengths_channel,
first_payload_channel,
payload_channel_count,
} => {
out.push(OP_INTERLEAVE_CHANNELS);
out.extend_from_slice(&kinds_channel.to_le_bytes());
out.extend_from_slice(&lengths_channel.to_le_bytes());
out.extend_from_slice(&first_payload_channel.to_le_bytes());
out.push(*payload_channel_count);
}
Op::MarkOffset { slot } => {
out.push(OP_MARK_OFFSET);
out.push(*slot);
}
Op::EmitOffset { slot, width } => {
out.push(OP_EMIT_OFFSET);
out.push(*slot);
out.push(*width);
}
Op::PackSegments { data_object, items } => {
out.push(OP_PACK_SEGMENTS);
out.extend_from_slice(&data_object.to_le_bytes());
write_item_table(items, out)?;
}
Op::PackedChannels {
data_channel,
plan_channel,
declared_output_len,
} => {
out.push(OP_PACKED_CHANNELS);
out.extend_from_slice(&data_channel.to_le_bytes());
out.extend_from_slice(&plan_channel.to_le_bytes());
out.extend_from_slice(&declared_output_len.to_le_bytes());
}
}
Ok(())
}
pub fn decode(data: &[u8], pos: &mut usize, limits: Limits) -> Result<Op> {
let tag = *data
.get(*pos)
.ok_or_else(|| Error::invalid_graph("truncated instruction opcode"))?;
*pos += 1;
match tag {
OP_EMIT_OBJECT => {
let id = read_u32(data, pos)?;
Ok(Op::EmitObject { object_id: id })
}
OP_INLINE => {
let len = read_u32(data, pos)?;
if len > limits.max_record_len {
return Err(Error::resource_limit(
"inline literal length exceeds record limit",
));
}
let end = pos
.checked_add(len as usize)
.ok_or_else(|| Error::invalid_graph("inline length overflow"))?;
if end > data.len() {
return Err(Error::invalid_graph("truncated inline literal"));
}
let bytes = data[*pos..end].to_vec();
*pos = end;
Ok(Op::Inline { bytes })
}
OP_REPEAT_LAST => {
let count = read_u32(data, pos)?;
Ok(Op::RepeatLast { count })
}
OP_DECODE_CHANNEL => {
let id = read_u32(data, pos)?;
Ok(Op::DecodeChannel { channel_id: id })
}
OP_INTERLEAVE_CHANNELS => {
let kinds_channel = read_u32(data, pos)?;
let lengths_channel = read_u32(data, pos)?;
let first_payload_channel = read_u32(data, pos)?;
let payload_channel_count = *data
.get(*pos)
.ok_or_else(|| Error::invalid_graph("truncated instruction operand"))?;
*pos += 1;
Ok(Op::InterleaveChannels {
kinds_channel,
lengths_channel,
first_payload_channel,
payload_channel_count,
})
}
OP_MARK_OFFSET => {
let slot = read_u8(data, pos)?;
Ok(Op::MarkOffset { slot })
}
OP_EMIT_OFFSET => {
let slot = read_u8(data, pos)?;
let width = read_u8(data, pos)?;
Ok(Op::EmitOffset { slot, width })
}
OP_PACK_SEGMENTS => {
let data_object = read_u32(data, pos)?;
let item_count = read_u32(data, pos)?;
let items = read_items(data, pos, item_count, limits)?;
Ok(Op::PackSegments { data_object, items })
}
OP_PACKED_CHANNELS => {
let data_channel = read_u32(data, pos)?;
let plan_channel = read_u32(data, pos)?;
let declared_output_len = read_u64(data, pos)?;
Ok(Op::PackedChannels {
data_channel,
plan_channel,
declared_output_len,
})
}
other => Err(Error::invalid_graph(format!(
"unknown DRA opcode {other:#04x}"
))),
}
}
}
pub fn encode_items(items: &[PackItem]) -> Result<Vec<u8>> {
let mut out = Vec::with_capacity(4 + items.len() * 3);
write_item_table(items, &mut out)?;
Ok(out)
}
pub fn decode_items(bytes: &[u8], limits: Limits) -> Result<Vec<PackItem>> {
if bytes.len() < 4 {
return Err(Error::invalid_graph("truncated packed item table header"));
}
let count = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let mut pos = 4usize;
let items = read_items(bytes, &mut pos, count, limits)?;
if pos != bytes.len() {
return Err(Error::invalid_graph(format!(
"packed item table has {} trailing bytes",
bytes.len() - pos
)));
}
Ok(items)
}
fn write_item_table(items: &[PackItem], out: &mut Vec<u8>) -> Result<()> {
let count = u32::try_from(items.len())
.map_err(|_| Error::resource_limit("packed item table exceeds 4 GiB"))?;
out.extend_from_slice(&count.to_le_bytes());
write_items_into(items, out)
}
fn write_items_into(items: &[PackItem], out: &mut Vec<u8>) -> Result<()> {
for item in items {
match item {
PackItem::Literal { len } => {
out.push(PACK_ITEM_LITERAL);
write_leb128_u32(*len, out);
}
PackItem::Mark { slot } => {
out.push(PACK_ITEM_MARK);
out.push(*slot);
}
PackItem::Emit { slot, width } => {
out.push(PACK_ITEM_EMIT);
out.push(*slot);
out.push(*width);
}
}
}
Ok(())
}
fn read_items(data: &[u8], pos: &mut usize, count: u32, limits: Limits) -> Result<Vec<PackItem>> {
if count > limits.max_graph_ops {
return Err(Error::resource_limit(format!(
"packed item count {count} exceeds limit {}",
limits.max_graph_ops
)));
}
let mut items = Vec::with_capacity(count.min(4096) as usize);
for _ in 0..count {
let tag = *data
.get(*pos)
.ok_or_else(|| Error::invalid_graph("truncated packed item"))?;
*pos += 1;
match tag {
PACK_ITEM_LITERAL => {
let len = read_leb128_u32(data, pos)?;
items.push(PackItem::Literal { len });
}
PACK_ITEM_MARK => {
let slot = read_u8(data, pos)?;
items.push(PackItem::Mark { slot });
}
PACK_ITEM_EMIT => {
let slot = read_u8(data, pos)?;
let width = read_u8(data, pos)?;
items.push(PackItem::Emit { slot, width });
}
other => {
return Err(Error::invalid_graph(format!(
"unknown packed item tag {other:#04x}"
)));
}
}
}
Ok(items)
}
fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8> {
let v = *data
.get(*pos)
.ok_or_else(|| Error::invalid_graph("truncated instruction operand"))?;
*pos += 1;
Ok(v)
}
fn write_leb128_u32(value: u32, out: &mut Vec<u8>) {
let mut v = value;
loop {
let byte = (v & 0x7f) as u8;
v >>= 7;
if v == 0 {
out.push(byte);
return;
}
out.push(byte | 0x80);
}
}
fn read_leb128_u32(data: &[u8], pos: &mut usize) -> Result<u32> {
let mut result: u32 = 0;
for shift in [0u32, 7, 14, 21, 28] {
let byte = *data
.get(*pos)
.ok_or_else(|| Error::invalid_graph("truncated LEB128 operand"))?;
*pos += 1;
let low = u32::from(byte & 0x7f);
if shift == 28 && low > 0x0f {
return Err(Error::invalid_graph("LEB128 varint overflows u32"));
}
result |= low << shift;
if byte & 0x80 == 0 {
return Ok(result);
}
}
Err(Error::invalid_graph("LEB128 varint overflows u32"))
}
fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64> {
let end = pos
.checked_add(8)
.ok_or_else(|| Error::invalid_graph("operand offset overflow"))?;
if end > data.len() {
return Err(Error::invalid_graph("truncated instruction operand"));
}
let mut buf = [0u8; 8];
buf.copy_from_slice(&data[*pos..end]);
*pos = end;
Ok(u64::from_le_bytes(buf))
}
fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32> {
let end = pos
.checked_add(4)
.ok_or_else(|| Error::invalid_graph("operand offset overflow"))?;
if end > data.len() {
return Err(Error::invalid_graph("truncated instruction operand"));
}
let v = u32::from_le_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]);
*pos = end;
Ok(v)
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(op: Op) {
let mut buf = Vec::new();
op.encode(&mut buf).unwrap();
let mut pos = 0;
let back = Op::decode(&buf, &mut pos, Limits::DEFAULT).unwrap();
assert_eq!(op, back);
assert_eq!(pos, buf.len());
}
#[test]
fn op_roundtrips() {
roundtrip(Op::EmitObject { object_id: 7 });
roundtrip(Op::Inline {
bytes: vec![1, 2, 3, 4, 5],
});
roundtrip(Op::RepeatLast { count: 1_000_000 });
roundtrip(Op::Inline { bytes: Vec::new() });
roundtrip(Op::DecodeChannel { channel_id: 3 });
roundtrip(Op::InterleaveChannels {
kinds_channel: 0,
lengths_channel: 1,
first_payload_channel: 2,
payload_channel_count: 3,
});
roundtrip(Op::MarkOffset { slot: 0 });
roundtrip(Op::MarkOffset { slot: 15 });
roundtrip(Op::EmitOffset { slot: 0, width: 1 });
roundtrip(Op::EmitOffset { slot: 7, width: 20 });
roundtrip(Op::PackSegments {
data_object: 2,
items: vec![
PackItem::Literal { len: 3 },
PackItem::Mark { slot: 0 },
PackItem::Literal { len: 300 },
PackItem::Emit { slot: 0, width: 3 },
],
});
roundtrip(Op::PackedChannels {
data_channel: 0,
plan_channel: 1,
declared_output_len: 9,
});
roundtrip(Op::PackedChannels {
data_channel: 3,
plan_channel: 4,
declared_output_len: u64::MAX,
});
}
#[test]
fn rejects_unknown_opcode() {
let data = [0xEEu8];
let mut pos = 0;
let e = Op::decode(&data, &mut pos, Limits::DEFAULT).unwrap_err();
assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
}
#[test]
fn rejects_truncated_operand() {
let data = [OP_EMIT_OBJECT, 0x01, 0x02];
let mut pos = 0;
let e = Op::decode(&data, &mut pos, Limits::DEFAULT).unwrap_err();
assert_eq!(e.class(), crate::ErrorClass::InvalidGraph);
}
}