1use std::fmt;
12
13pub const MAGIC: &[u8; 4] = b"FMIR";
19
20pub const HEADER_SIZE: usize = 16;
22
23pub const SECTION_TABLE_SIZE: usize = 32;
25
26pub const IR_VERSION: u16 = 2;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum IrError {
36 BufferTooShort { expected: usize, actual: usize },
38 BadMagic([u8; 4]),
40 UnsupportedVersion(u16),
42 SectionOutOfBounds {
44 section: usize,
45 offset: u32,
46 size: u32,
47 file_len: usize,
48 },
49 InvalidOpcode(u8),
51 InvalidSlotType(u8),
53 InvalidIslandTrigger(u8),
55 InvalidPropsMode(u8),
57 InvalidSlotSource(u8),
59 StringIndexOutOfBounds { index: u32, len: usize },
61 InvalidUtf8(String),
63 ListDepthExceeded { max: u8 },
65 IslandNotFound(u16),
67 JsonParseError(String),
69}
70
71impl fmt::Display for IrError {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 IrError::BufferTooShort { expected, actual } => {
75 write!(
76 f,
77 "buffer too short: expected at least {expected} bytes, got {actual}"
78 )
79 }
80 IrError::BadMagic(got) => {
81 write!(f, "bad magic: expected FMIR, got {:?}", got)
82 }
83 IrError::UnsupportedVersion(v) => {
84 write!(f, "unsupported IR version: {v} (expected {IR_VERSION})")
85 }
86 IrError::SectionOutOfBounds {
87 section,
88 offset,
89 size,
90 file_len,
91 } => {
92 write!(
93 f,
94 "section {section} out of bounds: offset={offset}, size={size}, file_len={file_len}"
95 )
96 }
97 IrError::InvalidOpcode(b) => write!(f, "invalid opcode: 0x{b:02x}"),
98 IrError::InvalidSlotType(b) => write!(f, "invalid slot type: 0x{b:02x}"),
99 IrError::InvalidIslandTrigger(b) => {
100 write!(f, "invalid island trigger: 0x{b:02x}")
101 }
102 IrError::InvalidPropsMode(b) => write!(f, "invalid props mode: 0x{b:02x}"),
103 IrError::InvalidSlotSource(b) => write!(f, "invalid slot source: 0x{b:02x}"),
104 IrError::StringIndexOutOfBounds { index, len } => {
105 write!(f, "string index {index} out of bounds (table has {len} entries)")
106 }
107 IrError::InvalidUtf8(msg) => write!(f, "invalid UTF-8: {msg}"),
108 IrError::ListDepthExceeded { max } => {
109 write!(f, "nested LIST depth exceeded maximum of {max}")
110 }
111 IrError::IslandNotFound(id) => {
112 write!(f, "island with id {id} not found in island table")
113 }
114 IrError::JsonParseError(msg) => {
115 write!(f, "JSON parse error: {msg}")
116 }
117 }
118 }
119}
120
121impl std::error::Error for IrError {}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct IrHeader {
138 pub version: u16,
139 pub flags: u16,
140 pub source_hash: u64,
141}
142
143impl IrHeader {
144 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
146 if data.len() < HEADER_SIZE {
147 return Err(IrError::BufferTooShort {
148 expected: HEADER_SIZE,
149 actual: data.len(),
150 });
151 }
152
153 let magic: [u8; 4] = data[0..4].try_into().unwrap();
154 if &magic != MAGIC {
155 return Err(IrError::BadMagic(magic));
156 }
157
158 let version = u16::from_le_bytes(data[4..6].try_into().unwrap());
159 if version != IR_VERSION {
160 return Err(IrError::UnsupportedVersion(version));
161 }
162
163 let flags = u16::from_le_bytes(data[6..8].try_into().unwrap());
164 let source_hash = u64::from_le_bytes(data[8..16].try_into().unwrap());
165
166 Ok(IrHeader {
167 version,
168 flags,
169 source_hash,
170 })
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub struct SectionDescriptor {
181 pub offset: u32,
182 pub size: u32,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub struct SectionTable {
196 pub sections: [SectionDescriptor; 4],
197}
198
199impl SectionTable {
200 pub fn parse(data: &[u8]) -> Result<Self, IrError> {
202 if data.len() < SECTION_TABLE_SIZE {
203 return Err(IrError::BufferTooShort {
204 expected: SECTION_TABLE_SIZE,
205 actual: data.len(),
206 });
207 }
208
209 let mut sections = [SectionDescriptor { offset: 0, size: 0 }; 4];
210 for (i, section) in sections.iter_mut().enumerate() {
211 let base = i * 8;
212 let offset = u32::from_le_bytes(data[base..base + 4].try_into().unwrap());
213 let size = u32::from_le_bytes(data[base + 4..base + 8].try_into().unwrap());
214 *section = SectionDescriptor { offset, size };
215 }
216
217 Ok(SectionTable { sections })
218 }
219
220 pub fn validate(&self, file_len: usize) -> Result<(), IrError> {
222 for (i, sec) in self.sections.iter().enumerate() {
223 let end = sec.offset as usize + sec.size as usize;
224 if end > file_len {
225 return Err(IrError::SectionOutOfBounds {
226 section: i,
227 offset: sec.offset,
228 size: sec.size,
229 file_len,
230 });
231 }
232 }
233 Ok(())
234 }
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243#[repr(u8)]
244pub enum Opcode {
245 OpenTag = 0x01,
246 CloseTag = 0x02,
247 VoidTag = 0x03,
248 Text = 0x04,
249 DynText = 0x05,
250 DynAttr = 0x06,
251 ShowIf = 0x07,
252 ShowElse = 0x08,
253 Switch = 0x09,
254 List = 0x0A,
255 IslandStart = 0x0B,
256 IslandEnd = 0x0C,
257 TryStart = 0x0D,
258 Fallback = 0x0E,
259 Preload = 0x0F,
260 Comment = 0x10,
261 ListItemKey = 0x11,
262 Prop = 0x12,
265}
266
267impl Opcode {
268 pub fn from_byte(b: u8) -> Result<Self, IrError> {
270 match b {
271 0x01 => Ok(Opcode::OpenTag),
272 0x02 => Ok(Opcode::CloseTag),
273 0x03 => Ok(Opcode::VoidTag),
274 0x04 => Ok(Opcode::Text),
275 0x05 => Ok(Opcode::DynText),
276 0x06 => Ok(Opcode::DynAttr),
277 0x07 => Ok(Opcode::ShowIf),
278 0x08 => Ok(Opcode::ShowElse),
279 0x09 => Ok(Opcode::Switch),
280 0x0A => Ok(Opcode::List),
281 0x0B => Ok(Opcode::IslandStart),
282 0x0C => Ok(Opcode::IslandEnd),
283 0x0D => Ok(Opcode::TryStart),
284 0x0E => Ok(Opcode::Fallback),
285 0x0F => Ok(Opcode::Preload),
286 0x10 => Ok(Opcode::Comment),
287 0x11 => Ok(Opcode::ListItemKey),
288 0x12 => Ok(Opcode::Prop),
289 _ => Err(IrError::InvalidOpcode(b)),
290 }
291 }
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300#[repr(u8)]
301pub enum SlotType {
302 Text = 0x01,
303 Bool = 0x02,
304 Number = 0x03,
305 Array = 0x04,
306 Object = 0x05,
307}
308
309impl SlotType {
310 pub fn from_byte(b: u8) -> Result<Self, IrError> {
312 match b {
313 0x01 => Ok(SlotType::Text),
314 0x02 => Ok(SlotType::Bool),
315 0x03 => Ok(SlotType::Number),
316 0x04 => Ok(SlotType::Array),
317 0x05 => Ok(SlotType::Object),
318 _ => Err(IrError::InvalidSlotType(b)),
319 }
320 }
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
329#[repr(u8)]
330pub enum IslandTrigger {
331 Load = 0x01,
332 Visible = 0x02,
333 Interaction = 0x03,
334 Idle = 0x04,
335}
336
337impl IslandTrigger {
338 pub fn from_byte(b: u8) -> Result<Self, IrError> {
340 match b {
341 0x01 => Ok(IslandTrigger::Load),
342 0x02 => Ok(IslandTrigger::Visible),
343 0x03 => Ok(IslandTrigger::Interaction),
344 0x04 => Ok(IslandTrigger::Idle),
345 _ => Err(IrError::InvalidIslandTrigger(b)),
346 }
347 }
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356#[repr(u8)]
357pub enum PropsMode {
358 Inline = 0x01,
359 ScriptTag = 0x02,
360 Deferred = 0x03,
361}
362
363impl PropsMode {
364 pub fn from_byte(b: u8) -> Result<Self, IrError> {
366 match b {
367 0x01 => Ok(PropsMode::Inline),
368 0x02 => Ok(PropsMode::ScriptTag),
369 0x03 => Ok(PropsMode::Deferred),
370 _ => Err(IrError::InvalidPropsMode(b)),
371 }
372 }
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
381#[repr(u8)]
382pub enum SlotSource {
383 Server = 0x00,
384 Client = 0x01,
385}
386
387impl SlotSource {
388 pub fn from_byte(b: u8) -> Result<Self, IrError> {
390 match b {
391 0x00 => Ok(SlotSource::Server),
392 0x01 => Ok(SlotSource::Client),
393 _ => Err(IrError::InvalidSlotSource(b)),
394 }
395 }
396}
397
398#[derive(Debug, Clone, PartialEq, Eq)]
404pub struct SlotEntry {
405 pub slot_id: u16,
407 pub name_str_idx: u32,
409 pub type_hint: SlotType,
411 pub source: SlotSource,
413 pub default_bytes: Vec<u8>,
415}
416
417#[derive(Debug, Clone, PartialEq, Eq)]
423pub struct IslandEntry {
424 pub id: u16,
426 pub trigger: IslandTrigger,
428 pub props_mode: PropsMode,
430 pub name_str_idx: u32,
432 pub byte_offset: u32,
434 pub slot_ids: Vec<u16>,
436}
437
438#[cfg(test)]
443mod tests {
444 use super::*;
445
446 fn make_header(version: u16, flags: u16, source_hash: u64) -> Vec<u8> {
448 let mut buf = Vec::with_capacity(HEADER_SIZE);
449 buf.extend_from_slice(MAGIC);
450 buf.extend_from_slice(&version.to_le_bytes());
451 buf.extend_from_slice(&flags.to_le_bytes());
452 buf.extend_from_slice(&source_hash.to_le_bytes());
453 buf
454 }
455
456 #[test]
457 fn parse_valid_header() {
458 let data = make_header(2, 0, 0xDEAD_BEEF_CAFE_BABE);
459 let hdr = IrHeader::parse(&data).unwrap();
460 assert_eq!(hdr.version, 2);
461 assert_eq!(hdr.flags, 0);
462 assert_eq!(hdr.source_hash, 0xDEAD_BEEF_CAFE_BABE);
463 }
464
465 #[test]
466 fn reject_bad_magic() {
467 let mut data = make_header(2, 0, 0);
468 data[0..4].copy_from_slice(b"NOPE");
469 let err = IrHeader::parse(&data).unwrap_err();
470 assert_eq!(err, IrError::BadMagic(*b"NOPE"));
471 }
472
473 #[test]
474 fn reject_unsupported_version() {
475 let data = make_header(99, 0, 0);
476 let err = IrHeader::parse(&data).unwrap_err();
477 assert_eq!(err, IrError::UnsupportedVersion(99));
478 }
479
480 fn make_section_table(sections: [(u32, u32); 4]) -> Vec<u8> {
482 let mut buf = Vec::with_capacity(SECTION_TABLE_SIZE);
483 for (offset, size) in §ions {
484 buf.extend_from_slice(&offset.to_le_bytes());
485 buf.extend_from_slice(&size.to_le_bytes());
486 }
487 buf
488 }
489
490 #[test]
491 fn parse_section_table() {
492 let data = make_section_table([
493 (48, 100), (148, 200), (348, 50), (398, 30), ]);
498 let st = SectionTable::parse(&data).unwrap();
499 assert_eq!(st.sections[0], SectionDescriptor { offset: 48, size: 100 });
500 assert_eq!(st.sections[1], SectionDescriptor { offset: 148, size: 200 });
501 assert_eq!(st.sections[2], SectionDescriptor { offset: 348, size: 50 });
502 assert_eq!(st.sections[3], SectionDescriptor { offset: 398, size: 30 });
503 }
504
505 #[test]
506 fn validate_section_bounds() {
507 let data = make_section_table([
508 (48, 100),
509 (148, 200),
510 (348, 50),
511 (398, 9999), ]);
513 let st = SectionTable::parse(&data).unwrap();
514 let err = st.validate(500).unwrap_err();
515 assert_eq!(
516 err,
517 IrError::SectionOutOfBounds {
518 section: 3,
519 offset: 398,
520 size: 9999,
521 file_len: 500,
522 }
523 );
524 }
525
526 #[test]
527 fn opcode_from_byte_all_valid() {
528 let expected = [
529 (0x01, Opcode::OpenTag),
530 (0x02, Opcode::CloseTag),
531 (0x03, Opcode::VoidTag),
532 (0x04, Opcode::Text),
533 (0x05, Opcode::DynText),
534 (0x06, Opcode::DynAttr),
535 (0x07, Opcode::ShowIf),
536 (0x08, Opcode::ShowElse),
537 (0x09, Opcode::Switch),
538 (0x0A, Opcode::List),
539 (0x0B, Opcode::IslandStart),
540 (0x0C, Opcode::IslandEnd),
541 (0x0D, Opcode::TryStart),
542 (0x0E, Opcode::Fallback),
543 (0x0F, Opcode::Preload),
544 (0x10, Opcode::Comment),
545 (0x11, Opcode::ListItemKey),
546 (0x12, Opcode::Prop),
547 ];
548 for (byte, op) in &expected {
549 assert_eq!(Opcode::from_byte(*byte).unwrap(), *op, "byte 0x{byte:02x}");
550 }
551 }
552
553 #[test]
554 fn opcode_from_byte_invalid() {
555 assert_eq!(Opcode::from_byte(0x00).unwrap_err(), IrError::InvalidOpcode(0x00));
556 assert_eq!(Opcode::from_byte(0x13).unwrap_err(), IrError::InvalidOpcode(0x13));
557 assert_eq!(Opcode::from_byte(0xFF).unwrap_err(), IrError::InvalidOpcode(0xFF));
558 }
559
560 #[test]
561 fn slot_type_from_byte() {
562 let expected = [
563 (0x01, SlotType::Text),
564 (0x02, SlotType::Bool),
565 (0x03, SlotType::Number),
566 (0x04, SlotType::Array),
567 (0x05, SlotType::Object),
568 ];
569 for (byte, st) in &expected {
570 assert_eq!(SlotType::from_byte(*byte).unwrap(), *st, "byte 0x{byte:02x}");
571 }
572 assert_eq!(
573 SlotType::from_byte(0x00).unwrap_err(),
574 IrError::InvalidSlotType(0x00)
575 );
576 assert_eq!(
577 SlotType::from_byte(0x06).unwrap_err(),
578 IrError::InvalidSlotType(0x06)
579 );
580 }
581
582 #[test]
583 fn island_trigger_from_byte() {
584 let expected = [
585 (0x01, IslandTrigger::Load),
586 (0x02, IslandTrigger::Visible),
587 (0x03, IslandTrigger::Interaction),
588 (0x04, IslandTrigger::Idle),
589 ];
590 for (byte, trigger) in &expected {
591 assert_eq!(
592 IslandTrigger::from_byte(*byte).unwrap(),
593 *trigger,
594 "byte 0x{byte:02x}"
595 );
596 }
597 assert_eq!(
598 IslandTrigger::from_byte(0x00).unwrap_err(),
599 IrError::InvalidIslandTrigger(0x00)
600 );
601 assert_eq!(
602 IslandTrigger::from_byte(0x05).unwrap_err(),
603 IrError::InvalidIslandTrigger(0x05)
604 );
605 }
606
607 #[test]
608 fn props_mode_from_byte() {
609 let expected = [
610 (0x01, PropsMode::Inline),
611 (0x02, PropsMode::ScriptTag),
612 (0x03, PropsMode::Deferred),
613 ];
614 for (byte, mode) in &expected {
615 assert_eq!(
616 PropsMode::from_byte(*byte).unwrap(),
617 *mode,
618 "byte 0x{byte:02x}"
619 );
620 }
621 assert_eq!(
622 PropsMode::from_byte(0x00).unwrap_err(),
623 IrError::InvalidPropsMode(0x00)
624 );
625 assert_eq!(
626 PropsMode::from_byte(0x04).unwrap_err(),
627 IrError::InvalidPropsMode(0x04)
628 );
629 }
630
631 #[test]
632 fn slot_source_from_byte() {
633 assert_eq!(SlotSource::from_byte(0x00).unwrap(), SlotSource::Server);
634 assert_eq!(SlotSource::from_byte(0x01).unwrap(), SlotSource::Client);
635 assert_eq!(
636 SlotSource::from_byte(0x02).unwrap_err(),
637 IrError::InvalidSlotSource(0x02)
638 );
639 assert_eq!(
640 SlotSource::from_byte(0xFF).unwrap_err(),
641 IrError::InvalidSlotSource(0xFF)
642 );
643 }
644}