use crate::format::{
le_u16, le_u32, limit, IrError, IrHeader, IslandEntry, IslandTrigger, Opcode, SectionTable,
SlotEntry, SlotSource, SlotType, HEADER_SIZE, MAX_ISLANDS, MAX_STRINGS, SECTION_BYTECODE,
SECTION_ISLANDS, SECTION_ISLAND_CONTRACTS, SECTION_KEY_PATHS, SECTION_PROPS_SCHEMA,
SECTION_REQUIRED, SECTION_SLOTS, SECTION_SOURCE_POSITIONS, SECTION_STRINGS,
};
use crate::schema::{self, Cursor, KeyPath, PropsSchema, SourcePosition};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StringTable {
strings: Vec<String>,
}
impl StringTable {
pub fn parse(data: &[u8]) -> Result<Self, IrError> {
if data.len() < 4 {
return Err(IrError::BufferTooShort {
expected: 4,
actual: data.len(),
});
}
let count = le_u32(&data[0..4]) as usize;
limit("strings", count, MAX_STRINGS)?;
let max_entries = (data.len() - 4) / 2;
if count > max_entries {
return Err(IrError::BufferTooShort {
expected: 4usize.saturating_add(count.saturating_mul(2)),
actual: data.len(),
});
}
let mut offset = 4;
let mut strings = Vec::with_capacity(count);
for _ in 0..count {
if offset + 2 > data.len() {
return Err(IrError::BufferTooShort {
expected: offset + 2,
actual: data.len(),
});
}
let str_len = le_u16(&data[offset..offset + 2]) as usize;
offset += 2;
if offset + str_len > data.len() {
return Err(IrError::BufferTooShort {
expected: offset + str_len,
actual: data.len(),
});
}
let s = std::str::from_utf8(&data[offset..offset + str_len])
.map_err(|e| IrError::InvalidUtf8(e.to_string()))?;
strings.push(s.to_owned());
offset += str_len;
}
if offset != data.len() {
return Err(IrError::TrailingBytes {
section: SECTION_STRINGS,
});
}
Ok(StringTable { strings })
}
pub fn get(&self, idx: u32) -> Result<&str, IrError> {
self.strings
.get(idx as usize)
.map(|s| s.as_str())
.ok_or(IrError::StringIndexOutOfBounds {
index: idx,
len: self.strings.len(),
})
}
pub fn len(&self) -> usize {
self.strings.len()
}
pub fn is_empty(&self) -> bool {
self.strings.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SlotTable {
slots: Vec<SlotEntry>,
}
const SLOT_ENTRY_MIN_SIZE: usize = 10;
impl SlotTable {
pub fn parse(data: &[u8]) -> Result<Self, IrError> {
if data.len() < 2 {
return Err(IrError::BufferTooShort {
expected: 2,
actual: data.len(),
});
}
let count = le_u16(&data[0..2]) as usize;
let max_entries = (data.len() - 2) / SLOT_ENTRY_MIN_SIZE;
if count > max_entries {
return Err(IrError::BufferTooShort {
expected: 2usize.saturating_add(count.saturating_mul(SLOT_ENTRY_MIN_SIZE)),
actual: data.len(),
});
}
let mut slots = Vec::with_capacity(count);
let mut offset = 2;
for _ in 0..count {
if offset + SLOT_ENTRY_MIN_SIZE > data.len() {
return Err(IrError::BufferTooShort {
expected: offset + SLOT_ENTRY_MIN_SIZE,
actual: data.len(),
});
}
let slot_id = le_u16(&data[offset..offset + 2]);
let name_str_idx = le_u32(&data[offset + 2..offset + 6]);
let type_hint = SlotType::from_byte(data[offset + 6])?;
let source = SlotSource::from_byte(data[offset + 7])?;
let default_len = le_u16(&data[offset + 8..offset + 10]) as usize;
offset += SLOT_ENTRY_MIN_SIZE;
if offset + default_len > data.len() {
return Err(IrError::BufferTooShort {
expected: offset + default_len,
actual: data.len(),
});
}
let default_bytes = data[offset..offset + default_len].to_vec();
offset += default_len;
validate_default(slot_id, type_hint, &default_bytes)?;
slots.push(SlotEntry {
slot_id,
name_str_idx,
type_hint,
source,
default_bytes,
});
}
if offset != data.len() {
return Err(IrError::TrailingBytes {
section: SECTION_SLOTS,
});
}
Ok(SlotTable { slots })
}
pub fn len(&self) -> usize {
self.slots.len()
}
pub fn is_empty(&self) -> bool {
self.slots.is_empty()
}
pub fn entries(&self) -> &[SlotEntry] {
&self.slots
}
}
fn validate_default(id: u16, kind: SlotType, bytes: &[u8]) -> Result<(), IrError> {
if bytes.is_empty() {
return Ok(());
}
let text = std::str::from_utf8(bytes).map_err(|e| IrError::InvalidUtf8(e.to_string()))?;
let valid = match kind {
SlotType::Text => true,
SlotType::Bool => matches!(text, "true" | "false"),
SlotType::Number => decimal(text) && text.parse::<f64>().is_ok_and(|n| n.is_finite()),
SlotType::Array => text == "[]",
SlotType::Object => false,
};
if valid {
Ok(())
} else {
Err(IrError::InvalidDefault(id))
}
}
fn decimal(text: &str) -> bool {
let b = text.as_bytes();
let mut p = 0;
if matches!(b.get(p), Some(b'+' | b'-')) {
p += 1;
}
let mut digits = 0;
while b.get(p).is_some_and(u8::is_ascii_digit) {
p += 1;
digits += 1;
}
if b.get(p) == Some(&b'.') {
p += 1;
while b.get(p).is_some_and(u8::is_ascii_digit) {
p += 1;
digits += 1;
}
}
if digits == 0 {
return false;
}
if matches!(b.get(p), Some(b'e' | b'E')) {
p += 1;
if matches!(b.get(p), Some(b'+' | b'-')) {
p += 1;
}
let start = p;
while b.get(p).is_some_and(u8::is_ascii_digit) {
p += 1;
}
if p == start {
return false;
}
}
p == b.len()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IslandTableParsed {
islands: Vec<IslandEntry>,
}
impl IslandTableParsed {
pub fn parse(data: &[u8]) -> Result<Self, IrError> {
let mut r = Cursor::new(data);
let count = r.u16()? as usize;
limit("islands", count, MAX_ISLANDS)?;
r.bound(count, 19)?;
let mut islands = Vec::with_capacity(count);
for _ in 0..count {
let entry_len = r.u16()? as usize;
let mut e = Cursor::new(r.take(entry_len)?);
let id = e.u16()?;
let trigger = IslandTrigger::from_byte(e.u8()?)?;
if e.u8()? != 0 {
return Err(IrError::ReservedBits { field: "island" });
}
let name_str_idx = e.u32()?;
let byte_offset = e.u32()?;
let schema_object = e.u16()?;
let digest_len = e.u8()? as usize;
let digest = e.take(digest_len)?.to_vec();
let slot_count = e.u16()? as usize;
e.bound(slot_count, 2)?;
let mut slot_ids = Vec::with_capacity(slot_count);
for _ in 0..slot_count {
slot_ids.push(e.u16()?);
}
let extension = e.take(e.remaining())?.to_vec();
islands.push(IslandEntry {
id,
trigger,
name_str_idx,
byte_offset,
schema_object,
digest,
slot_ids,
extension,
});
}
r.finish(SECTION_ISLANDS)?;
Ok(Self { islands })
}
pub fn len(&self) -> usize {
self.islands.len()
}
pub fn is_empty(&self) -> bool {
self.islands.is_empty()
}
pub fn entries(&self) -> &[IslandEntry] {
&self.islands
}
}
const NO_INDEX: u32 = u32::MAX;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct IrModule {
pub header: IrHeader,
pub strings: StringTable,
pub slots: SlotTable,
pub opcodes: Vec<u8>,
pub islands: IslandTableParsed,
pub sections: SectionTable,
pub schema: PropsSchema,
pub key_paths: Vec<KeyPath>,
pub source_positions: Vec<SourcePosition>,
raw: Vec<u8>,
pub(crate) contracts: Option<crate::contracts::Contracts>,
slot_index: Vec<u32>,
island_index: Vec<u32>,
}
impl IrModule {
pub fn parse(data: &[u8]) -> Result<Self, IrError> {
let header = IrHeader::parse(data)?;
let section_table = SectionTable::parse(&data[HEADER_SIZE..])?;
section_table.validate(data.len())?;
for section in §ion_table.sections {
if !(SECTION_BYTECODE..=SECTION_SOURCE_POSITIONS).contains(§ion.id)
&& section.id != SECTION_ISLAND_CONTRACTS
&& section.flags & SECTION_REQUIRED != 0
{
return Err(IrError::UnknownRequiredSection {
id: section.id,
writer: header.writer,
});
}
}
for id in SECTION_BYTECODE..=SECTION_PROPS_SCHEMA {
if section_table.get(id).is_none() {
return Err(IrError::MissingSection(id));
}
}
let payload = |id| -> Result<&[u8], IrError> {
let section = section_table.get(id).ok_or(IrError::MissingSection(id))?;
let start = section.offset as usize;
data.get(start..start + section.size as usize)
.ok_or(IrError::InvalidSection)
};
if let Some(section) = section_table.get(SECTION_ISLAND_CONTRACTS) {
if section.flags != SECTION_REQUIRED {
return Err(IrError::InvalidSchema("island contracts must be required"));
}
}
let contracts = if section_table.get(SECTION_ISLAND_CONTRACTS).is_some() {
Some(crate::contracts::Contracts::parse(payload(
SECTION_ISLAND_CONTRACTS,
)?)?)
} else {
None
};
let strings = StringTable::parse(payload(SECTION_STRINGS)?)?;
let slots = SlotTable::parse(payload(SECTION_SLOTS)?)?;
let islands = IslandTableParsed::parse(payload(SECTION_ISLANDS)?)?;
let schema = PropsSchema::parse(payload(SECTION_PROPS_SCHEMA)?, &strings)?;
let opcodes = payload(SECTION_BYTECODE)?.to_vec();
let slot_index = build_id_index(slots.entries().iter().map(|e| e.slot_id));
let island_index = build_id_index(islands.entries().iter().map(|e| e.id));
let mut module = Self {
header,
strings,
slots,
islands,
schema,
opcodes,
slot_index,
island_index,
sections: section_table.clone(),
key_paths: Vec::new(),
source_positions: Vec::new(),
raw: data.to_vec(),
contracts,
};
let starts = module.validation_starts()?;
if section_table.get(SECTION_KEY_PATHS).is_some() {
module.key_paths =
schema::key_paths(payload(SECTION_KEY_PATHS)?, &module.strings, &starts)?;
}
if section_table.get(SECTION_SOURCE_POSITIONS).is_some() {
module.source_positions = schema::source_positions(
payload(SECTION_SOURCE_POSITIONS)?,
&module.strings,
&starts,
)?;
}
module.validate_contracts(&starts)?;
Ok(module)
}
pub fn island_contract_bytes(&self, id: u16) -> Option<(&[u8], &[u8])> {
let contract = self.contracts.as_ref()?.islands.get(&id)?;
Some((&contract.binding_bytes, &contract.schema_bytes))
}
pub fn raw_bytes(&self) -> &[u8] {
&self.raw
}
pub fn slot_entry(&self, slot_id: u16) -> Option<&SlotEntry> {
match self.slot_index.get(slot_id as usize) {
Some(&idx) if idx != NO_INDEX => self.slots.entries().get(idx as usize),
_ => None,
}
}
pub fn island_entry(&self, island_id: u16) -> Option<&IslandEntry> {
match self.island_index.get(island_id as usize) {
Some(&idx) if idx != NO_INDEX => self.islands.entries().get(idx as usize),
_ => None,
}
}
pub fn validate(&self) -> Result<(), IrError> {
let starts = self.validation_starts()?;
self.validate_contracts(&starts)
}
fn validation_starts(&self) -> Result<Vec<(u32, Opcode)>, IrError> {
let str_count = self.strings.len();
let mut seen_slot = vec![false; self.slot_index.len()];
for slot in self.slots.entries() {
if slot.name_str_idx as usize >= str_count {
return Err(IrError::StringIndexOutOfBounds {
index: slot.name_str_idx,
len: str_count,
});
}
let idx = slot.slot_id as usize;
if seen_slot[idx] {
return Err(IrError::DuplicateSlotId(slot.slot_id));
}
seen_slot[idx] = true;
}
let mut seen_island = vec![false; self.island_index.len()];
for island in self.islands.entries() {
if island.name_str_idx as usize >= str_count {
return Err(IrError::StringIndexOutOfBounds {
index: island.name_str_idx,
len: str_count,
});
}
let idx = island.id as usize;
if seen_island[idx] {
return Err(IrError::DuplicateIslandId(island.id));
}
seen_island[idx] = true;
for &slot_id in &island.slot_ids {
if self.slot_entry(slot_id).is_none() {
return Err(IrError::UnknownSlotId(slot_id));
}
}
let off = island.byte_offset as usize;
let id_matches = self.opcodes.get(off) == Some(&(Opcode::IslandStart as u8))
&& crate::walker::read_u16(&self.opcodes, off + 1) == Ok(island.id);
if !id_matches {
return Err(IrError::IslandOffsetMismatch {
island_id: island.id,
byte_offset: island.byte_offset,
});
}
}
let mut starts = Vec::new();
self.scan_region(0, self.opcodes.len(), 0, &mut starts)?;
for island in self.islands.entries() {
if starts
.binary_search_by_key(&island.byte_offset, |(o, _)| *o)
.is_err()
{
return Err(IrError::IslandOffsetMismatch {
island_id: island.id,
byte_offset: island.byte_offset,
});
}
if island.schema_object != 0xffff
&& island.schema_object as usize >= self.schema.objects.len()
{
return Err(IrError::InvalidSchema("island object index out of range"));
}
}
Ok(starts)
}
fn validate_contracts(&self, starts: &[(u32, Opcode)]) -> Result<(), IrError> {
if let Some(contracts) = &self.contracts {
contracts.validate(self, starts)?;
} else if !self.islands.is_empty() || starts.iter().any(|(_, op)| *op == Opcode::Coalesce) {
return Err(IrError::MissingSection(SECTION_ISLAND_CONTRACTS));
}
Ok(())
}
fn scan_region(
&self,
start: usize,
end: usize,
depth: usize,
starts: &mut Vec<(u32, Opcode)>,
) -> Result<(), IrError> {
limit("control-flow depth", depth, MAX_VALIDATION_DEPTH)?;
let ops = &self.opcodes;
let mut pos = start;
while pos < end {
let op_start = pos;
let op = Opcode::from_byte(ops[pos])?;
starts.push((pos as u32, op));
pos += 1;
match op {
Opcode::OpenTag | Opcode::VoidTag => {
self.strings.get(read_u32_at(ops, pos, end)?)?;
let n = read_u16_at(ops, pos + 4, end)? as usize;
let next = bounded(pos, 6 + n * 8, end, op_start)?;
for i in 0..n {
self.strings.get(read_u32_at(ops, pos + 6 + i * 8, end)?)?;
self.strings.get(read_u32_at(ops, pos + 10 + i * 8, end)?)?;
}
pos = next;
}
Opcode::CloseTag | Opcode::Text => {
self.strings.get(read_u32_at(ops, pos, end)?)?;
pos = bounded(pos, 4, end, op_start)?;
}
Opcode::DynText => {
self.check_slot(read_u16_at(ops, pos, end)?)?;
pos = bounded(pos, 4, end, op_start)?;
}
Opcode::DynAttr => {
self.strings.get(read_u32_at(ops, pos, end)?)?;
self.check_slot(read_u16_at(ops, pos + 4, end)?)?;
pos = bounded(pos, 6, end, op_start)?;
}
Opcode::IslandStart => {
let id = read_u16_at(ops, pos, end)?;
if self.island_entry(id).is_none() {
return Err(IrError::UnknownIslandId(id));
}
pos = bounded(pos, 2, end, op_start)?;
}
Opcode::IslandEnd => {
pos = bounded(pos, 2, end, op_start)?;
}
Opcode::ShowElse => {} Opcode::Coalesce => {
self.check_slot(read_u16_at(ops, pos, end)?)?;
self.check_slot(read_u16_at(ops, pos + 2, end)?)?;
self.check_slot(read_u16_at(ops, pos + 4, end)?)?;
pos = bounded(pos, 6, end, op_start)?;
}
Opcode::Prop => {
self.check_slot(read_u16_at(ops, pos, end)?)?;
self.strings.get(read_u32_at(ops, pos + 2, end)?)?;
self.check_slot(read_u16_at(ops, pos + 6, end)?)?;
pos = bounded(pos, 8, end, op_start)?;
}
Opcode::ShowIf => {
self.check_slot(read_u16_at(ops, pos, end)?)?;
let tl = read_u32_at(ops, pos + 2, end)? as usize;
let el = read_u32_at(ops, pos + 6, end)? as usize;
let ts = bounded(pos, 10, end, op_start)?;
let te = bounded(ts, tl, end, op_start)?;
if te >= end || ops[te] != Opcode::ShowElse as u8 {
return Err(IrError::MalformedControlFlow { pos: te });
}
let es = te + 1;
let ee = bounded(es, el, end, op_start)?;
self.scan_region(ts, te, depth + 1, starts)?;
starts.push((te as u32, Opcode::ShowElse));
self.scan_region(es, ee, depth + 1, starts)?;
pos = ee;
}
Opcode::List => {
self.check_slot(read_u16_at(ops, pos, end)?)?;
self.check_slot(read_u16_at(ops, pos + 2, end)?)?;
let len = read_u32_at(ops, pos + 4, end)? as usize;
let bs = bounded(pos, 8, end, op_start)?;
let be = bounded(bs, len, end, op_start)?;
self.scan_region(bs, be, depth + 1, starts)?;
pos = be;
}
}
}
Ok(())
}
fn check_slot(&self, slot_id: u16) -> Result<(), IrError> {
if self.slot_entry(slot_id).is_none() {
return Err(IrError::UnknownSlotId(slot_id));
}
Ok(())
}
pub fn slot_id_by_name(&self, name: &str) -> Option<u16> {
for slot in self.slots.entries() {
if let Ok(slot_name) = self.strings.get(slot.name_str_idx) {
if slot_name == name {
return Some(slot.slot_id);
}
}
}
None
}
}
const MAX_VALIDATION_DEPTH: usize = crate::format::MAX_CONTROL_DEPTH;
fn build_id_index(ids: impl Iterator<Item = u16> + Clone) -> Vec<u32> {
let max_id = ids.clone().max();
let Some(max_id) = max_id else {
return Vec::new();
};
let mut index = vec![NO_INDEX; max_id as usize + 1];
for (i, id) in ids.enumerate() {
let entry = &mut index[id as usize];
if *entry == NO_INDEX {
*entry = i as u32;
}
}
index
}
fn bounded(base: usize, len: usize, end: usize, op_start: usize) -> Result<usize, IrError> {
base.checked_add(len)
.filter(|e| *e <= end)
.ok_or(IrError::MalformedControlFlow { pos: op_start })
}
fn read_u16_at(ops: &[u8], pos: usize, end: usize) -> Result<u16, IrError> {
if pos.checked_add(2).map(|e| e > end).unwrap_or(true) {
return Err(IrError::MalformedControlFlow { pos });
}
crate::walker::read_u16(ops, pos)
}
fn read_u32_at(ops: &[u8], pos: usize, end: usize) -> Result<u32, IrError> {
if pos.checked_add(4).map(|e| e > end).unwrap_or(true) {
return Err(IrError::MalformedControlFlow { pos });
}
crate::walker::read_u32(ops, pos)
}
#[cfg(any(test, feature = "test-helpers"))]
#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
pub mod test_helpers {
use crate::format::{
crc32, reader_version, HEADER_SIZE, SECTION_ENTRY_SIZE, SIGNATURE, TABLE_PREFIX_SIZE,
};
fn to_u16(n: usize) -> [u8; 2] {
u16::try_from(n)
.unwrap_or_else(|_| panic!("{n} does not fit the u16 field this encoder writes"))
.to_le_bytes()
}
fn to_u32(n: usize) -> [u8; 4] {
u32::try_from(n)
.unwrap_or_else(|_| panic!("{n} does not fit the u32 field this encoder writes"))
.to_le_bytes()
}
pub fn build_string_table(strings: &[&str]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&to_u32(strings.len()));
for s in strings {
let bytes = s.as_bytes();
buf.extend_from_slice(&to_u16(bytes.len()));
buf.extend_from_slice(bytes);
}
buf
}
pub fn build_slot_table(entries: &[(u16, u32, u8, u8, &[u8])]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&to_u16(entries.len()));
for &(slot_id, name_str_idx, type_hint, source, default_bytes) in entries {
buf.extend_from_slice(&slot_id.to_le_bytes());
buf.extend_from_slice(&name_str_idx.to_le_bytes());
buf.push(type_hint);
buf.push(source);
buf.extend_from_slice(&to_u16(default_bytes.len()));
buf.extend_from_slice(default_bytes);
}
buf
}
pub fn build_island_table(entries: &[(u16, u8, u8, u32, u32, &[u16])]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&to_u16(entries.len()));
for &(id, trigger, _obsolete_mode, name_str_idx, byte_offset, slot_ids) in entries {
buf.extend_from_slice(&to_u16(17 + 2 * slot_ids.len()));
buf.extend_from_slice(&id.to_le_bytes());
buf.push(trigger);
buf.push(0);
buf.extend_from_slice(&name_str_idx.to_le_bytes());
buf.extend_from_slice(&byte_offset.to_le_bytes());
buf.extend_from_slice(&0xffffu16.to_le_bytes());
buf.push(0); buf.extend_from_slice(&to_u16(slot_ids.len()));
for &slot_id in slot_ids.iter() {
buf.extend_from_slice(&slot_id.to_le_bytes());
}
}
buf
}
pub fn encode_open_tag(str_idx: u32, attrs: &[(u32, u32)]) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(0x01); buf.extend_from_slice(&str_idx.to_le_bytes());
buf.extend_from_slice(&to_u16(attrs.len()));
for &(key_idx, val_idx) in attrs {
buf.extend_from_slice(&key_idx.to_le_bytes());
buf.extend_from_slice(&val_idx.to_le_bytes());
}
buf
}
pub fn encode_close_tag(str_idx: u32) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(0x02); buf.extend_from_slice(&str_idx.to_le_bytes());
buf
}
pub fn encode_void_tag(str_idx: u32, attrs: &[(u32, u32)]) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(0x03); buf.extend_from_slice(&str_idx.to_le_bytes());
buf.extend_from_slice(&to_u16(attrs.len()));
for &(key_idx, val_idx) in attrs {
buf.extend_from_slice(&key_idx.to_le_bytes());
buf.extend_from_slice(&val_idx.to_le_bytes());
}
buf
}
pub fn encode_text(str_idx: u32) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(0x04); buf.extend_from_slice(&str_idx.to_le_bytes());
buf
}
pub fn encode_show_if(slot_id: u16, then_ops: &[u8], else_ops: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(0x07); buf.extend_from_slice(&slot_id.to_le_bytes());
buf.extend_from_slice(&to_u32(then_ops.len()));
buf.extend_from_slice(&to_u32(else_ops.len()));
buf.extend_from_slice(then_ops); buf.push(0x08); buf.extend_from_slice(else_ops); buf
}
pub fn encode_list(slot_id: u16, item_slot_id: u16, body_ops: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(0x0A); buf.extend_from_slice(&slot_id.to_le_bytes());
buf.extend_from_slice(&item_slot_id.to_le_bytes());
buf.extend_from_slice(&to_u32(body_ops.len()));
buf.extend_from_slice(body_ops);
buf
}
pub fn encode_switch(slot_id: u16, cases: &[(u32, &[u8])]) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(0x09); buf.extend_from_slice(&slot_id.to_le_bytes());
buf.extend_from_slice(&to_u16(cases.len()));
for (val_str_idx, body) in cases {
buf.extend_from_slice(&val_str_idx.to_le_bytes());
buf.extend_from_slice(&to_u32(body.len()));
}
for (_, body) in cases {
buf.extend_from_slice(body);
}
buf
}
pub fn encode_try(main_ops: &[u8], fallback_ops: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(0x0D); buf.extend_from_slice(&to_u32(fallback_ops.len()));
buf.extend_from_slice(main_ops);
buf.push(0x0E); buf.extend_from_slice(fallback_ops);
buf
}
pub fn encode_preload(resource_type: u8, url_str_idx: u32) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(0x0F); buf.push(resource_type);
buf.extend_from_slice(&url_str_idx.to_le_bytes());
buf
}
pub fn build_minimal_ir(
strings: &[&str],
slots: &[(u16, u32, u8, u8, &[u8])],
opcodes: &[u8],
islands: &[(u16, u8, u8, u32, u32, &[u16])],
) -> Vec<u8> {
let string_section = build_string_table(strings);
let slot_section = build_slot_table(slots);
let island_section = build_island_table(islands);
build_ir_sections(&[
(1, 1, opcodes),
(2, 1, &string_section),
(3, 1, &slot_section),
(4, 1, &island_section),
(5, 1, &[1, 0, 0, 0]),
])
}
pub fn build_declared_fixture(
strings: &[&str],
slots: &[(u16, u32, u8, u8, &[u8])],
opcodes: &[u8],
islands: &[(u16, u8, u8, u32, u32, &[u16])],
) -> Vec<u8> {
if islands.is_empty() {
return build_minimal_ir(strings, slots, opcodes, islands);
}
fn kind(hint: u8) -> Vec<u8> {
match hint {
1 => vec![2],
2 => vec![1],
3 => vec![3],
4 => vec![6, 2],
_ => panic!("fixture requires an explicit non-scalar schema"),
}
}
fn fields(out: &mut Vec<u8>, declarations: &[(u32, Vec<u8>)]) {
out.extend(to_u16(declarations.len()));
for (name, kind) in declarations {
out.extend(name.to_le_bytes());
out.extend(0u32.to_le_bytes());
out.extend(1u32.to_le_bytes());
out.extend(1u32.to_le_bytes());
out.extend(kind);
}
}
let mut page = Vec::new();
for &(_, name, hint, source, _) in slots {
if source == 0 && !page.iter().any(|(prior, _)| *prior == name) {
page.push((name, kind(hint)));
}
}
let mut schema = to_u16(islands.len() + 1).to_vec();
fields(&mut schema, &page);
let mut island_table = to_u16(islands.len()).to_vec();
let mut records = Vec::new();
for (index, &(id, trigger, _, name, offset, inputs)) in islands.iter().enumerate() {
let declared = inputs
.iter()
.map(|id| {
let &(_, name, hint, _, _) = slots
.iter()
.find(|slot| slot.0 == *id)
.expect("declared fixture slot");
(name, kind(hint))
})
.collect::<Vec<_>>();
fields(&mut schema, &declared);
let mut record = id.to_le_bytes().to_vec();
record.extend([trigger, 0]);
record.extend(name.to_le_bytes());
record.extend(offset.to_le_bytes());
record.extend(to_u16(index + 1));
record.push(8);
record.extend([id as u8; 8]); record.extend(to_u16(inputs.len()));
for input in inputs {
record.extend(input.to_le_bytes());
}
island_table.extend(to_u16(record.len()));
island_table.extend(record);
let mut canonical = vec![1, 1, 0];
canonical.extend(to_u16(declared.len()));
let mut sorted = declared.iter().collect::<Vec<_>>();
sorted.sort_by_key(|(name, _)| strings[*name as usize]);
for (name, kind) in sorted {
let name = strings[*name as usize].as_bytes();
canonical.extend(to_u16(name.len()));
canonical.extend(name);
canonical.extend(kind);
}
let mut record = id.to_le_bytes().to_vec();
record.extend(4u32.to_le_bytes());
record.extend([1, 0, 0, 0]);
record.extend(to_u32(canonical.len()));
record.extend(canonical);
records.push((id, record));
}
records.sort_by_key(|(id, _)| *id);
let mut contracts = to_u16(records.len()).to_vec();
for (_, record) in records {
contracts.extend(to_u32(record.len()));
contracts.extend(record);
}
contracts.extend([0, 0]);
build_ir_sections(&[
(1, 1, opcodes),
(2, 1, &build_string_table(strings)),
(3, 1, &build_slot_table(slots)),
(4, 1, &island_table),
(5, 1, &schema),
(10, 1, &contracts),
])
}
pub fn build_ir_sections(sections: &[(u16, u16, &[u8])]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&SIGNATURE);
for v in reader_version() {
buf.extend_from_slice(&v.to_le_bytes());
}
buf.extend_from_slice(&[0; 10]); buf.extend_from_slice(&to_u16(sections.len()));
buf.extend_from_slice(&[0; 2]);
let mut offset = HEADER_SIZE + TABLE_PREFIX_SIZE + SECTION_ENTRY_SIZE * sections.len();
for &(id, flags, bytes) in sections {
buf.extend_from_slice(&id.to_le_bytes());
buf.extend_from_slice(&flags.to_le_bytes());
buf.extend_from_slice(&to_u32(offset));
buf.extend_from_slice(&to_u32(bytes.len()));
offset += bytes.len();
}
for &(_, _, bytes) in sections {
buf.extend_from_slice(bytes);
}
restamp(&mut buf);
buf
}
pub fn with_key_paths(ir: &[u8], paths: &[(u32, &[u32])]) -> Vec<u8> {
let table = &ir[HEADER_SIZE..];
let count = u16::from_le_bytes([table[0], table[1]]) as usize;
let mut sections: Vec<(u16, u16, &[u8])> = (0..count)
.map(|index| {
let entry = &table[TABLE_PREFIX_SIZE + index * SECTION_ENTRY_SIZE..];
let field = |at: usize| u32::from_le_bytes(entry[at..at + 4].try_into().unwrap());
let (offset, size) = (field(4) as usize, field(8) as usize);
(
u16::from_le_bytes([entry[0], entry[1]]),
u16::from_le_bytes([entry[2], entry[3]]),
&ir[offset..offset + size],
)
})
.collect();
let mut payload = to_u32(paths.len()).to_vec();
for (offset, segments) in paths {
payload.extend_from_slice(&offset.to_le_bytes());
payload.push(u8::try_from(segments.len()).unwrap());
for segment in *segments {
payload.extend_from_slice(&segment.to_le_bytes());
}
}
assert!(
sections.iter().all(|(id, _, _)| *id != 6),
"the module already has a key-path section"
);
let at = sections.partition_point(|(id, _, _)| *id < 6);
sections.insert(at, (6, 1, &payload));
build_ir_sections(§ions)
}
pub fn restamp(buf: &mut [u8]) {
let body_len = to_u32(buf.len() - HEADER_SIZE);
buf[14..18].copy_from_slice(&body_len);
let checksum = crc32(&[&buf[..18], &buf[22..]]);
buf[18..22].copy_from_slice(&checksum.to_le_bytes());
}
}
#[cfg(test)]
mod tests {
use super::test_helpers::*;
use super::*;
use crate::format::{IrError, IslandTrigger, SlotSource, SlotType};
#[test]
fn parse_string_table() {
let data = build_string_table(&["div", "class", "container"]);
let table = StringTable::parse(&data).unwrap();
assert_eq!(table.len(), 3);
assert_eq!(table.get(0).unwrap(), "div");
assert_eq!(table.get(1).unwrap(), "class");
assert_eq!(table.get(2).unwrap(), "container");
let err = table.get(3).unwrap_err();
assert_eq!(err, IrError::StringIndexOutOfBounds { index: 3, len: 3 });
}
#[test]
fn parse_string_table_empty() {
let data = build_string_table(&[]);
let table = StringTable::parse(&data).unwrap();
assert_eq!(table.len(), 0);
}
#[test]
fn parse_string_table_unicode() {
let data = build_string_table(&["héllo"]);
let table = StringTable::parse(&data).unwrap();
assert_eq!(table.len(), 1);
assert_eq!(table.get(0).unwrap(), "héllo");
}
#[test]
fn parse_string_table_truncated() {
let mut data = Vec::new();
data.extend_from_slice(&2u32.to_le_bytes()); data.extend_from_slice(&3u16.to_le_bytes()); data.extend_from_slice(b"div");
let err = StringTable::parse(&data).unwrap_err();
match err {
IrError::BufferTooShort { .. } => {} other => panic!("expected BufferTooShort, got {other:?}"),
}
}
#[test]
fn parse_slot_table() {
let data = build_slot_table(&[
(1, 0, 0x01, 0x00, &[]), (2, 1, 0x03, 0x01, b"7"), ]);
let table = SlotTable::parse(&data).unwrap();
assert_eq!(table.len(), 2);
let entries = table.entries();
assert_eq!(entries[0].slot_id, 1);
assert_eq!(entries[0].name_str_idx, 0);
assert_eq!(entries[0].type_hint, SlotType::Text);
assert_eq!(entries[0].source, SlotSource::Server);
assert_eq!(entries[0].default_bytes, Vec::<u8>::new());
assert_eq!(entries[1].slot_id, 2);
assert_eq!(entries[1].name_str_idx, 1);
assert_eq!(entries[1].type_hint, SlotType::Number);
assert_eq!(entries[1].source, SlotSource::Client);
assert_eq!(entries[1].default_bytes, b"7");
}
#[test]
fn parse_slot_table_empty() {
let data = build_slot_table(&[]);
let table = SlotTable::parse(&data).unwrap();
assert_eq!(table.len(), 0);
}
#[test]
fn parse_island_table() {
let data = build_island_table(&[
(1, 0x02, 0x01, 5, 0, &[]), ]);
let table = IslandTableParsed::parse(&data).unwrap();
assert_eq!(table.len(), 1);
let entry = &table.entries()[0];
assert_eq!(entry.id, 1);
assert_eq!(entry.trigger, IslandTrigger::Visible);
assert_eq!(entry.schema_object, 0xffff);
assert_eq!(entry.name_str_idx, 5);
assert_eq!(entry.slot_ids, Vec::<u16>::new());
}
#[test]
fn parse_island_table_with_slot_ids() {
let data = build_island_table(&[
(0, 0x01, 0x01, 0, 0, &[0, 1]), ]);
let table = IslandTableParsed::parse(&data).unwrap();
assert_eq!(table.len(), 1);
let entry = &table.entries()[0];
assert_eq!(entry.id, 0);
assert_eq!(entry.trigger, IslandTrigger::Load);
assert_eq!(entry.schema_object, 0xffff);
assert_eq!(entry.slot_ids, vec![0, 1]);
}
#[test]
fn parse_island_table_empty() {
let data = build_island_table(&[]);
let table = IslandTableParsed::parse(&data).unwrap();
assert_eq!(table.len(), 0);
}
#[test]
fn parse_minimal_ir_file() {
let mut opcodes = Vec::new();
opcodes.extend_from_slice(&encode_open_tag(0, &[]));
opcodes.extend_from_slice(&encode_close_tag(0));
let data = build_minimal_ir(&["div"], &[], &opcodes, &[]);
let module = IrModule::parse(&data).unwrap();
assert_eq!(module.header.writer, crate::format::reader_version());
assert_eq!(module.strings.get(0).unwrap(), "div");
assert_eq!(module.strings.len(), 1);
assert_eq!(module.slots.len(), 0);
assert_eq!(module.islands.len(), 0);
assert_eq!(module.opcodes.len(), opcodes.len());
}
#[test]
fn parse_ir_with_slots() {
let opcodes = encode_text(0);
let data = build_minimal_ir(
&["greeting", "count", "Hello"],
&[
(1, 0, 0x01, 0x00, &[]), (2, 1, 0x03, 0x00, &[]), ],
&opcodes,
&[],
);
let module = IrModule::parse(&data).unwrap();
assert_eq!(module.slots.len(), 2);
let entries = module.slots.entries();
assert_eq!(entries[0].slot_id, 1);
assert_eq!(entries[0].name_str_idx, 0);
assert_eq!(entries[0].type_hint, SlotType::Text);
assert_eq!(entries[1].slot_id, 2);
assert_eq!(entries[1].name_str_idx, 1);
assert_eq!(entries[1].type_hint, SlotType::Number);
}
#[test]
fn parse_ir_with_islands() {
let mut opcodes = vec![0x0B]; opcodes.extend_from_slice(&1u16.to_le_bytes());
opcodes.extend_from_slice(&encode_text(1));
opcodes.push(0x0C); opcodes.extend_from_slice(&1u16.to_le_bytes());
let data = build_declared_fixture(
&["Counter", "Hello"],
&[],
&opcodes,
&[
(1, 0x01, 0x01, 0, 0, &[]), ],
);
let module = IrModule::parse(&data).unwrap();
assert_eq!(module.islands.len(), 1);
let entry = &module.islands.entries()[0];
assert_eq!(entry.id, 1);
assert_eq!(entry.trigger, IslandTrigger::Load);
assert_eq!(entry.schema_object, 1);
assert_eq!(entry.name_str_idx, 0);
assert_eq!(module.strings.get(entry.name_str_idx).unwrap(), "Counter");
}
#[test]
fn parse_ir_rejects_truncated() {
let data = b"FMIR\x02\x00";
let err = IrModule::parse(data).unwrap_err();
match err {
IrError::BufferTooShort {
expected: 24,
actual: 6,
} => {}
other => panic!("expected BufferTooShort(24, 6), got {other:?}"),
}
}
#[test]
fn parse_ir_rejects_bad_section_bounds() {
let mut data = build_minimal_ir(&["x"], &[], &encode_text(0), &[]);
data[72..76].copy_from_slice(&99999u32.to_le_bytes());
restamp(&mut data);
assert_eq!(
IrModule::parse(&data).unwrap_err(),
IrError::SectionsDoNotTile
);
}
#[test]
fn validate_catches_bad_slot_str_idx() {
let opcodes = encode_text(0);
let data = build_minimal_ir(
&["a", "b", "c"],
&[(1, 99, 0x01, 0x00, &[])], &opcodes,
&[],
);
let err = IrModule::parse(&data).unwrap_err();
assert_eq!(err, IrError::StringIndexOutOfBounds { index: 99, len: 3 });
}
#[test]
fn validate_catches_bad_island_str_idx() {
let opcodes = encode_text(0);
let mut island_opcodes = vec![0x0B];
island_opcodes.extend_from_slice(&1u16.to_le_bytes());
island_opcodes.extend_from_slice(&opcodes);
island_opcodes.push(0x0C);
island_opcodes.extend_from_slice(&1u16.to_le_bytes());
let data = build_minimal_ir(
&["a", "b", "c"],
&[],
&island_opcodes,
&[(1, 0x01, 0x01, 99, 0, &[])],
);
let err = IrModule::parse(&data).unwrap_err();
assert_eq!(err, IrError::StringIndexOutOfBounds { index: 99, len: 3 });
}
#[test]
fn duplicate_slot_names_resolve_first_wins_everywhere() {
let opcodes = encode_text(0);
let data = build_minimal_ir(
&["dup"],
&[(0, 0, 0x01, 0x00, &[]), (1, 0, 0x01, 0x00, &[])],
&opcodes,
&[],
);
let module = IrModule::parse(&data).unwrap();
assert_eq!(module.slot_id_by_name("dup"), Some(0));
let slots = crate::slot::SlotData::from_json(r#"{"dup":"x"}"#, &module).unwrap();
assert_eq!(slots.get(0).to_text(), "x", "the first slot must be filled");
assert_eq!(slots.get(1).to_text(), "", "the second must stay untouched");
}
#[test]
fn validate_rejects_list_item_slot_not_in_table() {
let body = {
let mut b = vec![0x05]; b.extend_from_slice(&7u16.to_le_bytes());
b.extend_from_slice(&0u16.to_le_bytes());
b
};
let opcodes = encode_list(0, 7, &body);
let data = build_minimal_ir(&["items"], &[(0, 0, 0x04, 0x00, &[])], &opcodes, &[]);
assert_eq!(
IrModule::parse(&data).unwrap_err(),
IrError::UnknownSlotId(7)
);
}
#[test]
fn validate_rejects_island_start_missing_from_table() {
let mut opcodes = vec![0x0B]; opcodes.extend_from_slice(&3u16.to_le_bytes());
opcodes.extend_from_slice(&encode_text(0));
opcodes.push(0x0C); opcodes.extend_from_slice(&3u16.to_le_bytes());
let data = build_minimal_ir(&["hi"], &[], &opcodes, &[]);
assert_eq!(
IrModule::parse(&data).unwrap_err(),
IrError::UnknownIslandId(3)
);
}
#[test]
fn string_table_rejects_count_the_section_cannot_hold() {
let mut data = Vec::new();
data.extend_from_slice(&1_000_000u32.to_le_bytes()); data.extend_from_slice(&[0u8; 16]); let err = StringTable::parse(&data).unwrap_err();
assert!(matches!(err, IrError::BufferTooShort { .. }));
}
#[test]
fn slot_table_rejects_count_the_section_cannot_hold() {
let data = 65535u16.to_le_bytes().to_vec();
let err = SlotTable::parse(&data).unwrap_err();
assert!(matches!(err, IrError::BufferTooShort { .. }));
}
#[test]
fn island_table_rejects_count_the_section_cannot_hold() {
let count = 4096u16.to_le_bytes();
assert!(matches!(
IslandTableParsed::parse(&count),
Err(IrError::BufferTooShort { .. })
));
assert_eq!(
IslandTableParsed::parse(&4097u16.to_le_bytes())
.unwrap_err()
.code(),
"LOAD-107"
);
}
#[test]
fn validate_rejects_duplicate_slot_ids() {
let opcodes = encode_text(0);
let data = build_minimal_ir(
&["a", "b"],
&[(0, 0, 0x01, 0x00, &[]), (0, 1, 0x01, 0x00, &[])],
&opcodes,
&[],
);
assert_eq!(
IrModule::parse(&data).unwrap_err(),
IrError::DuplicateSlotId(0)
);
}
#[test]
fn validate_rejects_duplicate_island_ids() {
let mut opcodes = vec![0x0B];
opcodes.extend_from_slice(&0u16.to_le_bytes());
opcodes.push(0x0C);
opcodes.extend_from_slice(&0u16.to_le_bytes());
let data = build_minimal_ir(
&["Comp"],
&[],
&opcodes,
&[(0, 0x01, 0x01, 0, 0, &[]), (0, 0x01, 0x01, 0, 0, &[])],
);
assert_eq!(
IrModule::parse(&data).unwrap_err(),
IrError::DuplicateIslandId(0)
);
}
#[test]
fn validate_rejects_island_slot_id_absent_from_slot_table() {
let mut opcodes = vec![0x0B];
opcodes.extend_from_slice(&0u16.to_le_bytes());
opcodes.push(0x0C);
opcodes.extend_from_slice(&0u16.to_le_bytes());
let data = build_minimal_ir(&["Comp"], &[], &opcodes, &[(0, 0x01, 0x01, 0, 0, &[7])]);
assert_eq!(
IrModule::parse(&data).unwrap_err(),
IrError::UnknownSlotId(7)
);
}
#[test]
fn validate_rejects_island_offset_landing_mid_operand() {
let mut opcodes = vec![0x0B];
opcodes.extend_from_slice(&1u16.to_le_bytes());
opcodes.push(0x04);
opcodes.extend_from_slice(&0x0Bu32.to_le_bytes());
opcodes.push(0x0C);
opcodes.extend_from_slice(&1u16.to_le_bytes());
let data = build_minimal_ir(&["Comp"], &[], &opcodes, &[(1, 0x01, 0x01, 0, 4, &[])]);
assert_eq!(
IrModule::parse(&data).unwrap_err(),
IrError::IslandOffsetMismatch {
island_id: 1,
byte_offset: 4
}
);
}
#[test]
fn validate_rejects_opcode_slot_id_absent_from_slot_table() {
let mut opcodes = vec![0x05];
opcodes.extend_from_slice(&9u16.to_le_bytes());
opcodes.extend_from_slice(&0u16.to_le_bytes());
let data = build_minimal_ir(&["a"], &[], &opcodes, &[]);
assert_eq!(
IrModule::parse(&data).unwrap_err(),
IrError::UnknownSlotId(9)
);
}
#[test]
fn slot_and_island_lookups_are_indexed() {
let mut opcodes = vec![0x0B];
opcodes.extend_from_slice(&3u16.to_le_bytes());
opcodes.push(0x0C);
opcodes.extend_from_slice(&3u16.to_le_bytes());
let data = build_declared_fixture(
&["Comp", "title"],
&[(4, 1, 0x01, 0x00, &[])],
&opcodes,
&[(3, 0x01, 0x01, 0, 0, &[4])],
);
let module = IrModule::parse(&data).unwrap();
assert_eq!(module.slot_entry(4).unwrap().slot_id, 4);
assert!(module.slot_entry(0).is_none());
assert_eq!(module.island_entry(3).unwrap().id, 3);
assert!(module.island_entry(0).is_none());
assert!(module.slot_entry(u16::MAX).is_none());
assert!(module.island_entry(u16::MAX).is_none());
}
}