Skip to main content

openbim_ids/
read.rs

1//! Reading IDS 1.0 documents.
2//!
3//! [`from_str`] and [`from_slice`] turn a document into the typed [`Ids`]
4//! model or refuse it with a [`ReadError`] that names the line and column.
5//!
6//! # Two passes
7//!
8//! The first pass is lenient. It walks every element and records the
9//! [`Signal`]s that tell IDS revisions apart, reads the revision the document
10//! declares in `xsi:schemaLocation`, and asks [`detect_version`] for a
11//! verdict. A pre-1.0 draft, or a document whose declaration contradicts its
12//! shape, is refused here with [`ErrorKind::UnsupportedVersion`] carrying the
13//! evidence, instead of failing later on the first draft-only attribute with
14//! an error that hides the real cause.
15//!
16//! The second pass is strict and follows `ids.xsd` 1.0.0: element order,
17//! required elements and attributes, enumerated tokens, and the lexical forms
18//! of numbers and dates. Anything the schema would reject is refused.
19//!
20//! # A document that says nothing about its revision
21//!
22//! A file with no `xsi:schemaLocation` naming a revision, and none of the
23//! shapes that distinguish revisions, is read as 1.0 only when the strict
24//! pass accepts the whole document, and is reported as
25//! [`Detected::Inferred`] so a caller that insists on a declaration can
26//! refuse it.
27//!
28//! # What is not checked
29//!
30//! The reader checks the document against the IDS schema, not against IFC.
31//! An entity name that no IFC release defines, a `dataType` that is not an
32//! IFC defined type, or a value that cannot be cast to one is a well-formed
33//! IDS document and is returned as such; judging it needs the IFC schema,
34//! which is the auditor's job. Likewise the facet patterns of a restriction
35//! are returned as written, not compiled.
36
37use core::fmt;
38
39use openbim_core::Detected;
40use roxmltree::{Document, Node, ParsingOptions};
41
42use crate::model::{
43    Applicability, Attribute, Classification, Entity, Facet, IfcVersion, Info, Material, PartOf,
44    Property, Relation, Requirement, Requirements, Restriction, Specification, Value,
45};
46use crate::{detect_version, Ids, IdsVersion, Occurrence, Signal, VersionSignals, NAMESPACE};
47
48/// The XML Schema namespace, in which `<xs:restriction>` and its facets live.
49pub const XSD_NAMESPACE: &str = "http://www.w3.org/2001/XMLSchema";
50const XSI_NAMESPACE: &str = "http://www.w3.org/2001/XMLSchema-instance";
51const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
52
53/// Why a document was refused, and where.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct ReadError {
56    kind: ErrorKind,
57    line: u32,
58    column: u32,
59}
60
61impl ReadError {
62    /// What was wrong.
63    #[must_use]
64    pub fn kind(&self) -> &ErrorKind {
65        &self.kind
66    }
67
68    /// The 1-based line of the offending node.
69    #[must_use]
70    pub fn line(&self) -> u32 {
71        self.line
72    }
73
74    /// The 1-based column of the offending node.
75    #[must_use]
76    pub fn column(&self) -> u32 {
77        self.column
78    }
79}
80
81impl fmt::Display for ReadError {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        write!(f, "{}:{}: {}", self.line, self.column, self.kind)
84    }
85}
86
87impl std::error::Error for ReadError {}
88
89/// The kinds of [`ReadError`].
90#[derive(Debug, Clone, PartialEq, Eq)]
91#[non_exhaustive]
92pub enum ErrorKind {
93    /// The bytes are not UTF-8.
94    NotUtf8,
95    /// The text is not well-formed XML, or uses a DTD.
96    Xml(String),
97    /// The root element is not `<ids>` in the IDS namespace.
98    NotIds {
99        /// The root element as `{namespace}name`.
100        found: String,
101    },
102    /// The document is not IDS 1.0, or its declaration contradicts its shape.
103    UnsupportedVersion(Detected<IdsVersion>),
104    /// An element that the schema does not allow here, or not in this order.
105    UnexpectedElement {
106        /// The parent element.
107        parent: String,
108        /// The unexpected child as `{namespace}name`.
109        found: String,
110    },
111    /// A required child element is missing.
112    MissingElement {
113        /// The parent element.
114        parent: String,
115        /// The missing child.
116        expected: &'static str,
117    },
118    /// An attribute that the schema does not allow on this element.
119    UnexpectedAttribute {
120        /// The element.
121        element: String,
122        /// The attribute as `{namespace}name`, or bare when unqualified.
123        attribute: String,
124    },
125    /// A required attribute is missing.
126    MissingAttribute {
127        /// The element.
128        element: String,
129        /// The missing attribute.
130        attribute: &'static str,
131    },
132    /// An element or attribute value outside its lexical space.
133    InvalidValue {
134        /// The element or `element/@attribute` holding the value.
135        location: String,
136        /// The value as written.
137        found: String,
138        /// What the schema allows.
139        expected: &'static str,
140    },
141    /// Character data inside an element that may only contain elements.
142    UnexpectedText {
143        /// The element.
144        element: String,
145    },
146    /// A valid XML Schema construct this reader does not represent.
147    Unsupported {
148        /// The element.
149        element: String,
150        /// What is not supported.
151        construct: String,
152    },
153}
154
155impl fmt::Display for ErrorKind {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        match self {
158            ErrorKind::NotUtf8 => f.write_str("the document is not UTF-8"),
159            ErrorKind::Xml(message) => write!(f, "not well-formed XML: {message}"),
160            ErrorKind::NotIds { found } => {
161                write!(f, "the root element is {found}, not {{{NAMESPACE}}}ids")
162            }
163            ErrorKind::UnsupportedVersion(detected) => match detected {
164                Detected::Conflict { declared, observed } => write!(
165                    f,
166                    "the document declares IDS {declared} but its shape is IDS {observed}"
167                ),
168                Detected::Declared(version) => {
169                    write!(f, "the document declares IDS {version}; only 1.0 is read")
170                }
171                Detected::Inferred(version) => write!(
172                    f,
173                    "the document's shape is IDS {version} (pre-1.0); only 1.0 is read"
174                ),
175            },
176            ErrorKind::UnexpectedElement { parent, found } => {
177                write!(f, "<{parent}> does not allow {found} here")
178            }
179            ErrorKind::MissingElement { parent, expected } => {
180                write!(f, "<{parent}> is missing <{expected}>")
181            }
182            ErrorKind::UnexpectedAttribute { element, attribute } => {
183                write!(f, "<{element}> does not allow the attribute {attribute}")
184            }
185            ErrorKind::MissingAttribute { element, attribute } => {
186                write!(f, "<{element}> is missing the attribute {attribute}")
187            }
188            ErrorKind::InvalidValue {
189                location,
190                found,
191                expected,
192            } => write!(f, "{location} is {found:?}; expected {expected}"),
193            ErrorKind::UnexpectedText { element } => {
194                write!(f, "<{element}> may only contain elements, not text")
195            }
196            ErrorKind::Unsupported { element, construct } => {
197                write!(f, "<{element}> uses {construct}, which is not supported")
198            }
199        }
200    }
201}
202
203/// Reads an IDS 1.0 document from bytes.
204///
205/// The bytes must be UTF-8; a leading byte-order mark is skipped.
206///
207/// # Errors
208///
209/// Returns [`ErrorKind::NotUtf8`] for other encodings, and otherwise the
210/// errors of [`from_str`].
211pub fn from_slice(bytes: &[u8]) -> Result<Ids, ReadError> {
212    let bytes = bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(bytes);
213    let text = core::str::from_utf8(bytes).map_err(|_| ReadError {
214        kind: ErrorKind::NotUtf8,
215        line: 1,
216        column: 1,
217    })?;
218    from_str(text)
219}
220
221/// Reads an IDS 1.0 document.
222///
223/// ```
224/// let ids = openbim_ids::read::from_str(r#"
225///   <ids xmlns="http://standards.buildingsmart.org/IDS">
226///     <info><title>Walls</title></info>
227///     <specifications>
228///       <specification name="Walls are named" ifcVersion="IFC4">
229///         <applicability><entity><name><simpleValue>IFCWALL</simpleValue></name></entity></applicability>
230///         <requirements><attribute><name><simpleValue>Name</simpleValue></name></attribute></requirements>
231///       </specification>
232///     </specifications>
233///   </ids>"#).unwrap();
234/// assert_eq!(ids.specifications[0].name, "Walls are named");
235/// ```
236///
237/// # Errors
238///
239/// Returns a [`ReadError`] when the text is not well-formed XML, is not IDS
240/// 1.0, or does not conform to the IDS 1.0 schema.
241pub fn from_str(text: &str) -> Result<Ids, ReadError> {
242    let options = ParsingOptions {
243        allow_dtd: false,
244        ..ParsingOptions::default()
245    };
246    let document = Document::parse_with_options(text, options).map_err(|error| {
247        let position = error.pos();
248        ReadError {
249            kind: ErrorKind::Xml(error.to_string()),
250            line: position.row,
251            column: position.col,
252        }
253    })?;
254    Reader {
255        document: &document,
256    }
257    .ids()
258}
259
260/// Which facets and which attributes an element may carry.
261#[derive(Clone, Copy, PartialEq, Eq)]
262enum Role {
263    Applicability,
264    Requirement,
265}
266
267struct Reader<'d, 'input> {
268    document: &'d Document<'input>,
269}
270
271type Result<T, E = ReadError> = core::result::Result<T, E>;
272
273impl<'d, 'input> Reader<'d, 'input> {
274    fn error(&self, node: Node<'_, '_>, kind: ErrorKind) -> ReadError {
275        let position = self.document.text_pos_at(node.range().start);
276        ReadError {
277            kind,
278            line: position.row,
279            column: position.col,
280        }
281    }
282
283    fn ids(&self) -> Result<Ids> {
284        let root = self.document.root_element();
285        if !is_ids(root, "ids") {
286            return Err(self.error(
287                root,
288                ErrorKind::NotIds {
289                    found: expanded(root),
290                },
291            ));
292        }
293        let version = match detect_version(declared_version(root), &signals(root)) {
294            Some(
295                detected @ (Detected::Declared(IdsVersion::Ids1_0)
296                | Detected::Inferred(IdsVersion::Ids1_0)),
297            ) => detected,
298            None => Detected::Inferred(IdsVersion::Ids1_0),
299            Some(other) => return Err(self.error(root, ErrorKind::UnsupportedVersion(other))),
300        };
301        self.attributes(root, &[])?;
302        let mut children = self.sequence(root)?;
303        let info = self.info(children.require("info")?)?;
304        let specifications = self.specifications(children.require("specifications")?)?;
305        children.finish()?;
306        Ok(Ids {
307            version,
308            info,
309            specifications,
310        })
311    }
312
313    fn info(&self, node: Node<'d, 'input>) -> Result<Info> {
314        self.attributes(node, &[])?;
315        let mut children = self.sequence(node)?;
316        let title = self.text(children.require("title")?)?;
317        let copyright = children
318            .optional("copyright")
319            .map(|n| self.text(n))
320            .transpose()?;
321        let version = children
322            .optional("version")
323            .map(|n| self.text(n))
324            .transpose()?;
325        let description = children
326            .optional("description")
327            .map(|n| self.text(n))
328            .transpose()?;
329        let author = children
330            .optional("author")
331            .map(|n| self.author(n))
332            .transpose()?;
333        let date = children
334            .optional("date")
335            .map(|n| self.date(n))
336            .transpose()?;
337        let purpose = children
338            .optional("purpose")
339            .map(|n| self.text(n))
340            .transpose()?;
341        let milestone = children
342            .optional("milestone")
343            .map(|n| self.text(n))
344            .transpose()?;
345        children.finish()?;
346        Ok(Info {
347            title,
348            copyright,
349            version,
350            description,
351            author,
352            date,
353            purpose,
354            milestone,
355        })
356    }
357
358    fn author(&self, node: Node<'d, 'input>) -> Result<String> {
359        let author = self.text(node)?;
360        if is_author(&author) {
361            Ok(author)
362        } else {
363            Err(self.error(
364                node,
365                ErrorKind::InvalidValue {
366                    location: "author".into(),
367                    found: author,
368                    expected: "an e-mail address (pattern [^@]+@[^\\.]+\\..+)",
369                },
370            ))
371        }
372    }
373
374    fn date(&self, node: Node<'d, 'input>) -> Result<String> {
375        // `xs:date` collapses whitespace before checking the lexical form.
376        let date = self.text(node)?.trim().to_owned();
377        if is_xs_date(&date) {
378            Ok(date)
379        } else {
380            Err(self.error(
381                node,
382                ErrorKind::InvalidValue {
383                    location: "date".into(),
384                    found: date,
385                    expected: "an xs:date such as 2024-06-01",
386                },
387            ))
388        }
389    }
390
391    fn specifications(&self, node: Node<'d, 'input>) -> Result<Vec<Specification>> {
392        self.attributes(node, &[])?;
393        let mut children = self.sequence(node)?;
394        let mut specifications = vec![self.specification(children.require("specification")?)?];
395        while let Some(child) = children.optional("specification") {
396            specifications.push(self.specification(child)?);
397        }
398        children.finish()?;
399        Ok(specifications)
400    }
401
402    fn specification(&self, node: Node<'d, 'input>) -> Result<Specification> {
403        self.attributes(
404            node,
405            &[
406                "name",
407                "ifcVersion",
408                "identifier",
409                "description",
410                "instructions",
411            ],
412        )?;
413        let name = self.required_attribute(node, "name")?.to_owned();
414        let ifc_versions = self.ifc_versions(node)?;
415        let mut children = self.sequence(node)?;
416        let applicability = self.applicability(children.require("applicability")?)?;
417        let requirements = children
418            .optional("requirements")
419            .map(|n| self.requirements(n))
420            .transpose()?;
421        children.finish()?;
422        Ok(Specification {
423            name,
424            ifc_versions,
425            identifier: attribute(node, "identifier"),
426            description: attribute(node, "description"),
427            instructions: attribute(node, "instructions"),
428            applicability,
429            requirements,
430        })
431    }
432
433    fn ifc_versions(&self, node: Node<'d, 'input>) -> Result<Vec<IfcVersion>> {
434        let list = self.required_attribute(node, "ifcVersion")?;
435        list.split(is_xml_space)
436            .filter(|token| !token.is_empty())
437            .map(|token| {
438                IfcVersion::from_token(token).ok_or_else(|| {
439                    self.error(
440                        node,
441                        ErrorKind::InvalidValue {
442                            location: "specification/@ifcVersion".into(),
443                            found: token.to_owned(),
444                            expected: "IFC2X3, IFC4 or IFC4X3_ADD2",
445                        },
446                    )
447                })
448            })
449            .collect()
450    }
451
452    fn applicability(&self, node: Node<'d, 'input>) -> Result<Applicability> {
453        self.attributes(node, &["minOccurs", "maxOccurs"])?;
454        let min_occurs = match node.attribute("minOccurs") {
455            Some(value) => self.non_negative(node, "applicability/@minOccurs", value)?,
456            None => 1,
457        };
458        let max_occurs = match node.attribute("maxOccurs") {
459            Some(value) if value.trim() == "unbounded" => None,
460            Some(value) => Some(self.non_negative(node, "applicability/@maxOccurs", value)?),
461            None => Some(1),
462        };
463        let mut children = self.sequence(node)?;
464        let mut facets = Vec::new();
465        if let Some(child) = children.optional("entity") {
466            facets.push(self.facet(child, Role::Applicability)?);
467        }
468        for kind in [
469            "partOf",
470            "classification",
471            "attribute",
472            "property",
473            "material",
474        ] {
475            while let Some(child) = children.optional(kind) {
476                facets.push(self.facet(child, Role::Applicability)?);
477            }
478        }
479        children.finish()?;
480        Ok(Applicability {
481            min_occurs,
482            max_occurs,
483            facets,
484        })
485    }
486
487    fn requirements(&self, node: Node<'d, 'input>) -> Result<Requirements> {
488        self.attributes(node, &["description"])?;
489        let facets = self
490            .elements(node)?
491            .into_iter()
492            .map(|child| self.requirement(node, child))
493            .collect::<Result<_>>()?;
494        Ok(Requirements {
495            description: attribute(node, "description"),
496            facets,
497        })
498    }
499
500    fn requirement(&self, parent: Node<'d, 'input>, node: Node<'d, 'input>) -> Result<Requirement> {
501        if !FACETS.iter().any(|kind| is_ids(node, kind)) {
502            return Err(self.error(
503                node,
504                ErrorKind::UnexpectedElement {
505                    parent: local(parent),
506                    found: expanded(node),
507                },
508            ));
509        }
510        let facet = self.facet(node, Role::Requirement)?;
511        let occurrence = match node.attribute("cardinality") {
512            None => Occurrence::DEFAULT,
513            Some(token) => {
514                let occurrence = Occurrence::from_cardinality(token);
515                // partOf takes the simple cardinality, which has no `optional`.
516                let allowed = match occurrence {
517                    Some(Occurrence::Optional) => !matches!(facet, Facet::PartOf(_)),
518                    Some(_) => true,
519                    None => false,
520                };
521                if !allowed {
522                    return Err(self.error(
523                        node,
524                        ErrorKind::InvalidValue {
525                            location: format!("{}/@cardinality", local(node)),
526                            found: token.to_owned(),
527                            expected: if matches!(facet, Facet::PartOf(_)) {
528                                "required or prohibited"
529                            } else {
530                                "required, prohibited or optional"
531                            },
532                        },
533                    ));
534                }
535                occurrence.unwrap_or(Occurrence::DEFAULT)
536            }
537        };
538        Ok(Requirement {
539            facet,
540            occurrence,
541            uri: attribute(node, "uri"),
542            instructions: attribute(node, "instructions"),
543        })
544    }
545
546    fn facet(&self, node: Node<'d, 'input>, role: Role) -> Result<Facet> {
547        let kind = node.tag_name().name();
548        self.attributes(node, facet_attributes(kind, role))?;
549        let mut children = self.sequence(node)?;
550        let facet = match kind {
551            "entity" => Facet::Entity(self.entity_content(&mut children)?),
552            "partOf" => {
553                let entity = children.require("entity")?;
554                self.attributes(entity, &[])?;
555                let mut inner = self.sequence(entity)?;
556                let entity = self.entity_content(&mut inner)?;
557                inner.finish()?;
558                Facet::PartOf(PartOf {
559                    entity,
560                    relation: self.relation(node)?,
561                })
562            }
563            "classification" => Facet::Classification(Classification {
564                value: children
565                    .optional("value")
566                    .map(|n| self.value(n))
567                    .transpose()?,
568                system: self.value(children.require("system")?)?,
569            }),
570            "attribute" => Facet::Attribute(Attribute {
571                name: self.value(children.require("name")?)?,
572                value: children
573                    .optional("value")
574                    .map(|n| self.value(n))
575                    .transpose()?,
576            }),
577            "property" => Facet::Property(Property {
578                property_set: self.value(children.require("propertySet")?)?,
579                base_name: self.value(children.require("baseName")?)?,
580                value: children
581                    .optional("value")
582                    .map(|n| self.value(n))
583                    .transpose()?,
584                data_type: self.data_type(node)?,
585            }),
586            "material" => Facet::Material(Material {
587                value: children
588                    .optional("value")
589                    .map(|n| self.value(n))
590                    .transpose()?,
591            }),
592            _ => unreachable!("callers pass facet elements only"),
593        };
594        children.finish()?;
595        Ok(facet)
596    }
597
598    fn entity_content(&self, children: &mut Sequence<'_, 'd, 'input>) -> Result<Entity> {
599        Ok(Entity {
600            name: self.value(children.require("name")?)?,
601            predefined_type: children
602                .optional("predefinedType")
603                .map(|n| self.value(n))
604                .transpose()?,
605        })
606    }
607
608    fn relation(&self, node: Node<'d, 'input>) -> Result<Option<Relation>> {
609        node.attribute("relation")
610            .map(|token| {
611                Relation::from_token(token).ok_or_else(|| {
612                    self.error(
613                        node,
614                        ErrorKind::InvalidValue {
615                            location: "partOf/@relation".into(),
616                            found: token.to_owned(),
617                            expected: "an IDS relation such as IFCRELAGGREGATES",
618                        },
619                    )
620                })
621            })
622            .transpose()
623    }
624
625    fn data_type(&self, node: Node<'d, 'input>) -> Result<Option<String>> {
626        node.attribute("dataType")
627            .map(|value| {
628                if !value.is_empty() && value.bytes().all(|b| b.is_ascii_uppercase()) {
629                    Ok(value.to_owned())
630                } else {
631                    Err(self.error(
632                        node,
633                        ErrorKind::InvalidValue {
634                            location: "property/@dataType".into(),
635                            found: value.to_owned(),
636                            expected: "an upper-case name matching [A-Z]+",
637                        },
638                    ))
639                }
640            })
641            .transpose()
642    }
643
644    fn value(&self, node: Node<'d, 'input>) -> Result<Value> {
645        self.attributes(node, &[])?;
646        let elements = self.elements(node)?;
647        let [child] = elements[..] else {
648            return Err(match elements.get(1) {
649                Some(extra) => self.error(
650                    *extra,
651                    ErrorKind::UnexpectedElement {
652                        parent: local(node),
653                        found: expanded(*extra),
654                    },
655                ),
656                None => self.error(
657                    node,
658                    ErrorKind::MissingElement {
659                        parent: local(node),
660                        expected: "simpleValue or xs:restriction",
661                    },
662                ),
663            });
664        };
665        if is_ids(child, "simpleValue") {
666            self.attributes(child, &[])?;
667            Ok(Value::Simple(self.text(child)?))
668        } else if is_xsd(child, "restriction") {
669            Ok(Value::Restriction(Box::new(self.restriction(child)?)))
670        } else {
671            Err(self.error(
672                child,
673                ErrorKind::UnexpectedElement {
674                    parent: local(node),
675                    found: expanded(child),
676                },
677            ))
678        }
679    }
680
681    fn restriction(&self, node: Node<'d, 'input>) -> Result<Restriction> {
682        self.attributes(node, &["base", "id"])?;
683        let base = self.base(node)?;
684        let mut restriction = Restriction {
685            base,
686            ..Restriction::default()
687        };
688        for child in self.elements(node)? {
689            if !child.has_tag_name((XSD_NAMESPACE, child.tag_name().name())) {
690                return Err(self.error(
691                    child,
692                    ErrorKind::UnexpectedElement {
693                        parent: "xs:restriction".into(),
694                        found: expanded(child),
695                    },
696                ));
697            }
698            let name = child.tag_name().name();
699            if name == "annotation" {
700                continue;
701            }
702            let Some(facet) = RestrictionFacet::from_name(name) else {
703                let kind = if name == "simpleType" || name == "whiteSpace" || name == "assertion" {
704                    ErrorKind::Unsupported {
705                        element: "xs:restriction".into(),
706                        construct: format!("xs:{name}"),
707                    }
708                } else {
709                    ErrorKind::UnexpectedElement {
710                        parent: "xs:restriction".into(),
711                        found: expanded(child),
712                    }
713                };
714                return Err(self.error(child, kind));
715            };
716            self.attributes(child, &["value", "id", "fixed"])?;
717            for grandchild in self.elements(child)? {
718                if !is_xsd(grandchild, "annotation") {
719                    return Err(self.error(
720                        grandchild,
721                        ErrorKind::UnexpectedElement {
722                            parent: format!("xs:{name}"),
723                            found: expanded(grandchild),
724                        },
725                    ));
726                }
727            }
728            let value = self.required_attribute(child, "value")?;
729            self.apply(&mut restriction, facet, child, value)?;
730        }
731        Ok(restriction)
732    }
733
734    fn apply(
735        &self,
736        restriction: &mut Restriction,
737        facet: RestrictionFacet,
738        node: Node<'d, 'input>,
739        value: &str,
740    ) -> Result<()> {
741        let location = || format!("xs:{}/@value", node.tag_name().name());
742        let once_text = |slot: &mut Option<String>| -> Result<()> {
743            if slot.is_some() {
744                return Err(self.error(node, duplicate(node)));
745            }
746            *slot = Some(value.to_owned());
747            Ok(())
748        };
749        let once_count = |slot: &mut Option<u64>, positive: bool| -> Result<()> {
750            if slot.is_some() {
751                return Err(self.error(node, duplicate(node)));
752            }
753            let count = parse_non_negative(value)
754                .filter(|count| !positive || *count > 0)
755                .ok_or_else(|| {
756                    self.error(
757                        node,
758                        ErrorKind::InvalidValue {
759                            location: location(),
760                            found: value.to_owned(),
761                            expected: if positive {
762                                "a positive integer"
763                            } else {
764                                "a non-negative integer"
765                            },
766                        },
767                    )
768                })?;
769            *slot = Some(count);
770            Ok(())
771        };
772        match facet {
773            RestrictionFacet::Enumeration => restriction.enumeration.push(value.to_owned()),
774            RestrictionFacet::Pattern => restriction.patterns.push(value.to_owned()),
775            RestrictionFacet::MinInclusive => once_text(&mut restriction.min_inclusive)?,
776            RestrictionFacet::MaxInclusive => once_text(&mut restriction.max_inclusive)?,
777            RestrictionFacet::MinExclusive => once_text(&mut restriction.min_exclusive)?,
778            RestrictionFacet::MaxExclusive => once_text(&mut restriction.max_exclusive)?,
779            RestrictionFacet::Length => once_count(&mut restriction.length, false)?,
780            RestrictionFacet::MinLength => once_count(&mut restriction.min_length, false)?,
781            RestrictionFacet::MaxLength => once_count(&mut restriction.max_length, false)?,
782            RestrictionFacet::TotalDigits => once_count(&mut restriction.total_digits, true)?,
783            RestrictionFacet::FractionDigits => {
784                once_count(&mut restriction.fraction_digits, false)?;
785            }
786        }
787        Ok(())
788    }
789
790    /// The local name of `@base`, which must name an XML Schema type.
791    fn base(&self, node: Node<'d, 'input>) -> Result<String> {
792        let base = self.required_attribute(node, "base")?.trim();
793        let (prefix, name) = match base.split_once(':') {
794            Some((prefix, name)) => (Some(prefix), name),
795            None => (None, base),
796        };
797        if node.lookup_namespace_uri(prefix) == Some(XSD_NAMESPACE) && XSD_TYPES.contains(&name) {
798            Ok(name.to_owned())
799        } else {
800            Err(self.error(
801                node,
802                ErrorKind::InvalidValue {
803                    location: "xs:restriction/@base".into(),
804                    found: base.to_owned(),
805                    expected: "an XML Schema built-in simple type, such as xs:string",
806                },
807            ))
808        }
809    }
810
811    fn non_negative(&self, node: Node<'d, 'input>, location: &str, value: &str) -> Result<u32> {
812        parse_non_negative(value)
813            .and_then(|count| u32::try_from(count).ok())
814            .ok_or_else(|| {
815                self.error(
816                    node,
817                    ErrorKind::InvalidValue {
818                        location: location.to_owned(),
819                        found: value.to_owned(),
820                        expected: "a non-negative integer",
821                    },
822                )
823            })
824    }
825
826    /// Checks that `node` carries only the given unqualified attributes.
827    ///
828    /// Attributes in the `xml` namespace are allowed anywhere; the root may
829    /// also carry schema-location hints.
830    fn attributes(&self, node: Node<'d, 'input>, allowed: &[&str]) -> Result<()> {
831        let is_root = node == self.document.root_element();
832        for attribute in node.attributes() {
833            let accepted = match attribute.namespace() {
834                None => allowed.contains(&attribute.name()),
835                Some(XML_NAMESPACE) => true,
836                Some(XSI_NAMESPACE) => {
837                    is_root
838                        && matches!(
839                            attribute.name(),
840                            "schemaLocation" | "noNamespaceSchemaLocation"
841                        )
842                }
843                Some(_) => false,
844            };
845            if !accepted {
846                let name = match attribute.namespace() {
847                    Some(namespace) => format!("{{{namespace}}}{}", attribute.name()),
848                    None => attribute.name().to_owned(),
849                };
850                return Err(self.error(
851                    node,
852                    ErrorKind::UnexpectedAttribute {
853                        element: local(node),
854                        attribute: name,
855                    },
856                ));
857            }
858        }
859        Ok(())
860    }
861
862    fn required_attribute(&self, node: Node<'d, 'input>, name: &'static str) -> Result<&'d str> {
863        node.attribute(name).ok_or_else(|| {
864            self.error(
865                node,
866                ErrorKind::MissingAttribute {
867                    element: local(node),
868                    attribute: name,
869                },
870            )
871        })
872    }
873
874    /// The element children of an element-only node.
875    fn elements(&self, node: Node<'d, 'input>) -> Result<Vec<Node<'d, 'input>>> {
876        let mut elements = Vec::new();
877        for child in node.children() {
878            if child.is_element() {
879                elements.push(child);
880            } else if child.is_text() && !child.text().unwrap_or("").chars().all(is_xml_space) {
881                return Err(self.error(
882                    child,
883                    ErrorKind::UnexpectedText {
884                        element: local(node),
885                    },
886                ));
887            }
888        }
889        Ok(elements)
890    }
891
892    fn sequence(&self, node: Node<'d, 'input>) -> Result<Sequence<'_, 'd, 'input>> {
893        Ok(Sequence {
894            reader: self,
895            parent: node,
896            items: self.elements(node)?,
897            next: 0,
898        })
899    }
900
901    /// The character data of a text-only element, verbatim.
902    fn text(&self, node: Node<'d, 'input>) -> Result<String> {
903        self.attributes(node, &[])?;
904        let mut text = String::new();
905        for child in node.children() {
906            if child.is_element() {
907                return Err(self.error(
908                    child,
909                    ErrorKind::UnexpectedElement {
910                        parent: local(node),
911                        found: expanded(child),
912                    },
913                ));
914            }
915            if let Some(part) = child.text().filter(|_| child.is_text()) {
916                text.push_str(part);
917            }
918        }
919        Ok(text)
920    }
921}
922
923/// An element's children consumed in schema order.
924struct Sequence<'r, 'd, 'input> {
925    reader: &'r Reader<'d, 'input>,
926    parent: Node<'d, 'input>,
927    items: Vec<Node<'d, 'input>>,
928    next: usize,
929}
930
931impl<'d, 'input> Sequence<'_, 'd, 'input> {
932    /// The next child, if it is the IDS element `name`.
933    fn optional(&mut self, name: &str) -> Option<Node<'d, 'input>> {
934        let node = *self.items.get(self.next)?;
935        is_ids(node, name).then(|| {
936            self.next += 1;
937            node
938        })
939    }
940
941    fn require(&mut self, name: &'static str) -> Result<Node<'d, 'input>> {
942        self.optional(name)
943            .ok_or_else(|| match self.items.get(self.next) {
944                // Something else stands where `name` must: report that element,
945                // it is either misplaced or misspelled.
946                Some(found) => self.reader.error(
947                    *found,
948                    ErrorKind::UnexpectedElement {
949                        parent: local(self.parent),
950                        found: expanded(*found),
951                    },
952                ),
953                None => self.reader.error(
954                    self.parent,
955                    ErrorKind::MissingElement {
956                        parent: local(self.parent),
957                        expected: name,
958                    },
959                ),
960            })
961    }
962
963    /// Refuses any child left over: it is out of order or not allowed.
964    fn finish(self) -> Result<()> {
965        match self.items.get(self.next) {
966            Some(found) => Err(self.reader.error(
967                *found,
968                ErrorKind::UnexpectedElement {
969                    parent: local(self.parent),
970                    found: expanded(*found),
971                },
972            )),
973            None => Ok(()),
974        }
975    }
976}
977
978/// The built-in simple types of XML Schema 1.0, the revision `ids.xsd`
979/// imports. A restriction can only derive from one of these.
980pub(crate) const XSD_TYPES: [&str; 45] = [
981    "anySimpleType",
982    "string",
983    "normalizedString",
984    "token",
985    "language",
986    "Name",
987    "NCName",
988    "ID",
989    "IDREF",
990    "IDREFS",
991    "ENTITY",
992    "ENTITIES",
993    "NMTOKEN",
994    "NMTOKENS",
995    "boolean",
996    "decimal",
997    "integer",
998    "nonPositiveInteger",
999    "negativeInteger",
1000    "long",
1001    "int",
1002    "short",
1003    "byte",
1004    "nonNegativeInteger",
1005    "unsignedLong",
1006    "unsignedInt",
1007    "unsignedShort",
1008    "unsignedByte",
1009    "positiveInteger",
1010    "float",
1011    "double",
1012    "duration",
1013    "dateTime",
1014    "time",
1015    "date",
1016    "gYearMonth",
1017    "gYear",
1018    "gMonthDay",
1019    "gDay",
1020    "gMonth",
1021    "hexBinary",
1022    "base64Binary",
1023    "anyURI",
1024    "QName",
1025    "NOTATION",
1026];
1027
1028const FACETS: [&str; 6] = [
1029    "entity",
1030    "partOf",
1031    "classification",
1032    "attribute",
1033    "property",
1034    "material",
1035];
1036
1037/// The unqualified attributes a facet element may carry in a role.
1038fn facet_attributes(kind: &str, role: Role) -> &'static [&'static str] {
1039    match (kind, role) {
1040        ("partOf", Role::Applicability) => &["relation"],
1041        ("property", Role::Applicability) => &["dataType"],
1042        ("entity", Role::Requirement) => &["instructions"],
1043        ("partOf", Role::Requirement) => &["relation", "cardinality", "instructions"],
1044        ("classification" | "material", Role::Requirement) => {
1045            &["uri", "cardinality", "instructions"]
1046        }
1047        ("attribute", Role::Requirement) => &["cardinality", "instructions"],
1048        ("property", Role::Requirement) => &["dataType", "uri", "cardinality", "instructions"],
1049        _ => &[],
1050    }
1051}
1052
1053#[derive(Clone, Copy)]
1054enum RestrictionFacet {
1055    Enumeration,
1056    Pattern,
1057    MinInclusive,
1058    MaxInclusive,
1059    MinExclusive,
1060    MaxExclusive,
1061    Length,
1062    MinLength,
1063    MaxLength,
1064    TotalDigits,
1065    FractionDigits,
1066}
1067
1068impl RestrictionFacet {
1069    fn from_name(name: &str) -> Option<RestrictionFacet> {
1070        Some(match name {
1071            "enumeration" => RestrictionFacet::Enumeration,
1072            "pattern" => RestrictionFacet::Pattern,
1073            "minInclusive" => RestrictionFacet::MinInclusive,
1074            "maxInclusive" => RestrictionFacet::MaxInclusive,
1075            "minExclusive" => RestrictionFacet::MinExclusive,
1076            "maxExclusive" => RestrictionFacet::MaxExclusive,
1077            "length" => RestrictionFacet::Length,
1078            "minLength" => RestrictionFacet::MinLength,
1079            "maxLength" => RestrictionFacet::MaxLength,
1080            "totalDigits" => RestrictionFacet::TotalDigits,
1081            "fractionDigits" => RestrictionFacet::FractionDigits,
1082            _ => return None,
1083        })
1084    }
1085}
1086
1087fn duplicate(node: Node<'_, '_>) -> ErrorKind {
1088    ErrorKind::UnexpectedElement {
1089        parent: "xs:restriction".into(),
1090        found: format!("a second {}", expanded(node)),
1091    }
1092}
1093
1094/// The revision `xsi:schemaLocation` names for the IDS namespace, if any.
1095///
1096/// Published schemas live at `.../IDS/<version>/ids.xsd`, so the segment
1097/// before the file name is the revision. A location of any other shape
1098/// declares nothing.
1099fn declared_version(root: Node<'_, '_>) -> Option<IdsVersion> {
1100    let hint = root.attribute((XSI_NAMESPACE, "schemaLocation"))?;
1101    let tokens: Vec<&str> = hint.split(is_xml_space).filter(|t| !t.is_empty()).collect();
1102    tokens.chunks(2).find_map(|pair| match pair {
1103        [namespace, location] if *namespace == NAMESPACE => {
1104            let mut segments = location.rsplit('/');
1105            let file = segments.next()?;
1106            if !file.to_ascii_lowercase().ends_with(".xsd") {
1107                return None;
1108            }
1109            segments.next()?.parse().ok()
1110        }
1111        _ => None,
1112    })
1113}
1114
1115/// Every revision-distinguishing shape in the document, collected leniently.
1116fn signals(root: Node<'_, '_>) -> VersionSignals {
1117    let mut signals = VersionSignals::new();
1118    for node in root.descendants().filter(|n| n.is_element()) {
1119        if node.tag_name().namespace() != Some(NAMESPACE) {
1120            continue;
1121        }
1122        let name = node.tag_name().name();
1123        if name == "specification" {
1124            for token in node
1125                .attribute("ifcVersion")
1126                .unwrap_or("")
1127                .split(is_xml_space)
1128            {
1129                match token {
1130                    "IFC4X3" => signals.observe(Signal::Ifc4x3Unsuffixed),
1131                    "IFC4X3_ADD2" => signals.observe(Signal::Ifc4x3Add2),
1132                    _ => &mut signals,
1133                };
1134            }
1135        }
1136        let in_requirements = node
1137            .parent_element()
1138            .is_some_and(|parent| is_ids(parent, "requirements"));
1139        if in_requirements && FACETS.contains(&name) {
1140            if node.has_attribute("minOccurs") || node.has_attribute("maxOccurs") {
1141                signals.observe(Signal::OccursOnRequirementFacet);
1142            }
1143            if node.has_attribute("cardinality") {
1144                signals.observe(Signal::CardinalityOnRequirementFacet);
1145            }
1146        }
1147        if name == "property" {
1148            for child in node
1149                .children()
1150                .filter(|c| c.tag_name().namespace() == Some(NAMESPACE))
1151            {
1152                match child.tag_name().name() {
1153                    "name" => signals.observe(Signal::PropertyName),
1154                    "baseName" => signals.observe(Signal::PropertyBaseName),
1155                    _ => &mut signals,
1156                };
1157            }
1158            if node.has_attribute("measure") {
1159                signals.observe(Signal::MeasureAttribute);
1160            }
1161            if node.has_attribute("dataType") {
1162                signals.observe(Signal::DataTypeAttribute);
1163            }
1164        }
1165        if name == "classification" && !node.children().any(|c| is_ids(c, "system")) {
1166            signals.observe(Signal::ClassificationWithoutSystem);
1167        }
1168    }
1169    signals
1170}
1171
1172fn is_ids(node: Node<'_, '_>, name: &str) -> bool {
1173    node.is_element() && node.has_tag_name((NAMESPACE, name))
1174}
1175
1176fn is_xsd(node: Node<'_, '_>, name: &str) -> bool {
1177    node.is_element() && node.has_tag_name((XSD_NAMESPACE, name))
1178}
1179
1180fn local(node: Node<'_, '_>) -> String {
1181    node.tag_name().name().to_owned()
1182}
1183
1184fn expanded(node: Node<'_, '_>) -> String {
1185    match node.tag_name().namespace() {
1186        Some(namespace) => format!("{{{namespace}}}{}", node.tag_name().name()),
1187        None => node.tag_name().name().to_owned(),
1188    }
1189}
1190
1191fn attribute(node: Node<'_, '_>, name: &str) -> Option<String> {
1192    node.attribute(name).map(ToOwned::to_owned)
1193}
1194
1195fn is_xml_space(c: char) -> bool {
1196    matches!(c, ' ' | '\t' | '\n' | '\r')
1197}
1198
1199/// An `xs:nonNegativeInteger` after whitespace collapsing: an optional `+`
1200/// and at least one digit.
1201fn parse_non_negative(value: &str) -> Option<u64> {
1202    let digits = value.trim_matches(is_xml_space);
1203    let digits = digits.strip_prefix('+').unwrap_or(digits);
1204    if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
1205        return None;
1206    }
1207    digits.parse().ok()
1208}
1209
1210/// The schema's `author` pattern `[^@]+@[^\.]+\..+`, which XML Schema
1211/// anchors at both ends.
1212pub(crate) fn is_author(value: &str) -> bool {
1213    let Some((local, domain)) = value.split_once('@') else {
1214        return false;
1215    };
1216    let Some((host, rest)) = domain.split_once('.') else {
1217        return false;
1218    };
1219    // `.` matches anything but a line break.
1220    !local.is_empty() && !host.is_empty() && !rest.is_empty() && !rest.contains(['\n', '\r'])
1221}
1222
1223/// The lexical form of `xs:date`: `-?YYYY-MM-DD` and an optional zone.
1224pub(crate) fn is_xs_date(value: &str) -> bool {
1225    let unsigned = value.strip_prefix('-').unwrap_or(value);
1226    let (date, zone) = match unsigned.find(['Z', '+']).or_else(|| {
1227        // A zone's `-` comes after the day, so search past `YYYY-MM-DD`.
1228        unsigned
1229            .char_indices()
1230            .filter(|(_, c)| *c == '-')
1231            .nth(2)
1232            .map(|(i, _)| i)
1233    }) {
1234        Some(split) => unsigned.split_at(split),
1235        None => (unsigned, ""),
1236    };
1237    let parts: Vec<&str> = date.split('-').collect();
1238    let [year, month, day] = parts[..] else {
1239        return false;
1240    };
1241    let digits = |s: &str, len: usize| s.len() == len && s.bytes().all(|b| b.is_ascii_digit());
1242    let year_ok = year.len() >= 4
1243        && year.bytes().all(|b| b.is_ascii_digit())
1244        && (year.len() == 4 || !year.starts_with('0'));
1245    if !year_ok || !digits(month, 2) || !digits(day, 2) {
1246        return false;
1247    }
1248    let (Ok(year), Ok(month), Ok(day)) = (
1249        year.parse::<u64>(),
1250        month.parse::<u32>(),
1251        day.parse::<u32>(),
1252    ) else {
1253        return false;
1254    };
1255    let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
1256    let days = match month {
1257        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1258        4 | 6 | 9 | 11 => 30,
1259        2 if leap => 29,
1260        2 => 28,
1261        _ => return false,
1262    };
1263    if day == 0 || day > days {
1264        return false;
1265    }
1266    is_zone(zone)
1267}
1268
1269fn is_zone(zone: &str) -> bool {
1270    if zone.is_empty() || zone == "Z" {
1271        return true;
1272    }
1273    let Some(offset) = zone.strip_prefix(['+', '-']) else {
1274        return false;
1275    };
1276    let Some((hours, minutes)) = offset.split_once(':') else {
1277        return false;
1278    };
1279    let two = |s: &str| s.len() == 2 && s.bytes().all(|b| b.is_ascii_digit());
1280    if !two(hours) || !two(minutes) {
1281        return false;
1282    }
1283    let (hours, minutes): (u32, u32) = (hours.parse().unwrap_or(99), minutes.parse().unwrap_or(99));
1284    (hours < 14 && minutes < 60) || (hours == 14 && minutes == 0)
1285}
1286
1287#[cfg(test)]
1288mod tests {
1289    use super::*;
1290
1291    #[test]
1292    fn author_follows_the_schema_pattern() {
1293        for good in ["a@b.c", "first.last@example.org", "x@y.z.w"] {
1294            assert!(is_author(good), "{good}");
1295        }
1296        for bad in ["", "a@b", "@b.c", "a@.c", "a@b.", "ab.c", "a@b.c\n"] {
1297            assert!(!is_author(bad), "{bad:?}");
1298        }
1299    }
1300
1301    #[test]
1302    fn dates_follow_the_xs_date_lexical_form() {
1303        for good in [
1304            "2024-06-01",
1305            "2024-02-29",
1306            "2000-02-29",
1307            "2024-06-01Z",
1308            "2024-06-01+02:00",
1309            "2024-06-01-14:00",
1310            "-0044-03-15",
1311            "12024-01-01",
1312        ] {
1313            assert!(is_xs_date(good), "{good}");
1314        }
1315        for bad in [
1316            "",
1317            "2024-6-1",
1318            "2023-02-29",
1319            "1900-02-29",
1320            "2024-13-01",
1321            "2024-00-10",
1322            "2024-06-31",
1323            "24-06-01",
1324            "02024-01-01",
1325            "2024-06-01T00:00:00",
1326            "2024-06-01+2:00",
1327            "2024-06-01+14:30",
1328            "2024/06/01",
1329        ] {
1330            assert!(!is_xs_date(bad), "{bad}");
1331        }
1332    }
1333
1334    #[test]
1335    fn non_negative_integers_follow_the_lexical_form() {
1336        assert_eq!(parse_non_negative("0"), Some(0));
1337        assert_eq!(parse_non_negative(" +007 "), Some(7));
1338        for bad in ["", "-1", "1.0", "one", "+", "1 2"] {
1339            assert_eq!(parse_non_negative(bad), None, "{bad:?}");
1340        }
1341    }
1342}