1use 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
48pub 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#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct ReadError {
56 kind: ErrorKind,
57 line: u32,
58 column: u32,
59}
60
61impl ReadError {
62 #[must_use]
64 pub fn kind(&self) -> &ErrorKind {
65 &self.kind
66 }
67
68 #[must_use]
70 pub fn line(&self) -> u32 {
71 self.line
72 }
73
74 #[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#[derive(Debug, Clone, PartialEq, Eq)]
91#[non_exhaustive]
92pub enum ErrorKind {
93 NotUtf8,
95 Xml(String),
97 NotIds {
99 found: String,
101 },
102 UnsupportedVersion(Detected<IdsVersion>),
104 UnexpectedElement {
106 parent: String,
108 found: String,
110 },
111 MissingElement {
113 parent: String,
115 expected: &'static str,
117 },
118 UnexpectedAttribute {
120 element: String,
122 attribute: String,
124 },
125 MissingAttribute {
127 element: String,
129 attribute: &'static str,
131 },
132 InvalidValue {
134 location: String,
136 found: String,
138 expected: &'static str,
140 },
141 UnexpectedText {
143 element: String,
145 },
146 Unsupported {
148 element: String,
150 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
203pub 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
221pub 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#[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 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 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 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 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 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 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
923struct 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 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 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 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
978pub(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
1037fn 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
1094fn 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
1115fn 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
1199fn 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
1210pub(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 !local.is_empty() && !host.is_empty() && !rest.is_empty() && !rest.contains(['\n', '\r'])
1221}
1222
1223pub(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 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}