use std::fmt;
include!("fmir_constants.rs");
pub fn reader_version() -> [u16; 3] {
[
env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap_or(u16::MAX),
env!("CARGO_PKG_VERSION_MINOR").parse().unwrap_or(u16::MAX),
env!("CARGO_PKG_VERSION_PATCH").parse().unwrap_or(u16::MAX),
]
}
pub const fn accepts_writer(reader: [u16; 3], writer: [u16; 3]) -> bool {
reader[0] == writer[0] && writer[1] <= reader[1]
}
pub fn crc32(chunks: &[&[u8]]) -> u32 {
const fn table() -> [u32; 256] {
let mut out = [0; 256];
let mut i = 0;
while i < 256 {
let mut c = i as u32;
let mut bit = 0;
while bit < 8 {
c = (c >> 1) ^ if c & 1 != 0 { CRC_POLYNOMIAL } else { 0 };
bit += 1;
}
out[i] = c;
i += 1;
}
out
}
const TABLE: [u32; 256] = table();
let mut c = 0xffffffff;
for chunk in chunks {
for &b in *chunk {
c = TABLE[((c ^ u32::from(b)) & 255) as usize] ^ (c >> 8);
}
}
c ^ 0xffffffff
}
pub(crate) fn limit(name: &'static str, actual: usize, max: usize) -> Result<(), IrError> {
if actual > max {
Err(IrError::LimitExceeded(Box::new(LimitViolation {
limit: name,
max,
actual,
})))
} else {
Ok(())
}
}
pub(crate) fn le_u16(b: &[u8]) -> u16 {
u16::from_le_bytes([b[0], b[1]])
}
pub(crate) fn le_u32(b: &[u8]) -> u32 {
u32::from_le_bytes([b[0], b[1], b[2], b[3]])
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LimitViolation {
pub limit: &'static str,
pub max: usize,
pub actual: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IslandStateViolation {
pub island: u16,
pub path: String,
pub reason: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ListKeyViolation {
pub list_offset: u32,
pub row: usize,
pub path: String,
pub reason: ListKeyReason,
}
impl ListKeyViolation {
pub fn new(
list_offset: u32,
row: usize,
path: impl Into<String>,
reason: ListKeyReason,
) -> Self {
ListKeyViolation {
list_offset,
row,
path: path.into(),
reason,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ListKeyReason {
MissingField {
segment: String,
},
DuplicateField {
segment: String,
},
NotAnObject {
segment: String,
},
NotAnIdentity {
kind: &'static str,
},
NotASafeInteger,
Duplicate {
first_row: usize,
},
RowShape {
roots: usize,
stray_content: bool,
},
AuthoredKey,
UnbalancedRow,
}
impl fmt::Display for ListKeyReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ListKeyReason::MissingField { segment } => {
write!(f, "the row has no field `{segment}`")
}
ListKeyReason::DuplicateField { segment } => {
write!(f, "the row declares the field `{segment}` more than once")
}
ListKeyReason::NotAnObject { segment } => {
write!(f, "`{segment}` is not an object, so the path cannot continue")
}
ListKeyReason::NotAnIdentity { kind } => {
write!(f, "the key is {kind}, not a string or a safe integer")
}
ListKeyReason::NotASafeInteger => write!(
f,
"the key number is not an integer within the safe range (a fraction, an infinity, NaN or beyond 2^53-1)"
),
ListKeyReason::Duplicate { first_row } => {
write!(f, "the row repeats the key row {first_row} already rendered")
}
ListKeyReason::RowShape {
roots,
stray_content,
} => write!(
f,
"the row must render exactly one root element to carry its key ({roots} root element{}{})",
if *roots == 1 { "" } else { "s" },
if *stray_content {
", with text, a range or an island envelope beside it"
} else {
""
}
),
ListKeyReason::AuthoredKey => write!(
f,
"the row root carries an authored data-forma-key; the attribute is reserved for the row identity"
),
ListKeyReason::UnbalancedRow => write!(
f,
"the row closes an element it did not open or leaves one open, so it has no single balanced root"
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum IrError {
BufferTooShort {
expected: usize,
actual: usize,
},
BadSignature([u8; 8]),
LegacyModule {
version: u16,
},
UnsupportedWriter {
writer: [u16; 3],
reader: [u16; 3],
},
LengthMismatch {
declared: usize,
actual: usize,
},
ChecksumMismatch {
expected: u32,
computed: u32,
},
ReservedBits {
field: &'static str,
},
DuplicateSection(u16),
SectionsDoNotTile,
MissingSection(u16),
UnknownRequiredSection {
id: u16,
writer: [u16; 3],
},
LimitExceeded(Box<LimitViolation>),
TrailingBytes {
section: u16,
},
InvalidSchema(&'static str),
InvalidKeyPath(&'static str),
InvalidSourcePosition(&'static str),
InvalidDefault(u16),
SectionOutOfBounds {
section: usize,
offset: u32,
size: u32,
file_len: usize,
},
InvalidOpcode(u8),
InvalidSlotType(u8),
InvalidIslandTrigger(u8),
InvalidSlotSource(u8),
StringIndexOutOfBounds {
index: u32,
len: usize,
},
InvalidUtf8(String),
ListDepthExceeded {
max: u8,
},
IslandNotFound(u16),
JsonParseError(String),
InvalidSection,
RecursionLimitExceeded,
OutputLimitExceeded {
limit: usize,
},
InvalidIslandState(Box<IslandStateViolation>),
IslandStateLimitExceeded {
limit: usize,
},
InvalidListKey(Box<ListKeyViolation>),
ListBudgetExceeded {
max: usize,
},
InvalidName(String),
DuplicateSlotId(u16),
DuplicateIslandId(u16),
IslandOffsetMismatch {
island_id: u16,
byte_offset: u32,
},
UnknownSlotId(u16),
MalformedControlFlow {
pos: usize,
},
UnknownIslandId(u16),
NestedIslandRoot {
outer: u16,
inner: u16,
},
Panicked {
message: String,
},
}
impl fmt::Display for IrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IrError::BufferTooShort { expected, actual } => {
write!(
f,
"buffer too short: expected at least {expected} bytes, got {actual}"
)
}
IrError::BadSignature(got) => write!(f, "bad signature: {got:02x?}"),
IrError::LegacyModule { version } => write!(
f,
"an FMIR module before v3 (header version {version}): rebuild with a 3.x compiler"
),
IrError::UnsupportedWriter { writer, reader } => write!(
f,
"writer {}.{}.{} is incompatible with reader {}.{}.{}",
writer[0], writer[1], writer[2], reader[0], reader[1], reader[2]
),
IrError::LengthMismatch { declared, actual } => write!(
f,
"length mismatch: declared body {declared}, actual {actual}"
),
IrError::ChecksumMismatch { expected, computed } => write!(
f,
"checksum mismatch: expected {expected:08x}, computed {computed:08x}"
),
IrError::ReservedBits { field } => write!(f, "reserved bits in {field}"),
IrError::DuplicateSection(id) => write!(f, "duplicate or unordered section 0x{id:04x}"),
IrError::SectionsDoNotTile => write!(f, "sections do not tile the file"),
IrError::MissingSection(id) => write!(f, "missing required section 0x{id:04x}"),
IrError::UnknownRequiredSection { id, writer } => {
let reader = reader_version();
write!(f, "unknown required section 0x{id:04x} written by {}.{}.{}; this reader is {}.{}.{}",writer[0],writer[1],writer[2],reader[0],reader[1],reader[2])
}
IrError::LimitExceeded(details) => {
let LimitViolation { limit, max, actual } = details.as_ref();
write!(f, "{limit} exceeds limit {max}: {actual}")
}
IrError::TrailingBytes { section } => {
write!(f, "trailing bytes in section 0x{section:04x}")
}
IrError::InvalidSchema(reason) => write!(f, "invalid props schema: {reason}"),
IrError::InvalidKeyPath(reason) => write!(f, "invalid key path: {reason}"),
IrError::InvalidSourcePosition(reason) => {
write!(f, "invalid source position: {reason}")
}
IrError::InvalidDefault(id) => {
write!(f, "default for slot {id} does not parse for its type")
}
IrError::SectionOutOfBounds {
section,
offset,
size,
file_len,
} => {
write!(
f,
"section {section} out of bounds: offset={offset}, size={size}, file_len={file_len}"
)
}
IrError::InvalidOpcode(b) => write!(f, "invalid opcode: 0x{b:02x}"),
IrError::InvalidSlotType(b) => write!(f, "invalid slot type: 0x{b:02x}"),
IrError::InvalidIslandTrigger(b) => {
write!(f, "invalid island trigger: 0x{b:02x}")
}
IrError::InvalidSlotSource(b) => write!(f, "invalid slot source: 0x{b:02x}"),
IrError::StringIndexOutOfBounds { index, len } => {
write!(
f,
"string index {index} out of bounds (table has {len} entries)"
)
}
IrError::InvalidUtf8(msg) => write!(f, "invalid UTF-8: {msg}"),
IrError::ListDepthExceeded { max } => {
write!(f, "nested LIST depth exceeded maximum of {max}")
}
IrError::IslandNotFound(id) => {
write!(f, "island with id {id} not found in island table")
}
IrError::JsonParseError(msg) => {
write!(f, "JSON parse error: {msg}")
}
IrError::InvalidSection => {
write!(f, "invalid section or opcode offset (out of range)")
}
IrError::RecursionLimitExceeded => {
write!(f, "recursion limit exceeded (maximum depth: 64)")
}
IrError::OutputLimitExceeded { limit } => {
write!(f, "rendered output exceeded the limit of {limit} bytes")
}
IrError::InvalidIslandState(problem) => {
write!(
f,
"RENDER-501: island {} {}: {}",
problem.island, problem.path, problem.reason
)
}
IrError::IslandStateLimitExceeded { limit } => {
write!(f, "RENDER-502: island state exceeded {limit} encoded bytes")
}
IrError::InvalidListKey(violation) => {
let ListKeyViolation {
list_offset,
row,
path,
reason,
} = violation.as_ref();
write!(
f,
"RENDER-503: LIST at bytecode offset {list_offset}, row {row}, key path `{path}`: {reason}"
)
}
IrError::ListBudgetExceeded { max } => {
write!(f, "total LIST iterations exceeded the budget of {max}")
}
IrError::InvalidName(name) => {
write!(f, "invalid tag or attribute name: {name:?}")
}
IrError::DuplicateSlotId(id) => {
write!(f, "duplicate slot id {id} in slot table")
}
IrError::DuplicateIslandId(id) => {
write!(f, "duplicate island id {id} in island table")
}
IrError::IslandOffsetMismatch {
island_id,
byte_offset,
} => {
write!(
f,
"island {island_id} byte_offset {byte_offset} does not point at its ISLAND_START opcode"
)
}
IrError::UnknownSlotId(id) => {
write!(f, "slot id {id} is not declared in the slot table")
}
IrError::MalformedControlFlow { pos } => {
write!(
f,
"malformed control flow: region lengths do not tile at byte {pos}"
)
}
IrError::UnknownIslandId(id) => {
write!(f, "island id {id} is not declared in the island table")
}
IrError::NestedIslandRoot { outer, inner } => {
write!(
f,
"island {inner} starts before island {outer} has opened its root element — \
one of them could never carry its data-forma-* attributes"
)
}
IrError::Panicked { message } => write!(f, "the walk panicked: {message}"),
}
}
}
impl IrError {
pub fn code(&self) -> &'static str {
match self {
Self::BadSignature(_) | Self::LegacyModule { .. } => LOAD_102,
Self::LengthMismatch { .. } | Self::ChecksumMismatch { .. } => LOAD_104,
Self::MissingSection(_) => LOAD_105,
Self::UnknownRequiredSection { .. } => LOAD_106,
Self::LimitExceeded(_) => LOAD_107,
Self::UnsupportedWriter { .. } => LOAD_108,
_ => LOAD_103,
}
}
}
impl std::error::Error for IrError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IrHeader {
pub writer: [u16; 3],
pub body_len: u32,
pub crc: u32,
}
impl IrHeader {
pub fn parse(data: &[u8]) -> Result<Self, IrError> {
Self::parse_for_reader(data, reader_version())
}
fn parse_for_reader(data: &[u8], reader: [u16; 3]) -> Result<Self, IrError> {
limit("module bytes", data.len(), MAX_MODULE_SIZE)?;
if data.len() < HEADER_SIZE {
return Err(IrError::BufferTooShort {
expected: HEADER_SIZE,
actual: data.len(),
});
}
if data[..8] != SIGNATURE {
if data[..4] == *b"FMIR" {
return Err(IrError::LegacyModule {
version: le_u16(&data[4..6]),
});
}
let mut got = [0; 8];
got.copy_from_slice(&data[..8]);
return Err(IrError::BadSignature(got));
}
let writer = [
le_u16(&data[8..10]),
le_u16(&data[10..12]),
le_u16(&data[12..14]),
];
if !accepts_writer(reader, writer) {
return Err(IrError::UnsupportedWriter { writer, reader });
}
let body_len = le_u32(&data[14..18]);
let actual = data.len() - HEADER_SIZE;
if body_len as usize != actual {
return Err(IrError::LengthMismatch {
declared: body_len as usize,
actual,
});
}
let crc = le_u32(&data[18..22]);
let computed = crc32(&[&data[..18], &data[22..]]);
if computed != crc {
return Err(IrError::ChecksumMismatch {
expected: crc,
computed,
});
}
if data[22..24] != [0, 0] {
return Err(IrError::ReservedBits { field: "header" });
}
Ok(Self {
writer,
body_len,
crc,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SectionDescriptor {
pub id: u16,
pub flags: u16,
pub offset: u32,
pub size: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SectionTable {
pub sections: Vec<SectionDescriptor>,
}
impl SectionTable {
pub fn parse(data: &[u8]) -> Result<Self, IrError> {
if data.len() < 2 {
return Err(IrError::BufferTooShort {
expected: 2,
actual: data.len(),
});
}
let count = le_u16(data) as usize;
limit("sections", count, MAX_SECTIONS)?;
let end = TABLE_PREFIX_SIZE + count * SECTION_ENTRY_SIZE;
if end > data.len() {
return Err(IrError::BufferTooShort {
expected: end,
actual: data.len(),
});
}
if le_u16(&data[2..4]) != 0 {
return Err(IrError::ReservedBits {
field: "section table",
});
}
let mut sections = Vec::with_capacity(count);
let mut previous = None;
for entry in data[TABLE_PREFIX_SIZE..end].chunks_exact(SECTION_ENTRY_SIZE) {
let id = le_u16(entry);
let flags = le_u16(&entry[2..]);
if previous.is_some_and(|p| id <= p) {
return Err(IrError::DuplicateSection(id));
}
if flags & !SECTION_REQUIRED != 0 {
return Err(IrError::ReservedBits {
field: "section flags",
});
}
sections.push(SectionDescriptor {
id,
flags,
offset: le_u32(&entry[4..]),
size: le_u32(&entry[8..]),
});
previous = Some(id);
}
Ok(Self { sections })
}
pub fn validate(&self, file_len: usize) -> Result<(), IrError> {
let mut expected =
HEADER_SIZE + TABLE_PREFIX_SIZE + self.sections.len() * SECTION_ENTRY_SIZE;
for section in &self.sections {
if section.offset as usize != expected {
return Err(IrError::SectionsDoNotTile);
}
expected = expected
.checked_add(section.size as usize)
.ok_or(IrError::InvalidSection)?;
if expected > file_len {
return Err(IrError::SectionsDoNotTile);
}
}
if expected != file_len {
return Err(IrError::SectionsDoNotTile);
}
Ok(())
}
pub fn get(&self, id: u16) -> Option<&SectionDescriptor> {
self.sections.iter().find(|s| s.id == id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Opcode {
OpenTag = OP_OPEN_TAG,
CloseTag = OP_CLOSE_TAG,
VoidTag = OP_VOID_TAG,
Text = OP_TEXT,
DynText = OP_DYN_TEXT,
DynAttr = OP_DYN_ATTR,
ShowIf = OP_SHOW_IF,
ShowElse = OP_SHOW_ELSE,
List = OP_LIST,
IslandStart = OP_ISLAND_START,
IslandEnd = OP_ISLAND_END,
Prop = OP_PROP,
Coalesce = OP_COALESCE,
}
impl Opcode {
pub fn from_byte(b: u8) -> Result<Self, IrError> {
match b {
OP_OPEN_TAG => Ok(Opcode::OpenTag),
OP_CLOSE_TAG => Ok(Opcode::CloseTag),
OP_VOID_TAG => Ok(Opcode::VoidTag),
OP_TEXT => Ok(Opcode::Text),
OP_DYN_TEXT => Ok(Opcode::DynText),
OP_DYN_ATTR => Ok(Opcode::DynAttr),
OP_SHOW_IF => Ok(Opcode::ShowIf),
OP_SHOW_ELSE => Ok(Opcode::ShowElse),
OP_LIST => Ok(Opcode::List),
OP_ISLAND_START => Ok(Opcode::IslandStart),
OP_ISLAND_END => Ok(Opcode::IslandEnd),
OP_PROP => Ok(Opcode::Prop),
OP_COALESCE => Ok(Opcode::Coalesce),
_ => Err(IrError::InvalidOpcode(b)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SlotType {
Text = SLOT_TYPE_TEXT,
Bool = SLOT_TYPE_BOOL,
Number = SLOT_TYPE_NUMBER,
Array = SLOT_TYPE_ARRAY,
Object = SLOT_TYPE_OBJECT,
}
impl SlotType {
pub fn from_byte(b: u8) -> Result<Self, IrError> {
match b {
SLOT_TYPE_TEXT => Ok(SlotType::Text),
SLOT_TYPE_BOOL => Ok(SlotType::Bool),
SLOT_TYPE_NUMBER => Ok(SlotType::Number),
SLOT_TYPE_ARRAY => Ok(SlotType::Array),
SLOT_TYPE_OBJECT => Ok(SlotType::Object),
_ => Err(IrError::InvalidSlotType(b)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum IslandTrigger {
Load = TRIGGER_LOAD,
Visible = TRIGGER_VISIBLE,
Interaction = TRIGGER_INTERACTION,
Idle = TRIGGER_IDLE,
}
impl IslandTrigger {
pub fn from_byte(b: u8) -> Result<Self, IrError> {
match b {
TRIGGER_LOAD => Ok(IslandTrigger::Load),
TRIGGER_VISIBLE => Ok(IslandTrigger::Visible),
TRIGGER_INTERACTION => Ok(IslandTrigger::Interaction),
TRIGGER_IDLE => Ok(IslandTrigger::Idle),
_ => Err(IrError::InvalidIslandTrigger(b)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SlotSource {
Server = SLOT_SOURCE_SERVER,
Client = SLOT_SOURCE_CLIENT,
}
impl SlotSource {
pub fn from_byte(b: u8) -> Result<Self, IrError> {
match b {
SLOT_SOURCE_SERVER => Ok(SlotSource::Server),
SLOT_SOURCE_CLIENT => Ok(SlotSource::Client),
_ => Err(IrError::InvalidSlotSource(b)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SlotEntry {
pub slot_id: u16,
pub name_str_idx: u32,
pub type_hint: SlotType,
pub source: SlotSource,
pub default_bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IslandEntry {
pub id: u16,
pub trigger: IslandTrigger,
pub schema_object: u16,
pub digest: Vec<u8>,
pub extension: Vec<u8>,
pub name_str_idx: u32,
pub byte_offset: u32,
pub slot_ids: Vec<u16>,
}
#[cfg(test)]
mod compatibility_tests {
use super::*;
#[test]
fn shared_version_pairs_pass_through_the_actual_header_reader() {
let pairs: serde_json::Value = serde_json::from_str(include_str!(
"../tests/fixtures/compatibility/version-pairs.json"
))
.unwrap();
let base = |s: &str| -> [u16; 3] {
let parts: Vec<u16> = s
.split('-')
.next()
.unwrap()
.split('.')
.map(|p| p.parse().unwrap())
.collect();
[parts[0], parts[1], parts[2]]
};
for pair in pairs.as_array().unwrap() {
let reader = base(pair["reader"].as_str().unwrap());
let writer = base(pair["writer"].as_str().unwrap());
let mut bytes = crate::parser::test_helpers::build_minimal_ir(&[], &[], &[], &[]);
for (i, value) in writer.iter().enumerate() {
bytes[8 + i * 2..10 + i * 2].copy_from_slice(&value.to_le_bytes());
}
let crc = crc32(&[&bytes[..18], &bytes[22..]]);
bytes[18..22].copy_from_slice(&crc.to_le_bytes());
let result = IrHeader::parse_for_reader(&bytes, reader);
assert_eq!(
result.is_ok(),
pair["accepted"].as_bool().unwrap(),
"{pair}: {result:?}"
);
if let Err(error) = result {
assert_eq!(error.code(), "LOAD-108");
for version in [reader, writer] {
assert!(error
.to_string()
.contains(&format!("{}.{}.{}", version[0], version[1], version[2])));
}
}
}
}
}