Skip to main content

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}