1use crate::format::{
7 le_u16, le_u32, limit, IrError, IrHeader, IslandEntry, IslandTrigger, Opcode, SectionTable,
8 SlotEntry, SlotSource, SlotType, HEADER_SIZE, MAX_ISLANDS, MAX_STRINGS, SECTION_BYTECODE,
9 SECTION_ISLANDS, SECTION_ISLAND_CONTRACTS, SECTION_KEY_PATHS, SECTION_PROPS_SCHEMA,
10 SECTION_REQUIRED, SECTION_SLOTS, SECTION_SOURCE_POSITIONS, SECTION_STRINGS,
11};
12use crate::schema::{self, Cursor, KeyPath, PropsSchema, SourcePosition};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct StringTable {
23 strings: Vec<String>,
24}
25
26impl StringTable {
27 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
29 if data.len() < 4 {
30 return Err(IrError::BufferTooShort {
31 expected: 4,
32 actual: data.len(),
33 });
34 }
35
36 let count = le_u32(&data[0..4]) as usize;
37 limit("strings", count, MAX_STRINGS)?;
38 let max_entries = (data.len() - 4) / 2;
49 if count > max_entries {
50 return Err(IrError::BufferTooShort {
51 expected: 4usize.saturating_add(count.saturating_mul(2)),
54 actual: data.len(),
55 });
56 }
57 let mut offset = 4;
58 let mut strings = Vec::with_capacity(count);
62
63 for _ in 0..count {
64 if offset + 2 > data.len() {
66 return Err(IrError::BufferTooShort {
67 expected: offset + 2,
68 actual: data.len(),
69 });
70 }
71
72 let str_len = le_u16(&data[offset..offset + 2]) as usize;
73 offset += 2;
74
75 if offset + str_len > data.len() {
76 return Err(IrError::BufferTooShort {
77 expected: offset + str_len,
78 actual: data.len(),
79 });
80 }
81
82 let s = std::str::from_utf8(&data[offset..offset + str_len])
83 .map_err(|e| IrError::InvalidUtf8(e.to_string()))?;
84 strings.push(s.to_owned());
85 offset += str_len;
86 }
87
88 if offset != data.len() {
89 return Err(IrError::TrailingBytes {
90 section: SECTION_STRINGS,
91 });
92 }
93 Ok(StringTable { strings })
94 }
95
96 pub fn get(&self, idx: u32) -> Result<&str, IrError> {
98 self.strings
99 .get(idx as usize)
100 .map(|s| s.as_str())
101 .ok_or(IrError::StringIndexOutOfBounds {
102 index: idx,
103 len: self.strings.len(),
104 })
105 }
106
107 pub fn len(&self) -> usize {
109 self.strings.len()
110 }
111
112 pub fn is_empty(&self) -> bool {
114 self.strings.is_empty()
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct SlotTable {
129 slots: Vec<SlotEntry>,
130}
131
132const SLOT_ENTRY_MIN_SIZE: usize = 10;
135
136impl SlotTable {
137 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
139 if data.len() < 2 {
140 return Err(IrError::BufferTooShort {
141 expected: 2,
142 actual: data.len(),
143 });
144 }
145
146 let count = le_u16(&data[0..2]) as usize;
147 let max_entries = (data.len() - 2) / SLOT_ENTRY_MIN_SIZE;
151 if count > max_entries {
152 return Err(IrError::BufferTooShort {
153 expected: 2usize.saturating_add(count.saturating_mul(SLOT_ENTRY_MIN_SIZE)),
154 actual: data.len(),
155 });
156 }
157 let mut slots = Vec::with_capacity(count);
158 let mut offset = 2;
159
160 for _ in 0..count {
161 if offset + SLOT_ENTRY_MIN_SIZE > data.len() {
163 return Err(IrError::BufferTooShort {
164 expected: offset + SLOT_ENTRY_MIN_SIZE,
165 actual: data.len(),
166 });
167 }
168
169 let slot_id = le_u16(&data[offset..offset + 2]);
170 let name_str_idx = le_u32(&data[offset + 2..offset + 6]);
171 let type_hint = SlotType::from_byte(data[offset + 6])?;
172 let source = SlotSource::from_byte(data[offset + 7])?;
173 let default_len = le_u16(&data[offset + 8..offset + 10]) as usize;
174 offset += SLOT_ENTRY_MIN_SIZE;
175
176 if offset + default_len > data.len() {
178 return Err(IrError::BufferTooShort {
179 expected: offset + default_len,
180 actual: data.len(),
181 });
182 }
183 let default_bytes = data[offset..offset + default_len].to_vec();
184 offset += default_len;
185 validate_default(slot_id, type_hint, &default_bytes)?;
186
187 slots.push(SlotEntry {
188 slot_id,
189 name_str_idx,
190 type_hint,
191 source,
192 default_bytes,
193 });
194 }
195
196 if offset != data.len() {
197 return Err(IrError::TrailingBytes {
198 section: SECTION_SLOTS,
199 });
200 }
201 Ok(SlotTable { slots })
202 }
203
204 pub fn len(&self) -> usize {
206 self.slots.len()
207 }
208
209 pub fn is_empty(&self) -> bool {
211 self.slots.is_empty()
212 }
213
214 pub fn entries(&self) -> &[SlotEntry] {
216 &self.slots
217 }
218}
219
220fn validate_default(id: u16, kind: SlotType, bytes: &[u8]) -> Result<(), IrError> {
222 if bytes.is_empty() {
223 return Ok(());
224 }
225 let text = std::str::from_utf8(bytes).map_err(|e| IrError::InvalidUtf8(e.to_string()))?;
226 let valid = match kind {
227 SlotType::Text => true,
228 SlotType::Bool => matches!(text, "true" | "false"),
229 SlotType::Number => decimal(text) && text.parse::<f64>().is_ok_and(|n| n.is_finite()),
230 SlotType::Array => text == "[]",
231 SlotType::Object => false,
232 };
233 if valid {
234 Ok(())
235 } else {
236 Err(IrError::InvalidDefault(id))
237 }
238}
239fn decimal(text: &str) -> bool {
240 let b = text.as_bytes();
241 let mut p = 0;
242 if matches!(b.get(p), Some(b'+' | b'-')) {
243 p += 1;
244 }
245 let mut digits = 0;
246 while b.get(p).is_some_and(u8::is_ascii_digit) {
247 p += 1;
248 digits += 1;
249 }
250 if b.get(p) == Some(&b'.') {
251 p += 1;
252 while b.get(p).is_some_and(u8::is_ascii_digit) {
253 p += 1;
254 digits += 1;
255 }
256 }
257 if digits == 0 {
258 return false;
259 }
260 if matches!(b.get(p), Some(b'e' | b'E')) {
261 p += 1;
262 if matches!(b.get(p), Some(b'+' | b'-')) {
263 p += 1;
264 }
265 let start = p;
266 while b.get(p).is_some_and(u8::is_ascii_digit) {
267 p += 1;
268 }
269 if p == start {
270 return false;
271 }
272 }
273 p == b.len()
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct IslandTableParsed {
287 islands: Vec<IslandEntry>,
288}
289
290impl IslandTableParsed {
291 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
293 let mut r = Cursor::new(data);
294 let count = r.u16()? as usize;
295 limit("islands", count, MAX_ISLANDS)?;
296 r.bound(count, 19)?;
297 let mut islands = Vec::with_capacity(count);
298 for _ in 0..count {
299 let entry_len = r.u16()? as usize;
300 let mut e = Cursor::new(r.take(entry_len)?);
301 let id = e.u16()?;
302 let trigger = IslandTrigger::from_byte(e.u8()?)?;
303 if e.u8()? != 0 {
304 return Err(IrError::ReservedBits { field: "island" });
305 }
306 let name_str_idx = e.u32()?;
307 let byte_offset = e.u32()?;
308 let schema_object = e.u16()?;
309 let digest_len = e.u8()? as usize;
310 let digest = e.take(digest_len)?.to_vec();
311 let slot_count = e.u16()? as usize;
312 e.bound(slot_count, 2)?;
313 let mut slot_ids = Vec::with_capacity(slot_count);
314 for _ in 0..slot_count {
315 slot_ids.push(e.u16()?);
316 }
317 let extension = e.take(e.remaining())?.to_vec();
318 islands.push(IslandEntry {
319 id,
320 trigger,
321 name_str_idx,
322 byte_offset,
323 schema_object,
324 digest,
325 slot_ids,
326 extension,
327 });
328 }
329 r.finish(SECTION_ISLANDS)?;
330 Ok(Self { islands })
331 }
332
333 pub fn len(&self) -> usize {
335 self.islands.len()
336 }
337
338 pub fn is_empty(&self) -> bool {
340 self.islands.is_empty()
341 }
342
343 pub fn entries(&self) -> &[IslandEntry] {
345 &self.islands
346 }
347}
348
349const NO_INDEX: u32 = u32::MAX;
355
356#[derive(Debug, Clone)]
361#[non_exhaustive]
362pub struct IrModule {
363 pub header: IrHeader,
365 pub strings: StringTable,
367 pub slots: SlotTable,
369 pub opcodes: Vec<u8>,
371 pub islands: IslandTableParsed,
373 pub sections: SectionTable,
375 pub schema: PropsSchema,
377 pub key_paths: Vec<KeyPath>,
379 pub source_positions: Vec<SourcePosition>,
381 raw: Vec<u8>,
383 pub(crate) contracts: Option<crate::contracts::Contracts>,
384 slot_index: Vec<u32>,
387 island_index: Vec<u32>,
391}
392
393impl IrModule {
394 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
396 let header = IrHeader::parse(data)?;
397 let section_table = SectionTable::parse(&data[HEADER_SIZE..])?;
398 section_table.validate(data.len())?;
399 for section in §ion_table.sections {
400 if !(SECTION_BYTECODE..=SECTION_SOURCE_POSITIONS).contains(§ion.id)
401 && section.id != SECTION_ISLAND_CONTRACTS
402 && section.flags & SECTION_REQUIRED != 0
403 {
404 return Err(IrError::UnknownRequiredSection {
405 id: section.id,
406 writer: header.writer,
407 });
408 }
409 }
410 for id in SECTION_BYTECODE..=SECTION_PROPS_SCHEMA {
411 if section_table.get(id).is_none() {
412 return Err(IrError::MissingSection(id));
413 }
414 }
415 let payload = |id| -> Result<&[u8], IrError> {
416 let section = section_table.get(id).ok_or(IrError::MissingSection(id))?;
417 let start = section.offset as usize;
418 data.get(start..start + section.size as usize)
419 .ok_or(IrError::InvalidSection)
420 };
421 if let Some(section) = section_table.get(SECTION_ISLAND_CONTRACTS) {
422 if section.flags != SECTION_REQUIRED {
423 return Err(IrError::InvalidSchema("island contracts must be required"));
424 }
425 }
426 let contracts = if section_table.get(SECTION_ISLAND_CONTRACTS).is_some() {
427 Some(crate::contracts::Contracts::parse(payload(
428 SECTION_ISLAND_CONTRACTS,
429 )?)?)
430 } else {
431 None
432 };
433 let strings = StringTable::parse(payload(SECTION_STRINGS)?)?;
434 let slots = SlotTable::parse(payload(SECTION_SLOTS)?)?;
435 let islands = IslandTableParsed::parse(payload(SECTION_ISLANDS)?)?;
436 let schema = PropsSchema::parse(payload(SECTION_PROPS_SCHEMA)?, &strings)?;
437 let opcodes = payload(SECTION_BYTECODE)?.to_vec();
438 let slot_index = build_id_index(slots.entries().iter().map(|e| e.slot_id));
439 let island_index = build_id_index(islands.entries().iter().map(|e| e.id));
440 let mut module = Self {
441 header,
442 strings,
443 slots,
444 islands,
445 schema,
446 opcodes,
447 slot_index,
448 island_index,
449 sections: section_table.clone(),
450 key_paths: Vec::new(),
451 source_positions: Vec::new(),
452 raw: data.to_vec(),
453 contracts,
454 };
455 let starts = module.validation_starts()?;
456 if section_table.get(SECTION_KEY_PATHS).is_some() {
457 module.key_paths =
458 schema::key_paths(payload(SECTION_KEY_PATHS)?, &module.strings, &starts)?;
459 }
460 if section_table.get(SECTION_SOURCE_POSITIONS).is_some() {
461 module.source_positions = schema::source_positions(
462 payload(SECTION_SOURCE_POSITIONS)?,
463 &module.strings,
464 &starts,
465 )?;
466 }
467 module.validate_contracts(&starts)?;
470 Ok(module)
471 }
472
473 pub fn island_contract_bytes(&self, id: u16) -> Option<(&[u8], &[u8])> {
476 let contract = self.contracts.as_ref()?.islands.get(&id)?;
477 Some((&contract.binding_bytes, &contract.schema_bytes))
478 }
479
480 pub fn raw_bytes(&self) -> &[u8] {
482 &self.raw
483 }
484
485 pub fn slot_entry(&self, slot_id: u16) -> Option<&SlotEntry> {
487 match self.slot_index.get(slot_id as usize) {
488 Some(&idx) if idx != NO_INDEX => self.slots.entries().get(idx as usize),
489 _ => None,
490 }
491 }
492
493 pub fn island_entry(&self, island_id: u16) -> Option<&IslandEntry> {
495 match self.island_index.get(island_id as usize) {
496 Some(&idx) if idx != NO_INDEX => self.islands.entries().get(idx as usize),
497 _ => None,
498 }
499 }
500
501 pub fn validate(&self) -> Result<(), IrError> {
507 let starts = self.validation_starts()?;
508 self.validate_contracts(&starts)
509 }
510
511 fn validation_starts(&self) -> Result<Vec<(u32, Opcode)>, IrError> {
512 let str_count = self.strings.len();
513
514 let mut seen_slot = vec![false; self.slot_index.len()];
518 for slot in self.slots.entries() {
519 if slot.name_str_idx as usize >= str_count {
520 return Err(IrError::StringIndexOutOfBounds {
521 index: slot.name_str_idx,
522 len: str_count,
523 });
524 }
525 let idx = slot.slot_id as usize;
526 if seen_slot[idx] {
527 return Err(IrError::DuplicateSlotId(slot.slot_id));
528 }
529 seen_slot[idx] = true;
530 }
531
532 let mut seen_island = vec![false; self.island_index.len()];
536 for island in self.islands.entries() {
537 if island.name_str_idx as usize >= str_count {
538 return Err(IrError::StringIndexOutOfBounds {
539 index: island.name_str_idx,
540 len: str_count,
541 });
542 }
543 let idx = island.id as usize;
544 if seen_island[idx] {
545 return Err(IrError::DuplicateIslandId(island.id));
546 }
547 seen_island[idx] = true;
548
549 for &slot_id in &island.slot_ids {
550 if self.slot_entry(slot_id).is_none() {
551 return Err(IrError::UnknownSlotId(slot_id));
552 }
553 }
554
555 let off = island.byte_offset as usize;
559 let id_matches = self.opcodes.get(off) == Some(&(Opcode::IslandStart as u8))
560 && crate::walker::read_u16(&self.opcodes, off + 1) == Ok(island.id);
561 if !id_matches {
562 return Err(IrError::IslandOffsetMismatch {
563 island_id: island.id,
564 byte_offset: island.byte_offset,
565 });
566 }
567 }
568
569 let mut starts = Vec::new();
570 self.scan_region(0, self.opcodes.len(), 0, &mut starts)?;
571 for island in self.islands.entries() {
572 if starts
573 .binary_search_by_key(&island.byte_offset, |(o, _)| *o)
574 .is_err()
575 {
576 return Err(IrError::IslandOffsetMismatch {
577 island_id: island.id,
578 byte_offset: island.byte_offset,
579 });
580 }
581 if island.schema_object != 0xffff
582 && island.schema_object as usize >= self.schema.objects.len()
583 {
584 return Err(IrError::InvalidSchema("island object index out of range"));
585 }
586 }
587 Ok(starts)
588 }
589
590 fn validate_contracts(&self, starts: &[(u32, Opcode)]) -> Result<(), IrError> {
591 if let Some(contracts) = &self.contracts {
592 contracts.validate(self, starts)?;
593 } else if !self.islands.is_empty() || starts.iter().any(|(_, op)| *op == Opcode::Coalesce) {
594 return Err(IrError::MissingSection(SECTION_ISLAND_CONTRACTS));
595 }
596 Ok(())
597 }
598
599 fn scan_region(
616 &self,
617 start: usize,
618 end: usize,
619 depth: usize,
620 starts: &mut Vec<(u32, Opcode)>,
621 ) -> Result<(), IrError> {
622 limit("control-flow depth", depth, MAX_VALIDATION_DEPTH)?;
623 let ops = &self.opcodes;
624 let mut pos = start;
625 while pos < end {
626 let op_start = pos;
627 let op = Opcode::from_byte(ops[pos])?;
628 starts.push((pos as u32, op));
629 pos += 1;
630 match op {
631 Opcode::OpenTag | Opcode::VoidTag => {
632 self.strings.get(read_u32_at(ops, pos, end)?)?;
633 let n = read_u16_at(ops, pos + 4, end)? as usize;
634 let next = bounded(pos, 6 + n * 8, end, op_start)?;
635 for i in 0..n {
636 self.strings.get(read_u32_at(ops, pos + 6 + i * 8, end)?)?;
637 self.strings.get(read_u32_at(ops, pos + 10 + i * 8, end)?)?;
638 }
639 pos = next;
640 }
641 Opcode::CloseTag | Opcode::Text => {
642 self.strings.get(read_u32_at(ops, pos, end)?)?;
643 pos = bounded(pos, 4, end, op_start)?;
644 }
645 Opcode::DynText => {
646 self.check_slot(read_u16_at(ops, pos, end)?)?;
647 pos = bounded(pos, 4, end, op_start)?;
648 }
649 Opcode::DynAttr => {
650 self.strings.get(read_u32_at(ops, pos, end)?)?;
651 self.check_slot(read_u16_at(ops, pos + 4, end)?)?;
652 pos = bounded(pos, 6, end, op_start)?;
653 }
654 Opcode::IslandStart => {
655 let id = read_u16_at(ops, pos, end)?;
656 if self.island_entry(id).is_none() {
657 return Err(IrError::UnknownIslandId(id));
658 }
659 pos = bounded(pos, 2, end, op_start)?;
660 }
661 Opcode::IslandEnd => {
662 pos = bounded(pos, 2, end, op_start)?;
663 }
664 Opcode::ShowElse => {} Opcode::Coalesce => {
666 self.check_slot(read_u16_at(ops, pos, end)?)?;
667 self.check_slot(read_u16_at(ops, pos + 2, end)?)?;
668 self.check_slot(read_u16_at(ops, pos + 4, end)?)?;
669 pos = bounded(pos, 6, end, op_start)?;
670 }
671 Opcode::Prop => {
672 self.check_slot(read_u16_at(ops, pos, end)?)?;
673 self.strings.get(read_u32_at(ops, pos + 2, end)?)?;
674 self.check_slot(read_u16_at(ops, pos + 6, end)?)?;
675 pos = bounded(pos, 8, end, op_start)?;
676 }
677 Opcode::ShowIf => {
678 self.check_slot(read_u16_at(ops, pos, end)?)?;
679 let tl = read_u32_at(ops, pos + 2, end)? as usize;
680 let el = read_u32_at(ops, pos + 6, end)? as usize;
681 let ts = bounded(pos, 10, end, op_start)?;
682 let te = bounded(ts, tl, end, op_start)?;
683 if te >= end || ops[te] != Opcode::ShowElse as u8 {
684 return Err(IrError::MalformedControlFlow { pos: te });
685 }
686 let es = te + 1;
687 let ee = bounded(es, el, end, op_start)?;
688 self.scan_region(ts, te, depth + 1, starts)?;
689 starts.push((te as u32, Opcode::ShowElse));
690 self.scan_region(es, ee, depth + 1, starts)?;
691 pos = ee;
692 }
693 Opcode::List => {
694 self.check_slot(read_u16_at(ops, pos, end)?)?;
695 self.check_slot(read_u16_at(ops, pos + 2, end)?)?;
696 let len = read_u32_at(ops, pos + 4, end)? as usize;
697 let bs = bounded(pos, 8, end, op_start)?;
698 let be = bounded(bs, len, end, op_start)?;
699 self.scan_region(bs, be, depth + 1, starts)?;
700 pos = be;
701 }
702 }
703 }
704 Ok(())
705 }
706
707 fn check_slot(&self, slot_id: u16) -> Result<(), IrError> {
712 if self.slot_entry(slot_id).is_none() {
713 return Err(IrError::UnknownSlotId(slot_id));
714 }
715 Ok(())
716 }
717
718 pub fn slot_id_by_name(&self, name: &str) -> Option<u16> {
732 for slot in self.slots.entries() {
733 if let Ok(slot_name) = self.strings.get(slot.name_str_idx) {
734 if slot_name == name {
735 return Some(slot.slot_id);
736 }
737 }
738 }
739 None
740 }
741}
742
743const MAX_VALIDATION_DEPTH: usize = crate::format::MAX_CONTROL_DEPTH;
746
747fn build_id_index(ids: impl Iterator<Item = u16> + Clone) -> Vec<u32> {
750 let max_id = ids.clone().max();
751 let Some(max_id) = max_id else {
752 return Vec::new();
753 };
754 let mut index = vec![NO_INDEX; max_id as usize + 1];
758 for (i, id) in ids.enumerate() {
759 let entry = &mut index[id as usize];
760 if *entry == NO_INDEX {
761 *entry = i as u32;
762 }
763 }
764 index
765}
766
767fn bounded(base: usize, len: usize, end: usize, op_start: usize) -> Result<usize, IrError> {
771 base.checked_add(len)
772 .filter(|e| *e <= end)
773 .ok_or(IrError::MalformedControlFlow { pos: op_start })
774}
775
776fn read_u16_at(ops: &[u8], pos: usize, end: usize) -> Result<u16, IrError> {
778 if pos.checked_add(2).map(|e| e > end).unwrap_or(true) {
779 return Err(IrError::MalformedControlFlow { pos });
780 }
781 crate::walker::read_u16(ops, pos)
782}
783
784fn read_u32_at(ops: &[u8], pos: usize, end: usize) -> Result<u32, IrError> {
786 if pos.checked_add(4).map(|e| e > end).unwrap_or(true) {
787 return Err(IrError::MalformedControlFlow { pos });
788 }
789 crate::walker::read_u32(ops, pos)
790}
791
792#[cfg(any(test, feature = "test-helpers"))]
806#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
809pub mod test_helpers {
810 use crate::format::{
811 crc32, reader_version, HEADER_SIZE, SECTION_ENTRY_SIZE, SIGNATURE, TABLE_PREFIX_SIZE,
812 };
813
814 fn to_u16(n: usize) -> [u8; 2] {
821 u16::try_from(n)
822 .unwrap_or_else(|_| panic!("{n} does not fit the u16 field this encoder writes"))
823 .to_le_bytes()
824 }
825
826 fn to_u32(n: usize) -> [u8; 4] {
828 u32::try_from(n)
829 .unwrap_or_else(|_| panic!("{n} does not fit the u32 field this encoder writes"))
830 .to_le_bytes()
831 }
832
833 pub fn build_string_table(strings: &[&str]) -> Vec<u8> {
835 let mut buf = Vec::new();
836 buf.extend_from_slice(&to_u32(strings.len()));
837 for s in strings {
838 let bytes = s.as_bytes();
839 buf.extend_from_slice(&to_u16(bytes.len()));
840 buf.extend_from_slice(bytes);
841 }
842 buf
843 }
844
845 pub fn build_slot_table(entries: &[(u16, u32, u8, u8, &[u8])]) -> Vec<u8> {
848 let mut buf = Vec::new();
849 buf.extend_from_slice(&to_u16(entries.len()));
850 for &(slot_id, name_str_idx, type_hint, source, default_bytes) in entries {
851 buf.extend_from_slice(&slot_id.to_le_bytes());
852 buf.extend_from_slice(&name_str_idx.to_le_bytes());
853 buf.push(type_hint);
854 buf.push(source);
855 buf.extend_from_slice(&to_u16(default_bytes.len()));
856 buf.extend_from_slice(default_bytes);
857 }
858 buf
859 }
860
861 pub fn build_island_table(entries: &[(u16, u8, u8, u32, u32, &[u16])]) -> Vec<u8> {
865 let mut buf = Vec::new();
866 buf.extend_from_slice(&to_u16(entries.len()));
867 for &(id, trigger, _obsolete_mode, name_str_idx, byte_offset, slot_ids) in entries {
868 buf.extend_from_slice(&to_u16(17 + 2 * slot_ids.len()));
869 buf.extend_from_slice(&id.to_le_bytes());
870 buf.push(trigger);
871 buf.push(0);
872 buf.extend_from_slice(&name_str_idx.to_le_bytes());
873 buf.extend_from_slice(&byte_offset.to_le_bytes());
874 buf.extend_from_slice(&0xffffu16.to_le_bytes());
875 buf.push(0); buf.extend_from_slice(&to_u16(slot_ids.len()));
877 for &slot_id in slot_ids.iter() {
878 buf.extend_from_slice(&slot_id.to_le_bytes());
879 }
880 }
881 buf
882 }
883
884 pub fn encode_open_tag(str_idx: u32, attrs: &[(u32, u32)]) -> Vec<u8> {
888 let mut buf = Vec::new();
889 buf.push(0x01); buf.extend_from_slice(&str_idx.to_le_bytes());
891 buf.extend_from_slice(&to_u16(attrs.len()));
892 for &(key_idx, val_idx) in attrs {
893 buf.extend_from_slice(&key_idx.to_le_bytes());
894 buf.extend_from_slice(&val_idx.to_le_bytes());
895 }
896 buf
897 }
898
899 pub fn encode_close_tag(str_idx: u32) -> Vec<u8> {
901 let mut buf = Vec::new();
902 buf.push(0x02); buf.extend_from_slice(&str_idx.to_le_bytes());
904 buf
905 }
906
907 pub fn encode_void_tag(str_idx: u32, attrs: &[(u32, u32)]) -> Vec<u8> {
909 let mut buf = Vec::new();
910 buf.push(0x03); buf.extend_from_slice(&str_idx.to_le_bytes());
912 buf.extend_from_slice(&to_u16(attrs.len()));
913 for &(key_idx, val_idx) in attrs {
914 buf.extend_from_slice(&key_idx.to_le_bytes());
915 buf.extend_from_slice(&val_idx.to_le_bytes());
916 }
917 buf
918 }
919
920 pub fn encode_text(str_idx: u32) -> Vec<u8> {
922 let mut buf = Vec::new();
923 buf.push(0x04); buf.extend_from_slice(&str_idx.to_le_bytes());
925 buf
926 }
927
928 pub fn encode_show_if(slot_id: u16, then_ops: &[u8], else_ops: &[u8]) -> Vec<u8> {
936 let mut buf = Vec::new();
937 buf.push(0x07); buf.extend_from_slice(&slot_id.to_le_bytes());
939 buf.extend_from_slice(&to_u32(then_ops.len()));
940 buf.extend_from_slice(&to_u32(else_ops.len()));
941 buf.extend_from_slice(then_ops); buf.push(0x08); buf.extend_from_slice(else_ops); buf
945 }
946
947 pub fn encode_list(slot_id: u16, item_slot_id: u16, body_ops: &[u8]) -> Vec<u8> {
949 let mut buf = Vec::new();
950 buf.push(0x0A); buf.extend_from_slice(&slot_id.to_le_bytes());
952 buf.extend_from_slice(&item_slot_id.to_le_bytes());
953 buf.extend_from_slice(&to_u32(body_ops.len()));
954 buf.extend_from_slice(body_ops);
955 buf
956 }
957
958 pub fn encode_switch(slot_id: u16, cases: &[(u32, &[u8])]) -> Vec<u8> {
969 let mut buf = Vec::new();
970 buf.push(0x09); buf.extend_from_slice(&slot_id.to_le_bytes());
972 buf.extend_from_slice(&to_u16(cases.len()));
973 for (val_str_idx, body) in cases {
975 buf.extend_from_slice(&val_str_idx.to_le_bytes());
976 buf.extend_from_slice(&to_u32(body.len()));
977 }
978 for (_, body) in cases {
980 buf.extend_from_slice(body);
981 }
982 buf
983 }
984
985 pub fn encode_try(main_ops: &[u8], fallback_ops: &[u8]) -> Vec<u8> {
995 let mut buf = Vec::new();
996 buf.push(0x0D); buf.extend_from_slice(&to_u32(fallback_ops.len()));
998 buf.extend_from_slice(main_ops);
999 buf.push(0x0E); buf.extend_from_slice(fallback_ops);
1001 buf
1002 }
1003
1004 pub fn encode_preload(resource_type: u8, url_str_idx: u32) -> Vec<u8> {
1006 let mut buf = Vec::new();
1007 buf.push(0x0F); buf.push(resource_type);
1009 buf.extend_from_slice(&url_str_idx.to_le_bytes());
1010 buf
1011 }
1012
1013 pub fn build_minimal_ir(
1022 strings: &[&str],
1023 slots: &[(u16, u32, u8, u8, &[u8])],
1024 opcodes: &[u8],
1025 islands: &[(u16, u8, u8, u32, u32, &[u16])],
1026 ) -> Vec<u8> {
1027 let string_section = build_string_table(strings);
1028 let slot_section = build_slot_table(slots);
1029 let island_section = build_island_table(islands);
1030
1031 build_ir_sections(&[
1032 (1, 1, opcodes),
1033 (2, 1, &string_section),
1034 (3, 1, &slot_section),
1035 (4, 1, &island_section),
1036 (5, 1, &[1, 0, 0, 0]),
1037 ])
1038 }
1039
1040 pub fn build_declared_fixture(
1048 strings: &[&str],
1049 slots: &[(u16, u32, u8, u8, &[u8])],
1050 opcodes: &[u8],
1051 islands: &[(u16, u8, u8, u32, u32, &[u16])],
1052 ) -> Vec<u8> {
1053 if islands.is_empty() {
1054 return build_minimal_ir(strings, slots, opcodes, islands);
1055 }
1056 fn kind(hint: u8) -> Vec<u8> {
1057 match hint {
1058 1 => vec![2],
1059 2 => vec![1],
1060 3 => vec![3],
1061 4 => vec![6, 2],
1062 _ => panic!("fixture requires an explicit non-scalar schema"),
1063 }
1064 }
1065 fn fields(out: &mut Vec<u8>, declarations: &[(u32, Vec<u8>)]) {
1066 out.extend(to_u16(declarations.len()));
1067 for (name, kind) in declarations {
1068 out.extend(name.to_le_bytes());
1069 out.extend(0u32.to_le_bytes());
1070 out.extend(1u32.to_le_bytes());
1071 out.extend(1u32.to_le_bytes());
1072 out.extend(kind);
1073 }
1074 }
1075 let mut page = Vec::new();
1076 for &(_, name, hint, source, _) in slots {
1077 if source == 0 && !page.iter().any(|(prior, _)| *prior == name) {
1078 page.push((name, kind(hint)));
1079 }
1080 }
1081 let mut schema = to_u16(islands.len() + 1).to_vec();
1082 fields(&mut schema, &page);
1083 let mut island_table = to_u16(islands.len()).to_vec();
1084 let mut records = Vec::new();
1085 for (index, &(id, trigger, _, name, offset, inputs)) in islands.iter().enumerate() {
1086 let declared = inputs
1087 .iter()
1088 .map(|id| {
1089 let &(_, name, hint, _, _) = slots
1090 .iter()
1091 .find(|slot| slot.0 == *id)
1092 .expect("declared fixture slot");
1093 (name, kind(hint))
1094 })
1095 .collect::<Vec<_>>();
1096 fields(&mut schema, &declared);
1097 let mut record = id.to_le_bytes().to_vec();
1098 record.extend([trigger, 0]);
1099 record.extend(name.to_le_bytes());
1100 record.extend(offset.to_le_bytes());
1101 record.extend(to_u16(index + 1));
1102 record.push(8);
1103 record.extend([id as u8; 8]); record.extend(to_u16(inputs.len()));
1105 for input in inputs {
1106 record.extend(input.to_le_bytes());
1107 }
1108 island_table.extend(to_u16(record.len()));
1109 island_table.extend(record);
1110 let mut canonical = vec![1, 1, 0];
1111 canonical.extend(to_u16(declared.len()));
1112 let mut sorted = declared.iter().collect::<Vec<_>>();
1113 sorted.sort_by_key(|(name, _)| strings[*name as usize]);
1114 for (name, kind) in sorted {
1115 let name = strings[*name as usize].as_bytes();
1116 canonical.extend(to_u16(name.len()));
1117 canonical.extend(name);
1118 canonical.extend(kind);
1119 }
1120 let mut record = id.to_le_bytes().to_vec();
1121 record.extend(4u32.to_le_bytes());
1122 record.extend([1, 0, 0, 0]);
1123 record.extend(to_u32(canonical.len()));
1124 record.extend(canonical);
1125 records.push((id, record));
1126 }
1127 records.sort_by_key(|(id, _)| *id);
1128 let mut contracts = to_u16(records.len()).to_vec();
1129 for (_, record) in records {
1130 contracts.extend(to_u32(record.len()));
1131 contracts.extend(record);
1132 }
1133 contracts.extend([0, 0]);
1134 build_ir_sections(&[
1135 (1, 1, opcodes),
1136 (2, 1, &build_string_table(strings)),
1137 (3, 1, &build_slot_table(slots)),
1138 (4, 1, &island_table),
1139 (5, 1, &schema),
1140 (10, 1, &contracts),
1141 ])
1142 }
1143
1144 pub fn build_ir_sections(sections: &[(u16, u16, &[u8])]) -> Vec<u8> {
1146 let mut buf = Vec::new();
1147 buf.extend_from_slice(&SIGNATURE);
1148 for v in reader_version() {
1149 buf.extend_from_slice(&v.to_le_bytes());
1150 }
1151 buf.extend_from_slice(&[0; 10]); buf.extend_from_slice(&to_u16(sections.len()));
1153 buf.extend_from_slice(&[0; 2]);
1154 let mut offset = HEADER_SIZE + TABLE_PREFIX_SIZE + SECTION_ENTRY_SIZE * sections.len();
1155 for &(id, flags, bytes) in sections {
1156 buf.extend_from_slice(&id.to_le_bytes());
1157 buf.extend_from_slice(&flags.to_le_bytes());
1158 buf.extend_from_slice(&to_u32(offset));
1159 buf.extend_from_slice(&to_u32(bytes.len()));
1160 offset += bytes.len();
1161 }
1162 for &(_, _, bytes) in sections {
1163 buf.extend_from_slice(bytes);
1164 }
1165 restamp(&mut buf);
1166 buf
1167 }
1168
1169 pub fn with_key_paths(ir: &[u8], paths: &[(u32, &[u32])]) -> Vec<u8> {
1174 let table = &ir[HEADER_SIZE..];
1175 let count = u16::from_le_bytes([table[0], table[1]]) as usize;
1176 let mut sections: Vec<(u16, u16, &[u8])> = (0..count)
1177 .map(|index| {
1178 let entry = &table[TABLE_PREFIX_SIZE + index * SECTION_ENTRY_SIZE..];
1179 let field = |at: usize| u32::from_le_bytes(entry[at..at + 4].try_into().unwrap());
1180 let (offset, size) = (field(4) as usize, field(8) as usize);
1181 (
1182 u16::from_le_bytes([entry[0], entry[1]]),
1183 u16::from_le_bytes([entry[2], entry[3]]),
1184 &ir[offset..offset + size],
1185 )
1186 })
1187 .collect();
1188 let mut payload = to_u32(paths.len()).to_vec();
1189 for (offset, segments) in paths {
1190 payload.extend_from_slice(&offset.to_le_bytes());
1191 payload.push(u8::try_from(segments.len()).unwrap());
1192 for segment in *segments {
1193 payload.extend_from_slice(&segment.to_le_bytes());
1194 }
1195 }
1196 assert!(
1197 sections.iter().all(|(id, _, _)| *id != 6),
1198 "the module already has a key-path section"
1199 );
1200 let at = sections.partition_point(|(id, _, _)| *id < 6);
1201 sections.insert(at, (6, 1, &payload));
1202 build_ir_sections(§ions)
1203 }
1204
1205 pub fn restamp(buf: &mut [u8]) {
1207 let body_len = to_u32(buf.len() - HEADER_SIZE);
1208 buf[14..18].copy_from_slice(&body_len);
1209 let checksum = crc32(&[&buf[..18], &buf[22..]]);
1210 buf[18..22].copy_from_slice(&checksum.to_le_bytes());
1211 }
1212}
1213
1214#[cfg(test)]
1219mod tests {
1220 use super::test_helpers::*;
1221 use super::*;
1222 use crate::format::{IrError, IslandTrigger, SlotSource, SlotType};
1223
1224 #[test]
1227 fn parse_string_table() {
1228 let data = build_string_table(&["div", "class", "container"]);
1229 let table = StringTable::parse(&data).unwrap();
1230
1231 assert_eq!(table.len(), 3);
1232 assert_eq!(table.get(0).unwrap(), "div");
1233 assert_eq!(table.get(1).unwrap(), "class");
1234 assert_eq!(table.get(2).unwrap(), "container");
1235
1236 let err = table.get(3).unwrap_err();
1238 assert_eq!(err, IrError::StringIndexOutOfBounds { index: 3, len: 3 });
1239 }
1240
1241 #[test]
1242 fn parse_string_table_empty() {
1243 let data = build_string_table(&[]);
1244 let table = StringTable::parse(&data).unwrap();
1245 assert_eq!(table.len(), 0);
1246 }
1247
1248 #[test]
1249 fn parse_string_table_unicode() {
1250 let data = build_string_table(&["héllo"]);
1251 let table = StringTable::parse(&data).unwrap();
1252
1253 assert_eq!(table.len(), 1);
1254 assert_eq!(table.get(0).unwrap(), "héllo");
1255 }
1256
1257 #[test]
1258 fn parse_string_table_truncated() {
1259 let mut data = Vec::new();
1261 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();
1267 match err {
1268 IrError::BufferTooShort { .. } => {} other => panic!("expected BufferTooShort, got {other:?}"),
1270 }
1271 }
1272
1273 #[test]
1276 fn parse_slot_table() {
1277 let data = build_slot_table(&[
1278 (1, 0, 0x01, 0x00, &[]), (2, 1, 0x03, 0x01, b"7"), ]);
1281 let table = SlotTable::parse(&data).unwrap();
1282
1283 assert_eq!(table.len(), 2);
1284
1285 let entries = table.entries();
1286 assert_eq!(entries[0].slot_id, 1);
1287 assert_eq!(entries[0].name_str_idx, 0);
1288 assert_eq!(entries[0].type_hint, SlotType::Text);
1289 assert_eq!(entries[0].source, SlotSource::Server);
1290 assert_eq!(entries[0].default_bytes, Vec::<u8>::new());
1291
1292 assert_eq!(entries[1].slot_id, 2);
1293 assert_eq!(entries[1].name_str_idx, 1);
1294 assert_eq!(entries[1].type_hint, SlotType::Number);
1295 assert_eq!(entries[1].source, SlotSource::Client);
1296 assert_eq!(entries[1].default_bytes, b"7");
1297 }
1298
1299 #[test]
1300 fn parse_slot_table_empty() {
1301 let data = build_slot_table(&[]);
1302 let table = SlotTable::parse(&data).unwrap();
1303 assert_eq!(table.len(), 0);
1304 }
1305
1306 #[test]
1309 fn parse_island_table() {
1310 let data = build_island_table(&[
1311 (1, 0x02, 0x01, 5, 0, &[]), ]);
1313 let table = IslandTableParsed::parse(&data).unwrap();
1314
1315 assert_eq!(table.len(), 1);
1316
1317 let entry = &table.entries()[0];
1318 assert_eq!(entry.id, 1);
1319 assert_eq!(entry.trigger, IslandTrigger::Visible);
1320 assert_eq!(entry.schema_object, 0xffff);
1321 assert_eq!(entry.name_str_idx, 5);
1322 assert_eq!(entry.slot_ids, Vec::<u16>::new());
1323 }
1324
1325 #[test]
1326 fn parse_island_table_with_slot_ids() {
1327 let data = build_island_table(&[
1328 (0, 0x01, 0x01, 0, 0, &[0, 1]), ]);
1330 let table = IslandTableParsed::parse(&data).unwrap();
1331 assert_eq!(table.len(), 1);
1332 let entry = &table.entries()[0];
1333 assert_eq!(entry.id, 0);
1334 assert_eq!(entry.trigger, IslandTrigger::Load);
1335 assert_eq!(entry.schema_object, 0xffff);
1336 assert_eq!(entry.slot_ids, vec![0, 1]);
1337 }
1338
1339 #[test]
1340 fn parse_island_table_empty() {
1341 let data = build_island_table(&[]);
1342 let table = IslandTableParsed::parse(&data).unwrap();
1343 assert_eq!(table.len(), 0);
1344 }
1345
1346 #[test]
1349 fn parse_minimal_ir_file() {
1350 let mut opcodes = Vec::new();
1352 opcodes.extend_from_slice(&encode_open_tag(0, &[]));
1353 opcodes.extend_from_slice(&encode_close_tag(0));
1354
1355 let data = build_minimal_ir(&["div"], &[], &opcodes, &[]);
1356 let module = IrModule::parse(&data).unwrap();
1357
1358 assert_eq!(module.header.writer, crate::format::reader_version());
1359 assert_eq!(module.strings.get(0).unwrap(), "div");
1360 assert_eq!(module.strings.len(), 1);
1361 assert_eq!(module.slots.len(), 0);
1362 assert_eq!(module.islands.len(), 0);
1363 assert_eq!(module.opcodes.len(), opcodes.len());
1364 }
1365
1366 #[test]
1367 fn parse_ir_with_slots() {
1368 let opcodes = encode_text(0);
1369 let data = build_minimal_ir(
1370 &["greeting", "count", "Hello"],
1371 &[
1372 (1, 0, 0x01, 0x00, &[]), (2, 1, 0x03, 0x00, &[]), ],
1375 &opcodes,
1376 &[],
1377 );
1378 let module = IrModule::parse(&data).unwrap();
1379
1380 assert_eq!(module.slots.len(), 2);
1381 let entries = module.slots.entries();
1382 assert_eq!(entries[0].slot_id, 1);
1383 assert_eq!(entries[0].name_str_idx, 0);
1384 assert_eq!(entries[0].type_hint, SlotType::Text);
1385 assert_eq!(entries[1].slot_id, 2);
1386 assert_eq!(entries[1].name_str_idx, 1);
1387 assert_eq!(entries[1].type_hint, SlotType::Number);
1388 }
1389
1390 #[test]
1391 fn parse_ir_with_islands() {
1392 let mut opcodes = vec![0x0B]; opcodes.extend_from_slice(&1u16.to_le_bytes());
1397 opcodes.extend_from_slice(&encode_text(1));
1398 opcodes.push(0x0C); opcodes.extend_from_slice(&1u16.to_le_bytes());
1400 let data = build_declared_fixture(
1401 &["Counter", "Hello"],
1402 &[],
1403 &opcodes,
1404 &[
1405 (1, 0x01, 0x01, 0, 0, &[]), ],
1407 );
1408 let module = IrModule::parse(&data).unwrap();
1409
1410 assert_eq!(module.islands.len(), 1);
1411 let entry = &module.islands.entries()[0];
1412 assert_eq!(entry.id, 1);
1413 assert_eq!(entry.trigger, IslandTrigger::Load);
1414 assert_eq!(entry.schema_object, 1);
1415 assert_eq!(entry.name_str_idx, 0);
1416 assert_eq!(module.strings.get(entry.name_str_idx).unwrap(), "Counter");
1417 }
1418
1419 #[test]
1420 fn parse_ir_rejects_truncated() {
1421 let data = b"FMIR\x02\x00";
1423 let err = IrModule::parse(data).unwrap_err();
1424 match err {
1425 IrError::BufferTooShort {
1426 expected: 24,
1427 actual: 6,
1428 } => {}
1429 other => panic!("expected BufferTooShort(24, 6), got {other:?}"),
1430 }
1431 }
1432
1433 #[test]
1434 fn parse_ir_rejects_bad_section_bounds() {
1435 let mut data = build_minimal_ir(&["x"], &[], &encode_text(0), &[]);
1436 data[72..76].copy_from_slice(&99999u32.to_le_bytes());
1438 restamp(&mut data);
1439 assert_eq!(
1440 IrModule::parse(&data).unwrap_err(),
1441 IrError::SectionsDoNotTile
1442 );
1443 }
1444
1445 #[test]
1446 fn validate_catches_bad_slot_str_idx() {
1447 let opcodes = encode_text(0);
1449 let data = build_minimal_ir(
1450 &["a", "b", "c"],
1451 &[(1, 99, 0x01, 0x00, &[])], &opcodes,
1453 &[],
1454 );
1455 let err = IrModule::parse(&data).unwrap_err();
1456 assert_eq!(err, IrError::StringIndexOutOfBounds { index: 99, len: 3 });
1457 }
1458
1459 #[test]
1460 fn validate_catches_bad_island_str_idx() {
1461 let opcodes = encode_text(0);
1463 let mut island_opcodes = vec![0x0B];
1464 island_opcodes.extend_from_slice(&1u16.to_le_bytes());
1465 island_opcodes.extend_from_slice(&opcodes);
1466 island_opcodes.push(0x0C);
1467 island_opcodes.extend_from_slice(&1u16.to_le_bytes());
1468 let data = build_minimal_ir(
1469 &["a", "b", "c"],
1470 &[],
1471 &island_opcodes,
1472 &[(1, 0x01, 0x01, 99, 0, &[])],
1473 );
1474 let err = IrModule::parse(&data).unwrap_err();
1475 assert_eq!(err, IrError::StringIndexOutOfBounds { index: 99, len: 3 });
1476 }
1477
1478 #[test]
1479 fn duplicate_slot_names_resolve_first_wins_everywhere() {
1480 let opcodes = encode_text(0);
1485 let data = build_minimal_ir(
1486 &["dup"],
1487 &[(0, 0, 0x01, 0x00, &[]), (1, 0, 0x01, 0x00, &[])],
1488 &opcodes,
1489 &[],
1490 );
1491 let module = IrModule::parse(&data).unwrap();
1492
1493 assert_eq!(module.slot_id_by_name("dup"), Some(0));
1494
1495 let slots = crate::slot::SlotData::from_json(r#"{"dup":"x"}"#, &module).unwrap();
1496 assert_eq!(slots.get(0).to_text(), "x", "the first slot must be filled");
1497 assert_eq!(slots.get(1).to_text(), "", "the second must stay untouched");
1498 }
1499
1500 #[test]
1501 fn validate_rejects_list_item_slot_not_in_table() {
1502 let body = {
1506 let mut b = vec![0x05]; b.extend_from_slice(&7u16.to_le_bytes());
1508 b.extend_from_slice(&0u16.to_le_bytes());
1509 b
1510 };
1511 let opcodes = encode_list(0, 7, &body);
1512 let data = build_minimal_ir(&["items"], &[(0, 0, 0x04, 0x00, &[])], &opcodes, &[]);
1513 assert_eq!(
1514 IrModule::parse(&data).unwrap_err(),
1515 IrError::UnknownSlotId(7)
1516 );
1517 }
1518
1519 #[test]
1520 fn validate_rejects_island_start_missing_from_table() {
1521 let mut opcodes = vec![0x0B]; opcodes.extend_from_slice(&3u16.to_le_bytes());
1526 opcodes.extend_from_slice(&encode_text(0));
1527 opcodes.push(0x0C); opcodes.extend_from_slice(&3u16.to_le_bytes());
1529
1530 let data = build_minimal_ir(&["hi"], &[], &opcodes, &[]);
1531 assert_eq!(
1532 IrModule::parse(&data).unwrap_err(),
1533 IrError::UnknownIslandId(3)
1534 );
1535 }
1536
1537 #[test]
1540 fn string_table_rejects_count_the_section_cannot_hold() {
1541 let mut data = Vec::new();
1546 data.extend_from_slice(&1_000_000u32.to_le_bytes()); data.extend_from_slice(&[0u8; 16]); let err = StringTable::parse(&data).unwrap_err();
1549 assert!(matches!(err, IrError::BufferTooShort { .. }));
1550 }
1551
1552 #[test]
1553 fn slot_table_rejects_count_the_section_cannot_hold() {
1554 let data = 65535u16.to_le_bytes().to_vec();
1556 let err = SlotTable::parse(&data).unwrap_err();
1557 assert!(matches!(err, IrError::BufferTooShort { .. }));
1558 }
1559
1560 #[test]
1561 fn island_table_rejects_count_the_section_cannot_hold() {
1562 let count = 4096u16.to_le_bytes();
1563 assert!(matches!(
1564 IslandTableParsed::parse(&count),
1565 Err(IrError::BufferTooShort { .. })
1566 ));
1567 assert_eq!(
1568 IslandTableParsed::parse(&4097u16.to_le_bytes())
1569 .unwrap_err()
1570 .code(),
1571 "LOAD-107"
1572 );
1573 }
1574
1575 #[test]
1578 fn validate_rejects_duplicate_slot_ids() {
1579 let opcodes = encode_text(0);
1580 let data = build_minimal_ir(
1581 &["a", "b"],
1582 &[(0, 0, 0x01, 0x00, &[]), (0, 1, 0x01, 0x00, &[])],
1583 &opcodes,
1584 &[],
1585 );
1586 assert_eq!(
1587 IrModule::parse(&data).unwrap_err(),
1588 IrError::DuplicateSlotId(0)
1589 );
1590 }
1591
1592 #[test]
1593 fn validate_rejects_duplicate_island_ids() {
1594 let mut opcodes = vec![0x0B];
1595 opcodes.extend_from_slice(&0u16.to_le_bytes());
1596 opcodes.push(0x0C);
1597 opcodes.extend_from_slice(&0u16.to_le_bytes());
1598 let data = build_minimal_ir(
1599 &["Comp"],
1600 &[],
1601 &opcodes,
1602 &[(0, 0x01, 0x01, 0, 0, &[]), (0, 0x01, 0x01, 0, 0, &[])],
1603 );
1604 assert_eq!(
1605 IrModule::parse(&data).unwrap_err(),
1606 IrError::DuplicateIslandId(0)
1607 );
1608 }
1609
1610 #[test]
1611 fn validate_rejects_island_slot_id_absent_from_slot_table() {
1612 let mut opcodes = vec![0x0B];
1617 opcodes.extend_from_slice(&0u16.to_le_bytes());
1618 opcodes.push(0x0C);
1619 opcodes.extend_from_slice(&0u16.to_le_bytes());
1620 let data = build_minimal_ir(&["Comp"], &[], &opcodes, &[(0, 0x01, 0x01, 0, 0, &[7])]);
1621 assert_eq!(
1622 IrModule::parse(&data).unwrap_err(),
1623 IrError::UnknownSlotId(7)
1624 );
1625 }
1626
1627 #[test]
1628 fn validate_rejects_island_offset_landing_mid_operand() {
1629 let mut opcodes = vec![0x0B];
1635 opcodes.extend_from_slice(&1u16.to_le_bytes());
1636 opcodes.push(0x04);
1638 opcodes.extend_from_slice(&0x0Bu32.to_le_bytes());
1639 opcodes.push(0x0C);
1640 opcodes.extend_from_slice(&1u16.to_le_bytes());
1641
1642 let data = build_minimal_ir(&["Comp"], &[], &opcodes, &[(1, 0x01, 0x01, 0, 4, &[])]);
1643 assert_eq!(
1644 IrModule::parse(&data).unwrap_err(),
1645 IrError::IslandOffsetMismatch {
1646 island_id: 1,
1647 byte_offset: 4
1648 }
1649 );
1650 }
1651
1652 #[test]
1653 fn validate_rejects_opcode_slot_id_absent_from_slot_table() {
1654 let mut opcodes = vec![0x05];
1656 opcodes.extend_from_slice(&9u16.to_le_bytes());
1657 opcodes.extend_from_slice(&0u16.to_le_bytes());
1658 let data = build_minimal_ir(&["a"], &[], &opcodes, &[]);
1659 assert_eq!(
1660 IrModule::parse(&data).unwrap_err(),
1661 IrError::UnknownSlotId(9)
1662 );
1663 }
1664
1665 #[test]
1666 fn slot_and_island_lookups_are_indexed() {
1667 let mut opcodes = vec![0x0B];
1668 opcodes.extend_from_slice(&3u16.to_le_bytes());
1669 opcodes.push(0x0C);
1670 opcodes.extend_from_slice(&3u16.to_le_bytes());
1671 let data = build_declared_fixture(
1672 &["Comp", "title"],
1673 &[(4, 1, 0x01, 0x00, &[])],
1674 &opcodes,
1675 &[(3, 0x01, 0x01, 0, 0, &[4])],
1676 );
1677 let module = IrModule::parse(&data).unwrap();
1678
1679 assert_eq!(module.slot_entry(4).unwrap().slot_id, 4);
1680 assert!(module.slot_entry(0).is_none());
1681 assert_eq!(module.island_entry(3).unwrap().id, 3);
1682 assert!(module.island_entry(0).is_none());
1683 assert!(module.slot_entry(u16::MAX).is_none());
1685 assert!(module.island_entry(u16::MAX).is_none());
1686 }
1687}