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