Skip to main content

falcon_library/model3d/
step.rs

1//! A reader for STEP physical files (ISO 10303-21, the text form IFC
2//! files use): the header's schema name and the `DATA;` section's
3//! instances, `#id = ENTITY(args);`, with nested lists, strings, enums,
4//! `$`, `*`, references and typed values such as `IFCLABEL('x')`.
5//!
6//! The parser works on tokens with an explicit stack, so nesting costs no
7//! recursion; lists deeper than [`MAX_DEPTH`] are refused. Line breaks and
8//! comments may fall anywhere between tokens.
9
10use std::collections::HashMap;
11
12use super::{ParseError, Result};
13
14/// How deeply lists may nest inside one instance.
15pub const MAX_DEPTH: usize = 64;
16
17/// One parameter of an instance.
18#[derive(Debug, Clone, PartialEq)]
19pub enum Value {
20    /// `$`: no value.
21    Null,
22    /// `*`: derived from a supertype.
23    Derived,
24    Int(i64),
25    Real(f64),
26    Str(String),
27    /// `.NAME.`, without the dots.
28    Enum(String),
29    /// `#id`.
30    Ref(u64),
31    /// `"hex"`.
32    Binary(String),
33    List(Vec<Value>),
34    /// `NAME(args)` inside a parameter list.
35    Typed(String, Vec<Value>),
36}
37
38impl Value {
39    pub fn as_ref_id(&self) -> Option<u64> {
40        match self {
41            Value::Ref(r) => Some(*r),
42            _ => None,
43        }
44    }
45
46    /// A number, integer or real; a typed number such as
47    /// `IFCLENGTHMEASURE(2.)` too.
48    pub fn as_f64(&self) -> Option<f64> {
49        match self {
50            Value::Int(i) => Some(*i as f64),
51            Value::Real(r) => Some(*r),
52            Value::Typed(_, v) if v.len() == 1 => v[0].as_f64(),
53            _ => None,
54        }
55    }
56
57    pub fn as_int(&self) -> Option<i64> {
58        match self {
59            Value::Int(i) => Some(*i),
60            _ => None,
61        }
62    }
63
64    pub fn as_list(&self) -> Option<&[Value]> {
65        match self {
66            Value::List(l) => Some(l),
67            _ => None,
68        }
69    }
70
71    /// A string, or a typed string such as `IFCLABEL('x')`.
72    pub fn as_str(&self) -> Option<&str> {
73        match self {
74            Value::Str(s) => Some(s),
75            Value::Typed(_, v) if v.len() == 1 => v[0].as_str(),
76            _ => None,
77        }
78    }
79
80    pub fn as_enum(&self) -> Option<&str> {
81        match self {
82            Value::Enum(s) => Some(s),
83            _ => None,
84        }
85    }
86}
87
88/// One instance: its entity name in capitals and its parameters. A
89/// complex instance (`#1=(A(..)B(..));`) is named `""` and holds its parts
90/// as typed values.
91#[derive(Debug, Clone, PartialEq)]
92pub struct Entity {
93    pub name: String,
94    pub args: Vec<Value>,
95}
96
97impl Entity {
98    pub fn arg(&self, k: usize) -> &Value {
99        self.args.get(k).unwrap_or(&Value::Null)
100    }
101}
102
103/// A whole file: the schema names from `FILE_SCHEMA` and the instances by
104/// id.
105#[derive(Debug, Default)]
106pub struct StepFile {
107    pub schemas: Vec<String>,
108    pub entities: HashMap<u64, Entity>,
109}
110
111impl StepFile {
112    pub fn get(&self, id: u64) -> Option<&Entity> {
113        self.entities.get(&id)
114    }
115
116    /// The instance a value refers to, if it is a reference to one.
117    pub fn deref(&self, v: &Value) -> Option<&Entity> {
118        self.get(v.as_ref_id()?)
119    }
120
121    /// The instance a value refers to when it is named `name`.
122    pub fn deref_named(&self, v: &Value, name: &str) -> Option<&Entity> {
123        self.deref(v).filter(|e| e.name == name)
124    }
125}
126
127#[derive(Debug, Clone, PartialEq)]
128enum Tok {
129    Open,
130    Close,
131    Comma,
132    Semi,
133    Eq,
134    Dollar,
135    Star,
136    Int(i64),
137    Real(f64),
138    Str(String),
139    Enum(String),
140    Ref(u64),
141    Binary(String),
142    Keyword(String),
143}
144
145struct Lexer<'a> {
146    b: &'a [u8],
147    i: usize,
148}
149
150fn err(msg: &str) -> ParseError {
151    ParseError(format!("IFC: {msg}"))
152}
153
154impl<'a> Lexer<'a> {
155    fn skip_space(&mut self) -> Result<()> {
156        loop {
157            while self.i < self.b.len() && self.b[self.i].is_ascii_whitespace() {
158                self.i += 1;
159            }
160            if self.b[self.i..].starts_with(b"/*") {
161                match self.b[self.i + 2..].windows(2).position(|w| w == b"*/") {
162                    Some(p) => self.i += 2 + p + 2,
163                    None => return Err(err("unterminated comment")),
164                }
165            } else {
166                return Ok(());
167            }
168        }
169    }
170
171    /// The next token, `None` at the end of the input.
172    fn next(&mut self) -> Result<Option<Tok>> {
173        self.skip_space()?;
174        let Some(&c) = self.b.get(self.i) else { return Ok(None) };
175        let start = self.i;
176        self.i += 1;
177        let t = match c {
178            b'(' => Tok::Open,
179            b')' => Tok::Close,
180            b',' => Tok::Comma,
181            b';' => Tok::Semi,
182            b'=' => Tok::Eq,
183            b'$' => Tok::Dollar,
184            b'*' => Tok::Star,
185            b'#' => {
186                let s = self.take_while(|c| c.is_ascii_digit());
187                Tok::Ref(s.parse().map_err(|_| err("bad instance name"))?)
188            }
189            b'\'' => Tok::Str(self.string()?),
190            b'"' => {
191                let s = self.take_while(|c| c.is_ascii_hexdigit()).to_string();
192                if self.b.get(self.i) != Some(&b'"') {
193                    return Err(err("unterminated binary value"));
194                }
195                self.i += 1;
196                Tok::Binary(s)
197            }
198            b'.' if self.b.get(self.i).is_some_and(|c| c.is_ascii_alphabetic() || *c == b'_') => {
199                let s = self.take_while(|c| c.is_ascii_alphanumeric() || c == b'_').to_ascii_uppercase();
200                if self.b.get(self.i) != Some(&b'.') {
201                    return Err(err("unterminated enumeration"));
202                }
203                self.i += 1;
204                Tok::Enum(s)
205            }
206            b'+' | b'-' | b'0'..=b'9' | b'.' => {
207                self.i = start;
208                let s = self.take_while(|c| c.is_ascii_digit() || matches!(c, b'+' | b'-' | b'.' | b'e' | b'E'));
209                if s.contains(['.', 'e', 'E']) {
210                    Tok::Real(s.parse().map_err(|_| err("bad number"))?)
211                } else {
212                    Tok::Int(s.parse().map_err(|_| err("bad number"))?)
213                }
214            }
215            c if c.is_ascii_alphabetic() || c == b'_' || c == b'!' => {
216                self.i = start;
217                let s = self.take_while(|c| c.is_ascii_alphanumeric() || c == b'_' || c == b'-' || c == b'!');
218                Tok::Keyword(s.to_ascii_uppercase())
219            }
220            _ => return Err(err("unexpected character")),
221        };
222        Ok(Some(t))
223    }
224
225    fn take_while(&mut self, f: impl Fn(u8) -> bool) -> &'a str {
226        let start = self.i;
227        while self.i < self.b.len() && f(self.b[self.i]) {
228            self.i += 1;
229        }
230        // Only ASCII bytes pass the filters used here.
231        std::str::from_utf8(&self.b[start..self.i]).unwrap_or("")
232    }
233
234    /// A quoted string after its opening quote; `''` is a quote.
235    fn string(&mut self) -> Result<String> {
236        let mut raw = Vec::new();
237        loop {
238            let Some(&c) = self.b.get(self.i) else { return Err(err("unterminated string")) };
239            self.i += 1;
240            if c == b'\'' {
241                if self.b.get(self.i) == Some(&b'\'') {
242                    raw.push(b'\'');
243                    self.i += 1;
244                } else {
245                    break;
246                }
247            } else if c != b'\n' && c != b'\r' {
248                raw.push(c);
249            }
250        }
251        Ok(decode_string(&raw))
252    }
253}
254
255/// Decodes the escapes of ISO 10303-21 strings: `\\`, `\S\c`, `\X\hh`,
256/// `\X2\hhhh…\X0\` and `\X4\hhhhhhhh…\X0\`; `\P?\` code page switches are
257/// dropped. Bytes that are not escapes are read as UTF-8 (lossily), which
258/// many exporters write directly.
259pub fn decode_string(raw: &[u8]) -> String {
260    let mut out = String::new();
261    let mut plain: Vec<u8> = Vec::new();
262    let flush = |plain: &mut Vec<u8>, out: &mut String| {
263        out.push_str(&String::from_utf8_lossy(plain));
264        plain.clear();
265    };
266    let hex = |s: &[u8]| std::str::from_utf8(s).ok().and_then(|s| u32::from_str_radix(s, 16).ok());
267    let mut i = 0;
268    while i < raw.len() {
269        let rest = &raw[i..];
270        if rest.starts_with(b"\\\\") {
271            plain.push(b'\\');
272            i += 2;
273        } else if rest.starts_with(b"\\X2\\") || rest.starts_with(b"\\X4\\") {
274            let width = if rest[2] == b'2' { 4 } else { 8 };
275            let end = rest[4..].windows(4).position(|w| w == b"\\X0\\").map(|p| p + 4);
276            let Some(end) = end else {
277                plain.extend_from_slice(rest);
278                break;
279            };
280            flush(&mut plain, &mut out);
281            let units: Vec<u32> = rest[4..end].chunks(width).filter_map(hex).collect();
282            if width == 4 {
283                let u16s: Vec<u16> = units.iter().map(|&u| u as u16).collect();
284                out.push_str(&String::from_utf16_lossy(&u16s));
285            } else {
286                out.extend(units.iter().filter_map(|&u| char::from_u32(u)));
287            }
288            i += end + 4;
289        } else if rest.starts_with(b"\\X\\") && rest.len() >= 5 {
290            flush(&mut plain, &mut out);
291            if let Some(c) = hex(&rest[3..5]).and_then(char::from_u32) {
292                out.push(c);
293            }
294            i += 5;
295        } else if rest.starts_with(b"\\S\\") && rest.len() >= 4 {
296            flush(&mut plain, &mut out);
297            if let Some(c) = char::from_u32(u32::from(rest[3]) + 128) {
298                out.push(c);
299            }
300            i += 4;
301        } else if rest.starts_with(b"\\P") && rest.len() >= 4 && rest[3] == b'\\' {
302            i += 4;
303        } else {
304            plain.push(raw[i]);
305            i += 1;
306        }
307    }
308    flush(&mut plain, &mut out);
309    out
310}
311
312/// A partly built list: the typed name it belongs to, if any, and its
313/// values so far.
314struct Frame {
315    typed: Option<String>,
316    values: Vec<Value>,
317}
318
319/// Reads one parameter list after its opening `(`, up to and including
320/// the matching `)`, without recursion.
321fn read_list(lx: &mut Lexer) -> Result<Vec<Value>> {
322    let mut stack = vec![Frame { typed: None, values: Vec::new() }];
323    let mut pending: Option<String> = None;
324    loop {
325        let Some(t) = lx.next()? else { return Err(err("the file is truncated")) };
326        if pending.is_some() && t != Tok::Open {
327            return Err(err("a typed value without its parameters"));
328        }
329        let v = match t {
330            Tok::Open => {
331                if stack.len() >= MAX_DEPTH {
332                    return Err(err("lists are nested too deeply"));
333                }
334                stack.push(Frame { typed: pending.take(), values: Vec::new() });
335                continue;
336            }
337            Tok::Close => {
338                let f = stack.pop().expect("a frame is open");
339                let v = match f.typed {
340                    Some(name) => Value::Typed(name, f.values),
341                    None => Value::List(f.values),
342                };
343                match stack.last_mut() {
344                    Some(parent) => {
345                        parent.values.push(v);
346                        continue;
347                    }
348                    None => {
349                        let Value::List(values) = v else { unreachable!("the outer frame is untyped") };
350                        return Ok(values);
351                    }
352                }
353            }
354            Tok::Comma => continue,
355            Tok::Keyword(k) => {
356                pending = Some(k);
357                continue;
358            }
359            Tok::Dollar => Value::Null,
360            Tok::Star => Value::Derived,
361            Tok::Int(i) => Value::Int(i),
362            Tok::Real(r) => Value::Real(r),
363            Tok::Str(s) => Value::Str(s),
364            Tok::Enum(s) => Value::Enum(s),
365            Tok::Ref(r) => Value::Ref(r),
366            Tok::Binary(s) => Value::Binary(s),
367            Tok::Semi | Tok::Eq => return Err(err("a parameter list is not closed")),
368        };
369        stack.last_mut().expect("a frame is open").values.push(v);
370    }
371}
372
373fn expect(lx: &mut Lexer, want: Tok) -> Result<()> {
374    match lx.next()? {
375        Some(t) if t == want => Ok(()),
376        Some(_) => Err(err("unexpected token")),
377        None => Err(err("the file is truncated")),
378    }
379}
380
381/// Reads `NAME(args)` or, for a complex instance, `(A(..) B(..))`, then the
382/// closing `;`.
383fn read_record(lx: &mut Lexer) -> Result<Entity> {
384    let e = match lx.next()? {
385        Some(Tok::Keyword(name)) => {
386            expect(lx, Tok::Open)?;
387            Entity { name, args: read_list(lx)? }
388        }
389        Some(Tok::Open) => {
390            let parts = read_list(lx)?;
391            Entity { name: String::new(), args: parts }
392        }
393        Some(_) => return Err(err("unexpected token")),
394        None => return Err(err("the file is truncated")),
395    };
396    expect(lx, Tok::Semi)?;
397    Ok(e)
398}
399
400/// Parses a STEP physical file.
401pub fn parse(bytes: &[u8]) -> Result<StepFile> {
402    let bytes = bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(bytes);
403    let mut lx = Lexer { b: bytes, i: 0 };
404    if lx.next()? != Some(Tok::Keyword("ISO-10303-21".into())) {
405        return Err(err("not a STEP file"));
406    }
407    expect(&mut lx, Tok::Semi)?;
408    let mut file = StepFile::default();
409    let mut seen_data = false;
410    loop {
411        match lx.next()? {
412            Some(Tok::Keyword(k)) if k == "HEADER" => {
413                expect(&mut lx, Tok::Semi)?;
414                loop {
415                    match lx.next()? {
416                        Some(Tok::Keyword(k)) if k == "ENDSEC" => {
417                            expect(&mut lx, Tok::Semi)?;
418                            break;
419                        }
420                        Some(Tok::Keyword(k)) => {
421                            expect(&mut lx, Tok::Open)?;
422                            let args = read_list(&mut lx)?;
423                            expect(&mut lx, Tok::Semi)?;
424                            if k == "FILE_SCHEMA" {
425                                let names = args.first().and_then(Value::as_list).unwrap_or_default();
426                                file.schemas = names.iter().filter_map(|v| v.as_str().map(str::to_string)).collect();
427                            }
428                        }
429                        Some(_) => return Err(err("unexpected token in the header")),
430                        None => return Err(err("the file is truncated")),
431                    }
432                }
433            }
434            Some(Tok::Keyword(k)) if k == "DATA" => {
435                // `DATA('name', ...);` in later editions.
436                match lx.next()? {
437                    Some(Tok::Semi) => {}
438                    Some(Tok::Open) => {
439                        read_list(&mut lx)?;
440                        expect(&mut lx, Tok::Semi)?;
441                    }
442                    _ => return Err(err("unexpected token")),
443                }
444                seen_data = true;
445                loop {
446                    match lx.next()? {
447                        Some(Tok::Keyword(k)) if k == "ENDSEC" => {
448                            expect(&mut lx, Tok::Semi)?;
449                            break;
450                        }
451                        Some(Tok::Ref(id)) => {
452                            expect(&mut lx, Tok::Eq)?;
453                            let e = read_record(&mut lx)?;
454                            file.entities.insert(id, e);
455                        }
456                        Some(_) => return Err(err("unexpected token in the data section")),
457                        None => return Err(err("the file is truncated")),
458                    }
459                }
460            }
461            Some(Tok::Keyword(k)) if k == "END-ISO-10303-21" => break,
462            Some(Tok::Keyword(_)) => {
463                // Another section (ANCHOR, REFERENCE, SIGNATURE): skip its
464                // records up to ENDSEC.
465                loop {
466                    match lx.next()? {
467                        Some(Tok::Keyword(k)) if k == "ENDSEC" => break,
468                        Some(_) => {}
469                        None => return Err(err("the file is truncated")),
470                    }
471                }
472            }
473            Some(_) => return Err(err("unexpected token")),
474            None => break,
475        }
476    }
477    if !seen_data {
478        return Err(err("the file has no DATA section"));
479    }
480    Ok(file)
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    #[test]
488    fn reads_instances_with_every_kind_of_value() {
489        let src = "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION(('ViewDefinition'),'2;1');\nFILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;\n\
490            /* a comment */\n#1= IFCCARTESIANPOINT((1.,-2.5E1,3));\n#2=IFCX($,*,.T.,'it''s',#1,\n  (#1,(1,2)),IFCLABEL('a'),\"0F\");\n\
491            #3=(IFCA(1)IFCB(2));\nENDSEC;\nEND-ISO-10303-21;\n";
492        let f = parse(src.as_bytes()).unwrap();
493        assert_eq!(f.schemas, ["IFC4"]);
494        let p = f.get(1).unwrap();
495        assert_eq!(p.name, "IFCCARTESIANPOINT");
496        assert_eq!(p.args[0], Value::List(vec![Value::Real(1.0), Value::Real(-25.0), Value::Int(3)]));
497        let x = f.get(2).unwrap();
498        assert_eq!(x.args[0], Value::Null);
499        assert_eq!(x.args[1], Value::Derived);
500        assert_eq!(x.args[2], Value::Enum("T".into()));
501        assert_eq!(x.args[3].as_str(), Some("it's"));
502        assert_eq!(x.args[4], Value::Ref(1));
503        assert_eq!(x.args[5], Value::List(vec![Value::Ref(1), Value::List(vec![Value::Int(1), Value::Int(2)])]));
504        assert_eq!(x.args[6].as_str(), Some("a"));
505        assert_eq!(x.args[7], Value::Binary("0F".into()));
506        assert_eq!(f.get(3).unwrap().name, "");
507        assert_eq!(f.get(3).unwrap().args.len(), 2);
508    }
509
510    #[test]
511    fn decodes_string_escapes() {
512        assert_eq!(decode_string(br"Caf\X\E9"), "Caf\u{e9}");
513        assert_eq!(decode_string(br"\X2\00FC00DF\X0\x"), "\u{fc}\u{df}x");
514        assert_eq!(decode_string(br"a\\b"), "a\\b");
515        assert_eq!(decode_string(br"\PA\\S\D"), "\u{c4}");
516        assert_eq!(decode_string("Größe".as_bytes()), "Größe");
517    }
518
519    #[test]
520    fn bad_input_is_an_error_not_a_panic() {
521        let head = "ISO-10303-21;HEADER;ENDSEC;DATA;";
522        for bad in [
523            "",
524            "hello",
525            "ISO-10303-21;",
526            "ISO-10303-21;HEADER;FILE_SCHEMA(('IFC4'",
527            &format!("{head}#1=IFCX((1,2);"),
528            &format!("{head}#1=IFCX('open);ENDSEC;"),
529            &format!("{head}#1=IFCX(1);"),
530            &format!("{head}#1=IFCX(1)"),
531            &format!("{head}#1=IFCX(@);ENDSEC;"),
532            &format!("{head}#1=IFCX(IFCLABEL);ENDSEC;"),
533            &format!("{head}#=IFCX(1);ENDSEC;"),
534            &format!("{head}/* never closed"),
535        ] {
536            assert!(parse(bad.as_bytes()).is_err(), "{bad:?}");
537        }
538        // Every prefix of a good file fails cleanly or parses.
539        let good = format!("{head}#1=IFCX((1,(2.,'a')),.E.,$);ENDSEC;END-ISO-10303-21;");
540        for n in 0..good.len() {
541            let _ = parse(&good.as_bytes()[..n]);
542        }
543        assert!(parse(good.as_bytes()).is_ok());
544    }
545
546    #[test]
547    fn deep_nesting_is_refused_without_recursion() {
548        let depth = 200_000;
549        let src = format!("ISO-10303-21;HEADER;ENDSEC;DATA;#1=IFCX({}1{});ENDSEC;", "(".repeat(depth), ")".repeat(depth));
550        let e = parse(src.as_bytes()).unwrap_err();
551        assert!(e.0.contains("nested"), "{e}");
552        // Up to the limit is fine.
553        let ok = MAX_DEPTH - 1;
554        let src = format!("ISO-10303-21;HEADER;ENDSEC;DATA;#1=IFCX({}1{});ENDSEC;", "(".repeat(ok - 1), ")".repeat(ok - 1));
555        assert!(parse(src.as_bytes()).is_ok());
556    }
557}