1use std::collections::BTreeMap;
11use std::fmt::Write;
12
13use geop_core_math::geop_error::{GeopError, GeopResult};
14
15#[derive(Clone, Debug, PartialEq)]
17pub enum Value {
18 Null,
20 Derived,
22 Integer(i64),
23 Real(f64),
24 String(String),
25 Enum(String),
27 Ref(u64),
29 Binary(String),
31 List(Vec<Value>),
32 Typed(String, Box<Value>),
34}
35
36#[derive(Clone, Debug, PartialEq)]
38pub struct Record {
39 pub name: String,
40 pub args: Vec<Value>,
41}
42
43#[derive(Clone, Debug, PartialEq)]
46pub enum Instance {
47 Simple(Record),
48 Complex(Vec<Record>),
49}
50
51impl Instance {
52 pub fn record(&self, name: &str) -> Option<&Record> {
55 match self {
56 Instance::Simple(r) => (r.name == name).then_some(r),
57 Instance::Complex(rs) => rs.iter().find(|r| r.name == name),
58 }
59 }
60
61 pub fn is(&self, name: &str) -> bool {
63 self.record(name).is_some()
64 }
65
66 pub fn type_name(&self) -> String {
69 match self {
70 Instance::Simple(r) => r.name.clone(),
71 Instance::Complex(rs) => rs
72 .iter()
73 .map(|r| r.name.as_str())
74 .collect::<Vec<_>>()
75 .join("+"),
76 }
77 }
78}
79
80#[derive(Clone, Debug, Default, PartialEq)]
82pub struct Exchange {
83 pub header: Vec<Record>,
84 pub instances: BTreeMap<u64, Instance>,
85}
86
87impl Exchange {
88 pub fn get(&self, id: u64) -> GeopResult<&Instance> {
90 self.instances.get(&id).ok_or_else(|| {
91 GeopError::new(format!("the file refers to #{id}, which it does not have"))
92 })
93 }
94
95 pub fn all_of(&self, name: &str) -> Vec<u64> {
97 self.instances
98 .iter()
99 .filter(|(_, instance)| instance.is(name))
100 .map(|(&id, _)| id)
101 .collect()
102 }
103
104 pub fn parse(text: &str) -> GeopResult<Self> {
106 Parser {
107 lexer: Lexer {
108 bytes: text.as_bytes(),
109 pos: 0,
110 },
111 }
112 .exchange()
113 }
114
115 pub fn write(&self) -> String {
117 let mut out = String::from("ISO-10303-21;\nHEADER;\n");
118 for record in &self.header {
119 write_record(&mut out, record);
120 out.push_str(";\n");
121 }
122 out.push_str("ENDSEC;\nDATA;\n");
123 for (id, instance) in &self.instances {
124 let _ = write!(out, "#{id}=");
125 match instance {
126 Instance::Simple(record) => write_record(&mut out, record),
127 Instance::Complex(records) => {
128 out.push('(');
129 for (k, record) in records.iter().enumerate() {
130 if k > 0 {
131 out.push(' ');
132 }
133 write_record(&mut out, record);
134 }
135 out.push(')');
136 }
137 }
138 out.push_str(";\n");
139 }
140 out.push_str("ENDSEC;\nEND-ISO-10303-21;\n");
141 out
142 }
143}
144
145fn write_record(out: &mut String, record: &Record) {
146 out.push_str(&record.name);
147 write_list(out, &record.args);
148}
149
150fn write_list(out: &mut String, values: &[Value]) {
151 out.push('(');
152 for (k, value) in values.iter().enumerate() {
153 if k > 0 {
154 out.push(',');
155 }
156 write_value(out, value);
157 }
158 out.push(')');
159}
160
161fn write_value(out: &mut String, value: &Value) {
162 match value {
163 Value::Null => out.push('$'),
164 Value::Derived => out.push('*'),
165 Value::Integer(i) => {
166 let _ = write!(out, "{i}");
167 }
168 Value::Real(r) => out.push_str(&real(*r)),
169 Value::String(s) => {
170 out.push('\'');
171 for c in s.chars() {
172 match c {
173 '\'' => out.push_str("''"),
174 '\\' => out.push_str("\\\\"),
175 ' '..='~' => out.push(c),
176 c => {
177 let mut units = [0u16; 2];
178 for unit in c.encode_utf16(&mut units) {
179 let _ = write!(out, "\\X2\\{unit:04X}\\X0\\");
180 }
181 }
182 }
183 }
184 out.push('\'');
185 }
186 Value::Enum(e) => {
187 let _ = write!(out, ".{e}.");
188 }
189 Value::Ref(id) => {
190 let _ = write!(out, "#{id}");
191 }
192 Value::Binary(b) => {
193 let _ = write!(out, "\"{b}\"");
194 }
195 Value::List(values) => write_list(out, values),
196 Value::Typed(name, value) => {
197 out.push_str(name);
198 out.push('(');
199 write_value(out, value);
200 out.push(')');
201 }
202 }
203}
204
205pub fn real(r: f64) -> String {
209 let text = format!("{r:?}");
210 let (mantissa, exponent) = match text.split_once('e') {
211 Some((m, e)) => (m.to_string(), Some(e.to_string())),
212 None => (text, None),
213 };
214 let mantissa = if mantissa.contains('.') {
215 mantissa
216 } else {
217 format!("{mantissa}.")
218 };
219 match exponent {
220 Some(e) => format!("{mantissa}E{e}"),
221 None => mantissa,
222 }
223}
224
225#[derive(Clone, Debug, PartialEq)]
226enum Token {
227 Keyword(String),
230 Ref(u64),
231 Integer(i64),
232 Real(f64),
233 String(String),
234 Enum(String),
235 Binary(String),
236 Null,
237 Derived,
238 Open,
239 Close,
240 Comma,
241 Equals,
242 Semicolon,
243 End,
244}
245
246struct Lexer<'a> {
247 bytes: &'a [u8],
248 pos: usize,
249}
250
251impl Lexer<'_> {
252 fn error(&self, what: impl std::fmt::Display) -> GeopError {
253 let line = 1 + self.bytes[..self.pos.min(self.bytes.len())]
254 .iter()
255 .filter(|&&b| b == b'\n')
256 .count();
257 GeopError::new(format!("reading the STEP file, line {line}: {what}"))
258 }
259
260 fn skip_space(&mut self) -> GeopResult<()> {
261 loop {
262 match self.bytes.get(self.pos) {
263 Some(b) if b.is_ascii_whitespace() => self.pos += 1,
264 Some(b'/') if self.bytes.get(self.pos + 1) == Some(&b'*') => {
265 let start = self.pos;
266 self.pos += 2;
267 loop {
268 match self.bytes.get(self.pos) {
269 None => {
270 self.pos = start;
271 return Err(self.error("a comment that never ends"));
272 }
273 Some(b'*') if self.bytes.get(self.pos + 1) == Some(&b'/') => {
274 self.pos += 2;
275 break;
276 }
277 Some(_) => self.pos += 1,
278 }
279 }
280 }
281 _ => return Ok(()),
282 }
283 }
284 }
285
286 fn next(&mut self) -> GeopResult<Token> {
287 self.skip_space()?;
288 let Some(&b) = self.bytes.get(self.pos) else {
289 return Ok(Token::End);
290 };
291 let single = |lexer: &mut Self, token| {
292 lexer.pos += 1;
293 Ok(token)
294 };
295 match b {
296 b'(' => single(self, Token::Open),
297 b')' => single(self, Token::Close),
298 b',' => single(self, Token::Comma),
299 b'=' => single(self, Token::Equals),
300 b';' => single(self, Token::Semicolon),
301 b'$' => single(self, Token::Null),
302 b'*' => single(self, Token::Derived),
303 b'#' => {
304 self.pos += 1;
305 let start = self.pos;
306 while self.bytes.get(self.pos).is_some_and(u8::is_ascii_digit) {
307 self.pos += 1;
308 }
309 let digits = std::str::from_utf8(&self.bytes[start..self.pos]).unwrap_or("");
310 digits
311 .parse()
312 .map(Token::Ref)
313 .map_err(|_| self.error("a `#` that is not followed by an instance number"))
314 }
315 b'\'' => self.string(),
316 b'"' => {
317 self.pos += 1;
318 let start = self.pos;
319 while self.bytes.get(self.pos).is_some_and(|&c| c != b'"') {
320 self.pos += 1;
321 }
322 let text = String::from_utf8_lossy(&self.bytes[start..self.pos]).into_owned();
323 self.pos += 1;
324 Ok(Token::Binary(text))
325 }
326 b'.' if self
327 .bytes
328 .get(self.pos + 1)
329 .is_some_and(|c| c.is_ascii_alphabetic() || *c == b'_') =>
330 {
331 self.pos += 1;
332 let start = self.pos;
333 while self
334 .bytes
335 .get(self.pos)
336 .is_some_and(|c| c.is_ascii_alphanumeric() || *c == b'_')
337 {
338 self.pos += 1;
339 }
340 let name = String::from_utf8_lossy(&self.bytes[start..self.pos]).to_uppercase();
341 if self.bytes.get(self.pos) != Some(&b'.') {
342 return Err(
343 self.error(format!("the enumeration value .{name} has no closing `.`"))
344 );
345 }
346 self.pos += 1;
347 Ok(Token::Enum(name))
348 }
349 b'+' | b'-' | b'.' | b'0'..=b'9' => self.number(),
350 b'!' | b'A'..=b'Z' | b'a'..=b'z' | b'_' => {
351 let start = self.pos;
352 self.pos += 1;
353 while self
354 .bytes
355 .get(self.pos)
356 .is_some_and(|c| c.is_ascii_alphanumeric() || *c == b'_' || *c == b'-')
357 {
358 self.pos += 1;
359 }
360 Ok(Token::Keyword(
361 String::from_utf8_lossy(&self.bytes[start..self.pos]).to_uppercase(),
362 ))
363 }
364 other => Err(self.error(format!("an unexpected character {:?}", other as char))),
365 }
366 }
367
368 fn number(&mut self) -> GeopResult<Token> {
369 let start = self.pos;
370 if matches!(self.bytes.get(self.pos), Some(b'+' | b'-')) {
371 self.pos += 1;
372 }
373 let mut real = false;
374 while let Some(&c) = self.bytes.get(self.pos) {
375 match c {
376 b'0'..=b'9' => self.pos += 1,
377 b'.' => {
378 real = true;
379 self.pos += 1;
380 }
381 b'E' | b'e' => {
382 real = true;
383 self.pos += 1;
384 if matches!(self.bytes.get(self.pos), Some(b'+' | b'-')) {
385 self.pos += 1;
386 }
387 }
388 _ => break,
389 }
390 }
391 let text = std::str::from_utf8(&self.bytes[start..self.pos]).unwrap_or("");
392 if real {
393 let text = text.replace(".E", ".0E").replace(".e", ".0e");
395 let text = if text.ends_with('.') {
396 format!("{text}0")
397 } else {
398 text
399 };
400 text.parse()
401 .map(Token::Real)
402 .map_err(|_| self.error(format!("{text:?} is not a number")))
403 } else {
404 text.parse()
405 .map(Token::Integer)
406 .map_err(|_| self.error(format!("{text:?} is not a number")))
407 }
408 }
409
410 fn string(&mut self) -> GeopResult<Token> {
414 self.pos += 1;
415 let mut raw = Vec::new();
416 loop {
417 match self.bytes.get(self.pos) {
418 None => return Err(self.error("a string that never ends")),
419 Some(b'\'') if self.bytes.get(self.pos + 1) == Some(&b'\'') => {
420 raw.push(b'\'');
421 self.pos += 2;
422 }
423 Some(b'\'') => {
424 self.pos += 1;
425 break;
426 }
427 Some(&c) => {
428 raw.push(c);
429 self.pos += 1;
430 }
431 }
432 }
433 let raw = String::from_utf8_lossy(&raw).into_owned();
434 Ok(Token::String(decode(&raw)))
435 }
436}
437
438fn decode(raw: &str) -> String {
439 let mut out = String::new();
440 let mut rest = raw;
441 while let Some(k) = rest.find('\\') {
442 out.push_str(&rest[..k]);
443 rest = &rest[k..];
444 if let Some(after) = rest.strip_prefix("\\\\") {
445 out.push('\\');
446 rest = after;
447 } else if let Some(after) = rest.strip_prefix("\\X2\\") {
448 let end = after.find("\\X0\\").unwrap_or(after.len());
449 let hex = &after[..end];
450 let units: Vec<u16> = (0..hex.len() / 4)
451 .filter_map(|i| u16::from_str_radix(&hex[4 * i..4 * i + 4], 16).ok())
452 .collect();
453 out.push_str(&String::from_utf16_lossy(&units));
454 rest = after.get(end + 4..).unwrap_or("");
455 } else if let Some(after) = rest.strip_prefix("\\X\\") {
456 match after.get(..2).and_then(|h| u8::from_str_radix(h, 16).ok()) {
457 Some(byte) => {
458 out.push(byte as char);
459 rest = &after[2..];
460 }
461 None => {
462 out.push_str("\\X\\");
463 rest = after;
464 }
465 }
466 } else {
467 out.push('\\');
468 rest = &rest[1..];
469 }
470 }
471 out.push_str(rest);
472 out
473}
474
475struct Parser<'a> {
476 lexer: Lexer<'a>,
477}
478
479impl Parser<'_> {
480 fn expect(&mut self, expected: Token) -> GeopResult<()> {
481 let token = self.lexer.next()?;
482 if token == expected {
483 Ok(())
484 } else {
485 Err(self
486 .lexer
487 .error(format!("expected {expected:?}, found {token:?}")))
488 }
489 }
490
491 fn exchange(mut self) -> GeopResult<Exchange> {
492 let mut exchange = Exchange::default();
493 loop {
494 match self.lexer.next()? {
495 Token::End => break,
496 Token::Keyword(k) if k == "ISO-10303-21" => self.expect(Token::Semicolon)?,
497 Token::Keyword(k) if k == "END-ISO-10303-21" => {
498 self.expect(Token::Semicolon)?;
499 break;
500 }
501 Token::Keyword(k) if k == "HEADER" => {
502 self.expect(Token::Semicolon)?;
503 loop {
504 match self.lexer.next()? {
505 Token::Keyword(k) if k == "ENDSEC" => {
506 self.expect(Token::Semicolon)?;
507 break;
508 }
509 Token::Keyword(name) => {
510 let record = self.record_after(name)?;
511 exchange.header.push(record);
512 self.expect(Token::Semicolon)?;
513 }
514 other => {
515 return Err(self
516 .lexer
517 .error(format!("expected a header entry, found {other:?}")));
518 }
519 }
520 }
521 }
522 Token::Keyword(k) if k == "DATA" => {
523 match self.lexer.next()? {
526 Token::Semicolon => {}
527 Token::Open => {
528 self.list_after_open()?;
529 self.expect(Token::Semicolon)?;
530 }
531 other => {
532 return Err(self
533 .lexer
534 .error(format!("expected `;` after DATA, found {other:?}")));
535 }
536 }
537 self.data(&mut exchange)?;
538 }
539 other => {
540 return Err(self
541 .lexer
542 .error(format!("expected a section, found {other:?}")));
543 }
544 }
545 }
546 Ok(exchange)
547 }
548
549 fn data(&mut self, exchange: &mut Exchange) -> GeopResult<()> {
550 loop {
551 match self.lexer.next()? {
552 Token::Keyword(k) if k == "ENDSEC" => {
553 self.expect(Token::Semicolon)?;
554 return Ok(());
555 }
556 Token::Ref(id) => {
557 self.expect(Token::Equals)?;
558 let instance = match self.lexer.next()? {
559 Token::Keyword(name) => Instance::Simple(self.record_after(name)?),
560 Token::Open => {
561 let mut records = Vec::new();
562 loop {
563 match self.lexer.next()? {
564 Token::Close => break,
565 Token::Keyword(name) => records.push(self.record_after(name)?),
566 other => {
567 return Err(self.lexer.error(format!(
568 "#{id}: expected an entity of a complex instance, found {other:?}"
569 )));
570 }
571 }
572 }
573 Instance::Complex(records)
574 }
575 other => {
576 return Err(self
577 .lexer
578 .error(format!("#{id}: expected an entity, found {other:?}")));
579 }
580 };
581 self.expect(Token::Semicolon)?;
582 if exchange.instances.insert(id, instance).is_some() {
583 return Err(self.lexer.error(format!("#{id} is defined twice")));
584 }
585 }
586 other => {
587 return Err(self
588 .lexer
589 .error(format!("expected an instance `#id = ...`, found {other:?}")));
590 }
591 }
592 }
593 }
594
595 fn record_after(&mut self, name: String) -> GeopResult<Record> {
597 self.expect(Token::Open)?;
598 Ok(Record {
599 name,
600 args: self.list_after_open()?,
601 })
602 }
603
604 fn list_after_open(&mut self) -> GeopResult<Vec<Value>> {
606 let mut values = Vec::new();
607 let mut token = self.lexer.next()?;
608 if token == Token::Close {
609 return Ok(values);
610 }
611 loop {
612 values.push(self.value(token)?);
613 match self.lexer.next()? {
614 Token::Comma => token = self.lexer.next()?,
615 Token::Close => return Ok(values),
616 other => {
617 return Err(self
618 .lexer
619 .error(format!("expected `,` or `)` in a list, found {other:?}")));
620 }
621 }
622 }
623 }
624
625 fn value(&mut self, token: Token) -> GeopResult<Value> {
626 Ok(match token {
627 Token::Null => Value::Null,
628 Token::Derived => Value::Derived,
629 Token::Integer(i) => Value::Integer(i),
630 Token::Real(r) => Value::Real(r),
631 Token::String(s) => Value::String(s),
632 Token::Enum(e) => Value::Enum(e),
633 Token::Ref(id) => Value::Ref(id),
634 Token::Binary(b) => Value::Binary(b),
635 Token::Open => Value::List(self.list_after_open()?),
636 Token::Keyword(name) => {
637 self.expect(Token::Open)?;
638 let mut inner = self.list_after_open()?;
639 let value = if inner.len() == 1 {
640 inner.pop().expect("one value")
641 } else {
642 Value::List(inner)
643 };
644 Value::Typed(name, Box::new(value))
645 }
646 other => {
647 return Err(self
648 .lexer
649 .error(format!("expected a value, found {other:?}")));
650 }
651 })
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658
659 const SAMPLE: &str = "ISO-10303-21;
660HEADER;
661/* a comment */
662FILE_DESCRIPTION(('a test'),'2;1');
663FILE_NAME('it''s.stp','2026-10-04T00:00:00',('me'),(''),'','','');
664FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
665ENDSEC;
666DATA;
667#1=CARTESIAN_POINT('',(0.,1.5,-2.E-3));
668#2 = DIRECTION ( 'd' , ( 0.0 , 0.0 , 1.0 ) ) ;
669#3=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));
670#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-05),#3,'distance_accuracy_value','');
671#5=B_SPLINE_CURVE_WITH_KNOTS('',2,(#1,#1,#1),.UNSPECIFIED.,.F.,.F.,(3,3),(0.,1.),.UNSPECIFIED.);
672#6=PRODUCT('caf\\X2\\00E9\\X0\\','',$,(#7));
673ENDSEC;
674END-ISO-10303-21;
675";
676
677 #[test]
678 fn a_file_reads_with_complex_instances_and_typed_values() {
679 let exchange = Exchange::parse(SAMPLE).unwrap();
680 assert_eq!(exchange.header.len(), 3);
681 assert_eq!(exchange.header[1].args[0], Value::String("it's.stp".into()));
682 let point = exchange.get(1).unwrap().record("CARTESIAN_POINT").unwrap();
683 assert_eq!(
684 point.args[1],
685 Value::List(vec![Value::Real(0.0), Value::Real(1.5), Value::Real(-2e-3)])
686 );
687 let unit = exchange.get(3).unwrap();
688 assert!(unit.is("SI_UNIT") && unit.is("LENGTH_UNIT"));
689 assert_eq!(
690 unit.record("SI_UNIT").unwrap().args,
691 vec![Value::Enum("MILLI".into()), Value::Enum("METRE".into())]
692 );
693 assert_eq!(
694 exchange
695 .get(4)
696 .unwrap()
697 .record("UNCERTAINTY_MEASURE_WITH_UNIT")
698 .unwrap()
699 .args[0],
700 Value::Typed("LENGTH_MEASURE".into(), Box::new(Value::Real(1e-5)))
701 );
702 assert_eq!(
703 exchange.get(6).unwrap().record("PRODUCT").unwrap().args[0],
704 Value::String("café".into())
705 );
706 assert!(exchange.get(7).is_err());
707 }
708
709 #[test]
710 fn a_written_file_reads_back_the_same() {
711 let exchange = Exchange::parse(SAMPLE).unwrap();
712 let again = Exchange::parse(&exchange.write()).unwrap();
713 assert_eq!(exchange, again);
714 }
715
716 #[test]
717 fn reals_are_written_as_part_21_reads_them() {
718 assert_eq!(real(1.0), "1.0");
719 assert_eq!(real(-0.25), "-0.25");
720 assert_eq!(real(1.5e-7), "1.5E-7");
721 assert_eq!(real(1e20), "1.E20");
722 for r in [0.1, 1.0 / 3.0, -123456.789e-30, 2.5e300] {
723 let Value::Real(back) = Exchange::parse(&format!("DATA;#1=A({});ENDSEC;", real(r)))
724 .unwrap()
725 .get(1)
726 .unwrap()
727 .record("A")
728 .unwrap()
729 .args[0]
730 else {
731 panic!()
732 };
733 assert_eq!(back, r);
734 }
735 }
736
737 #[test]
738 fn errors_say_where() {
739 let error = Exchange::parse("ISO-10303-21;\nDATA;\n#1=A(1,;\nENDSEC;").unwrap_err();
740 assert!(error.to_string().contains("line 3"), "{error}");
741 }
742}