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