1use std::collections::HashMap;
11
12use super::{ParseError, Result};
13
14pub const MAX_DEPTH: usize = 64;
16
17#[derive(Debug, Clone, PartialEq)]
19pub enum Value {
20 Null,
22 Derived,
24 Int(i64),
25 Real(f64),
26 Str(String),
27 Enum(String),
29 Ref(u64),
31 Binary(String),
33 List(Vec<Value>),
34 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 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 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#[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#[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 pub fn deref(&self, v: &Value) -> Option<&Entity> {
118 self.get(v.as_ref_id()?)
119 }
120
121 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 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 std::str::from_utf8(&self.b[start..self.i]).unwrap_or("")
232 }
233
234 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
255pub 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
312struct Frame {
315 typed: Option<String>,
316 values: Vec<Value>,
317}
318
319fn 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
381fn 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
400pub 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 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 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 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 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}