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, ClassDef, Element, FastMap, FastSet, FieldValue, TypeRegistry};
7use crate::registry;
8
9/// Whether `text` looks like CGMES or NC RDF/XML: it binds one of the two
10/// CIM namespaces near the top.
11pub fn is_cim(text: &str) -> bool {
12    let mut end = text.len().min(8192);
13    while !text.is_char_boundary(end) {
14        end -= 1;
15    }
16    let head = &text[..end];
17    head.contains("http://iec.ch/TC57/") || head.contains("https://cim.ucaiug.io/ns#")
18}
19
20pub struct CimDataset {
21    pub entries: FastMap<String, Element>,
22    /// Maps `type_name()` → list of MRIDs of that type. Populated on insert, maintained on merge.
23    pub by_type: FastMap<String, Vec<String>>,
24}
25
26impl Default for CimDataset {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl CimDataset {
33    pub fn new() -> Self {
34        Self {
35            entries: FastMap::default(),
36            by_type: FastMap::default(),
37        }
38    }
39
40    /// Decode an RDF/XML string into a CimDataset, using the provided type registry.
41    pub fn decode_str(content: &str) -> Result<Self, Box<dyn std::error::Error>> {
42        let mut ds = Self::new();
43        parse_rdf(content, registry::type_registry(), &mut ds)?;
44        Ok(ds)
45    }
46
47    /// Decode an RDF/XML file into a CimDataset, using the provided type registry.
48    pub fn decode_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
49        Self::decode_str(&std::fs::read_to_string(path)?)
50    }
51
52    /// Decode multiple RDF/XML files into a single CimDataset, merging entries with the same MRID.
53    pub fn decode_files(paths: &[&Path]) -> Result<Self, Box<dyn std::error::Error>> {
54        let mut combined = Self::new();
55        for path in paths {
56            let file_ds = Self::decode_file(path)?;
57            combined.merge(file_ds);
58        }
59        Ok(combined)
60    }
61
62    /// Decode files in parallel using one thread per file, without merging.
63    /// Preserves input order. Falls back to sequential decoding for 0–1 paths
64    /// to avoid thread-spawn overhead.
65    pub fn decode_files_parallel_separate(paths: &[&Path]) -> Result<Vec<Self>, Box<dyn std::error::Error>> {
66        if paths.len() <= 1 {
67            return paths.iter().map(|p| Self::decode_file(p)).collect();
68        }
69        let results: Vec<Result<Self, String>> = std::thread::scope(|s| {
70            paths
71                .iter()
72                .map(|p| s.spawn(|| Self::decode_file(p).map_err(|e| e.to_string())))
73                .collect::<Vec<_>>()
74                .into_iter()
75                .map(|h| h.join().expect("decode thread panicked"))
76                .collect()
77        });
78        results
79            .into_iter()
80            .collect::<Result<_, String>>()
81            .map_err(|e| e.into())
82    }
83
84    /// Decode files in parallel using one thread per file, then merge sequentially.
85    pub fn decode_files_parallel(paths: &[&Path]) -> Result<Self, Box<dyn std::error::Error>> {
86        let datasets = Self::decode_files_parallel_separate(paths)?;
87        Ok(datasets
88            .into_iter()
89            .reduce(|mut a, b| {
90                a.merge(b);
91                a
92            })
93            .unwrap_or_default())
94    }
95
96    /// Merge another dataset into self, combining objects with the same MRID:
97    /// a later scalar wins, reference lists are joined. An object keeps the
98    /// class it was first decoded as.
99    ///
100    /// Where the files type an object differently — an SSH file writing
101    /// `cim:Equipment` for what the EQ file wrote as `cim:ACLineSegment` — the
102    /// object keeps every type, as the RDF union does ([`Element::types`]),
103    /// and its class (and `by_type` bucket) is the most specific, whichever
104    /// file came first. Of unrelated classes the first stays the class.
105    pub fn merge(&mut self, other: CimDataset) {
106        let reg = registry::type_registry();
107        let mut moved: FastMap<&'static str, FastSet<String>> = FastMap::default();
108        for (mrid, incoming) in other.entries {
109            if let Some(existing) = self.entries.get_mut(&mrid) {
110                existing.merge_from(&incoming);
111                let old = existing.class();
112                let mut changed = false;
113                for t in incoming.types() {
114                    changed |= existing.add_type(t, reg);
115                }
116                if changed {
117                    moved.entry(old.qualified).or_default().insert(mrid.clone());
118                    self.by_type.entry(existing.type_name().to_string()).or_default().push(mrid);
119                }
120            } else {
121                let type_name = incoming.type_name().to_string();
122                self.by_type.entry(type_name).or_default().push(mrid.clone());
123                self.entries.insert(mrid, incoming);
124            }
125        }
126        // One pass per bucket left, however many of its objects moved.
127        for (old, mrids) in moved {
128            if let Some(bucket) = self.by_type.get_mut(old) {
129                bucket.retain(|m| !mrids.contains(m));
130            }
131        }
132    }
133
134    /// Insert or replace the element at `mrid`, keeping `by_type` consistent
135    /// (including the case where `mrid` already exists under a different type).
136    pub fn set(&mut self, mrid: String, element: Element) {
137        let new_type = element.type_name().to_string();
138        if let Some(old) = self.entries.get(&mrid) {
139            let old_type = old.type_name();
140            if old_type != new_type
141                && let Some(v) = self.by_type.get_mut(old_type) {
142                    v.retain(|m| m != &mrid);
143                }
144        }
145        let already_indexed = self
146            .by_type
147            .get(&new_type)
148            .is_some_and(|v| v.contains(&mrid));
149        if !already_indexed {
150            self.by_type.entry(new_type).or_default().push(mrid.clone());
151        }
152        self.entries.insert(mrid, element);
153    }
154
155    /// Remove the element at `mrid`, pruning it from `by_type`. Returns the
156    /// removed element, or `None` if `mrid` was not present.
157    pub fn remove(&mut self, mrid: &str) -> Option<Element> {
158        let removed = self.entries.remove(mrid)?;
159        if let Some(v) = self.by_type.get_mut(removed.type_name()) {
160            v.retain(|m| m != mrid);
161        }
162        Some(removed)
163    }
164}
165
166// --- XML streaming parser ---------------------------------------------------
167
168/// Push an MRID onto its type bucket, allocating the key only when the bucket
169/// is new rather than once per element.
170fn index(by_type: &mut FastMap<String, Vec<String>>, type_name: &'static str, mrid: String) {
171    if let Some(bucket) = by_type.get_mut(type_name) {
172        bucket.push(mrid);
173    } else {
174        by_type.insert(type_name.to_string(), vec![mrid]);
175    }
176}
177
178type Table = FastMap<&'static str, &'static ClassDef>;
179
180/// The document's `xmlns` bindings, resolved once to dispatch tables.
181///
182/// Resolving a prefix per element rather than a namespace IRI is what keeps
183/// this cheap: prefixes are two to four bytes, IRIs are thirty-odd, and a
184/// document binds a handful of them and then repeats them on every element.
185#[derive(Default)]
186struct Scope {
187    prefixes: Vec<(Vec<u8>, Option<&'static Table>)>,
188    default: Option<&'static Table>,
189}
190
191impl Scope {
192    /// Collect `xmlns:*` and `xmlns` declarations from an element.
193    fn absorb(&mut self, e: &BytesStart, reg: &'static TypeRegistry) {
194        for attr in e.attributes().flatten() {
195            let key = attr.key.as_ref();
196            let Some(rest) = key.strip_prefix(b"xmlns".as_slice()) else {
197                continue;
198            };
199            let Ok(ns) = std::str::from_utf8(&attr.value) else {
200                continue;
201            };
202            // A namespace written without its delimiter still names the same vocabulary.
203            let table = reg.ns_table(ns).or_else(|| {
204                if ns.ends_with('#') || ns.ends_with('/') {
205                    None
206                } else {
207                    reg.ns_table(&format!("{ns}#"))
208                }
209            });
210            match rest.strip_prefix(b":".as_slice()) {
211                Some(prefix) => {
212                    let prefix = prefix.to_vec();
213                    match self.prefixes.iter_mut().find(|(k, _)| *k == prefix) {
214                        Some(slot) => slot.1 = table,
215                        None => self.prefixes.push((prefix, table)),
216                    }
217                }
218                None if rest.is_empty() => self.default = table,
219                None => {}
220            }
221        }
222    }
223
224    fn table(&self, prefix: Option<&[u8]>) -> Option<&'static Table> {
225        match prefix {
226            None => self.default,
227            Some(p) => self
228                .prefixes
229                .iter()
230                .find(|(k, _)| k.as_slice() == p)
231                .and_then(|(_, t)| *t),
232        }
233    }
234
235}
236
237/// One pass over an element's attributes, yielding what the parser needs: the
238/// MRID, whether the element only describes the object (`rdf:about`, no
239/// `rdf:ID`) and whether it redeclares any namespace. Scanning twice
240/// costs more than the nested-`xmlns` case it would serve, because every element
241/// pays for it and almost none declare one.
242fn scan_attrs(e: &BytesStart) -> Result<(String, bool, bool), Box<dyn std::error::Error>> {
243    let mut mrid = String::new();
244    let mut described = false;
245    let mut defined = false;
246    let mut has_xmlns = false;
247    for attr in e.attributes().flatten() {
248        let key = attr.key.as_ref();
249        if key == b"rdf:about" || key == b"rdf:ID" {
250            mrid = strip_fragment(std::str::from_utf8(&attr.value)?);
251            described |= key == b"rdf:about";
252            defined |= key == b"rdf:ID";
253        } else if key.starts_with(b"xmlns") {
254            has_xmlns = true;
255        }
256    }
257    Ok((mrid, described && !defined, has_xmlns))
258}
259
260/// Split a qualified name into its prefix and local part.
261fn split_qname(raw: &[u8]) -> (Option<&[u8]>, &[u8]) {
262    match raw.iter().position(|b| *b == b':') {
263        Some(i) => (Some(&raw[..i]), &raw[i + 1..]),
264        None => (None, raw),
265    }
266}
267
268/// Resolve an element name to its class.
269///
270/// The namespace wins when the document binds one we know. Otherwise we fall
271/// back to the bare local name, which is what every pre-namespace release did:
272/// real files bind prefixes we do not expect (one CGMES test file binds the
273/// ModelDescription IRI to `mdc`), and write elements under a namespace their
274/// class was not declared in (`<cim:BoundaryPoint>` where `BoundaryPoint` is
275/// declared in `CIM100-European#`). The fallback is therefore per element, not
276/// per document.
277fn resolve(
278    scope: &Scope,
279    reg: &'static TypeRegistry,
280    raw: &[u8],
281) -> Result<Option<&'static ClassDef>, Box<dyn std::error::Error>> {
282    let (prefix, local_bytes) = split_qname(raw);
283    let local = std::str::from_utf8(local_bytes)?;
284    if let Some(class) = scope.table(prefix).and_then(|t| t.get(local)) {
285        return Ok(Some(*class));
286    }
287    Ok(reg.bare().get(local).copied())
288}
289
290/// Parse an RDF/XML string into a CimDataset, using the provided type registry.
291fn parse_rdf(
292    content: &str,
293    reg: &'static TypeRegistry,
294    ds: &mut CimDataset,
295) -> Result<(), Box<dyn std::error::Error>> {
296    let mut reader = Reader::from_str(content);
297    let mut buf = Vec::new();
298
299    let mut depth: u32 = 0;
300    let mut scope = Scope::default();
301    // Outer scope parked while a nested `xmlns` override is in effect.
302    let mut nested: Option<Scope> = None;
303    // Resolved at the element's start tag, so an unregistered element never
304    // accumulates fields.
305    let mut current: Option<Element> = None;
306    // The attributes `current`'s class declares, which supply most field keys.
307    let mut declared: Option<&'static FastMap<&'static str, &'static AttrDef>> = None;
308    let mut pending_key: Option<&'static str> = None;
309    // The `xml:lang` of the field element whose text comes next.
310    let mut pending_lang: Option<String> = None;
311    // A field key as stored: the class table's id when the class declares it,
312    // else the interned name.
313    let key_of = |declared: Option<&'static FastMap<&'static str, &'static AttrDef>>, local: &str| {
314        declared.and_then(|d| d.get(local)).map_or_else(|| intern(local), |a| a.id)
315    };
316
317    loop {
318        match reader.read_event_into(&mut buf) {
319            Ok(Event::Start(ref e)) => {
320                depth += 1;
321
322                match depth {
323                    1 => scope.absorb(e, reg),
324                    2 => {
325                        // Every CGMES and NC file declares its namespaces on the
326                        // root, but a nested redeclaration is legal RDF/XML, so
327                        // honour one when it appears. The End arm restores the
328                        // outer scope.
329                        let (mrid, described, has_xmlns) = scan_attrs(e)?;
330                        if has_xmlns {
331                            let mut inner = Scope {
332                                prefixes: scope.prefixes.clone(),
333                                default: scope.default,
334                            };
335                            inner.absorb(e, reg);
336                            nested = Some(std::mem::replace(&mut scope, inner));
337                        }
338
339                        current = resolve(&scope, reg, e.name().as_ref())?.map(|class| {
340                            let mut element = Element::new(class, mrid);
341                            if described {
342                                element.mark_described();
343                            }
344                            element
345                        });
346                        declared = current.as_ref().and_then(|c| reg.declared(c.class()));
347                    }
348                    3 => {
349                        if let Some(element) = current.as_mut() {
350                            let name = e.name();
351                            let (_, local_bytes) = split_qname(name.as_ref());
352                            let local = key_of(declared, std::str::from_utf8(local_bytes)?);
353                            let (res, lang) = field_attrs(e.attributes())?;
354                            if let Some(res) = res {
355                                element.add_field(local, FieldValue::Resource(res));
356                            } else {
357                                pending_key = Some(local);
358                                pending_lang = lang;
359                            }
360                        }
361                    }
362                    _ => {}
363                }
364            }
365
366            Ok(Event::Empty(ref e)) => {
367                match depth {
368                    1 => {
369                        // Top-level self-closing element: <cim:Foo rdf:ID="x" />
370                        // It can carry its own xmlns; with no children there is
371                        // nothing to restore afterwards, so resolve against a
372                        // throwaway scope.
373                        let (mrid, described, has_xmlns) = scan_attrs(e)?;
374                        let local_scope = if has_xmlns {
375                            let mut inner = Scope {
376                                prefixes: scope.prefixes.clone(),
377                                default: scope.default,
378                            };
379                            inner.absorb(e, reg);
380                            Some(inner)
381                        } else {
382                            None
383                        };
384                        let active = local_scope.as_ref().unwrap_or(&scope);
385                        if !mrid.is_empty()
386                            && let Some(class) = resolve(active, reg, e.name().as_ref())? {
387                                index(&mut ds.by_type, class.qualified, mrid.clone());
388                                let mut element = Element::new(class, mrid.clone());
389                                if described {
390                                    element.mark_described();
391                                }
392                                ds.entries.insert(mrid, element);
393                            }
394                    }
395                    2 => {
396                        // Self-closing field element within the current type block.
397                        if let Some(element) = current.as_mut()
398                            && let Some(res) = find_resource(e.attributes())? {
399                                let name = e.name();
400                                let (_, local_bytes) = split_qname(name.as_ref());
401                                let local = key_of(declared, std::str::from_utf8(local_bytes)?);
402                                element.add_field(local, FieldValue::Resource(res));
403                            }
404                    }
405                    _ => {}
406                }
407            }
408
409            Ok(Event::Text(ref e)) => {
410                if depth == 3
411                    && let (Some(element), Some(key)) = (current.as_mut(), pending_key.take()) {
412                        let text = e.unescape()?.trim().to_string();
413                        if !text.is_empty() {
414                            element.add_field(key, FieldValue::Text(text));
415                            if let Some(lang) = pending_lang.take() {
416                                element.add_lang(key, &lang);
417                            }
418                        }
419                    }
420            }
421
422            Ok(Event::End(_)) => {
423                if depth == 2 {
424                    pending_key = None;
425                    pending_lang = None;
426                    if let Some(element) = current.take()
427                        && !element.mrid().is_empty() {
428                            index(&mut ds.by_type, element.type_name(), element.mrid().to_string());
429                            ds.entries.insert(element.mrid().to_string(), element);
430                        }
431                    if let Some(outer) = nested.take() {
432                        scope = outer;
433                    }
434                }
435                depth = depth.saturating_sub(1);
436            }
437
438            Ok(Event::Eof) => break,
439            Err(e) => return Err(Box::new(e)),
440            _ => {}
441        }
442        buf.clear();
443    }
444
445    Ok(())
446}
447
448// --- helpers ----------------------------------------------------------------
449
450
451/// Strip the fragment from a URI, e.g. "http://example.com#foo" → "foo".
452fn strip_fragment(s: &str) -> String {
453    if let Some(i) = s.rfind('#') {
454        s[i + 1..].to_string()
455    } else {
456        s.trim_start_matches('#').to_string()
457    }
458}
459
460
461/// A field element's `rdf:resource` (as [`find_resource`] gives it) and
462/// `xml:lang`, in one pass over its attributes.
463fn field_attrs(
464    attrs: quick_xml::events::attributes::Attributes<'_>,
465) -> Result<(Option<String>, Option<String>), Box<dyn std::error::Error>> {
466    let (mut res, mut lang) = (None, None);
467    for attr in attrs.flatten() {
468        match attr.key.as_ref() {
469            b"rdf:resource" => res = Some(strip_fragment(std::str::from_utf8(&attr.value)?)),
470            b"xml:lang" => lang = Some(std::str::from_utf8(&attr.value)?.to_string()),
471            _ => {}
472        }
473    }
474    Ok((res, lang))
475}
476
477/// Find the value of the "rdf:resource" attribute from an element.
478fn find_resource(
479    attrs: quick_xml::events::attributes::Attributes<'_>,
480) -> Result<Option<String>, Box<dyn std::error::Error>> {
481    for attr in attrs.flatten() {
482        let key = std::str::from_utf8(attr.key.as_ref())?;
483        if key == "rdf:resource" {
484            return Ok(Some(strip_fragment(std::str::from_utf8(&attr.value)?)));
485        }
486    }
487    Ok(None)
488}