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};
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    reject_non_finite_reals(model)?;
51    let mut out = String::with_capacity(model.len() * 96);
52    let schema_token = model.header().schema.first().cloned().unwrap_or_default();
53    let namespace = if let Some(profile) = codec.profile() {
54        if schema_token != profile.schema_token() {
55            return Err(XmlError::Profile {
56                expected: profile.schema_token(),
57                found: (!schema_token.is_empty()).then_some(schema_token),
58            });
59        }
60        profile.namespace()
61    } else {
62        "http://www.buildingsmart-tech.org/ifcXML/IFC4/final"
63    };
64
65    out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
66    write!(
67        out,
68        "<ifcXML xmlns=\"{namespace}\" xmlns:xsi=\"{XSI_NAMESPACE}\""
69    )
70    .map_err(fmt_err)?;
71    write_attr(&mut out, "schema", &schema_token);
72    out.push_str(">\n");
73
74    write_header(&mut out, model);
75
76    for (id, entity) in model.iter() {
77        let names = slots::attribute_names(codec, &entity.type_name, entity.attributes.len());
78
79        write!(out, "  <{}", entity.type_name).map_err(fmt_err)?;
80        write_attr(&mut out, "id", &format_ref(id));
81
82        // Scalars become XML attributes; everything else becomes a child.
83        let mut children: Vec<(usize, &Value)> = Vec::new();
84        for (i, value) in entity.attributes.iter().enumerate() {
85            match attribute_text(value) {
86                Some(text) => write_attr(&mut out, &names[i], &text),
87                None => children.push((i, value)),
88            }
89        }
90
91        if children.is_empty() {
92            out.push_str("/>\n");
93        } else {
94            out.push_str(">\n");
95            for (i, value) in children {
96                write_child(&mut out, &names[i], value, 2)?;
97            }
98            writeln!(out, "  </{}>", entity.type_name).map_err(fmt_err)?;
99        }
100    }
101
102    out.push_str("</ifcXML>\n");
103    Ok(out.into_bytes())
104}
105
106fn fmt_err(e: std::fmt::Error) -> XmlError {
107    XmlError::Write(e.to_string())
108}
109
110/// The header, mirroring STEP's `FILE_DESCRIPTION`/`FILE_NAME` content.
111fn write_header(out: &mut String, model: &Model) {
112    let h = model.header();
113    out.push_str("  <header>\n");
114    push_element(out, "name", &h.name);
115    push_element(out, "time_stamp", &h.time_stamp);
116    push_element(out, "preprocessor_version", &h.preprocessor_version);
117    push_element(out, "originating_system", &h.originating_system);
118    push_element(out, "authorization", &h.authorization);
119    for a in &h.author {
120        push_element(out, "author", a);
121    }
122    for o in &h.organization {
123        push_element(out, "organization", o);
124    }
125    for d in &h.description {
126        push_element(out, "description", d);
127    }
128    out.push_str("  </header>\n");
129}
130
131fn push_element(out: &mut String, tag: &str, text: &str) {
132    if text.is_empty() {
133        return;
134    }
135    out.push_str("    <");
136    out.push_str(tag);
137    out.push('>');
138    escape_into(out, text);
139    out.push_str("</");
140    out.push_str(tag);
141    out.push_str(">\n");
142}
143
144/// Write a value that needs its own element to preserve its kind.
145fn write_child(out: &mut String, name: &str, value: &Value, depth: usize) -> Result<(), XmlError> {
146    let pad = "  ".repeat(depth);
147    match value {
148        Value::Null => {
149            writeln!(out, "{pad}<{name} xsi:nil=\"true\"/>").map_err(fmt_err)?;
150        }
151        Value::Derived => {
152            writeln!(out, "{pad}<{name} derived=\"true\"/>").map_err(fmt_err)?;
153        }
154        Value::Typed { type_name, value } => {
155            write!(out, "{pad}<{name} kind=\"typed\"").map_err(fmt_err)?;
156            write_attr(out, "type", type_name);
157            out.push_str(">\n");
158            write_child(out, "value", value, depth + 1)?;
159            writeln!(out, "{pad}</{name}>").map_err(fmt_err)?;
160        }
161        Value::List(items) => {
162            writeln!(out, "{pad}<{name} kind=\"list\">").map_err(fmt_err)?;
163            for item in items {
164                write_child(out, "item", item, depth + 1)?;
165            }
166            writeln!(out, "{pad}</{name}>").map_err(fmt_err)?;
167        }
168        leaf => {
169            // Leaf kinds always carry an explicit `kind` here, including the
170            // text, numbers and references that `attribute_text` refused: an
171            // element without one would be re-read by inference.
172            let (kind, text) = element_form(leaf)
173                .ok_or_else(|| XmlError::Write(format!("no element form for value {leaf:?}")))?;
174            write!(out, "{pad}<{name} kind=\"{kind}\">").map_err(fmt_err)?;
175            escape_into(out, &text);
176            writeln!(out, "</{name}>").map_err(fmt_err)?;
177        }
178    }
179    Ok(())
180}
181
182fn write_attr(out: &mut String, name: &str, value: &str) {
183    out.push(' ');
184    out.push_str(name);
185    out.push_str("=\"");
186    escape_into(out, value);
187    out.push('"');
188}
189
190/// XML-escape into an existing buffer.
191///
192/// Tab, line feed and carriage return are written as character references:
193/// a conforming parser normalizes them to spaces inside attribute values and
194/// folds CR/LF line ends in text, so a literal one would not survive.
195fn escape_into(out: &mut String, text: &str) {
196    for c in text.chars() {
197        match c {
198            '\t' => out.push_str("&#9;"),
199            '\n' => out.push_str("&#10;"),
200            '\r' => out.push_str("&#13;"),
201            '&' => out.push_str("&amp;"),
202            '<' => out.push_str("&lt;"),
203            '>' => out.push_str("&gt;"),
204            '"' => out.push_str("&quot;"),
205            '\'' => out.push_str("&apos;"),
206            c => out.push(c),
207        }
208    }
209}