Skip to main content

ifc_xml/
reader.rs

1//! ifcXML text to [`Model`].
2//!
3//! Uses `quick-xml`'s pull parser: an IFC file can be very large, so the
4//! document is never materialized as a tree.
5//!
6//! What happens to content the schema does not describe depends on the
7//! codec, and the distinction is deliberate:
8//!
9//! - **Native layout, no schema or [`SchemaReading::Lenient`]:** unknown
10//!   elements and attributes are preserved rather than rejected, on the same
11//!   principle as the STEP reader: a file containing entities from a schema
12//!   we do not know must still round-trip. Value kinds are inferred from
13//!   attribute text.
14//! - **Native layout with a schema, [`SchemaReading::Strict`] (the
15//!   default):** every value is typed from its attribute's declaration, and
16//!   an entity, attribute or value the schema does not declare is a typed
17//!   [`XmlError`] naming it.
18//! - **[`XmlLayout::Xsd`]:** the buildingSMART configuration, always strict;
19//!   see the crate documentation.
20//!
21//! [`SchemaReading::Lenient`]: crate::SchemaReading::Lenient
22//! [`SchemaReading::Strict`]: crate::SchemaReading::Strict
23
24use crate::error::XmlError;
25use crate::scalar::{decode_element, format_ref, parse_ref};
26use crate::slots::Raw;
27use crate::{slots, XmlCodec, XmlLayout};
28use ifc_model::{Entity, EntityId, Model, Value};
29use quick_xml::escape::unescape;
30use quick_xml::events::attributes::Attribute;
31use quick_xml::events::{BytesRef, BytesStart, Event};
32use quick_xml::name::ResolveResult;
33use quick_xml::reader::NsReader;
34
35const XSI_NAMESPACE: &str = "http://www.w3.org/2001/XMLSchema-instance";
36
37/// Cheap sniff: does this look like an XML document?
38pub fn looks_like_xml(bytes: &[u8]) -> bool {
39    let head = &bytes[..bytes.len().min(512)];
40    let text = String::from_utf8_lossy(head);
41    let trimmed = text.trim_start_matches(['\u{feff}', ' ', '\n', '\r', '\t']);
42    // A prefixed root (`<ifc:ifcXML`), as the buildingSMART examples write.
43    let prefixed = trimmed.starts_with('<')
44        && trimmed[1..]
45            .split(|c: char| c.is_whitespace() || c == '>')
46            .next()
47            .is_some_and(|name| name.ends_with(":ifcXML"));
48    trimmed.starts_with("<?xml") || trimmed.starts_with("<ifcXML") || prefixed
49}
50
51/// The schema tables a strict native read types values from.
52#[cfg(feature = "schema")]
53type Strict<'s> = Option<crate::typing::Layouts<'s>>;
54#[cfg(not(feature = "schema"))]
55type Strict<'s> = Option<std::marker::PhantomData<&'s ()>>;
56
57/// Parse an ifcXML document into a model, in the codec's layout.
58///
59/// Unlike [`ifc_model::Codec::read_bytes`], which flattens failures into a
60/// [`ifc_model::ModelError`], this keeps the typed [`XmlError`] and its path.
61pub fn read(codec: &XmlCodec, bytes: &[u8]) -> Result<Model, XmlError> {
62    if codec.layout() == XmlLayout::Xsd {
63        return read_xsd(codec, bytes);
64    }
65    #[cfg(feature = "schema")]
66    let mut strict: Strict<'_> = codec.strict_schema().map(crate::typing::Layouts::new);
67    #[cfg(not(feature = "schema"))]
68    let mut strict: Strict<'_> = None;
69    let model = read_native(codec, bytes, &mut strict)?;
70    #[cfg(feature = "schema")]
71    if let Some(layouts) = strict.as_mut() {
72        check_references(layouts, &model)?;
73    }
74    Ok(model)
75}
76
77#[cfg(feature = "schema")]
78fn read_xsd(codec: &XmlCodec, bytes: &[u8]) -> Result<Model, XmlError> {
79    match (codec.schema(), codec.profile()) {
80        (Some(schema), Some(profile)) => crate::xsd::read(schema, profile, bytes),
81        _ => Err(XmlError::Unsupported {
82            construct: "the XSD layout without a schema and release profile".into(),
83        }),
84    }
85}
86
87#[cfg(not(feature = "schema"))]
88fn read_xsd(_: &XmlCodec, _: &[u8]) -> Result<Model, XmlError> {
89    Err(XmlError::Unsupported {
90        construct: "the XSD layout without the `schema` feature".into(),
91    })
92}
93
94/// Every reference of a strictly read model resolves to an entity its
95/// declared type admits.
96#[cfg(feature = "schema")]
97fn check_references(
98    layouts: &mut crate::typing::Layouts<'_>,
99    model: &Model,
100) -> Result<(), XmlError> {
101    let schema = layouts.schema();
102    let mut references = Vec::new();
103    for (id, entity) in model.iter() {
104        let Some(layout) = layouts.entity(&entity.type_name, false)? else {
105            continue;
106        };
107        for (value, slot) in entity.attributes.iter().zip(&layout.slots) {
108            let path = format!(
109                "{}/{}",
110                raw_entity_path(&entity.type_name, Some(format_ref(id))),
111                slot.name
112            );
113            references.clear();
114            crate::typing::references(schema, &slot.shape, value, &mut references)
115                .map_err(|error| error.at(path.clone()))?;
116            for (target, declared) in &references {
117                let Some(found) = model.get(*target) else {
118                    return Err(XmlError::UnresolvedReference {
119                        id: format_ref(*target),
120                    }
121                    .at(path));
122                };
123                if !schema.accepts_type(declared, &found.type_name) {
124                    return Err(XmlError::TypeMismatch {
125                        declared: declared.to_string(),
126                        found: format!("a reference to an entity `{}`", found.type_name),
127                    }
128                    .at(path));
129                }
130            }
131        }
132    }
133    Ok(())
134}
135
136fn read_native(codec: &XmlCodec, bytes: &[u8], strict: &mut Strict<'_>) -> Result<Model, XmlError> {
137    // Text is NOT trimmed: a value element's text is the value, and leading
138    // or trailing whitespace in a string is data. Indentation between
139    // elements lands in `text_buf` too, but every value start clears it and
140    // only a value end consumes it.
141    let mut reader = NsReader::from_reader(bytes);
142
143    let mut model = Model::new();
144    let mut buf = Vec::new();
145    let mut seen_root = false;
146    let mut root_closed = false;
147    let mut element_depth = 0usize;
148
149    // Header parsing state.
150    let mut in_header = false;
151    let mut header_tag: Option<String> = None;
152
153    // Entity parsing state.
154    let mut current: Option<PendingEntity> = None;
155    // Stack of open child-value elements: (name, kind, type, accumulated items)
156    let mut stack: Vec<PendingValue> = Vec::new();
157    let mut text_buf = String::new();
158
159    loop {
160        match reader.read_resolved_event_into(&mut buf) {
161            Err(error) => {
162                return Err(
163                    XmlError::Malformed(error.to_string()).at(current_path(&current, &stack, None))
164                );
165            }
166            Ok((_, Event::Eof)) => break,
167
168            Ok((namespace, Event::Start(e))) => {
169                let name = local_name(&e);
170                validate_element(codec, namespace, &name)?;
171                validate_root(codec, &e, &name, element_depth, root_closed, &mut seen_root)?;
172                element_depth += 1;
173                match name.as_str() {
174                    "ifcXML" => {
175                        if let Some(schema) = attr_value(&e, "schema") {
176                            model.header_mut().schema = vec![schema];
177                        }
178                    }
179                    "header" => in_header = true,
180                    _ if in_header => {
181                        header_tag = Some(name);
182                        text_buf.clear();
183                    }
184                    _ if current.is_none() => match start_entity(&e, &name) {
185                        Ok(Some(started)) => current = Some(started),
186                        Ok(None) => {}
187                        Err(error) => {
188                            return Err(error.at(raw_entity_path(&name, attr_value(&e, "id"))));
189                        }
190                    },
191                    _ => {
192                        let item_index = stack.last().map_or(0, |parent| parent.items.len());
193                        stack.push(PendingValue::from_start(&e, name, item_index));
194                        text_buf.clear();
195                    }
196                }
197            }
198
199            Ok((namespace, Event::Empty(e))) => {
200                let name = local_name(&e);
201                validate_element(codec, namespace, &name)?;
202                validate_root(codec, &e, &name, element_depth, root_closed, &mut seen_root)?;
203                if codec.profile().is_some() && element_depth == 0 {
204                    root_closed = true;
205                }
206                if current.is_none() && !in_header {
207                    match start_entity(&e, &name) {
208                        Ok(Some(entity)) => finish_entity(codec, &mut model, entity, strict)?,
209                        Ok(None) => {}
210                        Err(error) => {
211                            return Err(error.at(raw_entity_path(&name, attr_value(&e, "id"))));
212                        }
213                    }
214                } else if current.is_some() {
215                    let value = if has_true_xsi_nil(&reader, &e) {
216                        Value::Null
217                    } else if attr_value(&e, "derived").as_deref() == Some("true") {
218                        Value::Derived
219                    } else {
220                        let item_index = stack.last().map_or(0, |parent| parent.items.len());
221                        let pending = PendingValue::from_start(&e, name.clone(), item_index);
222                        let path =
223                            current_path(&current, &stack, Some(pending.path_segment.as_str()));
224                        pending.finish("").map_err(|error| error.at(path))?
225                    };
226                    push_value(&mut stack, &mut current, name, value);
227                }
228            }
229
230            // Text arrives verbatim (no end-of-line normalisation) and each
231            // entity or character reference as its own event; concatenating
232            // both reproduces the unescaped text of the whole run.
233            Ok((_, Event::Text(t))) => text_buf.push_str(&t),
234
235            Ok((_, Event::GeneralRef(reference))) => {
236                let resolved = resolve_reference(&reference).map_err(|error| {
237                    XmlError::Malformed(error.to_string()).at(current_path(&current, &stack, None))
238                })?;
239                text_buf.push_str(&resolved);
240            }
241
242            Ok((namespace, Event::End(e))) => {
243                let name = e.local_name().as_ref().to_owned();
244                validate_element(codec, namespace, &name)?;
245                element_depth = element_depth.saturating_sub(1);
246                if codec.profile().is_some() && element_depth == 0 {
247                    root_closed = true;
248                }
249                match name.as_str() {
250                    "header" => in_header = false,
251                    "ifcXML" => {}
252                    _ if in_header => {
253                        if let Some(tag) = header_tag.take() {
254                            apply_header_field(&mut model, &tag, &text_buf);
255                        }
256                        text_buf.clear();
257                    }
258                    _ => {
259                        if let Some(pending) = stack.pop() {
260                            let path =
261                                current_path(&current, &stack, Some(pending.path_segment.as_str()));
262                            let value =
263                                pending.finish(&text_buf).map_err(|error| error.at(path))?;
264                            push_value(&mut stack, &mut current, pending.name.clone(), value);
265                            text_buf.clear();
266                        } else if let Some(entity) = current.take() {
267                            finish_entity(codec, &mut model, entity, strict)?;
268                        }
269                        text_buf.clear();
270                    }
271                }
272            }
273            Ok(_) => {}
274        }
275        buf.clear();
276    }
277
278    if codec.profile().is_some() && (!seen_root || !root_closed) {
279        return Err(XmlError::Root { found: None });
280    }
281
282    Ok(model)
283}
284
285fn validate_element(
286    codec: &XmlCodec,
287    namespace: ResolveResult<'_>,
288    element: &str,
289) -> Result<(), XmlError> {
290    let Some(profile) = codec.profile() else {
291        return Ok(());
292    };
293    let found = match namespace {
294        ResolveResult::Unbound => None,
295        ResolveResult::Bound(namespace) => Some(namespace.as_ref().to_owned()),
296        ResolveResult::Unknown(prefix) => Some(format!("unresolved prefix `{prefix}`")),
297    };
298    if found.as_deref() != Some(profile.namespace()) {
299        return Err(XmlError::Namespace {
300            element: element.into(),
301            expected: profile.namespace(),
302            found,
303        });
304    }
305    Ok(())
306}
307
308fn validate_root(
309    codec: &XmlCodec,
310    element: &BytesStart<'_>,
311    name: &str,
312    depth: usize,
313    root_closed: bool,
314    seen_root: &mut bool,
315) -> Result<(), XmlError> {
316    let Some(profile) = codec.profile() else {
317        return Ok(());
318    };
319    if *seen_root {
320        if depth == 0 || root_closed || name == "ifcXML" {
321            return Err(XmlError::Root {
322                found: Some(name.into()),
323            });
324        }
325        return Ok(());
326    }
327    if depth != 0 || name != "ifcXML" {
328        return Err(XmlError::Root {
329            found: Some(name.into()),
330        });
331    }
332    *seen_root = true;
333    let found = attr_value(element, "schema");
334    if found.as_deref() != Some(profile.schema_token()) {
335        return Err(XmlError::Profile {
336            expected: profile.schema_token(),
337            found,
338        });
339    }
340    Ok(())
341}
342
343/// An entity being assembled: its id, type name, and named attributes.
344///
345/// Named rather than an inline tuple because it threads through four
346/// functions; clippy flags the raw form as too complex, and it is right.
347struct PendingEntity {
348    id: EntityId,
349    type_name: String,
350    attrs: Vec<(String, Raw)>,
351}
352
353/// A child element whose value is still being accumulated.
354struct PendingValue {
355    name: String,
356    path_segment: String,
357    kind: String,
358    type_name: Option<String>,
359    items: Vec<Value>,
360}
361
362impl PendingValue {
363    fn from_start(e: &BytesStart<'_>, name: String, item_index: usize) -> Self {
364        let path_segment = if name == "item" {
365            format!("item[{item_index}]")
366        } else {
367            name.clone()
368        };
369        Self {
370            name,
371            path_segment,
372            kind: attr_value(e, "kind").unwrap_or_default(),
373            type_name: attr_value(e, "type"),
374            items: Vec::new(),
375        }
376    }
377
378    fn finish(&self, text: &str) -> Result<Value, XmlError> {
379        if let Some(value) = decode_element(&self.kind, text)? {
380            return Ok(value);
381        }
382        let value = if self.kind == "list" {
383            Value::List(self.items.clone())
384        } else {
385            let inner = self.items.first().cloned().unwrap_or(Value::Null);
386            Value::Typed {
387                type_name: self.type_name.clone().unwrap_or_default().into(),
388                value: Box::new(inner),
389            }
390        };
391        Ok(value)
392    }
393}
394
395/// Attach a finished value to its parent: an open list, or the entity.
396fn push_value(
397    stack: &mut [PendingValue],
398    current: &mut Option<PendingEntity>,
399    name: String,
400    value: Value,
401) {
402    if let Some(parent) = stack.last_mut() {
403        parent.items.push(value);
404        return;
405    }
406    if let Some(entity) = current.as_mut() {
407        entity.attrs.push((name, Raw::Value(value)));
408    }
409}
410
411/// Begin an entity element, reading its scalar attributes.
412fn start_entity(e: &BytesStart<'_>, name: &str) -> Result<Option<PendingEntity>, XmlError> {
413    let Some(id_text) = attr_value(e, "id") else {
414        return Ok(None);
415    };
416    let id = parse_ref(&id_text).ok_or_else(|| XmlError::BadId(id_text.clone()))?;
417    let mut attrs = Vec::new();
418    for attr in e.attributes().flatten() {
419        let key = attr.key.local_name().as_ref().to_owned();
420        if key == "id" {
421            continue;
422        }
423        let value = unescape_attribute(&attr)
424            .map(|value| value.into_owned())
425            .map_err(|error| XmlError::Malformed(error.to_string()))?;
426        attrs.push((key, Raw::Text(value)));
427    }
428    Ok(Some(PendingEntity {
429        id,
430        type_name: name.to_string(),
431        attrs,
432    }))
433}
434
435/// Store a completed entity, placing each named value in its slot.
436///
437/// Strictly read, each value is typed from its slot's declaration; else
438/// an attribute's kind is inferred from its text.
439fn finish_entity(
440    codec: &XmlCodec,
441    model: &mut Model,
442    entity: PendingEntity,
443    strict: &mut Strict<'_>,
444) -> Result<(), XmlError> {
445    let path = raw_entity_path(&entity.type_name, Some(format_ref(entity.id)));
446    #[cfg(feature = "schema")]
447    if let Some(layouts) = strict.as_mut() {
448        let values = slots::order_strict(layouts, &entity.type_name, &path, entity.attrs)
449            .map_err(|error| error.at(path))?;
450        model.insert(entity.id, Entity::new(entity.type_name, values));
451        return Ok(());
452    }
453    #[cfg(not(feature = "schema"))]
454    let _ = strict;
455    let values =
456        slots::order(codec, &entity.type_name, entity.attrs).map_err(|error| error.at(path))?;
457    model.insert(entity.id, Entity::new(entity.type_name, values));
458    Ok(())
459}
460
461fn raw_entity_path(type_name: &str, id: Option<String>) -> String {
462    match id {
463        Some(id) => format!("/ifcXML/{type_name}[@id='{id}']"),
464        None => format!("/ifcXML/{type_name}"),
465    }
466}
467
468fn current_path(
469    current: &Option<PendingEntity>,
470    stack: &[PendingValue],
471    leaf: Option<&str>,
472) -> String {
473    let mut path = String::from("/ifcXML");
474    if let Some(entity) = current {
475        path.push('/');
476        path.push_str(&entity.type_name);
477        path.push_str(&format!("[@id='i{}']", entity.id.0));
478    }
479    for value in stack {
480        path.push('/');
481        path.push_str(&value.path_segment);
482    }
483    if let Some(leaf) = leaf {
484        path.push('/');
485        path.push_str(leaf);
486    }
487    path
488}
489
490fn local_name(e: &BytesStart<'_>) -> String {
491    e.local_name().as_ref().to_owned()
492}
493
494/// An attribute value with entity and character references replaced, and
495/// nothing else changed.
496///
497/// Deliberately not `Attribute::normalized_value`: XML attribute-value
498/// normalisation would turn literal tabs and line breaks into spaces, and a
499/// string attribute's whitespace is data the reader has always preserved.
500fn unescape_attribute<'a>(
501    attribute: &'a Attribute<'_>,
502) -> Result<std::borrow::Cow<'a, str>, quick_xml::escape::EscapeError> {
503    unescape(&attribute.value)
504}
505
506/// The text a `&name;` or `&#N;` reference in element content stands for:
507/// one of the five predefined entities or a character reference. Any other
508/// entity is refused, as no DTD is processed.
509fn resolve_reference(reference: &BytesRef<'_>) -> Result<String, quick_xml::escape::EscapeError> {
510    unescape(&format!("&{};", &**reference)).map(std::borrow::Cow::into_owned)
511}
512
513fn has_true_xsi_nil(reader: &NsReader<&[u8]>, element: &BytesStart<'_>) -> bool {
514    element.attributes().flatten().any(|attribute| {
515        if attribute.key.local_name().as_ref() != "nil" {
516            return false;
517        }
518        let (namespace, _) = reader.resolver().resolve_attribute(attribute.key);
519        matches!(
520            namespace,
521            ResolveResult::Bound(namespace) if namespace.as_ref() == XSI_NAMESPACE
522        ) && unescape_attribute(&attribute).is_ok_and(|value| value.as_ref() == "true")
523    })
524}
525
526fn attr_value(e: &BytesStart<'_>, key: &str) -> Option<String> {
527    e.attributes().flatten().find_map(|a| {
528        (a.key.local_name().as_ref() == key)
529            .then(|| unescape_attribute(&a).map(|v| v.into_owned()).ok())
530            .flatten()
531    })
532}
533
534fn apply_header_field(model: &mut Model, tag: &str, text: &str) {
535    let h = model.header_mut();
536    match tag {
537        "name" => h.name = text.to_string(),
538        "time_stamp" => h.time_stamp = text.to_string(),
539        "preprocessor_version" => h.preprocessor_version = text.to_string(),
540        "originating_system" => h.originating_system = text.to_string(),
541        "authorization" => h.authorization = text.to_string(),
542        "author" => h.author.push(text.to_string()),
543        "organization" => h.organization.push(text.to_string()),
544        "description" => h.description.push(text.to_string()),
545        _ => {}
546    }
547}