Skip to main content

cimmodel/
decode.rs

1use std::path::Path;
2
3use quick_xml::events::{BytesStart, Event};
4use quick_xml::Reader;
5
6use crate::base::{intern, AttrDef, AttrKind, ClassDef, Element, FastMap, FastSet, FieldValue, TypeRegistry};
7use crate::registry;
8
9/// Whether `text` looks like CGMES or NC RDF/XML: near the top it names one
10/// of the two CIM namespaces, an NC profile (`https://ap.cim4.eu/`, which a
11/// header's `dcterms:conformsTo` names: an NC dataset may be its DCAT header
12/// alone) or ENTSO-E reference data (`https://energy.referencedata.eu/`,
13/// SKOS/DCAT with no CIM namespace).
14pub fn is_cim(text: &str) -> bool {
15    let mut end = text.len().min(8192);
16    while !text.is_char_boundary(end) {
17        end -= 1;
18    }
19    let head = &text[..end];
20    ["http://iec.ch/TC57/", "https://cim.ucaiug.io/ns#", "https://ap.cim4.eu/", "https://energy.referencedata.eu/"]
21        .iter()
22        .any(|marker| head.contains(marker))
23}
24
25/// How a dataset's files wrote one field of one class
26/// ([`CimDataset::field_forms`]). Each part is the first the files showed;
27/// a field written two ways keeps the first.
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub struct FieldForm {
30    /// The field's namespace IRI where it is not the declared one: for a key
31    /// the class tables do not declare (`conformsTo` →
32    /// `http://purl.org/dc/terms/`), or a declared attribute written in
33    /// another namespace (an NC file's `IdentifiedObject.mRID` in CIM100).
34    pub ns: Option<&'static str>,
35    /// The `rdf:datatype` its literals carried (`dcat:temporalResolution` →
36    /// `xsd:duration`).
37    pub datatype: Option<&'static str>,
38    /// The namespace its references were cut from, when not the document's
39    /// own (`#_x`) and not an enumeration's, whose namespace the class table
40    /// holds: a `PropertyReference` to `http://iec.ch/TC57/CIM100#Switch.open`
41    /// decodes as `Switch.open`.
42    pub resource_ns: Option<&'static str>,
43}
44
45impl FieldForm {
46    fn fill_from(&mut self, other: FieldForm) {
47        self.ns = self.ns.or(other.ns);
48        self.datatype = self.datatype.or(other.datatype);
49        self.resource_ns = self.resource_ns.or(other.resource_ns);
50    }
51}
52
53pub struct CimDataset {
54    pub entries: FastMap<String, Element>,
55    /// Maps `type_name()` → list of MRIDs of that type. Populated on insert, maintained on merge.
56    pub by_type: FastMap<String, Vec<String>>,
57    /// How the files wrote each field, by the class of its object
58    /// (`type_name()`) and its key, where the class tables cannot tell and the
59    /// decoded value no longer shows: what the encoder needs to write the
60    /// field back as it was read. By class, because one document may use one
61    /// local name in two vocabularies (`dcterms:type` on a dataset,
62    /// `prov:type` on an activity).
63    pub field_forms: FastMap<(&'static str, &'static str), FieldForm>,
64    /// The namespace an object's element was written in, by mRID, where it is
65    /// not its class's: a class the decoder found by local name only
66    /// (`cim:BoundaryPoint` for CIM100-European's, an NC file's
67    /// `cim:PhaseTapChangerLinear` for CGMES's). First file wins.
68    pub type_ns: FastMap<String, &'static str>,
69    /// The prefix the files bound each namespace to (`qudt` →
70    /// `http://qudt.org/schema/qudt/`), for the encoder to reuse. First file
71    /// wins.
72    pub prefixes: FastMap<&'static str, &'static str>,
73    /// Objects of classes no class table knows — reference data's SKOS
74    /// concepts, PROV agents, DCAT catalogues — keyed by their identifier as
75    /// written: the full IRI of `rdf:about`, `_:` and the `rdf:nodeID` of a
76    /// blank node. Kept apart from `entries` so that validation, SPARQL and
77    /// every lookup of a CIM object see only CIM objects; the encoder writes
78    /// them back ([`crate::convert::dataset_to_xml`]). Their classes come from
79    /// [`crate::base::foreign_class`].
80    pub foreign: FastMap<String, Element>,
81}
82
83impl Default for CimDataset {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl CimDataset {
90    pub fn new() -> Self {
91        Self {
92            entries: FastMap::default(),
93            by_type: FastMap::default(),
94            field_forms: FastMap::default(),
95            type_ns: FastMap::default(),
96            prefixes: FastMap::default(),
97            foreign: FastMap::default(),
98        }
99    }
100
101    /// Decode an RDF/XML string into a CimDataset, using the provided type registry.
102    pub fn decode_str(content: &str) -> Result<Self, Box<dyn std::error::Error>> {
103        let mut ds = Self::new();
104        parse_rdf(content, registry::type_registry(), &mut ds)?;
105        Ok(ds)
106    }
107
108    /// Decode an RDF/XML file into a CimDataset, using the provided type registry.
109    pub fn decode_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
110        Self::decode_str(&std::fs::read_to_string(path)?)
111    }
112
113    /// Decode multiple RDF/XML files into a single CimDataset, merging entries with the same MRID.
114    pub fn decode_files(paths: &[&Path]) -> Result<Self, Box<dyn std::error::Error>> {
115        let mut combined = Self::new();
116        for path in paths {
117            let file_ds = Self::decode_file(path)?;
118            combined.merge(file_ds);
119        }
120        Ok(combined)
121    }
122
123    /// Decode files in parallel using one thread per file, without merging.
124    /// Preserves input order. Falls back to sequential decoding for 0–1 paths
125    /// to avoid thread-spawn overhead.
126    pub fn decode_files_parallel_separate(paths: &[&Path]) -> Result<Vec<Self>, Box<dyn std::error::Error>> {
127        if paths.len() <= 1 {
128            return paths.iter().map(|p| Self::decode_file(p)).collect();
129        }
130        let results: Vec<Result<Self, String>> = std::thread::scope(|s| {
131            paths
132                .iter()
133                .map(|p| s.spawn(|| Self::decode_file(p).map_err(|e| e.to_string())))
134                .collect::<Vec<_>>()
135                .into_iter()
136                .map(|h| h.join().expect("decode thread panicked"))
137                .collect()
138        });
139        results
140            .into_iter()
141            .collect::<Result<_, String>>()
142            .map_err(|e| e.into())
143    }
144
145    /// Decode files in parallel using one thread per file, then merge sequentially.
146    pub fn decode_files_parallel(paths: &[&Path]) -> Result<Self, Box<dyn std::error::Error>> {
147        let datasets = Self::decode_files_parallel_separate(paths)?;
148        Ok(datasets
149            .into_iter()
150            .reduce(|mut a, b| {
151                a.merge(b);
152                a
153            })
154            .unwrap_or_default())
155    }
156
157    /// Merge another dataset into self, combining objects with the same MRID:
158    /// a later scalar wins, reference lists are joined. An object keeps the
159    /// class it was first decoded as.
160    ///
161    /// Where the files type an object differently — an SSH file writing
162    /// `cim:Equipment` for what the EQ file wrote as `cim:ACLineSegment` — the
163    /// object keeps every type, as the RDF union does ([`Element::types`]),
164    /// and its class (and `by_type` bucket) is the most specific, whichever
165    /// file came first. Of unrelated classes the first stays the class.
166    pub fn merge(&mut self, other: CimDataset) {
167        for (key, form) in other.field_forms {
168            self.field_forms.entry(key).or_default().fill_from(form);
169        }
170        for (mrid, ns) in other.type_ns {
171            self.type_ns.entry(mrid).or_insert(ns);
172        }
173        for (ns, prefix) in other.prefixes {
174            self.prefixes.entry(ns).or_insert(prefix);
175        }
176        for (id, incoming) in other.foreign {
177            match self.foreign.get_mut(&id) {
178                Some(existing) => existing.merge_from(&incoming),
179                None => {
180                    self.foreign.insert(id, incoming);
181                }
182            }
183        }
184        let reg = registry::type_registry();
185        let mut moved: FastMap<&'static str, FastSet<String>> = FastMap::default();
186        for (mrid, incoming) in other.entries {
187            if let Some(existing) = self.entries.get_mut(&mrid) {
188                existing.merge_from(&incoming);
189                let old = existing.class();
190                let mut changed = false;
191                for t in incoming.types() {
192                    changed |= existing.add_type(t, reg);
193                }
194                if changed {
195                    moved.entry(old.qualified).or_default().insert(mrid.clone());
196                    self.by_type.entry(existing.type_name().to_string()).or_default().push(mrid);
197                }
198            } else {
199                let type_name = incoming.type_name().to_string();
200                self.by_type.entry(type_name).or_default().push(mrid.clone());
201                self.entries.insert(mrid, incoming);
202            }
203        }
204        // One pass per bucket left, however many of its objects moved.
205        for (old, mrids) in moved {
206            if let Some(bucket) = self.by_type.get_mut(old) {
207                bucket.retain(|m| !mrids.contains(m));
208            }
209        }
210    }
211
212    /// An element of `class` from its JSON form ([`Element::from_json`]), with
213    /// the dataset as evidence for what JSON cannot say: whether a field's
214    /// string is a reference or text. Where this dataset holds the field on an
215    /// object of the same class, it is held the same way — a DCAT header's
216    /// undeclared `conformsTo` as a reference, `dcat:version`, which the NC
217    /// table declares an association, as the text the data writes. Otherwise
218    /// the class table decides, and an undeclared field is text.
219    pub fn element_from_json(&self, class: &'static ClassDef, value: &serde_json::Value) -> Result<Element, String> {
220        let reg = registry::type_registry();
221        let mut element = Element::from_json(class, reg, value)?;
222        let keys: Vec<&'static str> = element.fields().keys().copied().collect();
223        for key in keys {
224            if let Some(reference) = self.held_as_reference(class.qualified, key) {
225                element.retype(key, reference);
226            }
227        }
228        Ok(element)
229    }
230
231    /// How objects of class `type_name` hold field `key` in this dataset:
232    /// `Some(true)` as a reference, `Some(false)` as text, `None` if none
233    /// holds it. The first object found decides.
234    fn held_as_reference(&self, type_name: &str, key: &str) -> Option<bool> {
235        self.by_type
236            .get(type_name)
237            .into_iter()
238            .flatten()
239            .filter_map(|m| self.entries.get(m))
240            .find_map(|e| match e.get(key)? {
241                FieldValue::Resource(_) | FieldValue::ResourceList(_) => Some(true),
242                FieldValue::Text(_) | FieldValue::TextList(_) => Some(false),
243            })
244            .or_else(|| self.field_forms.get(&(type_name, key)).and_then(|f| f.resource_ns).map(|_| true))
245    }
246
247    /// Insert or replace the element at `mrid`, keeping `by_type` consistent
248    /// (including the case where `mrid` already exists under a different type).
249    pub fn set(&mut self, mrid: String, element: Element) {
250        let new_type = element.type_name().to_string();
251        if let Some(old) = self.entries.get(&mrid) {
252            let old_type = old.type_name();
253            if old_type != new_type
254                && let Some(v) = self.by_type.get_mut(old_type) {
255                    v.retain(|m| m != &mrid);
256                }
257        }
258        let already_indexed = self
259            .by_type
260            .get(&new_type)
261            .is_some_and(|v| v.contains(&mrid));
262        if !already_indexed {
263            self.by_type.entry(new_type).or_default().push(mrid.clone());
264        }
265        self.entries.insert(mrid, element);
266    }
267
268    /// Remove the element at `mrid`, pruning it from `by_type`. Returns the
269    /// removed element, or `None` if `mrid` was not present.
270    pub fn remove(&mut self, mrid: &str) -> Option<Element> {
271        let removed = self.entries.remove(mrid)?;
272        if let Some(v) = self.by_type.get_mut(removed.type_name()) {
273            v.retain(|m| m != mrid);
274        }
275        Some(removed)
276    }
277
278    /// The objects the files typed with classes of which neither is the
279    /// other's ancestor, sorted, each with its types: what a merge of datasets
280    /// that disagree about an object leaves ([`CimDataset::merge`] keeps every
281    /// type). An SSH file's `cim:Equipment` for an EQ `cim:ACLineSegment` is a
282    /// superclass, not a conflict.
283    pub fn type_conflicts(&self) -> Vec<(&str, Vec<&'static str>)> {
284        let reg = registry::type_registry();
285        let related = |a: &ClassDef, b: &ClassDef| {
286            reg.chain(a).iter().any(|c| c.qualified == b.qualified)
287                || reg.chain(b).iter().any(|c| c.qualified == a.qualified)
288        };
289        let mut out: Vec<(&str, Vec<&'static str>)> = self
290            .entries
291            .iter()
292            .filter_map(|(mrid, e)| {
293                let types: Vec<&'static ClassDef> = e.types().collect();
294                let conflict = types.iter().enumerate().any(|(i, a)| types[i + 1..].iter().any(|b| !related(a, b)));
295                conflict.then(|| (mrid.as_str(), types.iter().map(|c| c.qualified).collect()))
296            })
297            .collect();
298        out.sort_unstable();
299        out
300    }
301}
302
303// --- XML streaming parser ---------------------------------------------------
304
305/// Push an MRID onto its type bucket, allocating the key only when the bucket
306/// is new rather than once per element.
307fn index(by_type: &mut FastMap<String, Vec<String>>, type_name: &'static str, mrid: String) {
308    if let Some(bucket) = by_type.get_mut(type_name) {
309        bucket.push(mrid);
310    } else {
311        by_type.insert(type_name.to_string(), vec![mrid]);
312    }
313}
314
315type Table = FastMap<&'static str, &'static ClassDef>;
316
317/// The document's `xmlns` bindings, resolved once to dispatch tables.
318///
319/// Resolving a prefix per element rather than a namespace IRI is what keeps
320/// this cheap: prefixes are two to four bytes, IRIs are thirty-odd, and a
321/// document binds a handful of them and then repeats them on every element.
322#[derive(Default)]
323struct Scope {
324    /// Prefix → its namespace IRI (interned) and that namespace's table.
325    prefixes: Vec<(Vec<u8>, &'static str, Option<&'static Table>)>,
326    default: Option<&'static Table>,
327    default_ns: &'static str,
328}
329
330impl Scope {
331    /// Collect `xmlns:*` and `xmlns` declarations from an element.
332    fn absorb(&mut self, e: &BytesStart, reg: &'static TypeRegistry) {
333        for attr in e.attributes().flatten() {
334            let key = attr.key.as_ref();
335            let Some(rest) = key.strip_prefix(b"xmlns".as_slice()) else {
336                continue;
337            };
338            let Ok(ns) = std::str::from_utf8(&attr.value) else {
339                continue;
340            };
341            // A namespace written without its delimiter still names the same vocabulary.
342            let table = reg.ns_table(ns).or_else(|| {
343                if ns.ends_with('#') || ns.ends_with('/') {
344                    None
345                } else {
346                    reg.ns_table(&format!("{ns}#"))
347                }
348            });
349            let ns = intern(ns);
350            match rest.strip_prefix(b":".as_slice()) {
351                Some(prefix) => {
352                    let prefix = prefix.to_vec();
353                    match self.prefixes.iter_mut().find(|(k, _, _)| *k == prefix) {
354                        Some(slot) => (slot.1, slot.2) = (ns, table),
355                        None => self.prefixes.push((prefix, ns, table)),
356                    }
357                }
358                None if rest.is_empty() => (self.default, self.default_ns) = (table, ns),
359                None => {}
360            }
361        }
362    }
363
364    fn table(&self, prefix: Option<&[u8]>) -> Option<&'static Table> {
365        match prefix {
366            None => self.default,
367            Some(p) => self
368                .prefixes
369                .iter()
370                .find(|(k, _, _)| k.as_slice() == p)
371                .and_then(|(_, _, t)| *t),
372        }
373    }
374
375    /// The namespace IRI `prefix` is bound to, `""` if none.
376    fn ns(&self, prefix: Option<&[u8]>) -> &'static str {
377        match prefix {
378            None => self.default_ns,
379            Some(p) => self.prefixes.iter().find(|(k, _, _)| k.as_slice() == p).map_or("", |(_, ns, _)| ns),
380        }
381    }
382
383}
384
385/// What an object element's attributes say about it.
386struct Subject {
387    /// A CIM object's key: the text after the last `#` of `rdf:about` or
388    /// `rdf:ID`, an IRI without one whole; `_:` and the label of a blank node.
389    mrid: String,
390    /// A foreign object's key ([`CimDataset::foreign`]): `rdf:about` as
391    /// written (less a leading `#`), `rdf:ID`, or `_:` and the `rdf:nodeID`.
392    id: String,
393    /// The element only describes the object (`rdf:about`, no `rdf:ID`).
394    described: bool,
395    has_xmlns: bool,
396}
397
398/// One pass over an element's attributes, yielding what the parser needs.
399/// Scanning twice costs more than the nested-`xmlns` case it would serve,
400/// because every element pays for it and almost none declare one.
401fn scan_attrs(e: &BytesStart) -> Result<Subject, Box<dyn std::error::Error>> {
402    let mut subject = Subject { mrid: String::new(), id: String::new(), described: false, has_xmlns: false };
403    let mut defined = false;
404    for attr in e.attributes().flatten() {
405        let key = attr.key.as_ref();
406        if key == b"rdf:about" || key == b"rdf:ID" {
407            let value = std::str::from_utf8(&attr.value)?;
408            subject.mrid = strip_fragment(value);
409            subject.id = value.strip_prefix('#').unwrap_or(value).to_string();
410            subject.described |= key == b"rdf:about";
411            defined |= key == b"rdf:ID";
412        } else if key == b"rdf:nodeID" {
413            subject.mrid = format!("_:{}", std::str::from_utf8(&attr.value)?);
414            subject.id = subject.mrid.clone();
415        } else if key.starts_with(b"xmlns") {
416            subject.has_xmlns = true;
417        }
418    }
419    subject.described &= !defined;
420    Ok(subject)
421}
422
423/// The object an element starts: of its class when a table knows it, else a
424/// foreign object (`true`) of a class named by the element; and the
425/// namespace the element was written in, when it is not the class's. `None`
426/// for an element with no identifier, which nothing can refer to.
427fn object(
428    scope: &Scope,
429    reg: &'static TypeRegistry,
430    e: &BytesStart,
431    subject: Subject,
432) -> Result<Option<(Element, bool, Option<&'static str>)>, Box<dyn std::error::Error>> {
433    if subject.id.is_empty() {
434        return Ok(None);
435    }
436    let name = e.name();
437    let (prefix, local) = split_qname(name.as_ref());
438    let ns = scope.ns(prefix);
439    let (mut element, foreign) = match resolve(scope, reg, name.as_ref())? {
440        Some(class) => (Element::new(class, subject.mrid), false),
441        None => (Element::new(crate::base::foreign_class(ns, std::str::from_utf8(local)?), subject.id), true),
442    };
443    if subject.described {
444        element.mark_described();
445    }
446    let written = (!ns.is_empty() && !same_ns(element.type_ns(), ns)).then_some(ns);
447    Ok(Some((element, foreign, written)))
448}
449
450/// Whether two namespace IRIs name the same vocabulary: a document may write
451/// one without its trailing `#`, which the decoder's tables accept.
452fn same_ns(a: &str, b: &str) -> bool {
453    a == b || a.strip_suffix('#') == Some(b) || b.strip_suffix('#') == Some(a)
454}
455
456/// Add a decoded object to the dataset, a foreign one apart.
457fn store(ds: &mut CimDataset, element: Element, foreign: bool, written: Option<&'static str>) {
458    if let Some(ns) = written
459        && !foreign
460        && !element.mrid().is_empty()
461    {
462        ds.type_ns.entry(element.mrid().to_string()).or_insert(ns);
463    }
464    if foreign {
465        ds.foreign.insert(element.mrid().to_string(), element);
466    } else if !element.mrid().is_empty() {
467        index(&mut ds.by_type, element.type_name(), element.mrid().to_string());
468        ds.entries.insert(element.mrid().to_string(), element);
469    }
470}
471
472/// Split a qualified name into its prefix and local part.
473fn split_qname(raw: &[u8]) -> (Option<&[u8]>, &[u8]) {
474    match raw.iter().position(|b| *b == b':') {
475        Some(i) => (Some(&raw[..i]), &raw[i + 1..]),
476        None => (None, raw),
477    }
478}
479
480/// Resolve an element name to its class.
481///
482/// The namespace wins when the document binds one we know. Otherwise we fall
483/// back to the bare local name, which is what every pre-namespace release did:
484/// real files bind prefixes we do not expect (one CGMES test file binds the
485/// ModelDescription IRI to `mdc`), and write elements under a namespace their
486/// class was not declared in (`<cim:BoundaryPoint>` where `BoundaryPoint` is
487/// declared in `CIM100-European#`). The fallback is therefore per element, not
488/// per document.
489fn resolve(
490    scope: &Scope,
491    reg: &'static TypeRegistry,
492    raw: &[u8],
493) -> Result<Option<&'static ClassDef>, Box<dyn std::error::Error>> {
494    let (prefix, local_bytes) = split_qname(raw);
495    let local = std::str::from_utf8(local_bytes)?;
496    if let Some(class) = scope.table(prefix).and_then(|t| t.get(local)) {
497        return Ok(Some(*class));
498    }
499    Ok(reg.bare().get(local).copied())
500}
501
502/// Parse an RDF/XML string into a CimDataset, using the provided type registry.
503fn parse_rdf(
504    content: &str,
505    reg: &'static TypeRegistry,
506    ds: &mut CimDataset,
507) -> Result<(), Box<dyn std::error::Error>> {
508    let mut reader = Reader::from_str(content);
509    let mut buf = Vec::new();
510
511    let mut depth: u32 = 0;
512    let mut scope = Scope::default();
513    // Outer scope parked while a nested `xmlns` override is in effect.
514    let mut nested: Option<Scope> = None;
515    // Resolved at the element's start tag, so an unregistered element never
516    // accumulates fields.
517    let mut current: Option<Element> = None;
518    // `current` is an object of a class no table knows ([`CimDataset::foreign`]).
519    let mut current_foreign = false;
520    // The namespace `current` was written in, when not its class's.
521    let mut current_ns: Option<&'static str> = None;
522    // The attributes `current`'s class declares, which supply most field keys.
523    let mut declared: Option<&'static FastMap<&'static str, &'static AttrDef>> = None;
524    let mut pending_key: Option<&'static str> = None;
525    // The `xml:lang` of the field element whose text comes next.
526    let mut pending_lang: Option<String> = None;
527    // The literal field's text so far. Stored at its end tag, as written —
528    // untrimmed, and empty for `<cim:X></cim:X>` — since a comment or CDATA
529    // section splits it into several events.
530    let mut pending_text = String::new();
531    // A field element's key as stored — the class table's id when the class
532    // declares it, else the interned name — and its attributes. What the key
533    // and value lose of how it was written goes to `field_forms`: an
534    // undeclared key's namespace, a datatype, the namespace a reference was
535    // cut from.
536    let field = |declared: Option<&'static FastMap<&'static str, &'static AttrDef>>,
537                 class: &'static str,
538                 e: &BytesStart,
539                 scope: &Scope,
540                 forms: &mut FastMap<(&'static str, &'static str), FieldForm>|
541     -> Result<(&'static str, FieldAttrs), Box<dyn std::error::Error>> {
542        let name = e.name();
543        let (prefix, local_bytes) = split_qname(name.as_ref());
544        let local = std::str::from_utf8(local_bytes)?;
545        let attr = declared.and_then(|d| d.get(local));
546        let key = attr.map_or_else(|| intern(local), |a| a.id);
547        let attrs = field_attrs(e.attributes())?;
548        let ns = scope.ns(prefix);
549        let undeclared_ns = (!ns.is_empty() && attr.is_none_or(|a| !same_ns(a.ns, ns))).then_some(ns);
550        let resource_ns = attrs.resource_ns.filter(|_| attr.is_none_or(|a| a.kind != AttrKind::Enum));
551        if undeclared_ns.is_some() || attrs.datatype.is_some() || resource_ns.is_some() {
552            forms.entry((class, key)).or_default().fill_from(FieldForm {
553                ns: undeclared_ns,
554                datatype: attrs.datatype,
555                resource_ns,
556            });
557        }
558        Ok((key, attrs))
559    };
560
561    loop {
562        match reader.read_event_into(&mut buf) {
563            Ok(Event::Start(ref e)) => {
564                depth += 1;
565
566                match depth {
567                    1 => {
568                        scope.absorb(e, reg);
569                        for (prefix, ns, _) in &scope.prefixes {
570                            if let Ok(prefix) = std::str::from_utf8(prefix) {
571                                ds.prefixes.entry(ns).or_insert_with(|| intern(prefix));
572                            }
573                        }
574                    }
575                    2 => {
576                        // Every CGMES and NC file declares its namespaces on the
577                        // root, but a nested redeclaration is legal RDF/XML, so
578                        // honour one when it appears. The End arm restores the
579                        // outer scope.
580                        let subject = scan_attrs(e)?;
581                        if subject.has_xmlns {
582                            let mut inner = Scope {
583                                prefixes: scope.prefixes.clone(),
584                                default: scope.default,
585                                default_ns: scope.default_ns,
586                            };
587                            inner.absorb(e, reg);
588                            nested = Some(std::mem::replace(&mut scope, inner));
589                        }
590
591                        (current, current_foreign, current_ns) = match object(&scope, reg, e, subject)? {
592                            Some((element, foreign, written)) => (Some(element), foreign, written),
593                            None => (None, false, None),
594                        };
595                        declared = current.as_ref().and_then(|c| reg.declared(c.class()));
596                    }
597                    3 => {
598                        if let Some(element) = current.as_mut() {
599                            let (key, attrs) = field(declared, element.type_name(), e, &scope, &mut ds.field_forms)?;
600                            if let Some(res) = attrs.resource {
601                                element.add_field(key, FieldValue::Resource(res));
602                            } else if !attrs.nodes {
603                                pending_key = Some(key);
604                                pending_lang = attrs.lang;
605                                pending_text.clear();
606                            }
607                        }
608                    }
609                    // A child element: the field holds statements or XML
610                    // (`rdf:parseType`), not a literal.
611                    _ => pending_key = None,
612                }
613            }
614
615            Ok(Event::Empty(ref e)) => {
616                match depth {
617                    1 => {
618                        // Top-level self-closing element: <cim:Foo rdf:ID="x" />
619                        // It can carry its own xmlns; with no children there is
620                        // nothing to restore afterwards, so resolve against a
621                        // throwaway scope.
622                        let subject = scan_attrs(e)?;
623                        let local_scope = if subject.has_xmlns {
624                            let mut inner = Scope {
625                                prefixes: scope.prefixes.clone(),
626                                default: scope.default,
627                                default_ns: scope.default_ns,
628                            };
629                            inner.absorb(e, reg);
630                            Some(inner)
631                        } else {
632                            None
633                        };
634                        let active = local_scope.as_ref().unwrap_or(&scope);
635                        if let Some((element, foreign, written)) = object(active, reg, e, subject)? {
636                            store(ds, element, foreign, written);
637                        }
638                    }
639                    2 => {
640                        // Self-closing field element within the current type
641                        // block: a reference, or else an empty literal.
642                        if let Some(element) = current.as_mut() {
643                            let (key, attrs) = field(declared, element.type_name(), e, &scope, &mut ds.field_forms)?;
644                            if let Some(res) = attrs.resource {
645                                element.add_field(key, FieldValue::Resource(res));
646                            } else if !attrs.nodes {
647                                element.add_field(key, FieldValue::Text(String::new()));
648                                if let Some(lang) = attrs.lang {
649                                    element.add_lang(key, &lang);
650                                }
651                            }
652                        }
653                    }
654                    _ => pending_key = None,
655                }
656            }
657
658            Ok(Event::Text(ref e)) if depth == 3 && pending_key.is_some() => {
659                pending_text.push_str(&e.unescape()?);
660            }
661
662            Ok(Event::CData(ref e)) if depth == 3 && pending_key.is_some() => {
663                pending_text.push_str(std::str::from_utf8(e)?);
664            }
665
666            Ok(Event::End(_)) => {
667                if depth == 3
668                    && let (Some(element), Some(key)) = (current.as_mut(), pending_key.take()) {
669                        element.add_field(key, FieldValue::Text(std::mem::take(&mut pending_text)));
670                        if let Some(lang) = pending_lang.take() {
671                            element.add_lang(key, &lang);
672                        }
673                    }
674                if depth == 2 {
675                    pending_key = None;
676                    pending_lang = None;
677                    if let Some(element) = current.take() {
678                        store(ds, element, current_foreign, current_ns.take());
679                    }
680                    if let Some(outer) = nested.take() {
681                        scope = outer;
682                    }
683                }
684                depth = depth.saturating_sub(1);
685            }
686
687            Ok(Event::Eof) => break,
688            Err(e) => return Err(Box::new(e)),
689            _ => {}
690        }
691        buf.clear();
692    }
693
694    Ok(())
695}
696
697// --- helpers ----------------------------------------------------------------
698
699
700/// Strip the fragment from a URI, e.g. "http://example.com#foo" → "foo".
701///
702/// An IRI whose fragment is empty names a vocabulary, not an object
703/// (`dcterms:conformsTo rdf:resource="http://iec.ch/TC57/CIM100#"`), and is
704/// kept whole: stripped, it was the empty string, which no encoder can write
705/// back.
706fn strip_fragment(s: &str) -> String {
707    match s.rfind('#') {
708        Some(i) if i + 1 == s.len() && i > 0 => s.to_string(),
709        Some(i) => s[i + 1..].to_string(),
710        None => s.to_string(),
711    }
712}
713
714
715/// What a field element's attributes say about its value.
716struct FieldAttrs {
717    /// `rdf:resource`, as [`strip_fragment`] keeps it.
718    resource: Option<String>,
719    /// The part of `rdf:resource` that was cut off, when the IRI has one
720    /// before its `#` (not a reference into the document itself).
721    resource_ns: Option<&'static str>,
722    lang: Option<String>,
723    datatype: Option<&'static str>,
724    /// Its `rdf:parseType` makes its content nodes rather than a literal
725    /// (`Resource`, `Collection`, a difference model's `Statements`; not
726    /// `Literal`).
727    nodes: bool,
728}
729
730/// A field element's attributes, in one pass.
731fn field_attrs(attrs: quick_xml::events::attributes::Attributes<'_>) -> Result<FieldAttrs, Box<dyn std::error::Error>> {
732    let mut out = FieldAttrs { resource: None, resource_ns: None, lang: None, datatype: None, nodes: false };
733    for attr in attrs.flatten() {
734        match attr.key.as_ref() {
735            b"rdf:resource" => {
736                let iri = std::str::from_utf8(&attr.value)?;
737                let kept = strip_fragment(iri);
738                if kept.len() < iri.len() && kept.len() + 1 < iri.len() {
739                    out.resource_ns = Some(intern(&iri[..iri.len() - kept.len()]));
740                }
741                out.resource = Some(kept);
742            }
743            b"rdf:nodeID" => out.resource = Some(format!("_:{}", std::str::from_utf8(&attr.value)?)),
744            b"xml:lang" => out.lang = Some(std::str::from_utf8(&attr.value)?.to_string()),
745            b"rdf:datatype" => out.datatype = Some(intern(std::str::from_utf8(&attr.value)?)),
746            b"rdf:parseType" => out.nodes = attr.value.as_ref() != b"Literal",
747            _ => {}
748        }
749    }
750    Ok(out)
751}