1mod mapping;
60mod pattern;
61
62use core::fmt;
63use std::collections::BTreeSet;
64use std::sync::OnceLock;
65
66use ifc_schema::{Schema, TypeKind};
67#[cfg(feature = "audit")]
68use ifc_template_catalog::catalog::Catalog;
69#[cfg(feature = "audit")]
70use ifc_template_catalog::definition::{
71 CatalogEdition, PropertyKind, QuantityKind, SetTemplateKind,
72};
73
74use crate::model::{
75 Attribute, Entity, Facet, PartOf, Property, Relation, Restriction, Specification, Value,
76};
77use crate::{Ids, IfcVersion};
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
81pub enum Severity {
82 Error,
84 Warning,
86}
87
88impl fmt::Display for Severity {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 f.write_str(match self {
91 Severity::Error => "error",
92 Severity::Warning => "warning",
93 })
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
99#[non_exhaustive]
100pub enum AuditCode {
101 EntityNameCase,
104 EntityUnknown,
106 EntityPatternMatchesNothing,
108 PredefinedTypeUnavailable,
111 EntityRequirementContradictsApplicability,
118 AttributeUnknown,
121 AttributeDerived,
124 AttributePatternMatchesNothing,
127 AttributeValueNotComparable,
130 ValueTypeMismatch,
133 PatternOnNonString,
136 PatternUnverified,
139 RestrictionBoundsContradict,
142 DataTypeUnknown,
145 PropertyNotInStandardSet,
148 PropertyDataTypeMismatch,
151 StandardSetUnknown,
154 PartOfRelationEntity,
157 ProhibitedWithRequirements,
160 OccurrenceBoundsContradict,
162}
163
164impl AuditCode {
165 #[must_use]
167 pub fn as_str(self) -> &'static str {
168 match self {
169 AuditCode::EntityNameCase => "entity-name-case",
170 AuditCode::EntityUnknown => "entity-unknown",
171 AuditCode::EntityPatternMatchesNothing => "entity-pattern-matches-nothing",
172 AuditCode::PredefinedTypeUnavailable => "predefined-type-unavailable",
173 AuditCode::EntityRequirementContradictsApplicability => {
174 "entity-requirement-contradicts-applicability"
175 }
176 AuditCode::AttributeUnknown => "attribute-unknown",
177 AuditCode::AttributeDerived => "attribute-derived",
178 AuditCode::AttributePatternMatchesNothing => "attribute-pattern-matches-nothing",
179 AuditCode::AttributeValueNotComparable => "attribute-value-not-comparable",
180 AuditCode::ValueTypeMismatch => "value-type-mismatch",
181 AuditCode::PatternOnNonString => "pattern-on-non-string",
182 AuditCode::PatternUnverified => "pattern-unverified",
183 AuditCode::RestrictionBoundsContradict => "restriction-bounds-contradict",
184 AuditCode::DataTypeUnknown => "data-type-unknown",
185 AuditCode::PropertyNotInStandardSet => "property-not-in-standard-set",
186 AuditCode::PropertyDataTypeMismatch => "property-data-type-mismatch",
187 AuditCode::StandardSetUnknown => "standard-set-unknown",
188 AuditCode::PartOfRelationEntity => "part-of-relation-entity",
189 AuditCode::ProhibitedWithRequirements => "prohibited-with-requirements",
190 AuditCode::OccurrenceBoundsContradict => "occurrence-bounds-contradict",
191 }
192 }
193
194 #[must_use]
196 pub fn severity(self) -> Severity {
197 match self {
198 AuditCode::PatternUnverified => Severity::Warning,
199 _ => Severity::Error,
200 }
201 }
202}
203
204impl fmt::Display for AuditCode {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 f.write_str(self.as_str())
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct AuditFinding {
213 pub code: AuditCode,
215 pub specification: usize,
217 pub path: String,
220 pub ifc_version: Option<IfcVersion>,
222 pub message: String,
224}
225
226impl AuditFinding {
227 #[must_use]
229 pub fn severity(&self) -> Severity {
230 self.code.severity()
231 }
232}
233
234impl fmt::Display for AuditFinding {
235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236 write!(
237 f,
238 "{} [{}] specifications[{}]",
239 self.severity(),
240 self.code,
241 self.specification
242 )?;
243 if !self.path.is_empty() {
244 write!(f, "/{}", self.path)?;
245 }
246 if let Some(version) = self.ifc_version {
247 write!(f, " ({version})")?;
248 }
249 write!(f, ": {}", self.message)
250 }
251}
252
253#[must_use]
271pub fn audit(ids: &Ids) -> Vec<AuditFinding> {
272 let mut auditor = Auditor::default();
273 for (index, specification) in ids.specifications.iter().enumerate() {
274 auditor.specification = index;
275 auditor.audit_specification(specification);
276 }
277 auditor.findings
278}
279
280struct Release {
282 version: IfcVersion,
283 schema: &'static Schema,
284 #[cfg(feature = "audit")]
285 templates: Option<Catalog>,
286 entities: Vec<String>,
288}
289
290fn release(version: IfcVersion) -> &'static Release {
291 static RELEASES: [OnceLock<Release>; 3] = [OnceLock::new(), OnceLock::new(), OnceLock::new()];
292 let (slot, schema): (usize, fn() -> &'static Schema) = match version {
293 IfcVersion::Ifc2x3 => (0, ifc_schema::ifc2x3),
294 IfcVersion::Ifc4 => (1, ifc_schema::ifc4),
295 IfcVersion::Ifc4x3Add2 => (2, ifc_schema::ifc4x3),
296 };
297 RELEASES[slot].get_or_init(|| {
298 let schema = schema();
299 let mut entities: BTreeSet<String> =
300 schema.entity_names().map(str::to_ascii_uppercase).collect();
301 if version == IfcVersion::Ifc2x3 {
302 entities.extend(
303 mapping::IFC2X3_OCCURRENCE_TYPES
304 .iter()
305 .map(|(name, _, _)| (*name).to_owned()),
306 );
307 }
308 Release {
309 version,
310 schema,
311 #[cfg(feature = "audit")]
312 templates: templates(version),
313 entities: entities.into_iter().collect(),
314 }
315 })
316}
317
318#[cfg(feature = "audit")]
320fn templates(version: IfcVersion) -> Option<Catalog> {
321 let edition = match version {
322 IfcVersion::Ifc2x3 => CatalogEdition::Ifc2x3Tc1,
323 IfcVersion::Ifc4 => CatalogEdition::Ifc4Add2Tc1,
324 IfcVersion::Ifc4x3Add2 => CatalogEdition::Ifc4x3Add2,
325 };
326 ifc_template_catalog::embedded::official_catalog(edition).ok()
327}
328
329impl Release {
330 fn can_be(&self, one: &str, other: &str) -> bool {
338 if one == other {
339 return true;
340 }
341 if self.version != IfcVersion::Ifc2x3 {
342 return false;
343 }
344 let occurrence = |name: &str| {
345 mapping::IFC2X3_OCCURRENCE_TYPES
346 .iter()
347 .find(|(mapped, _, _)| *mapped == name)
348 .map(|(_, occurrence, _)| *occurrence)
349 };
350 match (occurrence(one), occurrence(other)) {
351 (Some(occurrence), None) => occurrence == other,
352 (None, Some(occurrence)) => occurrence == one,
353 _ => false,
354 }
355 }
356
357 fn has_entity(&self, name: &str) -> bool {
358 self.entities
359 .binary_search_by(|e| e.as_str().cmp(name))
360 .is_ok()
361 }
362
363 fn resolve(&self, name: &str) -> Option<(String, Option<String>)> {
365 if self.version == IfcVersion::Ifc2x3 {
366 if let Some((_, occurrence, type_entity)) = mapping::IFC2X3_OCCURRENCE_TYPES
367 .iter()
368 .find(|(mapped, _, _)| *mapped == name)
369 {
370 return Some(((*occurrence).to_owned(), Some((*type_entity).to_owned())));
371 }
372 }
373 let entity = self.schema.entity(name)?;
374 let type_entity = format!("{name}TYPE");
375 let type_entity = (!name.ends_with("TYPE") && self.schema.entity(&type_entity).is_some())
376 .then_some(type_entity);
377 Some((entity.name.to_ascii_uppercase(), type_entity))
378 }
379
380 fn has_attribute(&self, entity: &str, attribute: &str) -> bool {
381 self.schema.attribute_names(entity).contains(&attribute)
382 }
383
384 fn has_predefined_type(&self, name: &str) -> bool {
385 self.resolve(name).is_some_and(|(occurrence, type_entity)| {
386 self.has_attribute(&occurrence, "PredefinedType")
387 || type_entity.is_some_and(|t| self.has_attribute(&t, "PredefinedType"))
388 })
389 }
390
391 fn is_derived(&self, entity: &str, attribute: &str) -> bool {
393 std::iter::once(entity)
394 .chain(self.schema.supertypes(entity))
395 .filter_map(|name| self.schema.entity(name))
396 .any(|def| def.derived.iter().any(|d| d == attribute))
397 }
398
399 fn is_defined_type(&self, name: &str) -> bool {
400 self.schema
401 .type_def(name)
402 .is_some_and(|def| matches!(def.kind, TypeKind::Defined(_) | TypeKind::Enumeration(_)))
403 }
404
405 fn kind_of_type(&self, type_name: &str) -> Kind {
407 if self.schema.entity(type_name).is_some() {
408 return Kind::NotComparable("an entity");
409 }
410 match self.schema.type_def(type_name).map(|def| &def.kind) {
411 Some(TypeKind::Select(_)) => return Kind::NotComparable("a select"),
412 Some(TypeKind::Enumeration(_)) => return Kind::Unchecked,
413 _ => {}
414 }
415 let base = self.schema.resolve_defined(type_name).to_ascii_uppercase();
416 match base.as_str() {
417 "INTEGER" => Kind::Integer,
418 "REAL" | "NUMBER" => Kind::Real,
419 "BOOLEAN" => Kind::Boolean,
420 "LOGICAL" => Kind::Logical,
421 "STRING" | "BINARY" => Kind::String,
422 other
423 if other.starts_with("LIST")
424 || other.starts_with("SET")
425 || other.starts_with("ARRAY")
426 || other.starts_with("BAG") =>
427 {
428 Kind::NotComparable("a list")
429 }
430 _ => match self.schema.type_def(&base).map(|def| &def.kind) {
431 Some(TypeKind::Select(_)) => Kind::NotComparable("a select"),
432 _ => Kind::Unchecked,
433 },
434 }
435 }
436}
437
438#[derive(Debug, Clone, Copy, PartialEq, Eq)]
440enum Kind {
441 Integer,
442 Real,
443 Boolean,
444 Logical,
445 String,
446 Unchecked,
448 NotComparable(&'static str),
450}
451
452impl Kind {
453 fn accepts(self, value: &str) -> bool {
454 let value = value.trim_matches(|c| matches!(c, ' ' | '\t' | '\n' | '\r'));
455 match self {
456 Kind::Integer => is_xs_integer(value),
457 Kind::Real => is_xs_double(value),
458 Kind::Boolean => matches!(value, "true" | "false"),
459 Kind::Logical => matches!(value, "true" | "false" | "unknown"),
460 Kind::String | Kind::Unchecked | Kind::NotComparable(_) => true,
461 }
462 }
463
464 fn is_string(self) -> bool {
465 matches!(self, Kind::String | Kind::Unchecked)
466 }
467
468 fn describe(self) -> &'static str {
469 match self {
470 Kind::Integer => "an integer",
471 Kind::Real => "a real number",
472 Kind::Boolean => "a boolean (true or false)",
473 Kind::Logical => "a logical (true, false or unknown)",
474 Kind::String => "a string",
475 Kind::Unchecked => "an enumerated value",
476 Kind::NotComparable(what) => what,
477 }
478 }
479}
480
481enum Names {
483 Listed(Vec<String>),
485 Matched(Vec<String>),
487 Unknown,
489}
490
491fn names<'u>(value: &Value, universe: impl Iterator<Item = &'u str>) -> Names {
492 match value {
493 Value::Simple(value) => Names::Listed(vec![value.clone()]),
494 Value::Restriction(restriction) if !restriction.enumeration.is_empty() => {
495 Names::Listed(restriction.enumeration.clone())
496 }
497 Value::Restriction(restriction) if !restriction.patterns.is_empty() => {
498 let Ok(patterns) = restriction
499 .patterns
500 .iter()
501 .map(|p| pattern::compile(p))
502 .collect::<Result<Vec<_>, _>>()
503 else {
504 return Names::Unknown;
505 };
506 Names::Matched(
507 universe
508 .filter(|name| patterns.iter().any(|p| p.is_match(name)))
509 .map(str::to_owned)
510 .collect(),
511 )
512 }
513 Value::Restriction(_) => Names::Unknown,
514 }
515}
516
517#[derive(Default)]
518struct Auditor {
519 findings: Vec<AuditFinding>,
520 specification: usize,
521}
522
523impl Auditor {
524 fn report(
525 &mut self,
526 code: AuditCode,
527 path: &str,
528 version: Option<IfcVersion>,
529 message: String,
530 ) {
531 self.findings.push(AuditFinding {
532 code,
533 specification: self.specification,
534 path: path.to_owned(),
535 ifc_version: version,
536 message,
537 });
538 }
539
540 fn audit_specification(&mut self, spec: &Specification) {
541 let applicability = &spec.applicability;
542 if applicability
543 .max_occurs
544 .is_some_and(|max| max < applicability.min_occurs)
545 {
546 self.report(
547 AuditCode::OccurrenceBoundsContradict,
548 "applicability",
549 None,
550 format!(
551 "minOccurs {} exceeds maxOccurs {}",
552 applicability.min_occurs,
553 applicability.max_occurs.unwrap_or_default()
554 ),
555 );
556 }
557 if applicability.max_occurs == Some(0)
558 && spec
559 .requirements
560 .as_ref()
561 .is_some_and(|r| !r.facets.is_empty())
562 {
563 self.report(
564 AuditCode::ProhibitedWithRequirements,
565 "requirements",
566 None,
567 "a prohibited specification fails on any applicable object, so its \
568 requirements are never checked"
569 .into(),
570 );
571 }
572
573 let facets = facets(spec);
575 for (path, facet) in &facets {
576 self.facet_independent(path, facet);
577 }
578
579 for &version in &spec.ifc_versions {
581 let release = release(version);
582 let applicable = applicability.facets.iter().find_map(|facet| match facet {
583 Facet::Entity(entity) => Some(entity),
584 _ => None,
585 });
586 let applicable = applicable
587 .and_then(|entity| self.entity(release, "applicability/facets[0]", entity))
588 .map(|(names, _)| names);
589 for (path, facet) in &facets {
590 match facet {
591 Facet::Entity(entity) if path.starts_with("requirements") => {
592 let required = self.entity(release, path, entity).map(|(names, _)| names);
593 if let (Some(required), Some(applicable)) = (&required, &applicable) {
594 if !required.is_empty()
595 && !applicable.is_empty()
596 && !required
597 .iter()
598 .any(|r| applicable.iter().any(|a| release.can_be(r, a)))
599 {
600 self.report(
601 AuditCode::EntityRequirementContradictsApplicability,
602 path,
603 Some(version),
604 format!(
605 "requires {} but applies to {}; IDS does not match subtypes",
606 alternatives(required),
607 alternatives(applicable)
608 ),
609 );
610 }
611 }
612 }
613 Facet::Entity(_) => {}
614 Facet::PartOf(part_of) => self.part_of(release, path, part_of),
615 Facet::Attribute(attribute) => {
616 self.attribute(release, path, attribute, applicable.as_deref());
617 }
618 Facet::Property(property) => self.property(release, path, property),
619 Facet::Classification(_) | Facet::Material(_) => {}
620 }
621 }
622 }
623 }
624
625 fn facet_independent(&mut self, path: &str, facet: &Facet) {
626 let entities: Vec<(&Entity, String)> = match facet {
627 Facet::Entity(entity) => vec![(entity, path.to_owned())],
628 Facet::PartOf(part_of) => vec![(&part_of.entity, format!("{path}/entity"))],
629 _ => Vec::new(),
630 };
631 for (entity, entity_path) in entities {
632 if let Names::Listed(names) = names(&entity.name, std::iter::empty()) {
633 for name in names.iter().filter(|n| **n != n.to_ascii_uppercase()) {
634 self.report(
635 AuditCode::EntityNameCase,
636 &format!("{entity_path}/name"),
637 None,
638 format!(
639 "{name:?} is not upper case; IFC entity names are written as {:?}",
640 name.to_ascii_uppercase()
641 ),
642 );
643 }
644 }
645 }
646 for (value_path, value) in values(facet) {
647 if let Value::Restriction(restriction) = value {
648 self.restriction(&format!("{path}/{value_path}"), restriction);
649 }
650 }
651 }
652
653 fn entity(
656 &mut self,
657 release: &Release,
658 path: &str,
659 entity: &Entity,
660 ) -> Option<(Vec<String>, bool)> {
661 let version = Some(release.version);
662 let known = match names(&entity.name, release.entities.iter().map(String::as_str)) {
663 Names::Listed(names) => {
664 let mut known = Vec::new();
665 for name in names {
666 if name != name.to_ascii_uppercase() {
667 continue; }
669 if release.has_entity(&name) {
670 known.push(name);
671 } else {
672 self.report(
673 AuditCode::EntityUnknown,
674 &format!("{path}/name"),
675 version,
676 format!("{} does not define {name}", release.version),
677 );
678 }
679 }
680 if entity.predefined_type.is_some() {
681 for name in &known {
682 if !release.has_predefined_type(name) {
683 self.report(
684 AuditCode::PredefinedTypeUnavailable,
685 &format!("{path}/predefinedType"),
686 version,
687 format!(
688 "{name} has no PredefinedType in {}, neither itself nor \
689 through a type entity",
690 release.version
691 ),
692 );
693 }
694 }
695 }
696 (known, true)
697 }
698 Names::Matched(names) => {
699 if names.is_empty() {
700 self.report(
701 AuditCode::EntityPatternMatchesNothing,
702 &format!("{path}/name"),
703 version,
704 format!("the pattern matches no entity of {}", release.version),
705 );
706 }
707 (names, false)
708 }
709 Names::Unknown => return None,
710 };
711 Some(known)
712 }
713
714 fn attribute(
715 &mut self,
716 release: &Release,
717 path: &str,
718 attribute: &Attribute,
719 applicable: Option<&[String]>,
720 ) {
721 let Some(applicable) = applicable.filter(|a| !a.is_empty()) else {
722 return;
723 };
724 let version = Some(release.version);
725 let entities: Vec<String> = applicable
726 .iter()
727 .filter_map(|name| release.resolve(name).map(|(occurrence, _)| occurrence))
728 .collect();
729 let mut universe: BTreeSet<&str> = BTreeSet::new();
730 for entity in &entities {
731 universe.extend(release.schema.attribute_names(entity));
732 }
733 let name_path = format!("{path}/name");
734 let matched: Vec<String> = match names(&attribute.name, universe.iter().copied()) {
735 Names::Listed(names) => names
736 .into_iter()
737 .filter(|name| {
738 let explicit = entities
739 .iter()
740 .any(|e| release.has_attribute(e, name) && !release.is_derived(e, name));
741 if !explicit {
742 let derived = entities.iter().any(|e| release.is_derived(e, name));
743 let (code, message) = if derived {
744 (
745 AuditCode::AttributeDerived,
746 format!("{name} is derived on {}", alternatives(applicable)),
747 )
748 } else {
749 (
750 AuditCode::AttributeUnknown,
751 format!(
752 "{} has no explicit attribute {name} in {}",
753 alternatives(applicable),
754 release.version
755 ),
756 )
757 };
758 self.report(code, &name_path, version, message);
759 }
760 explicit
761 })
762 .collect(),
763 Names::Matched(names) => {
764 let names: Vec<String> = names
765 .into_iter()
766 .filter(|n| !entities.iter().any(|e| release.is_derived(e, n)))
767 .collect();
768 if names.is_empty() {
769 self.report(
770 AuditCode::AttributePatternMatchesNothing,
771 &name_path,
772 version,
773 format!(
774 "the pattern matches no explicit attribute of {}",
775 alternatives(applicable)
776 ),
777 );
778 }
779 names
780 }
781 Names::Unknown => return,
782 };
783 let Some(value) = &attribute.value else {
784 return;
785 };
786 let kinds: Vec<Kind> = matched
787 .iter()
788 .flat_map(|name| {
789 entities.iter().flat_map(move |entity| {
790 release
791 .schema
792 .attributes(entity)
793 .into_iter()
794 .filter(move |a| a.name == *name)
795 .map(|a| {
796 if a.aggregate {
797 Kind::NotComparable("a list")
798 } else {
799 release.kind_of_type(&a.type_name)
800 }
801 })
802 })
803 })
804 .collect();
805 if kinds.is_empty() {
806 return;
807 }
808 if let Some(Kind::NotComparable(what)) = kinds
809 .iter()
810 .copied()
811 .find(|k| matches!(k, Kind::NotComparable(_)))
812 .filter(|_| kinds.iter().all(|k| matches!(k, Kind::NotComparable(_))))
813 {
814 self.report(
815 AuditCode::AttributeValueNotComparable,
816 &format!("{path}/value"),
817 version,
818 format!("the attribute holds {what}, which never equals a value"),
819 );
820 return;
821 }
822 let comparable: Vec<Kind> = kinds
823 .into_iter()
824 .filter(|k| !matches!(k, Kind::NotComparable(_)))
825 .collect();
826 self.value(&format!("{path}/value"), version, value, &comparable);
827 }
828
829 fn property(&mut self, release: &Release, path: &str, property: &Property) {
830 let version = Some(release.version);
831 let mut kinds = Vec::new();
832 if let Some(data_type) = &property.data_type {
833 if release.is_defined_type(data_type) {
834 kinds.push(release.kind_of_type(data_type));
835 } else {
836 self.report(
837 AuditCode::DataTypeUnknown,
838 &format!("{path}/@dataType"),
839 version,
840 format!("{data_type} is not a defined type of {}", release.version),
841 );
842 }
843 }
844 if let Some(value) = &property.value {
845 self.value(&format!("{path}/value"), version, value, &kinds);
846 }
847 #[cfg(feature = "audit")]
848 self.standard_set(release, path, property);
849 }
850
851 #[cfg(feature = "audit")]
852 fn standard_set(&mut self, release: &Release, path: &str, property: &Property) {
853 let Some(catalog) = &release.templates else {
854 return;
855 };
856 let Names::Listed(sets) = names(&property.property_set, std::iter::empty()) else {
857 return;
858 };
859 let version = Some(release.version);
860 for set in sets
861 .iter()
862 .filter(|s| s.starts_with("Pset_") || s.starts_with("Qto_"))
863 {
864 let Some(template) = catalog.get(set) else {
865 self.report(
866 AuditCode::StandardSetUnknown,
867 &format!("{path}/propertySet"),
868 version,
869 format!(
870 "{set} uses a reserved prefix but is not a standard set of {}",
871 release.version
872 ),
873 );
874 continue;
875 };
876 let members: Vec<(String, Option<String>)> = match &template.kind {
877 SetTemplateKind::Property { properties, .. } => properties
878 .iter()
879 .map(|p| (p.name.clone(), property_data_type(&p.kind)))
880 .collect(),
881 SetTemplateKind::Quantity { quantities, .. } => quantities
882 .iter()
883 .map(|q| (q.name.clone(), quantity_data_type(q.kind)))
884 .collect(),
885 _ => continue,
886 };
887 let Names::Listed(base_names) = names(&property.base_name, std::iter::empty()) else {
888 continue;
889 };
890 for base_name in base_names {
891 match members.iter().find(|(name, _)| *name == base_name) {
892 None => self.report(
893 AuditCode::PropertyNotInStandardSet,
894 &format!("{path}/baseName"),
895 version,
896 format!("{set} of {} has no property {base_name}", release.version),
897 ),
898 Some((_, Some(expected))) => {
899 if let Some(data_type) = &property.data_type {
900 if !data_type.eq_ignore_ascii_case(expected) {
901 self.report(
902 AuditCode::PropertyDataTypeMismatch,
903 &format!("{path}/@dataType"),
904 version,
905 format!(
906 "{set}.{base_name} is {} in {}, not {data_type}",
907 expected.to_ascii_uppercase(),
908 release.version
909 ),
910 );
911 }
912 }
913 }
914 Some((_, None)) => {}
915 }
916 }
917 }
918 }
919
920 fn part_of(&mut self, release: &Release, path: &str, part_of: &PartOf) {
921 let entity_path = format!("{path}/entity");
922 let Some((wholes, true)) = self.entity(release, &entity_path, &part_of.entity) else {
925 return;
926 };
927 let Some(relation) = part_of.relation else {
928 return;
929 };
930 let required = match (relation, release.version) {
931 (Relation::Aggregates | Relation::Nests, _) => "IfcObjectDefinition",
932 (Relation::AssignsToGroup, _) => "IfcGroup",
933 (Relation::ContainedInSpatialStructure, IfcVersion::Ifc2x3) => {
934 "IfcSpatialStructureElement"
935 }
936 (Relation::ContainedInSpatialStructure, _) => "IfcSpatialElement",
937 (Relation::VoidsElementFillsElement, _) => "IfcElement",
938 };
939 for whole in &wholes {
940 let Some((occurrence, _)) = release.resolve(whole) else {
941 continue;
942 };
943 if !release.schema.is_a(&occurrence, required) {
944 self.report(
945 AuditCode::PartOfRelationEntity,
946 &entity_path,
947 Some(release.version),
948 format!(
949 "{relation} needs a whole that is an {required}; {whole} is not in {}",
950 release.version
951 ),
952 );
953 }
954 }
955 }
956
957 fn value(&mut self, path: &str, version: Option<IfcVersion>, value: &Value, kinds: &[Kind]) {
960 if kinds.is_empty() {
961 return;
962 }
963 let expected = || {
964 kinds
965 .iter()
966 .map(|k| k.describe())
967 .collect::<Vec<_>>()
968 .join(" or ")
969 };
970 match value {
971 Value::Simple(literal) => {
972 if !kinds.iter().any(|k| k.accepts(literal)) {
973 self.report(
974 AuditCode::ValueTypeMismatch,
975 path,
976 version,
977 format!("{literal:?} is not {}", expected()),
978 );
979 }
980 }
981 Value::Restriction(restriction) => {
982 if !restriction.patterns.is_empty() && !kinds.iter().any(|k| k.is_string()) {
983 self.report(
984 AuditCode::PatternOnNonString,
985 path,
986 version,
987 format!("a pattern never matches {}", expected()),
988 );
989 }
990 let literals = restriction.enumeration.iter().chain(
991 [
992 &restriction.min_inclusive,
993 &restriction.max_inclusive,
994 &restriction.min_exclusive,
995 &restriction.max_exclusive,
996 ]
997 .into_iter()
998 .flatten(),
999 );
1000 for literal in literals {
1001 if !kinds.iter().any(|k| k.accepts(literal)) {
1002 self.report(
1003 AuditCode::ValueTypeMismatch,
1004 path,
1005 version,
1006 format!("{literal:?} is not {}", expected()),
1007 );
1008 }
1009 }
1010 }
1011 }
1012 }
1013
1014 fn restriction(&mut self, path: &str, restriction: &Restriction) {
1015 for pattern in &restriction.patterns {
1016 if let Err(reason) = pattern::compile(pattern) {
1017 self.report(
1018 AuditCode::PatternUnverified,
1019 path,
1020 None,
1021 format!("the pattern {pattern:?} was not evaluated: {reason}"),
1022 );
1023 }
1024 }
1025 let number = |v: &Option<String>| v.as_deref().and_then(|v| v.trim().parse::<f64>().ok());
1026 let lower = [
1027 (number(&restriction.min_inclusive), false),
1028 (number(&restriction.min_exclusive), true),
1029 ];
1030 let upper = [
1031 (number(&restriction.max_inclusive), false),
1032 (number(&restriction.max_exclusive), true),
1033 ];
1034 let mut contradiction = None;
1035 for (low, low_exclusive) in lower {
1036 for (high, high_exclusive) in upper {
1037 if let (Some(low), Some(high)) = (low, high) {
1038 let empty = if low_exclusive || high_exclusive {
1039 low >= high
1040 } else {
1041 low > high
1042 };
1043 if empty {
1044 contradiction = Some(format!("no value lies between {low} and {high}"));
1045 }
1046 }
1047 }
1048 }
1049 let lengths = (
1050 restriction.length,
1051 restriction.min_length,
1052 restriction.max_length,
1053 );
1054 match lengths {
1055 (_, Some(min), Some(max)) if min > max => {
1056 contradiction = Some(format!("minLength {min} exceeds maxLength {max}"));
1057 }
1058 (Some(length), Some(min), _) if length < min => {
1059 contradiction = Some(format!("length {length} is below minLength {min}"));
1060 }
1061 (Some(length), _, Some(max)) if length > max => {
1062 contradiction = Some(format!("length {length} exceeds maxLength {max}"));
1063 }
1064 _ => {}
1065 }
1066 if let Some(message) = contradiction {
1067 self.report(AuditCode::RestrictionBoundsContradict, path, None, message);
1068 }
1069 }
1070}
1071
1072fn alternatives(names: &[String]) -> String {
1074 const SHOWN: usize = 5;
1075 let mut text = names
1076 .iter()
1077 .take(SHOWN)
1078 .map(String::as_str)
1079 .collect::<Vec<_>>()
1080 .join(" or ");
1081 if names.len() > SHOWN {
1082 text.push_str(&format!(" (and {} more)", names.len() - SHOWN));
1083 }
1084 text
1085}
1086
1087fn facets(spec: &Specification) -> Vec<(String, &Facet)> {
1089 let mut facets: Vec<(String, &Facet)> = spec
1090 .applicability
1091 .facets
1092 .iter()
1093 .enumerate()
1094 .map(|(i, facet)| (format!("applicability/facets[{i}]"), facet))
1095 .collect();
1096 if let Some(requirements) = &spec.requirements {
1097 facets.extend(
1098 requirements
1099 .facets
1100 .iter()
1101 .enumerate()
1102 .map(|(i, r)| (format!("requirements/facets[{i}]"), &r.facet)),
1103 );
1104 }
1105 facets
1106}
1107
1108fn values(facet: &Facet) -> Vec<(&'static str, &Value)> {
1110 let values: Vec<(&'static str, Option<&Value>)> = match facet {
1111 Facet::Entity(entity) => vec![
1112 ("name", Some(&entity.name)),
1113 ("predefinedType", entity.predefined_type.as_ref()),
1114 ],
1115 Facet::PartOf(part_of) => vec![
1116 ("entity/name", Some(&part_of.entity.name)),
1117 (
1118 "entity/predefinedType",
1119 part_of.entity.predefined_type.as_ref(),
1120 ),
1121 ],
1122 Facet::Classification(classification) => vec![
1123 ("value", classification.value.as_ref()),
1124 ("system", Some(&classification.system)),
1125 ],
1126 Facet::Attribute(attribute) => vec![
1127 ("name", Some(&attribute.name)),
1128 ("value", attribute.value.as_ref()),
1129 ],
1130 Facet::Property(property) => vec![
1131 ("propertySet", Some(&property.property_set)),
1132 ("baseName", Some(&property.base_name)),
1133 ("value", property.value.as_ref()),
1134 ],
1135 Facet::Material(material) => vec![("value", material.value.as_ref())],
1136 };
1137 values
1138 .into_iter()
1139 .filter_map(|(path, value)| value.map(|value| (path, value)))
1140 .collect()
1141}
1142
1143#[cfg(feature = "audit")]
1145fn property_data_type(kind: &PropertyKind) -> Option<String> {
1146 let data_type = match kind {
1147 PropertyKind::SingleValue { data_type }
1148 | PropertyKind::BoundedValue { data_type }
1149 | PropertyKind::ListValue { data_type } => Some(data_type),
1150 PropertyKind::EnumeratedValue { data_type, .. } => data_type.as_ref(),
1151 _ => None,
1152 }?;
1153 data_type.type_name.as_ref().map(|t| t.to_ascii_uppercase())
1154}
1155
1156#[cfg(feature = "audit")]
1158fn quantity_data_type(kind: QuantityKind) -> Option<String> {
1159 let name = match kind {
1160 QuantityKind::Length => "IFCLENGTHMEASURE",
1161 QuantityKind::Area => "IFCAREAMEASURE",
1162 QuantityKind::Volume => "IFCVOLUMEMEASURE",
1163 QuantityKind::Weight => "IFCMASSMEASURE",
1164 QuantityKind::Time => "IFCTIMEMEASURE",
1165 QuantityKind::Count => "IFCCOUNTMEASURE",
1166 QuantityKind::Number => "IFCNUMERICMEASURE",
1167 _ => return None,
1168 };
1169 Some(name.to_owned())
1170}
1171
1172fn is_xs_integer(value: &str) -> bool {
1174 let digits = value.strip_prefix(['+', '-']).unwrap_or(value);
1175 !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit())
1176}
1177
1178fn is_xs_double(value: &str) -> bool {
1180 if matches!(value, "INF" | "+INF" | "-INF" | "NaN") {
1181 return true;
1182 }
1183 let unsigned = value.strip_prefix(['+', '-']).unwrap_or(value);
1184 let (mantissa, exponent) = match unsigned.find(['e', 'E']) {
1185 Some(at) => (&unsigned[..at], Some(&unsigned[at + 1..])),
1186 None => (unsigned, None),
1187 };
1188 let (whole, fraction) = mantissa.split_once('.').unwrap_or((mantissa, ""));
1189 let digits = |s: &str| s.bytes().all(|b| b.is_ascii_digit());
1190 let mantissa_ok =
1191 (!whole.is_empty() || !fraction.is_empty()) && digits(whole) && digits(fraction);
1192 mantissa_ok && exponent.is_none_or(is_xs_integer)
1193}
1194
1195#[cfg(test)]
1196mod tests {
1197 use super::*;
1198
1199 #[test]
1200 fn numbers_follow_the_xsd_lexical_spaces() {
1201 for good in ["42", "+42", "-0", "007"] {
1202 assert!(is_xs_integer(good), "{good}");
1203 }
1204 for bad in ["42.0", "42.", "4 2", "", "+", "1e3"] {
1205 assert!(!is_xs_integer(bad), "{bad}");
1206 }
1207 for good in ["42", "42.", ".5", "-1.5e-3", "1E10", "INF", "-INF", "NaN"] {
1208 assert!(is_xs_double(good), "{good}");
1209 }
1210 for bad in ["42,3", "123,4.5", "", ".", "1e", "e3", "inf", "1.2.3"] {
1211 assert!(!is_xs_double(bad), "{bad}");
1212 }
1213 }
1214}