1use 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#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum PropType {
14 Boolean,
16 String,
18 Number,
20 Date,
22 Enum(Vec<u32>),
24 Array(Box<PropType>),
26 Optional(Box<PropType>),
28 Object(u16),
30}
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct PropField {
34 pub name: u32,
36 pub file: u32,
38 pub line: u32,
40 pub column: u32,
42 pub kind: PropType,
44}
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct PropsSchema {
48 pub objects: Vec<Vec<PropField>>,
50}
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct KeyPath {
54 pub list_offset: u32,
56 pub segments: Vec<u32>,
58}
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct SourcePosition {
62 pub opcode_offset: u32,
64 pub file: u32,
66 pub line: u32,
68 pub column: u32,
70 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 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}