use crate::format::{
IrError, IrHeader, IslandEntry, IslandTrigger, PropsMode, SectionTable, SlotEntry, SlotSource,
SlotType, HEADER_SIZE, SECTION_TABLE_SIZE,
};
#[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 = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
if count > data.len() {
return Err(IrError::BufferTooShort {
expected: count,
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 = u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap()) 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;
}
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 = u16::from_le_bytes(data[0..2].try_into().unwrap()) as usize;
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 = u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap());
let name_str_idx = u32::from_le_bytes(data[offset + 2..offset + 6].try_into().unwrap());
let type_hint = SlotType::from_byte(data[offset + 6])?;
let source = SlotSource::from_byte(data[offset + 7])?;
let default_len =
u16::from_le_bytes(data[offset + 8..offset + 10].try_into().unwrap()) 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;
slots.push(SlotEntry {
slot_id,
name_str_idx,
type_hint,
source,
default_bytes,
});
}
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
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IslandTableParsed {
islands: Vec<IslandEntry>,
}
const ISLAND_ENTRY_MIN_SIZE: usize = 14;
impl IslandTableParsed {
pub fn parse(data: &[u8]) -> Result<Self, IrError> {
if data.len() < 2 {
return Err(IrError::BufferTooShort {
expected: 2,
actual: data.len(),
});
}
let count = u16::from_le_bytes(data[0..2].try_into().unwrap()) as usize;
let mut islands = Vec::with_capacity(count);
let mut offset = 2;
for _ in 0..count {
if offset + ISLAND_ENTRY_MIN_SIZE > data.len() {
return Err(IrError::BufferTooShort {
expected: offset + ISLAND_ENTRY_MIN_SIZE,
actual: data.len(),
});
}
let id = u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap());
let trigger = IslandTrigger::from_byte(data[offset + 2])?;
let props_mode = PropsMode::from_byte(data[offset + 3])?;
let name_str_idx = u32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap());
let byte_offset = u32::from_le_bytes(data[offset + 8..offset + 12].try_into().unwrap());
let slot_count =
u16::from_le_bytes(data[offset + 12..offset + 14].try_into().unwrap()) as usize;
offset += ISLAND_ENTRY_MIN_SIZE;
let needed = slot_count * 2;
if offset + needed > data.len() {
return Err(IrError::BufferTooShort {
expected: offset + needed,
actual: data.len(),
});
}
let mut slot_ids = Vec::with_capacity(slot_count);
for _ in 0..slot_count {
slot_ids.push(u16::from_le_bytes(
data[offset..offset + 2].try_into().unwrap(),
));
offset += 2;
}
islands.push(IslandEntry {
id,
trigger,
props_mode,
name_str_idx,
byte_offset,
slot_ids,
});
}
Ok(IslandTableParsed { 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
}
}
#[derive(Debug, Clone)]
pub struct IrModule {
pub header: IrHeader,
pub strings: StringTable,
pub slots: SlotTable,
pub opcodes: Vec<u8>,
pub islands: IslandTableParsed,
}
impl IrModule {
pub fn parse(data: &[u8]) -> Result<Self, IrError> {
let header = IrHeader::parse(data)?;
if data.len() < HEADER_SIZE + SECTION_TABLE_SIZE {
return Err(IrError::BufferTooShort {
expected: HEADER_SIZE + SECTION_TABLE_SIZE,
actual: data.len(),
});
}
let section_table = SectionTable::parse(&data[HEADER_SIZE..])?;
section_table.validate(data.len())?;
let sec_bytecode = §ion_table.sections[0];
let sec_strings = §ion_table.sections[1];
let sec_slots = §ion_table.sections[2];
let sec_islands = §ion_table.sections[3];
let string_data = &data[sec_strings.offset as usize
..(sec_strings.offset as usize + sec_strings.size as usize)];
let strings = StringTable::parse(string_data)?;
let slot_data =
&data[sec_slots.offset as usize..(sec_slots.offset as usize + sec_slots.size as usize)];
let slots = SlotTable::parse(slot_data)?;
let opcodes = data[sec_bytecode.offset as usize
..(sec_bytecode.offset as usize + sec_bytecode.size as usize)]
.to_vec();
let island_data = &data[sec_islands.offset as usize
..(sec_islands.offset as usize + sec_islands.size as usize)];
let islands = IslandTableParsed::parse(island_data)?;
let module = IrModule {
header,
strings,
slots,
opcodes,
islands,
};
module.validate()?;
Ok(module)
}
pub fn validate(&self) -> Result<(), IrError> {
let str_count = self.strings.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,
});
}
}
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,
});
}
}
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
}
}
pub mod test_helpers {
use crate::format::{HEADER_SIZE, SECTION_TABLE_SIZE};
pub fn build_string_table(strings: &[&str]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&(strings.len() as u32).to_le_bytes());
for s in strings {
let bytes = s.as_bytes();
buf.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
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(&(entries.len() as u16).to_le_bytes());
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(&(default_bytes.len() as u16).to_le_bytes());
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(&(entries.len() as u16).to_le_bytes());
for &(id, trigger, props_mode, name_str_idx, byte_offset, slot_ids) in entries {
buf.extend_from_slice(&id.to_le_bytes());
buf.push(trigger);
buf.push(props_mode);
buf.extend_from_slice(&name_str_idx.to_le_bytes());
buf.extend_from_slice(&byte_offset.to_le_bytes());
buf.extend_from_slice(&(slot_ids.len() as u16).to_le_bytes());
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(&(attrs.len() as u16).to_le_bytes());
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(&(attrs.len() as u16).to_le_bytes());
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(&(then_ops.len() as u32).to_le_bytes());
buf.extend_from_slice(&(else_ops.len() as u32).to_le_bytes());
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(&(body_ops.len() as u32).to_le_bytes());
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(&(cases.len() as u16).to_le_bytes());
for (val_str_idx, body) in cases {
buf.extend_from_slice(&val_str_idx.to_le_bytes());
buf.extend_from_slice(&(body.len() as u32).to_le_bytes());
}
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(&(fallback_ops.len() as u32).to_le_bytes());
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);
let data_start = HEADER_SIZE + SECTION_TABLE_SIZE;
let bytecode_offset = data_start;
let bytecode_size = opcodes.len();
let string_offset = bytecode_offset + bytecode_size;
let string_size = string_section.len();
let slot_offset = string_offset + string_size;
let slot_size = slot_section.len();
let island_offset = slot_offset + slot_size;
let island_size = island_section.len();
let mut buf = Vec::new();
buf.extend_from_slice(b"FMIR");
buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u64.to_le_bytes());
buf.extend_from_slice(&(bytecode_offset as u32).to_le_bytes());
buf.extend_from_slice(&(bytecode_size as u32).to_le_bytes());
buf.extend_from_slice(&(string_offset as u32).to_le_bytes());
buf.extend_from_slice(&(string_size as u32).to_le_bytes());
buf.extend_from_slice(&(slot_offset as u32).to_le_bytes());
buf.extend_from_slice(&(slot_size as u32).to_le_bytes());
buf.extend_from_slice(&(island_offset as u32).to_le_bytes());
buf.extend_from_slice(&(island_size as u32).to_le_bytes());
buf.extend_from_slice(opcodes);
buf.extend_from_slice(&string_section);
buf.extend_from_slice(&slot_section);
buf.extend_from_slice(&island_section);
buf
}
}
#[cfg(test)]
mod tests {
use super::test_helpers::*;
use super::*;
use crate::format::{IrError, IslandTrigger, PropsMode, 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, &[0x42]), ]);
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, vec![0x42]);
}
#[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.props_mode, PropsMode::Inline);
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.props_mode, PropsMode::Inline);
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.version, 2);
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 opcodes = encode_text(0);
let data = build_minimal_ir(
&["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.props_mode, PropsMode::Inline);
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: 16,
actual: 6,
} => {}
other => panic!("expected BufferTooShort(16, 6), got {other:?}"),
}
}
#[test]
fn parse_ir_rejects_bad_section_bounds() {
let opcodes = encode_text(0);
let mut data = build_minimal_ir(&["x"], &[], &opcodes, &[]);
let big_size: u32 = 99999;
data[44..48].copy_from_slice(&big_size.to_le_bytes());
let err = IrModule::parse(&data).unwrap_err();
match err {
IrError::SectionOutOfBounds { section: 3, .. } => {}
other => panic!("expected SectionOutOfBounds for section 3, got {other:?}"),
}
}
#[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 data = build_minimal_ir(
&["a", "b", "c"],
&[],
&opcodes,
&[(1, 0x01, 0x01, 99, 0, &[])],
);
let err = IrModule::parse(&data).unwrap_err();
assert_eq!(err, IrError::StringIndexOutOfBounds { index: 99, len: 3 });
}
}