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//! Unknown elements and attributes are preserved rather than rejected, on the
7//! same principle as the STEP reader: a file containing entities from a
8//! schema we do not know must still round-trip.
9
10use crate::error::XmlError;
11use crate::scalar::{decode_element, format_ref, infer, parse_ref};
12use crate::{slots, XmlCodec};
13use ifc_model::{Entity, EntityId, Model, Value};
14use quick_xml::escape::unescape;
15use quick_xml::events::attributes::Attribute;
16use quick_xml::events::{BytesRef, BytesStart, Event};
17use quick_xml::name::ResolveResult;
18use quick_xml::reader::NsReader;
19
20const XSI_NAMESPACE: &str = "http://www.w3.org/2001/XMLSchema-instance";
21
22/// Cheap sniff: does this look like an XML document?
23pub fn looks_like_xml(bytes: &[u8]) -> bool {
24    let head = &bytes[..bytes.len().min(512)];
25    let text = String::from_utf8_lossy(head);
26    let trimmed = text.trim_start_matches(['\u{feff}', ' ', '\n', '\r', '\t']);
27    trimmed.starts_with("<?xml") || trimmed.starts_with("<ifcXML")
28}
29
30/// Parse an ifcXML document into a model.
31pub fn read(codec: &XmlCodec, bytes: &[u8]) -> Result<Model, XmlError> {
32    // Text is NOT trimmed: a value element's text is the value, and leading
33    // or trailing whitespace in a string is data. Indentation between
34    // elements lands in `text_buf` too, but every value start clears it and
35    // only a value end consumes it.
36    let mut reader = NsReader::from_reader(bytes);
37
38    let mut model = Model::new();
39    let mut buf = Vec::new();
40    let mut seen_root = false;
41    let mut root_closed = false;
42    let mut element_depth = 0usize;
43
44    // Header parsing state.
45    let mut in_header = false;
46    let mut header_tag: Option<String> = None;
47
48    // Entity parsing state.
49    let mut current: Option<PendingEntity> = None;
50    // Stack of open child-value elements: (name, kind, type, accumulated items)
51    let mut stack: Vec<PendingValue> = Vec::new();
52    let mut text_buf = String::new();
53
54    loop {
55        match reader.read_resolved_event_into(&mut buf) {
56            Err(error) => {
57                return Err(
58                    XmlError::Malformed(error.to_string()).at(current_path(&current, &stack, None))
59                );
60            }
61            Ok((_, Event::Eof)) => break,
62
63            Ok((namespace, Event::Start(e))) => {
64                let name = local_name(&e);
65                validate_element(codec, namespace, &name)?;
66                validate_root(codec, &e, &name, element_depth, root_closed, &mut seen_root)?;
67                element_depth += 1;
68                match name.as_str() {
69                    "ifcXML" => {
70                        if let Some(schema) = attr_value(&e, "schema") {
71                            model.header_mut().schema = vec![schema];
72                        }
73                    }
74                    "header" => in_header = true,
75                    _ if in_header => {
76                        header_tag = Some(name);
77                        text_buf.clear();
78                    }
79                    _ if current.is_none() => match start_entity(&e, &name) {
80                        Ok(Some(started)) => current = Some(started),
81                        Ok(None) => {}
82                        Err(error) => {
83                            return Err(error.at(raw_entity_path(&name, attr_value(&e, "id"))));
84                        }
85                    },
86                    _ => {
87                        let item_index = stack.last().map_or(0, |parent| parent.items.len());
88                        stack.push(PendingValue::from_start(&e, name, item_index));
89                        text_buf.clear();
90                    }
91                }
92            }
93
94            Ok((namespace, Event::Empty(e))) => {
95                let name = local_name(&e);
96                validate_element(codec, namespace, &name)?;
97                validate_root(codec, &e, &name, element_depth, root_closed, &mut seen_root)?;
98                if codec.profile().is_some() && element_depth == 0 {
99                    root_closed = true;
100                }
101                if current.is_none() && !in_header {
102                    match start_entity(&e, &name) {
103                        Ok(Some(entity)) => finish_entity(codec, &mut model, entity)?,
104                        Ok(None) => {}
105                        Err(error) => {
106                            return Err(error.at(raw_entity_path(&name, attr_value(&e, "id"))));
107                        }
108                    }
109                } else if current.is_some() {
110                    let value = if has_true_xsi_nil(&reader, &e) {
111                        Value::Null
112                    } else if attr_value(&e, "derived").as_deref() == Some("true") {
113                        Value::Derived
114                    } else {
115                        let item_index = stack.last().map_or(0, |parent| parent.items.len());
116                        let pending = PendingValue::from_start(&e, name.clone(), item_index);
117                        let path =
118                            current_path(&current, &stack, Some(pending.path_segment.as_str()));
119                        pending.finish("").map_err(|error| error.at(path))?
120                    };
121                    push_value(&mut stack, &mut current, name, value);
122                }
123            }
124
125            // Text arrives verbatim (no end-of-line normalisation) and each
126            // entity or character reference as its own event; concatenating
127            // both reproduces the unescaped text of the whole run.
128            Ok((_, Event::Text(t))) => text_buf.push_str(&t),
129
130            Ok((_, Event::GeneralRef(reference))) => {
131                let resolved = resolve_reference(&reference).map_err(|error| {
132                    XmlError::Malformed(error.to_string()).at(current_path(&current, &stack, None))
133                })?;
134                text_buf.push_str(&resolved);
135            }
136
137            Ok((namespace, Event::End(e))) => {
138                let name = e.local_name().as_ref().to_owned();
139                validate_element(codec, namespace, &name)?;
140                element_depth = element_depth.saturating_sub(1);
141                if codec.profile().is_some() && element_depth == 0 {
142                    root_closed = true;
143                }
144                match name.as_str() {
145                    "header" => in_header = false,
146                    "ifcXML" => {}
147                    _ if in_header => {
148                        if let Some(tag) = header_tag.take() {
149                            apply_header_field(&mut model, &tag, &text_buf);
150                        }
151                        text_buf.clear();
152                    }
153                    _ => {
154                        if let Some(pending) = stack.pop() {
155                            let path =
156                                current_path(&current, &stack, Some(pending.path_segment.as_str()));
157                            let value =
158                                pending.finish(&text_buf).map_err(|error| error.at(path))?;
159                            push_value(&mut stack, &mut current, pending.name.clone(), value);
160                            text_buf.clear();
161                        } else if let Some(entity) = current.take() {
162                            finish_entity(codec, &mut model, entity)?;
163                        }
164                        text_buf.clear();
165                    }
166                }
167            }
168            Ok(_) => {}
169        }
170        buf.clear();
171    }
172
173    if codec.profile().is_some() && (!seen_root || !root_closed) {
174        return Err(XmlError::Root { found: None });
175    }
176
177    Ok(model)
178}
179
180fn validate_element(
181    codec: &XmlCodec,
182    namespace: ResolveResult<'_>,
183    element: &str,
184) -> Result<(), XmlError> {
185    let Some(profile) = codec.profile() else {
186        return Ok(());
187    };
188    let found = match namespace {
189        ResolveResult::Unbound => None,
190        ResolveResult::Bound(namespace) => Some(namespace.as_ref().to_owned()),
191        ResolveResult::Unknown(prefix) => Some(format!("unresolved prefix `{prefix}`")),
192    };
193    if found.as_deref() != Some(profile.namespace()) {
194        return Err(XmlError::Namespace {
195            element: element.into(),
196            expected: profile.namespace(),
197            found,
198        });
199    }
200    Ok(())
201}
202
203fn validate_root(
204    codec: &XmlCodec,
205    element: &BytesStart<'_>,
206    name: &str,
207    depth: usize,
208    root_closed: bool,
209    seen_root: &mut bool,
210) -> Result<(), XmlError> {
211    let Some(profile) = codec.profile() else {
212        return Ok(());
213    };
214    if *seen_root {
215        if depth == 0 || root_closed || name == "ifcXML" {
216            return Err(XmlError::Root {
217                found: Some(name.into()),
218            });
219        }
220        return Ok(());
221    }
222    if depth != 0 || name != "ifcXML" {
223        return Err(XmlError::Root {
224            found: Some(name.into()),
225        });
226    }
227    *seen_root = true;
228    let found = attr_value(element, "schema");
229    if found.as_deref() != Some(profile.schema_token()) {
230        return Err(XmlError::Profile {
231            expected: profile.schema_token(),
232            found,
233        });
234    }
235    Ok(())
236}
237
238/// An entity being assembled: its id, type name, and named attributes.
239///
240/// Named rather than an inline tuple because it threads through four
241/// functions; clippy flags the raw form as too complex, and it is right.
242struct PendingEntity {
243    id: EntityId,
244    type_name: String,
245    attrs: Vec<(String, Value)>,
246}
247
248/// A child element whose value is still being accumulated.
249struct PendingValue {
250    name: String,
251    path_segment: String,
252    kind: String,
253    type_name: Option<String>,
254    items: Vec<Value>,
255}
256
257impl PendingValue {
258    fn from_start(e: &BytesStart<'_>, name: String, item_index: usize) -> Self {
259        let path_segment = if name == "item" {
260            format!("item[{item_index}]")
261        } else {
262            name.clone()
263        };
264        Self {
265            name,
266            path_segment,
267            kind: attr_value(e, "kind").unwrap_or_default(),
268            type_name: attr_value(e, "type"),
269            items: Vec::new(),
270        }
271    }
272
273    fn finish(&self, text: &str) -> Result<Value, XmlError> {
274        if let Some(value) = decode_element(&self.kind, text)? {
275            return Ok(value);
276        }
277        let value = if self.kind == "list" {
278            Value::List(self.items.clone())
279        } else {
280            let inner = self.items.first().cloned().unwrap_or(Value::Null);
281            Value::Typed {
282                type_name: self.type_name.clone().unwrap_or_default().into(),
283                value: Box::new(inner),
284            }
285        };
286        Ok(value)
287    }
288}
289
290/// Attach a finished value to its parent: an open list, or the entity.
291fn push_value(
292    stack: &mut [PendingValue],
293    current: &mut Option<PendingEntity>,
294    name: String,
295    value: Value,
296) {
297    if let Some(parent) = stack.last_mut() {
298        parent.items.push(value);
299        return;
300    }
301    if let Some(entity) = current.as_mut() {
302        entity.attrs.push((name, value));
303    }
304}
305
306/// Begin an entity element, reading its scalar attributes.
307fn start_entity(e: &BytesStart<'_>, name: &str) -> Result<Option<PendingEntity>, XmlError> {
308    let Some(id_text) = attr_value(e, "id") else {
309        return Ok(None);
310    };
311    let id = parse_ref(&id_text).ok_or_else(|| XmlError::BadId(id_text.clone()))?;
312    let mut attrs = Vec::new();
313    for attr in e.attributes().flatten() {
314        let key = attr.key.local_name().as_ref().to_owned();
315        if key == "id" {
316            continue;
317        }
318        let value = unescape_attribute(&attr)
319            .map(|value| value.into_owned())
320            .map_err(|error| XmlError::Malformed(error.to_string()))?;
321        attrs.push((key, infer(&value)));
322    }
323    Ok(Some(PendingEntity {
324        id,
325        type_name: name.to_string(),
326        attrs,
327    }))
328}
329
330/// Store a completed entity, placing each named value in its slot.
331fn finish_entity(
332    codec: &XmlCodec,
333    model: &mut Model,
334    entity: PendingEntity,
335) -> Result<(), XmlError> {
336    let values = slots::order(codec, &entity.type_name, entity.attrs).map_err(|error| {
337        error.at(raw_entity_path(
338            &entity.type_name,
339            Some(format_ref(entity.id)),
340        ))
341    })?;
342    model.insert(entity.id, Entity::new(entity.type_name, values));
343    Ok(())
344}
345
346fn raw_entity_path(type_name: &str, id: Option<String>) -> String {
347    match id {
348        Some(id) => format!("/ifcXML/{type_name}[@id='{id}']"),
349        None => format!("/ifcXML/{type_name}"),
350    }
351}
352
353fn current_path(
354    current: &Option<PendingEntity>,
355    stack: &[PendingValue],
356    leaf: Option<&str>,
357) -> String {
358    let mut path = String::from("/ifcXML");
359    if let Some(entity) = current {
360        path.push('/');
361        path.push_str(&entity.type_name);
362        path.push_str(&format!("[@id='i{}']", entity.id.0));
363    }
364    for value in stack {
365        path.push('/');
366        path.push_str(&value.path_segment);
367    }
368    if let Some(leaf) = leaf {
369        path.push('/');
370        path.push_str(leaf);
371    }
372    path
373}
374
375fn local_name(e: &BytesStart<'_>) -> String {
376    e.local_name().as_ref().to_owned()
377}
378
379/// An attribute value with entity and character references replaced, and
380/// nothing else changed.
381///
382/// Deliberately not `Attribute::normalized_value`: XML attribute-value
383/// normalisation would turn literal tabs and line breaks into spaces, and a
384/// string attribute's whitespace is data the reader has always preserved.
385fn unescape_attribute<'a>(
386    attribute: &'a Attribute<'_>,
387) -> Result<std::borrow::Cow<'a, str>, quick_xml::escape::EscapeError> {
388    unescape(&attribute.value)
389}
390
391/// The text a `&name;` or `&#N;` reference in element content stands for:
392/// one of the five predefined entities or a character reference. Any other
393/// entity is refused, as no DTD is processed.
394fn resolve_reference(reference: &BytesRef<'_>) -> Result<String, quick_xml::escape::EscapeError> {
395    unescape(&format!("&{};", &**reference)).map(std::borrow::Cow::into_owned)
396}
397
398fn has_true_xsi_nil(reader: &NsReader<&[u8]>, element: &BytesStart<'_>) -> bool {
399    element.attributes().flatten().any(|attribute| {
400        if attribute.key.local_name().as_ref() != "nil" {
401            return false;
402        }
403        let (namespace, _) = reader.resolver().resolve_attribute(attribute.key);
404        matches!(
405            namespace,
406            ResolveResult::Bound(namespace) if namespace.as_ref() == XSI_NAMESPACE
407        ) && unescape_attribute(&attribute).is_ok_and(|value| value.as_ref() == "true")
408    })
409}
410
411fn attr_value(e: &BytesStart<'_>, key: &str) -> Option<String> {
412    e.attributes().flatten().find_map(|a| {
413        (a.key.local_name().as_ref() == key)
414            .then(|| unescape_attribute(&a).map(|v| v.into_owned()).ok())
415            .flatten()
416    })
417}
418
419fn apply_header_field(model: &mut Model, tag: &str, text: &str) {
420    let h = model.header_mut();
421    match tag {
422        "name" => h.name = text.to_string(),
423        "time_stamp" => h.time_stamp = text.to_string(),
424        "preprocessor_version" => h.preprocessor_version = text.to_string(),
425        "originating_system" => h.originating_system = text.to_string(),
426        "authorization" => h.authorization = text.to_string(),
427        "author" => h.author.push(text.to_string()),
428        "organization" => h.organization.push(text.to_string()),
429        "description" => h.description.push(text.to_string()),
430        _ => {}
431    }
432}