extern crate alloc;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use rkyv::{Archive, Deserialize, Serialize};
use crate::bytecode::{
BYTECODE_MAGIC, BYTECODE_VERSION, BlockType, Chunk, ConstValue, DataLayout, LoadError, Module,
Op, StructTemplate, TypeTag, crc32,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WireFormatError {
OpcodeRecordParityMismatch,
UnknownOpcodeId(u8),
OperandPoolIndexOutOfBounds(usize),
OperandPoolParityMismatch,
OperandPoolTagMismatch {
observed: u8,
expected: u8,
},
OperandPoolIndexOverflow,
TupleFieldKindUnknown(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OpcodeId(pub u8);
pub const POOL_TAG_U16_U16: u8 = 0x01;
pub const POOL_TAG_U16_U16_U8: u8 = 0x02;
pub const POOL_TAG_U16_U16_U16: u8 = 0x03;
pub const TUPLE_FIELD_BOXED_SENTINEL: u8 = 0xFF;
pub const FLAT_NESTED_SENTINEL_BASE: u8 = 0xF0;
pub const NEW_COMPOSITE_INLINE_MAX_COUNT: u16 = 0x3E;
pub const NEW_COMPOSITE_POOL_SENTINEL: u8 = 0x3F;
pub const FLAT_NESTED_SENTINEL_MASK: u8 = 0xFC;
pub const OPCODE_RECORD_BYTES: usize = 4;
pub const OPERAND_POOL_ENTRY_BYTES: usize = 8;
pub const WIRE_FORMAT_HEADER_BYTES: usize = 64;
pub const SIGNATURE_METADATA_BYTES: usize = 8;
pub const FLAG_REQUIRES_SIGNATURE: u8 = 0x02;
pub const FLAG_ENCRYPTED: u8 = 0x04;
pub const SIGNATURE_SCHEME_ED25519: u8 = 0x01;
pub const ED25519_SIGNATURE_BYTES: usize = 64;
pub const ED25519_VERIFYING_KEY_BYTES: usize = 32;
pub const ENCRYPTION_METADATA_BYTES: usize = 88;
pub const MAX_POOL_ENTRIES: usize = 1 << 24;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OpcodeRecord(pub [u8; OPCODE_RECORD_BYTES]);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OperandPoolEntry(pub [u8; OPERAND_POOL_ENTRY_BYTES]);
impl OpcodeRecord {
pub fn from_raw(bytes: [u8; OPCODE_RECORD_BYTES]) -> Self {
OpcodeRecord(bytes)
}
pub fn from_id_and_operand(id: OpcodeId, operand: [u8; 3]) -> Self {
let raw = [id.0 & 0x7F, operand[0], operand[1], operand[2]];
let parity = compute_parity(&raw);
let mut bytes = raw;
bytes[0] |= parity << 7;
OpcodeRecord(bytes)
}
pub fn opcode_id(&self) -> OpcodeId {
OpcodeId(self.0[0] & 0x7F)
}
pub fn operand_bytes(&self) -> [u8; 3] {
[self.0[1], self.0[2], self.0[3]]
}
pub fn check_parity(&self) -> Result<(), WireFormatError> {
let masked = [self.0[0] & 0x7F, self.0[1], self.0[2], self.0[3]];
let expected = (self.0[0] >> 7) & 1;
if compute_parity(&masked) == expected {
Ok(())
} else {
Err(WireFormatError::OpcodeRecordParityMismatch)
}
}
pub fn operand_u8(&self) -> u8 {
self.0[1]
}
pub fn operand_u16(&self) -> u16 {
u16::from_le_bytes([self.0[1], self.0[2]])
}
pub fn operand_u16_u8(&self) -> (u16, u8) {
(u16::from_le_bytes([self.0[1], self.0[2]]), self.0[3])
}
pub fn operand_pool_index(&self) -> u32 {
u32::from_le_bytes([self.0[1], self.0[2], self.0[3], 0])
}
}
impl OperandPoolEntry {
pub fn from_u16_u16(a: u16, b: u16) -> Self {
let mut bytes = [0u8; OPERAND_POOL_ENTRY_BYTES];
bytes[0] = POOL_TAG_U16_U16;
bytes[2..4].copy_from_slice(&a.to_le_bytes());
bytes[4..6].copy_from_slice(&b.to_le_bytes());
let parity_payload = [
bytes[0], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
];
bytes[1] = pool_entry_parity(&parity_payload);
OperandPoolEntry(bytes)
}
pub fn from_u16_u16_u8(a: u16, b: u16, c: u8) -> Self {
let mut bytes = [0u8; OPERAND_POOL_ENTRY_BYTES];
bytes[0] = POOL_TAG_U16_U16_U8;
bytes[2..4].copy_from_slice(&a.to_le_bytes());
bytes[4..6].copy_from_slice(&b.to_le_bytes());
bytes[6] = c;
let parity_payload = [
bytes[0], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
];
bytes[1] = pool_entry_parity(&parity_payload);
OperandPoolEntry(bytes)
}
pub fn from_u16_u16_u16(a: u16, b: u16, c: u16) -> Self {
let mut bytes = [0u8; OPERAND_POOL_ENTRY_BYTES];
bytes[0] = POOL_TAG_U16_U16_U16;
bytes[2..4].copy_from_slice(&a.to_le_bytes());
bytes[4..6].copy_from_slice(&b.to_le_bytes());
bytes[6..8].copy_from_slice(&c.to_le_bytes());
let parity_payload = [
bytes[0], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
];
bytes[1] = pool_entry_parity(&parity_payload);
OperandPoolEntry(bytes)
}
pub fn check_parity(&self) -> Result<(), WireFormatError> {
let parity_payload = [
self.0[0], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7],
];
if pool_entry_parity(&parity_payload) == self.0[1] {
Ok(())
} else {
Err(WireFormatError::OperandPoolParityMismatch)
}
}
pub fn tag(&self) -> u8 {
self.0[0]
}
pub fn as_u16_u16(&self) -> (u16, u16) {
(
u16::from_le_bytes([self.0[2], self.0[3]]),
u16::from_le_bytes([self.0[4], self.0[5]]),
)
}
pub fn as_u16_u16_u16(&self) -> (u16, u16, u16) {
let (a, b) = self.as_u16_u16();
(a, b, u16::from_le_bytes([self.0[6], self.0[7]]))
}
pub fn as_u16_u16_u8(&self) -> (u16, u16, u8) {
let (a, b) = self.as_u16_u16();
(a, b, self.0[6])
}
}
fn compute_parity(bytes: &[u8; OPCODE_RECORD_BYTES]) -> u8 {
let total = bytes.iter().map(|b| b.count_ones()).sum::<u32>();
(total & 1) as u8
}
fn pool_entry_parity(bytes: &[u8; 7]) -> u8 {
bytes.iter().fold(0u8, |acc, b| acc ^ b)
}
const OPCODE_ID_TABLE: &[(&str, u8)] = &[
("Const", 0),
("GetLocal", 1),
("SetLocal", 2),
("GetData", 3),
("SetData", 4),
("GetDataIndexed", 5),
("SetDataIndexed", 6),
("BoundsCheck", 7),
("Add", 8),
("Sub", 9),
("Mul", 10),
("Div", 11),
("Mod", 12),
("Neg", 13),
("CmpEq", 14),
("CmpNe", 15),
("CmpLt", 16),
("CmpGt", 17),
("CmpLe", 18),
("CmpGe", 19),
("Not", 20),
("If", 21),
("Else", 22),
("EndIf", 23),
("Loop", 24),
("EndLoop", 25),
("Break", 26),
("BreakIf", 27),
("Stream", 28),
("Reset", 29),
("Call", 30),
("Return", 31),
("Yield", 32),
("Dup", 33),
("GetField", 38),
("GetIndex", 39),
("GetTupleField", 40),
("GetEnumField", 41),
("Len", 42),
("IsEnum", 43),
("IsStruct", 44),
("IntToFloat", 45),
("FloatToInt", 46),
("WordToByte", 47),
("ByteToWord", 48),
("WordToFixed", 49),
("FixedToWord", 50),
("FixedMul", 51),
("FixedDiv", 52),
("Trap", 53),
("CheckedAdd", 54),
("CheckedSub", 55),
("CheckedMul", 56),
("CheckedNeg", 57),
("CheckedDiv", 58),
("CheckedMod", 59),
("PushImmediate", 60),
("PopN", 61),
("BitAnd", 62),
("BitOr", 63),
("BitXor", 64),
("Shl", 65),
("Shr", 66),
("CallVerifiedNative", 67),
("CallExternalNative", 68),
("NewComposite", 69),
];
pub fn opcode_id_of(op: &Op) -> OpcodeId {
let id = match op {
Op::Const(_) => 0,
Op::GetLocal(_) => 1,
Op::SetLocal(_) => 2,
Op::GetData(_) => 3,
Op::SetData(_) => 4,
Op::GetDataIndexed(_, _) => 5,
Op::SetDataIndexed(_, _) => 6,
Op::BoundsCheck(_) => 7,
Op::Add => 8,
Op::Sub => 9,
Op::Mul => 10,
Op::Div => 11,
Op::Mod => 12,
Op::Neg => 13,
Op::CmpEq => 14,
Op::CmpNe => 15,
Op::CmpLt => 16,
Op::CmpGt => 17,
Op::CmpLe => 18,
Op::CmpGe => 19,
Op::Not => 20,
Op::If(_) => 21,
Op::Else(_) => 22,
Op::EndIf => 23,
Op::Loop(_) => 24,
Op::EndLoop(_) => 25,
Op::Break(_) => 26,
Op::BreakIf(_) => 27,
Op::Stream => 28,
Op::Reset => 29,
Op::Call(_, _) => 30,
Op::Return => 31,
Op::Yield => 32,
Op::Dup => 33,
Op::GetField(_) => 38,
Op::GetIndex(_) => 39,
Op::GetTupleField(_) => 40,
Op::GetEnumField(_) => 41,
Op::Len => 42,
Op::IsEnum(_, _, _) => 43,
Op::IsStruct(_) => 44,
Op::IntToFloat => 45,
Op::FloatToInt => 46,
Op::WordToByte => 47,
Op::ByteToWord => 48,
Op::WordToFixed(_) => 49,
Op::FixedToWord(_) => 50,
Op::FixedMul(_) => 51,
Op::FixedDiv(_) => 52,
Op::Trap(_) => 53,
Op::CheckedAdd => 54,
Op::CheckedSub => 55,
Op::CheckedMul(_) => 56,
Op::CheckedNeg => 57,
Op::CheckedDiv(_) => 58,
Op::CheckedMod => 59,
Op::PushImmediate(_) => 60,
Op::PopN(_) => 61,
Op::BitAnd => 62,
Op::BitOr => 63,
Op::BitXor => 64,
Op::Shl => 65,
Op::Shr => 66,
Op::CallVerifiedNative(_, _) => 67,
Op::CallExternalNative(_, _) => 68,
Op::NewComposite(_) => 69,
};
OpcodeId(id)
}
fn opcode_name_for_id(id: u8) -> Option<&'static str> {
OPCODE_ID_TABLE
.iter()
.find_map(|(name, code)| if *code == id { Some(*name) } else { None })
}
fn encode_nested(
pool: &mut Vec<OperandPoolEntry>,
a: u16,
b: u16,
variant: crate::value_layout::CompositeKind,
) -> Result<[u8; 3], WireFormatError> {
let idx = pool.len();
if idx >= MAX_POOL_ENTRIES || idx > u16::MAX as usize {
return Err(WireFormatError::OperandPoolIndexOverflow);
}
pool.push(OperandPoolEntry::from_u16_u16(a, b));
let idx_bytes = (idx as u16).to_le_bytes();
Ok([
idx_bytes[0],
idx_bytes[1],
FLAT_NESTED_SENTINEL_BASE | variant.to_tag(),
])
}
pub fn encode_op(
op: &Op,
pool: &mut Vec<OperandPoolEntry>,
) -> Result<OpcodeRecord, WireFormatError> {
let id = opcode_id_of(op);
let operand = match op {
Op::Add
| Op::Sub
| Op::Mul
| Op::Div
| Op::Mod
| Op::Neg
| Op::CmpEq
| Op::CmpNe
| Op::CmpLt
| Op::CmpGt
| Op::CmpLe
| Op::CmpGe
| Op::Not
| Op::EndIf
| Op::Stream
| Op::Reset
| Op::Return
| Op::Yield
| Op::Dup
| Op::Len
| Op::IntToFloat
| Op::FloatToInt
| Op::WordToByte
| Op::ByteToWord
| Op::CheckedAdd
| Op::CheckedSub
| Op::CheckedNeg
| Op::CheckedMod
| Op::BitAnd
| Op::BitOr
| Op::BitXor
| Op::Shl
| Op::Shr => [0, 0, 0],
Op::GetEnumField(crate::bytecode::EnumField::Flat { offset, kind }) => {
let b = offset.to_le_bytes();
[b[0], b[1], kind.to_tag()]
}
Op::GetEnumField(crate::bytecode::EnumField::FlatNested {
offset,
size,
variant,
}) => encode_nested(pool, *offset, *size, *variant)?,
Op::GetEnumField(crate::bytecode::EnumField::Boxed { index }) => {
[*index, 0, TUPLE_FIELD_BOXED_SENTINEL]
}
Op::WordToFixed(n)
| Op::FixedToWord(n)
| Op::FixedMul(n)
| Op::FixedDiv(n)
| Op::CheckedMul(n)
| Op::CheckedDiv(n)
| Op::PushImmediate(n)
| Op::PopN(n) => [*n, 0, 0],
Op::Const(v)
| Op::GetLocal(v)
| Op::SetLocal(v)
| Op::GetData(v)
| Op::SetData(v)
| Op::BoundsCheck(v)
| Op::If(v)
| Op::Else(v)
| Op::Loop(v)
| Op::EndLoop(v)
| Op::Break(v)
| Op::BreakIf(v)
| Op::IsStruct(v)
| Op::Trap(v) => {
let b = v.to_le_bytes();
[b[0], b[1], 0]
}
Op::Call(c, n) | Op::CallVerifiedNative(c, n) | Op::CallExternalNative(c, n) => {
let b = c.to_le_bytes();
[b[0], b[1], *n]
}
Op::GetTupleField(crate::bytecode::TupleField::Flat { offset, kind }) => {
let b = offset.to_le_bytes();
[b[0], b[1], kind.to_tag()]
}
Op::GetTupleField(crate::bytecode::TupleField::FlatNested {
offset,
size,
variant,
}) => encode_nested(pool, *offset, *size, *variant)?,
Op::GetTupleField(crate::bytecode::TupleField::Boxed { index }) => {
[*index, 0, TUPLE_FIELD_BOXED_SENTINEL]
}
Op::GetField(crate::bytecode::StructField::Flat { offset, kind }) => {
let b = offset.to_le_bytes();
[b[0], b[1], kind.to_tag()]
}
Op::GetField(crate::bytecode::StructField::FlatNested {
offset,
size,
variant,
}) => encode_nested(pool, *offset, *size, *variant)?,
Op::GetField(crate::bytecode::StructField::Boxed { name_const }) => {
let b = name_const.to_le_bytes();
[b[0], b[1], TUPLE_FIELD_BOXED_SENTINEL]
}
Op::GetIndex(crate::bytecode::ArrayElem::Flat { kind }) => [kind.to_tag(), 0, 0],
Op::GetIndex(crate::bytecode::ArrayElem::FlatNested { size, variant }) => {
encode_nested(pool, *size, 0, *variant)?
}
Op::GetIndex(crate::bytecode::ArrayElem::Boxed) => [TUPLE_FIELD_BOXED_SENTINEL, 0, 0],
Op::GetDataIndexed(a, b) | Op::SetDataIndexed(a, b) => {
let idx = pool.len();
if idx >= MAX_POOL_ENTRIES {
return Err(WireFormatError::OperandPoolIndexOverflow);
}
pool.push(OperandPoolEntry::from_u16_u16(*a, *b));
let idx_bytes = (idx as u32).to_le_bytes();
[idx_bytes[0], idx_bytes[1], idx_bytes[2]]
}
Op::IsEnum(a, b, d) => {
let idx = pool.len();
if idx >= MAX_POOL_ENTRIES {
return Err(WireFormatError::OperandPoolIndexOverflow);
}
pool.push(OperandPoolEntry::from_u16_u16_u16(*a, *b, *d));
let idx_bytes = (idx as u32).to_le_bytes();
[idx_bytes[0], idx_bytes[1], idx_bytes[2]]
}
Op::NewComposite(operand) => {
let (kind, count, b_field, boxed_flag) = match operand {
crate::bytecode::NewCompositeOperand::Flat {
kind,
count,
byte_size,
} => (*kind, *count, *byte_size, 0u8),
crate::bytecode::NewCompositeOperand::Boxed { kind, count, meta } => {
(*kind, *count, *meta, 1u8)
}
};
let ktag = kind.to_tag();
if boxed_flag == 0 && count <= NEW_COMPOSITE_INLINE_MAX_COUNT {
let b = b_field.to_le_bytes();
[b[0], b[1], (ktag << 6) | (count as u8)]
} else {
let idx = pool.len();
if idx >= MAX_POOL_ENTRIES || idx > u16::MAX as usize {
return Err(WireFormatError::OperandPoolIndexOverflow);
}
pool.push(OperandPoolEntry::from_u16_u16_u8(
count, b_field, boxed_flag,
));
let ib = (idx as u16).to_le_bytes();
[ib[0], ib[1], (ktag << 6) | NEW_COMPOSITE_POOL_SENTINEL]
}
}
};
Ok(OpcodeRecord::from_id_and_operand(id, operand))
}
fn decode_nested_variant(tag: u8) -> Option<crate::value_layout::CompositeKind> {
if tag & FLAT_NESTED_SENTINEL_MASK == FLAT_NESTED_SENTINEL_BASE {
crate::value_layout::CompositeKind::from_tag(tag & !FLAT_NESTED_SENTINEL_MASK)
} else {
None
}
}
pub fn decode_op(record: OpcodeRecord, pool: &[OperandPoolEntry]) -> Result<Op, WireFormatError> {
record.check_parity()?;
let id = record.opcode_id().0;
let op = match id {
0 => Op::Const(record.operand_u16()),
1 => Op::GetLocal(record.operand_u16()),
2 => Op::SetLocal(record.operand_u16()),
3 => Op::GetData(record.operand_u16()),
4 => Op::SetData(record.operand_u16()),
5 => {
let (a, b) = decode_pool_u16_u16(record, pool)?;
Op::GetDataIndexed(a, b)
}
6 => {
let (a, b) = decode_pool_u16_u16(record, pool)?;
Op::SetDataIndexed(a, b)
}
7 => Op::BoundsCheck(record.operand_u16()),
8 => Op::Add,
9 => Op::Sub,
10 => Op::Mul,
11 => Op::Div,
12 => Op::Mod,
13 => Op::Neg,
14 => Op::CmpEq,
15 => Op::CmpNe,
16 => Op::CmpLt,
17 => Op::CmpGt,
18 => Op::CmpLe,
19 => Op::CmpGe,
20 => Op::Not,
21 => Op::If(record.operand_u16()),
22 => Op::Else(record.operand_u16()),
23 => Op::EndIf,
24 => Op::Loop(record.operand_u16()),
25 => Op::EndLoop(record.operand_u16()),
26 => Op::Break(record.operand_u16()),
27 => Op::BreakIf(record.operand_u16()),
28 => Op::Stream,
29 => Op::Reset,
30 => {
let (c, n) = record.operand_u16_u8();
Op::Call(c, n)
}
31 => Op::Return,
32 => Op::Yield,
33 => Op::Dup,
38 => {
let bytes = record.operand_bytes();
if bytes[2] == TUPLE_FIELD_BOXED_SENTINEL {
let name_const = u16::from_le_bytes([bytes[0], bytes[1]]);
Op::GetField(crate::bytecode::StructField::Boxed { name_const })
} else if let Some(variant) = decode_nested_variant(bytes[2]) {
let (offset, size) = decode_nested_pool(record, pool)?;
Op::GetField(crate::bytecode::StructField::FlatNested {
offset,
size,
variant,
})
} else {
let offset = u16::from_le_bytes([bytes[0], bytes[1]]);
let kind = crate::value_layout::ScalarKind::from_tag(bytes[2])
.ok_or(WireFormatError::TupleFieldKindUnknown(bytes[2]))?;
Op::GetField(crate::bytecode::StructField::Flat { offset, kind })
}
}
39 => {
let bytes = record.operand_bytes();
if bytes[2] == 0 && bytes[0] == TUPLE_FIELD_BOXED_SENTINEL {
Op::GetIndex(crate::bytecode::ArrayElem::Boxed)
} else if let Some(variant) = decode_nested_variant(bytes[2]) {
let (size, _) = decode_nested_pool(record, pool)?;
Op::GetIndex(crate::bytecode::ArrayElem::FlatNested { size, variant })
} else {
let kind = crate::value_layout::ScalarKind::from_tag(bytes[0])
.ok_or(WireFormatError::TupleFieldKindUnknown(bytes[0]))?;
Op::GetIndex(crate::bytecode::ArrayElem::Flat { kind })
}
}
40 => {
let bytes = record.operand_bytes();
if bytes[2] == TUPLE_FIELD_BOXED_SENTINEL {
Op::GetTupleField(crate::bytecode::TupleField::Boxed { index: bytes[0] })
} else if let Some(variant) = decode_nested_variant(bytes[2]) {
let (offset, size) = decode_nested_pool(record, pool)?;
Op::GetTupleField(crate::bytecode::TupleField::FlatNested {
offset,
size,
variant,
})
} else {
let offset = u16::from_le_bytes([bytes[0], bytes[1]]);
let kind = crate::value_layout::ScalarKind::from_tag(bytes[2])
.ok_or(WireFormatError::TupleFieldKindUnknown(bytes[2]))?;
Op::GetTupleField(crate::bytecode::TupleField::Flat { offset, kind })
}
}
41 => {
let bytes = record.operand_bytes();
if bytes[2] == TUPLE_FIELD_BOXED_SENTINEL {
Op::GetEnumField(crate::bytecode::EnumField::Boxed { index: bytes[0] })
} else if let Some(variant) = decode_nested_variant(bytes[2]) {
let (offset, size) = decode_nested_pool(record, pool)?;
Op::GetEnumField(crate::bytecode::EnumField::FlatNested {
offset,
size,
variant,
})
} else {
let offset = u16::from_le_bytes([bytes[0], bytes[1]]);
let kind = crate::value_layout::ScalarKind::from_tag(bytes[2])
.ok_or(WireFormatError::TupleFieldKindUnknown(bytes[2]))?;
Op::GetEnumField(crate::bytecode::EnumField::Flat { offset, kind })
}
}
42 => Op::Len,
43 => {
let (a, b, d) = decode_pool_u16_u16_u16(record, pool)?;
Op::IsEnum(a, b, d)
}
44 => Op::IsStruct(record.operand_u16()),
45 => Op::IntToFloat,
46 => Op::FloatToInt,
47 => Op::WordToByte,
48 => Op::ByteToWord,
49 => Op::WordToFixed(record.operand_u8()),
50 => Op::FixedToWord(record.operand_u8()),
51 => Op::FixedMul(record.operand_u8()),
52 => Op::FixedDiv(record.operand_u8()),
53 => Op::Trap(record.operand_u16()),
54 => Op::CheckedAdd,
55 => Op::CheckedSub,
56 => Op::CheckedMul(record.operand_u8()),
57 => Op::CheckedNeg,
58 => Op::CheckedDiv(record.operand_u8()),
59 => Op::CheckedMod,
60 => Op::PushImmediate(record.operand_u8()),
61 => Op::PopN(record.operand_u8()),
62 => Op::BitAnd,
63 => Op::BitOr,
64 => Op::BitXor,
65 => Op::Shl,
66 => Op::Shr,
67 => {
let (c, n) = record.operand_u16_u8();
Op::CallVerifiedNative(c, n)
}
68 => {
let (c, n) = record.operand_u16_u8();
Op::CallExternalNative(c, n)
}
69 => {
let bytes = record.operand_bytes();
let kind = crate::value_layout::CompositeKind::from_tag(bytes[2] >> 6)
.ok_or(WireFormatError::TupleFieldKindUnknown(bytes[2]))?;
let low = bytes[2] & 0x3F;
if low == NEW_COMPOSITE_POOL_SENTINEL {
let idx = u16::from_le_bytes([bytes[0], bytes[1]]) as usize;
let entry = pool
.get(idx)
.copied()
.ok_or(WireFormatError::OperandPoolIndexOutOfBounds(idx))?;
entry.check_parity()?;
if entry.tag() != POOL_TAG_U16_U16_U8 {
return Err(WireFormatError::OperandPoolTagMismatch {
observed: entry.tag(),
expected: POOL_TAG_U16_U16_U8,
});
}
let (count, b_field, boxed_flag) = entry.as_u16_u16_u8();
if boxed_flag == 1 {
Op::NewComposite(crate::bytecode::NewCompositeOperand::Boxed {
kind,
count,
meta: b_field,
})
} else {
Op::NewComposite(crate::bytecode::NewCompositeOperand::Flat {
kind,
count,
byte_size: b_field,
})
}
} else {
Op::NewComposite(crate::bytecode::NewCompositeOperand::Flat {
kind,
count: low as u16,
byte_size: u16::from_le_bytes([bytes[0], bytes[1]]),
})
}
}
other => return Err(WireFormatError::UnknownOpcodeId(other)),
};
let _ = opcode_name_for_id(id);
Ok(op)
}
fn decode_pool_u16_u16(
record: OpcodeRecord,
pool: &[OperandPoolEntry],
) -> Result<(u16, u16), WireFormatError> {
let idx = record.operand_pool_index() as usize;
let entry = pool
.get(idx)
.copied()
.ok_or(WireFormatError::OperandPoolIndexOutOfBounds(idx))?;
entry.check_parity()?;
if entry.tag() != POOL_TAG_U16_U16 {
return Err(WireFormatError::OperandPoolTagMismatch {
observed: entry.tag(),
expected: POOL_TAG_U16_U16,
});
}
Ok(entry.as_u16_u16())
}
fn decode_nested_pool(
record: OpcodeRecord,
pool: &[OperandPoolEntry],
) -> Result<(u16, u16), WireFormatError> {
let bytes = record.operand_bytes();
let idx = u16::from_le_bytes([bytes[0], bytes[1]]) as usize;
let entry = pool
.get(idx)
.copied()
.ok_or(WireFormatError::OperandPoolIndexOutOfBounds(idx))?;
entry.check_parity()?;
if entry.tag() != POOL_TAG_U16_U16 {
return Err(WireFormatError::OperandPoolTagMismatch {
observed: entry.tag(),
expected: POOL_TAG_U16_U16,
});
}
Ok(entry.as_u16_u16())
}
fn decode_pool_u16_u16_u16(
record: OpcodeRecord,
pool: &[OperandPoolEntry],
) -> Result<(u16, u16, u16), WireFormatError> {
let idx = record.operand_pool_index() as usize;
let entry = pool
.get(idx)
.copied()
.ok_or(WireFormatError::OperandPoolIndexOutOfBounds(idx))?;
entry.check_parity()?;
if entry.tag() != POOL_TAG_U16_U16_U16 {
return Err(WireFormatError::OperandPoolTagMismatch {
observed: entry.tag(),
expected: POOL_TAG_U16_U16_U16,
});
}
Ok(entry.as_u16_u16_u16())
}
pub const WIRE_FORMAT_FOOTER_BYTES: usize = 4;
#[derive(Debug, Clone, Copy)]
pub struct SignatureInfo {
pub scheme_id: u8,
pub signature_offset: usize,
pub signature_length: usize,
}
pub const fn signed_header_length(signature_length: usize) -> usize {
let unpadded = WIRE_FORMAT_HEADER_BYTES + SIGNATURE_METADATA_BYTES + signature_length;
(unpadded + 7) & !7
}
pub fn parse_signature_metadata(
bytes: &[u8],
header_length: usize,
) -> Result<Option<SignatureInfo>, LoadError> {
if header_length == WIRE_FORMAT_HEADER_BYTES {
return Ok(None);
}
if header_length < WIRE_FORMAT_HEADER_BYTES + SIGNATURE_METADATA_BYTES {
return Err(LoadError::Codec(format!(
"header_length {} is less than the minimum {} required for a signature extension",
header_length,
WIRE_FORMAT_HEADER_BYTES + SIGNATURE_METADATA_BYTES,
)));
}
let scheme_id = bytes[64];
let reserved_byte = bytes[65];
let signature_length = u16::from_le_bytes([bytes[66], bytes[67]]) as usize;
let reserved_word = u32::from_le_bytes([bytes[68], bytes[69], bytes[70], bytes[71]]);
if reserved_byte != 0 || reserved_word != 0 {
return Err(LoadError::Codec(String::from(
"signature metadata reserved fields must be zero",
)));
}
if scheme_id == 0 {
return Err(LoadError::Codec(String::from(
"signature metadata scheme_id 0 is reserved; signed modules must use scheme_id >= 1",
)));
}
if scheme_id != SIGNATURE_SCHEME_ED25519 {
return Err(LoadError::Codec(format!(
"signature scheme_id {} is not supported in this build (only Ed25519 = {} is implemented in V0.2.0)",
scheme_id, SIGNATURE_SCHEME_ED25519,
)));
}
if signature_length != ED25519_SIGNATURE_BYTES {
return Err(LoadError::Codec(format!(
"Ed25519 signature_length must be {}; got {}",
ED25519_SIGNATURE_BYTES, signature_length,
)));
}
let expected_header_length = signed_header_length(signature_length);
let expected_encrypted_header_length = expected_header_length + ENCRYPTION_METADATA_BYTES;
let encrypted = bytes[15] & FLAG_ENCRYPTED != 0;
let acceptable = if encrypted {
header_length == expected_encrypted_header_length
} else {
header_length == expected_header_length
};
if !acceptable {
let expected = if encrypted {
expected_encrypted_header_length
} else {
expected_header_length
};
return Err(LoadError::Codec(format!(
"header_length {} does not match expected {} (sig metadata {} + sig {}{} + padding to 8-byte multiple)",
header_length,
expected,
SIGNATURE_METADATA_BYTES,
signature_length,
if encrypted {
" + encryption metadata"
} else {
""
},
)));
}
Ok(Some(SignatureInfo {
scheme_id,
signature_offset: WIRE_FORMAT_HEADER_BYTES + SIGNATURE_METADATA_BYTES,
signature_length,
}))
}
const WIRE_FORMAT_CRC32_RESIDUE: u32 = 0x2144DF1C;
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
pub struct WireChunk {
pub name: String,
pub constants: Vec<ConstValue>,
pub struct_templates: Vec<StructTemplate>,
pub local_count: u16,
pub param_count: u8,
pub block_type: BlockType,
pub param_types: Vec<TypeTag>,
pub op_byte_offset: u32,
pub op_record_count: u32,
pub debug_pool_bytes: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
pub struct WireAuxBody {
pub chunks: Vec<WireChunk>,
pub native_names: Vec<String>,
pub entry_point: Option<usize>,
pub data_layout: Option<DataLayout>,
pub word_bits_log2: u8,
pub addr_bits_log2: u8,
pub float_bits_log2: u8,
pub wcet_cycles: u32,
pub wcmu_bytes: u32,
pub flags: u8,
pub shared_data_bytes: u32,
pub private_data_bytes: u32,
pub schema_hash: u32,
pub enum_layouts: Vec<crate::bytecode::EnumLayout>,
pub signatures: Vec<crate::bytecode::ChunkSignature>,
pub native_return_shapes: Vec<crate::bytecode::WireShape>,
}
pub(crate) fn strip_shebang_prefix(bytes: &[u8]) -> &[u8] {
if bytes.starts_with(b"#!") {
if let Some(newline_pos) = bytes.iter().position(|&b| b == b'\n') {
&bytes[newline_pos + 1..]
} else {
bytes
}
} else {
bytes
}
}
#[derive(Debug, Clone, Copy)]
pub struct WireSections<'a> {
pub opcode_stream: &'a [u8],
pub operand_pool: &'a [u8],
pub aux_body: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HeaderFields {
pub word_bits_log2: u8,
pub addr_bits_log2: u8,
pub float_bits_log2: u8,
pub flags: u8,
pub wcet_cycles: u32,
pub wcmu_bytes: u32,
pub shared_data_bytes: u32,
pub private_data_bytes: u32,
}
pub fn parse_wire_sections(bytes: &[u8]) -> Result<WireSections<'_>, LoadError> {
let bytes = strip_shebang_prefix(bytes);
if bytes.len() < WIRE_FORMAT_HEADER_BYTES + WIRE_FORMAT_FOOTER_BYTES {
return Err(LoadError::Truncated);
}
if bytes[0..4] != BYTECODE_MAGIC {
return Err(LoadError::BadMagic);
}
let version = u16::from_le_bytes([bytes[4], bytes[5]]);
if version != BYTECODE_VERSION {
return Err(LoadError::UnsupportedVersion {
got: version,
expected: BYTECODE_VERSION,
});
}
let header_length = u16::from_le_bytes([bytes[6], bytes[7]]) as usize;
if header_length < WIRE_FORMAT_HEADER_BYTES {
return Err(LoadError::Codec(format!(
"wire format header_length {} is below the minimum {}",
header_length, WIRE_FORMAT_HEADER_BYTES
)));
}
if header_length > bytes.len() {
return Err(LoadError::Truncated);
}
let total_length = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as usize;
if total_length < header_length + WIRE_FORMAT_FOOTER_BYTES || total_length > bytes.len() {
return Err(LoadError::Truncated);
}
let bytes = &bytes[..total_length];
if crc32(bytes) != WIRE_FORMAT_CRC32_RESIDUE {
return Err(LoadError::BadChecksum);
}
let flags = bytes[15];
let signed = (flags & FLAG_REQUIRES_SIGNATURE) != 0;
let sig_info = parse_signature_metadata(bytes, header_length)?;
match (signed, sig_info) {
(true, None) => {
return Err(LoadError::Codec(String::from(
"FLAG_REQUIRES_SIGNATURE is set but the header carries no signature extension",
)));
}
(false, Some(_)) => {
return Err(LoadError::Codec(String::from(
"header carries a signature extension but FLAG_REQUIRES_SIGNATURE is not set; V0.2.0 does not admit audit-only signatures",
)));
}
_ => {}
}
let opcode_stream_offset =
u32::from_le_bytes([bytes[32], bytes[33], bytes[34], bytes[35]]) as usize;
let opcode_stream_length =
u32::from_le_bytes([bytes[36], bytes[37], bytes[38], bytes[39]]) as usize;
let operand_pool_offset =
u32::from_le_bytes([bytes[40], bytes[41], bytes[42], bytes[43]]) as usize;
let operand_pool_length =
u32::from_le_bytes([bytes[44], bytes[45], bytes[46], bytes[47]]) as usize;
let aux_body_offset = u32::from_le_bytes([bytes[48], bytes[49], bytes[50], bytes[51]]) as usize;
let aux_body_length = u32::from_le_bytes([bytes[52], bytes[53], bytes[54], bytes[55]]) as usize;
let body_end = total_length - WIRE_FORMAT_FOOTER_BYTES;
let in_body = |off: usize, len: usize| -> bool {
off >= header_length && off.checked_add(len).is_some_and(|end| end <= body_end)
};
if !in_body(opcode_stream_offset, opcode_stream_length)
|| !in_body(operand_pool_offset, operand_pool_length)
|| !in_body(aux_body_offset, aux_body_length)
{
return Err(LoadError::Truncated);
}
if !opcode_stream_length.is_multiple_of(OPCODE_RECORD_BYTES) {
return Err(LoadError::Codec(format!(
"opcode stream length {} is not a multiple of the record size {}",
opcode_stream_length, OPCODE_RECORD_BYTES,
)));
}
if !operand_pool_length.is_multiple_of(OPERAND_POOL_ENTRY_BYTES) {
return Err(LoadError::Codec(format!(
"operand pool length {} is not a multiple of the entry size {}",
operand_pool_length, OPERAND_POOL_ENTRY_BYTES,
)));
}
Ok(WireSections {
opcode_stream: &bytes[opcode_stream_offset..opcode_stream_offset + opcode_stream_length],
operand_pool: &bytes[operand_pool_offset..operand_pool_offset + operand_pool_length],
aux_body: &bytes[aux_body_offset..aux_body_offset + aux_body_length],
})
}
pub fn read_header_fields(bytes: &[u8]) -> Result<HeaderFields, LoadError> {
let _ = parse_wire_sections(bytes)?;
let bytes = strip_shebang_prefix(bytes);
Ok(HeaderFields {
word_bits_log2: bytes[12],
addr_bits_log2: bytes[13],
float_bits_log2: bytes[14],
flags: bytes[15],
wcet_cycles: u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]),
wcmu_bytes: u32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]),
shared_data_bytes: u32::from_le_bytes([bytes[24], bytes[25], bytes[26], bytes[27]]),
private_data_bytes: u32::from_le_bytes([bytes[28], bytes[29], bytes[30], bytes[31]]),
})
}
pub fn decode_op_stream(
opcode_stream: &[u8],
operand_pool_bytes: &[u8],
) -> Result<Vec<Op>, LoadError> {
if !opcode_stream.len().is_multiple_of(OPCODE_RECORD_BYTES) {
return Err(LoadError::Codec(format!(
"opcode stream length {} is not a multiple of {}",
opcode_stream.len(),
OPCODE_RECORD_BYTES,
)));
}
if !operand_pool_bytes
.len()
.is_multiple_of(OPERAND_POOL_ENTRY_BYTES)
{
return Err(LoadError::Codec(format!(
"operand pool length {} is not a multiple of {}",
operand_pool_bytes.len(),
OPERAND_POOL_ENTRY_BYTES,
)));
}
let mut pool: Vec<OperandPoolEntry> =
Vec::with_capacity(operand_pool_bytes.len() / OPERAND_POOL_ENTRY_BYTES);
for chunk_off in (0..operand_pool_bytes.len()).step_by(OPERAND_POOL_ENTRY_BYTES) {
let mut entry = [0u8; OPERAND_POOL_ENTRY_BYTES];
entry.copy_from_slice(&operand_pool_bytes[chunk_off..chunk_off + OPERAND_POOL_ENTRY_BYTES]);
let entry = OperandPoolEntry(entry);
entry
.check_parity()
.map_err(|e| LoadError::Codec(format!("operand pool entry corruption: {:?}", e)))?;
pool.push(entry);
}
let mut ops: Vec<Op> = Vec::with_capacity(opcode_stream.len() / OPCODE_RECORD_BYTES);
for off in (0..opcode_stream.len()).step_by(OPCODE_RECORD_BYTES) {
let mut rec = [0u8; OPCODE_RECORD_BYTES];
rec.copy_from_slice(&opcode_stream[off..off + OPCODE_RECORD_BYTES]);
let op = decode_op(OpcodeRecord(rec), &pool)
.map_err(|e| LoadError::Codec(format!("opcode decode failed: {:?}", e)))?;
ops.push(op);
}
Ok(ops)
}
pub fn module_to_wire_bytes(module: &Module) -> Result<Vec<u8>, LoadError> {
let mut opcode_stream: Vec<u8> = Vec::new();
let mut operand_pool: Vec<OperandPoolEntry> = Vec::new();
let mut wire_chunks: Vec<WireChunk> = Vec::with_capacity(module.chunks.len());
for chunk in &module.chunks {
let op_byte_offset = opcode_stream.len() as u32;
let op_record_count = chunk.ops.len() as u32;
for op in &chunk.ops {
let record = encode_op(op, &mut operand_pool)
.map_err(|e| LoadError::Codec(format!("opcode encode failed: {:?}", e)))?;
opcode_stream.extend_from_slice(&record.0);
}
wire_chunks.push(WireChunk {
name: chunk.name.clone(),
constants: chunk.constants.clone(),
struct_templates: chunk.struct_templates.clone(),
local_count: chunk.local_count,
param_count: chunk.param_count,
block_type: chunk.block_type,
param_types: chunk.param_types.clone(),
op_byte_offset,
op_record_count,
debug_pool_bytes: chunk.debug_pool.as_ref().map(|p| p.encode()),
});
}
let aux = WireAuxBody {
chunks: wire_chunks,
enum_layouts: module.enum_layouts.clone(),
signatures: module.signatures.clone(),
native_return_shapes: module.native_return_shapes.clone(),
native_names: module.native_names.clone(),
entry_point: module.entry_point,
data_layout: module.data_layout.clone(),
word_bits_log2: module.word_bits_log2,
addr_bits_log2: module.addr_bits_log2,
float_bits_log2: module.float_bits_log2,
wcet_cycles: module.wcet_cycles,
wcmu_bytes: module.wcmu_bytes,
flags: module.flags,
shared_data_bytes: module.shared_data_bytes,
private_data_bytes: module.private_data_bytes,
schema_hash: module.schema_hash,
};
let aux_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&aux)
.map_err(|e| LoadError::Codec(format!("aux body encode failed: {}", e)))?;
let mut operand_pool_bytes: Vec<u8> =
Vec::with_capacity(operand_pool.len() * OPERAND_POOL_ENTRY_BYTES);
for entry in &operand_pool {
operand_pool_bytes.extend_from_slice(&entry.0);
}
assemble_wire_bytes(
module,
&opcode_stream,
&operand_pool_bytes,
&aux_bytes,
None,
)
}
fn assemble_wire_bytes(
module: &Module,
opcode_stream: &[u8],
operand_pool_bytes: &[u8],
aux_bytes: &[u8],
signature: Option<u8>,
) -> Result<Vec<u8>, LoadError> {
let (header_length, effective_flags) = match signature {
None => (WIRE_FORMAT_HEADER_BYTES, module.flags),
Some(SIGNATURE_SCHEME_ED25519) => (
signed_header_length(ED25519_SIGNATURE_BYTES),
module.flags | FLAG_REQUIRES_SIGNATURE,
),
Some(other) => {
return Err(LoadError::Codec(format!(
"unsupported signature scheme_id {} (only Ed25519 = {} ships in V0.2.0)",
other, SIGNATURE_SCHEME_ED25519
)));
}
};
let opcode_stream_offset = header_length as u32;
let opcode_stream_length = opcode_stream.len() as u32;
let mut after_opcodes = opcode_stream_offset + opcode_stream_length;
let opcode_padding = ((8 - (after_opcodes as usize % 8)) % 8) as u32;
after_opcodes += opcode_padding;
let operand_pool_offset = after_opcodes;
let operand_pool_length = operand_pool_bytes.len() as u32;
let mut after_pool = operand_pool_offset + operand_pool_length;
let pool_padding = ((8 - (after_pool as usize % 8)) % 8) as u32;
after_pool += pool_padding;
let aux_body_offset = after_pool;
let aux_body_length = aux_bytes.len() as u32;
let after_aux = aux_body_offset + aux_body_length;
let total_length = after_aux + WIRE_FORMAT_FOOTER_BYTES as u32;
let mut buf: Vec<u8> = Vec::with_capacity(total_length as usize);
buf.extend_from_slice(&BYTECODE_MAGIC);
buf.extend_from_slice(&BYTECODE_VERSION.to_le_bytes());
buf.extend_from_slice(&(header_length as u16).to_le_bytes());
buf.extend_from_slice(&total_length.to_le_bytes());
buf.push(module.word_bits_log2);
buf.push(module.addr_bits_log2);
buf.push(module.float_bits_log2);
buf.push(effective_flags);
buf.extend_from_slice(&module.wcet_cycles.to_le_bytes());
buf.extend_from_slice(&module.wcmu_bytes.to_le_bytes());
buf.extend_from_slice(&module.shared_data_bytes.to_le_bytes());
buf.extend_from_slice(&module.private_data_bytes.to_le_bytes());
buf.extend_from_slice(&opcode_stream_offset.to_le_bytes());
buf.extend_from_slice(&opcode_stream_length.to_le_bytes());
buf.extend_from_slice(&operand_pool_offset.to_le_bytes());
buf.extend_from_slice(&operand_pool_length.to_le_bytes());
buf.extend_from_slice(&aux_body_offset.to_le_bytes());
buf.extend_from_slice(&aux_body_length.to_le_bytes());
buf.extend_from_slice(&module.aux_arena_bytes.to_le_bytes()); buf.extend_from_slice(&module.persistent_composite_bytes.to_le_bytes()); debug_assert_eq!(buf.len(), WIRE_FORMAT_HEADER_BYTES);
if let Some(scheme_id) = signature {
buf.push(scheme_id);
buf.push(0); let signature_length = match scheme_id {
SIGNATURE_SCHEME_ED25519 => ED25519_SIGNATURE_BYTES,
_ => unreachable!("validated above"),
};
buf.extend_from_slice(&(signature_length as u16).to_le_bytes());
buf.extend_from_slice(&[0u8; 4]); buf.resize(buf.len() + signature_length, 0);
while buf.len() < header_length {
buf.push(0);
}
debug_assert_eq!(buf.len(), header_length);
}
buf.extend_from_slice(opcode_stream);
buf.resize(buf.len() + opcode_padding as usize, 0);
buf.extend_from_slice(operand_pool_bytes);
buf.resize(buf.len() + pool_padding as usize, 0);
buf.extend_from_slice(aux_bytes);
let crc = crc32(&buf);
buf.extend_from_slice(&crc.to_le_bytes());
debug_assert_eq!(buf.len(), total_length as usize);
Ok(buf)
}
#[cfg(feature = "signatures")]
pub fn module_to_signed_wire_bytes(
module: &Module,
signing_key: &ed25519_dalek::SigningKey,
) -> Result<Vec<u8>, LoadError> {
use ed25519_dalek::Signer;
let mut opcode_stream: Vec<u8> = Vec::new();
let mut operand_pool: Vec<OperandPoolEntry> = Vec::new();
let mut wire_chunks: Vec<WireChunk> = Vec::with_capacity(module.chunks.len());
for chunk in &module.chunks {
let op_byte_offset = opcode_stream.len() as u32;
let op_record_count = chunk.ops.len() as u32;
for op in &chunk.ops {
let record = encode_op(op, &mut operand_pool)
.map_err(|e| LoadError::Codec(format!("opcode encode failed: {:?}", e)))?;
opcode_stream.extend_from_slice(&record.0);
}
wire_chunks.push(WireChunk {
name: chunk.name.clone(),
constants: chunk.constants.clone(),
struct_templates: chunk.struct_templates.clone(),
local_count: chunk.local_count,
param_count: chunk.param_count,
block_type: chunk.block_type,
param_types: chunk.param_types.clone(),
op_byte_offset,
op_record_count,
debug_pool_bytes: chunk.debug_pool.as_ref().map(|p| p.encode()),
});
}
let aux = WireAuxBody {
chunks: wire_chunks,
enum_layouts: module.enum_layouts.clone(),
signatures: module.signatures.clone(),
native_return_shapes: module.native_return_shapes.clone(),
native_names: module.native_names.clone(),
entry_point: module.entry_point,
data_layout: module.data_layout.clone(),
word_bits_log2: module.word_bits_log2,
addr_bits_log2: module.addr_bits_log2,
float_bits_log2: module.float_bits_log2,
wcet_cycles: module.wcet_cycles,
wcmu_bytes: module.wcmu_bytes,
flags: module.flags | FLAG_REQUIRES_SIGNATURE,
shared_data_bytes: module.shared_data_bytes,
private_data_bytes: module.private_data_bytes,
schema_hash: module.schema_hash,
};
let aux_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&aux)
.map_err(|e| LoadError::Codec(format!("aux body encode failed: {}", e)))?;
let mut operand_pool_bytes: Vec<u8> =
Vec::with_capacity(operand_pool.len() * OPERAND_POOL_ENTRY_BYTES);
for entry in &operand_pool {
operand_pool_bytes.extend_from_slice(&entry.0);
}
let mut buf = assemble_wire_bytes(
module,
&opcode_stream,
&operand_pool_bytes,
&aux_bytes,
Some(SIGNATURE_SCHEME_ED25519),
)?;
let total_length = buf.len();
let footer_start = total_length - WIRE_FORMAT_FOOTER_BYTES;
for byte in &mut buf[footer_start..] {
*byte = 0;
}
let signature = signing_key.sign(&buf);
let sig_bytes = signature.to_bytes();
let sig_offset = WIRE_FORMAT_HEADER_BYTES + SIGNATURE_METADATA_BYTES;
buf[sig_offset..sig_offset + ED25519_SIGNATURE_BYTES].copy_from_slice(&sig_bytes);
let crc = crc32(&buf[..footer_start]);
buf[footer_start..].copy_from_slice(&crc.to_le_bytes());
Ok(buf)
}
#[cfg(feature = "signatures")]
pub fn verify_module_signature(
bytes: &[u8],
keys: &[ed25519_dalek::VerifyingKey],
) -> Result<(), LoadError> {
let bytes = strip_shebang_prefix(bytes);
if bytes.len() < WIRE_FORMAT_HEADER_BYTES + WIRE_FORMAT_FOOTER_BYTES {
return Err(LoadError::Truncated);
}
if bytes[0..4] != BYTECODE_MAGIC {
return Err(LoadError::BadMagic);
}
let header_length = u16::from_le_bytes([bytes[6], bytes[7]]) as usize;
let total_length = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as usize;
if total_length > bytes.len() {
return Err(LoadError::Truncated);
}
let bytes = &bytes[..total_length];
let sig_info = parse_signature_metadata(bytes, header_length)?
.ok_or_else(|| LoadError::Codec(String::from("module is not signed")))?;
let mut message: Vec<u8> = bytes.to_vec();
let sig_start = sig_info.signature_offset;
let sig_end = sig_start + sig_info.signature_length;
for byte in &mut message[sig_start..sig_end] {
*byte = 0;
}
let footer_start = total_length - WIRE_FORMAT_FOOTER_BYTES;
for byte in &mut message[footer_start..] {
*byte = 0;
}
let mut signature_bytes = [0u8; ED25519_SIGNATURE_BYTES];
signature_bytes.copy_from_slice(&bytes[sig_start..sig_end]);
let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes);
for key in keys {
if key.verify_strict(&message, &signature).is_ok() {
return Ok(());
}
}
Err(LoadError::InvalidSignature)
}
pub fn header_requires_signature(bytes: &[u8]) -> bool {
let bytes = strip_shebang_prefix(bytes);
bytes.len() > 15 && (bytes[15] & FLAG_REQUIRES_SIGNATURE) != 0
}
pub fn header_requires_encryption(bytes: &[u8]) -> bool {
let bytes = strip_shebang_prefix(bytes);
bytes.len() > 15 && (bytes[15] & FLAG_ENCRYPTED) != 0
}
pub const fn encrypted_signed_header_length() -> usize {
let signed_part = signed_header_length(ED25519_SIGNATURE_BYTES);
signed_part + ENCRYPTION_METADATA_BYTES
}
#[cfg(all(feature = "signatures", feature = "encryption"))]
pub fn module_to_encrypted_signed_wire_bytes(
module: &Module,
signing_key: &ed25519_dalek::SigningKey,
recipient_public_key: &[u8; crate::encryption::X25519_PUBLIC_KEY_LEN],
ephemeral_seed: &[u8; crate::encryption::X25519_PRIVATE_KEY_LEN],
) -> Result<Vec<u8>, LoadError> {
use crate::encryption::{AES_GCM_TAG_LEN, encrypt_to_recipient};
use ed25519_dalek::Signer;
let signed_bytes = module_to_signed_wire_bytes(module, signing_key)?;
let signed_header_len = signed_header_length(ED25519_SIGNATURE_BYTES);
let plaintext_body_start = signed_header_len;
let plaintext_body_end = signed_bytes.len() - WIRE_FORMAT_FOOTER_BYTES;
let plaintext_body = &signed_bytes[plaintext_body_start..plaintext_body_end];
let (encryption_metadata, ciphertext_with_tag) =
encrypt_to_recipient(plaintext_body, recipient_public_key, ephemeral_seed)
.map_err(|e| LoadError::Codec(format!("encryption failed: {}", e)))?;
debug_assert_eq!(
ciphertext_with_tag.len(),
plaintext_body.len() + AES_GCM_TAG_LEN
);
let encrypted_header_len = encrypted_signed_header_length();
let on_disk_total = encrypted_header_len + ciphertext_with_tag.len() + WIRE_FORMAT_FOOTER_BYTES;
let mut buf: Vec<u8> = Vec::with_capacity(on_disk_total);
buf.extend_from_slice(&signed_bytes[..WIRE_FORMAT_HEADER_BYTES]);
buf[15] |= FLAG_ENCRYPTED;
buf[6..8].copy_from_slice(&(encrypted_header_len as u16).to_le_bytes());
buf[8..12].copy_from_slice(&(on_disk_total as u32).to_le_bytes());
let shift = encrypted_header_len - signed_header_len;
patch_section_offsets(&mut buf, shift);
buf.extend_from_slice(&signed_bytes[WIRE_FORMAT_HEADER_BYTES..signed_header_len]);
buf.extend_from_slice(&encryption_metadata.to_bytes());
debug_assert_eq!(buf.len(), encrypted_header_len);
buf.extend_from_slice(&ciphertext_with_tag);
buf.extend_from_slice(&[0u8; WIRE_FORMAT_FOOTER_BYTES]);
debug_assert_eq!(buf.len(), on_disk_total);
let sig_offset = WIRE_FORMAT_HEADER_BYTES + SIGNATURE_METADATA_BYTES;
for byte in &mut buf[sig_offset..sig_offset + ED25519_SIGNATURE_BYTES] {
*byte = 0;
}
let signature = signing_key.sign(&buf);
let sig_bytes = signature.to_bytes();
buf[sig_offset..sig_offset + ED25519_SIGNATURE_BYTES].copy_from_slice(&sig_bytes);
let crc_offset = on_disk_total - WIRE_FORMAT_FOOTER_BYTES;
let crc = crc32(&buf[..crc_offset]);
buf[crc_offset..].copy_from_slice(&crc.to_le_bytes());
Ok(buf)
}
#[cfg(all(feature = "signatures", feature = "encryption"))]
fn patch_section_offsets(buf: &mut [u8], shift: usize) {
let shift = shift as u32;
for offset in [32, 40, 48] {
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&buf[offset..offset + 4]);
let old = u32::from_le_bytes(bytes);
let new = old + shift;
buf[offset..offset + 4].copy_from_slice(&new.to_le_bytes());
}
}
#[cfg(all(feature = "signatures", feature = "encryption"))]
pub fn decrypt_encrypted_signed_to_signed_bytes(
bytes: &[u8],
verifying_keys: &[ed25519_dalek::VerifyingKey],
local_private_key: &[u8; crate::encryption::X25519_PRIVATE_KEY_LEN],
) -> Result<Vec<u8>, LoadError> {
use crate::encryption::{AES_GCM_TAG_LEN, EncryptionMetadata, decrypt_from_metadata};
let bytes = strip_shebang_prefix(bytes);
if bytes.len() < encrypted_signed_header_length() + WIRE_FORMAT_FOOTER_BYTES {
return Err(LoadError::Truncated);
}
if bytes[0..4] != BYTECODE_MAGIC[..] {
return Err(LoadError::BadMagic);
}
let flags = bytes[15];
if flags & FLAG_ENCRYPTED == 0 {
return Err(LoadError::Codec(String::from(
"decrypt_encrypted_signed_to_signed_bytes called on bytes without FLAG_ENCRYPTED",
)));
}
if flags & FLAG_REQUIRES_SIGNATURE == 0 {
return Err(LoadError::Codec(String::from(
"FLAG_ENCRYPTED requires FLAG_REQUIRES_SIGNATURE; encrypted modules must be signed",
)));
}
verify_module_signature(bytes, verifying_keys)?;
let encrypted_header_len = encrypted_signed_header_length();
let signed_header_len = signed_header_length(ED25519_SIGNATURE_BYTES);
let metadata_offset = signed_header_len;
let metadata_bytes = &bytes[metadata_offset..metadata_offset + ENCRYPTION_METADATA_BYTES];
let metadata = EncryptionMetadata::from_bytes(metadata_bytes).ok_or_else(|| {
LoadError::Codec(String::from(
"encryption metadata malformed or scheme unsupported",
))
})?;
let body_start = encrypted_header_len;
let body_end = bytes.len() - WIRE_FORMAT_FOOTER_BYTES;
if body_end <= body_start || body_end - body_start < AES_GCM_TAG_LEN {
return Err(LoadError::Truncated);
}
let ciphertext_with_tag = &bytes[body_start..body_end];
let plaintext = decrypt_from_metadata(&metadata, ciphertext_with_tag, local_private_key)
.map_err(|e| LoadError::Codec(format!("decryption failed: {}", e)))?;
let reconstructed_total = signed_header_len + plaintext.len() + WIRE_FORMAT_FOOTER_BYTES;
let mut buf: Vec<u8> = Vec::with_capacity(reconstructed_total);
buf.extend_from_slice(&bytes[..WIRE_FORMAT_HEADER_BYTES]);
buf[15] &= !FLAG_ENCRYPTED;
buf[6..8].copy_from_slice(&(signed_header_len as u16).to_le_bytes());
buf[8..12].copy_from_slice(&(reconstructed_total as u32).to_le_bytes());
let shift = encrypted_header_len - signed_header_len;
patch_section_offsets_subtract(&mut buf, shift);
buf.extend_from_slice(&bytes[WIRE_FORMAT_HEADER_BYTES..signed_header_len]);
buf.extend_from_slice(&plaintext);
let crc_offset = reconstructed_total - WIRE_FORMAT_FOOTER_BYTES;
let crc = crc32(&buf[..crc_offset]);
buf.extend_from_slice(&crc.to_le_bytes());
debug_assert_eq!(buf.len(), reconstructed_total);
Ok(buf)
}
#[cfg(all(feature = "signatures", feature = "encryption"))]
fn patch_section_offsets_subtract(buf: &mut [u8], shift: usize) {
let shift = shift as u32;
for offset in [32, 40, 48] {
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&buf[offset..offset + 4]);
let old = u32::from_le_bytes(bytes);
let new = old.saturating_sub(shift);
buf[offset..offset + 4].copy_from_slice(&new.to_le_bytes());
}
}
pub fn module_from_wire_bytes(bytes: &[u8]) -> Result<Module, LoadError> {
let bytes = strip_shebang_prefix(bytes);
if bytes.len() < WIRE_FORMAT_HEADER_BYTES + WIRE_FORMAT_FOOTER_BYTES {
return Err(LoadError::Truncated);
}
if bytes[0..4] != BYTECODE_MAGIC {
return Err(LoadError::BadMagic);
}
let version = u16::from_le_bytes([bytes[4], bytes[5]]);
if version != BYTECODE_VERSION {
return Err(LoadError::UnsupportedVersion {
got: version,
expected: BYTECODE_VERSION,
});
}
let header_length = u16::from_le_bytes([bytes[6], bytes[7]]) as usize;
if header_length < WIRE_FORMAT_HEADER_BYTES {
return Err(LoadError::Codec(format!(
"wire format header_length {} is below the minimum {}",
header_length, WIRE_FORMAT_HEADER_BYTES
)));
}
if header_length > bytes.len() {
return Err(LoadError::Truncated);
}
let total_length = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as usize;
if total_length < header_length + WIRE_FORMAT_FOOTER_BYTES || total_length > bytes.len() {
return Err(LoadError::Truncated);
}
let bytes = &bytes[..total_length];
if crc32(bytes) != WIRE_FORMAT_CRC32_RESIDUE {
return Err(LoadError::BadChecksum);
}
let word_bits_log2 = bytes[12];
let addr_bits_log2 = bytes[13];
let float_bits_log2 = bytes[14];
let flags = bytes[15];
let wcet_cycles = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
let wcmu_bytes = u32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
let shared_data_bytes = u32::from_le_bytes([bytes[24], bytes[25], bytes[26], bytes[27]]);
let private_data_bytes = u32::from_le_bytes([bytes[28], bytes[29], bytes[30], bytes[31]]);
let opcode_stream_offset =
u32::from_le_bytes([bytes[32], bytes[33], bytes[34], bytes[35]]) as usize;
let opcode_stream_length =
u32::from_le_bytes([bytes[36], bytes[37], bytes[38], bytes[39]]) as usize;
let operand_pool_offset =
u32::from_le_bytes([bytes[40], bytes[41], bytes[42], bytes[43]]) as usize;
let operand_pool_length =
u32::from_le_bytes([bytes[44], bytes[45], bytes[46], bytes[47]]) as usize;
let aux_body_offset = u32::from_le_bytes([bytes[48], bytes[49], bytes[50], bytes[51]]) as usize;
let aux_body_length = u32::from_le_bytes([bytes[52], bytes[53], bytes[54], bytes[55]]) as usize;
let aux_arena_bytes = u32::from_le_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]);
let persistent_composite_bytes =
u32::from_le_bytes([bytes[60], bytes[61], bytes[62], bytes[63]]);
let signed = (flags & FLAG_REQUIRES_SIGNATURE) != 0;
let sig_info = parse_signature_metadata(bytes, header_length)?;
match (signed, sig_info) {
(true, None) => {
return Err(LoadError::Codec(String::from(
"FLAG_REQUIRES_SIGNATURE is set but the header carries no signature extension",
)));
}
(false, Some(_)) => {
return Err(LoadError::Codec(String::from(
"header carries a signature extension but FLAG_REQUIRES_SIGNATURE is not set; V0.2.0 does not admit audit-only signatures",
)));
}
_ => {}
}
let body_end = total_length - WIRE_FORMAT_FOOTER_BYTES;
let in_body = |off: usize, len: usize| -> bool {
off >= header_length && off.checked_add(len).is_some_and(|end| end <= body_end)
};
if !in_body(opcode_stream_offset, opcode_stream_length)
|| !in_body(operand_pool_offset, operand_pool_length)
|| !in_body(aux_body_offset, aux_body_length)
{
return Err(LoadError::Truncated);
}
if !opcode_stream_length.is_multiple_of(OPCODE_RECORD_BYTES) {
return Err(LoadError::Codec(format!(
"opcode stream length {} is not a multiple of the record size {}",
opcode_stream_length, OPCODE_RECORD_BYTES,
)));
}
if !operand_pool_length.is_multiple_of(OPERAND_POOL_ENTRY_BYTES) {
return Err(LoadError::Codec(format!(
"operand pool length {} is not a multiple of the entry size {}",
operand_pool_length, OPERAND_POOL_ENTRY_BYTES,
)));
}
let opcode_stream = &bytes[opcode_stream_offset..opcode_stream_offset + opcode_stream_length];
let operand_pool_bytes = &bytes[operand_pool_offset..operand_pool_offset + operand_pool_length];
let aux_body_bytes = &bytes[aux_body_offset..aux_body_offset + aux_body_length];
let mut operand_pool: Vec<OperandPoolEntry> = Vec::with_capacity(operand_pool_bytes.len() / 8);
for chunk_offset in (0..operand_pool_bytes.len()).step_by(OPERAND_POOL_ENTRY_BYTES) {
let mut entry_bytes = [0u8; OPERAND_POOL_ENTRY_BYTES];
entry_bytes.copy_from_slice(
&operand_pool_bytes[chunk_offset..chunk_offset + OPERAND_POOL_ENTRY_BYTES],
);
let entry = OperandPoolEntry(entry_bytes);
entry
.check_parity()
.map_err(|e| LoadError::Codec(format!("operand pool entry corruption: {:?}", e)))?;
operand_pool.push(entry);
}
if word_bits_log2 > crate::bytecode::RUNTIME_WORD_BITS_LOG2 {
return Err(LoadError::WordSizeMismatch {
got: word_bits_log2,
max_supported: crate::bytecode::RUNTIME_WORD_BITS_LOG2,
});
}
if addr_bits_log2 > crate::bytecode::RUNTIME_ADDRESS_BITS_LOG2 {
return Err(LoadError::AddressSizeMismatch {
got: addr_bits_log2,
max_supported: crate::bytecode::RUNTIME_ADDRESS_BITS_LOG2,
});
}
if float_bits_log2 > crate::bytecode::RUNTIME_FLOAT_BITS_LOG2 {
return Err(LoadError::FloatSizeMismatch {
got: float_bits_log2,
max_supported: crate::bytecode::RUNTIME_FLOAT_BITS_LOG2,
});
}
let mut aligned: rkyv::util::AlignedVec<8> =
rkyv::util::AlignedVec::with_capacity(aux_body_bytes.len());
aligned.extend_from_slice(aux_body_bytes);
let aux = rkyv::from_bytes::<WireAuxBody, rkyv::rancor::Error>(&aligned)
.map_err(|e| LoadError::Codec(format!("aux body decode failed: {}", e)))?;
if aux.word_bits_log2 != word_bits_log2
|| aux.addr_bits_log2 != addr_bits_log2
|| aux.float_bits_log2 != float_bits_log2
|| aux.flags != flags
|| aux.wcet_cycles != wcet_cycles
|| aux.wcmu_bytes != wcmu_bytes
|| aux.shared_data_bytes != shared_data_bytes
|| aux.private_data_bytes != private_data_bytes
{
return Err(LoadError::Codec(String::from(
"wire-format header fields disagree with the auxiliary body",
)));
}
let mut chunks: Vec<Chunk> = Vec::with_capacity(aux.chunks.len());
for wc in &aux.chunks {
let start = wc.op_byte_offset as usize;
let record_count = wc.op_record_count as usize;
let byte_span = record_count
.checked_mul(OPCODE_RECORD_BYTES)
.ok_or_else(|| LoadError::Codec(String::from("opcode span overflow")))?;
let end = start
.checked_add(byte_span)
.ok_or_else(|| LoadError::Codec(String::from("opcode span overflow")))?;
if end > opcode_stream.len() {
return Err(LoadError::Codec(format!(
"chunk `{}` opcode span [{}..{}) exceeds opcode stream length {}",
wc.name,
start,
end,
opcode_stream.len(),
)));
}
let mut ops: Vec<Op> = Vec::with_capacity(record_count);
let chunk_bytes = &opcode_stream[start..end];
for offset in (0..byte_span).step_by(OPCODE_RECORD_BYTES) {
let mut rec = [0u8; OPCODE_RECORD_BYTES];
rec.copy_from_slice(&chunk_bytes[offset..offset + OPCODE_RECORD_BYTES]);
let op = decode_op(OpcodeRecord(rec), &operand_pool)
.map_err(|e| LoadError::Codec(format!("opcode decode failed: {:?}", e)))?;
ops.push(op);
}
let debug_pool = match &wc.debug_pool_bytes {
Some(bytes) => Some(
crate::debug_meta::DebugPool::decode(bytes)
.map_err(|e| LoadError::Codec(format!("debug pool decode failed: {:?}", e)))?,
),
None => None,
};
chunks.push(Chunk {
name: wc.name.clone(),
ops,
constants: wc.constants.clone(),
struct_templates: wc.struct_templates.clone(),
local_count: wc.local_count,
param_count: wc.param_count,
block_type: wc.block_type,
param_types: wc.param_types.clone(),
debug_pool,
});
}
Ok(Module {
chunks,
enum_layouts: aux.enum_layouts,
signatures: aux.signatures,
native_return_shapes: aux.native_return_shapes,
native_names: aux.native_names,
entry_point: aux.entry_point,
data_layout: aux.data_layout,
word_bits_log2: aux.word_bits_log2,
addr_bits_log2: aux.addr_bits_log2,
float_bits_log2: aux.float_bits_log2,
wcet_cycles: aux.wcet_cycles,
wcmu_bytes: aux.wcmu_bytes,
aux_arena_bytes,
persistent_composite_bytes,
flags: aux.flags,
shared_data_bytes: aux.shared_data_bytes,
private_data_bytes: aux.private_data_bytes,
schema_hash: aux.schema_hash,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(op: Op) {
let mut pool: Vec<OperandPoolEntry> = Vec::new();
let record = encode_op(&op, &mut pool).expect("encode");
record.check_parity().expect("parity");
let decoded = decode_op(record, &pool).expect("decode");
assert_eq!(op, decoded);
}
#[test]
fn opcode_id_table_is_dense_and_unique() {
let mut seen: alloc::collections::BTreeSet<u8> = alloc::collections::BTreeSet::new();
for (_, id) in OPCODE_ID_TABLE.iter() {
assert!(*id < 128, "opcode id {} does not fit in seven bits", id);
assert!(seen.insert(*id), "duplicate opcode id {}", id);
}
assert_eq!(seen.len(), OPCODE_ID_TABLE.len());
}
#[test]
fn opcode_id_of_matches_table() {
let cases: &[(Op, u8)] = &[
(Op::Const(0), 0),
(Op::GetLocal(0), 1),
(Op::SetLocal(0), 2),
(Op::GetData(0), 3),
(Op::SetData(0), 4),
(Op::GetDataIndexed(0, 0), 5),
(Op::SetDataIndexed(0, 0), 6),
(Op::BoundsCheck(0), 7),
(Op::Add, 8),
(Op::Sub, 9),
(Op::Mul, 10),
(Op::Div, 11),
(Op::Mod, 12),
(Op::Neg, 13),
(Op::CmpEq, 14),
(Op::CmpNe, 15),
(Op::CmpLt, 16),
(Op::CmpGt, 17),
(Op::CmpLe, 18),
(Op::CmpGe, 19),
(Op::Not, 20),
(Op::If(0), 21),
(Op::Else(0), 22),
(Op::EndIf, 23),
(Op::Loop(0), 24),
(Op::EndLoop(0), 25),
(Op::Break(0), 26),
(Op::BreakIf(0), 27),
(Op::Stream, 28),
(Op::Reset, 29),
(Op::Call(0, 0), 30),
(Op::Return, 31),
(Op::Yield, 32),
(Op::Dup, 33),
(
Op::GetField(crate::bytecode::StructField::Boxed { name_const: 0 }),
38,
),
(Op::GetIndex(crate::bytecode::ArrayElem::Boxed), 39),
(
Op::GetTupleField(crate::bytecode::TupleField::Boxed { index: 0 }),
40,
),
(
Op::GetEnumField(crate::bytecode::EnumField::Boxed { index: 0 }),
41,
),
(Op::Len, 42),
(Op::IsEnum(0, 0, 0), 43),
(Op::IsStruct(0), 44),
(Op::IntToFloat, 45),
(Op::FloatToInt, 46),
(Op::WordToByte, 47),
(Op::ByteToWord, 48),
(Op::WordToFixed(0), 49),
(Op::FixedToWord(0), 50),
(Op::FixedMul(0), 51),
(Op::FixedDiv(0), 52),
(Op::Trap(0), 53),
(Op::CheckedAdd, 54),
(Op::CheckedSub, 55),
(Op::CheckedMul(0), 56),
(Op::CheckedNeg, 57),
(Op::CheckedDiv(0), 58),
(Op::CheckedMod, 59),
(Op::PushImmediate(0), 60),
(Op::PopN(0), 61),
(Op::BitAnd, 62),
(Op::BitOr, 63),
(Op::BitXor, 64),
(Op::Shl, 65),
(Op::Shr, 66),
(Op::CallVerifiedNative(0, 0), 67),
(Op::CallExternalNative(0, 0), 68),
(
Op::NewComposite(crate::bytecode::NewCompositeOperand::Flat {
kind: crate::value_layout::CompositeKind::Tuple,
count: 0,
byte_size: 0,
}),
69,
),
];
for (op, expected) in cases {
assert_eq!(
opcode_id_of(op).0,
*expected,
"wrong opcode id for {:?}",
op,
);
}
assert_eq!(cases.len(), OPCODE_ID_TABLE.len());
}
#[test]
fn new_composite_roundtrips_all_forms() {
use crate::bytecode::NewCompositeOperand as NCO;
use crate::value_layout::CompositeKind as CK;
let cases = [
NCO::Flat {
kind: CK::Struct,
count: 3,
byte_size: 24,
},
NCO::Flat {
kind: CK::Enum,
count: 62,
byte_size: 65535,
},
NCO::Flat {
kind: CK::Array,
count: 1000,
byte_size: 8000,
},
NCO::Boxed {
kind: CK::Struct,
count: 2,
meta: 7,
},
NCO::Boxed {
kind: CK::Tuple,
count: 4,
meta: 0,
},
];
for operand in cases {
let mut pool = Vec::new();
let record = encode_op(&Op::NewComposite(operand), &mut pool).unwrap();
let decoded = decode_op(record, &pool).unwrap();
assert_eq!(decoded, Op::NewComposite(operand), "round-trip mismatch");
}
}
#[test]
fn opcode_record_roundtrip_no_operand() {
for op in [
Op::Add,
Op::Sub,
Op::Mul,
Op::Div,
Op::Mod,
Op::Neg,
Op::CmpEq,
Op::CmpNe,
Op::CmpLt,
Op::CmpGt,
Op::CmpLe,
Op::CmpGe,
Op::Not,
Op::EndIf,
Op::Stream,
Op::Reset,
Op::Return,
Op::Yield,
Op::Dup,
Op::Len,
Op::IntToFloat,
Op::FloatToInt,
Op::WordToByte,
Op::ByteToWord,
Op::CheckedAdd,
Op::CheckedSub,
Op::CheckedNeg,
Op::CheckedMod,
Op::BitAnd,
Op::BitOr,
Op::BitXor,
Op::Shl,
Op::Shr,
] {
roundtrip(op);
}
}
#[test]
fn opcode_record_roundtrip_u8_operand() {
for op in [
Op::GetEnumField(crate::bytecode::EnumField::Boxed { index: 3 }),
Op::WordToFixed(32),
Op::FixedToWord(16),
Op::FixedMul(8),
Op::FixedDiv(4),
Op::CheckedMul(0),
Op::CheckedMul(8),
Op::CheckedDiv(0),
Op::CheckedDiv(4),
Op::PushImmediate(5),
Op::PopN(2),
] {
roundtrip(op);
}
}
#[test]
fn opcode_record_roundtrip_tuple_field() {
use crate::bytecode::TupleField;
use crate::value_layout::ScalarKind;
for op in [
Op::GetTupleField(TupleField::Boxed { index: 0 }),
Op::GetTupleField(TupleField::Boxed { index: 255 }),
Op::GetTupleField(TupleField::Flat {
offset: 0,
kind: ScalarKind::Bool,
}),
Op::GetTupleField(TupleField::Flat {
offset: 9,
kind: ScalarKind::Int,
}),
Op::GetTupleField(TupleField::Flat {
offset: 65535,
kind: ScalarKind::Byte,
}),
Op::GetTupleField(TupleField::Flat {
offset: 16,
kind: ScalarKind::Fixed,
}),
Op::GetIndex(crate::bytecode::ArrayElem::Boxed),
Op::GetIndex(crate::bytecode::ArrayElem::Flat {
kind: ScalarKind::Int,
}),
Op::GetIndex(crate::bytecode::ArrayElem::Flat {
kind: ScalarKind::Byte,
}),
] {
roundtrip(op);
}
}
#[test]
fn tuple_field_unknown_kind_tag_rejected() {
let record = OpcodeRecord::from_id_and_operand(OpcodeId(40), [0, 0, 9]);
let result = decode_op(record, &[]);
assert_eq!(result, Err(WireFormatError::TupleFieldKindUnknown(9)));
}
#[test]
fn opcode_record_roundtrip_u16_operand() {
for op in [
Op::Const(0),
Op::Const(65535),
Op::GetLocal(12),
Op::SetLocal(34),
Op::GetData(56),
Op::SetData(78),
Op::BoundsCheck(100),
Op::If(200),
Op::Else(300),
Op::Loop(400),
Op::EndLoop(500),
Op::Break(600),
Op::BreakIf(700),
Op::GetField(crate::bytecode::StructField::Boxed { name_const: 1000 }),
Op::IsStruct(1100),
Op::Trap(1200),
] {
roundtrip(op);
}
}
#[test]
fn opcode_record_roundtrip_u16_u8_operand() {
for op in [
Op::Call(0, 0),
Op::Call(65535, 255),
Op::CallVerifiedNative(42, 3),
Op::CallExternalNative(43, 1),
] {
roundtrip(op);
}
}
#[test]
fn opcode_record_roundtrip_pool_u16_u16() {
for op in [
Op::GetDataIndexed(0, 0),
Op::GetDataIndexed(65535, 65535),
Op::SetDataIndexed(100, 200),
Op::IsEnum(7, 13, 21),
] {
roundtrip(op);
}
}
#[test]
fn parity_detects_bit_flip_in_opcode_record() {
let mut pool: Vec<OperandPoolEntry> = Vec::new();
let record = encode_op(&Op::Const(42), &mut pool).expect("encode");
let mut corrupted = record.0;
corrupted[1] ^= 0x01;
let result = OpcodeRecord(corrupted).check_parity();
assert_eq!(result, Err(WireFormatError::OpcodeRecordParityMismatch));
}
#[test]
fn parity_detects_bit_flip_in_opcode_id() {
let mut pool: Vec<OperandPoolEntry> = Vec::new();
let record = encode_op(&Op::Add, &mut pool).expect("encode");
let mut corrupted = record.0;
corrupted[0] ^= 0x01;
let result = OpcodeRecord(corrupted).check_parity();
assert_eq!(result, Err(WireFormatError::OpcodeRecordParityMismatch));
}
#[test]
fn parity_detects_bit_flip_in_pool_entry() {
let entry = OperandPoolEntry::from_u16_u16(1234, 5678);
let mut corrupted = entry.0;
corrupted[3] ^= 0x80;
let result = OperandPoolEntry(corrupted).check_parity();
assert_eq!(result, Err(WireFormatError::OperandPoolParityMismatch));
}
#[test]
fn pool_tag_mismatch_surfaces_error() {
let mut pool: Vec<OperandPoolEntry> = Vec::new();
let _record = encode_op(&Op::GetDataIndexed(1, 2), &mut pool).expect("encode");
let id = opcode_id_of(&Op::NewComposite(
crate::bytecode::NewCompositeOperand::Boxed {
kind: crate::value_layout::CompositeKind::Struct,
count: 0,
meta: 0,
},
));
let byte2 = (crate::value_layout::CompositeKind::Struct.to_tag() << 6)
| NEW_COMPOSITE_POOL_SENTINEL;
let bad_record = OpcodeRecord::from_id_and_operand(id, [0, 0, byte2]);
let result = decode_op(bad_record, &pool);
assert_eq!(
result,
Err(WireFormatError::OperandPoolTagMismatch {
observed: POOL_TAG_U16_U16,
expected: POOL_TAG_U16_U16_U8,
}),
);
}
#[test]
fn pool_index_out_of_bounds_surfaces_error() {
let pool: Vec<OperandPoolEntry> = Vec::new();
let id = opcode_id_of(&Op::GetDataIndexed(0, 0));
let idx_bytes = (5u32).to_le_bytes();
let record =
OpcodeRecord::from_id_and_operand(id, [idx_bytes[0], idx_bytes[1], idx_bytes[2]]);
let result = decode_op(record, &pool);
assert_eq!(result, Err(WireFormatError::OperandPoolIndexOutOfBounds(5)));
}
#[test]
fn unknown_opcode_id_surfaces_error() {
let record = OpcodeRecord::from_id_and_operand(OpcodeId(100), [0, 0, 0]);
let pool: Vec<OperandPoolEntry> = Vec::new();
let result = decode_op(record, &pool);
assert_eq!(result, Err(WireFormatError::UnknownOpcodeId(100)));
}
fn module_roundtrip_through_wire_format(module: Module) {
let bytes = module_to_wire_bytes(&module).expect("encode");
assert_eq!(&bytes[0..4], &BYTECODE_MAGIC);
assert_eq!(u16::from_le_bytes([bytes[4], bytes[5]]), BYTECODE_VERSION,);
assert_eq!(
u16::from_le_bytes([bytes[6], bytes[7]]),
WIRE_FORMAT_HEADER_BYTES as u16,
);
let decoded = module_from_wire_bytes(&bytes).expect("decode");
let re_encoded = module_to_wire_bytes(&decoded).expect("re-encode");
assert_eq!(bytes, re_encoded, "wire-format round trip differs");
}
#[test]
fn signature_table_survives_wire_round_trip() {
use crate::bytecode::{ChunkSignature, WireShape};
let mut module = make_minimal_module();
module.signatures = alloc::vec![ChunkSignature {
params: alloc::vec![WireShape::Flat { kind: 2, size: 16 }],
ret: WireShape::Scalar { kind: 3 },
resume: WireShape::Top,
}];
module.native_return_shapes =
alloc::vec![WireShape::Flat { kind: 2, size: 24 }, WireShape::Top];
module_roundtrip_through_wire_format(module.clone());
let bytes = module_to_wire_bytes(&module).expect("encode");
let decoded = module_from_wire_bytes(&bytes).expect("decode");
assert_eq!(decoded.signatures.len(), 1);
let s = &decoded.signatures[0];
assert_eq!(s.params.len(), 1);
assert!(matches!(s.params[0], WireShape::Flat { kind: 2, size: 16 }));
assert!(matches!(s.ret, WireShape::Scalar { kind: 3 }));
assert!(matches!(s.resume, WireShape::Top));
assert_eq!(decoded.native_return_shapes.len(), 2);
assert!(matches!(
decoded.native_return_shapes[0],
WireShape::Flat { kind: 2, size: 24 }
));
assert!(matches!(decoded.native_return_shapes[1], WireShape::Top));
}
fn make_minimal_module() -> Module {
let chunk = Chunk {
name: alloc::string::String::from("main"),
ops: alloc::vec![Op::PushImmediate(5), Op::Return],
constants: alloc::vec::Vec::new(),
struct_templates: alloc::vec::Vec::new(),
local_count: 0,
param_count: 0,
block_type: BlockType::Func,
param_types: alloc::vec::Vec::new(),
debug_pool: None,
};
Module {
chunks: alloc::vec![chunk],
native_names: alloc::vec::Vec::new(),
enum_layouts: alloc::vec::Vec::new(),
signatures: alloc::vec::Vec::new(),
native_return_shapes: alloc::vec::Vec::new(),
entry_point: Some(0),
data_layout: None,
word_bits_log2: crate::bytecode::RUNTIME_WORD_BITS_LOG2,
addr_bits_log2: crate::bytecode::RUNTIME_ADDRESS_BITS_LOG2,
float_bits_log2: crate::bytecode::RUNTIME_FLOAT_BITS_LOG2,
wcet_cycles: 0,
wcmu_bytes: 0,
aux_arena_bytes: 0,
persistent_composite_bytes: 0,
flags: 0,
shared_data_bytes: 0,
private_data_bytes: 0,
schema_hash: 0,
}
}
#[test]
fn module_roundtrip_preserves_aux_arena_bytes() {
let mut module = make_minimal_module();
module.aux_arena_bytes = 4096;
let bytes = module_to_wire_bytes(&module).expect("encode");
assert_eq!(
u32::from_le_bytes([bytes[56], bytes[57], bytes[58], bytes[59]]),
4096,
"aux_arena_bytes must occupy header offset 56"
);
let decoded = module_from_wire_bytes(&bytes).expect("decode");
assert_eq!(decoded.aux_arena_bytes, 4096);
let zero = make_minimal_module();
let zbytes = module_to_wire_bytes(&zero).expect("encode");
assert_eq!(&zbytes[56..64], &[0u8; 8], "zero aux + reserved stays zero");
module_roundtrip_through_wire_format(module);
}
#[test]
fn module_roundtrip_empty_chunks() {
let module = Module {
chunks: alloc::vec::Vec::new(),
native_names: alloc::vec::Vec::new(),
enum_layouts: alloc::vec::Vec::new(),
signatures: alloc::vec::Vec::new(),
native_return_shapes: alloc::vec::Vec::new(),
entry_point: None,
data_layout: None,
word_bits_log2: crate::bytecode::RUNTIME_WORD_BITS_LOG2,
addr_bits_log2: crate::bytecode::RUNTIME_ADDRESS_BITS_LOG2,
float_bits_log2: crate::bytecode::RUNTIME_FLOAT_BITS_LOG2,
wcet_cycles: 0,
wcmu_bytes: 0,
aux_arena_bytes: 0,
persistent_composite_bytes: 0,
flags: 0,
shared_data_bytes: 0,
private_data_bytes: 0,
schema_hash: 0,
};
module_roundtrip_through_wire_format(module);
}
#[test]
fn module_roundtrip_minimal_program() {
module_roundtrip_through_wire_format(make_minimal_module());
}
fn sample_debug_pool() -> crate::debug_meta::DebugPool {
use crate::debug_meta::{DebugPool, DebugRecord, DebugRecordKind};
DebugPool {
string_pool: alloc::vec![alloc::string::String::from("main.kel")],
span_pool: alloc::vec![(0, 0, 2)],
type_pool: alloc::vec::Vec::new(),
records: alloc::vec![
DebugRecord {
op_index: 0,
kind: DebugRecordKind::SourceSpan,
operands: alloc::vec![0],
},
DebugRecord {
op_index: 1,
kind: DebugRecordKind::CallSite,
operands: alloc::vec![0],
},
],
}
}
#[test]
fn module_roundtrip_preserves_debug_pool() {
let mut module = make_minimal_module();
let pool = sample_debug_pool();
module.chunks[0].debug_pool = Some(pool.clone());
let bytes = module_to_wire_bytes(&module).expect("encode");
let decoded = module_from_wire_bytes(&bytes).expect("decode");
let decoded_pool = decoded.chunks[0]
.debug_pool
.as_ref()
.expect("debug pool survives round trip");
assert_eq!(decoded_pool.encode(), pool.encode());
let re = module_to_wire_bytes(&decoded).expect("re-encode");
assert_eq!(bytes, re);
}
#[test]
fn debug_pool_does_not_alter_opcode_stream() {
let without = make_minimal_module();
let mut with = make_minimal_module();
with.chunks[0].debug_pool = Some(sample_debug_pool());
let bytes_without = module_to_wire_bytes(&without).expect("encode");
let bytes_with = module_to_wire_bytes(&with).expect("encode");
let s_without = parse_wire_sections(&bytes_without).expect("sections");
let s_with = parse_wire_sections(&bytes_with).expect("sections");
assert_eq!(
s_without.opcode_stream, s_with.opcode_stream,
"debug metadata must not change the opcode stream"
);
}
#[test]
fn stripping_debug_pool_reproduces_release_bytes() {
let release = make_minimal_module();
let release_bytes = module_to_wire_bytes(&release).expect("encode");
let mut debug = make_minimal_module();
debug.chunks[0].debug_pool = Some(sample_debug_pool());
debug.chunks[0].debug_pool = None;
let stripped_bytes = module_to_wire_bytes(&debug).expect("encode");
assert_eq!(
release_bytes, stripped_bytes,
"stripped bytecode must be byte-identical to a release build"
);
}
#[test]
fn module_roundtrip_branchy_program() {
let body_chunk = Chunk {
name: alloc::string::String::from("loop_body"),
ops: alloc::vec![
Op::Loop(7),
Op::PushImmediate(2),
Op::PushImmediate(2),
Op::CmpEq,
Op::BreakIf(7),
Op::EndLoop(1),
Op::PushImmediate(0),
Op::Return,
],
constants: alloc::vec::Vec::new(),
struct_templates: alloc::vec::Vec::new(),
local_count: 0,
param_count: 0,
block_type: BlockType::Func,
param_types: alloc::vec::Vec::new(),
debug_pool: None,
};
let if_chunk = Chunk {
name: alloc::string::String::from("if_chain"),
ops: alloc::vec![
Op::PushImmediate(1),
Op::If(4),
Op::PushImmediate(5),
Op::Else(5),
Op::PushImmediate(6),
Op::EndIf,
Op::Return,
],
constants: alloc::vec::Vec::new(),
struct_templates: alloc::vec::Vec::new(),
local_count: 0,
param_count: 0,
block_type: BlockType::Func,
param_types: alloc::vec::Vec::new(),
debug_pool: None,
};
let module = Module {
chunks: alloc::vec![body_chunk, if_chunk],
native_names: alloc::vec::Vec::new(),
enum_layouts: alloc::vec::Vec::new(),
signatures: alloc::vec::Vec::new(),
native_return_shapes: alloc::vec::Vec::new(),
entry_point: Some(0),
data_layout: None,
word_bits_log2: crate::bytecode::RUNTIME_WORD_BITS_LOG2,
addr_bits_log2: crate::bytecode::RUNTIME_ADDRESS_BITS_LOG2,
float_bits_log2: crate::bytecode::RUNTIME_FLOAT_BITS_LOG2,
wcet_cycles: 0,
wcmu_bytes: 0,
aux_arena_bytes: 0,
persistent_composite_bytes: 0,
flags: 0,
shared_data_bytes: 0,
private_data_bytes: 0,
schema_hash: 0,
};
module_roundtrip_through_wire_format(module);
}
#[test]
fn module_roundtrip_pool_using_program() {
let chunk = Chunk {
name: alloc::string::String::from("pool_user"),
ops: alloc::vec![
Op::NewComposite(crate::bytecode::NewCompositeOperand::Boxed {
kind: crate::value_layout::CompositeKind::Enum,
count: 1,
meta: 4,
}),
Op::NewComposite(crate::bytecode::NewCompositeOperand::Boxed {
kind: crate::value_layout::CompositeKind::Struct,
count: 0,
meta: 0,
}),
Op::IsEnum(3, 4, 5),
Op::GetDataIndexed(7, 8),
Op::SetDataIndexed(7, 8),
Op::Return,
],
constants: alloc::vec::Vec::new(),
struct_templates: alloc::vec::Vec::new(),
local_count: 0,
param_count: 0,
block_type: BlockType::Func,
param_types: alloc::vec::Vec::new(),
debug_pool: None,
};
let module = Module {
chunks: alloc::vec![chunk],
native_names: alloc::vec::Vec::new(),
enum_layouts: alloc::vec::Vec::new(),
signatures: alloc::vec::Vec::new(),
native_return_shapes: alloc::vec::Vec::new(),
entry_point: Some(0),
data_layout: None,
word_bits_log2: crate::bytecode::RUNTIME_WORD_BITS_LOG2,
addr_bits_log2: crate::bytecode::RUNTIME_ADDRESS_BITS_LOG2,
float_bits_log2: crate::bytecode::RUNTIME_FLOAT_BITS_LOG2,
wcet_cycles: 0,
wcmu_bytes: 0,
aux_arena_bytes: 0,
persistent_composite_bytes: 0,
flags: 0,
shared_data_bytes: 0,
private_data_bytes: 0,
schema_hash: 0,
};
module_roundtrip_through_wire_format(module);
}
#[test]
fn module_roundtrip_stream_chunk() {
let chunk = Chunk {
name: alloc::string::String::from("tick"),
ops: alloc::vec![
Op::Stream,
Op::PushImmediate(7),
Op::Yield,
Op::PopN(1),
Op::Reset,
],
constants: alloc::vec::Vec::new(),
struct_templates: alloc::vec::Vec::new(),
local_count: 0,
param_count: 0,
block_type: BlockType::Stream,
param_types: alloc::vec::Vec::new(),
debug_pool: None,
};
let module = Module {
chunks: alloc::vec![chunk],
native_names: alloc::vec::Vec::new(),
enum_layouts: alloc::vec::Vec::new(),
signatures: alloc::vec::Vec::new(),
native_return_shapes: alloc::vec::Vec::new(),
entry_point: Some(0),
data_layout: None,
word_bits_log2: crate::bytecode::RUNTIME_WORD_BITS_LOG2,
addr_bits_log2: crate::bytecode::RUNTIME_ADDRESS_BITS_LOG2,
float_bits_log2: crate::bytecode::RUNTIME_FLOAT_BITS_LOG2,
wcet_cycles: 0,
wcmu_bytes: 0,
aux_arena_bytes: 0,
persistent_composite_bytes: 0,
flags: 0,
shared_data_bytes: 0,
private_data_bytes: 0,
schema_hash: 0,
};
module_roundtrip_through_wire_format(module);
}
#[test]
fn module_roundtrip_bad_magic_rejected() {
let module = make_minimal_module();
let mut bytes = module_to_wire_bytes(&module).expect("encode");
bytes[0] = b'X';
let err = module_from_wire_bytes(&bytes).unwrap_err();
assert!(matches!(err, LoadError::BadMagic));
}
#[test]
fn module_roundtrip_bad_crc_rejected() {
let module = make_minimal_module();
let mut bytes = module_to_wire_bytes(&module).expect("encode");
let len = bytes.len();
bytes[len - 1] ^= 0x01;
let err = module_from_wire_bytes(&bytes).unwrap_err();
assert!(matches!(err, LoadError::BadChecksum));
}
#[test]
fn module_roundtrip_truncated_rejected() {
let module = make_minimal_module();
let bytes = module_to_wire_bytes(&module).expect("encode");
let err = module_from_wire_bytes(&bytes[..32]).unwrap_err();
assert!(matches!(err, LoadError::Truncated));
}
#[test]
fn module_roundtrip_shebang_stripped() {
let module = make_minimal_module();
let bytes = module_to_wire_bytes(&module).expect("encode");
let mut with_shebang: alloc::vec::Vec<u8> =
alloc::vec::Vec::from(b"#!/usr/bin/env keleusma\n".as_slice());
with_shebang.extend_from_slice(&bytes);
let decoded = module_from_wire_bytes(&with_shebang).expect("decode");
assert_eq!(decoded.chunks.len(), 1);
assert_eq!(decoded.chunks[0].ops.len(), 2);
}
#[test]
fn unsigned_module_header_length_is_64() {
let bytes = module_to_wire_bytes(&make_minimal_module()).expect("encode");
let header_length = u16::from_le_bytes([bytes[6], bytes[7]]);
assert_eq!(header_length, WIRE_FORMAT_HEADER_BYTES as u16);
assert_eq!(bytes[15] & FLAG_REQUIRES_SIGNATURE, 0);
}
#[test]
fn flag_requires_signature_without_extension_rejected() {
let mut bytes = module_to_wire_bytes(&make_minimal_module()).expect("encode");
bytes[15] |= FLAG_REQUIRES_SIGNATURE;
let footer_start = bytes.len() - WIRE_FORMAT_FOOTER_BYTES;
let crc = crc32(&bytes[..footer_start]);
bytes[footer_start..].copy_from_slice(&crc.to_le_bytes());
let err = module_from_wire_bytes(&bytes).unwrap_err();
match err {
LoadError::Codec(msg) => assert!(
msg.contains("FLAG_REQUIRES_SIGNATURE is set"),
"expected flag/extension consistency error, got: {}",
msg
),
other => panic!("unexpected error: {:?}", other),
}
}
#[test]
fn parse_signature_metadata_rejects_unsupported_scheme() {
let mut bytes = alloc::vec![0u8; 144];
bytes[64] = 99; bytes[66] = 64; let err = parse_signature_metadata(&bytes, 144).unwrap_err();
match err {
LoadError::Codec(msg) => assert!(
msg.contains("scheme_id 99 is not supported"),
"expected scheme rejection, got: {}",
msg
),
other => panic!("unexpected error: {:?}", other),
}
}
#[test]
fn parse_signature_metadata_rejects_scheme_id_zero() {
let bytes = alloc::vec![0u8; 144];
let err = parse_signature_metadata(&bytes, 144).unwrap_err();
match err {
LoadError::Codec(msg) => assert!(
msg.contains("scheme_id 0 is reserved"),
"expected zero-scheme rejection, got: {}",
msg
),
other => panic!("unexpected error: {:?}", other),
}
}
#[test]
fn signed_header_length_for_ed25519() {
assert_eq!(signed_header_length(ED25519_SIGNATURE_BYTES), 136);
}
#[cfg(feature = "signatures")]
#[test]
fn ed25519_round_trip_verifies() {
use ed25519_dalek::SigningKey;
let seed = [7u8; 32];
let signing_key = SigningKey::from_bytes(&seed);
let verifying_key = signing_key.verifying_key();
let module = make_minimal_module();
let signed_bytes = module_to_signed_wire_bytes(&module, &signing_key).expect("sign+encode");
assert_eq!(
u16::from_le_bytes([signed_bytes[6], signed_bytes[7]]),
signed_header_length(ED25519_SIGNATURE_BYTES) as u16,
);
assert_ne!(signed_bytes[15] & FLAG_REQUIRES_SIGNATURE, 0);
assert_eq!(signed_bytes[64], SIGNATURE_SCHEME_ED25519);
assert_eq!(
u16::from_le_bytes([signed_bytes[66], signed_bytes[67]]),
ED25519_SIGNATURE_BYTES as u16,
);
verify_module_signature(&signed_bytes, &[verifying_key]).expect("verify");
let decoded = module_from_wire_bytes(&signed_bytes).expect("decode");
assert_eq!(decoded.entry_point, module.entry_point);
assert_eq!(decoded.chunks.len(), module.chunks.len());
assert_eq!(decoded.chunks[0].ops.len(), module.chunks[0].ops.len());
}
#[cfg(feature = "signatures")]
#[test]
fn ed25519_verify_rejects_wrong_key() {
use ed25519_dalek::SigningKey;
let signer = SigningKey::from_bytes(&[7u8; 32]);
let wrong = SigningKey::from_bytes(&[8u8; 32]).verifying_key();
let signed = module_to_signed_wire_bytes(&make_minimal_module(), &signer).expect("sign");
let err = verify_module_signature(&signed, &[wrong]).unwrap_err();
assert!(
matches!(err, LoadError::InvalidSignature),
"expected InvalidSignature, got: {:?}",
err
);
}
#[cfg(feature = "signatures")]
#[test]
fn ed25519_verify_rejects_empty_key_set() {
use ed25519_dalek::SigningKey;
let signer = SigningKey::from_bytes(&[7u8; 32]);
let signed = module_to_signed_wire_bytes(&make_minimal_module(), &signer).expect("sign");
let err = verify_module_signature(&signed, &[]).unwrap_err();
assert!(
matches!(err, LoadError::InvalidSignature),
"expected InvalidSignature, got: {:?}",
err
);
}
#[cfg(feature = "signatures")]
#[test]
fn ed25519_tamper_in_body_caught_by_crc_before_signature() {
use ed25519_dalek::SigningKey;
let signer = SigningKey::from_bytes(&[7u8; 32]);
let verifying = signer.verifying_key();
let mut signed =
module_to_signed_wire_bytes(&make_minimal_module(), &signer).expect("sign");
let opcode_offset = signed_header_length(ED25519_SIGNATURE_BYTES);
signed[opcode_offset] ^= 0x01;
let err = verify_module_signature(&signed, &[verifying]).unwrap_err();
match err {
LoadError::BadChecksum | LoadError::Codec(_) | LoadError::InvalidSignature => {}
other => panic!(
"expected BadChecksum / Codec / InvalidSignature, got: {:?}",
other
),
}
}
#[cfg(feature = "signatures")]
#[test]
fn ed25519_signature_mutation_caught_after_crc_repair() {
use ed25519_dalek::SigningKey;
let signer = SigningKey::from_bytes(&[7u8; 32]);
let verifying = signer.verifying_key();
let mut signed =
module_to_signed_wire_bytes(&make_minimal_module(), &signer).expect("sign");
let sig_offset = WIRE_FORMAT_HEADER_BYTES + SIGNATURE_METADATA_BYTES;
signed[sig_offset] ^= 0x01;
let footer_start = signed.len() - WIRE_FORMAT_FOOTER_BYTES;
let crc = crc32(&signed[..footer_start]);
signed[footer_start..].copy_from_slice(&crc.to_le_bytes());
let err = verify_module_signature(&signed, &[verifying]).unwrap_err();
assert!(
matches!(err, LoadError::InvalidSignature),
"expected InvalidSignature after sig mutation, got: {:?}",
err
);
}
#[cfg(all(feature = "signatures", feature = "encryption"))]
#[test]
fn encrypted_signed_wire_round_trip() {
use crate::encryption::public_key_from_private;
use ed25519_dalek::SigningKey;
let signer = SigningKey::from_bytes(&[0xa1; 32]);
let verifying = signer.verifying_key();
let recipient_sk = [0xb2u8; 32];
let recipient_pk = public_key_from_private(&recipient_sk);
let ephemeral_seed = [0xc3u8; 32];
let module = make_minimal_module();
let encrypted =
module_to_encrypted_signed_wire_bytes(&module, &signer, &recipient_pk, &ephemeral_seed)
.expect("encrypt+sign");
assert_ne!(encrypted[15] & FLAG_REQUIRES_SIGNATURE, 0);
assert_ne!(encrypted[15] & FLAG_ENCRYPTED, 0);
let hl = u16::from_le_bytes([encrypted[6], encrypted[7]]) as usize;
assert_eq!(hl, encrypted_signed_header_length());
let reconstructed =
decrypt_encrypted_signed_to_signed_bytes(&encrypted, &[verifying], &recipient_sk)
.expect("decrypt");
assert_eq!(reconstructed[15] & FLAG_ENCRYPTED, 0);
assert_ne!(reconstructed[15] & FLAG_REQUIRES_SIGNATURE, 0);
let decoded = module_from_wire_bytes(&reconstructed).expect("parse reconstructed");
assert_eq!(decoded.chunks.len(), module.chunks.len());
assert_eq!(decoded.chunks[0].ops, module.chunks[0].ops);
}
#[cfg(all(feature = "signatures", feature = "encryption"))]
#[test]
fn encrypted_signed_wrong_recipient_rejected() {
use crate::encryption::public_key_from_private;
use ed25519_dalek::SigningKey;
let signer = SigningKey::from_bytes(&[0xa2; 32]);
let verifying = signer.verifying_key();
let alice_sk = [0x11u8; 32];
let alice_pk = public_key_from_private(&alice_sk);
let bob_sk = [0x22u8; 32];
let ephemeral_seed = [0x33u8; 32];
let encrypted = module_to_encrypted_signed_wire_bytes(
&make_minimal_module(),
&signer,
&alice_pk,
&ephemeral_seed,
)
.expect("encrypt+sign");
let result = decrypt_encrypted_signed_to_signed_bytes(&encrypted, &[verifying], &bob_sk);
assert!(result.is_err(), "expected error decrypting as bob");
}
#[cfg(all(feature = "signatures", feature = "encryption"))]
#[test]
fn encrypted_signed_wrong_signer_rejected() {
use crate::encryption::public_key_from_private;
use ed25519_dalek::SigningKey;
let real_signer = SigningKey::from_bytes(&[0xa3; 32]);
let other_signer = SigningKey::from_bytes(&[0xa4; 32]);
let other_verifying = other_signer.verifying_key();
let recipient_sk = [0x55u8; 32];
let recipient_pk = public_key_from_private(&recipient_sk);
let ephemeral_seed = [0x66u8; 32];
let encrypted = module_to_encrypted_signed_wire_bytes(
&make_minimal_module(),
&real_signer,
&recipient_pk,
&ephemeral_seed,
)
.expect("encrypt+sign");
let result =
decrypt_encrypted_signed_to_signed_bytes(&encrypted, &[other_verifying], &recipient_sk);
match result {
Err(LoadError::InvalidSignature) => (),
other => panic!(
"expected InvalidSignature when verifying with wrong key, got: {:?}",
other
),
}
}
#[cfg(all(feature = "signatures", feature = "encryption"))]
#[test]
fn encrypted_signed_tampered_ciphertext_rejected() {
use crate::encryption::public_key_from_private;
use ed25519_dalek::SigningKey;
let signer = SigningKey::from_bytes(&[0xa5; 32]);
let verifying = signer.verifying_key();
let recipient_sk = [0x77u8; 32];
let recipient_pk = public_key_from_private(&recipient_sk);
let ephemeral_seed = [0x88u8; 32];
let mut encrypted = module_to_encrypted_signed_wire_bytes(
&make_minimal_module(),
&signer,
&recipient_pk,
&ephemeral_seed,
)
.expect("encrypt+sign");
let body_start = encrypted_signed_header_length();
encrypted[body_start + 4] ^= 0x01;
let footer_start = encrypted.len() - WIRE_FORMAT_FOOTER_BYTES;
let crc = crc32(&encrypted[..footer_start]);
encrypted[footer_start..].copy_from_slice(&crc.to_le_bytes());
let result =
decrypt_encrypted_signed_to_signed_bytes(&encrypted, &[verifying], &recipient_sk);
assert!(
result.is_err(),
"expected rejection on tampered ciphertext after CRC repair"
);
}
#[test]
fn header_requires_encryption_detects_flag() {
let mut bytes: Vec<u8> = alloc::vec![0u8; 20];
bytes[0..4].copy_from_slice(&BYTECODE_MAGIC[..]);
assert!(!header_requires_encryption(&bytes));
bytes[15] = FLAG_ENCRYPTED;
assert!(header_requires_encryption(&bytes));
bytes[15] = FLAG_REQUIRES_SIGNATURE | FLAG_ENCRYPTED;
assert!(header_requires_encryption(&bytes));
assert!(header_requires_signature(&bytes));
}
}