Skip to main content

step_io/parser/
p21.rs

1use std::collections::BTreeMap;
2
3use super::entity::{Attribute, Error, Graph, ParseWarning, RawEntity, RawEntityPart};
4use super::lexer::{Lexer, Span, Token, TokenKind};
5use super::schema::SchemaId;
6
7/// Convenience function: parse a complete Part 21 source into an [`Graph`].
8///
9/// # Errors
10///
11/// Returns the first [`Error`] encountered.
12pub fn parse(source: &str) -> Result<Graph, Error> {
13    Parser::new(source).parse()
14}
15
16/// Parse a Part 21 source given as raw bytes. Tries UTF-8 first; on
17/// failure falls back to ISO 8859-1 (Latin-1), which ISO 10303-21 §3.2
18/// defines as the file format's default encoding. Real-world STEP files
19/// frequently embed raw non-ASCII bytes (Cyrillic, Latin-1) directly
20/// instead of using the spec's `\X\` / `\X2\` / `\X4\` escapes — those
21/// bytes decode losslessly under the fallback (every byte 0x00..0xFF
22/// maps 1:1 to U+0000..U+00FF).
23///
24/// # Errors
25///
26/// Returns the first [`Error`] encountered by the underlying parser.
27pub fn parse_bytes(bytes: &[u8]) -> Result<Graph, Error> {
28    if let Ok(s) = std::str::from_utf8(bytes) {
29        parse(s)
30    } else {
31        let s: String = bytes.iter().map(|&b| b as char).collect();
32        parse(&s)
33    }
34}
35
36/// Recursive-descent parser for ISO 10303-21 (Part 21) files.
37///
38/// Consumes `self` on [`Parser::parse`] because the underlying [`Lexer`] is
39/// a one-pass iterator and cannot be rewound.
40/// Maximum parameter nesting depth. In practice STEP parameters nest only a few
41/// levels (nested LIST OF LIST, typed-wrapped measures); this bound of 64 is a
42/// large margin over any realistic file yet small enough that the
43/// recursive-descent frames stay well within a thread's stack (a debug build
44/// spans several frames per nesting level). It turns adversarially deep
45/// `(((…)))` input into a graceful [`Error::NestingTooDeep`] instead of a
46/// stack-overflow abort.
47pub const MAX_NESTING_DEPTH: usize = 64;
48
49pub struct Parser<'src> {
50    lexer: Lexer<'src>,
51    warnings: Vec<ParseWarning>,
52    /// Current parameter nesting depth (guards against stack overflow).
53    depth: usize,
54}
55
56impl<'src> Parser<'src> {
57    #[must_use]
58    pub fn new(source: &'src str) -> Self {
59        Self {
60            lexer: Lexer::new(source),
61            warnings: Vec::new(),
62            depth: 0,
63        }
64    }
65
66    /// Parse the entire Part 21 file and return an [`Graph`].
67    ///
68    /// # Errors
69    ///
70    /// Returns a [`Error`] on the first structural or lexical problem.
71    pub fn parse(self) -> Result<Graph, Error> {
72        let mut this = self;
73        this.parse_file()
74    }
75
76    // ------------------------------------------------------------------
77    // Top-level grammar
78    // ------------------------------------------------------------------
79
80    fn parse_file(&mut self) -> Result<Graph, Error> {
81        // ISO-10303-21;
82        self.expect_token_kind(&TokenKind::IsoStart, "ISO-10303-21")?;
83        self.expect_semicolon()?;
84
85        // HEADER; ... ENDSEC;
86        self.expect_token_kind(&TokenKind::Header, "HEADER")?;
87        self.expect_semicolon()?;
88        let (header, schema) = self.parse_header_section()?;
89        self.expect_token_kind(&TokenKind::EndSec, "ENDSEC")?;
90        self.expect_semicolon()?;
91
92        // Optional P21 edition 3 sections (ANCHOR / REFERENCE / SIGNATURE).
93        // ANCHOR / REFERENCE are parsed (so external references survive);
94        // SIGNATURE and any unrecognised line shape fall back to a discard +
95        // ParseWarning.
96        let mut external_references: BTreeMap<u64, String> = BTreeMap::new();
97        let mut anchors: Vec<(String, u64)> = Vec::new();
98        while self.peek_is_ed3_section()? {
99            self.parse_or_skip_ed3_section(&mut external_references, &mut anchors)?;
100        }
101
102        // DATA; ... ENDSEC;
103        self.expect_token_kind(&TokenKind::Data, "DATA")?;
104        self.expect_semicolon()?;
105        let entities = self.parse_data_section()?;
106        self.expect_token_kind(&TokenKind::EndSec, "ENDSEC")?;
107        self.expect_semicolon()?;
108
109        // END-ISO-10303-21;
110        self.expect_token_kind(&TokenKind::IsoEnd, "END-ISO-10303-21")?;
111        self.expect_semicolon()?;
112
113        // EOF verification — nothing should follow.
114        if let Some(result) = self.lexer.next() {
115            let tok = result?;
116            return Err(Error::UnexpectedToken {
117                expected: "end of file",
118                found: tok.kind,
119                span: tok.span,
120            });
121        }
122
123        Ok(Graph {
124            schema,
125            header,
126            entities,
127            external_references,
128            anchors,
129            warnings: std::mem::take(&mut self.warnings),
130        })
131    }
132
133    // ------------------------------------------------------------------
134    // P21 edition 3 sections (ANCHOR / REFERENCE / SIGNATURE)
135    // ------------------------------------------------------------------
136
137    fn peek_is_ed3_section(&mut self) -> Result<bool, Error> {
138        Ok(matches!(
139            self.peek_kind()?,
140            TokenKind::Keyword(k)
141                if matches!(k.to_uppercase().as_str(),
142                            "ANCHOR" | "REFERENCE" | "SIGNATURE")
143        ))
144    }
145
146    /// Parse an ANCHOR or REFERENCE section into `external_references` /
147    /// `anchors`; SIGNATURE (and any line shape we don't recognise) falls back
148    /// to a discard + `Ed3SectionDiscarded` warning so the parse never fails.
149    fn parse_or_skip_ed3_section(
150        &mut self,
151        external_references: &mut BTreeMap<u64, String>,
152        anchors: &mut Vec<(String, u64)>,
153    ) -> Result<(), Error> {
154        let tok = self.next_token()?;
155        let section = match &tok.kind {
156            TokenKind::Keyword(k) => k.to_uppercase(),
157            _ => unreachable!("peek_is_ed3_section guarantees a Keyword"),
158        };
159        let span = tok.span;
160        self.expect_semicolon()?;
161
162        let mut discarded = false;
163        while !matches!(self.peek_kind()?, TokenKind::EndSec) {
164            // Each entry is one `lhs = rhs ;` line. REFERENCE lines are
165            // `#N = <anchor>`; ANCHOR lines are `<name> = #N`. Anything else
166            // (SIGNATURE bodies, unexpected shapes) is consumed and the whole
167            // section is flagged as discarded.
168            let lhs = self.next_token()?;
169            if !matches!(self.peek_kind()?, TokenKind::Equals) {
170                discarded = true;
171                continue;
172            }
173            self.next_token()?; // consume `=`
174            let rhs = self.next_token()?;
175            match (section.as_str(), lhs.kind, rhs.kind) {
176                ("REFERENCE", TokenKind::EntityRef(id), TokenKind::AnchorRef(s)) => {
177                    external_references.insert(id, s);
178                }
179                ("ANCHOR", TokenKind::AnchorRef(name), TokenKind::EntityRef(id)) => {
180                    anchors.push((name, id));
181                }
182                _ => discarded = true,
183            }
184            // Consume the line's terminating `;` (tolerate its absence).
185            if matches!(self.peek_kind()?, TokenKind::Semicolon) {
186                self.next_token()?;
187            }
188        }
189        self.expect_token_kind(&TokenKind::EndSec, "ENDSEC")?;
190        self.expect_semicolon()?;
191        if discarded || section == "SIGNATURE" {
192            self.warnings
193                .push(ParseWarning::Ed3SectionDiscarded { section, span });
194        }
195        Ok(())
196    }
197
198    // ------------------------------------------------------------------
199    // HEADER section
200    // ------------------------------------------------------------------
201
202    /// Parse the HEADER section content (between `HEADER;` and `ENDSEC;`).
203    ///
204    /// HEADER entities are "uninstantiated": they have no `#N =` prefix, just
205    /// `KEYWORD(...);`. The section must contain at least `FILE_DESCRIPTION`,
206    /// `FILE_NAME`, and `FILE_SCHEMA` in order.
207    fn parse_header_section(&mut self) -> Result<(Vec<RawEntity>, SchemaId), Error> {
208        let mut header = Vec::new();
209        let mut schema_raw = Vec::new();
210
211        // Read header entities until we see ENDSEC.
212        // HEADER entities use an auto-incrementing pseudo-id (not in the file).
213        let mut pseudo_id = 0u64;
214        while !matches!(self.peek_kind()?, TokenKind::EndSec) {
215            let tok = self.next_token()?;
216            let name = match tok.kind {
217                TokenKind::Keyword(name) => name.to_uppercase(),
218                other => {
219                    return Err(Error::UnexpectedToken {
220                        expected: "HEADER entity name",
221                        found: other,
222                        span: tok.span,
223                    });
224                }
225            };
226
227            let attributes = self.parse_parameter_list()?;
228
229            // Extract FILE_SCHEMA strings before consuming the semicolon.
230            if name == "FILE_SCHEMA" {
231                schema_raw = Self::extract_file_schema_strings(&attributes, tok.span)?;
232            }
233
234            // Some non-spec writers omit the trailing `;` after a HEADER
235            // entity. If the next token already starts the following
236            // entity (keyword) or closes the section (ENDSEC), accept
237            // the missing semicolon with a ParseWarning.
238            match self.peek_kind()? {
239                TokenKind::Semicolon => {
240                    self.next_token()?;
241                }
242                TokenKind::Keyword(_) | TokenKind::EndSec => {
243                    self.warnings.push(ParseWarning::MissingHeaderSemicolon {
244                        entity_name: name.clone(),
245                        span: tok.span,
246                    });
247                }
248                _ => {
249                    self.expect_semicolon()?;
250                }
251            }
252
253            pseudo_id += 1;
254            header.push(RawEntity::Simple {
255                id: pseudo_id,
256                name,
257                attributes,
258                span: tok.span,
259            });
260        }
261
262        // Validate required entities.
263        let names: Vec<&str> = header
264            .iter()
265            .filter_map(|e| match e {
266                RawEntity::Simple { name, .. } => Some(name.as_str()),
267                RawEntity::Complex { .. } => None,
268            })
269            .collect();
270
271        if !names.contains(&"FILE_DESCRIPTION") {
272            return Err(Error::MissingHeaderEntity {
273                name: "FILE_DESCRIPTION",
274            });
275        }
276        if !names.contains(&"FILE_NAME") {
277            return Err(Error::MissingHeaderEntity { name: "FILE_NAME" });
278        }
279        if !names.contains(&"FILE_SCHEMA") {
280            return Err(Error::MissingHeaderEntity {
281                name: "FILE_SCHEMA",
282            });
283        }
284
285        let schema = super::schema::identify_schema(&schema_raw);
286
287        Ok((header, schema))
288    }
289
290    /// Extract all string values from the first (and typically only) list
291    /// attribute of a `FILE_SCHEMA((...))` entity.
292    fn extract_file_schema_strings(
293        attributes: &[Attribute],
294        span: Span,
295    ) -> Result<Vec<String>, Error> {
296        // FILE_SCHEMA has exactly one attribute: a list of strings.
297        let list = attributes
298            .first()
299            .ok_or(Error::MalformedFileSchema { span })?;
300        match list {
301            Attribute::List(items) => {
302                let mut result = Vec::with_capacity(items.len());
303                for item in items {
304                    match item {
305                        Attribute::String(s) => result.push(s.clone()),
306                        _ => return Err(Error::MalformedFileSchema { span }),
307                    }
308                }
309                Ok(result)
310            }
311            _ => Err(Error::MalformedFileSchema { span }),
312        }
313    }
314
315    // ------------------------------------------------------------------
316    // DATA section
317    // ------------------------------------------------------------------
318
319    /// Parse the DATA section content (between `DATA;` and `ENDSEC;`).
320    fn parse_data_section(&mut self) -> Result<BTreeMap<u64, RawEntity>, Error> {
321        let mut entities = BTreeMap::new();
322
323        while !matches!(self.peek_kind()?, TokenKind::EndSec) {
324            let entity = self.parse_entity_instance()?;
325            let id = entity.id();
326            let span = entity.span();
327            if entities.insert(id, entity).is_some() {
328                return Err(Error::DuplicateEntityId { id, span });
329            }
330        }
331
332        Ok(entities)
333    }
334
335    // ------------------------------------------------------------------
336    // Entity instance parsing
337    // ------------------------------------------------------------------
338
339    /// Parse one entity instance: `#N = NAME(...);` or `#N = ( NAME(...) ... );`
340    fn parse_entity_instance(&mut self) -> Result<RawEntity, Error> {
341        // #N
342        let id_tok = self.expect(
343            |k| matches!(k, TokenKind::EntityRef(_)),
344            "entity reference #N",
345        )?;
346        let TokenKind::EntityRef(id) = id_tok.kind else {
347            unreachable!()
348        };
349        let span = id_tok.span;
350
351        // =
352        self.expect_equals()?;
353
354        // Peek to distinguish Simple vs Complex.
355        let kind = self.peek_kind()?.clone();
356        match kind {
357            TokenKind::Keyword(name) => {
358                self.next_token()?; // consume keyword
359                self.parse_simple_body(id, &name, span)
360            }
361            TokenKind::LParen => {
362                self.next_token()?; // consume `(`
363                self.parse_complex_body(id, span)
364            }
365            _ => {
366                let tok = self.next_token()?;
367                Err(Error::UnexpectedToken {
368                    expected: "entity type name or '('",
369                    found: tok.kind,
370                    span: tok.span,
371                })
372            }
373        }
374    }
375
376    fn parse_simple_body(&mut self, id: u64, name: &str, span: Span) -> Result<RawEntity, Error> {
377        let attributes = self.parse_parameter_list()?;
378        self.expect_semicolon()?;
379        Ok(RawEntity::Simple {
380            id,
381            name: name.to_uppercase(),
382            attributes,
383            span,
384        })
385    }
386
387    fn parse_complex_body(&mut self, id: u64, span: Span) -> Result<RawEntity, Error> {
388        let mut parts = Vec::new();
389
390        // At least one part is required — `( )` is an error.
391        if matches!(self.peek_kind()?, TokenKind::RParen) {
392            let tok = self.next_token()?;
393            return Err(Error::UnexpectedToken {
394                expected: "entity type name",
395                found: tok.kind,
396                span: tok.span,
397            });
398        }
399
400        loop {
401            let kind = self.peek_kind()?.clone();
402            match kind {
403                TokenKind::Keyword(name) => {
404                    self.next_token()?; // consume keyword
405                    let attributes = self.parse_parameter_list()?;
406                    parts.push(RawEntityPart {
407                        name: name.to_uppercase(),
408                        attributes,
409                    });
410                }
411                TokenKind::RParen => {
412                    self.next_token()?; // consume `)`
413                    break;
414                }
415                _ => {
416                    let tok = self.next_token()?;
417                    return Err(Error::UnexpectedToken {
418                        expected: "entity type name or ')'",
419                        found: tok.kind,
420                        span: tok.span,
421                    });
422                }
423            }
424        }
425
426        self.expect_semicolon()?;
427        Ok(RawEntity::Complex { id, parts, span })
428    }
429
430    // ------------------------------------------------------------------
431    // Attribute parsing
432    // ------------------------------------------------------------------
433
434    /// Parse a comma-separated parameter list between parentheses.
435    /// Expects the opening `(` to have been consumed already? No — this
436    /// method consumes `(`, reads parameters, and consumes `)`.
437    fn parse_parameter_list(&mut self) -> Result<Vec<Attribute>, Error> {
438        self.expect_lparen()?;
439        let attrs = self.parse_list_value()?;
440        self.expect_rparen()?;
441        Ok(attrs)
442    }
443
444    /// Parse comma-separated parameters until a `)` is encountered.
445    /// The `)` is **not** consumed — the caller is responsible.
446    fn parse_list_value(&mut self) -> Result<Vec<Attribute>, Error> {
447        let mut items = Vec::new();
448        // Empty list `()`.
449        if matches!(self.peek_kind()?, TokenKind::RParen) {
450            return Ok(items);
451        }
452        items.push(self.parse_parameter_or_unset_if_empty()?);
453        while matches!(self.peek_kind()?, TokenKind::Comma) {
454            // Consume the comma.
455            self.next_token()?;
456            items.push(self.parse_parameter_or_unset_if_empty()?);
457        }
458        Ok(items)
459    }
460
461    /// Like [`Self::parse_parameter`] but tolerates a blank attribute
462    /// position — `(a, , b)` or trailing `(a, )`. Spec requires `$` for
463    /// omitted slots, but some writers leave them empty. The slot is
464    /// normalised to [`Attribute::Unset`] and a [`ParseWarning`] is
465    /// recorded so the lenient repair surfaces to the caller.
466    fn parse_parameter_or_unset_if_empty(&mut self) -> Result<Attribute, Error> {
467        if matches!(self.peek_kind()?, TokenKind::Comma | TokenKind::RParen) {
468            let span = self.peek_span().unwrap_or(Span {
469                start: 0,
470                end: 0,
471                line: 0,
472                column: 0,
473            });
474            self.warnings.push(ParseWarning::EmptyAttribute { span });
475            return Ok(Attribute::Unset);
476        }
477        self.parse_parameter()
478    }
479
480    /// Peek at the next token's span without consuming it. Returns
481    /// `None` on EOF or a lex error (the warning path then falls back
482    /// to a zero span — positional precision is non-critical).
483    fn peek_span(&mut self) -> Option<Span> {
484        match self.lexer.peek() {
485            Some(Ok(tok)) => Some(tok.span),
486            _ => None,
487        }
488    }
489
490    /// Parse a single Part 21 parameter value.
491    /// Parse one parameter, guarding nesting depth. The recursive cases (nested
492    /// list, typed value) re-enter through this wrapper, so `depth` tracks the
493    /// current nesting; exceeding [`MAX_NESTING_DEPTH`] is a graceful error rather
494    /// than a stack overflow. Balanced inc/dec → siblings don't accumulate.
495    fn parse_parameter(&mut self) -> Result<Attribute, Error> {
496        self.depth += 1;
497        if self.depth > MAX_NESTING_DEPTH {
498            self.depth -= 1;
499            let span = self.peek_span().unwrap_or(Span {
500                start: 0,
501                end: 0,
502                line: 0,
503                column: 0,
504            });
505            return Err(Error::NestingTooDeep { span });
506        }
507        let r = self.parse_parameter_inner();
508        self.depth -= 1;
509        r
510    }
511
512    fn parse_parameter_inner(&mut self) -> Result<Attribute, Error> {
513        let kind = self.peek_kind()?.clone();
514        match kind {
515            TokenKind::Integer(v) => {
516                self.next_token()?;
517                Ok(Attribute::Integer(v))
518            }
519            TokenKind::Real(v) => {
520                self.next_token()?;
521                Ok(Attribute::Real(v))
522            }
523            TokenKind::String(ref s) => {
524                let s = s.clone();
525                self.next_token()?;
526                Ok(Attribute::String(s))
527            }
528            TokenKind::Enum(ref s) => {
529                let s = s.clone();
530                self.next_token()?;
531                Ok(Attribute::Enum(s))
532            }
533            TokenKind::Binary(ref s) => {
534                let s = s.clone();
535                self.next_token()?;
536                Ok(Attribute::Binary(s))
537            }
538            TokenKind::EntityRef(id) => {
539                self.next_token()?;
540                Ok(Attribute::EntityRef(id))
541            }
542            TokenKind::Dollar => {
543                self.next_token()?;
544                Ok(Attribute::Unset)
545            }
546            TokenKind::Asterisk => {
547                self.next_token()?;
548                Ok(Attribute::Derived)
549            }
550            TokenKind::LParen => {
551                // Nested list.
552                self.next_token()?; // consume `(`
553                let items = self.parse_list_value()?;
554                self.expect_rparen()?;
555                Ok(Attribute::List(items))
556            }
557            TokenKind::Keyword(ref name) => {
558                // Typed parameter: `NAME(value)`.
559                let type_name = name.to_uppercase();
560                self.next_token()?; // consume keyword
561                self.expect_lparen()?;
562                let value = self.parse_parameter()?;
563                self.expect_rparen()?;
564                Ok(Attribute::Typed {
565                    type_name,
566                    value: Box::new(value),
567                })
568            }
569            _ => {
570                let tok = self.next_token()?;
571                Err(Error::InvalidAttributePosition {
572                    span: tok.span,
573                    detail: "unexpected token in attribute position",
574                })
575            }
576        }
577    }
578
579    // ------------------------------------------------------------------
580    // Token-level helpers
581    // ------------------------------------------------------------------
582
583    /// Consume the next token, returning [`Error::UnexpectedEof`] if the
584    /// stream is exhausted.
585    fn next_token(&mut self) -> Result<Token, Error> {
586        match self.lexer.next() {
587            Some(result) => Ok(result?),
588            None => Err(Error::UnexpectedEof {
589                expected: "any token",
590            }),
591        }
592    }
593
594    /// Peek at the next token's kind without consuming it.
595    fn peek_kind(&mut self) -> Result<&TokenKind, Error> {
596        match self.lexer.peek() {
597            Some(Ok(tok)) => Ok(&tok.kind),
598            Some(Err(err)) => Err(Error::Lex(err.clone())),
599            None => Err(Error::UnexpectedEof {
600                expected: "any token",
601            }),
602        }
603    }
604
605    /// Consume the next token if it matches `pred`; otherwise return an error.
606    fn expect<F>(&mut self, pred: F, expected: &'static str) -> Result<Token, Error>
607    where
608        F: Fn(&TokenKind) -> bool,
609    {
610        let tok = self.next_token()?;
611        if pred(&tok.kind) {
612            Ok(tok)
613        } else {
614            Err(Error::UnexpectedToken {
615                expected,
616                found: tok.kind,
617                span: tok.span,
618            })
619        }
620    }
621
622    /// Consume the next token and verify it matches a specific [`TokenKind`]
623    /// variant (by discriminant, ignoring inner data).
624    fn expect_token_kind(
625        &mut self,
626        kind: &TokenKind,
627        expected: &'static str,
628    ) -> Result<Token, Error> {
629        self.expect(
630            |k| std::mem::discriminant(k) == std::mem::discriminant(kind),
631            expected,
632        )
633    }
634
635    /// Expect and consume a semicolon.
636    fn expect_semicolon(&mut self) -> Result<Span, Error> {
637        Ok(self
638            .expect(|k| matches!(k, TokenKind::Semicolon), ";")?
639            .span)
640    }
641
642    /// Expect and consume a left parenthesis.
643    fn expect_lparen(&mut self) -> Result<Span, Error> {
644        Ok(self.expect(|k| matches!(k, TokenKind::LParen), "(")?.span)
645    }
646
647    /// Expect and consume a right parenthesis.
648    fn expect_rparen(&mut self) -> Result<Span, Error> {
649        Ok(self.expect(|k| matches!(k, TokenKind::RParen), ")")?.span)
650    }
651
652    /// Expect and consume an equals sign.
653    fn expect_equals(&mut self) -> Result<Span, Error> {
654        Ok(self.expect(|k| matches!(k, TokenKind::Equals), "=")?.span)
655    }
656}
657
658#[cfg(test)]
659mod tests {
660    use super::*;
661
662    #[test]
663    fn parse_empty_input_errors() {
664        let err = parse("").unwrap_err();
665        assert!(matches!(err, Error::UnexpectedEof { .. }));
666    }
667
668    #[test]
669    fn parser_next_token_returns_eof_on_empty() {
670        let mut parser = Parser::new("");
671        assert!(matches!(
672            parser.next_token(),
673            Err(Error::UnexpectedEof { .. })
674        ));
675    }
676
677    #[test]
678    fn parser_peek_kind_returns_eof_on_empty() {
679        let mut parser = Parser::new("");
680        assert!(matches!(
681            parser.peek_kind(),
682            Err(Error::UnexpectedEof { .. })
683        ));
684    }
685
686    #[test]
687    fn parser_expect_semicolon_ok() {
688        let mut parser = Parser::new(";");
689        assert!(parser.expect_semicolon().is_ok());
690    }
691
692    #[test]
693    fn parser_expect_semicolon_wrong_token() {
694        let mut parser = Parser::new("(");
695        let err = parser.expect_semicolon().unwrap_err();
696        assert!(matches!(err, Error::UnexpectedToken { expected: ";", .. }));
697    }
698
699    // --- parse_parameter helpers ---
700
701    /// Parse a single parameter by feeding it directly to `parse_parameter`.
702    fn parse_single_attr(param_text: &str) -> Attribute {
703        let mut parser = Parser::new(param_text);
704        parser
705            .parse_parameter()
706            .unwrap_or_else(|e| panic!("parse_parameter failed on {param_text:?}: {e}"))
707    }
708
709    // --- Scalar tests ---
710
711    #[test]
712    fn parse_param_integer() {
713        assert_eq!(parse_single_attr("42"), Attribute::Integer(42));
714    }
715
716    #[test]
717    fn parse_param_real() {
718        assert_eq!(parse_single_attr("1.5"), Attribute::Real(1.5));
719    }
720
721    #[test]
722    fn parse_param_real_exponent() {
723        assert_eq!(parse_single_attr("1.E-07"), Attribute::Real(1e-7));
724    }
725
726    #[test]
727    fn parse_param_string() {
728        assert_eq!(
729            parse_single_attr("'hello'"),
730            Attribute::String("hello".into())
731        );
732    }
733
734    #[test]
735    fn parse_param_enum() {
736        assert_eq!(
737            parse_single_attr(".MILLI."),
738            Attribute::Enum("MILLI".into())
739        );
740    }
741
742    #[test]
743    fn parse_param_binary() {
744        assert_eq!(
745            parse_single_attr("\"3FFA\""),
746            Attribute::Binary("3FFA".into())
747        );
748    }
749
750    #[test]
751    fn parse_param_entity_ref() {
752        assert_eq!(parse_single_attr("#42"), Attribute::EntityRef(42));
753    }
754
755    #[test]
756    fn parse_param_unset() {
757        assert_eq!(parse_single_attr("$"), Attribute::Unset);
758    }
759
760    #[test]
761    fn parse_param_derived() {
762        assert_eq!(parse_single_attr("*"), Attribute::Derived);
763    }
764
765    // --- List tests ---
766
767    #[test]
768    fn parse_param_empty_list() {
769        assert_eq!(parse_single_attr("()"), Attribute::List(vec![]));
770    }
771
772    #[test]
773    fn parse_param_flat_list() {
774        assert_eq!(
775            parse_single_attr("(1,2,3)"),
776            Attribute::List(vec![
777                Attribute::Integer(1),
778                Attribute::Integer(2),
779                Attribute::Integer(3),
780            ])
781        );
782    }
783
784    #[test]
785    fn parse_param_nested_list() {
786        assert_eq!(
787            parse_single_attr("((#1),(#2,#3))"),
788            Attribute::List(vec![
789                Attribute::List(vec![Attribute::EntityRef(1)]),
790                Attribute::List(vec![Attribute::EntityRef(2), Attribute::EntityRef(3)]),
791            ])
792        );
793    }
794
795    #[test]
796    fn parse_param_enum_list() {
797        assert_eq!(
798            parse_single_attr("(.T.,.F.)"),
799            Attribute::List(vec![
800                Attribute::Enum("T".into()),
801                Attribute::Enum("F".into()),
802            ])
803        );
804    }
805
806    // --- Typed literal tests ---
807
808    #[test]
809    fn parse_param_typed_real() {
810        assert_eq!(
811            parse_single_attr("LENGTH_MEASURE(1.E-07)"),
812            Attribute::Typed {
813                type_name: "LENGTH_MEASURE".into(),
814                value: Box::new(Attribute::Real(1e-7)),
815            }
816        );
817    }
818
819    #[test]
820    fn parse_param_typed_positive_length() {
821        assert_eq!(
822            parse_single_attr("POSITIVE_LENGTH_MEASURE(0.1)"),
823            Attribute::Typed {
824                type_name: "POSITIVE_LENGTH_MEASURE".into(),
825                value: Box::new(Attribute::Real(0.1)),
826            }
827        );
828    }
829
830    #[test]
831    fn parse_param_typed_of_list() {
832        assert_eq!(
833            parse_single_attr("BOUNDED_CURVE((#1,#2))"),
834            Attribute::Typed {
835                type_name: "BOUNDED_CURVE".into(),
836                value: Box::new(Attribute::List(vec![
837                    Attribute::EntityRef(1),
838                    Attribute::EntityRef(2),
839                ])),
840            }
841        );
842    }
843
844    #[test]
845    fn parse_param_typed_nested() {
846        // TYPE1(TYPE2(42)) — grammar allows recursive typed parameters.
847        assert_eq!(
848            parse_single_attr("TYPE1(TYPE2(42))"),
849            Attribute::Typed {
850                type_name: "TYPE1".into(),
851                value: Box::new(Attribute::Typed {
852                    type_name: "TYPE2".into(),
853                    value: Box::new(Attribute::Integer(42)),
854                }),
855            }
856        );
857    }
858
859    // --- Entity instance parsing ---
860
861    fn parse_entity(src: &str) -> RawEntity {
862        let mut parser = Parser::new(src);
863        parser
864            .parse_entity_instance()
865            .unwrap_or_else(|e| panic!("parse_entity_instance failed: {e}"))
866    }
867
868    #[test]
869    fn parse_simple_entity() {
870        let e = parse_entity("#1 = LINE('', #2, #3);");
871        match e {
872            RawEntity::Simple {
873                id,
874                name,
875                attributes,
876                ..
877            } => {
878                assert_eq!(id, 1);
879                assert_eq!(name, "LINE");
880                assert_eq!(attributes.len(), 3);
881                assert_eq!(attributes[0], Attribute::String(String::new()));
882                assert_eq!(attributes[1], Attribute::EntityRef(2));
883                assert_eq!(attributes[2], Attribute::EntityRef(3));
884            }
885            RawEntity::Complex { .. } => panic!("expected Simple"),
886        }
887    }
888
889    #[test]
890    fn parse_simple_entity_with_derived_and_unset() {
891        let e = parse_entity("#20 = ORIENTED_EDGE('',*,*,#21,.T.);");
892        match e {
893            RawEntity::Simple { id, attributes, .. } => {
894                assert_eq!(id, 20);
895                assert_eq!(attributes[0], Attribute::String(String::new()));
896                assert_eq!(attributes[1], Attribute::Derived);
897                assert_eq!(attributes[2], Attribute::Derived);
898                assert_eq!(attributes[3], Attribute::EntityRef(21));
899                assert_eq!(attributes[4], Attribute::Enum("T".into()));
900            }
901            RawEntity::Complex { .. } => panic!("expected Simple"),
902        }
903    }
904
905    #[test]
906    fn parse_simple_entity_name_is_uppercased() {
907        let e = parse_entity("#1 = cartesian_point('', (0., 0., 0.));");
908        match e {
909            RawEntity::Simple { name, .. } => {
910                assert_eq!(name, "CARTESIAN_POINT");
911            }
912            RawEntity::Complex { .. } => panic!("expected Simple"),
913        }
914    }
915
916    #[test]
917    fn parse_complex_entity_four_parts() {
918        let src = "#165 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) \
919                   GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#169)) \
920                   GLOBAL_UNIT_ASSIGNED_CONTEXT((#166,#167,#168)) \
921                   REPRESENTATION_CONTEXT('a','b') );";
922        let e = parse_entity(src);
923        match e {
924            RawEntity::Complex { id, parts, .. } => {
925                assert_eq!(id, 165);
926                assert_eq!(parts.len(), 4);
927                assert_eq!(parts[0].name, "GEOMETRIC_REPRESENTATION_CONTEXT");
928                assert_eq!(parts[1].name, "GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT");
929                assert_eq!(parts[2].name, "GLOBAL_UNIT_ASSIGNED_CONTEXT");
930                assert_eq!(parts[3].name, "REPRESENTATION_CONTEXT");
931                assert_eq!(parts[0].attributes, vec![Attribute::Integer(3)]);
932                assert_eq!(parts[3].attributes.len(), 2);
933            }
934            RawEntity::Simple { .. } => panic!("expected Complex"),
935        }
936    }
937
938    #[test]
939    fn parse_complex_entity_three_parts_mixed_attrs() {
940        let src = "#166 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );";
941        let e = parse_entity(src);
942        match e {
943            RawEntity::Complex { id, parts, .. } => {
944                assert_eq!(id, 166);
945                assert_eq!(parts.len(), 3);
946                assert_eq!(parts[0].name, "LENGTH_UNIT");
947                assert_eq!(parts[0].attributes, vec![]);
948                assert_eq!(parts[1].name, "NAMED_UNIT");
949                assert_eq!(parts[1].attributes, vec![Attribute::Derived]);
950                assert_eq!(parts[2].name, "SI_UNIT");
951                assert_eq!(parts[2].attributes.len(), 2);
952            }
953            RawEntity::Simple { .. } => panic!("expected Complex"),
954        }
955    }
956
957    #[test]
958    fn parse_complex_entity_name_uppercased() {
959        let src = "#1 = ( length_unit() named_unit(*) );";
960        let e = parse_entity(src);
961        match e {
962            RawEntity::Complex { parts, .. } => {
963                assert_eq!(parts[0].name, "LENGTH_UNIT");
964                assert_eq!(parts[1].name, "NAMED_UNIT");
965            }
966            RawEntity::Simple { .. } => panic!("expected Complex"),
967        }
968    }
969
970    #[test]
971    fn parse_empty_complex_entity_errors() {
972        let mut parser = Parser::new("#1 = ( );");
973        let err = parser.parse_entity_instance().unwrap_err();
974        assert!(matches!(
975            err,
976            Error::UnexpectedToken {
977                expected: "entity type name",
978                ..
979            }
980        ));
981    }
982
983    #[test]
984    fn parse_entity_missing_semicolon_errors() {
985        let mut parser = Parser::new("#1 = LINE('', #2)");
986        let err = parser.parse_entity_instance().unwrap_err();
987        assert!(matches!(err, Error::UnexpectedEof { .. }));
988    }
989
990    // --- parse_bytes: non-UTF-8 input ---
991
992    fn minimal_step_with_string_attr(s_bytes: &[u8]) -> Vec<u8> {
993        let prefix = b"ISO-10303-21;\n\
994                      HEADER;\n\
995                      FILE_DESCRIPTION((' ',";
996        let mid = b"),'2;1');\n\
997                    FILE_NAME('n','t',(' '),(' '),'p','o','a');\n\
998                    FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));\n\
999                    ENDSEC;\n\
1000                    DATA;\n\
1001                    ENDSEC;\n\
1002                    END-ISO-10303-21;\n";
1003        let mut buf = Vec::with_capacity(prefix.len() + s_bytes.len() + mid.len() + 2);
1004        buf.extend_from_slice(prefix);
1005        buf.push(b'\'');
1006        buf.extend_from_slice(s_bytes);
1007        buf.push(b'\'');
1008        buf.extend_from_slice(mid);
1009        buf
1010    }
1011
1012    #[test]
1013    fn parse_bytes_accepts_latin1_byte_via_fallback() {
1014        // 0xC0 alone is not valid UTF-8 (it is a leading byte for a 2-byte
1015        // sequence). Latin-1 maps it to U+00C0 ('À').
1016        let buf = minimal_step_with_string_attr(&[0xC0]);
1017        assert!(
1018            std::str::from_utf8(&buf).is_err(),
1019            "fixture must be invalid UTF-8 to exercise the fallback"
1020        );
1021        let graph = parse_bytes(&buf).expect("parse_bytes must accept Latin-1 fallback");
1022        // FILE_DESCRIPTION is the first header entity; its first attribute
1023        // is a list of strings. The second string of that list carries our
1024        // injected byte.
1025        let RawEntity::Simple {
1026            name, attributes, ..
1027        } = &graph.header[0]
1028        else {
1029            panic!("expected Simple HEADER entity");
1030        };
1031        assert_eq!(name, "FILE_DESCRIPTION");
1032        let Attribute::List(items) = &attributes[0] else {
1033            panic!("FILE_DESCRIPTION attr[0] must be a list");
1034        };
1035        let Attribute::String(s) = &items[1] else {
1036            panic!("expected String attribute");
1037        };
1038        assert_eq!(s.chars().count(), 1);
1039        assert_eq!(s.chars().next().unwrap(), '\u{00C0}');
1040    }
1041
1042    #[test]
1043    fn parse_bytes_passes_through_valid_utf8() {
1044        // Valid UTF-8 'À' (0xC3 0x80) — the UTF-8 path is taken and the
1045        // character decodes identically.
1046        let buf = minimal_step_with_string_attr("À".as_bytes());
1047        assert!(std::str::from_utf8(&buf).is_ok());
1048        let graph = parse_bytes(&buf).expect("valid UTF-8 must parse");
1049        let RawEntity::Simple { attributes, .. } = &graph.header[0] else {
1050            panic!();
1051        };
1052        let Attribute::List(items) = &attributes[0] else {
1053            panic!();
1054        };
1055        let Attribute::String(s) = &items[1] else {
1056            panic!();
1057        };
1058        assert_eq!(s, "\u{00C0}");
1059    }
1060
1061    // --- ParseWarning: lenient acceptance of non-spec / ed.3 inputs ---
1062
1063    #[test]
1064    fn parse_records_ed3_anchor_and_reference_sections() {
1065        let src = "ISO-10303-21;\n\
1066                   HEADER;\n\
1067                   FILE_DESCRIPTION((''),'2;1');\n\
1068                   FILE_NAME('n','t',(''),(''),'p','o','a');\n\
1069                   FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));\n\
1070                   ENDSEC;\n\
1071                   ANCHOR;\n\
1072                   <ParentAnchor>=#123;\n\
1073                   ENDSEC;\n\
1074                   REFERENCE;\n\
1075                   #123=<testAnchorAndData.stp#TestAnchor>;\n\
1076                   ENDSEC;\n\
1077                   DATA;\n\
1078                   #1=CIRCULAR_AREA('testarea',#123,2.);\n\
1079                   ENDSEC;\n\
1080                   END-ISO-10303-21;\n";
1081        let graph = parse(src).expect("ed.3 ANCHOR/REFERENCE must be tolerated");
1082        assert_eq!(graph.entities.len(), 1);
1083        // REFERENCE recorded: #123 -> external anchor string.
1084        assert_eq!(
1085            graph.external_references.get(&123).map(String::as_str),
1086            Some("<testAnchorAndData.stp#TestAnchor>")
1087        );
1088        // ANCHOR recorded: <ParentAnchor> -> #123.
1089        assert_eq!(graph.anchors, vec![("<ParentAnchor>".to_string(), 123)]);
1090        // Both sections parsed cleanly — no discard warning.
1091        assert!(
1092            !graph
1093                .warnings
1094                .iter()
1095                .any(|w| matches!(w, ParseWarning::Ed3SectionDiscarded { .. }))
1096        );
1097    }
1098
1099    #[test]
1100    fn parse_discards_unrecognised_ed3_signature_section() {
1101        let src = "ISO-10303-21;\n\
1102                   HEADER;\n\
1103                   FILE_DESCRIPTION((''),'2;1');\n\
1104                   FILE_NAME('n','t',(''),(''),'p','o','a');\n\
1105                   FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));\n\
1106                   ENDSEC;\n\
1107                   SIGNATURE;\n\
1108                   some opaque signature payload;\n\
1109                   ENDSEC;\n\
1110                   DATA;\n\
1111                   #1 = CARTESIAN_POINT('',(0.,0.,0.));\n\
1112                   ENDSEC;\n\
1113                   END-ISO-10303-21;\n";
1114        let graph = parse(src).expect("ed.3 SIGNATURE must be tolerated");
1115        assert_eq!(graph.entities.len(), 1);
1116        assert!(
1117            graph
1118                .warnings
1119                .iter()
1120                .any(|w| matches!(w, ParseWarning::Ed3SectionDiscarded { section, .. } if section == "SIGNATURE"))
1121        );
1122    }
1123
1124    #[test]
1125    fn parse_warns_on_missing_header_semicolon() {
1126        // FILE_DESCRIPTION lacks its trailing `;` — non-spec but common.
1127        let src = "ISO-10303-21;\n\
1128                   HEADER;\n\
1129                   FILE_DESCRIPTION((''),'2;1')\n\
1130                   FILE_NAME('n','t',(''),(''),'p','o','a');\n\
1131                   FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));\n\
1132                   ENDSEC;\n\
1133                   DATA;\n\
1134                   ENDSEC;\n\
1135                   END-ISO-10303-21;\n";
1136        let graph = parse(src).expect("missing `;` must be tolerated");
1137        assert!(graph.warnings.iter().any(|w| matches!(
1138            w,
1139            ParseWarning::MissingHeaderSemicolon { entity_name, .. }
1140                if entity_name == "FILE_DESCRIPTION"
1141        )));
1142    }
1143
1144    #[test]
1145    fn parse_normalises_empty_attribute_to_unset_with_warning() {
1146        // Trailing blank attribute: `#0,   )` — spec wants `$`.
1147        let src = "ISO-10303-21;\n\
1148                   HEADER;\n\
1149                   FILE_DESCRIPTION((''),'2;1');\n\
1150                   FILE_NAME('n','t',(''),(''),'p','o','a');\n\
1151                   FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));\n\
1152                   ENDSEC;\n\
1153                   DATA;\n\
1154                   #1 = EDGE_CURVE('', #2, #3, #0,   );\n\
1155                   #2 = VERTEX_POINT('', #4);\n\
1156                   #3 = VERTEX_POINT('', #4);\n\
1157                   #4 = CARTESIAN_POINT('',(0.,0.,0.));\n\
1158                   ENDSEC;\n\
1159                   END-ISO-10303-21;\n";
1160        let graph = parse(src).expect("empty attribute slot must be tolerated");
1161        // The fifth attribute of #1 is the omitted slot, normalised to Unset.
1162        let RawEntity::Simple { attributes, .. } = &graph.entities[&1] else {
1163            panic!("expected Simple entity");
1164        };
1165        assert_eq!(attributes.len(), 5);
1166        assert!(matches!(attributes[4], Attribute::Unset));
1167        assert!(
1168            graph
1169                .warnings
1170                .iter()
1171                .any(|w| matches!(w, ParseWarning::EmptyAttribute { .. }))
1172        );
1173    }
1174}