1use 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
19pub 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
27pub 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 let mut in_header = false;
40 let mut header_tag: Option<String> = None;
41
42 let mut current: Option<PendingEntity> = None;
44 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(¤t, &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(¤t, &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(¤t, &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(¤t, &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
231struct PendingEntity {
236 id: EntityId,
237 type_name: String,
238 attrs: Vec<(String, Value)>,
239}
240
241struct 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
306fn 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
322fn 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
347fn finish_entity(model: &mut Model, entity: PendingEntity) {
349 let mut ordered = entity.attrs;
350 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
385fn positional_index(name: &str) -> Option<usize> {
387 name.strip_prefix('a')?.parse().ok()
388}
389
390fn parse_ref(text: &str) -> Option<EntityId> {
392 let n: u64 = text.trim().strip_prefix('i')?.parse().ok()?;
393 Some(EntityId(n))
394}
395
396fn 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
416fn 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}