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