Skip to main content

ifc_xml/
writer.rs

1//! [`Model`] to ifcXML text.
2//!
3//! # The lossless-encoding problem
4//!
5//! STEP distinguishes `$` (unset), `*` (derived), `.T.` (logical true),
6//! `.ELEMENT.` (enum), `'text'` (string) and `1.` (real) syntactically. XML
7//! attribute values are all just strings, so a naive writer collapses those
8//! distinctions and the round-trip loses type information.
9//!
10//! This writer preserves the distinction structurally: scalars become
11//! attributes, and anything whose *kind* cannot be inferred from an attribute
12//! string is written as a typed child element. That keeps the output readable
13//! for the common case while remaining exactly reversible.
14
15use crate::error::XmlError;
16use crate::scalar::{attribute_text, element_form, format_ref};
17use crate::{slots, XmlCodec, XmlLayout};
18use ifc_model::{Model, Value};
19use std::fmt::Write as _;
20
21const XSI_NAMESPACE: &str = "http://www.w3.org/2001/XMLSchema-instance";
22
23fn reject_non_finite_reals(model: &Model) -> Result<(), XmlError> {
24    for (id, entity) in model.iter() {
25        for (slot, value) in entity.attributes.iter().enumerate() {
26            if let Some(non_finite) = first_non_finite_real(value) {
27                let path = format!("/ifcXML/{}[@id='i{}']/a{}", entity.type_name, id.0, slot);
28                return Err(XmlError::InvalidScalar {
29                    kind: "real".into(),
30                    value: non_finite.to_string(),
31                }
32                .at(path));
33            }
34        }
35    }
36    Ok(())
37}
38
39fn first_non_finite_real(value: &Value) -> Option<f64> {
40    match value {
41        Value::Real(value) if !value.is_finite() => Some(*value),
42        Value::List(values) => values.iter().find_map(first_non_finite_real),
43        Value::Typed { value, .. } => first_non_finite_real(value),
44        _ => None,
45    }
46}
47
48/// Serialize a model as ifcXML.
49pub fn write(codec: &XmlCodec, model: &Model) -> Result<Vec<u8>, XmlError> {
50    if codec.layout() == XmlLayout::Xsd {
51        return Err(XmlError::Unsupported {
52            construct: "writing the XSD configuration; write the native layout instead".into(),
53        });
54    }
55    reject_non_finite_reals(model)?;
56    #[cfg(feature = "schema")]
57    let mut strict = codec.strict_schema().map(crate::typing::Layouts::new);
58    let mut out = String::with_capacity(model.len() * 96);
59    let schema_token = model.header().schema.first().cloned().unwrap_or_default();
60    let namespace = if let Some(profile) = codec.profile() {
61        if schema_token != profile.schema_token() {
62            return Err(XmlError::Profile {
63                expected: profile.schema_token(),
64                found: (!schema_token.is_empty()).then_some(schema_token),
65            });
66        }
67        profile.namespace()
68    } else {
69        "http://www.buildingsmart-tech.org/ifcXML/IFC4/final"
70    };
71
72    out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
73    write!(
74        out,
75        "<ifcXML xmlns=\"{namespace}\" xmlns:xsi=\"{XSI_NAMESPACE}\""
76    )
77    .map_err(fmt_err)?;
78    write_attr(&mut out, "schema", &schema_token);
79    out.push_str(">\n");
80
81    write_header(&mut out, model);
82
83    for (id, entity) in model.iter() {
84        let names = slots::attribute_names(codec, &entity.type_name, entity.attributes.len());
85
86        write!(out, "  <{}", entity.type_name).map_err(fmt_err)?;
87        write_attr(&mut out, "id", &format_ref(id));
88
89        // Scalars become XML attributes; everything else becomes a child.
90        #[cfg(feature = "schema")]
91        let layout = match strict.as_mut() {
92            // A type the schema cannot resolve is one the strict reader
93            // refuses whatever the writer does; write it losslessly anyway.
94            Some(layouts) => layouts
95                .entity(&entity.type_name, false)
96                .ok()
97                .flatten()
98                .filter(|layout| layout.slots.len() >= entity.attributes.len()),
99            None => None,
100        };
101        let mut children: Vec<(usize, &Value)> = Vec::new();
102        for (i, value) in entity.attributes.iter().enumerate() {
103            let text = attribute_text(value);
104            // A strict reader types the text from the slot's declaration, so
105            // an attribute is used only where that typing agrees as well.
106            #[cfg(feature = "schema")]
107            let text = text.filter(|text| {
108                layout.as_ref().is_none_or(|layout| {
109                    let shape = &layout.slots[i].shape;
110                    shape.levels.is_empty()
111                        && crate::typing::scalar(&shape.leaf, text, crate::typing::Lexical::Native)
112                            .is_ok_and(|typed| crate::scalar::same_scalar(&typed, value))
113                })
114            });
115            match text {
116                Some(text) => write_attr(&mut out, &names[i], &text),
117                None => children.push((i, value)),
118            }
119        }
120
121        if children.is_empty() {
122            out.push_str("/>\n");
123        } else {
124            out.push_str(">\n");
125            for (i, value) in children {
126                write_child(&mut out, &names[i], value, 2)?;
127            }
128            writeln!(out, "  </{}>", entity.type_name).map_err(fmt_err)?;
129        }
130    }
131
132    out.push_str("</ifcXML>\n");
133    Ok(out.into_bytes())
134}
135
136fn fmt_err(e: std::fmt::Error) -> XmlError {
137    XmlError::Write(e.to_string())
138}
139
140/// The header, mirroring STEP's `FILE_DESCRIPTION`/`FILE_NAME` content.
141fn write_header(out: &mut String, model: &Model) {
142    let h = model.header();
143    out.push_str("  <header>\n");
144    push_element(out, "name", &h.name);
145    push_element(out, "time_stamp", &h.time_stamp);
146    push_element(out, "preprocessor_version", &h.preprocessor_version);
147    push_element(out, "originating_system", &h.originating_system);
148    push_element(out, "authorization", &h.authorization);
149    for a in &h.author {
150        push_element(out, "author", a);
151    }
152    for o in &h.organization {
153        push_element(out, "organization", o);
154    }
155    for d in &h.description {
156        push_element(out, "description", d);
157    }
158    out.push_str("  </header>\n");
159}
160
161fn push_element(out: &mut String, tag: &str, text: &str) {
162    if text.is_empty() {
163        return;
164    }
165    out.push_str("    <");
166    out.push_str(tag);
167    out.push('>');
168    escape_into(out, text);
169    out.push_str("</");
170    out.push_str(tag);
171    out.push_str(">\n");
172}
173
174/// Write a value that needs its own element to preserve its kind.
175fn write_child(out: &mut String, name: &str, value: &Value, depth: usize) -> Result<(), XmlError> {
176    let pad = "  ".repeat(depth);
177    match value {
178        Value::Null => {
179            writeln!(out, "{pad}<{name} xsi:nil=\"true\"/>").map_err(fmt_err)?;
180        }
181        Value::Derived => {
182            writeln!(out, "{pad}<{name} derived=\"true\"/>").map_err(fmt_err)?;
183        }
184        Value::Typed { type_name, value } => {
185            write!(out, "{pad}<{name} kind=\"typed\"").map_err(fmt_err)?;
186            write_attr(out, "type", type_name);
187            out.push_str(">\n");
188            write_child(out, "value", value, depth + 1)?;
189            writeln!(out, "{pad}</{name}>").map_err(fmt_err)?;
190        }
191        Value::List(items) => {
192            writeln!(out, "{pad}<{name} kind=\"list\">").map_err(fmt_err)?;
193            for item in items {
194                write_child(out, "item", item, depth + 1)?;
195            }
196            writeln!(out, "{pad}</{name}>").map_err(fmt_err)?;
197        }
198        leaf => {
199            // Leaf kinds always carry an explicit `kind` here, including the
200            // text, numbers and references that `attribute_text` refused: an
201            // element without one would be re-read by inference.
202            let (kind, text) = element_form(leaf)
203                .ok_or_else(|| XmlError::Write(format!("no element form for value {leaf:?}")))?;
204            write!(out, "{pad}<{name} kind=\"{kind}\">").map_err(fmt_err)?;
205            escape_into(out, &text);
206            writeln!(out, "</{name}>").map_err(fmt_err)?;
207        }
208    }
209    Ok(())
210}
211
212fn write_attr(out: &mut String, name: &str, value: &str) {
213    out.push(' ');
214    out.push_str(name);
215    out.push_str("=\"");
216    escape_into(out, value);
217    out.push('"');
218}
219
220/// XML-escape into an existing buffer.
221///
222/// Tab, line feed and carriage return are written as character references:
223/// a conforming parser normalizes them to spaces inside attribute values and
224/// folds CR/LF line ends in text, so a literal one would not survive.
225fn escape_into(out: &mut String, text: &str) {
226    for c in text.chars() {
227        match c {
228            '\t' => out.push_str("&#9;"),
229            '\n' => out.push_str("&#10;"),
230            '\r' => out.push_str("&#13;"),
231            '&' => out.push_str("&amp;"),
232            '<' => out.push_str("&lt;"),
233            '>' => out.push_str("&gt;"),
234            '"' => out.push_str("&quot;"),
235            '\'' => out.push_str("&apos;"),
236            c => out.push(c),
237        }
238    }
239}