1use crate::format::{
7 IrError, IrHeader, IslandEntry, IslandTrigger, Opcode, PropsMode, SectionTable, SlotEntry,
8 SlotSource, SlotType, HEADER_SIZE, SECTION_TABLE_SIZE,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct StringTable {
20 strings: Vec<String>,
21}
22
23impl StringTable {
24 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
26 if data.len() < 4 {
27 return Err(IrError::BufferTooShort {
28 expected: 4,
29 actual: data.len(),
30 });
31 }
32
33 let count = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
34 let max_entries = (data.len() - 4) / 2;
47 if count > max_entries {
48 return Err(IrError::BufferTooShort {
49 expected: 4usize.saturating_add(count.saturating_mul(2)),
52 actual: data.len(),
53 });
54 }
55 let mut offset = 4;
56 let mut strings = Vec::with_capacity(count);
60
61 for _ in 0..count {
62 if offset + 2 > data.len() {
64 return Err(IrError::BufferTooShort {
65 expected: offset + 2,
66 actual: data.len(),
67 });
68 }
69
70 let str_len = u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap()) as usize;
71 offset += 2;
72
73 if offset + str_len > data.len() {
74 return Err(IrError::BufferTooShort {
75 expected: offset + str_len,
76 actual: data.len(),
77 });
78 }
79
80 let s = std::str::from_utf8(&data[offset..offset + str_len])
81 .map_err(|e| IrError::InvalidUtf8(e.to_string()))?;
82 strings.push(s.to_owned());
83 offset += str_len;
84 }
85
86 Ok(StringTable { strings })
87 }
88
89 pub fn get(&self, idx: u32) -> Result<&str, IrError> {
91 self.strings
92 .get(idx as usize)
93 .map(|s| s.as_str())
94 .ok_or(IrError::StringIndexOutOfBounds {
95 index: idx,
96 len: self.strings.len(),
97 })
98 }
99
100 pub fn len(&self) -> usize {
102 self.strings.len()
103 }
104
105 pub fn is_empty(&self) -> bool {
107 self.strings.is_empty()
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct SlotTable {
122 slots: Vec<SlotEntry>,
123}
124
125const SLOT_ENTRY_MIN_SIZE: usize = 10;
128
129impl SlotTable {
130 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
132 if data.len() < 2 {
133 return Err(IrError::BufferTooShort {
134 expected: 2,
135 actual: data.len(),
136 });
137 }
138
139 let count = u16::from_le_bytes(data[0..2].try_into().unwrap()) as usize;
140 let max_entries = (data.len() - 2) / SLOT_ENTRY_MIN_SIZE;
144 if count > max_entries {
145 return Err(IrError::BufferTooShort {
146 expected: 2usize.saturating_add(count.saturating_mul(SLOT_ENTRY_MIN_SIZE)),
147 actual: data.len(),
148 });
149 }
150 let mut slots = Vec::with_capacity(count);
151 let mut offset = 2;
152
153 for _ in 0..count {
154 if offset + SLOT_ENTRY_MIN_SIZE > data.len() {
156 return Err(IrError::BufferTooShort {
157 expected: offset + SLOT_ENTRY_MIN_SIZE,
158 actual: data.len(),
159 });
160 }
161
162 let slot_id = u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap());
163 let name_str_idx = u32::from_le_bytes(data[offset + 2..offset + 6].try_into().unwrap());
164 let type_hint = SlotType::from_byte(data[offset + 6])?;
165 let source = SlotSource::from_byte(data[offset + 7])?;
166 let default_len =
167 u16::from_le_bytes(data[offset + 8..offset + 10].try_into().unwrap()) as usize;
168 offset += SLOT_ENTRY_MIN_SIZE;
169
170 if offset + default_len > data.len() {
172 return Err(IrError::BufferTooShort {
173 expected: offset + default_len,
174 actual: data.len(),
175 });
176 }
177 let default_bytes = data[offset..offset + default_len].to_vec();
178 offset += default_len;
179
180 slots.push(SlotEntry {
181 slot_id,
182 name_str_idx,
183 type_hint,
184 source,
185 default_bytes,
186 });
187 }
188
189 Ok(SlotTable { slots })
190 }
191
192 pub fn len(&self) -> usize {
194 self.slots.len()
195 }
196
197 pub fn is_empty(&self) -> bool {
199 self.slots.is_empty()
200 }
201
202 pub fn entries(&self) -> &[SlotEntry] {
204 &self.slots
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct IslandTableParsed {
218 islands: Vec<IslandEntry>,
219}
220
221const ISLAND_ENTRY_MIN_SIZE: usize = 14;
224
225impl IslandTableParsed {
226 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
228 if data.len() < 2 {
229 return Err(IrError::BufferTooShort {
230 expected: 2,
231 actual: data.len(),
232 });
233 }
234
235 let count = u16::from_le_bytes(data[0..2].try_into().unwrap()) as usize;
236 let max_entries = (data.len() - 2) / ISLAND_ENTRY_MIN_SIZE;
239 if count > max_entries {
240 return Err(IrError::BufferTooShort {
241 expected: 2usize.saturating_add(count.saturating_mul(ISLAND_ENTRY_MIN_SIZE)),
242 actual: data.len(),
243 });
244 }
245 let mut islands = Vec::with_capacity(count);
246 let mut offset = 2;
247
248 for _ in 0..count {
249 if offset + ISLAND_ENTRY_MIN_SIZE > data.len() {
251 return Err(IrError::BufferTooShort {
252 expected: offset + ISLAND_ENTRY_MIN_SIZE,
253 actual: data.len(),
254 });
255 }
256
257 let id = u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap());
258 let trigger = IslandTrigger::from_byte(data[offset + 2])?;
259 let props_mode = PropsMode::from_byte(data[offset + 3])?;
260 let name_str_idx = u32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap());
261 let byte_offset = u32::from_le_bytes(data[offset + 8..offset + 12].try_into().unwrap());
262 let slot_count =
263 u16::from_le_bytes(data[offset + 12..offset + 14].try_into().unwrap()) as usize;
264 offset += ISLAND_ENTRY_MIN_SIZE;
265
266 let needed = slot_count * 2;
268 if offset + needed > data.len() {
269 return Err(IrError::BufferTooShort {
270 expected: offset + needed,
271 actual: data.len(),
272 });
273 }
274 let mut slot_ids = Vec::with_capacity(slot_count);
275 for _ in 0..slot_count {
276 slot_ids.push(u16::from_le_bytes(
277 data[offset..offset + 2].try_into().unwrap(),
278 ));
279 offset += 2;
280 }
281
282 islands.push(IslandEntry {
283 id,
284 trigger,
285 props_mode,
286 name_str_idx,
287 byte_offset,
288 slot_ids,
289 });
290 }
291
292 Ok(IslandTableParsed { islands })
293 }
294
295 pub fn len(&self) -> usize {
297 self.islands.len()
298 }
299
300 pub fn is_empty(&self) -> bool {
302 self.islands.is_empty()
303 }
304
305 pub fn entries(&self) -> &[IslandEntry] {
307 &self.islands
308 }
309}
310
311const NO_INDEX: u32 = u32::MAX;
317
318#[derive(Debug, Clone)]
323#[non_exhaustive]
324pub struct IrModule {
325 pub header: IrHeader,
327 pub strings: StringTable,
329 pub slots: SlotTable,
331 pub opcodes: Vec<u8>,
333 pub islands: IslandTableParsed,
335 slot_index: Vec<u32>,
338 island_index: Vec<u32>,
342}
343
344impl IrModule {
345 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
347 let header = IrHeader::parse(data)?;
349
350 if data.len() < HEADER_SIZE + SECTION_TABLE_SIZE {
352 return Err(IrError::BufferTooShort {
353 expected: HEADER_SIZE + SECTION_TABLE_SIZE,
354 actual: data.len(),
355 });
356 }
357 let section_table = SectionTable::parse(&data[HEADER_SIZE..])?;
358
359 section_table.validate(data.len())?;
361
362 let sec_bytecode = §ion_table.sections[0];
364 let sec_strings = §ion_table.sections[1];
365 let sec_slots = §ion_table.sections[2];
366 let sec_islands = §ion_table.sections[3];
367
368 let string_data = &data[sec_strings.offset as usize
370 ..(sec_strings.offset as usize + sec_strings.size as usize)];
371 let strings = StringTable::parse(string_data)?;
372
373 let slot_data =
375 &data[sec_slots.offset as usize..(sec_slots.offset as usize + sec_slots.size as usize)];
376 let slots = SlotTable::parse(slot_data)?;
377
378 let opcodes = data[sec_bytecode.offset as usize
380 ..(sec_bytecode.offset as usize + sec_bytecode.size as usize)]
381 .to_vec();
382
383 let island_data = &data[sec_islands.offset as usize
385 ..(sec_islands.offset as usize + sec_islands.size as usize)];
386 let islands = IslandTableParsed::parse(island_data)?;
387
388 let slot_index = build_id_index(slots.entries().iter().map(|e| e.slot_id));
393 let island_index = build_id_index(islands.entries().iter().map(|e| e.id));
394
395 let module = IrModule {
396 header,
397 strings,
398 slots,
399 opcodes,
400 islands,
401 slot_index,
402 island_index,
403 };
404
405 module.validate()?;
407
408 Ok(module)
409 }
410
411 pub fn slot_entry(&self, slot_id: u16) -> Option<&SlotEntry> {
413 match self.slot_index.get(slot_id as usize) {
414 Some(&idx) if idx != NO_INDEX => self.slots.entries().get(idx as usize),
415 _ => None,
416 }
417 }
418
419 pub fn island_entry(&self, island_id: u16) -> Option<&IslandEntry> {
421 match self.island_index.get(island_id as usize) {
422 Some(&idx) if idx != NO_INDEX => self.islands.entries().get(idx as usize),
423 _ => None,
424 }
425 }
426
427 pub fn validate(&self) -> Result<(), IrError> {
433 let str_count = self.strings.len();
434
435 let mut seen_slot = vec![false; self.slot_index.len()];
439 for slot in self.slots.entries() {
440 if slot.name_str_idx as usize >= str_count {
441 return Err(IrError::StringIndexOutOfBounds {
442 index: slot.name_str_idx,
443 len: str_count,
444 });
445 }
446 let idx = slot.slot_id as usize;
447 if seen_slot[idx] {
448 return Err(IrError::DuplicateSlotId(slot.slot_id));
449 }
450 seen_slot[idx] = true;
451 }
452
453 let mut seen_island = vec![false; self.island_index.len()];
457 for island in self.islands.entries() {
458 if island.name_str_idx as usize >= str_count {
459 return Err(IrError::StringIndexOutOfBounds {
460 index: island.name_str_idx,
461 len: str_count,
462 });
463 }
464 let idx = island.id as usize;
465 if seen_island[idx] {
466 return Err(IrError::DuplicateIslandId(island.id));
467 }
468 seen_island[idx] = true;
469
470 for &slot_id in &island.slot_ids {
471 if self.slot_entry(slot_id).is_none() {
472 return Err(IrError::UnknownSlotId(slot_id));
473 }
474 }
475
476 let off = island.byte_offset as usize;
480 let id_matches = self.opcodes.get(off) == Some(&(Opcode::IslandStart as u8))
481 && crate::walker::read_u16(&self.opcodes, off + 1) == Ok(island.id);
482 if !id_matches {
483 return Err(IrError::IslandOffsetMismatch {
484 island_id: island.id,
485 byte_offset: island.byte_offset,
486 });
487 }
488 }
489
490 self.scan_region(0, self.opcodes.len(), 0)?;
492
493 Ok(())
494 }
495
496 fn scan_region(&self, start: usize, end: usize, depth: usize) -> Result<(), IrError> {
514 if depth > MAX_VALIDATION_DEPTH {
515 return Err(IrError::RecursionLimitExceeded);
516 }
517 let ops = &self.opcodes;
518 let mut pos = start;
519 let mut fallback_stack: Vec<usize> = Vec::new();
522
523 while pos < end {
524 let op_start = pos;
525 let opcode = Opcode::from_byte(ops[pos])?;
526 pos += 1;
527
528 match opcode {
529 Opcode::OpenTag | Opcode::VoidTag => {
530 let attr_count = read_u16_at(ops, pos + 4, end)? as usize;
531 pos = bounded(pos, 6 + attr_count * 8, end, op_start)?;
532 }
533 Opcode::CloseTag
534 | Opcode::Text
535 | Opcode::Comment
536 | Opcode::ListItemKey
537 | Opcode::DynText => {
538 if opcode == Opcode::DynText {
539 self.check_slot(read_u16_at(ops, pos, end)?)?;
540 }
541 pos = bounded(pos, 4, end, op_start)?;
542 }
543 Opcode::DynAttr => {
544 self.check_slot(read_u16_at(ops, pos + 4, end)?)?;
545 pos = bounded(pos, 6, end, op_start)?;
546 }
547 Opcode::IslandStart => {
548 let island_id = read_u16_at(ops, pos, end)?;
553 if self.island_entry(island_id).is_none() {
554 return Err(IrError::UnknownIslandId(island_id));
555 }
556 pos = bounded(pos, 2, end, op_start)?;
557 }
558 Opcode::IslandEnd => {
559 pos = bounded(pos, 2, end, op_start)?;
560 }
561 Opcode::ShowElse => {
562 }
564 Opcode::Preload => {
565 pos = bounded(pos, 5, end, op_start)?;
566 }
567 Opcode::Prop => {
568 self.check_slot(read_u16_at(ops, pos, end)?)?;
569 self.check_slot(read_u16_at(ops, pos + 6, end)?)?;
570 pos = bounded(pos, 8, end, op_start)?;
571 }
572 Opcode::ShowIf => {
573 self.check_slot(read_u16_at(ops, pos, end)?)?;
574 let then_len = read_u32_at(ops, pos + 2, end)? as usize;
575 let else_len = read_u32_at(ops, pos + 6, end)? as usize;
576 let then_start = bounded(pos, 10, end, op_start)?;
577 let then_end = bounded(then_start, then_len, end, op_start)?;
578 if then_end >= end || ops[then_end] != Opcode::ShowElse as u8 {
582 return Err(IrError::MalformedControlFlow { pos: then_end });
583 }
584 let else_start = then_end + 1;
585 let else_end = bounded(else_start, else_len, end, op_start)?;
586 self.scan_region(then_start, then_end, depth + 1)?;
587 self.scan_region(else_start, else_end, depth + 1)?;
588 pos = else_end;
589 }
590 Opcode::Switch => {
591 self.check_slot(read_u16_at(ops, pos, end)?)?;
592 let case_count = read_u16_at(ops, pos + 2, end)? as usize;
593 let mut header_pos = bounded(pos, 4, end, op_start)?;
594 let mut lens =
595 Vec::with_capacity(case_count.min(end.saturating_sub(header_pos) / 8));
596 for _ in 0..case_count {
597 let body_len = read_u32_at(ops, header_pos + 4, end)? as usize;
598 lens.push(body_len);
599 header_pos = bounded(header_pos, 8, end, op_start)?;
600 }
601 let mut body_pos = header_pos;
602 for body_len in lens {
603 let case_end = bounded(body_pos, body_len, end, op_start)?;
604 self.scan_region(body_pos, case_end, depth + 1)?;
605 body_pos = case_end;
606 }
607 pos = body_pos;
608 }
609 Opcode::List => {
610 self.check_slot(read_u16_at(ops, pos, end)?)?;
611 self.check_slot(read_u16_at(ops, pos + 2, end)?)?;
612 let body_len = read_u32_at(ops, pos + 4, end)? as usize;
613 let body_start = bounded(pos, 8, end, op_start)?;
614 let body_end = bounded(body_start, body_len, end, op_start)?;
615 self.scan_region(body_start, body_end, depth + 1)?;
616 pos = body_end;
617 }
618 Opcode::TryStart => {
619 let fallback_len = read_u32_at(ops, pos, end)? as usize;
620 pos = bounded(pos, 4, end, op_start)?;
621 fallback_stack.push(fallback_len);
622 }
623 Opcode::Fallback => {
624 match fallback_stack.pop() {
633 Some(skip) => pos = bounded(pos, skip, end, op_start)?,
634 None => return Err(IrError::MalformedControlFlow { pos: op_start }),
635 }
636 }
637 }
638 }
639
640 if let Some(_unmatched) = fallback_stack.pop() {
645 return Err(IrError::MalformedControlFlow { pos: end });
646 }
647
648 Ok(())
649 }
650
651 fn check_slot(&self, slot_id: u16) -> Result<(), IrError> {
656 if self.slot_entry(slot_id).is_none() {
657 return Err(IrError::UnknownSlotId(slot_id));
658 }
659 Ok(())
660 }
661
662 pub fn slot_id_by_name(&self, name: &str) -> Option<u16> {
676 for slot in self.slots.entries() {
677 if let Ok(slot_name) = self.strings.get(slot.name_str_idx) {
678 if slot_name == name {
679 return Some(slot.slot_id);
680 }
681 }
682 }
683 None
684 }
685}
686
687const MAX_VALIDATION_DEPTH: usize = 64;
690
691fn build_id_index(ids: impl Iterator<Item = u16> + Clone) -> Vec<u32> {
694 let max_id = ids.clone().max();
695 let Some(max_id) = max_id else {
696 return Vec::new();
697 };
698 let mut index = vec![NO_INDEX; max_id as usize + 1];
702 for (i, id) in ids.enumerate() {
703 let entry = &mut index[id as usize];
704 if *entry == NO_INDEX {
705 *entry = i as u32;
706 }
707 }
708 index
709}
710
711fn bounded(base: usize, len: usize, end: usize, op_start: usize) -> Result<usize, IrError> {
716 base.checked_add(len)
717 .filter(|e| *e <= end)
718 .ok_or(IrError::MalformedControlFlow { pos: op_start })
719}
720
721fn read_u16_at(ops: &[u8], pos: usize, end: usize) -> Result<u16, IrError> {
723 if pos.checked_add(2).map(|e| e > end).unwrap_or(true) {
724 return Err(IrError::MalformedControlFlow { pos });
725 }
726 crate::walker::read_u16(ops, pos)
727}
728
729fn read_u32_at(ops: &[u8], pos: usize, end: usize) -> Result<u32, IrError> {
731 if pos.checked_add(4).map(|e| e > end).unwrap_or(true) {
732 return Err(IrError::MalformedControlFlow { pos });
733 }
734 crate::walker::read_u32(ops, pos)
735}
736
737#[cfg(any(test, feature = "test-helpers"))]
751pub mod test_helpers {
752 use crate::format::{HEADER_SIZE, SECTION_TABLE_SIZE};
753
754 fn to_u16(n: usize) -> [u8; 2] {
761 u16::try_from(n)
762 .unwrap_or_else(|_| panic!("{n} does not fit the u16 field this encoder writes"))
763 .to_le_bytes()
764 }
765
766 fn to_u32(n: usize) -> [u8; 4] {
768 u32::try_from(n)
769 .unwrap_or_else(|_| panic!("{n} does not fit the u32 field this encoder writes"))
770 .to_le_bytes()
771 }
772
773 pub fn build_string_table(strings: &[&str]) -> Vec<u8> {
775 let mut buf = Vec::new();
776 buf.extend_from_slice(&to_u32(strings.len()));
777 for s in strings {
778 let bytes = s.as_bytes();
779 buf.extend_from_slice(&to_u16(bytes.len()));
780 buf.extend_from_slice(bytes);
781 }
782 buf
783 }
784
785 pub fn build_slot_table(entries: &[(u16, u32, u8, u8, &[u8])]) -> Vec<u8> {
788 let mut buf = Vec::new();
789 buf.extend_from_slice(&to_u16(entries.len()));
790 for &(slot_id, name_str_idx, type_hint, source, default_bytes) in entries {
791 buf.extend_from_slice(&slot_id.to_le_bytes());
792 buf.extend_from_slice(&name_str_idx.to_le_bytes());
793 buf.push(type_hint);
794 buf.push(source);
795 buf.extend_from_slice(&to_u16(default_bytes.len()));
796 buf.extend_from_slice(default_bytes);
797 }
798 buf
799 }
800
801 pub fn build_island_table(entries: &[(u16, u8, u8, u32, u32, &[u16])]) -> Vec<u8> {
805 let mut buf = Vec::new();
806 buf.extend_from_slice(&to_u16(entries.len()));
807 for &(id, trigger, props_mode, name_str_idx, byte_offset, slot_ids) in entries {
808 buf.extend_from_slice(&id.to_le_bytes());
809 buf.push(trigger);
810 buf.push(props_mode);
811 buf.extend_from_slice(&name_str_idx.to_le_bytes());
812 buf.extend_from_slice(&byte_offset.to_le_bytes());
813 buf.extend_from_slice(&to_u16(slot_ids.len()));
814 for &slot_id in slot_ids.iter() {
815 buf.extend_from_slice(&slot_id.to_le_bytes());
816 }
817 }
818 buf
819 }
820
821 pub fn encode_open_tag(str_idx: u32, attrs: &[(u32, u32)]) -> Vec<u8> {
825 let mut buf = Vec::new();
826 buf.push(0x01); buf.extend_from_slice(&str_idx.to_le_bytes());
828 buf.extend_from_slice(&to_u16(attrs.len()));
829 for &(key_idx, val_idx) in attrs {
830 buf.extend_from_slice(&key_idx.to_le_bytes());
831 buf.extend_from_slice(&val_idx.to_le_bytes());
832 }
833 buf
834 }
835
836 pub fn encode_close_tag(str_idx: u32) -> Vec<u8> {
838 let mut buf = Vec::new();
839 buf.push(0x02); buf.extend_from_slice(&str_idx.to_le_bytes());
841 buf
842 }
843
844 pub fn encode_void_tag(str_idx: u32, attrs: &[(u32, u32)]) -> Vec<u8> {
846 let mut buf = Vec::new();
847 buf.push(0x03); buf.extend_from_slice(&str_idx.to_le_bytes());
849 buf.extend_from_slice(&to_u16(attrs.len()));
850 for &(key_idx, val_idx) in attrs {
851 buf.extend_from_slice(&key_idx.to_le_bytes());
852 buf.extend_from_slice(&val_idx.to_le_bytes());
853 }
854 buf
855 }
856
857 pub fn encode_text(str_idx: u32) -> Vec<u8> {
859 let mut buf = Vec::new();
860 buf.push(0x04); buf.extend_from_slice(&str_idx.to_le_bytes());
862 buf
863 }
864
865 pub fn encode_show_if(slot_id: u16, then_ops: &[u8], else_ops: &[u8]) -> Vec<u8> {
873 let mut buf = Vec::new();
874 buf.push(0x07); buf.extend_from_slice(&slot_id.to_le_bytes());
876 buf.extend_from_slice(&to_u32(then_ops.len()));
877 buf.extend_from_slice(&to_u32(else_ops.len()));
878 buf.extend_from_slice(then_ops); buf.push(0x08); buf.extend_from_slice(else_ops); buf
882 }
883
884 pub fn encode_list(slot_id: u16, item_slot_id: u16, body_ops: &[u8]) -> Vec<u8> {
886 let mut buf = Vec::new();
887 buf.push(0x0A); buf.extend_from_slice(&slot_id.to_le_bytes());
889 buf.extend_from_slice(&item_slot_id.to_le_bytes());
890 buf.extend_from_slice(&to_u32(body_ops.len()));
891 buf.extend_from_slice(body_ops);
892 buf
893 }
894
895 pub fn encode_switch(slot_id: u16, cases: &[(u32, &[u8])]) -> Vec<u8> {
906 let mut buf = Vec::new();
907 buf.push(0x09); buf.extend_from_slice(&slot_id.to_le_bytes());
909 buf.extend_from_slice(&to_u16(cases.len()));
910 for (val_str_idx, body) in cases {
912 buf.extend_from_slice(&val_str_idx.to_le_bytes());
913 buf.extend_from_slice(&to_u32(body.len()));
914 }
915 for (_, body) in cases {
917 buf.extend_from_slice(body);
918 }
919 buf
920 }
921
922 pub fn encode_try(main_ops: &[u8], fallback_ops: &[u8]) -> Vec<u8> {
932 let mut buf = Vec::new();
933 buf.push(0x0D); buf.extend_from_slice(&to_u32(fallback_ops.len()));
935 buf.extend_from_slice(main_ops);
936 buf.push(0x0E); buf.extend_from_slice(fallback_ops);
938 buf
939 }
940
941 pub fn encode_preload(resource_type: u8, url_str_idx: u32) -> Vec<u8> {
943 let mut buf = Vec::new();
944 buf.push(0x0F); buf.push(resource_type);
946 buf.extend_from_slice(&url_str_idx.to_le_bytes());
947 buf
948 }
949
950 pub fn build_minimal_ir(
959 strings: &[&str],
960 slots: &[(u16, u32, u8, u8, &[u8])],
961 opcodes: &[u8],
962 islands: &[(u16, u8, u8, u32, u32, &[u16])],
963 ) -> Vec<u8> {
964 let string_section = build_string_table(strings);
965 let slot_section = build_slot_table(slots);
966 let island_section = build_island_table(islands);
967
968 let data_start = HEADER_SIZE + SECTION_TABLE_SIZE;
970
971 let bytecode_offset = data_start;
973 let bytecode_size = opcodes.len();
974
975 let string_offset = bytecode_offset + bytecode_size;
976 let string_size = string_section.len();
977
978 let slot_offset = string_offset + string_size;
979 let slot_size = slot_section.len();
980
981 let island_offset = slot_offset + slot_size;
982 let island_size = island_section.len();
983
984 let mut buf = Vec::new();
986 buf.extend_from_slice(b"FMIR");
987 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(&to_u32(bytecode_offset));
993 buf.extend_from_slice(&to_u32(bytecode_size));
994 buf.extend_from_slice(&to_u32(string_offset));
995 buf.extend_from_slice(&to_u32(string_size));
996 buf.extend_from_slice(&to_u32(slot_offset));
997 buf.extend_from_slice(&to_u32(slot_size));
998 buf.extend_from_slice(&to_u32(island_offset));
999 buf.extend_from_slice(&to_u32(island_size));
1000
1001 buf.extend_from_slice(opcodes);
1003 buf.extend_from_slice(&string_section);
1004 buf.extend_from_slice(&slot_section);
1005 buf.extend_from_slice(&island_section);
1006
1007 buf
1008 }
1009}
1010
1011#[cfg(test)]
1016mod tests {
1017 use super::test_helpers::*;
1018 use super::*;
1019 use crate::format::{IrError, IslandTrigger, PropsMode, SlotSource, SlotType};
1020
1021 #[test]
1024 fn parse_string_table() {
1025 let data = build_string_table(&["div", "class", "container"]);
1026 let table = StringTable::parse(&data).unwrap();
1027
1028 assert_eq!(table.len(), 3);
1029 assert_eq!(table.get(0).unwrap(), "div");
1030 assert_eq!(table.get(1).unwrap(), "class");
1031 assert_eq!(table.get(2).unwrap(), "container");
1032
1033 let err = table.get(3).unwrap_err();
1035 assert_eq!(err, IrError::StringIndexOutOfBounds { index: 3, len: 3 });
1036 }
1037
1038 #[test]
1039 fn parse_string_table_empty() {
1040 let data = build_string_table(&[]);
1041 let table = StringTable::parse(&data).unwrap();
1042 assert_eq!(table.len(), 0);
1043 }
1044
1045 #[test]
1046 fn parse_string_table_unicode() {
1047 let data = build_string_table(&["héllo"]);
1048 let table = StringTable::parse(&data).unwrap();
1049
1050 assert_eq!(table.len(), 1);
1051 assert_eq!(table.get(0).unwrap(), "héllo");
1052 }
1053
1054 #[test]
1055 fn parse_string_table_truncated() {
1056 let mut data = Vec::new();
1058 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();
1064 match err {
1065 IrError::BufferTooShort { .. } => {} other => panic!("expected BufferTooShort, got {other:?}"),
1067 }
1068 }
1069
1070 #[test]
1073 fn parse_slot_table() {
1074 let data = build_slot_table(&[
1075 (1, 0, 0x01, 0x00, &[]), (2, 1, 0x03, 0x01, &[0x42]), ]);
1078 let table = SlotTable::parse(&data).unwrap();
1079
1080 assert_eq!(table.len(), 2);
1081
1082 let entries = table.entries();
1083 assert_eq!(entries[0].slot_id, 1);
1084 assert_eq!(entries[0].name_str_idx, 0);
1085 assert_eq!(entries[0].type_hint, SlotType::Text);
1086 assert_eq!(entries[0].source, SlotSource::Server);
1087 assert_eq!(entries[0].default_bytes, Vec::<u8>::new());
1088
1089 assert_eq!(entries[1].slot_id, 2);
1090 assert_eq!(entries[1].name_str_idx, 1);
1091 assert_eq!(entries[1].type_hint, SlotType::Number);
1092 assert_eq!(entries[1].source, SlotSource::Client);
1093 assert_eq!(entries[1].default_bytes, vec![0x42]);
1094 }
1095
1096 #[test]
1097 fn parse_slot_table_empty() {
1098 let data = build_slot_table(&[]);
1099 let table = SlotTable::parse(&data).unwrap();
1100 assert_eq!(table.len(), 0);
1101 }
1102
1103 #[test]
1106 fn parse_island_table() {
1107 let data = build_island_table(&[
1108 (1, 0x02, 0x01, 5, 0, &[]), ]);
1110 let table = IslandTableParsed::parse(&data).unwrap();
1111
1112 assert_eq!(table.len(), 1);
1113
1114 let entry = &table.entries()[0];
1115 assert_eq!(entry.id, 1);
1116 assert_eq!(entry.trigger, IslandTrigger::Visible);
1117 assert_eq!(entry.props_mode, PropsMode::Inline);
1118 assert_eq!(entry.name_str_idx, 5);
1119 assert_eq!(entry.slot_ids, Vec::<u16>::new());
1120 }
1121
1122 #[test]
1123 fn parse_island_table_with_slot_ids() {
1124 let data = build_island_table(&[
1125 (0, 0x01, 0x01, 0, 0, &[0, 1]), ]);
1127 let table = IslandTableParsed::parse(&data).unwrap();
1128 assert_eq!(table.len(), 1);
1129 let entry = &table.entries()[0];
1130 assert_eq!(entry.id, 0);
1131 assert_eq!(entry.trigger, IslandTrigger::Load);
1132 assert_eq!(entry.props_mode, PropsMode::Inline);
1133 assert_eq!(entry.slot_ids, vec![0, 1]);
1134 }
1135
1136 #[test]
1137 fn parse_island_table_empty() {
1138 let data = build_island_table(&[]);
1139 let table = IslandTableParsed::parse(&data).unwrap();
1140 assert_eq!(table.len(), 0);
1141 }
1142
1143 #[test]
1146 fn parse_minimal_ir_file() {
1147 let mut opcodes = Vec::new();
1149 opcodes.extend_from_slice(&encode_open_tag(0, &[]));
1150 opcodes.extend_from_slice(&encode_close_tag(0));
1151
1152 let data = build_minimal_ir(&["div"], &[], &opcodes, &[]);
1153 let module = IrModule::parse(&data).unwrap();
1154
1155 assert_eq!(module.header.version, 2);
1156 assert_eq!(module.strings.get(0).unwrap(), "div");
1157 assert_eq!(module.strings.len(), 1);
1158 assert_eq!(module.slots.len(), 0);
1159 assert_eq!(module.islands.len(), 0);
1160 assert_eq!(module.opcodes.len(), opcodes.len());
1161 }
1162
1163 #[test]
1164 fn parse_ir_with_slots() {
1165 let opcodes = encode_text(0);
1166 let data = build_minimal_ir(
1167 &["greeting", "count", "Hello"],
1168 &[
1169 (1, 0, 0x01, 0x00, &[]), (2, 1, 0x03, 0x00, &[]), ],
1172 &opcodes,
1173 &[],
1174 );
1175 let module = IrModule::parse(&data).unwrap();
1176
1177 assert_eq!(module.slots.len(), 2);
1178 let entries = module.slots.entries();
1179 assert_eq!(entries[0].slot_id, 1);
1180 assert_eq!(entries[0].name_str_idx, 0);
1181 assert_eq!(entries[0].type_hint, SlotType::Text);
1182 assert_eq!(entries[1].slot_id, 2);
1183 assert_eq!(entries[1].name_str_idx, 1);
1184 assert_eq!(entries[1].type_hint, SlotType::Number);
1185 }
1186
1187 #[test]
1188 fn parse_ir_with_islands() {
1189 let mut opcodes = vec![0x0B]; opcodes.extend_from_slice(&1u16.to_le_bytes());
1194 opcodes.extend_from_slice(&encode_text(1));
1195 opcodes.push(0x0C); opcodes.extend_from_slice(&1u16.to_le_bytes());
1197 let data = build_minimal_ir(
1198 &["Counter", "Hello"],
1199 &[],
1200 &opcodes,
1201 &[
1202 (1, 0x01, 0x01, 0, 0, &[]), ],
1204 );
1205 let module = IrModule::parse(&data).unwrap();
1206
1207 assert_eq!(module.islands.len(), 1);
1208 let entry = &module.islands.entries()[0];
1209 assert_eq!(entry.id, 1);
1210 assert_eq!(entry.trigger, IslandTrigger::Load);
1211 assert_eq!(entry.props_mode, PropsMode::Inline);
1212 assert_eq!(entry.name_str_idx, 0);
1213 assert_eq!(module.strings.get(entry.name_str_idx).unwrap(), "Counter");
1214 }
1215
1216 #[test]
1217 fn parse_ir_rejects_truncated() {
1218 let data = b"FMIR\x02\x00";
1220 let err = IrModule::parse(data).unwrap_err();
1221 match err {
1222 IrError::BufferTooShort {
1223 expected: 16,
1224 actual: 6,
1225 } => {}
1226 other => panic!("expected BufferTooShort(16, 6), got {other:?}"),
1227 }
1228 }
1229
1230 #[test]
1231 fn parse_ir_rejects_bad_section_bounds() {
1232 let opcodes = encode_text(0);
1234 let mut data = build_minimal_ir(&["x"], &[], &opcodes, &[]);
1235
1236 let big_size: u32 = 99999;
1240 data[44..48].copy_from_slice(&big_size.to_le_bytes());
1241
1242 let err = IrModule::parse(&data).unwrap_err();
1243 match err {
1244 IrError::SectionOutOfBounds { section: 3, .. } => {}
1245 other => panic!("expected SectionOutOfBounds for section 3, got {other:?}"),
1246 }
1247 }
1248
1249 #[test]
1250 fn validate_catches_bad_slot_str_idx() {
1251 let opcodes = encode_text(0);
1253 let data = build_minimal_ir(
1254 &["a", "b", "c"],
1255 &[(1, 99, 0x01, 0x00, &[])], &opcodes,
1257 &[],
1258 );
1259 let err = IrModule::parse(&data).unwrap_err();
1260 assert_eq!(err, IrError::StringIndexOutOfBounds { index: 99, len: 3 });
1261 }
1262
1263 #[test]
1264 fn validate_catches_bad_island_str_idx() {
1265 let opcodes = encode_text(0);
1267 let mut island_opcodes = vec![0x0B];
1268 island_opcodes.extend_from_slice(&1u16.to_le_bytes());
1269 island_opcodes.extend_from_slice(&opcodes);
1270 island_opcodes.push(0x0C);
1271 island_opcodes.extend_from_slice(&1u16.to_le_bytes());
1272 let data = build_minimal_ir(
1273 &["a", "b", "c"],
1274 &[],
1275 &island_opcodes,
1276 &[(1, 0x01, 0x01, 99, 0, &[])],
1277 );
1278 let err = IrModule::parse(&data).unwrap_err();
1279 assert_eq!(err, IrError::StringIndexOutOfBounds { index: 99, len: 3 });
1280 }
1281
1282 #[test]
1283 fn duplicate_slot_names_resolve_first_wins_everywhere() {
1284 let opcodes = encode_text(0);
1289 let data = build_minimal_ir(
1290 &["dup"],
1291 &[(0, 0, 0x01, 0x00, &[]), (1, 0, 0x01, 0x00, &[])],
1292 &opcodes,
1293 &[],
1294 );
1295 let module = IrModule::parse(&data).unwrap();
1296
1297 assert_eq!(module.slot_id_by_name("dup"), Some(0));
1298
1299 let slots = crate::slot::SlotData::from_json(r#"{"dup":"x"}"#, &module).unwrap();
1300 assert_eq!(slots.get(0).to_text(), "x", "the first slot must be filled");
1301 assert_eq!(slots.get(1).to_text(), "", "the second must stay untouched");
1302 }
1303
1304 #[test]
1305 fn validate_rejects_list_item_slot_not_in_table() {
1306 let body = {
1310 let mut b = vec![0x05]; b.extend_from_slice(&7u16.to_le_bytes());
1312 b.extend_from_slice(&0u16.to_le_bytes());
1313 b
1314 };
1315 let opcodes = encode_list(0, 7, &body);
1316 let data = build_minimal_ir(&["items"], &[(0, 0, 0x04, 0x00, &[])], &opcodes, &[]);
1317 assert_eq!(
1318 IrModule::parse(&data).unwrap_err(),
1319 IrError::UnknownSlotId(7)
1320 );
1321 }
1322
1323 #[test]
1324 fn validate_rejects_island_start_missing_from_table() {
1325 let mut opcodes = vec![0x0B]; opcodes.extend_from_slice(&3u16.to_le_bytes());
1330 opcodes.extend_from_slice(&encode_text(0));
1331 opcodes.push(0x0C); opcodes.extend_from_slice(&3u16.to_le_bytes());
1333
1334 let data = build_minimal_ir(&["hi"], &[], &opcodes, &[]);
1335 assert_eq!(
1336 IrModule::parse(&data).unwrap_err(),
1337 IrError::UnknownIslandId(3)
1338 );
1339 }
1340
1341 #[test]
1344 fn string_table_rejects_count_the_section_cannot_hold() {
1345 let mut data = Vec::new();
1350 data.extend_from_slice(&1_000_000u32.to_le_bytes()); data.extend_from_slice(&[0u8; 16]); let err = StringTable::parse(&data).unwrap_err();
1353 assert!(matches!(err, IrError::BufferTooShort { .. }));
1354 }
1355
1356 #[test]
1357 fn slot_table_rejects_count_the_section_cannot_hold() {
1358 let data = 65535u16.to_le_bytes().to_vec();
1360 let err = SlotTable::parse(&data).unwrap_err();
1361 assert!(matches!(err, IrError::BufferTooShort { .. }));
1362 }
1363
1364 #[test]
1365 fn island_table_rejects_count_the_section_cannot_hold() {
1366 let data = 65535u16.to_le_bytes().to_vec();
1367 let err = IslandTableParsed::parse(&data).unwrap_err();
1368 assert!(matches!(err, IrError::BufferTooShort { .. }));
1369 }
1370
1371 #[test]
1374 fn validate_rejects_duplicate_slot_ids() {
1375 let opcodes = encode_text(0);
1376 let data = build_minimal_ir(
1377 &["a", "b"],
1378 &[(0, 0, 0x01, 0x00, &[]), (0, 1, 0x01, 0x00, &[])],
1379 &opcodes,
1380 &[],
1381 );
1382 assert_eq!(
1383 IrModule::parse(&data).unwrap_err(),
1384 IrError::DuplicateSlotId(0)
1385 );
1386 }
1387
1388 #[test]
1389 fn validate_rejects_duplicate_island_ids() {
1390 let mut opcodes = vec![0x0B];
1391 opcodes.extend_from_slice(&0u16.to_le_bytes());
1392 opcodes.push(0x0C);
1393 opcodes.extend_from_slice(&0u16.to_le_bytes());
1394 let data = build_minimal_ir(
1395 &["Comp"],
1396 &[],
1397 &opcodes,
1398 &[(0, 0x01, 0x01, 0, 0, &[]), (0, 0x01, 0x01, 0, 0, &[])],
1399 );
1400 assert_eq!(
1401 IrModule::parse(&data).unwrap_err(),
1402 IrError::DuplicateIslandId(0)
1403 );
1404 }
1405
1406 #[test]
1407 fn validate_rejects_island_slot_id_absent_from_slot_table() {
1408 let mut opcodes = vec![0x0B];
1413 opcodes.extend_from_slice(&0u16.to_le_bytes());
1414 opcodes.push(0x0C);
1415 opcodes.extend_from_slice(&0u16.to_le_bytes());
1416 let data = build_minimal_ir(&["Comp"], &[], &opcodes, &[(0, 0x01, 0x01, 0, 0, &[7])]);
1417 assert_eq!(
1418 IrModule::parse(&data).unwrap_err(),
1419 IrError::UnknownSlotId(7)
1420 );
1421 }
1422
1423 #[test]
1424 fn validate_rejects_island_offset_landing_mid_operand() {
1425 let mut opcodes = vec![0x0B];
1431 opcodes.extend_from_slice(&1u16.to_le_bytes());
1432 opcodes.push(0x04);
1434 opcodes.extend_from_slice(&0x0Bu32.to_le_bytes());
1435 opcodes.push(0x0C);
1436 opcodes.extend_from_slice(&1u16.to_le_bytes());
1437
1438 let data = build_minimal_ir(&["Comp"], &[], &opcodes, &[(1, 0x01, 0x01, 0, 4, &[])]);
1439 assert_eq!(
1440 IrModule::parse(&data).unwrap_err(),
1441 IrError::IslandOffsetMismatch {
1442 island_id: 1,
1443 byte_offset: 4
1444 }
1445 );
1446 }
1447
1448 #[test]
1449 fn validate_rejects_opcode_slot_id_absent_from_slot_table() {
1450 let mut opcodes = vec![0x05];
1452 opcodes.extend_from_slice(&9u16.to_le_bytes());
1453 opcodes.extend_from_slice(&0u16.to_le_bytes());
1454 let data = build_minimal_ir(&["a"], &[], &opcodes, &[]);
1455 assert_eq!(
1456 IrModule::parse(&data).unwrap_err(),
1457 IrError::UnknownSlotId(9)
1458 );
1459 }
1460
1461 #[test]
1462 fn slot_and_island_lookups_are_indexed() {
1463 let mut opcodes = vec![0x0B];
1464 opcodes.extend_from_slice(&3u16.to_le_bytes());
1465 opcodes.push(0x0C);
1466 opcodes.extend_from_slice(&3u16.to_le_bytes());
1467 let data = build_minimal_ir(
1468 &["Comp", "title"],
1469 &[(4, 1, 0x01, 0x00, &[])],
1470 &opcodes,
1471 &[(3, 0x01, 0x01, 0, 0, &[4])],
1472 );
1473 let module = IrModule::parse(&data).unwrap();
1474
1475 assert_eq!(module.slot_entry(4).unwrap().slot_id, 4);
1476 assert!(module.slot_entry(0).is_none());
1477 assert_eq!(module.island_entry(3).unwrap().id, 3);
1478 assert!(module.island_entry(0).is_none());
1479 assert!(module.slot_entry(u16::MAX).is_none());
1481 assert!(module.island_entry(u16::MAX).is_none());
1482 }
1483}