1use crate::format::{
7 IrError, IrHeader, IslandEntry, IslandTrigger, PropsMode, SectionTable, SlotEntry, SlotSource,
8 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 if count > data.len() {
35 return Err(IrError::BufferTooShort {
36 expected: count,
37 actual: data.len(),
38 });
39 }
40 let mut offset = 4;
41 let mut strings = Vec::with_capacity(count);
42
43 for _ in 0..count {
44 if offset + 2 > data.len() {
46 return Err(IrError::BufferTooShort {
47 expected: offset + 2,
48 actual: data.len(),
49 });
50 }
51
52 let str_len = u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap()) as usize;
53 offset += 2;
54
55 if offset + str_len > data.len() {
56 return Err(IrError::BufferTooShort {
57 expected: offset + str_len,
58 actual: data.len(),
59 });
60 }
61
62 let s = std::str::from_utf8(&data[offset..offset + str_len])
63 .map_err(|e| IrError::InvalidUtf8(e.to_string()))?;
64 strings.push(s.to_owned());
65 offset += str_len;
66 }
67
68 Ok(StringTable { strings })
69 }
70
71 pub fn get(&self, idx: u32) -> Result<&str, IrError> {
73 self.strings
74 .get(idx as usize)
75 .map(|s| s.as_str())
76 .ok_or(IrError::StringIndexOutOfBounds {
77 index: idx,
78 len: self.strings.len(),
79 })
80 }
81
82 pub fn len(&self) -> usize {
84 self.strings.len()
85 }
86
87 pub fn is_empty(&self) -> bool {
89 self.strings.is_empty()
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct SlotTable {
104 slots: Vec<SlotEntry>,
105}
106
107const SLOT_ENTRY_MIN_SIZE: usize = 10;
110
111impl SlotTable {
112 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
114 if data.len() < 2 {
115 return Err(IrError::BufferTooShort {
116 expected: 2,
117 actual: data.len(),
118 });
119 }
120
121 let count = u16::from_le_bytes(data[0..2].try_into().unwrap()) as usize;
122 let mut slots = Vec::with_capacity(count);
123 let mut offset = 2;
124
125 for _ in 0..count {
126 if offset + SLOT_ENTRY_MIN_SIZE > data.len() {
128 return Err(IrError::BufferTooShort {
129 expected: offset + SLOT_ENTRY_MIN_SIZE,
130 actual: data.len(),
131 });
132 }
133
134 let slot_id = u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap());
135 let name_str_idx = u32::from_le_bytes(data[offset + 2..offset + 6].try_into().unwrap());
136 let type_hint = SlotType::from_byte(data[offset + 6])?;
137 let source = SlotSource::from_byte(data[offset + 7])?;
138 let default_len =
139 u16::from_le_bytes(data[offset + 8..offset + 10].try_into().unwrap()) as usize;
140 offset += SLOT_ENTRY_MIN_SIZE;
141
142 if offset + default_len > data.len() {
144 return Err(IrError::BufferTooShort {
145 expected: offset + default_len,
146 actual: data.len(),
147 });
148 }
149 let default_bytes = data[offset..offset + default_len].to_vec();
150 offset += default_len;
151
152 slots.push(SlotEntry {
153 slot_id,
154 name_str_idx,
155 type_hint,
156 source,
157 default_bytes,
158 });
159 }
160
161 Ok(SlotTable { slots })
162 }
163
164 pub fn len(&self) -> usize {
166 self.slots.len()
167 }
168
169 pub fn is_empty(&self) -> bool {
171 self.slots.is_empty()
172 }
173
174 pub fn entries(&self) -> &[SlotEntry] {
176 &self.slots
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct IslandTableParsed {
190 islands: Vec<IslandEntry>,
191}
192
193const ISLAND_ENTRY_MIN_SIZE: usize = 14;
196
197impl IslandTableParsed {
198 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
200 if data.len() < 2 {
201 return Err(IrError::BufferTooShort {
202 expected: 2,
203 actual: data.len(),
204 });
205 }
206
207 let count = u16::from_le_bytes(data[0..2].try_into().unwrap()) as usize;
208 let mut islands = Vec::with_capacity(count);
209 let mut offset = 2;
210
211 for _ in 0..count {
212 if offset + ISLAND_ENTRY_MIN_SIZE > data.len() {
214 return Err(IrError::BufferTooShort {
215 expected: offset + ISLAND_ENTRY_MIN_SIZE,
216 actual: data.len(),
217 });
218 }
219
220 let id = u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap());
221 let trigger = IslandTrigger::from_byte(data[offset + 2])?;
222 let props_mode = PropsMode::from_byte(data[offset + 3])?;
223 let name_str_idx = u32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap());
224 let byte_offset = u32::from_le_bytes(data[offset + 8..offset + 12].try_into().unwrap());
225 let slot_count =
226 u16::from_le_bytes(data[offset + 12..offset + 14].try_into().unwrap()) as usize;
227 offset += ISLAND_ENTRY_MIN_SIZE;
228
229 let needed = slot_count * 2;
231 if offset + needed > data.len() {
232 return Err(IrError::BufferTooShort {
233 expected: offset + needed,
234 actual: data.len(),
235 });
236 }
237 let mut slot_ids = Vec::with_capacity(slot_count);
238 for _ in 0..slot_count {
239 slot_ids.push(u16::from_le_bytes(
240 data[offset..offset + 2].try_into().unwrap(),
241 ));
242 offset += 2;
243 }
244
245 islands.push(IslandEntry {
246 id,
247 trigger,
248 props_mode,
249 name_str_idx,
250 byte_offset,
251 slot_ids,
252 });
253 }
254
255 Ok(IslandTableParsed { islands })
256 }
257
258 pub fn len(&self) -> usize {
260 self.islands.len()
261 }
262
263 pub fn is_empty(&self) -> bool {
265 self.islands.is_empty()
266 }
267
268 pub fn entries(&self) -> &[IslandEntry] {
270 &self.islands
271 }
272}
273
274#[derive(Debug, Clone)]
280pub struct IrModule {
281 pub header: IrHeader,
282 pub strings: StringTable,
283 pub slots: SlotTable,
284 pub opcodes: Vec<u8>,
286 pub islands: IslandTableParsed,
287}
288
289impl IrModule {
290 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
292 let header = IrHeader::parse(data)?;
294
295 if data.len() < HEADER_SIZE + SECTION_TABLE_SIZE {
297 return Err(IrError::BufferTooShort {
298 expected: HEADER_SIZE + SECTION_TABLE_SIZE,
299 actual: data.len(),
300 });
301 }
302 let section_table = SectionTable::parse(&data[HEADER_SIZE..])?;
303
304 section_table.validate(data.len())?;
306
307 let sec_bytecode = §ion_table.sections[0];
309 let sec_strings = §ion_table.sections[1];
310 let sec_slots = §ion_table.sections[2];
311 let sec_islands = §ion_table.sections[3];
312
313 let string_data = &data[sec_strings.offset as usize
315 ..(sec_strings.offset as usize + sec_strings.size as usize)];
316 let strings = StringTable::parse(string_data)?;
317
318 let slot_data =
320 &data[sec_slots.offset as usize..(sec_slots.offset as usize + sec_slots.size as usize)];
321 let slots = SlotTable::parse(slot_data)?;
322
323 let opcodes = data[sec_bytecode.offset as usize
325 ..(sec_bytecode.offset as usize + sec_bytecode.size as usize)]
326 .to_vec();
327
328 let island_data = &data[sec_islands.offset as usize
330 ..(sec_islands.offset as usize + sec_islands.size as usize)];
331 let islands = IslandTableParsed::parse(island_data)?;
332
333 let module = IrModule {
334 header,
335 strings,
336 slots,
337 opcodes,
338 islands,
339 };
340
341 module.validate()?;
343
344 Ok(module)
345 }
346
347 pub fn validate(&self) -> Result<(), IrError> {
349 let str_count = self.strings.len();
350
351 for slot in self.slots.entries() {
353 if slot.name_str_idx as usize >= str_count {
354 return Err(IrError::StringIndexOutOfBounds {
355 index: slot.name_str_idx,
356 len: str_count,
357 });
358 }
359 }
360
361 for island in self.islands.entries() {
363 if island.name_str_idx as usize >= str_count {
364 return Err(IrError::StringIndexOutOfBounds {
365 index: island.name_str_idx,
366 len: str_count,
367 });
368 }
369 }
370
371 Ok(())
372 }
373
374 pub fn slot_id_by_name(&self, name: &str) -> Option<u16> {
376 for slot in self.slots.entries() {
377 if let Ok(slot_name) = self.strings.get(slot.name_str_idx) {
378 if slot_name == name {
379 return Some(slot.slot_id);
380 }
381 }
382 }
383 None
384 }
385}
386
387pub mod test_helpers {
397 use crate::format::{HEADER_SIZE, SECTION_TABLE_SIZE};
398
399 pub fn build_string_table(strings: &[&str]) -> Vec<u8> {
401 let mut buf = Vec::new();
402 buf.extend_from_slice(&(strings.len() as u32).to_le_bytes());
403 for s in strings {
404 let bytes = s.as_bytes();
405 buf.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
406 buf.extend_from_slice(bytes);
407 }
408 buf
409 }
410
411 pub fn build_slot_table(entries: &[(u16, u32, u8, u8, &[u8])]) -> Vec<u8> {
414 let mut buf = Vec::new();
415 buf.extend_from_slice(&(entries.len() as u16).to_le_bytes());
416 for &(slot_id, name_str_idx, type_hint, source, default_bytes) in entries {
417 buf.extend_from_slice(&slot_id.to_le_bytes());
418 buf.extend_from_slice(&name_str_idx.to_le_bytes());
419 buf.push(type_hint);
420 buf.push(source);
421 buf.extend_from_slice(&(default_bytes.len() as u16).to_le_bytes());
422 buf.extend_from_slice(default_bytes);
423 }
424 buf
425 }
426
427 pub fn build_island_table(entries: &[(u16, u8, u8, u32, u32, &[u16])]) -> Vec<u8> {
431 let mut buf = Vec::new();
432 buf.extend_from_slice(&(entries.len() as u16).to_le_bytes());
433 for &(id, trigger, props_mode, name_str_idx, byte_offset, slot_ids) in entries {
434 buf.extend_from_slice(&id.to_le_bytes());
435 buf.push(trigger);
436 buf.push(props_mode);
437 buf.extend_from_slice(&name_str_idx.to_le_bytes());
438 buf.extend_from_slice(&byte_offset.to_le_bytes());
439 buf.extend_from_slice(&(slot_ids.len() as u16).to_le_bytes());
440 for &slot_id in slot_ids.iter() {
441 buf.extend_from_slice(&slot_id.to_le_bytes());
442 }
443 }
444 buf
445 }
446
447 pub fn encode_open_tag(str_idx: u32, attrs: &[(u32, u32)]) -> Vec<u8> {
451 let mut buf = Vec::new();
452 buf.push(0x01); buf.extend_from_slice(&str_idx.to_le_bytes());
454 buf.extend_from_slice(&(attrs.len() as u16).to_le_bytes());
455 for &(key_idx, val_idx) in attrs {
456 buf.extend_from_slice(&key_idx.to_le_bytes());
457 buf.extend_from_slice(&val_idx.to_le_bytes());
458 }
459 buf
460 }
461
462 pub fn encode_close_tag(str_idx: u32) -> Vec<u8> {
464 let mut buf = Vec::new();
465 buf.push(0x02); buf.extend_from_slice(&str_idx.to_le_bytes());
467 buf
468 }
469
470 pub fn encode_void_tag(str_idx: u32, attrs: &[(u32, u32)]) -> Vec<u8> {
472 let mut buf = Vec::new();
473 buf.push(0x03); buf.extend_from_slice(&str_idx.to_le_bytes());
475 buf.extend_from_slice(&(attrs.len() as u16).to_le_bytes());
476 for &(key_idx, val_idx) in attrs {
477 buf.extend_from_slice(&key_idx.to_le_bytes());
478 buf.extend_from_slice(&val_idx.to_le_bytes());
479 }
480 buf
481 }
482
483 pub fn encode_text(str_idx: u32) -> Vec<u8> {
485 let mut buf = Vec::new();
486 buf.push(0x04); buf.extend_from_slice(&str_idx.to_le_bytes());
488 buf
489 }
490
491 pub fn encode_show_if(slot_id: u16, then_ops: &[u8], else_ops: &[u8]) -> Vec<u8> {
499 let mut buf = Vec::new();
500 buf.push(0x07); buf.extend_from_slice(&slot_id.to_le_bytes());
502 buf.extend_from_slice(&(then_ops.len() as u32).to_le_bytes());
503 buf.extend_from_slice(&(else_ops.len() as u32).to_le_bytes());
504 buf.extend_from_slice(then_ops); buf.push(0x08); buf.extend_from_slice(else_ops); buf
508 }
509
510 pub fn encode_list(slot_id: u16, item_slot_id: u16, body_ops: &[u8]) -> Vec<u8> {
512 let mut buf = Vec::new();
513 buf.push(0x0A); buf.extend_from_slice(&slot_id.to_le_bytes());
515 buf.extend_from_slice(&item_slot_id.to_le_bytes());
516 buf.extend_from_slice(&(body_ops.len() as u32).to_le_bytes());
517 buf.extend_from_slice(body_ops);
518 buf
519 }
520
521 pub fn encode_switch(slot_id: u16, cases: &[(u32, &[u8])]) -> Vec<u8> {
532 let mut buf = Vec::new();
533 buf.push(0x09); buf.extend_from_slice(&slot_id.to_le_bytes());
535 buf.extend_from_slice(&(cases.len() as u16).to_le_bytes());
536 for (val_str_idx, body) in cases {
538 buf.extend_from_slice(&val_str_idx.to_le_bytes());
539 buf.extend_from_slice(&(body.len() as u32).to_le_bytes());
540 }
541 for (_, body) in cases {
543 buf.extend_from_slice(body);
544 }
545 buf
546 }
547
548 pub fn encode_try(main_ops: &[u8], fallback_ops: &[u8]) -> Vec<u8> {
558 let mut buf = Vec::new();
559 buf.push(0x0D); buf.extend_from_slice(&(fallback_ops.len() as u32).to_le_bytes());
561 buf.extend_from_slice(main_ops);
562 buf.push(0x0E); buf.extend_from_slice(fallback_ops);
564 buf
565 }
566
567 pub fn encode_preload(resource_type: u8, url_str_idx: u32) -> Vec<u8> {
569 let mut buf = Vec::new();
570 buf.push(0x0F); buf.push(resource_type);
572 buf.extend_from_slice(&url_str_idx.to_le_bytes());
573 buf
574 }
575
576 pub fn build_minimal_ir(
585 strings: &[&str],
586 slots: &[(u16, u32, u8, u8, &[u8])],
587 opcodes: &[u8],
588 islands: &[(u16, u8, u8, u32, u32, &[u16])],
589 ) -> Vec<u8> {
590 let string_section = build_string_table(strings);
591 let slot_section = build_slot_table(slots);
592 let island_section = build_island_table(islands);
593
594 let data_start = HEADER_SIZE + SECTION_TABLE_SIZE;
596
597 let bytecode_offset = data_start;
599 let bytecode_size = opcodes.len();
600
601 let string_offset = bytecode_offset + bytecode_size;
602 let string_size = string_section.len();
603
604 let slot_offset = string_offset + string_size;
605 let slot_size = slot_section.len();
606
607 let island_offset = slot_offset + slot_size;
608 let island_size = island_section.len();
609
610 let mut buf = Vec::new();
612 buf.extend_from_slice(b"FMIR");
613 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());
619 buf.extend_from_slice(&(bytecode_size as u32).to_le_bytes());
620 buf.extend_from_slice(&(string_offset as u32).to_le_bytes());
621 buf.extend_from_slice(&(string_size as u32).to_le_bytes());
622 buf.extend_from_slice(&(slot_offset as u32).to_le_bytes());
623 buf.extend_from_slice(&(slot_size as u32).to_le_bytes());
624 buf.extend_from_slice(&(island_offset as u32).to_le_bytes());
625 buf.extend_from_slice(&(island_size as u32).to_le_bytes());
626
627 buf.extend_from_slice(opcodes);
629 buf.extend_from_slice(&string_section);
630 buf.extend_from_slice(&slot_section);
631 buf.extend_from_slice(&island_section);
632
633 buf
634 }
635}
636
637#[cfg(test)]
642mod tests {
643 use super::test_helpers::*;
644 use super::*;
645 use crate::format::{IrError, IslandTrigger, PropsMode, SlotSource, SlotType};
646
647 #[test]
650 fn parse_string_table() {
651 let data = build_string_table(&["div", "class", "container"]);
652 let table = StringTable::parse(&data).unwrap();
653
654 assert_eq!(table.len(), 3);
655 assert_eq!(table.get(0).unwrap(), "div");
656 assert_eq!(table.get(1).unwrap(), "class");
657 assert_eq!(table.get(2).unwrap(), "container");
658
659 let err = table.get(3).unwrap_err();
661 assert_eq!(err, IrError::StringIndexOutOfBounds { index: 3, len: 3 });
662 }
663
664 #[test]
665 fn parse_string_table_empty() {
666 let data = build_string_table(&[]);
667 let table = StringTable::parse(&data).unwrap();
668 assert_eq!(table.len(), 0);
669 }
670
671 #[test]
672 fn parse_string_table_unicode() {
673 let data = build_string_table(&["héllo"]);
674 let table = StringTable::parse(&data).unwrap();
675
676 assert_eq!(table.len(), 1);
677 assert_eq!(table.get(0).unwrap(), "héllo");
678 }
679
680 #[test]
681 fn parse_string_table_truncated() {
682 let mut data = Vec::new();
684 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();
690 match err {
691 IrError::BufferTooShort { .. } => {} other => panic!("expected BufferTooShort, got {other:?}"),
693 }
694 }
695
696 #[test]
699 fn parse_slot_table() {
700 let data = build_slot_table(&[
701 (1, 0, 0x01, 0x00, &[]), (2, 1, 0x03, 0x01, &[0x42]), ]);
704 let table = SlotTable::parse(&data).unwrap();
705
706 assert_eq!(table.len(), 2);
707
708 let entries = table.entries();
709 assert_eq!(entries[0].slot_id, 1);
710 assert_eq!(entries[0].name_str_idx, 0);
711 assert_eq!(entries[0].type_hint, SlotType::Text);
712 assert_eq!(entries[0].source, SlotSource::Server);
713 assert_eq!(entries[0].default_bytes, Vec::<u8>::new());
714
715 assert_eq!(entries[1].slot_id, 2);
716 assert_eq!(entries[1].name_str_idx, 1);
717 assert_eq!(entries[1].type_hint, SlotType::Number);
718 assert_eq!(entries[1].source, SlotSource::Client);
719 assert_eq!(entries[1].default_bytes, vec![0x42]);
720 }
721
722 #[test]
723 fn parse_slot_table_empty() {
724 let data = build_slot_table(&[]);
725 let table = SlotTable::parse(&data).unwrap();
726 assert_eq!(table.len(), 0);
727 }
728
729 #[test]
732 fn parse_island_table() {
733 let data = build_island_table(&[
734 (1, 0x02, 0x01, 5, 0, &[]), ]);
736 let table = IslandTableParsed::parse(&data).unwrap();
737
738 assert_eq!(table.len(), 1);
739
740 let entry = &table.entries()[0];
741 assert_eq!(entry.id, 1);
742 assert_eq!(entry.trigger, IslandTrigger::Visible);
743 assert_eq!(entry.props_mode, PropsMode::Inline);
744 assert_eq!(entry.name_str_idx, 5);
745 assert_eq!(entry.slot_ids, Vec::<u16>::new());
746 }
747
748 #[test]
749 fn parse_island_table_with_slot_ids() {
750 let data = build_island_table(&[
751 (0, 0x01, 0x01, 0, 0, &[0, 1]), ]);
753 let table = IslandTableParsed::parse(&data).unwrap();
754 assert_eq!(table.len(), 1);
755 let entry = &table.entries()[0];
756 assert_eq!(entry.id, 0);
757 assert_eq!(entry.trigger, IslandTrigger::Load);
758 assert_eq!(entry.props_mode, PropsMode::Inline);
759 assert_eq!(entry.slot_ids, vec![0, 1]);
760 }
761
762 #[test]
763 fn parse_island_table_empty() {
764 let data = build_island_table(&[]);
765 let table = IslandTableParsed::parse(&data).unwrap();
766 assert_eq!(table.len(), 0);
767 }
768
769 #[test]
772 fn parse_minimal_ir_file() {
773 let mut opcodes = Vec::new();
775 opcodes.extend_from_slice(&encode_open_tag(0, &[]));
776 opcodes.extend_from_slice(&encode_close_tag(0));
777
778 let data = build_minimal_ir(&["div"], &[], &opcodes, &[]);
779 let module = IrModule::parse(&data).unwrap();
780
781 assert_eq!(module.header.version, 2);
782 assert_eq!(module.strings.get(0).unwrap(), "div");
783 assert_eq!(module.strings.len(), 1);
784 assert_eq!(module.slots.len(), 0);
785 assert_eq!(module.islands.len(), 0);
786 assert_eq!(module.opcodes.len(), opcodes.len());
787 }
788
789 #[test]
790 fn parse_ir_with_slots() {
791 let opcodes = encode_text(0);
792 let data = build_minimal_ir(
793 &["greeting", "count", "Hello"],
794 &[
795 (1, 0, 0x01, 0x00, &[]), (2, 1, 0x03, 0x00, &[]), ],
798 &opcodes,
799 &[],
800 );
801 let module = IrModule::parse(&data).unwrap();
802
803 assert_eq!(module.slots.len(), 2);
804 let entries = module.slots.entries();
805 assert_eq!(entries[0].slot_id, 1);
806 assert_eq!(entries[0].name_str_idx, 0);
807 assert_eq!(entries[0].type_hint, SlotType::Text);
808 assert_eq!(entries[1].slot_id, 2);
809 assert_eq!(entries[1].name_str_idx, 1);
810 assert_eq!(entries[1].type_hint, SlotType::Number);
811 }
812
813 #[test]
814 fn parse_ir_with_islands() {
815 let opcodes = encode_text(0);
816 let data = build_minimal_ir(
817 &["Counter", "Hello"],
818 &[],
819 &opcodes,
820 &[
821 (1, 0x01, 0x01, 0, 0, &[]), ],
823 );
824 let module = IrModule::parse(&data).unwrap();
825
826 assert_eq!(module.islands.len(), 1);
827 let entry = &module.islands.entries()[0];
828 assert_eq!(entry.id, 1);
829 assert_eq!(entry.trigger, IslandTrigger::Load);
830 assert_eq!(entry.props_mode, PropsMode::Inline);
831 assert_eq!(entry.name_str_idx, 0);
832 assert_eq!(module.strings.get(entry.name_str_idx).unwrap(), "Counter");
833 }
834
835 #[test]
836 fn parse_ir_rejects_truncated() {
837 let data = b"FMIR\x02\x00";
839 let err = IrModule::parse(data).unwrap_err();
840 match err {
841 IrError::BufferTooShort {
842 expected: 16,
843 actual: 6,
844 } => {}
845 other => panic!("expected BufferTooShort(16, 6), got {other:?}"),
846 }
847 }
848
849 #[test]
850 fn parse_ir_rejects_bad_section_bounds() {
851 let opcodes = encode_text(0);
853 let mut data = build_minimal_ir(&["x"], &[], &opcodes, &[]);
854
855 let big_size: u32 = 99999;
859 data[44..48].copy_from_slice(&big_size.to_le_bytes());
860
861 let err = IrModule::parse(&data).unwrap_err();
862 match err {
863 IrError::SectionOutOfBounds { section: 3, .. } => {}
864 other => panic!("expected SectionOutOfBounds for section 3, got {other:?}"),
865 }
866 }
867
868 #[test]
869 fn validate_catches_bad_slot_str_idx() {
870 let opcodes = encode_text(0);
872 let data = build_minimal_ir(
873 &["a", "b", "c"],
874 &[(1, 99, 0x01, 0x00, &[])], &opcodes,
876 &[],
877 );
878 let err = IrModule::parse(&data).unwrap_err();
879 assert_eq!(err, IrError::StringIndexOutOfBounds { index: 99, len: 3 });
880 }
881
882 #[test]
883 fn validate_catches_bad_island_str_idx() {
884 let opcodes = encode_text(0);
886 let data = build_minimal_ir(
887 &["a", "b", "c"],
888 &[],
889 &opcodes,
890 &[(1, 0x01, 0x01, 99, 0, &[])],
891 );
892 let err = IrModule::parse(&data).unwrap_err();
893 assert_eq!(err, IrError::StringIndexOutOfBounds { index: 99, len: 3 });
894 }
895}