1use crate::format::{
7 le_u16, le_u32, IrError, IrHeader, IslandEntry, IslandTrigger, Opcode, PropsMode, SectionTable,
8 SlotEntry, 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 = le_u32(&data[0..4]) 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 = le_u16(&data[offset..offset + 2]) 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 = le_u16(&data[0..2]) 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 = le_u16(&data[offset..offset + 2]);
163 let name_str_idx = le_u32(&data[offset + 2..offset + 6]);
164 let type_hint = SlotType::from_byte(data[offset + 6])?;
165 let source = SlotSource::from_byte(data[offset + 7])?;
166 let default_len = le_u16(&data[offset + 8..offset + 10]) as usize;
167 offset += SLOT_ENTRY_MIN_SIZE;
168
169 if offset + default_len > data.len() {
171 return Err(IrError::BufferTooShort {
172 expected: offset + default_len,
173 actual: data.len(),
174 });
175 }
176 let default_bytes = data[offset..offset + default_len].to_vec();
177 offset += default_len;
178
179 slots.push(SlotEntry {
180 slot_id,
181 name_str_idx,
182 type_hint,
183 source,
184 default_bytes,
185 });
186 }
187
188 Ok(SlotTable { slots })
189 }
190
191 pub fn len(&self) -> usize {
193 self.slots.len()
194 }
195
196 pub fn is_empty(&self) -> bool {
198 self.slots.is_empty()
199 }
200
201 pub fn entries(&self) -> &[SlotEntry] {
203 &self.slots
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct IslandTableParsed {
217 islands: Vec<IslandEntry>,
218}
219
220const ISLAND_ENTRY_MIN_SIZE: usize = 14;
223
224impl IslandTableParsed {
225 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
227 if data.len() < 2 {
228 return Err(IrError::BufferTooShort {
229 expected: 2,
230 actual: data.len(),
231 });
232 }
233
234 let count = le_u16(&data[0..2]) as usize;
235 let max_entries = (data.len() - 2) / ISLAND_ENTRY_MIN_SIZE;
238 if count > max_entries {
239 return Err(IrError::BufferTooShort {
240 expected: 2usize.saturating_add(count.saturating_mul(ISLAND_ENTRY_MIN_SIZE)),
241 actual: data.len(),
242 });
243 }
244 let mut islands = Vec::with_capacity(count);
245 let mut offset = 2;
246
247 for _ in 0..count {
248 if offset + ISLAND_ENTRY_MIN_SIZE > data.len() {
250 return Err(IrError::BufferTooShort {
251 expected: offset + ISLAND_ENTRY_MIN_SIZE,
252 actual: data.len(),
253 });
254 }
255
256 let id = le_u16(&data[offset..offset + 2]);
257 let trigger = IslandTrigger::from_byte(data[offset + 2])?;
258 let props_mode = PropsMode::from_byte(data[offset + 3])?;
259 let name_str_idx = le_u32(&data[offset + 4..offset + 8]);
260 let byte_offset = le_u32(&data[offset + 8..offset + 12]);
261 let slot_count = le_u16(&data[offset + 12..offset + 14]) as usize;
262 offset += ISLAND_ENTRY_MIN_SIZE;
263
264 let needed = slot_count * 2;
266 if offset + needed > data.len() {
267 return Err(IrError::BufferTooShort {
268 expected: offset + needed,
269 actual: data.len(),
270 });
271 }
272 let mut slot_ids = Vec::with_capacity(slot_count);
273 for _ in 0..slot_count {
274 slot_ids.push(le_u16(&data[offset..offset + 2]));
275 offset += 2;
276 }
277
278 islands.push(IslandEntry {
279 id,
280 trigger,
281 props_mode,
282 name_str_idx,
283 byte_offset,
284 slot_ids,
285 });
286 }
287
288 Ok(IslandTableParsed { islands })
289 }
290
291 pub fn len(&self) -> usize {
293 self.islands.len()
294 }
295
296 pub fn is_empty(&self) -> bool {
298 self.islands.is_empty()
299 }
300
301 pub fn entries(&self) -> &[IslandEntry] {
303 &self.islands
304 }
305}
306
307const NO_INDEX: u32 = u32::MAX;
313
314#[derive(Debug, Clone)]
319#[non_exhaustive]
320pub struct IrModule {
321 pub header: IrHeader,
323 pub strings: StringTable,
325 pub slots: SlotTable,
327 pub opcodes: Vec<u8>,
329 pub islands: IslandTableParsed,
331 slot_index: Vec<u32>,
334 island_index: Vec<u32>,
338}
339
340impl IrModule {
341 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
343 let header = IrHeader::parse(data)?;
345
346 if data.len() < HEADER_SIZE + SECTION_TABLE_SIZE {
348 return Err(IrError::BufferTooShort {
349 expected: HEADER_SIZE + SECTION_TABLE_SIZE,
350 actual: data.len(),
351 });
352 }
353 let section_table = SectionTable::parse(&data[HEADER_SIZE..])?;
354
355 section_table.validate(data.len())?;
357
358 let sec_bytecode = §ion_table.sections[0];
360 let sec_strings = §ion_table.sections[1];
361 let sec_slots = §ion_table.sections[2];
362 let sec_islands = §ion_table.sections[3];
363
364 let string_data = &data[sec_strings.offset as usize
366 ..(sec_strings.offset as usize + sec_strings.size as usize)];
367 let strings = StringTable::parse(string_data)?;
368
369 let slot_data =
371 &data[sec_slots.offset as usize..(sec_slots.offset as usize + sec_slots.size as usize)];
372 let slots = SlotTable::parse(slot_data)?;
373
374 let opcodes = data[sec_bytecode.offset as usize
376 ..(sec_bytecode.offset as usize + sec_bytecode.size as usize)]
377 .to_vec();
378
379 let island_data = &data[sec_islands.offset as usize
381 ..(sec_islands.offset as usize + sec_islands.size as usize)];
382 let islands = IslandTableParsed::parse(island_data)?;
383
384 let slot_index = build_id_index(slots.entries().iter().map(|e| e.slot_id));
389 let island_index = build_id_index(islands.entries().iter().map(|e| e.id));
390
391 let module = IrModule {
392 header,
393 strings,
394 slots,
395 opcodes,
396 islands,
397 slot_index,
398 island_index,
399 };
400
401 module.validate()?;
403
404 Ok(module)
405 }
406
407 pub fn slot_entry(&self, slot_id: u16) -> Option<&SlotEntry> {
409 match self.slot_index.get(slot_id as usize) {
410 Some(&idx) if idx != NO_INDEX => self.slots.entries().get(idx as usize),
411 _ => None,
412 }
413 }
414
415 pub fn island_entry(&self, island_id: u16) -> Option<&IslandEntry> {
417 match self.island_index.get(island_id as usize) {
418 Some(&idx) if idx != NO_INDEX => self.islands.entries().get(idx as usize),
419 _ => None,
420 }
421 }
422
423 pub fn validate(&self) -> Result<(), IrError> {
429 let str_count = self.strings.len();
430
431 let mut seen_slot = vec![false; self.slot_index.len()];
435 for slot in self.slots.entries() {
436 if slot.name_str_idx as usize >= str_count {
437 return Err(IrError::StringIndexOutOfBounds {
438 index: slot.name_str_idx,
439 len: str_count,
440 });
441 }
442 let idx = slot.slot_id as usize;
443 if seen_slot[idx] {
444 return Err(IrError::DuplicateSlotId(slot.slot_id));
445 }
446 seen_slot[idx] = true;
447 }
448
449 let mut seen_island = vec![false; self.island_index.len()];
453 for island in self.islands.entries() {
454 if island.name_str_idx as usize >= str_count {
455 return Err(IrError::StringIndexOutOfBounds {
456 index: island.name_str_idx,
457 len: str_count,
458 });
459 }
460 let idx = island.id as usize;
461 if seen_island[idx] {
462 return Err(IrError::DuplicateIslandId(island.id));
463 }
464 seen_island[idx] = true;
465
466 for &slot_id in &island.slot_ids {
467 if self.slot_entry(slot_id).is_none() {
468 return Err(IrError::UnknownSlotId(slot_id));
469 }
470 }
471
472 let off = island.byte_offset as usize;
476 let id_matches = self.opcodes.get(off) == Some(&(Opcode::IslandStart as u8))
477 && crate::walker::read_u16(&self.opcodes, off + 1) == Ok(island.id);
478 if !id_matches {
479 return Err(IrError::IslandOffsetMismatch {
480 island_id: island.id,
481 byte_offset: island.byte_offset,
482 });
483 }
484 }
485
486 self.scan_region(0, self.opcodes.len(), 0)?;
488
489 Ok(())
490 }
491
492 fn scan_region(&self, start: usize, end: usize, depth: usize) -> Result<(), IrError> {
510 if depth > MAX_VALIDATION_DEPTH {
511 return Err(IrError::RecursionLimitExceeded);
512 }
513 let ops = &self.opcodes;
514 let mut pos = start;
515 let mut fallback_stack: Vec<usize> = Vec::new();
518
519 while pos < end {
520 let op_start = pos;
521 let opcode = Opcode::from_byte(ops[pos])?;
522 pos += 1;
523
524 match opcode {
525 Opcode::OpenTag | Opcode::VoidTag => {
526 let attr_count = read_u16_at(ops, pos + 4, end)? as usize;
527 pos = bounded(pos, 6 + attr_count * 8, end, op_start)?;
528 }
529 Opcode::CloseTag
530 | Opcode::Text
531 | Opcode::Comment
532 | Opcode::ListItemKey
533 | Opcode::DynText => {
534 if opcode == Opcode::DynText {
535 self.check_slot(read_u16_at(ops, pos, end)?)?;
536 }
537 pos = bounded(pos, 4, end, op_start)?;
538 }
539 Opcode::DynAttr => {
540 self.check_slot(read_u16_at(ops, pos + 4, end)?)?;
541 pos = bounded(pos, 6, end, op_start)?;
542 }
543 Opcode::IslandStart => {
544 let island_id = read_u16_at(ops, pos, end)?;
549 if self.island_entry(island_id).is_none() {
550 return Err(IrError::UnknownIslandId(island_id));
551 }
552 pos = bounded(pos, 2, end, op_start)?;
553 }
554 Opcode::IslandEnd => {
555 pos = bounded(pos, 2, end, op_start)?;
556 }
557 Opcode::ShowElse => {
558 }
560 Opcode::Preload => {
561 pos = bounded(pos, 5, end, op_start)?;
562 }
563 Opcode::Prop => {
564 self.check_slot(read_u16_at(ops, pos, end)?)?;
565 self.check_slot(read_u16_at(ops, pos + 6, end)?)?;
566 pos = bounded(pos, 8, end, op_start)?;
567 }
568 Opcode::ShowIf => {
569 self.check_slot(read_u16_at(ops, pos, end)?)?;
570 let then_len = read_u32_at(ops, pos + 2, end)? as usize;
571 let else_len = read_u32_at(ops, pos + 6, end)? as usize;
572 let then_start = bounded(pos, 10, end, op_start)?;
573 let then_end = bounded(then_start, then_len, end, op_start)?;
574 if then_end >= end || ops[then_end] != Opcode::ShowElse as u8 {
578 return Err(IrError::MalformedControlFlow { pos: then_end });
579 }
580 let else_start = then_end + 1;
581 let else_end = bounded(else_start, else_len, end, op_start)?;
582 self.scan_region(then_start, then_end, depth + 1)?;
583 self.scan_region(else_start, else_end, depth + 1)?;
584 pos = else_end;
585 }
586 Opcode::Switch => {
587 self.check_slot(read_u16_at(ops, pos, end)?)?;
588 let case_count = read_u16_at(ops, pos + 2, end)? as usize;
589 let mut header_pos = bounded(pos, 4, end, op_start)?;
590 let mut lens =
591 Vec::with_capacity(case_count.min(end.saturating_sub(header_pos) / 8));
592 for _ in 0..case_count {
593 let body_len = read_u32_at(ops, header_pos + 4, end)? as usize;
594 lens.push(body_len);
595 header_pos = bounded(header_pos, 8, end, op_start)?;
596 }
597 let mut body_pos = header_pos;
598 for body_len in lens {
599 let case_end = bounded(body_pos, body_len, end, op_start)?;
600 self.scan_region(body_pos, case_end, depth + 1)?;
601 body_pos = case_end;
602 }
603 pos = body_pos;
604 }
605 Opcode::List => {
606 self.check_slot(read_u16_at(ops, pos, end)?)?;
607 self.check_slot(read_u16_at(ops, pos + 2, end)?)?;
608 let body_len = read_u32_at(ops, pos + 4, end)? as usize;
609 let body_start = bounded(pos, 8, end, op_start)?;
610 let body_end = bounded(body_start, body_len, end, op_start)?;
611 self.scan_region(body_start, body_end, depth + 1)?;
612 pos = body_end;
613 }
614 Opcode::TryStart => {
615 let fallback_len = read_u32_at(ops, pos, end)? as usize;
616 pos = bounded(pos, 4, end, op_start)?;
617 fallback_stack.push(fallback_len);
618 }
619 Opcode::Fallback => {
620 match fallback_stack.pop() {
629 Some(skip) => pos = bounded(pos, skip, end, op_start)?,
630 None => return Err(IrError::MalformedControlFlow { pos: op_start }),
631 }
632 }
633 }
634 }
635
636 if let Some(_unmatched) = fallback_stack.pop() {
641 return Err(IrError::MalformedControlFlow { pos: end });
642 }
643
644 Ok(())
645 }
646
647 fn check_slot(&self, slot_id: u16) -> Result<(), IrError> {
652 if self.slot_entry(slot_id).is_none() {
653 return Err(IrError::UnknownSlotId(slot_id));
654 }
655 Ok(())
656 }
657
658 pub fn slot_id_by_name(&self, name: &str) -> Option<u16> {
672 for slot in self.slots.entries() {
673 if let Ok(slot_name) = self.strings.get(slot.name_str_idx) {
674 if slot_name == name {
675 return Some(slot.slot_id);
676 }
677 }
678 }
679 None
680 }
681}
682
683const MAX_VALIDATION_DEPTH: usize = 64;
686
687fn build_id_index(ids: impl Iterator<Item = u16> + Clone) -> Vec<u32> {
690 let max_id = ids.clone().max();
691 let Some(max_id) = max_id else {
692 return Vec::new();
693 };
694 let mut index = vec![NO_INDEX; max_id as usize + 1];
698 for (i, id) in ids.enumerate() {
699 let entry = &mut index[id as usize];
700 if *entry == NO_INDEX {
701 *entry = i as u32;
702 }
703 }
704 index
705}
706
707fn bounded(base: usize, len: usize, end: usize, op_start: usize) -> Result<usize, IrError> {
712 base.checked_add(len)
713 .filter(|e| *e <= end)
714 .ok_or(IrError::MalformedControlFlow { pos: op_start })
715}
716
717fn read_u16_at(ops: &[u8], pos: usize, end: usize) -> Result<u16, IrError> {
719 if pos.checked_add(2).map(|e| e > end).unwrap_or(true) {
720 return Err(IrError::MalformedControlFlow { pos });
721 }
722 crate::walker::read_u16(ops, pos)
723}
724
725fn read_u32_at(ops: &[u8], pos: usize, end: usize) -> Result<u32, IrError> {
727 if pos.checked_add(4).map(|e| e > end).unwrap_or(true) {
728 return Err(IrError::MalformedControlFlow { pos });
729 }
730 crate::walker::read_u32(ops, pos)
731}
732
733#[cfg(any(test, feature = "test-helpers"))]
747#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
750pub mod test_helpers {
751 use crate::format::{HEADER_SIZE, SECTION_TABLE_SIZE};
752
753 fn to_u16(n: usize) -> [u8; 2] {
760 u16::try_from(n)
761 .unwrap_or_else(|_| panic!("{n} does not fit the u16 field this encoder writes"))
762 .to_le_bytes()
763 }
764
765 fn to_u32(n: usize) -> [u8; 4] {
767 u32::try_from(n)
768 .unwrap_or_else(|_| panic!("{n} does not fit the u32 field this encoder writes"))
769 .to_le_bytes()
770 }
771
772 pub fn build_string_table(strings: &[&str]) -> Vec<u8> {
774 let mut buf = Vec::new();
775 buf.extend_from_slice(&to_u32(strings.len()));
776 for s in strings {
777 let bytes = s.as_bytes();
778 buf.extend_from_slice(&to_u16(bytes.len()));
779 buf.extend_from_slice(bytes);
780 }
781 buf
782 }
783
784 pub fn build_slot_table(entries: &[(u16, u32, u8, u8, &[u8])]) -> Vec<u8> {
787 let mut buf = Vec::new();
788 buf.extend_from_slice(&to_u16(entries.len()));
789 for &(slot_id, name_str_idx, type_hint, source, default_bytes) in entries {
790 buf.extend_from_slice(&slot_id.to_le_bytes());
791 buf.extend_from_slice(&name_str_idx.to_le_bytes());
792 buf.push(type_hint);
793 buf.push(source);
794 buf.extend_from_slice(&to_u16(default_bytes.len()));
795 buf.extend_from_slice(default_bytes);
796 }
797 buf
798 }
799
800 pub fn build_island_table(entries: &[(u16, u8, u8, u32, u32, &[u16])]) -> Vec<u8> {
804 let mut buf = Vec::new();
805 buf.extend_from_slice(&to_u16(entries.len()));
806 for &(id, trigger, props_mode, name_str_idx, byte_offset, slot_ids) in entries {
807 buf.extend_from_slice(&id.to_le_bytes());
808 buf.push(trigger);
809 buf.push(props_mode);
810 buf.extend_from_slice(&name_str_idx.to_le_bytes());
811 buf.extend_from_slice(&byte_offset.to_le_bytes());
812 buf.extend_from_slice(&to_u16(slot_ids.len()));
813 for &slot_id in slot_ids.iter() {
814 buf.extend_from_slice(&slot_id.to_le_bytes());
815 }
816 }
817 buf
818 }
819
820 pub fn encode_open_tag(str_idx: u32, attrs: &[(u32, u32)]) -> Vec<u8> {
824 let mut buf = Vec::new();
825 buf.push(0x01); buf.extend_from_slice(&str_idx.to_le_bytes());
827 buf.extend_from_slice(&to_u16(attrs.len()));
828 for &(key_idx, val_idx) in attrs {
829 buf.extend_from_slice(&key_idx.to_le_bytes());
830 buf.extend_from_slice(&val_idx.to_le_bytes());
831 }
832 buf
833 }
834
835 pub fn encode_close_tag(str_idx: u32) -> Vec<u8> {
837 let mut buf = Vec::new();
838 buf.push(0x02); buf.extend_from_slice(&str_idx.to_le_bytes());
840 buf
841 }
842
843 pub fn encode_void_tag(str_idx: u32, attrs: &[(u32, u32)]) -> Vec<u8> {
845 let mut buf = Vec::new();
846 buf.push(0x03); buf.extend_from_slice(&str_idx.to_le_bytes());
848 buf.extend_from_slice(&to_u16(attrs.len()));
849 for &(key_idx, val_idx) in attrs {
850 buf.extend_from_slice(&key_idx.to_le_bytes());
851 buf.extend_from_slice(&val_idx.to_le_bytes());
852 }
853 buf
854 }
855
856 pub fn encode_text(str_idx: u32) -> Vec<u8> {
858 let mut buf = Vec::new();
859 buf.push(0x04); buf.extend_from_slice(&str_idx.to_le_bytes());
861 buf
862 }
863
864 pub fn encode_show_if(slot_id: u16, then_ops: &[u8], else_ops: &[u8]) -> Vec<u8> {
872 let mut buf = Vec::new();
873 buf.push(0x07); buf.extend_from_slice(&slot_id.to_le_bytes());
875 buf.extend_from_slice(&to_u32(then_ops.len()));
876 buf.extend_from_slice(&to_u32(else_ops.len()));
877 buf.extend_from_slice(then_ops); buf.push(0x08); buf.extend_from_slice(else_ops); buf
881 }
882
883 pub fn encode_list(slot_id: u16, item_slot_id: u16, body_ops: &[u8]) -> Vec<u8> {
885 let mut buf = Vec::new();
886 buf.push(0x0A); buf.extend_from_slice(&slot_id.to_le_bytes());
888 buf.extend_from_slice(&item_slot_id.to_le_bytes());
889 buf.extend_from_slice(&to_u32(body_ops.len()));
890 buf.extend_from_slice(body_ops);
891 buf
892 }
893
894 pub fn encode_switch(slot_id: u16, cases: &[(u32, &[u8])]) -> Vec<u8> {
905 let mut buf = Vec::new();
906 buf.push(0x09); buf.extend_from_slice(&slot_id.to_le_bytes());
908 buf.extend_from_slice(&to_u16(cases.len()));
909 for (val_str_idx, body) in cases {
911 buf.extend_from_slice(&val_str_idx.to_le_bytes());
912 buf.extend_from_slice(&to_u32(body.len()));
913 }
914 for (_, body) in cases {
916 buf.extend_from_slice(body);
917 }
918 buf
919 }
920
921 pub fn encode_try(main_ops: &[u8], fallback_ops: &[u8]) -> Vec<u8> {
931 let mut buf = Vec::new();
932 buf.push(0x0D); buf.extend_from_slice(&to_u32(fallback_ops.len()));
934 buf.extend_from_slice(main_ops);
935 buf.push(0x0E); buf.extend_from_slice(fallback_ops);
937 buf
938 }
939
940 pub fn encode_preload(resource_type: u8, url_str_idx: u32) -> Vec<u8> {
942 let mut buf = Vec::new();
943 buf.push(0x0F); buf.push(resource_type);
945 buf.extend_from_slice(&url_str_idx.to_le_bytes());
946 buf
947 }
948
949 pub fn build_minimal_ir(
958 strings: &[&str],
959 slots: &[(u16, u32, u8, u8, &[u8])],
960 opcodes: &[u8],
961 islands: &[(u16, u8, u8, u32, u32, &[u16])],
962 ) -> Vec<u8> {
963 let string_section = build_string_table(strings);
964 let slot_section = build_slot_table(slots);
965 let island_section = build_island_table(islands);
966
967 let data_start = HEADER_SIZE + SECTION_TABLE_SIZE;
969
970 let bytecode_offset = data_start;
972 let bytecode_size = opcodes.len();
973
974 let string_offset = bytecode_offset + bytecode_size;
975 let string_size = string_section.len();
976
977 let slot_offset = string_offset + string_size;
978 let slot_size = slot_section.len();
979
980 let island_offset = slot_offset + slot_size;
981 let island_size = island_section.len();
982
983 let mut buf = Vec::new();
985 buf.extend_from_slice(b"FMIR");
986 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));
992 buf.extend_from_slice(&to_u32(bytecode_size));
993 buf.extend_from_slice(&to_u32(string_offset));
994 buf.extend_from_slice(&to_u32(string_size));
995 buf.extend_from_slice(&to_u32(slot_offset));
996 buf.extend_from_slice(&to_u32(slot_size));
997 buf.extend_from_slice(&to_u32(island_offset));
998 buf.extend_from_slice(&to_u32(island_size));
999
1000 buf.extend_from_slice(opcodes);
1002 buf.extend_from_slice(&string_section);
1003 buf.extend_from_slice(&slot_section);
1004 buf.extend_from_slice(&island_section);
1005
1006 buf
1007 }
1008}
1009
1010#[cfg(test)]
1015mod tests {
1016 use super::test_helpers::*;
1017 use super::*;
1018 use crate::format::{IrError, IslandTrigger, PropsMode, SlotSource, SlotType};
1019
1020 #[test]
1023 fn parse_string_table() {
1024 let data = build_string_table(&["div", "class", "container"]);
1025 let table = StringTable::parse(&data).unwrap();
1026
1027 assert_eq!(table.len(), 3);
1028 assert_eq!(table.get(0).unwrap(), "div");
1029 assert_eq!(table.get(1).unwrap(), "class");
1030 assert_eq!(table.get(2).unwrap(), "container");
1031
1032 let err = table.get(3).unwrap_err();
1034 assert_eq!(err, IrError::StringIndexOutOfBounds { index: 3, len: 3 });
1035 }
1036
1037 #[test]
1038 fn parse_string_table_empty() {
1039 let data = build_string_table(&[]);
1040 let table = StringTable::parse(&data).unwrap();
1041 assert_eq!(table.len(), 0);
1042 }
1043
1044 #[test]
1045 fn parse_string_table_unicode() {
1046 let data = build_string_table(&["héllo"]);
1047 let table = StringTable::parse(&data).unwrap();
1048
1049 assert_eq!(table.len(), 1);
1050 assert_eq!(table.get(0).unwrap(), "héllo");
1051 }
1052
1053 #[test]
1054 fn parse_string_table_truncated() {
1055 let mut data = Vec::new();
1057 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();
1063 match err {
1064 IrError::BufferTooShort { .. } => {} other => panic!("expected BufferTooShort, got {other:?}"),
1066 }
1067 }
1068
1069 #[test]
1072 fn parse_slot_table() {
1073 let data = build_slot_table(&[
1074 (1, 0, 0x01, 0x00, &[]), (2, 1, 0x03, 0x01, &[0x42]), ]);
1077 let table = SlotTable::parse(&data).unwrap();
1078
1079 assert_eq!(table.len(), 2);
1080
1081 let entries = table.entries();
1082 assert_eq!(entries[0].slot_id, 1);
1083 assert_eq!(entries[0].name_str_idx, 0);
1084 assert_eq!(entries[0].type_hint, SlotType::Text);
1085 assert_eq!(entries[0].source, SlotSource::Server);
1086 assert_eq!(entries[0].default_bytes, Vec::<u8>::new());
1087
1088 assert_eq!(entries[1].slot_id, 2);
1089 assert_eq!(entries[1].name_str_idx, 1);
1090 assert_eq!(entries[1].type_hint, SlotType::Number);
1091 assert_eq!(entries[1].source, SlotSource::Client);
1092 assert_eq!(entries[1].default_bytes, vec![0x42]);
1093 }
1094
1095 #[test]
1096 fn parse_slot_table_empty() {
1097 let data = build_slot_table(&[]);
1098 let table = SlotTable::parse(&data).unwrap();
1099 assert_eq!(table.len(), 0);
1100 }
1101
1102 #[test]
1105 fn parse_island_table() {
1106 let data = build_island_table(&[
1107 (1, 0x02, 0x01, 5, 0, &[]), ]);
1109 let table = IslandTableParsed::parse(&data).unwrap();
1110
1111 assert_eq!(table.len(), 1);
1112
1113 let entry = &table.entries()[0];
1114 assert_eq!(entry.id, 1);
1115 assert_eq!(entry.trigger, IslandTrigger::Visible);
1116 assert_eq!(entry.props_mode, PropsMode::Inline);
1117 assert_eq!(entry.name_str_idx, 5);
1118 assert_eq!(entry.slot_ids, Vec::<u16>::new());
1119 }
1120
1121 #[test]
1122 fn parse_island_table_with_slot_ids() {
1123 let data = build_island_table(&[
1124 (0, 0x01, 0x01, 0, 0, &[0, 1]), ]);
1126 let table = IslandTableParsed::parse(&data).unwrap();
1127 assert_eq!(table.len(), 1);
1128 let entry = &table.entries()[0];
1129 assert_eq!(entry.id, 0);
1130 assert_eq!(entry.trigger, IslandTrigger::Load);
1131 assert_eq!(entry.props_mode, PropsMode::Inline);
1132 assert_eq!(entry.slot_ids, vec![0, 1]);
1133 }
1134
1135 #[test]
1136 fn parse_island_table_empty() {
1137 let data = build_island_table(&[]);
1138 let table = IslandTableParsed::parse(&data).unwrap();
1139 assert_eq!(table.len(), 0);
1140 }
1141
1142 #[test]
1145 fn parse_minimal_ir_file() {
1146 let mut opcodes = Vec::new();
1148 opcodes.extend_from_slice(&encode_open_tag(0, &[]));
1149 opcodes.extend_from_slice(&encode_close_tag(0));
1150
1151 let data = build_minimal_ir(&["div"], &[], &opcodes, &[]);
1152 let module = IrModule::parse(&data).unwrap();
1153
1154 assert_eq!(module.header.version, 2);
1155 assert_eq!(module.strings.get(0).unwrap(), "div");
1156 assert_eq!(module.strings.len(), 1);
1157 assert_eq!(module.slots.len(), 0);
1158 assert_eq!(module.islands.len(), 0);
1159 assert_eq!(module.opcodes.len(), opcodes.len());
1160 }
1161
1162 #[test]
1163 fn parse_ir_with_slots() {
1164 let opcodes = encode_text(0);
1165 let data = build_minimal_ir(
1166 &["greeting", "count", "Hello"],
1167 &[
1168 (1, 0, 0x01, 0x00, &[]), (2, 1, 0x03, 0x00, &[]), ],
1171 &opcodes,
1172 &[],
1173 );
1174 let module = IrModule::parse(&data).unwrap();
1175
1176 assert_eq!(module.slots.len(), 2);
1177 let entries = module.slots.entries();
1178 assert_eq!(entries[0].slot_id, 1);
1179 assert_eq!(entries[0].name_str_idx, 0);
1180 assert_eq!(entries[0].type_hint, SlotType::Text);
1181 assert_eq!(entries[1].slot_id, 2);
1182 assert_eq!(entries[1].name_str_idx, 1);
1183 assert_eq!(entries[1].type_hint, SlotType::Number);
1184 }
1185
1186 #[test]
1187 fn parse_ir_with_islands() {
1188 let mut opcodes = vec![0x0B]; opcodes.extend_from_slice(&1u16.to_le_bytes());
1193 opcodes.extend_from_slice(&encode_text(1));
1194 opcodes.push(0x0C); opcodes.extend_from_slice(&1u16.to_le_bytes());
1196 let data = build_minimal_ir(
1197 &["Counter", "Hello"],
1198 &[],
1199 &opcodes,
1200 &[
1201 (1, 0x01, 0x01, 0, 0, &[]), ],
1203 );
1204 let module = IrModule::parse(&data).unwrap();
1205
1206 assert_eq!(module.islands.len(), 1);
1207 let entry = &module.islands.entries()[0];
1208 assert_eq!(entry.id, 1);
1209 assert_eq!(entry.trigger, IslandTrigger::Load);
1210 assert_eq!(entry.props_mode, PropsMode::Inline);
1211 assert_eq!(entry.name_str_idx, 0);
1212 assert_eq!(module.strings.get(entry.name_str_idx).unwrap(), "Counter");
1213 }
1214
1215 #[test]
1216 fn parse_ir_rejects_truncated() {
1217 let data = b"FMIR\x02\x00";
1219 let err = IrModule::parse(data).unwrap_err();
1220 match err {
1221 IrError::BufferTooShort {
1222 expected: 16,
1223 actual: 6,
1224 } => {}
1225 other => panic!("expected BufferTooShort(16, 6), got {other:?}"),
1226 }
1227 }
1228
1229 #[test]
1230 fn parse_ir_rejects_bad_section_bounds() {
1231 let opcodes = encode_text(0);
1233 let mut data = build_minimal_ir(&["x"], &[], &opcodes, &[]);
1234
1235 let big_size: u32 = 99999;
1239 data[44..48].copy_from_slice(&big_size.to_le_bytes());
1240
1241 let err = IrModule::parse(&data).unwrap_err();
1242 match err {
1243 IrError::SectionOutOfBounds { section: 3, .. } => {}
1244 other => panic!("expected SectionOutOfBounds for section 3, got {other:?}"),
1245 }
1246 }
1247
1248 #[test]
1249 fn validate_catches_bad_slot_str_idx() {
1250 let opcodes = encode_text(0);
1252 let data = build_minimal_ir(
1253 &["a", "b", "c"],
1254 &[(1, 99, 0x01, 0x00, &[])], &opcodes,
1256 &[],
1257 );
1258 let err = IrModule::parse(&data).unwrap_err();
1259 assert_eq!(err, IrError::StringIndexOutOfBounds { index: 99, len: 3 });
1260 }
1261
1262 #[test]
1263 fn validate_catches_bad_island_str_idx() {
1264 let opcodes = encode_text(0);
1266 let mut island_opcodes = vec![0x0B];
1267 island_opcodes.extend_from_slice(&1u16.to_le_bytes());
1268 island_opcodes.extend_from_slice(&opcodes);
1269 island_opcodes.push(0x0C);
1270 island_opcodes.extend_from_slice(&1u16.to_le_bytes());
1271 let data = build_minimal_ir(
1272 &["a", "b", "c"],
1273 &[],
1274 &island_opcodes,
1275 &[(1, 0x01, 0x01, 99, 0, &[])],
1276 );
1277 let err = IrModule::parse(&data).unwrap_err();
1278 assert_eq!(err, IrError::StringIndexOutOfBounds { index: 99, len: 3 });
1279 }
1280
1281 #[test]
1282 fn duplicate_slot_names_resolve_first_wins_everywhere() {
1283 let opcodes = encode_text(0);
1288 let data = build_minimal_ir(
1289 &["dup"],
1290 &[(0, 0, 0x01, 0x00, &[]), (1, 0, 0x01, 0x00, &[])],
1291 &opcodes,
1292 &[],
1293 );
1294 let module = IrModule::parse(&data).unwrap();
1295
1296 assert_eq!(module.slot_id_by_name("dup"), Some(0));
1297
1298 let slots = crate::slot::SlotData::from_json(r#"{"dup":"x"}"#, &module).unwrap();
1299 assert_eq!(slots.get(0).to_text(), "x", "the first slot must be filled");
1300 assert_eq!(slots.get(1).to_text(), "", "the second must stay untouched");
1301 }
1302
1303 #[test]
1304 fn validate_rejects_list_item_slot_not_in_table() {
1305 let body = {
1309 let mut b = vec![0x05]; b.extend_from_slice(&7u16.to_le_bytes());
1311 b.extend_from_slice(&0u16.to_le_bytes());
1312 b
1313 };
1314 let opcodes = encode_list(0, 7, &body);
1315 let data = build_minimal_ir(&["items"], &[(0, 0, 0x04, 0x00, &[])], &opcodes, &[]);
1316 assert_eq!(
1317 IrModule::parse(&data).unwrap_err(),
1318 IrError::UnknownSlotId(7)
1319 );
1320 }
1321
1322 #[test]
1323 fn validate_rejects_island_start_missing_from_table() {
1324 let mut opcodes = vec![0x0B]; opcodes.extend_from_slice(&3u16.to_le_bytes());
1329 opcodes.extend_from_slice(&encode_text(0));
1330 opcodes.push(0x0C); opcodes.extend_from_slice(&3u16.to_le_bytes());
1332
1333 let data = build_minimal_ir(&["hi"], &[], &opcodes, &[]);
1334 assert_eq!(
1335 IrModule::parse(&data).unwrap_err(),
1336 IrError::UnknownIslandId(3)
1337 );
1338 }
1339
1340 #[test]
1343 fn string_table_rejects_count_the_section_cannot_hold() {
1344 let mut data = Vec::new();
1349 data.extend_from_slice(&1_000_000u32.to_le_bytes()); data.extend_from_slice(&[0u8; 16]); let err = StringTable::parse(&data).unwrap_err();
1352 assert!(matches!(err, IrError::BufferTooShort { .. }));
1353 }
1354
1355 #[test]
1356 fn slot_table_rejects_count_the_section_cannot_hold() {
1357 let data = 65535u16.to_le_bytes().to_vec();
1359 let err = SlotTable::parse(&data).unwrap_err();
1360 assert!(matches!(err, IrError::BufferTooShort { .. }));
1361 }
1362
1363 #[test]
1364 fn island_table_rejects_count_the_section_cannot_hold() {
1365 let data = 65535u16.to_le_bytes().to_vec();
1366 let err = IslandTableParsed::parse(&data).unwrap_err();
1367 assert!(matches!(err, IrError::BufferTooShort { .. }));
1368 }
1369
1370 #[test]
1373 fn validate_rejects_duplicate_slot_ids() {
1374 let opcodes = encode_text(0);
1375 let data = build_minimal_ir(
1376 &["a", "b"],
1377 &[(0, 0, 0x01, 0x00, &[]), (0, 1, 0x01, 0x00, &[])],
1378 &opcodes,
1379 &[],
1380 );
1381 assert_eq!(
1382 IrModule::parse(&data).unwrap_err(),
1383 IrError::DuplicateSlotId(0)
1384 );
1385 }
1386
1387 #[test]
1388 fn validate_rejects_duplicate_island_ids() {
1389 let mut opcodes = vec![0x0B];
1390 opcodes.extend_from_slice(&0u16.to_le_bytes());
1391 opcodes.push(0x0C);
1392 opcodes.extend_from_slice(&0u16.to_le_bytes());
1393 let data = build_minimal_ir(
1394 &["Comp"],
1395 &[],
1396 &opcodes,
1397 &[(0, 0x01, 0x01, 0, 0, &[]), (0, 0x01, 0x01, 0, 0, &[])],
1398 );
1399 assert_eq!(
1400 IrModule::parse(&data).unwrap_err(),
1401 IrError::DuplicateIslandId(0)
1402 );
1403 }
1404
1405 #[test]
1406 fn validate_rejects_island_slot_id_absent_from_slot_table() {
1407 let mut opcodes = vec![0x0B];
1412 opcodes.extend_from_slice(&0u16.to_le_bytes());
1413 opcodes.push(0x0C);
1414 opcodes.extend_from_slice(&0u16.to_le_bytes());
1415 let data = build_minimal_ir(&["Comp"], &[], &opcodes, &[(0, 0x01, 0x01, 0, 0, &[7])]);
1416 assert_eq!(
1417 IrModule::parse(&data).unwrap_err(),
1418 IrError::UnknownSlotId(7)
1419 );
1420 }
1421
1422 #[test]
1423 fn validate_rejects_island_offset_landing_mid_operand() {
1424 let mut opcodes = vec![0x0B];
1430 opcodes.extend_from_slice(&1u16.to_le_bytes());
1431 opcodes.push(0x04);
1433 opcodes.extend_from_slice(&0x0Bu32.to_le_bytes());
1434 opcodes.push(0x0C);
1435 opcodes.extend_from_slice(&1u16.to_le_bytes());
1436
1437 let data = build_minimal_ir(&["Comp"], &[], &opcodes, &[(1, 0x01, 0x01, 0, 4, &[])]);
1438 assert_eq!(
1439 IrModule::parse(&data).unwrap_err(),
1440 IrError::IslandOffsetMismatch {
1441 island_id: 1,
1442 byte_offset: 4
1443 }
1444 );
1445 }
1446
1447 #[test]
1448 fn validate_rejects_opcode_slot_id_absent_from_slot_table() {
1449 let mut opcodes = vec![0x05];
1451 opcodes.extend_from_slice(&9u16.to_le_bytes());
1452 opcodes.extend_from_slice(&0u16.to_le_bytes());
1453 let data = build_minimal_ir(&["a"], &[], &opcodes, &[]);
1454 assert_eq!(
1455 IrModule::parse(&data).unwrap_err(),
1456 IrError::UnknownSlotId(9)
1457 );
1458 }
1459
1460 #[test]
1461 fn slot_and_island_lookups_are_indexed() {
1462 let mut opcodes = vec![0x0B];
1463 opcodes.extend_from_slice(&3u16.to_le_bytes());
1464 opcodes.push(0x0C);
1465 opcodes.extend_from_slice(&3u16.to_le_bytes());
1466 let data = build_minimal_ir(
1467 &["Comp", "title"],
1468 &[(4, 1, 0x01, 0x00, &[])],
1469 &opcodes,
1470 &[(3, 0x01, 0x01, 0, 0, &[4])],
1471 );
1472 let module = IrModule::parse(&data).unwrap();
1473
1474 assert_eq!(module.slot_entry(4).unwrap().slot_id, 4);
1475 assert!(module.slot_entry(0).is_none());
1476 assert_eq!(module.island_entry(3).unwrap().id, 3);
1477 assert!(module.island_entry(0).is_none());
1478 assert!(module.slot_entry(u16::MAX).is_none());
1480 assert!(module.island_entry(u16::MAX).is_none());
1481 }
1482}