Skip to main content

forma_ir/
schema.rs

1//! FMIR v3 schema, key-path and source-position tables.
2
3use crate::format::{
4    limit, IrError, Opcode, MAX_KEY_SEGMENTS, MAX_SCHEMA_ITEMS, MAX_TYPE_DEPTH, SCHEMA_ARRAY,
5    SCHEMA_BOOLEAN, SCHEMA_DATE, SCHEMA_ENUM, SCHEMA_NUMBER, SCHEMA_OBJECT, SCHEMA_OPTIONAL,
6    SCHEMA_STRING, SECTION_KEY_PATHS, SECTION_PROPS_SCHEMA, SECTION_SOURCE_POSITIONS,
7};
8use crate::parser::StringTable;
9use std::collections::HashSet;
10
11/// One field's closed-set props type.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum PropType {
14    /// Boolean value.
15    Boolean,
16    /// UTF-8 string.
17    String,
18    /// Number, checked for range at bind/render.
19    Number,
20    /// Date represented as epoch milliseconds.
21    Date,
22    /// Unit-enum string-table indices.
23    Enum(Vec<u32>),
24    /// Homogeneous array.
25    Array(Box<PropType>),
26    /// Optional value.
27    Optional(Box<PropType>),
28    /// Object-table index.
29    Object(u16),
30}
31/// A props field and its declaration position.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct PropField {
34    /// Field name in the string table.
35    pub name: u32,
36    /// Project-relative declaring file in the string table.
37    pub file: u32,
38    /// One-based source line.
39    pub line: u32,
40    /// One-based UTF-16 source column.
41    pub column: u32,
42    /// Declared type.
43    pub kind: PropType,
44}
45/// Props schema; object zero is always the page's props type.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct PropsSchema {
48    /// Objects in declaration order, containing fields in source order.
49    pub objects: Vec<Vec<PropField>>,
50}
51/// The row-key property path attached to one LIST opcode.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct KeyPath {
54    /// Bytecode offset of the LIST.
55    pub list_offset: u32,
56    /// Property names as string-table indices.
57    pub segments: Vec<u32>,
58}
59/// Source location for an eligible structural opcode.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct SourcePosition {
62    /// Bytecode opcode offset.
63    pub opcode_offset: u32,
64    /// Project-relative file index.
65    pub file: u32,
66    /// One-based line.
67    pub line: u32,
68    /// One-based UTF-16 column.
69    pub column: u32,
70    /// Component name index.
71    pub component: u32,
72}
73
74pub(crate) struct Cursor<'a> {
75    data: &'a [u8],
76    pos: usize,
77}
78impl<'a> Cursor<'a> {
79    pub(crate) fn new(data: &'a [u8]) -> Self {
80        Self { data, pos: 0 }
81    }
82    pub(crate) fn remaining(&self) -> usize {
83        self.data.len() - self.pos
84    }
85    pub(crate) fn take(&mut self, n: usize) -> Result<&'a [u8], IrError> {
86        let end = self.pos.checked_add(n).ok_or(IrError::InvalidSection)?;
87        let bytes = self
88            .data
89            .get(self.pos..end)
90            .ok_or(IrError::BufferTooShort {
91                expected: end,
92                actual: self.data.len(),
93            })?;
94        self.pos = end;
95        Ok(bytes)
96    }
97    pub(crate) fn u8(&mut self) -> Result<u8, IrError> {
98        Ok(self.take(1)?[0])
99    }
100    pub(crate) fn u16(&mut self) -> Result<u16, IrError> {
101        Ok(crate::format::le_u16(self.take(2)?))
102    }
103    pub(crate) fn u32(&mut self) -> Result<u32, IrError> {
104        Ok(crate::format::le_u32(self.take(4)?))
105    }
106    pub(crate) fn bound(&self, count: usize, min: usize) -> Result<(), IrError> {
107        if count > self.remaining() / min {
108            Err(IrError::BufferTooShort {
109                expected: count.saturating_mul(min),
110                actual: self.remaining(),
111            })
112        } else {
113            Ok(())
114        }
115    }
116    pub(crate) fn finish(&self, section: u16) -> Result<(), IrError> {
117        if self.remaining() != 0 {
118            Err(IrError::TrailingBytes { section })
119        } else {
120            Ok(())
121        }
122    }
123    fn str_idx(&mut self, strings: &StringTable) -> Result<u32, IrError> {
124        let i = self.u32()?;
125        strings.get(i)?;
126        Ok(i)
127    }
128}
129
130impl PropsSchema {
131    pub(crate) fn parse(data: &[u8], strings: &StringTable) -> Result<Self, IrError> {
132        let mut r = Cursor::new(data);
133        let count = r.u16()? as usize;
134        limit("schema objects", count, MAX_SCHEMA_ITEMS)?;
135        r.bound(count, 2)?;
136        if count == 0 {
137            return Err(IrError::InvalidSchema("object zero is required"));
138        }
139        let mut objects = Vec::with_capacity(count);
140        let mut edges = Vec::with_capacity(count);
141        for _ in 0..count {
142            let n = r.u16()? as usize;
143            limit("schema fields", n, MAX_SCHEMA_ITEMS)?;
144            r.bound(n, 17)?;
145            let mut fields = Vec::with_capacity(n);
146            let mut names = HashSet::with_capacity(n);
147            let mut refs = Vec::new();
148            for _ in 0..n {
149                let name = r.str_idx(strings)?;
150                let file = r.str_idx(strings)?;
151                let line = r.u32()?;
152                let column = r.u32()?;
153                if !names.insert(strings.get(name)?) {
154                    return Err(IrError::InvalidSchema("duplicate field name"));
155                }
156                if line == 0 || column == 0 {
157                    return Err(IrError::InvalidSchema(
158                        "source coordinates must be positive",
159                    ));
160                }
161                let kind = read_type(&mut r, strings, count, 0, &mut refs)?;
162                fields.push(PropField {
163                    name,
164                    file,
165                    line,
166                    column,
167                    kind,
168                });
169            }
170            objects.push(fields);
171            edges.push(refs);
172        }
173        r.finish(SECTION_PROPS_SCHEMA)?;
174        // Iterative DFS: a 1,024-object chain does not consume the call stack.
175        let mut color = vec![0u8; count];
176        for root in 0..count {
177            if color[root] != 0 {
178                continue;
179            }
180            color[root] = 1;
181            let mut stack = vec![(root, 0usize)];
182            while let Some((node, next)) = stack.last_mut() {
183                if *next == edges[*node].len() {
184                    color[*node] = 2;
185                    stack.pop();
186                    continue;
187                }
188                let child = edges[*node][*next] as usize;
189                *next += 1;
190                match color[child] {
191                    1 => return Err(IrError::InvalidSchema("cyclic object references")),
192                    0 => {
193                        color[child] = 1;
194                        stack.push((child, 0));
195                    }
196                    _ => {}
197                }
198            }
199        }
200        Ok(Self { objects })
201    }
202}
203fn read_type(
204    r: &mut Cursor<'_>,
205    strings: &StringTable,
206    objects: usize,
207    depth: usize,
208    refs: &mut Vec<u16>,
209) -> Result<PropType, IrError> {
210    limit("schema type nesting", depth, MAX_TYPE_DEPTH)?;
211    Ok(match r.u8()? {
212        SCHEMA_BOOLEAN => PropType::Boolean,
213        SCHEMA_STRING => PropType::String,
214        SCHEMA_NUMBER => PropType::Number,
215        SCHEMA_DATE => PropType::Date,
216        SCHEMA_ENUM => {
217            let n = r.u16()? as usize;
218            limit("enum variants", n, MAX_SCHEMA_ITEMS)?;
219            r.bound(n, 4)?;
220            let mut v = Vec::with_capacity(n);
221            for _ in 0..n {
222                v.push(r.str_idx(strings)?);
223            }
224            PropType::Enum(v)
225        }
226        SCHEMA_ARRAY => PropType::Array(Box::new(read_type(r, strings, objects, depth + 1, refs)?)),
227        SCHEMA_OPTIONAL => {
228            let inner = read_type(r, strings, objects, depth + 1, refs)?;
229            if matches!(inner, PropType::Optional(_)) {
230                return Err(IrError::InvalidSchema("optional of optional"));
231            }
232            PropType::Optional(Box::new(inner))
233        }
234        SCHEMA_OBJECT => {
235            let i = r.u16()?;
236            if i as usize >= objects {
237                return Err(IrError::InvalidSchema("object index out of range"));
238            }
239            refs.push(i);
240            PropType::Object(i)
241        }
242        _ => return Err(IrError::InvalidSchema("unknown type tag")),
243    })
244}
245
246pub(crate) fn key_paths(
247    data: &[u8],
248    strings: &StringTable,
249    starts: &[(u32, Opcode)],
250) -> Result<Vec<KeyPath>, IrError> {
251    let mut r = Cursor::new(data);
252    let count = r.u32()? as usize;
253    r.bound(count, 9)?;
254    limit(
255        "key paths",
256        count,
257        starts.iter().filter(|(_, op)| *op == Opcode::List).count(),
258    )?;
259    let mut result = Vec::with_capacity(count);
260    let mut previous = None;
261    for _ in 0..count {
262        let off = r.u32()?;
263        let index = starts
264            .binary_search_by_key(&off, |(o, _)| *o)
265            .map_err(|_| IrError::InvalidKeyPath("offset is not a LIST boundary"))?;
266        if starts[index].1 != Opcode::List || previous.is_some_and(|p| off <= p) {
267            return Err(IrError::InvalidKeyPath(
268                "offset is not a unique ascending LIST",
269            ));
270        }
271        let n = r.u8()? as usize;
272        if n == 0 {
273            return Err(IrError::InvalidKeyPath("zero segments"));
274        }
275        limit("key segments", n, MAX_KEY_SEGMENTS)?;
276        r.bound(n, 4)?;
277        let mut segments = Vec::with_capacity(n);
278        for _ in 0..n {
279            segments.push(r.str_idx(strings)?);
280        }
281        result.push(KeyPath {
282            list_offset: off,
283            segments,
284        });
285        previous = Some(off);
286    }
287    r.finish(SECTION_KEY_PATHS)?;
288    Ok(result)
289}
290pub(crate) fn source_positions(
291    data: &[u8],
292    strings: &StringTable,
293    starts: &[(u32, Opcode)],
294) -> Result<Vec<SourcePosition>, IrError> {
295    let mut r = Cursor::new(data);
296    let count = r.u32()? as usize;
297    r.bound(count, 20)?;
298    limit("source positions", count, starts.len())?;
299    let mut result = Vec::with_capacity(count);
300    let mut previous = None;
301    for _ in 0..count {
302        let off = r.u32()?;
303        let index = starts
304            .binary_search_by_key(&off, |(o, _)| *o)
305            .map_err(|_| IrError::InvalidSourcePosition("not an opcode boundary"))?;
306        if !matches!(
307            starts[index].1,
308            Opcode::OpenTag | Opcode::VoidTag | Opcode::IslandStart | Opcode::ShowIf | Opcode::List
309        ) || previous.is_some_and(|p| off <= p)
310        {
311            return Err(IrError::InvalidSourcePosition(
312                "ineligible or nonascending opcode",
313            ));
314        }
315        let file = r.str_idx(strings)?;
316        let line = r.u32()?;
317        let column = r.u32()?;
318        let component = r.str_idx(strings)?;
319        if line == 0 || column == 0 {
320            return Err(IrError::InvalidSourcePosition(
321                "source coordinates must be positive",
322            ));
323        }
324        result.push(SourcePosition {
325            opcode_offset: off,
326            file,
327            line,
328            column,
329            component,
330        });
331        previous = Some(off);
332    }
333    r.finish(SECTION_SOURCE_POSITIONS)?;
334    Ok(result)
335}