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 =
53 u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap()) as usize;
54 offset += 2;
55
56 if offset + str_len > data.len() {
57 return Err(IrError::BufferTooShort {
58 expected: offset + str_len,
59 actual: data.len(),
60 });
61 }
62
63 let s = std::str::from_utf8(&data[offset..offset + str_len])
64 .map_err(|e| IrError::InvalidUtf8(e.to_string()))?;
65 strings.push(s.to_owned());
66 offset += str_len;
67 }
68
69 Ok(StringTable { strings })
70 }
71
72 pub fn get(&self, idx: u32) -> Result<&str, IrError> {
74 self.strings
75 .get(idx as usize)
76 .map(|s| s.as_str())
77 .ok_or(IrError::StringIndexOutOfBounds {
78 index: idx,
79 len: self.strings.len(),
80 })
81 }
82
83 pub fn len(&self) -> usize {
85 self.strings.len()
86 }
87
88 pub fn is_empty(&self) -> bool {
90 self.strings.is_empty()
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct SlotTable {
105 slots: Vec<SlotEntry>,
106}
107
108const SLOT_ENTRY_MIN_SIZE: usize = 10;
111
112impl SlotTable {
113 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
115 if data.len() < 2 {
116 return Err(IrError::BufferTooShort {
117 expected: 2,
118 actual: data.len(),
119 });
120 }
121
122 let count = u16::from_le_bytes(data[0..2].try_into().unwrap()) as usize;
123 let mut slots = Vec::with_capacity(count);
124 let mut offset = 2;
125
126 for _ in 0..count {
127 if offset + SLOT_ENTRY_MIN_SIZE > data.len() {
129 return Err(IrError::BufferTooShort {
130 expected: offset + SLOT_ENTRY_MIN_SIZE,
131 actual: data.len(),
132 });
133 }
134
135 let slot_id =
136 u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap());
137 let name_str_idx =
138 u32::from_le_bytes(data[offset + 2..offset + 6].try_into().unwrap());
139 let type_hint = SlotType::from_byte(data[offset + 6])?;
140 let source = SlotSource::from_byte(data[offset + 7])?;
141 let default_len =
142 u16::from_le_bytes(data[offset + 8..offset + 10].try_into().unwrap()) as usize;
143 offset += SLOT_ENTRY_MIN_SIZE;
144
145 if offset + default_len > data.len() {
147 return Err(IrError::BufferTooShort {
148 expected: offset + default_len,
149 actual: data.len(),
150 });
151 }
152 let default_bytes = data[offset..offset + default_len].to_vec();
153 offset += default_len;
154
155 slots.push(SlotEntry {
156 slot_id,
157 name_str_idx,
158 type_hint,
159 source,
160 default_bytes,
161 });
162 }
163
164 Ok(SlotTable { slots })
165 }
166
167 pub fn len(&self) -> usize {
169 self.slots.len()
170 }
171
172 pub fn is_empty(&self) -> bool {
174 self.slots.is_empty()
175 }
176
177 pub fn entries(&self) -> &[SlotEntry] {
179 &self.slots
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct IslandTableParsed {
193 islands: Vec<IslandEntry>,
194}
195
196const ISLAND_ENTRY_MIN_SIZE: usize = 14;
199
200impl IslandTableParsed {
201 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
203 if data.len() < 2 {
204 return Err(IrError::BufferTooShort {
205 expected: 2,
206 actual: data.len(),
207 });
208 }
209
210 let count = u16::from_le_bytes(data[0..2].try_into().unwrap()) as usize;
211 let mut islands = Vec::with_capacity(count);
212 let mut offset = 2;
213
214 for _ in 0..count {
215 if offset + ISLAND_ENTRY_MIN_SIZE > data.len() {
217 return Err(IrError::BufferTooShort {
218 expected: offset + ISLAND_ENTRY_MIN_SIZE,
219 actual: data.len(),
220 });
221 }
222
223 let id = u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap());
224 let trigger = IslandTrigger::from_byte(data[offset + 2])?;
225 let props_mode = PropsMode::from_byte(data[offset + 3])?;
226 let name_str_idx =
227 u32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap());
228 let byte_offset =
229 u32::from_le_bytes(data[offset + 8..offset + 12].try_into().unwrap());
230 let slot_count =
231 u16::from_le_bytes(data[offset + 12..offset + 14].try_into().unwrap()) as usize;
232 offset += ISLAND_ENTRY_MIN_SIZE;
233
234 let needed = slot_count * 2;
236 if offset + needed > data.len() {
237 return Err(IrError::BufferTooShort {
238 expected: offset + needed,
239 actual: data.len(),
240 });
241 }
242 let mut slot_ids = Vec::with_capacity(slot_count);
243 for _ in 0..slot_count {
244 slot_ids.push(u16::from_le_bytes(
245 data[offset..offset + 2].try_into().unwrap(),
246 ));
247 offset += 2;
248 }
249
250 islands.push(IslandEntry {
251 id,
252 trigger,
253 props_mode,
254 name_str_idx,
255 byte_offset,
256 slot_ids,
257 });
258 }
259
260 Ok(IslandTableParsed { islands })
261 }
262
263 pub fn len(&self) -> usize {
265 self.islands.len()
266 }
267
268 pub fn is_empty(&self) -> bool {
270 self.islands.is_empty()
271 }
272
273 pub fn entries(&self) -> &[IslandEntry] {
275 &self.islands
276 }
277}
278
279#[derive(Debug, Clone)]
285pub struct IrModule {
286 pub header: IrHeader,
287 pub strings: StringTable,
288 pub slots: SlotTable,
289 pub opcodes: Vec<u8>,
291 pub islands: IslandTableParsed,
292}
293
294impl IrModule {
295 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
297 let header = IrHeader::parse(data)?;
299
300 if data.len() < HEADER_SIZE + SECTION_TABLE_SIZE {
302 return Err(IrError::BufferTooShort {
303 expected: HEADER_SIZE + SECTION_TABLE_SIZE,
304 actual: data.len(),
305 });
306 }
307 let section_table = SectionTable::parse(&data[HEADER_SIZE..])?;
308
309 section_table.validate(data.len())?;
311
312 let sec_bytecode = §ion_table.sections[0];
314 let sec_strings = §ion_table.sections[1];
315 let sec_slots = §ion_table.sections[2];
316 let sec_islands = §ion_table.sections[3];
317
318 let string_data = &data[sec_strings.offset as usize
320 ..(sec_strings.offset as usize + sec_strings.size as usize)];
321 let strings = StringTable::parse(string_data)?;
322
323 let slot_data =
325 &data[sec_slots.offset as usize..(sec_slots.offset as usize + sec_slots.size as usize)];
326 let slots = SlotTable::parse(slot_data)?;
327
328 let opcodes = data[sec_bytecode.offset as usize
330 ..(sec_bytecode.offset as usize + sec_bytecode.size as usize)]
331 .to_vec();
332
333 let island_data = &data
335 [sec_islands.offset as usize..(sec_islands.offset as usize + sec_islands.size as usize)];
336 let islands = IslandTableParsed::parse(island_data)?;
337
338 let module = IrModule {
339 header,
340 strings,
341 slots,
342 opcodes,
343 islands,
344 };
345
346 module.validate()?;
348
349 Ok(module)
350 }
351
352 pub fn validate(&self) -> Result<(), IrError> {
354 let str_count = self.strings.len();
355
356 for slot in self.slots.entries() {
358 if slot.name_str_idx as usize >= str_count {
359 return Err(IrError::StringIndexOutOfBounds {
360 index: slot.name_str_idx,
361 len: str_count,
362 });
363 }
364 }
365
366 for island in self.islands.entries() {
368 if island.name_str_idx as usize >= str_count {
369 return Err(IrError::StringIndexOutOfBounds {
370 index: island.name_str_idx,
371 len: str_count,
372 });
373 }
374 }
375
376 Ok(())
377 }
378
379 pub fn slot_id_by_name(&self, name: &str) -> Option<u16> {
381 for slot in self.slots.entries() {
382 if let Ok(slot_name) = self.strings.get(slot.name_str_idx) {
383 if slot_name == name {
384 return Some(slot.slot_id);
385 }
386 }
387 }
388 None
389 }
390}
391
392pub mod test_helpers {
402 use crate::format::{HEADER_SIZE, SECTION_TABLE_SIZE};
403
404 pub fn build_string_table(strings: &[&str]) -> Vec<u8> {
406 let mut buf = Vec::new();
407 buf.extend_from_slice(&(strings.len() as u32).to_le_bytes());
408 for s in strings {
409 let bytes = s.as_bytes();
410 buf.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
411 buf.extend_from_slice(bytes);
412 }
413 buf
414 }
415
416 pub fn build_slot_table(entries: &[(u16, u32, u8, u8, &[u8])]) -> Vec<u8> {
419 let mut buf = Vec::new();
420 buf.extend_from_slice(&(entries.len() as u16).to_le_bytes());
421 for &(slot_id, name_str_idx, type_hint, source, default_bytes) in entries {
422 buf.extend_from_slice(&slot_id.to_le_bytes());
423 buf.extend_from_slice(&name_str_idx.to_le_bytes());
424 buf.push(type_hint);
425 buf.push(source);
426 buf.extend_from_slice(&(default_bytes.len() as u16).to_le_bytes());
427 buf.extend_from_slice(default_bytes);
428 }
429 buf
430 }
431
432 pub fn build_island_table(entries: &[(u16, u8, u8, u32, u32, &[u16])]) -> Vec<u8> {
436 let mut buf = Vec::new();
437 buf.extend_from_slice(&(entries.len() as u16).to_le_bytes());
438 for &(id, trigger, props_mode, name_str_idx, byte_offset, slot_ids) in entries {
439 buf.extend_from_slice(&id.to_le_bytes());
440 buf.push(trigger);
441 buf.push(props_mode);
442 buf.extend_from_slice(&name_str_idx.to_le_bytes());
443 buf.extend_from_slice(&byte_offset.to_le_bytes());
444 buf.extend_from_slice(&(slot_ids.len() as u16).to_le_bytes());
445 for &slot_id in slot_ids.iter() {
446 buf.extend_from_slice(&slot_id.to_le_bytes());
447 }
448 }
449 buf
450 }
451
452 pub fn encode_open_tag(str_idx: u32, attrs: &[(u32, u32)]) -> Vec<u8> {
456 let mut buf = Vec::new();
457 buf.push(0x01); buf.extend_from_slice(&str_idx.to_le_bytes());
459 buf.extend_from_slice(&(attrs.len() as u16).to_le_bytes());
460 for &(key_idx, val_idx) in attrs {
461 buf.extend_from_slice(&key_idx.to_le_bytes());
462 buf.extend_from_slice(&val_idx.to_le_bytes());
463 }
464 buf
465 }
466
467 pub fn encode_close_tag(str_idx: u32) -> Vec<u8> {
469 let mut buf = Vec::new();
470 buf.push(0x02); buf.extend_from_slice(&str_idx.to_le_bytes());
472 buf
473 }
474
475 pub fn encode_void_tag(str_idx: u32, attrs: &[(u32, u32)]) -> Vec<u8> {
477 let mut buf = Vec::new();
478 buf.push(0x03); buf.extend_from_slice(&str_idx.to_le_bytes());
480 buf.extend_from_slice(&(attrs.len() as u16).to_le_bytes());
481 for &(key_idx, val_idx) in attrs {
482 buf.extend_from_slice(&key_idx.to_le_bytes());
483 buf.extend_from_slice(&val_idx.to_le_bytes());
484 }
485 buf
486 }
487
488 pub fn encode_text(str_idx: u32) -> Vec<u8> {
490 let mut buf = Vec::new();
491 buf.push(0x04); buf.extend_from_slice(&str_idx.to_le_bytes());
493 buf
494 }
495
496 pub fn encode_show_if(slot_id: u16, then_ops: &[u8], else_ops: &[u8]) -> Vec<u8> {
504 let mut buf = Vec::new();
505 buf.push(0x07); buf.extend_from_slice(&slot_id.to_le_bytes());
507 buf.extend_from_slice(&(then_ops.len() as u32).to_le_bytes());
508 buf.extend_from_slice(&(else_ops.len() as u32).to_le_bytes());
509 buf.extend_from_slice(then_ops); buf.push(0x08); buf.extend_from_slice(else_ops); buf
513 }
514
515 pub fn encode_list(slot_id: u16, item_slot_id: u16, body_ops: &[u8]) -> Vec<u8> {
517 let mut buf = Vec::new();
518 buf.push(0x0A); buf.extend_from_slice(&slot_id.to_le_bytes());
520 buf.extend_from_slice(&item_slot_id.to_le_bytes());
521 buf.extend_from_slice(&(body_ops.len() as u32).to_le_bytes());
522 buf.extend_from_slice(body_ops);
523 buf
524 }
525
526 pub fn encode_switch(slot_id: u16, cases: &[(u32, &[u8])]) -> Vec<u8> {
537 let mut buf = Vec::new();
538 buf.push(0x09); buf.extend_from_slice(&slot_id.to_le_bytes());
540 buf.extend_from_slice(&(cases.len() as u16).to_le_bytes());
541 for (val_str_idx, body) in cases {
543 buf.extend_from_slice(&val_str_idx.to_le_bytes());
544 buf.extend_from_slice(&(body.len() as u32).to_le_bytes());
545 }
546 for (_, body) in cases {
548 buf.extend_from_slice(body);
549 }
550 buf
551 }
552
553 pub fn encode_try(main_ops: &[u8], fallback_ops: &[u8]) -> Vec<u8> {
563 let mut buf = Vec::new();
564 buf.push(0x0D); buf.extend_from_slice(&(fallback_ops.len() as u32).to_le_bytes());
566 buf.extend_from_slice(main_ops);
567 buf.push(0x0E); buf.extend_from_slice(fallback_ops);
569 buf
570 }
571
572 pub fn encode_preload(resource_type: u8, url_str_idx: u32) -> Vec<u8> {
574 let mut buf = Vec::new();
575 buf.push(0x0F); buf.push(resource_type);
577 buf.extend_from_slice(&url_str_idx.to_le_bytes());
578 buf
579 }
580
581 pub fn build_minimal_ir(
590 strings: &[&str],
591 slots: &[(u16, u32, u8, u8, &[u8])],
592 opcodes: &[u8],
593 islands: &[(u16, u8, u8, u32, u32, &[u16])],
594 ) -> Vec<u8> {
595 let string_section = build_string_table(strings);
596 let slot_section = build_slot_table(slots);
597 let island_section = build_island_table(islands);
598
599 let data_start = HEADER_SIZE + SECTION_TABLE_SIZE;
601
602 let bytecode_offset = data_start;
604 let bytecode_size = opcodes.len();
605
606 let string_offset = bytecode_offset + bytecode_size;
607 let string_size = string_section.len();
608
609 let slot_offset = string_offset + string_size;
610 let slot_size = slot_section.len();
611
612 let island_offset = slot_offset + slot_size;
613 let island_size = island_section.len();
614
615 let mut buf = Vec::new();
617 buf.extend_from_slice(b"FMIR");
618 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());
624 buf.extend_from_slice(&(bytecode_size as u32).to_le_bytes());
625 buf.extend_from_slice(&(string_offset as u32).to_le_bytes());
626 buf.extend_from_slice(&(string_size as u32).to_le_bytes());
627 buf.extend_from_slice(&(slot_offset as u32).to_le_bytes());
628 buf.extend_from_slice(&(slot_size as u32).to_le_bytes());
629 buf.extend_from_slice(&(island_offset as u32).to_le_bytes());
630 buf.extend_from_slice(&(island_size as u32).to_le_bytes());
631
632 buf.extend_from_slice(opcodes);
634 buf.extend_from_slice(&string_section);
635 buf.extend_from_slice(&slot_section);
636 buf.extend_from_slice(&island_section);
637
638 buf
639 }
640}
641
642#[cfg(test)]
647mod tests {
648 use super::*;
649 use super::test_helpers::*;
650 use crate::format::{IrError, IslandTrigger, PropsMode, SlotSource, SlotType};
651
652 #[test]
655 fn parse_string_table() {
656 let data = build_string_table(&["div", "class", "container"]);
657 let table = StringTable::parse(&data).unwrap();
658
659 assert_eq!(table.len(), 3);
660 assert_eq!(table.get(0).unwrap(), "div");
661 assert_eq!(table.get(1).unwrap(), "class");
662 assert_eq!(table.get(2).unwrap(), "container");
663
664 let err = table.get(3).unwrap_err();
666 assert_eq!(
667 err,
668 IrError::StringIndexOutOfBounds { index: 3, len: 3 }
669 );
670 }
671
672 #[test]
673 fn parse_string_table_empty() {
674 let data = build_string_table(&[]);
675 let table = StringTable::parse(&data).unwrap();
676 assert_eq!(table.len(), 0);
677 }
678
679 #[test]
680 fn parse_string_table_unicode() {
681 let data = build_string_table(&["héllo"]);
682 let table = StringTable::parse(&data).unwrap();
683
684 assert_eq!(table.len(), 1);
685 assert_eq!(table.get(0).unwrap(), "héllo");
686 }
687
688 #[test]
689 fn parse_string_table_truncated() {
690 let mut data = Vec::new();
692 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();
698 match err {
699 IrError::BufferTooShort { .. } => {} other => panic!("expected BufferTooShort, got {other:?}"),
701 }
702 }
703
704 #[test]
707 fn parse_slot_table() {
708 let data = build_slot_table(&[
709 (1, 0, 0x01, 0x00, &[]), (2, 1, 0x03, 0x01, &[0x42]), ]);
712 let table = SlotTable::parse(&data).unwrap();
713
714 assert_eq!(table.len(), 2);
715
716 let entries = table.entries();
717 assert_eq!(entries[0].slot_id, 1);
718 assert_eq!(entries[0].name_str_idx, 0);
719 assert_eq!(entries[0].type_hint, SlotType::Text);
720 assert_eq!(entries[0].source, SlotSource::Server);
721 assert_eq!(entries[0].default_bytes, Vec::<u8>::new());
722
723 assert_eq!(entries[1].slot_id, 2);
724 assert_eq!(entries[1].name_str_idx, 1);
725 assert_eq!(entries[1].type_hint, SlotType::Number);
726 assert_eq!(entries[1].source, SlotSource::Client);
727 assert_eq!(entries[1].default_bytes, vec![0x42]);
728 }
729
730 #[test]
731 fn parse_slot_table_empty() {
732 let data = build_slot_table(&[]);
733 let table = SlotTable::parse(&data).unwrap();
734 assert_eq!(table.len(), 0);
735 }
736
737 #[test]
740 fn parse_island_table() {
741 let data = build_island_table(&[
742 (1, 0x02, 0x01, 5, 0, &[]), ]);
744 let table = IslandTableParsed::parse(&data).unwrap();
745
746 assert_eq!(table.len(), 1);
747
748 let entry = &table.entries()[0];
749 assert_eq!(entry.id, 1);
750 assert_eq!(entry.trigger, IslandTrigger::Visible);
751 assert_eq!(entry.props_mode, PropsMode::Inline);
752 assert_eq!(entry.name_str_idx, 5);
753 assert_eq!(entry.slot_ids, Vec::<u16>::new());
754 }
755
756 #[test]
757 fn parse_island_table_with_slot_ids() {
758 let data = build_island_table(&[
759 (0, 0x01, 0x01, 0, 0, &[0, 1]), ]);
761 let table = IslandTableParsed::parse(&data).unwrap();
762 assert_eq!(table.len(), 1);
763 let entry = &table.entries()[0];
764 assert_eq!(entry.id, 0);
765 assert_eq!(entry.trigger, IslandTrigger::Load);
766 assert_eq!(entry.props_mode, PropsMode::Inline);
767 assert_eq!(entry.slot_ids, vec![0, 1]);
768 }
769
770 #[test]
771 fn parse_island_table_empty() {
772 let data = build_island_table(&[]);
773 let table = IslandTableParsed::parse(&data).unwrap();
774 assert_eq!(table.len(), 0);
775 }
776
777 #[test]
780 fn parse_minimal_ir_file() {
781 let mut opcodes = Vec::new();
783 opcodes.extend_from_slice(&encode_open_tag(0, &[]));
784 opcodes.extend_from_slice(&encode_close_tag(0));
785
786 let data = build_minimal_ir(&["div"], &[], &opcodes, &[]);
787 let module = IrModule::parse(&data).unwrap();
788
789 assert_eq!(module.header.version, 2);
790 assert_eq!(module.strings.get(0).unwrap(), "div");
791 assert_eq!(module.strings.len(), 1);
792 assert_eq!(module.slots.len(), 0);
793 assert_eq!(module.islands.len(), 0);
794 assert_eq!(module.opcodes.len(), opcodes.len());
795 }
796
797 #[test]
798 fn parse_ir_with_slots() {
799 let opcodes = encode_text(0);
800 let data = build_minimal_ir(
801 &["greeting", "count", "Hello"],
802 &[
803 (1, 0, 0x01, 0x00, &[]), (2, 1, 0x03, 0x00, &[]), ],
806 &opcodes,
807 &[],
808 );
809 let module = IrModule::parse(&data).unwrap();
810
811 assert_eq!(module.slots.len(), 2);
812 let entries = module.slots.entries();
813 assert_eq!(entries[0].slot_id, 1);
814 assert_eq!(entries[0].name_str_idx, 0);
815 assert_eq!(entries[0].type_hint, SlotType::Text);
816 assert_eq!(entries[1].slot_id, 2);
817 assert_eq!(entries[1].name_str_idx, 1);
818 assert_eq!(entries[1].type_hint, SlotType::Number);
819 }
820
821 #[test]
822 fn parse_ir_with_islands() {
823 let opcodes = encode_text(0);
824 let data = build_minimal_ir(
825 &["Counter", "Hello"],
826 &[],
827 &opcodes,
828 &[
829 (1, 0x01, 0x01, 0, 0, &[]), ],
831 );
832 let module = IrModule::parse(&data).unwrap();
833
834 assert_eq!(module.islands.len(), 1);
835 let entry = &module.islands.entries()[0];
836 assert_eq!(entry.id, 1);
837 assert_eq!(entry.trigger, IslandTrigger::Load);
838 assert_eq!(entry.props_mode, PropsMode::Inline);
839 assert_eq!(entry.name_str_idx, 0);
840 assert_eq!(module.strings.get(entry.name_str_idx).unwrap(), "Counter");
841 }
842
843 #[test]
844 fn parse_ir_rejects_truncated() {
845 let data = b"FMIR\x02\x00";
847 let err = IrModule::parse(data).unwrap_err();
848 match err {
849 IrError::BufferTooShort { expected: 16, actual: 6 } => {}
850 other => panic!("expected BufferTooShort(16, 6), got {other:?}"),
851 }
852 }
853
854 #[test]
855 fn parse_ir_rejects_bad_section_bounds() {
856 let opcodes = encode_text(0);
858 let mut data = build_minimal_ir(&["x"], &[], &opcodes, &[]);
859
860 let big_size: u32 = 99999;
864 data[44..48].copy_from_slice(&big_size.to_le_bytes());
865
866 let err = IrModule::parse(&data).unwrap_err();
867 match err {
868 IrError::SectionOutOfBounds { section: 3, .. } => {}
869 other => panic!("expected SectionOutOfBounds for section 3, got {other:?}"),
870 }
871 }
872
873 #[test]
874 fn validate_catches_bad_slot_str_idx() {
875 let opcodes = encode_text(0);
877 let data = build_minimal_ir(
878 &["a", "b", "c"],
879 &[(1, 99, 0x01, 0x00, &[])], &opcodes,
881 &[],
882 );
883 let err = IrModule::parse(&data).unwrap_err();
884 assert_eq!(
885 err,
886 IrError::StringIndexOutOfBounds { index: 99, len: 3 }
887 );
888 }
889
890 #[test]
891 fn validate_catches_bad_island_str_idx() {
892 let opcodes = encode_text(0);
894 let data = build_minimal_ir(
895 &["a", "b", "c"],
896 &[],
897 &opcodes,
898 &[(1, 0x01, 0x01, 99, 0, &[])],
899 );
900 let err = IrModule::parse(&data).unwrap_err();
901 assert_eq!(
902 err,
903 IrError::StringIndexOutOfBounds { index: 99, len: 3 }
904 );
905 }
906}