1use 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
20pub 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
28pub fn read(codec: &XmlCodec, bytes: &[u8]) -> Result<Model, XmlError> {
30 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 let mut in_header = false;
44 let mut header_tag: Option<String> = None;
45
46 let mut current: Option<PendingEntity> = None;
48 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(¤t, &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(¤t, &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(¤t, &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(¤t, &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
236struct PendingEntity {
241 id: EntityId,
242 type_name: String,
243 attrs: Vec<(String, Value)>,
244}
245
246struct 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
288fn 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
304fn 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
329fn 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}