openbim_step/scan.rs
1//! Lazy record index: frame every data record without decoding it.
2//!
3//! [`scan`] reads the header strictly and then walks the data section record
4//! by record, yielding each record's instance id, record name and byte span.
5//! Parameters are not tokenized; [`decode_record`] parses one record from its
6//! span when it is needed. This is the shape of a lazy model -- index once,
7//! decode what is touched -- and it is several times faster than a full
8//! parse when most records are never decoded.
9//!
10//! Correctness rests on the parser, not on the framing. Between records the
11//! parser's whole state is its byte offset, so a record decodes exactly as a
12//! whole-file parse reads it, and [`decode_record`] fails unless the record
13//! ends exactly at the end of its span. Everything between records -- trivia,
14//! `ENDSEC;` and the end marker -- is read with the lexer, just as the parser
15//! reads it. Hence: if the scan and the decode of every record succeed, then
16//! [`parse`](crate::parse) succeeds and returns exactly those records. A
17//! framing mistake can only surface as an error, never as a different record.
18//!
19//! What the scan does not do is check the syntax inside a record: that
20//! happens when the record is decoded. A file with a malformed parameter
21//! scans cleanly and fails on decoding that record.
22
23use crate::lexer::{Lexer, Token};
24use crate::parser::{decode_borrowed, decode_owned, parse_prefix};
25use crate::{DataRecord, Diagnostic, HeaderSection, InstanceId, ParseOptions, Span, StepError};
26use std::borrow::Cow;
27
28/// A data section indexed by [`scan`]: the parsed header and an iterator
29/// over the framed data records.
30#[derive(Debug, Clone)]
31pub struct Scan<'a> {
32 input: &'a [u8],
33 header: HeaderSection,
34 data_start: usize,
35}
36
37/// One framed data record: `#id = NAME(...);` or `#id = (A(...)B(...));`.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct ScannedRecord<'a> {
40 /// Instance id.
41 pub id: InstanceId,
42 /// Record name as written, for a simple instance; `None` for a complex
43 /// instance, whose names are only known after decoding. Compare with
44 /// `eq_ignore_ascii_case`: the owned parse upper-cases names.
45 pub name: Option<Cow<'a, str>>,
46 /// Bytes from `#` through the terminating `;`.
47 pub span: Span,
48}
49
50/// Indexes the data records of a physical file without decoding them.
51///
52/// The header is parsed and checked exactly as [`parse`](crate::parse) does.
53/// # Errors
54///
55/// Returns [`StepError`] for the wrong physical-file marker and for any
56/// header or section-structure defect up to `DATA;`. Defects after that are
57/// reported by the [`Scan::records`] iterator.
58pub fn scan(input: &[u8]) -> Result<Scan<'_>, StepError> {
59 if !crate::is_step_file(input) {
60 return Err(StepError::not_step("missing ISO-10303-21 marker"));
61 }
62 match parse_prefix(input, ParseOptions::strict())? {
63 Some((header, data_start)) => Ok(Scan {
64 input,
65 header,
66 data_start,
67 }),
68 // No `DATA;`: a full parse reports why the file is incomplete.
69 None => Err(crate::parse(input).err().unwrap_or_else(|| {
70 StepError::syntax(Span::new(input.len(), input.len()), "missing DATA section")
71 })),
72 }
73}
74
75impl<'a> Scan<'a> {
76 /// The parsed header section.
77 #[must_use]
78 pub const fn header(&self) -> &HeaderSection {
79 &self.header
80 }
81
82 /// The input this scan indexes.
83 #[must_use]
84 pub const fn input(&self) -> &'a [u8] {
85 self.input
86 }
87
88 /// Iterates over the data records in source order. After the last
89 /// record the iterator checks `ENDSEC;` and the end marker; a defect
90 /// anywhere is yielded as an error, after which the iterator ends.
91 #[must_use]
92 pub fn records(&self) -> Records<'a> {
93 let mut lexer = Lexer::new(self.input);
94 lexer.resume_at(self.data_start);
95 Records {
96 input: self.input,
97 lexer,
98 done: false,
99 }
100 }
101
102 /// Decodes one scanned record with owned text, exactly as
103 /// [`parse`](crate::parse) returns it.
104 /// # Errors
105 ///
106 /// Returns the syntax error inside the record, if any.
107 pub fn decode(&self, record: &ScannedRecord<'_>) -> Result<DataRecord, StepError> {
108 decode_record(self.input, record.span)
109 }
110
111 /// [`Self::decode`] under `options`, as [`parse_with`](crate::parse_with)
112 /// reads the record, with the record's diagnostics.
113 /// # Errors
114 ///
115 /// Returns the syntax error inside the record, if any.
116 pub fn decode_with(
117 &self,
118 record: &ScannedRecord<'_>,
119 options: ParseOptions,
120 ) -> Result<(DataRecord, Vec<Diagnostic>), StepError> {
121 decode_record_with(self.input, record.span, options)
122 }
123}
124
125/// Parses the data record that occupies exactly `span` of `input`, with
126/// owned text as [`parse`](crate::parse) returns it.
127///
128/// `span` should come from [`scan`]. Any span is checked to hold exactly one
129/// record, but only a scanned span is known to be a record of the data
130/// section rather than, say, text inside a string literal.
131/// # Errors
132///
133/// Returns a syntax error for a malformed record, a span that does not start
134/// at `#`, or a record that does not end exactly at `span.end`.
135pub fn decode_record(input: &[u8], span: Span) -> Result<DataRecord, StepError> {
136 decode_owned(input, span, ParseOptions::strict()).map(|(record, _)| record)
137}
138
139/// [`decode_record`] under `options`: the record exactly as
140/// [`parse_with`](crate::parse_with) reads it with the same options, and the
141/// diagnostics that parse reports for it.
142///
143/// Only the options that act inside one record apply:
144/// [`ParseOptions::accept_real_without_point`] does, while skipping a
145/// malformed record and checking references need the whole data section and
146/// do not. A malformed record is therefore always an error here.
147/// # Errors
148///
149/// The same errors as [`decode_record`], except those the options accept.
150pub fn decode_record_with(
151 input: &[u8],
152 span: Span,
153 options: ParseOptions,
154) -> Result<(DataRecord, Vec<Diagnostic>), StepError> {
155 decode_owned(input, span, options)
156}
157
158/// [`decode_record`] with text borrowed from `input` wherever it needs no
159/// rewriting, as [`parse_events_borrowed`](crate::parse_events_borrowed)
160/// returns it.
161/// # Errors
162///
163/// The same errors as [`decode_record`].
164pub fn decode_record_borrowed(
165 input: &[u8],
166 span: Span,
167) -> Result<DataRecord<Cow<'_, str>>, StepError> {
168 decode_borrowed(input, span, ParseOptions::strict()).map(|(record, _)| record)
169}
170
171/// [`decode_record_borrowed`] under `options`; see [`decode_record_with`].
172/// # Errors
173///
174/// The same errors as [`decode_record_with`].
175pub fn decode_record_borrowed_with(
176 input: &[u8],
177 span: Span,
178 options: ParseOptions,
179) -> Result<(DataRecord<Cow<'_, str>>, Vec<Diagnostic>), StepError> {
180 decode_borrowed(input, span, options)
181}
182
183/// Iterator over framed data records; see [`Scan::records`].
184#[derive(Debug, Clone)]
185pub struct Records<'a> {
186 input: &'a [u8],
187 lexer: Lexer<'a>,
188 done: bool,
189}
190
191impl<'a> Iterator for Records<'a> {
192 type Item = Result<ScannedRecord<'a>, StepError>;
193
194 fn next(&mut self) -> Option<Self::Item> {
195 if self.done {
196 return None;
197 }
198 let step = self.step();
199 if !matches!(step, Ok(Some(_))) {
200 self.done = true;
201 }
202 step.transpose()
203 }
204}
205
206impl<'a> Records<'a> {
207 /// Frames the next record, or checks the end of the file after the last.
208 fn step(&mut self) -> Result<Option<ScannedRecord<'a>>, StepError> {
209 if let Some(head) = self.lexer.record_head() {
210 let end = self.record_end(head.start)?;
211 return Ok(Some(ScannedRecord {
212 id: InstanceId::from_digits(head.id),
213 name: head.name.map(ascii_borrowed),
214 span: Span::new(head.start, end),
215 }));
216 }
217 let token = self.token("unterminated section (expected ENDSEC)")?;
218 match token.value {
219 Token::Id(id) => {
220 let start = token.span.start;
221 let equals = self.token("expected '=' after instance id")?;
222 if equals.value != Token::Equals {
223 return Err(StepError::syntax(
224 equals.span,
225 "expected '=' after instance id",
226 ));
227 }
228 let body = self.token("missing record body")?;
229 let name = match body.value {
230 Token::Name(name) => Some(ascii(name)),
231 Token::OpenParen => None,
232 _ => {
233 return Err(StepError::syntax(
234 body.span,
235 "expected record name or complex instance",
236 ));
237 }
238 };
239 let end = self.record_end(start)?;
240 Ok(Some(ScannedRecord {
241 id: InstanceId::from_ascii_digits(&id).expect("lexer validates instance ids"),
242 name,
243 span: Span::new(start, end),
244 }))
245 }
246 Token::Name(name) if name.eq_ignore_ascii_case(b"ENDSEC") => {
247 self.expect(&Token::Semicolon, "expected ';' after ENDSEC")?;
248 let marker =
249 self.token("physical file requires start, HEADER, DATA, and end markers")?;
250 if !matches!(&marker.value, Token::Name(name) if name.eq_ignore_ascii_case(b"END-ISO-10303-21"))
251 {
252 return Err(StepError::syntax(
253 marker.span,
254 format!("unexpected token {:?} in BeforeEnd", marker.value),
255 ));
256 }
257 self.expect(&Token::Semicolon, "expected ';' after END-ISO-10303-21")?;
258 if let Some(extra) = self.lexer.next_spanned()? {
259 return Err(StepError::syntax(
260 extra.span,
261 "content after END-ISO-10303-21",
262 ));
263 }
264 Ok(None)
265 }
266 value => Err(StepError::syntax(
267 token.span,
268 format!("unexpected token {value:?} in Data"),
269 )),
270 }
271 }
272
273 fn token(&mut self, at_end: &str) -> Result<crate::Spanned<Token<'a>>, StepError> {
274 self.lexer.next_spanned()?.ok_or_else(|| {
275 let end = self.input.len();
276 StepError::syntax(Span::new(end, end), at_end)
277 })
278 }
279
280 fn expect(&mut self, expected: &Token<'_>, message: &str) -> Result<(), StepError> {
281 let token = self.token(message)?;
282 if &token.value == expected {
283 Ok(())
284 } else {
285 Err(StepError::syntax(token.span, message))
286 }
287 }
288
289 /// Finds the `;` that ends the record starting at `start`, from the
290 /// lexer's current offset, and leaves the lexer just past it.
291 ///
292 /// Outside literals only `;`, `'` and `/` can matter, so the scan jumps
293 /// between them. A string is skipped by the lexer's own string reader. A
294 /// `/` is skipped as a comment when the lexer reads one there, else as a
295 /// single byte: a lone `/` is a syntax error the decode reports. Binary
296 /// literals need no case of their own: a valid body is hex digits, and
297 /// an invalid one fails to decode.
298 fn record_end(&mut self, start: usize) -> Result<usize, StepError> {
299 let input = self.input;
300 let mut position = self.lexer.offset();
301 loop {
302 let Some(offset) = memchr::memchr3(b';', b'\'', b'/', &input[position..]) else {
303 return Err(StepError::syntax(
304 Span::new(start, input.len()),
305 "unterminated data record",
306 ));
307 };
308 position += offset;
309 match input[position] {
310 b';' => {
311 position += 1;
312 self.lexer.resume_at(position);
313 return Ok(position);
314 }
315 b'\'' => {
316 self.lexer.skip_text(position)?;
317 position = self.lexer.offset();
318 }
319 _ => {
320 self.lexer.resume_at(position);
321 self.lexer.skip_trivia()?;
322 position = self.lexer.offset().max(position + 1);
323 }
324 }
325 }
326 }
327}
328
329/// A name token as text. The lexer admits only ASCII in names, so the lossy
330/// conversion never replaces anything.
331fn ascii(bytes: Cow<'_, [u8]>) -> Cow<'_, str> {
332 match bytes {
333 Cow::Borrowed(bytes) => ascii_borrowed(bytes),
334 Cow::Owned(bytes) => Cow::Owned(String::from_utf8_lossy(&bytes).into_owned()),
335 }
336}
337
338fn ascii_borrowed(bytes: &[u8]) -> Cow<'_, str> {
339 std::str::from_utf8(bytes).map_or_else(|_| String::from_utf8_lossy(bytes), Cow::Borrowed)
340}