1use 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
48pub 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 #[cfg(feature = "schema")]
91 let layout = match strict.as_mut() {
92 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 #[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
140fn 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
174fn 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 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
220fn escape_into(out: &mut String, text: &str) {
226 for c in text.chars() {
227 match c {
228 '\t' => out.push_str("	"),
229 '\n' => out.push_str(" "),
230 '\r' => out.push_str(" "),
231 '&' => out.push_str("&"),
232 '<' => out.push_str("<"),
233 '>' => out.push_str(">"),
234 '"' => out.push_str("""),
235 '\'' => out.push_str("'"),
236 c => out.push(c),
237 }
238 }
239}