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