use crate::format::{
limit, IrError, Opcode, MAX_KEY_SEGMENTS, MAX_SCHEMA_ITEMS, MAX_TYPE_DEPTH, SCHEMA_ARRAY,
SCHEMA_BOOLEAN, SCHEMA_DATE, SCHEMA_ENUM, SCHEMA_NUMBER, SCHEMA_OBJECT, SCHEMA_OPTIONAL,
SCHEMA_STRING, SECTION_KEY_PATHS, SECTION_PROPS_SCHEMA, SECTION_SOURCE_POSITIONS,
};
use crate::parser::StringTable;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PropType {
Boolean,
String,
Number,
Date,
Enum(Vec<u32>),
Array(Box<PropType>),
Optional(Box<PropType>),
Object(u16),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PropField {
pub name: u32,
pub file: u32,
pub line: u32,
pub column: u32,
pub kind: PropType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PropsSchema {
pub objects: Vec<Vec<PropField>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyPath {
pub list_offset: u32,
pub segments: Vec<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourcePosition {
pub opcode_offset: u32,
pub file: u32,
pub line: u32,
pub column: u32,
pub component: u32,
}
pub(crate) struct Cursor<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
pub(crate) fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
pub(crate) fn remaining(&self) -> usize {
self.data.len() - self.pos
}
pub(crate) fn take(&mut self, n: usize) -> Result<&'a [u8], IrError> {
let end = self.pos.checked_add(n).ok_or(IrError::InvalidSection)?;
let bytes = self
.data
.get(self.pos..end)
.ok_or(IrError::BufferTooShort {
expected: end,
actual: self.data.len(),
})?;
self.pos = end;
Ok(bytes)
}
pub(crate) fn u8(&mut self) -> Result<u8, IrError> {
Ok(self.take(1)?[0])
}
pub(crate) fn u16(&mut self) -> Result<u16, IrError> {
Ok(crate::format::le_u16(self.take(2)?))
}
pub(crate) fn u32(&mut self) -> Result<u32, IrError> {
Ok(crate::format::le_u32(self.take(4)?))
}
pub(crate) fn bound(&self, count: usize, min: usize) -> Result<(), IrError> {
if count > self.remaining() / min {
Err(IrError::BufferTooShort {
expected: count.saturating_mul(min),
actual: self.remaining(),
})
} else {
Ok(())
}
}
pub(crate) fn finish(&self, section: u16) -> Result<(), IrError> {
if self.remaining() != 0 {
Err(IrError::TrailingBytes { section })
} else {
Ok(())
}
}
fn str_idx(&mut self, strings: &StringTable) -> Result<u32, IrError> {
let i = self.u32()?;
strings.get(i)?;
Ok(i)
}
}
impl PropsSchema {
pub(crate) fn parse(data: &[u8], strings: &StringTable) -> Result<Self, IrError> {
let mut r = Cursor::new(data);
let count = r.u16()? as usize;
limit("schema objects", count, MAX_SCHEMA_ITEMS)?;
r.bound(count, 2)?;
if count == 0 {
return Err(IrError::InvalidSchema("object zero is required"));
}
let mut objects = Vec::with_capacity(count);
let mut edges = Vec::with_capacity(count);
for _ in 0..count {
let n = r.u16()? as usize;
limit("schema fields", n, MAX_SCHEMA_ITEMS)?;
r.bound(n, 17)?;
let mut fields = Vec::with_capacity(n);
let mut names = HashSet::with_capacity(n);
let mut refs = Vec::new();
for _ in 0..n {
let name = r.str_idx(strings)?;
let file = r.str_idx(strings)?;
let line = r.u32()?;
let column = r.u32()?;
if !names.insert(strings.get(name)?) {
return Err(IrError::InvalidSchema("duplicate field name"));
}
if line == 0 || column == 0 {
return Err(IrError::InvalidSchema(
"source coordinates must be positive",
));
}
let kind = read_type(&mut r, strings, count, 0, &mut refs)?;
fields.push(PropField {
name,
file,
line,
column,
kind,
});
}
objects.push(fields);
edges.push(refs);
}
r.finish(SECTION_PROPS_SCHEMA)?;
let mut color = vec![0u8; count];
for root in 0..count {
if color[root] != 0 {
continue;
}
color[root] = 1;
let mut stack = vec![(root, 0usize)];
while let Some((node, next)) = stack.last_mut() {
if *next == edges[*node].len() {
color[*node] = 2;
stack.pop();
continue;
}
let child = edges[*node][*next] as usize;
*next += 1;
match color[child] {
1 => return Err(IrError::InvalidSchema("cyclic object references")),
0 => {
color[child] = 1;
stack.push((child, 0));
}
_ => {}
}
}
}
Ok(Self { objects })
}
}
fn read_type(
r: &mut Cursor<'_>,
strings: &StringTable,
objects: usize,
depth: usize,
refs: &mut Vec<u16>,
) -> Result<PropType, IrError> {
limit("schema type nesting", depth, MAX_TYPE_DEPTH)?;
Ok(match r.u8()? {
SCHEMA_BOOLEAN => PropType::Boolean,
SCHEMA_STRING => PropType::String,
SCHEMA_NUMBER => PropType::Number,
SCHEMA_DATE => PropType::Date,
SCHEMA_ENUM => {
let n = r.u16()? as usize;
limit("enum variants", n, MAX_SCHEMA_ITEMS)?;
r.bound(n, 4)?;
let mut v = Vec::with_capacity(n);
for _ in 0..n {
v.push(r.str_idx(strings)?);
}
PropType::Enum(v)
}
SCHEMA_ARRAY => PropType::Array(Box::new(read_type(r, strings, objects, depth + 1, refs)?)),
SCHEMA_OPTIONAL => {
let inner = read_type(r, strings, objects, depth + 1, refs)?;
if matches!(inner, PropType::Optional(_)) {
return Err(IrError::InvalidSchema("optional of optional"));
}
PropType::Optional(Box::new(inner))
}
SCHEMA_OBJECT => {
let i = r.u16()?;
if i as usize >= objects {
return Err(IrError::InvalidSchema("object index out of range"));
}
refs.push(i);
PropType::Object(i)
}
_ => return Err(IrError::InvalidSchema("unknown type tag")),
})
}
pub(crate) fn key_paths(
data: &[u8],
strings: &StringTable,
starts: &[(u32, Opcode)],
) -> Result<Vec<KeyPath>, IrError> {
let mut r = Cursor::new(data);
let count = r.u32()? as usize;
r.bound(count, 9)?;
limit(
"key paths",
count,
starts.iter().filter(|(_, op)| *op == Opcode::List).count(),
)?;
let mut result = Vec::with_capacity(count);
let mut previous = None;
for _ in 0..count {
let off = r.u32()?;
let index = starts
.binary_search_by_key(&off, |(o, _)| *o)
.map_err(|_| IrError::InvalidKeyPath("offset is not a LIST boundary"))?;
if starts[index].1 != Opcode::List || previous.is_some_and(|p| off <= p) {
return Err(IrError::InvalidKeyPath(
"offset is not a unique ascending LIST",
));
}
let n = r.u8()? as usize;
if n == 0 {
return Err(IrError::InvalidKeyPath("zero segments"));
}
limit("key segments", n, MAX_KEY_SEGMENTS)?;
r.bound(n, 4)?;
let mut segments = Vec::with_capacity(n);
for _ in 0..n {
segments.push(r.str_idx(strings)?);
}
result.push(KeyPath {
list_offset: off,
segments,
});
previous = Some(off);
}
r.finish(SECTION_KEY_PATHS)?;
Ok(result)
}
pub(crate) fn source_positions(
data: &[u8],
strings: &StringTable,
starts: &[(u32, Opcode)],
) -> Result<Vec<SourcePosition>, IrError> {
let mut r = Cursor::new(data);
let count = r.u32()? as usize;
r.bound(count, 20)?;
limit("source positions", count, starts.len())?;
let mut result = Vec::with_capacity(count);
let mut previous = None;
for _ in 0..count {
let off = r.u32()?;
let index = starts
.binary_search_by_key(&off, |(o, _)| *o)
.map_err(|_| IrError::InvalidSourcePosition("not an opcode boundary"))?;
if !matches!(
starts[index].1,
Opcode::OpenTag | Opcode::VoidTag | Opcode::IslandStart | Opcode::ShowIf | Opcode::List
) || previous.is_some_and(|p| off <= p)
{
return Err(IrError::InvalidSourcePosition(
"ineligible or nonascending opcode",
));
}
let file = r.str_idx(strings)?;
let line = r.u32()?;
let column = r.u32()?;
let component = r.str_idx(strings)?;
if line == 0 || column == 0 {
return Err(IrError::InvalidSourcePosition(
"source coordinates must be positive",
));
}
result.push(SourcePosition {
opcode_offset: off,
file,
line,
column,
component,
});
previous = Some(off);
}
r.finish(SECTION_SOURCE_POSITIONS)?;
Ok(result)
}