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 both things the parser needs:
238/// the MRID and whether the element redeclares any namespace. Scanning twice
239/// costs more than the nested-`xmlns` case it would serve, because every element
240/// pays for it and almost none declare one.
241fn scan_attrs(e: &BytesStart) -> Result<(String, bool), Box<dyn std::error::Error>> {
242    let mut mrid = String::new();
243    let mut has_xmlns = false;
244    for attr in e.attributes().flatten() {
245        let key = attr.key.as_ref();
246        if key == b"rdf:about" || key == b"rdf:ID" {
247            mrid = strip_fragment(std::str::from_utf8(&attr.value)?);
248        } else if key.starts_with(b"xmlns") {
249            has_xmlns = true;
250        }
251    }
252    Ok((mrid, has_xmlns))
253}
254
255/// Split a qualified name into its prefix and local part.
256fn split_qname(raw: &[u8]) -> (Option<&[u8]>, &[u8]) {
257    match raw.iter().position(|b| *b == b':') {
258        Some(i) => (Some(&raw[..i]), &raw[i + 1..]),
259        None => (None, raw),
260    }
261}
262
263/// Resolve an element name to its class.
264///
265/// The namespace wins when the document binds one we know. Otherwise we fall
266/// back to the bare local name, which is what every pre-namespace release did:
267/// real files bind prefixes we do not expect (one CGMES test file binds the
268/// ModelDescription IRI to `mdc`), and write elements under a namespace their
269/// class was not declared in (`<cim:BoundaryPoint>` where `BoundaryPoint` is
270/// declared in `CIM100-European#`). The fallback is therefore per element, not
271/// per document.
272fn resolve(
273    scope: &Scope,
274    reg: &'static TypeRegistry,
275    raw: &[u8],
276) -> Result<Option<&'static ClassDef>, Box<dyn std::error::Error>> {
277    let (prefix, local_bytes) = split_qname(raw);
278    let local = std::str::from_utf8(local_bytes)?;
279    if let Some(class) = scope.table(prefix).and_then(|t| t.get(local)) {
280        return Ok(Some(*class));
281    }
282    Ok(reg.bare().get(local).copied())
283}
284
285/// Parse an RDF/XML string into a CimDataset, using the provided type registry.
286fn parse_rdf(
287    content: &str,
288    reg: &'static TypeRegistry,
289    ds: &mut CimDataset,
290) -> Result<(), Box<dyn std::error::Error>> {
291    let mut reader = Reader::from_str(content);
292    let mut buf = Vec::new();
293
294    let mut depth: u32 = 0;
295    let mut scope = Scope::default();
296    // Outer scope parked while a nested `xmlns` override is in effect.
297    let mut nested: Option<Scope> = None;
298    // Resolved at the element's start tag, so an unregistered element never
299    // accumulates fields.
300    let mut current: Option<Element> = None;
301    // The attributes `current`'s class declares, which supply most field keys.
302    let mut declared: Option<&'static FastMap<&'static str, &'static AttrDef>> = None;
303    let mut pending_key: Option<&'static str> = None;
304    // The `xml:lang` of the field element whose text comes next.
305    let mut pending_lang: Option<String> = None;
306    // A field key as stored: the class table's id when the class declares it,
307    // else the interned name.
308    let key_of = |declared: Option<&'static FastMap<&'static str, &'static AttrDef>>, local: &str| {
309        declared.and_then(|d| d.get(local)).map_or_else(|| intern(local), |a| a.id)
310    };
311
312    loop {
313        match reader.read_event_into(&mut buf) {
314            Ok(Event::Start(ref e)) => {
315                depth += 1;
316
317                match depth {
318                    1 => scope.absorb(e, reg),
319                    2 => {
320                        // Every CGMES and NC file declares its namespaces on the
321                        // root, but a nested redeclaration is legal RDF/XML, so
322                        // honour one when it appears. The End arm restores the
323                        // outer scope.
324                        let (mrid, has_xmlns) = scan_attrs(e)?;
325                        if has_xmlns {
326                            let mut inner = Scope {
327                                prefixes: scope.prefixes.clone(),
328                                default: scope.default,
329                            };
330                            inner.absorb(e, reg);
331                            nested = Some(std::mem::replace(&mut scope, inner));
332                        }
333
334                        current = resolve(&scope, reg, e.name().as_ref())?.map(|class| Element::new(class, mrid));
335                        declared = current.as_ref().and_then(|c| reg.declared(c.class()));
336                    }
337                    3 => {
338                        if let Some(element) = current.as_mut() {
339                            let name = e.name();
340                            let (_, local_bytes) = split_qname(name.as_ref());
341                            let local = key_of(declared, std::str::from_utf8(local_bytes)?);
342                            let (res, lang) = field_attrs(e.attributes())?;
343                            if let Some(res) = res {
344                                element.add_field(local, FieldValue::Resource(res));
345                            } else {
346                                pending_key = Some(local);
347                                pending_lang = lang;
348                            }
349                        }
350                    }
351                    _ => {}
352                }
353            }
354
355            Ok(Event::Empty(ref e)) => {
356                match depth {
357                    1 => {
358                        // Top-level self-closing element: <cim:Foo rdf:ID="x" />
359                        // It can carry its own xmlns; with no children there is
360                        // nothing to restore afterwards, so resolve against a
361                        // throwaway scope.
362                        let (mrid, has_xmlns) = scan_attrs(e)?;
363                        let local_scope = if has_xmlns {
364                            let mut inner = Scope {
365                                prefixes: scope.prefixes.clone(),
366                                default: scope.default,
367                            };
368                            inner.absorb(e, reg);
369                            Some(inner)
370                        } else {
371                            None
372                        };
373                        let active = local_scope.as_ref().unwrap_or(&scope);
374                        if !mrid.is_empty()
375                            && let Some(class) = resolve(active, reg, e.name().as_ref())? {
376                                index(&mut ds.by_type, class.qualified, mrid.clone());
377                                ds.entries.insert(mrid.clone(), Element::new(class, mrid));
378                            }
379                    }
380                    2 => {
381                        // Self-closing field element within the current type block.
382                        if let Some(element) = current.as_mut()
383                            && let Some(res) = find_resource(e.attributes())? {
384                                let name = e.name();
385                                let (_, local_bytes) = split_qname(name.as_ref());
386                                let local = key_of(declared, std::str::from_utf8(local_bytes)?);
387                                element.add_field(local, FieldValue::Resource(res));
388                            }
389                    }
390                    _ => {}
391                }
392            }
393
394            Ok(Event::Text(ref e)) => {
395                if depth == 3
396                    && let (Some(element), Some(key)) = (current.as_mut(), pending_key.take()) {
397                        let text = e.unescape()?.trim().to_string();
398                        if !text.is_empty() {
399                            element.add_field(key, FieldValue::Text(text));
400                            if let Some(lang) = pending_lang.take() {
401                                element.add_lang(key, &lang);
402                            }
403                        }
404                    }
405            }
406
407            Ok(Event::End(_)) => {
408                if depth == 2 {
409                    pending_key = None;
410                    pending_lang = None;
411                    if let Some(element) = current.take()
412                        && !element.mrid().is_empty() {
413                            index(&mut ds.by_type, element.type_name(), element.mrid().to_string());
414                            ds.entries.insert(element.mrid().to_string(), element);
415                        }
416                    if let Some(outer) = nested.take() {
417                        scope = outer;
418                    }
419                }
420                depth = depth.saturating_sub(1);
421            }
422
423            Ok(Event::Eof) => break,
424            Err(e) => return Err(Box::new(e)),
425            _ => {}
426        }
427        buf.clear();
428    }
429
430    Ok(())
431}
432
433// --- helpers ----------------------------------------------------------------
434
435
436/// Strip the fragment from a URI, e.g. "http://example.com#foo" → "foo".
437fn strip_fragment(s: &str) -> String {
438    if let Some(i) = s.rfind('#') {
439        s[i + 1..].to_string()
440    } else {
441        s.trim_start_matches('#').to_string()
442    }
443}
444
445
446/// A field element's `rdf:resource` (as [`find_resource`] gives it) and
447/// `xml:lang`, in one pass over its attributes.
448fn field_attrs(
449    attrs: quick_xml::events::attributes::Attributes<'_>,
450) -> Result<(Option<String>, Option<String>), Box<dyn std::error::Error>> {
451    let (mut res, mut lang) = (None, None);
452    for attr in attrs.flatten() {
453        match attr.key.as_ref() {
454            b"rdf:resource" => res = Some(strip_fragment(std::str::from_utf8(&attr.value)?)),
455            b"xml:lang" => lang = Some(std::str::from_utf8(&attr.value)?.to_string()),
456            _ => {}
457        }
458    }
459    Ok((res, lang))
460}
461
462/// Find the value of the "rdf:resource" attribute from an element.
463fn find_resource(
464    attrs: quick_xml::events::attributes::Attributes<'_>,
465) -> Result<Option<String>, Box<dyn std::error::Error>> {
466    for attr in attrs.flatten() {
467        let key = std::str::from_utf8(attr.key.as_ref())?;
468        if key == "rdf:resource" {
469            return Ok(Some(strip_fragment(std::str::from_utf8(&attr.value)?)));
470        }
471    }
472    Ok(None)
473}