1use crate::error::XmlError;
25use crate::scalar::{decode_element, format_ref, parse_ref};
26use crate::slots::Raw;
27use crate::{slots, XmlCodec, XmlLayout};
28use ifc_model::{Entity, EntityId, Model, Value};
29use quick_xml::escape::unescape;
30use quick_xml::events::attributes::Attribute;
31use quick_xml::events::{BytesRef, BytesStart, Event};
32use quick_xml::name::ResolveResult;
33use quick_xml::reader::NsReader;
34
35const XSI_NAMESPACE: &str = "http://www.w3.org/2001/XMLSchema-instance";
36
37pub fn looks_like_xml(bytes: &[u8]) -> bool {
39 let head = &bytes[..bytes.len().min(512)];
40 let text = String::from_utf8_lossy(head);
41 let trimmed = text.trim_start_matches(['\u{feff}', ' ', '\n', '\r', '\t']);
42 let prefixed = trimmed.starts_with('<')
44 && trimmed[1..]
45 .split(|c: char| c.is_whitespace() || c == '>')
46 .next()
47 .is_some_and(|name| name.ends_with(":ifcXML"));
48 trimmed.starts_with("<?xml") || trimmed.starts_with("<ifcXML") || prefixed
49}
50
51#[cfg(feature = "schema")]
53type Strict<'s> = Option<crate::typing::Layouts<'s>>;
54#[cfg(not(feature = "schema"))]
55type Strict<'s> = Option<std::marker::PhantomData<&'s ()>>;
56
57pub fn read(codec: &XmlCodec, bytes: &[u8]) -> Result<Model, XmlError> {
62 if codec.layout() == XmlLayout::Xsd {
63 return read_xsd(codec, bytes);
64 }
65 #[cfg(feature = "schema")]
66 let mut strict: Strict<'_> = codec.strict_schema().map(crate::typing::Layouts::new);
67 #[cfg(not(feature = "schema"))]
68 let mut strict: Strict<'_> = None;
69 let model = read_native(codec, bytes, &mut strict)?;
70 #[cfg(feature = "schema")]
71 if let Some(layouts) = strict.as_mut() {
72 check_references(layouts, &model)?;
73 }
74 Ok(model)
75}
76
77#[cfg(feature = "schema")]
78fn read_xsd(codec: &XmlCodec, bytes: &[u8]) -> Result<Model, XmlError> {
79 match (codec.schema(), codec.profile()) {
80 (Some(schema), Some(profile)) => crate::xsd::read(schema, profile, bytes),
81 _ => Err(XmlError::Unsupported {
82 construct: "the XSD layout without a schema and release profile".into(),
83 }),
84 }
85}
86
87#[cfg(not(feature = "schema"))]
88fn read_xsd(_: &XmlCodec, _: &[u8]) -> Result<Model, XmlError> {
89 Err(XmlError::Unsupported {
90 construct: "the XSD layout without the `schema` feature".into(),
91 })
92}
93
94#[cfg(feature = "schema")]
97fn check_references(
98 layouts: &mut crate::typing::Layouts<'_>,
99 model: &Model,
100) -> Result<(), XmlError> {
101 let schema = layouts.schema();
102 let mut references = Vec::new();
103 for (id, entity) in model.iter() {
104 let Some(layout) = layouts.entity(&entity.type_name, false)? else {
105 continue;
106 };
107 for (value, slot) in entity.attributes.iter().zip(&layout.slots) {
108 let path = format!(
109 "{}/{}",
110 raw_entity_path(&entity.type_name, Some(format_ref(id))),
111 slot.name
112 );
113 references.clear();
114 crate::typing::references(schema, &slot.shape, value, &mut references)
115 .map_err(|error| error.at(path.clone()))?;
116 for (target, declared) in &references {
117 let Some(found) = model.get(*target) else {
118 return Err(XmlError::UnresolvedReference {
119 id: format_ref(*target),
120 }
121 .at(path));
122 };
123 if !schema.accepts_type(declared, &found.type_name) {
124 return Err(XmlError::TypeMismatch {
125 declared: declared.to_string(),
126 found: format!("a reference to an entity `{}`", found.type_name),
127 }
128 .at(path));
129 }
130 }
131 }
132 }
133 Ok(())
134}
135
136fn read_native(codec: &XmlCodec, bytes: &[u8], strict: &mut Strict<'_>) -> Result<Model, XmlError> {
137 let mut reader = NsReader::from_reader(bytes);
142
143 let mut model = Model::new();
144 let mut buf = Vec::new();
145 let mut seen_root = false;
146 let mut root_closed = false;
147 let mut element_depth = 0usize;
148
149 let mut in_header = false;
151 let mut header_tag: Option<String> = None;
152
153 let mut current: Option<PendingEntity> = None;
155 let mut stack: Vec<PendingValue> = Vec::new();
157 let mut text_buf = String::new();
158
159 loop {
160 match reader.read_resolved_event_into(&mut buf) {
161 Err(error) => {
162 return Err(
163 XmlError::Malformed(error.to_string()).at(current_path(¤t, &stack, None))
164 );
165 }
166 Ok((_, Event::Eof)) => break,
167
168 Ok((namespace, Event::Start(e))) => {
169 let name = local_name(&e);
170 validate_element(codec, namespace, &name)?;
171 validate_root(codec, &e, &name, element_depth, root_closed, &mut seen_root)?;
172 element_depth += 1;
173 match name.as_str() {
174 "ifcXML" => {
175 if let Some(schema) = attr_value(&e, "schema") {
176 model.header_mut().schema = vec![schema];
177 }
178 }
179 "header" => in_header = true,
180 _ if in_header => {
181 header_tag = Some(name);
182 text_buf.clear();
183 }
184 _ if current.is_none() => match start_entity(&e, &name) {
185 Ok(Some(started)) => current = Some(started),
186 Ok(None) => {}
187 Err(error) => {
188 return Err(error.at(raw_entity_path(&name, attr_value(&e, "id"))));
189 }
190 },
191 _ => {
192 let item_index = stack.last().map_or(0, |parent| parent.items.len());
193 stack.push(PendingValue::from_start(&e, name, item_index));
194 text_buf.clear();
195 }
196 }
197 }
198
199 Ok((namespace, Event::Empty(e))) => {
200 let name = local_name(&e);
201 validate_element(codec, namespace, &name)?;
202 validate_root(codec, &e, &name, element_depth, root_closed, &mut seen_root)?;
203 if codec.profile().is_some() && element_depth == 0 {
204 root_closed = true;
205 }
206 if current.is_none() && !in_header {
207 match start_entity(&e, &name) {
208 Ok(Some(entity)) => finish_entity(codec, &mut model, entity, strict)?,
209 Ok(None) => {}
210 Err(error) => {
211 return Err(error.at(raw_entity_path(&name, attr_value(&e, "id"))));
212 }
213 }
214 } else if current.is_some() {
215 let value = if has_true_xsi_nil(&reader, &e) {
216 Value::Null
217 } else if attr_value(&e, "derived").as_deref() == Some("true") {
218 Value::Derived
219 } else {
220 let item_index = stack.last().map_or(0, |parent| parent.items.len());
221 let pending = PendingValue::from_start(&e, name.clone(), item_index);
222 let path =
223 current_path(¤t, &stack, Some(pending.path_segment.as_str()));
224 pending.finish("").map_err(|error| error.at(path))?
225 };
226 push_value(&mut stack, &mut current, name, value);
227 }
228 }
229
230 Ok((_, Event::Text(t))) => text_buf.push_str(&t),
234
235 Ok((_, Event::GeneralRef(reference))) => {
236 let resolved = resolve_reference(&reference).map_err(|error| {
237 XmlError::Malformed(error.to_string()).at(current_path(¤t, &stack, None))
238 })?;
239 text_buf.push_str(&resolved);
240 }
241
242 Ok((namespace, Event::End(e))) => {
243 let name = e.local_name().as_ref().to_owned();
244 validate_element(codec, namespace, &name)?;
245 element_depth = element_depth.saturating_sub(1);
246 if codec.profile().is_some() && element_depth == 0 {
247 root_closed = true;
248 }
249 match name.as_str() {
250 "header" => in_header = false,
251 "ifcXML" => {}
252 _ if in_header => {
253 if let Some(tag) = header_tag.take() {
254 apply_header_field(&mut model, &tag, &text_buf);
255 }
256 text_buf.clear();
257 }
258 _ => {
259 if let Some(pending) = stack.pop() {
260 let path =
261 current_path(¤t, &stack, Some(pending.path_segment.as_str()));
262 let value =
263 pending.finish(&text_buf).map_err(|error| error.at(path))?;
264 push_value(&mut stack, &mut current, pending.name.clone(), value);
265 text_buf.clear();
266 } else if let Some(entity) = current.take() {
267 finish_entity(codec, &mut model, entity, strict)?;
268 }
269 text_buf.clear();
270 }
271 }
272 }
273 Ok(_) => {}
274 }
275 buf.clear();
276 }
277
278 if codec.profile().is_some() && (!seen_root || !root_closed) {
279 return Err(XmlError::Root { found: None });
280 }
281
282 Ok(model)
283}
284
285fn validate_element(
286 codec: &XmlCodec,
287 namespace: ResolveResult<'_>,
288 element: &str,
289) -> Result<(), XmlError> {
290 let Some(profile) = codec.profile() else {
291 return Ok(());
292 };
293 let found = match namespace {
294 ResolveResult::Unbound => None,
295 ResolveResult::Bound(namespace) => Some(namespace.as_ref().to_owned()),
296 ResolveResult::Unknown(prefix) => Some(format!("unresolved prefix `{prefix}`")),
297 };
298 if found.as_deref() != Some(profile.namespace()) {
299 return Err(XmlError::Namespace {
300 element: element.into(),
301 expected: profile.namespace(),
302 found,
303 });
304 }
305 Ok(())
306}
307
308fn validate_root(
309 codec: &XmlCodec,
310 element: &BytesStart<'_>,
311 name: &str,
312 depth: usize,
313 root_closed: bool,
314 seen_root: &mut bool,
315) -> Result<(), XmlError> {
316 let Some(profile) = codec.profile() else {
317 return Ok(());
318 };
319 if *seen_root {
320 if depth == 0 || root_closed || name == "ifcXML" {
321 return Err(XmlError::Root {
322 found: Some(name.into()),
323 });
324 }
325 return Ok(());
326 }
327 if depth != 0 || name != "ifcXML" {
328 return Err(XmlError::Root {
329 found: Some(name.into()),
330 });
331 }
332 *seen_root = true;
333 let found = attr_value(element, "schema");
334 if found.as_deref() != Some(profile.schema_token()) {
335 return Err(XmlError::Profile {
336 expected: profile.schema_token(),
337 found,
338 });
339 }
340 Ok(())
341}
342
343struct PendingEntity {
348 id: EntityId,
349 type_name: String,
350 attrs: Vec<(String, Raw)>,
351}
352
353struct PendingValue {
355 name: String,
356 path_segment: String,
357 kind: String,
358 type_name: Option<String>,
359 items: Vec<Value>,
360}
361
362impl PendingValue {
363 fn from_start(e: &BytesStart<'_>, name: String, item_index: usize) -> Self {
364 let path_segment = if name == "item" {
365 format!("item[{item_index}]")
366 } else {
367 name.clone()
368 };
369 Self {
370 name,
371 path_segment,
372 kind: attr_value(e, "kind").unwrap_or_default(),
373 type_name: attr_value(e, "type"),
374 items: Vec::new(),
375 }
376 }
377
378 fn finish(&self, text: &str) -> Result<Value, XmlError> {
379 if let Some(value) = decode_element(&self.kind, text)? {
380 return Ok(value);
381 }
382 let value = if self.kind == "list" {
383 Value::List(self.items.clone())
384 } else {
385 let inner = self.items.first().cloned().unwrap_or(Value::Null);
386 Value::Typed {
387 type_name: self.type_name.clone().unwrap_or_default().into(),
388 value: Box::new(inner),
389 }
390 };
391 Ok(value)
392 }
393}
394
395fn push_value(
397 stack: &mut [PendingValue],
398 current: &mut Option<PendingEntity>,
399 name: String,
400 value: Value,
401) {
402 if let Some(parent) = stack.last_mut() {
403 parent.items.push(value);
404 return;
405 }
406 if let Some(entity) = current.as_mut() {
407 entity.attrs.push((name, Raw::Value(value)));
408 }
409}
410
411fn start_entity(e: &BytesStart<'_>, name: &str) -> Result<Option<PendingEntity>, XmlError> {
413 let Some(id_text) = attr_value(e, "id") else {
414 return Ok(None);
415 };
416 let id = parse_ref(&id_text).ok_or_else(|| XmlError::BadId(id_text.clone()))?;
417 let mut attrs = Vec::new();
418 for attr in e.attributes().flatten() {
419 let key = attr.key.local_name().as_ref().to_owned();
420 if key == "id" {
421 continue;
422 }
423 let value = unescape_attribute(&attr)
424 .map(|value| value.into_owned())
425 .map_err(|error| XmlError::Malformed(error.to_string()))?;
426 attrs.push((key, Raw::Text(value)));
427 }
428 Ok(Some(PendingEntity {
429 id,
430 type_name: name.to_string(),
431 attrs,
432 }))
433}
434
435fn finish_entity(
440 codec: &XmlCodec,
441 model: &mut Model,
442 entity: PendingEntity,
443 strict: &mut Strict<'_>,
444) -> Result<(), XmlError> {
445 let path = raw_entity_path(&entity.type_name, Some(format_ref(entity.id)));
446 #[cfg(feature = "schema")]
447 if let Some(layouts) = strict.as_mut() {
448 let values = slots::order_strict(layouts, &entity.type_name, &path, entity.attrs)
449 .map_err(|error| error.at(path))?;
450 model.insert(entity.id, Entity::new(entity.type_name, values));
451 return Ok(());
452 }
453 #[cfg(not(feature = "schema"))]
454 let _ = strict;
455 let values =
456 slots::order(codec, &entity.type_name, entity.attrs).map_err(|error| error.at(path))?;
457 model.insert(entity.id, Entity::new(entity.type_name, values));
458 Ok(())
459}
460
461fn raw_entity_path(type_name: &str, id: Option<String>) -> String {
462 match id {
463 Some(id) => format!("/ifcXML/{type_name}[@id='{id}']"),
464 None => format!("/ifcXML/{type_name}"),
465 }
466}
467
468fn current_path(
469 current: &Option<PendingEntity>,
470 stack: &[PendingValue],
471 leaf: Option<&str>,
472) -> String {
473 let mut path = String::from("/ifcXML");
474 if let Some(entity) = current {
475 path.push('/');
476 path.push_str(&entity.type_name);
477 path.push_str(&format!("[@id='i{}']", entity.id.0));
478 }
479 for value in stack {
480 path.push('/');
481 path.push_str(&value.path_segment);
482 }
483 if let Some(leaf) = leaf {
484 path.push('/');
485 path.push_str(leaf);
486 }
487 path
488}
489
490fn local_name(e: &BytesStart<'_>) -> String {
491 e.local_name().as_ref().to_owned()
492}
493
494fn unescape_attribute<'a>(
501 attribute: &'a Attribute<'_>,
502) -> Result<std::borrow::Cow<'a, str>, quick_xml::escape::EscapeError> {
503 unescape(&attribute.value)
504}
505
506fn resolve_reference(reference: &BytesRef<'_>) -> Result<String, quick_xml::escape::EscapeError> {
510 unescape(&format!("&{};", &**reference)).map(std::borrow::Cow::into_owned)
511}
512
513fn has_true_xsi_nil(reader: &NsReader<&[u8]>, element: &BytesStart<'_>) -> bool {
514 element.attributes().flatten().any(|attribute| {
515 if attribute.key.local_name().as_ref() != "nil" {
516 return false;
517 }
518 let (namespace, _) = reader.resolver().resolve_attribute(attribute.key);
519 matches!(
520 namespace,
521 ResolveResult::Bound(namespace) if namespace.as_ref() == XSI_NAMESPACE
522 ) && unescape_attribute(&attribute).is_ok_and(|value| value.as_ref() == "true")
523 })
524}
525
526fn attr_value(e: &BytesStart<'_>, key: &str) -> Option<String> {
527 e.attributes().flatten().find_map(|a| {
528 (a.key.local_name().as_ref() == key)
529 .then(|| unescape_attribute(&a).map(|v| v.into_owned()).ok())
530 .flatten()
531 })
532}
533
534fn apply_header_field(model: &mut Model, tag: &str, text: &str) {
535 let h = model.header_mut();
536 match tag {
537 "name" => h.name = text.to_string(),
538 "time_stamp" => h.time_stamp = text.to_string(),
539 "preprocessor_version" => h.preprocessor_version = text.to_string(),
540 "originating_system" => h.originating_system = text.to_string(),
541 "authorization" => h.authorization = text.to_string(),
542 "author" => h.author.push(text.to_string()),
543 "organization" => h.organization.push(text.to_string()),
544 "description" => h.description.push(text.to_string()),
545 _ => {}
546 }
547}