viva_genapi_xml/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! Load and pre-parse GenICam XML using quick-xml.
3//!
4//! This crate provides types and functions for parsing GenICam XML descriptions
5//! into a structured representation that can be used by the core evaluation engine.
6
7mod builders;
8#[cfg(feature = "fetch")]
9mod fetch;
10mod parsers;
11mod util;
12
13#[cfg(feature = "fetch")]
14pub use fetch::fetch_and_load_xml;
15
16use quick_xml::Reader;
17use quick_xml::events::{BytesStart, Event};
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20
21use parsers::{
22    parse_boolean, parse_category, parse_category_empty, parse_command, parse_command_empty,
23    parse_converter, parse_enum, parse_float, parse_int_converter, parse_integer, parse_string,
24    parse_struct_reg, parse_swissknife,
25};
26use util::{attribute_value, skip_element};
27
28/// Source of the numeric value backing an enumeration entry.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub enum EnumValueSrc {
31    /// Numeric literal declared directly in the XML.
32    Literal(i64),
33    /// Value obtained from another node referenced via `<pValue>`.
34    FromNode(String),
35}
36
37/// References to predicate provider nodes used for runtime gating.
38///
39/// GenICam's `pIsImplemented`, `pIsAvailable` and `pIsLocked` each point at
40/// another node (typically an Integer, Boolean or IntSwissKnife) whose current
41/// value gates whether a feature is implemented, accessible, or writable.
42/// These three references are shared by most node variants, so we collect them
43/// into one struct to keep the variant fields small.
44///
45/// All three fields are optional; a node with no predicates has
46/// [`PredicateRefs::default()`]. Serde fields use the GenICam XML spelling so
47/// round-trip JSON matches the XML attribute names.
48#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
49pub struct PredicateRefs {
50    /// Name of a node evaluating to non-zero iff the feature is implemented.
51    #[serde(
52        default,
53        skip_serializing_if = "Option::is_none",
54        rename = "pIsImplemented"
55    )]
56    pub p_is_implemented: Option<String>,
57    /// Name of a node evaluating to non-zero iff the feature is accessible now.
58    #[serde(
59        default,
60        skip_serializing_if = "Option::is_none",
61        rename = "pIsAvailable"
62    )]
63    pub p_is_available: Option<String>,
64    /// Name of a node evaluating to non-zero iff the feature is locked (RW→RO).
65    #[serde(default, skip_serializing_if = "Option::is_none", rename = "pIsLocked")]
66    pub p_is_locked: Option<String>,
67}
68
69impl PredicateRefs {
70    /// Iterate over all referenced node names (for dependency-graph walks).
71    pub fn references(&self) -> impl Iterator<Item = &str> {
72        [
73            self.p_is_implemented.as_deref(),
74            self.p_is_available.as_deref(),
75            self.p_is_locked.as_deref(),
76        ]
77        .into_iter()
78        .flatten()
79    }
80
81    /// `true` when every field is `None`.
82    pub fn is_empty(&self) -> bool {
83        self.p_is_implemented.is_none()
84            && self.p_is_available.is_none()
85            && self.p_is_locked.is_none()
86    }
87}
88
89/// Declaration for a single enumeration entry.
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct EnumEntryDecl {
92    /// Symbolic entry name exposed to clients.
93    pub name: String,
94    /// Source describing how to resolve the numeric value for this entry.
95    pub value: EnumValueSrc,
96    /// Optional user facing label.
97    pub display_name: Option<String>,
98    /// Predicate refs controlling whether this entry is implemented / available.
99    #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
100    pub predicates: PredicateRefs,
101}
102
103#[derive(Debug, Error)]
104#[non_exhaustive]
105pub enum XmlError {
106    #[error("xml: {0}")]
107    Xml(String),
108    #[error("invalid descriptor: {0}")]
109    Invalid(String),
110    #[error("transport: {0}")]
111    Transport(String),
112    #[error("unsupported URL: {0}")]
113    Unsupported(String),
114}
115
116/// Visibility level controlling which users see a feature.
117///
118/// GenICam defines four levels; features at a given level are visible to
119/// users at that level and above.
120#[derive(
121    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
122)]
123#[non_exhaustive]
124pub enum Visibility {
125    /// Shown to all users (default).
126    #[default]
127    Beginner,
128    /// Shown to experienced users.
129    Expert,
130    /// Shown only to advanced integrators.
131    Guru,
132    /// Hidden from all UI presentations.
133    Invisible,
134}
135
136impl Visibility {
137    pub(crate) fn parse(s: &str) -> Option<Self> {
138        match s.trim() {
139            "Beginner" => Some(Self::Beginner),
140            "Expert" => Some(Self::Expert),
141            "Guru" => Some(Self::Guru),
142            "Invisible" => Some(Self::Invisible),
143            _ => None,
144        }
145    }
146}
147
148/// Recommended UI representation for a numeric feature.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
150#[non_exhaustive]
151pub enum Representation {
152    Linear,
153    Logarithmic,
154    Boolean,
155    PureNumber,
156    HexNumber,
157    /// Display as dotted-quad IPv4 address.
158    IPV4Address,
159    /// Display as colon-separated MAC address.
160    MACAddress,
161}
162
163impl Representation {
164    pub(crate) fn parse(s: &str) -> Option<Self> {
165        match s.trim() {
166            "Linear" => Some(Self::Linear),
167            "Logarithmic" => Some(Self::Logarithmic),
168            "Boolean" => Some(Self::Boolean),
169            "PureNumber" => Some(Self::PureNumber),
170            "HexNumber" => Some(Self::HexNumber),
171            "IPV4Address" => Some(Self::IPV4Address),
172            "MACAddress" => Some(Self::MACAddress),
173            _ => None,
174        }
175    }
176}
177
178/// Shared metadata present on every GenICam node.
179#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
180pub struct NodeMeta {
181    /// Visibility level (Beginner, Expert, Guru, Invisible).
182    pub visibility: Visibility,
183    /// Long-form description of the feature.
184    pub description: Option<String>,
185    /// Short tooltip text for UI hover hints.
186    pub tooltip: Option<String>,
187    /// Human-readable label (may differ from the node name).
188    pub display_name: Option<String>,
189    /// Recommended UI representation for numeric features.
190    pub representation: Option<Representation>,
191}
192
193/// Access privileges for a GenICam node as described in the XML.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
195pub enum AccessMode {
196    /// Read-only node. The underlying register must not be modified by the client.
197    RO,
198    /// Write-only node. Reading the register is not permitted.
199    WO,
200    /// Read-write node. The register may be read and written by the client.
201    RW,
202}
203
204impl AccessMode {
205    pub(crate) fn parse(value: &str) -> Result<Self, XmlError> {
206        match value.trim().to_ascii_uppercase().as_str() {
207            "RO" => Ok(AccessMode::RO),
208            "WO" => Ok(AccessMode::WO),
209            "RW" => Ok(AccessMode::RW),
210            other => Err(XmlError::Invalid(format!("unknown access mode: {other}"))),
211        }
212    }
213}
214
215/// Register addressing metadata for a node.
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217pub enum Addressing {
218    /// Node uses a fixed register block regardless of selector state.
219    Fixed { address: u64, len: u32 },
220    /// Node switches between register blocks based on a selector value.
221    BySelector {
222        /// Name of the selector node controlling the address.
223        selector: String,
224        /// Mapping of selector value to `(address, length)` pair.
225        map: Vec<(String, (u64, u32))>,
226    },
227    /// Node resolves its register block through another node providing the address.
228    Indirect {
229        /// Node providing the register address at runtime.
230        p_address_node: String,
231        /// Length of the target register block in bytes.
232        len: u32,
233    },
234}
235
236/// Byte order used to interpret a multi-byte register payload.
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
238pub enum ByteOrder {
239    /// The first byte contains the least significant bits.
240    Little,
241    /// The first byte contains the most significant bits.
242    Big,
243}
244
245impl ByteOrder {
246    pub(crate) fn parse(tag: &str) -> Option<Self> {
247        match tag.trim().to_ascii_lowercase().as_str() {
248            "littleendian" => Some(ByteOrder::Little),
249            "bigendian" => Some(ByteOrder::Big),
250            _ => None,
251        }
252    }
253}
254
255fn default_big_endian() -> ByteOrder {
256    ByteOrder::Big
257}
258
259/// Bitfield metadata describing a sub-range of a register payload.
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
261pub struct BitField {
262    /// Starting bit offset within the interpreted register value.
263    pub bit_offset: u16,
264    /// Number of bits covered by the field.
265    pub bit_length: u16,
266    /// Byte order used when interpreting the enclosing register.
267    pub byte_order: ByteOrder,
268}
269
270/// Byte-level encoding of the payload behind a `<Float>` / `<FloatReg>` node.
271///
272/// GenICam's XSD lets a float feature be backed by either:
273///
274/// - a native IEEE 754 register — a `<FloatReg>` element, or a `<Float>` whose
275///   addressing reads exactly 4 or 8 bytes and carries no `<Scale>`/`<Offset>`;
276/// - a scaled integer register — a `<Float>` that declares `<Scale>` and/or
277///   `<Offset>` and reads the register bytes as a signed integer.
278///
279/// Prior to this field, `get_float`/`set_float` always used the scaled-integer
280/// codec. That returned the bit pattern of the IEEE 754 value decoded as i64
281/// for fields such as `AcquisitionFrameRate` (e.g. `1106247680` for 30.0) —
282/// see `doc/2026-04-12-genapi-numeric-type-dispatch.md`.
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
284pub enum FloatEncoding {
285    /// Register bytes are an IEEE 754 value (4 bytes → f32, 8 bytes → f64).
286    Ieee754,
287    /// Register bytes are a signed integer; `Scale` and `Offset` map to the
288    /// user-facing value.
289    #[default]
290    ScaledInteger,
291}
292
293/// Output type of a SwissKnife expression node.
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
295pub enum SkOutput {
296    /// Integer output. The runtime rounds the computed value to the nearest
297    /// integer with ties going towards zero.
298    Integer,
299    /// Floating point output. The runtime exposes the value as a `f64` without
300    /// any additional processing.
301    #[default]
302    Float,
303}
304
305impl SkOutput {
306    pub(crate) fn parse(tag: &str) -> Option<Self> {
307        match tag.trim().to_ascii_lowercase().as_str() {
308            "integer" => Some(SkOutput::Integer),
309            "float" => Some(SkOutput::Float),
310            _ => None,
311        }
312    }
313}
314
315/// Declaration of a SwissKnife node consisting of an arithmetic expression.
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct SwissKnifeDecl {
318    /// Feature name exposed to clients.
319    pub name: String,
320    /// Shared metadata.
321    pub meta: NodeMeta,
322    /// Raw expression string to be parsed by the runtime.
323    pub expr: String,
324    /// Mapping of variables used in the expression to provider node names.
325    pub variables: Vec<(String, String)>,
326    /// Desired output type (integer or float).
327    pub output: SkOutput,
328    /// Predicate refs gating implementation / availability.
329    #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
330    pub predicates: PredicateRefs,
331}
332
333/// Declaration of a Converter node for bidirectional value transformation.
334///
335/// Converters expose a floating-point value computed from an underlying
336/// register or node via a formula.
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct ConverterDecl {
339    /// Feature name exposed to clients.
340    pub name: String,
341    /// Shared metadata.
342    pub meta: NodeMeta,
343    /// Name of the node providing the raw register value.
344    pub p_value: String,
345    /// Expression converting raw register value to user-facing value (FROM direction).
346    pub formula_to: String,
347    /// Expression converting user-facing value back to raw register value (TO direction).
348    pub formula_from: String,
349    /// Mapping of expression variables to provider node names for `formula_to`.
350    pub variables_to: Vec<(String, String)>,
351    /// Mapping of expression variables to provider node names for `formula_from`.
352    pub variables_from: Vec<(String, String)>,
353    /// Engineering unit (if provided).
354    pub unit: Option<String>,
355    /// Desired output type.
356    pub output: SkOutput,
357    /// Predicate refs gating implementation / availability / lock state.
358    #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
359    pub predicates: PredicateRefs,
360}
361
362/// Declaration of an IntConverter node for integer-specific bidirectional conversion.
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct IntConverterDecl {
365    /// Feature name exposed to clients.
366    pub name: String,
367    /// Shared metadata.
368    pub meta: NodeMeta,
369    /// Name of the node providing the raw register value.
370    pub p_value: String,
371    /// Expression converting raw register value to user-facing value (FROM direction).
372    pub formula_to: String,
373    /// Expression converting user-facing value back to raw register value (TO direction).
374    pub formula_from: String,
375    /// Mapping of expression variables to provider node names for `formula_to`.
376    pub variables_to: Vec<(String, String)>,
377    /// Mapping of expression variables to provider node names for `formula_from`.
378    pub variables_from: Vec<(String, String)>,
379    /// Engineering unit (if provided).
380    pub unit: Option<String>,
381    /// Predicate refs gating implementation / availability / lock state.
382    #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
383    pub predicates: PredicateRefs,
384}
385
386/// Declaration of a StringReg node for string-typed register access.
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct StringDecl {
389    /// Feature name exposed to clients.
390    pub name: String,
391    /// Shared metadata.
392    pub meta: NodeMeta,
393    /// Addressing metadata for the register block.
394    pub addressing: Addressing,
395    /// Access privileges.
396    pub access: AccessMode,
397    /// Predicate refs gating implementation / availability / lock state.
398    #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
399    pub predicates: PredicateRefs,
400}
401
402/// Declaration of a node extracted from the GenICam XML description.
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub enum NodeDecl {
405    /// Integer feature backed by a register block or delegated via pValue.
406    Integer {
407        /// Feature name.
408        name: String,
409        /// Shared metadata (visibility, description, tooltip, etc.).
410        meta: NodeMeta,
411        /// Addressing metadata (absent when delegated via `pvalue`).
412        addressing: Option<Addressing>,
413        /// Length in bytes of the register payload.
414        len: u32,
415        /// Access privileges.
416        access: AccessMode,
417        /// Minimum allowed user value.
418        min: i64,
419        /// Maximum allowed user value.
420        max: i64,
421        /// Optional increment step enforced by the device.
422        inc: Option<i64>,
423        /// Engineering unit (if provided).
424        unit: Option<String>,
425        /// Optional bitfield metadata describing the active bit range.
426        bitfield: Option<BitField>,
427        /// Selector nodes referencing this feature.
428        selectors: Vec<String>,
429        /// Selector gating rules in the form (selector name, allowed values).
430        selected_if: Vec<(String, Vec<String>)>,
431        /// Node providing the value (delegates read/write to another node).
432        pvalue: Option<String>,
433        /// Node providing the dynamic maximum.
434        p_max: Option<String>,
435        /// Node providing the dynamic minimum.
436        p_min: Option<String>,
437        /// Static value (for constant integer nodes with `<Value>`).
438        value: Option<i64>,
439        /// Predicate refs gating implementation / availability / lock state.
440        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
441        predicates: PredicateRefs,
442    },
443    /// Floating point feature backed by an integer register with scaling,
444    /// a native IEEE 754 register, or delegated via pValue.
445    Float {
446        name: String,
447        meta: NodeMeta,
448        /// Addressing metadata (absent when delegated via `pvalue`).
449        addressing: Option<Addressing>,
450        access: AccessMode,
451        min: f64,
452        max: f64,
453        unit: Option<String>,
454        /// Optional rational scale applied to the raw register value.
455        scale: Option<(i64, i64)>,
456        /// Optional additive offset applied after scaling.
457        offset: Option<f64>,
458        selectors: Vec<String>,
459        selected_if: Vec<(String, Vec<String>)>,
460        /// Node providing the value (delegates read/write to another node).
461        pvalue: Option<String>,
462        /// How the register payload should be interpreted — native IEEE 754
463        /// or scaled integer. Defaults to [`FloatEncoding::ScaledInteger`] to
464        /// preserve existing behaviour for XML that relied on it.
465        #[serde(default)]
466        encoding: FloatEncoding,
467        /// Byte order of the register payload. Defaults to [`ByteOrder::Big`]
468        /// (the GenICam default).
469        #[serde(default = "default_big_endian")]
470        byte_order: ByteOrder,
471        /// Predicate refs gating implementation / availability / lock state.
472        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
473        predicates: PredicateRefs,
474    },
475    /// Enumeration feature exposing a list of named integer values.
476    Enum {
477        name: String,
478        meta: NodeMeta,
479        /// Addressing metadata (absent when delegated via `pvalue`).
480        addressing: Option<Addressing>,
481        access: AccessMode,
482        entries: Vec<EnumEntryDecl>,
483        default: Option<String>,
484        selectors: Vec<String>,
485        selected_if: Vec<(String, Vec<String>)>,
486        /// Node providing the integer value (delegates register read/write).
487        pvalue: Option<String>,
488        /// Predicate refs gating implementation / availability / lock state.
489        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
490        predicates: PredicateRefs,
491    },
492    /// Boolean feature backed by a single bit/byte register or delegated via pValue.
493    Boolean {
494        name: String,
495        meta: NodeMeta,
496        /// Addressing metadata (absent when delegated via `pvalue`).
497        addressing: Option<Addressing>,
498        len: u32,
499        access: AccessMode,
500        bitfield: Option<BitField>,
501        selectors: Vec<String>,
502        selected_if: Vec<(String, Vec<String>)>,
503        /// Node providing the value (delegates read/write to another node).
504        pvalue: Option<String>,
505        /// On value for pValue-backed booleans.
506        on_value: Option<i64>,
507        /// Off value for pValue-backed booleans.
508        off_value: Option<i64>,
509        /// Predicate refs gating implementation / availability / lock state.
510        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
511        predicates: PredicateRefs,
512    },
513    /// Command feature that triggers an action when written.
514    Command {
515        name: String,
516        meta: NodeMeta,
517        /// Fixed register address (absent when delegated via `pvalue`).
518        address: Option<u64>,
519        len: u32,
520        /// Node providing the command register (delegates write).
521        pvalue: Option<String>,
522        /// Value to write when executing the command.
523        command_value: Option<i64>,
524        /// Predicate refs gating implementation / availability / lock state.
525        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
526        predicates: PredicateRefs,
527    },
528    /// Category used to organise features.
529    Category {
530        name: String,
531        meta: NodeMeta,
532        children: Vec<String>,
533        /// Predicate refs gating implementation / availability.
534        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
535        predicates: PredicateRefs,
536    },
537    /// Computed value backed by an arithmetic expression referencing other nodes.
538    SwissKnife(SwissKnifeDecl),
539    /// Converter transforming raw values to/from user-facing floating-point values.
540    Converter(ConverterDecl),
541    /// IntConverter transforming raw values to/from user-facing integer values.
542    IntConverter(IntConverterDecl),
543    /// StringReg for string-typed register access.
544    String(StringDecl),
545}
546
547/// Full XML model describing the GenICam schema version and all declared nodes.
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct XmlModel {
550    /// Combined schema version extracted from the RegisterDescription attributes.
551    pub version: String,
552    /// Flat list of node declarations present in the document.
553    pub nodes: Vec<NodeDecl>,
554}
555
556/// Minimal metadata extracted from a quick XML scan.
557#[derive(Debug, Clone, PartialEq, Eq)]
558pub struct MinimalXmlInfo {
559    pub schema_version: Option<String>,
560    pub top_level_features: Vec<String>,
561}
562
563/// Parse a GenICam XML snippet and collect minimal metadata.
564pub fn parse_into_minimal_nodes(xml: &str) -> Result<MinimalXmlInfo, XmlError> {
565    let mut reader = Reader::from_str(xml);
566    reader.trim_text(true);
567    let mut buf = Vec::new();
568    let mut depth = 0usize;
569    let mut schema_version: Option<String> = None;
570    let mut top_level_features = Vec::new();
571
572    loop {
573        match reader.read_event_into(&mut buf) {
574            Ok(Event::Start(e)) => {
575                depth += 1;
576                handle_start(&e, depth, &mut schema_version, &mut top_level_features)?;
577            }
578            Ok(Event::Empty(e)) => {
579                depth += 1;
580                handle_start(&e, depth, &mut schema_version, &mut top_level_features)?;
581                if depth > 0 {
582                    depth = depth.saturating_sub(1);
583                }
584            }
585            Ok(Event::End(_)) => {
586                if depth > 0 {
587                    depth = depth.saturating_sub(1);
588                }
589            }
590            Ok(Event::Eof) => break,
591            Err(err) => return Err(XmlError::Xml(err.to_string())),
592            _ => {}
593        }
594        buf.clear();
595    }
596
597    Ok(MinimalXmlInfo {
598        schema_version,
599        top_level_features,
600    })
601}
602
603/// Parse a GenICam XML document into an [`XmlModel`].
604///
605/// The parser only understands a practical subset of the schema. Unknown tags
606/// are skipped which keeps the implementation forward compatible with richer
607/// documents.
608pub fn parse(xml: &str) -> Result<XmlModel, XmlError> {
609    let mut reader = Reader::from_str(xml);
610    reader.trim_text(true);
611    let mut buf = Vec::new();
612    let mut version = String::from("0.0.0");
613    let mut nodes = Vec::new();
614
615    loop {
616        match reader.read_event_into(&mut buf) {
617            Ok(Event::Start(ref e)) => match e.name().as_ref() {
618                b"RegisterDescription" => {
619                    version = schema_version_from(e)?;
620                }
621                b"Integer" | b"IntReg" | b"MaskedIntReg" => {
622                    let node = parse_integer(&mut reader, e.clone())?;
623                    nodes.push(node);
624                }
625                b"IntSwissKnife" => {
626                    let node = parse_swissknife(&mut reader, e.clone())?;
627                    nodes.push(node);
628                }
629                b"Float" | b"FloatReg" => {
630                    let node = parse_float(&mut reader, e.clone())?;
631                    nodes.push(node);
632                }
633                b"Enumeration" => {
634                    let node = parse_enum(&mut reader, e.clone())?;
635                    nodes.push(node);
636                }
637                b"Boolean" => {
638                    let node = parse_boolean(&mut reader, e.clone())?;
639                    nodes.push(node);
640                }
641                b"Command" => {
642                    let node = parse_command(&mut reader, e.clone())?;
643                    nodes.push(node);
644                }
645                b"Category" => {
646                    let node = parse_category(&mut reader, e.clone())?;
647                    nodes.push(node);
648                }
649                b"SwissKnife" => {
650                    let node = parse_swissknife(&mut reader, e.clone())?;
651                    nodes.push(node);
652                }
653                b"Converter" => {
654                    let node = parse_converter(&mut reader, e.clone())?;
655                    nodes.push(node);
656                }
657                b"IntConverter" => {
658                    let node = parse_int_converter(&mut reader, e.clone())?;
659                    nodes.push(node);
660                }
661                b"StringReg" | b"String" => {
662                    let node = parse_string(&mut reader, e.clone())?;
663                    nodes.push(node);
664                }
665                b"StructReg" => {
666                    let entries = parse_struct_reg(&mut reader, e.clone())?;
667                    nodes.extend(entries);
668                }
669                b"Group" => {
670                    // Group is a transparent container wrapping feature nodes;
671                    // let child events surface in the next loop iterations.
672                }
673                b"Port" => {
674                    // Port nodes are transport-level abstractions; skip them.
675                    skip_element(&mut reader, e.name().as_ref())?;
676                }
677                _ => {
678                    skip_element(&mut reader, e.name().as_ref())?;
679                }
680            },
681            Ok(Event::Empty(ref e)) => match e.name().as_ref() {
682                b"RegisterDescription" => {
683                    version = schema_version_from(e)?;
684                }
685                b"Command" => {
686                    let node = parse_command_empty(e)?;
687                    nodes.push(node);
688                }
689                b"Category" => {
690                    let node = parse_category_empty(e)?;
691                    nodes.push(node);
692                }
693                _ => {}
694            },
695            Ok(Event::Eof) => break,
696            Err(err) => return Err(XmlError::Xml(err.to_string())),
697            _ => {}
698        }
699        buf.clear();
700    }
701
702    Ok(XmlModel { version, nodes })
703}
704
705fn schema_version_from(event: &BytesStart<'_>) -> Result<String, XmlError> {
706    let major = attribute_value(event, b"SchemaMajorVersion")?;
707    let minor = attribute_value(event, b"SchemaMinorVersion")?;
708    let sub = attribute_value(event, b"SchemaSubMinorVersion")?;
709    let major = major.unwrap_or_else(|| "0".to_string());
710    let minor = minor.unwrap_or_else(|| "0".to_string());
711    let sub = sub.unwrap_or_else(|| "0".to_string());
712    Ok(format!("{major}.{minor}.{sub}"))
713}
714
715fn handle_start(
716    event: &BytesStart<'_>,
717    depth: usize,
718    schema_version: &mut Option<String>,
719    top_level: &mut Vec<String>,
720) -> Result<(), XmlError> {
721    if depth == 1 && schema_version.is_none() {
722        *schema_version = extract_schema_version(event);
723    } else if depth == 2 {
724        if let Some(name) = attribute_value(event, b"Name")? {
725            top_level.push(name);
726        } else {
727            top_level.push(String::from_utf8_lossy(event.name().as_ref()).to_string());
728        }
729    }
730    Ok(())
731}
732
733fn extract_schema_version(event: &BytesStart<'_>) -> Option<String> {
734    let major = attribute_value(event, b"SchemaMajorVersion").ok().flatten();
735    let minor = attribute_value(event, b"SchemaMinorVersion").ok().flatten();
736    let sub = attribute_value(event, b"SchemaSubMinorVersion")
737        .ok()
738        .flatten();
739    if major.is_none() && minor.is_none() && sub.is_none() {
740        None
741    } else {
742        let major = major.unwrap_or_else(|| "0".to_string());
743        let minor = minor.unwrap_or_else(|| "0".to_string());
744        let sub = sub.unwrap_or_else(|| "0".to_string());
745        Some(format!("{major}.{minor}.{sub}"))
746    }
747}
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752
753    const FIXTURE: &str = r#"
754        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="2" SchemaSubMinorVersion="3">
755            <Category Name="Root">
756                <pFeature>Gain</pFeature>
757                <pFeature>GainSelector</pFeature>
758            </Category>
759            <Integer Name="Width">
760                <Address>0x0000_0100</Address>
761                <Length>4</Length>
762                <AccessMode>RW</AccessMode>
763                <Min>16</Min>
764                <Max>4096</Max>
765                <Inc>2</Inc>
766            </Integer>
767            <Float Name="ExposureTime">
768                <Address>0x0000_0200</Address>
769                <Length>4</Length>
770                <AccessMode>RW</AccessMode>
771                <Min>10.0</Min>
772                <Max>200000.0</Max>
773                <Scale>1/1000</Scale>
774                <Offset>0.0</Offset>
775            </Float>
776            <Enumeration Name="GainSelector">
777                <Address>0x0000_0300</Address>
778                <Length>2</Length>
779                <AccessMode>RW</AccessMode>
780                <EnumEntry Name="AnalogAll" Value="0" />
781                <EnumEntry Name="DigitalAll" Value="1" />
782            </Enumeration>
783            <Integer Name="Gain">
784                <Address>0x0000_0304</Address>
785                <Length>2</Length>
786                <AccessMode>RW</AccessMode>
787                <Min>0</Min>
788                <Max>48</Max>
789                <pSelected>GainSelector</pSelected>
790                <Selected>AnalogAll</Selected>
791            </Integer>
792            <Boolean Name="GammaEnable">
793                <Address>0x0000_0400</Address>
794                <Length>1</Length>
795                <AccessMode>RW</AccessMode>
796            </Boolean>
797            <Command Name="AcquisitionStart">
798                <Address>0x0000_0500</Address>
799                <Length>4</Length>
800            </Command>
801        </RegisterDescription>
802    "#;
803
804    #[test]
805    fn parse_minimal_xml() {
806        let info = parse_into_minimal_nodes(FIXTURE).expect("parse xml");
807        assert_eq!(info.schema_version.as_deref(), Some("1.2.3"));
808        assert_eq!(info.top_level_features.len(), 7);
809        assert_eq!(info.top_level_features[0], "Root");
810    }
811
812    #[test]
813    fn parse_fixture_model() {
814        let model = parse(FIXTURE).expect("parse fixture");
815        assert_eq!(model.version, "1.2.3");
816        assert_eq!(model.nodes.len(), 7);
817        match &model.nodes[0] {
818            NodeDecl::Category { name, children, .. } => {
819                assert_eq!(name, "Root");
820                assert_eq!(
821                    children,
822                    &vec!["Gain".to_string(), "GainSelector".to_string()]
823                );
824            }
825            other => panic!("unexpected node: {other:?}"),
826        }
827        match &model.nodes[1] {
828            NodeDecl::Integer {
829                name,
830                min,
831                max,
832                inc,
833                ..
834            } => {
835                assert_eq!(name, "Width");
836                assert_eq!(*min, 16);
837                assert_eq!(*max, 4096);
838                assert_eq!(*inc, Some(2));
839            }
840            other => panic!("unexpected node: {other:?}"),
841        }
842        match &model.nodes[2] {
843            NodeDecl::Float {
844                name,
845                scale,
846                offset,
847                ..
848            } => {
849                assert_eq!(name, "ExposureTime");
850                assert_eq!(*scale, Some((1, 1000)));
851                assert_eq!(*offset, Some(0.0));
852            }
853            other => panic!("unexpected node: {other:?}"),
854        }
855        match &model.nodes[3] {
856            NodeDecl::Enum { name, entries, .. } => {
857                assert_eq!(name, "GainSelector");
858                assert_eq!(entries.len(), 2);
859                assert!(matches!(entries[0].value, EnumValueSrc::Literal(0)));
860                assert!(matches!(entries[1].value, EnumValueSrc::Literal(1)));
861            }
862            other => panic!("unexpected node: {other:?}"),
863        }
864        match &model.nodes[4] {
865            NodeDecl::Integer {
866                name, selected_if, ..
867            } => {
868                assert_eq!(name, "Gain");
869                assert_eq!(selected_if.len(), 1);
870                assert_eq!(selected_if[0].0, "GainSelector");
871                assert_eq!(selected_if[0].1, vec!["AnalogAll".to_string()]);
872            }
873            other => panic!("unexpected node: {other:?}"),
874        }
875    }
876
877    #[test]
878    fn parse_swissknife_node() {
879        const XML: &str = r#"
880            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
881                <Integer Name="GainRaw">
882                    <Address>0x3000</Address>
883                    <Length>4</Length>
884                    <AccessMode>RW</AccessMode>
885                    <Min>0</Min>
886                    <Max>1000</Max>
887                </Integer>
888                <Float Name="Offset">
889                    <Address>0x3008</Address>
890                    <Length>4</Length>
891                    <AccessMode>RW</AccessMode>
892                    <Min>-100.0</Min>
893                    <Max>100.0</Max>
894                </Float>
895                <SwissKnife Name="ComputedGain">
896                    <Expression>(GainRaw * 0.5) + Offset</Expression>
897                    <pVariable Name="GainRaw">GainRaw</pVariable>
898                    <pVariable Name="Offset">Offset</pVariable>
899                    <Output>Float</Output>
900                </SwissKnife>
901            </RegisterDescription>
902        "#;
903
904        let model = parse(XML).expect("parse swissknife xml");
905        assert_eq!(model.nodes.len(), 3);
906        let swiss = model
907            .nodes
908            .iter()
909            .find_map(|decl| match decl {
910                NodeDecl::SwissKnife(node) => Some(node),
911                _ => None,
912            })
913            .expect("swissknife present");
914        assert_eq!(swiss.name, "ComputedGain");
915        assert_eq!(swiss.expr, "(GainRaw * 0.5) + Offset");
916        assert_eq!(swiss.output, SkOutput::Float);
917        assert_eq!(swiss.variables.len(), 2);
918        assert_eq!(
919            swiss.variables[0],
920            ("GainRaw".to_string(), "GainRaw".to_string())
921        );
922        assert_eq!(
923            swiss.variables[1],
924            ("Offset".to_string(), "Offset".to_string())
925        );
926    }
927
928    #[test]
929    fn parse_int_swissknife_with_hex_and_ampersand() {
930        // Test that &amp; is decoded to & and hex literals are supported.
931        const XML: &str = r#"
932            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
933                <IntSwissKnife Name="PayloadSize">
934                    <pVariable Name="W">Width</pVariable>
935                    <pVariable Name="H">Height</pVariable>
936                    <pVariable Name="PF">PixelFormat</pVariable>
937                    <Formula>W * H * ((PF>>16)&amp;0xFF) / 8</Formula>
938                </IntSwissKnife>
939            </RegisterDescription>
940        "#;
941
942        let model = parse(XML).expect("parse intswissknife");
943        assert_eq!(model.nodes.len(), 1);
944        let swiss = model
945            .nodes
946            .iter()
947            .find_map(|decl| match decl {
948                NodeDecl::SwissKnife(node) => Some(node),
949                _ => None,
950            })
951            .expect("swissknife present");
952        assert_eq!(swiss.name, "PayloadSize");
953        // &amp; should be decoded to &
954        assert!(
955            swiss.expr.contains('&'),
956            "expression should contain decoded '&': {}",
957            swiss.expr
958        );
959        assert!(
960            swiss.expr.contains("0xFF"),
961            "expression should contain hex literal: {}",
962            swiss.expr
963        );
964    }
965
966    #[test]
967    fn parse_enum_entry_with_pvalue() {
968        const XML: &str = r#"
969            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
970                <Enumeration Name="Mode">
971                    <Address>0x0000_4000</Address>
972                    <Length>4</Length>
973                    <AccessMode>RW</AccessMode>
974                    <EnumEntry Name="Fixed10">
975                        <Value>10</Value>
976                    </EnumEntry>
977                    <EnumEntry Name="DynFromReg">
978                        <pValue>RegModeVal</pValue>
979                    </EnumEntry>
980                </Enumeration>
981                <Integer Name="RegModeVal">
982                    <Address>0x0000_4100</Address>
983                    <Length>4</Length>
984                    <AccessMode>RW</AccessMode>
985                    <Min>0</Min>
986                    <Max>65535</Max>
987                </Integer>
988            </RegisterDescription>
989        "#;
990
991        let model = parse(XML).expect("parse enum pvalue");
992        assert_eq!(model.nodes.len(), 2);
993        match &model.nodes[0] {
994            NodeDecl::Enum { entries, .. } => {
995                assert_eq!(entries.len(), 2);
996                assert!(matches!(entries[0].value, EnumValueSrc::Literal(10)));
997                match &entries[1].value {
998                    EnumValueSrc::FromNode(node) => assert_eq!(node, "RegModeVal"),
999                    other => panic!("unexpected entry value: {other:?}"),
1000                }
1001            }
1002            other => panic!("unexpected node: {other:?}"),
1003        }
1004    }
1005
1006    #[test]
1007    fn parse_indirect_addressing() {
1008        const XML: &str = r#"
1009            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1010                <Integer Name="RegAddr">
1011                    <Address>0x2000</Address>
1012                    <Length>4</Length>
1013                    <AccessMode>RW</AccessMode>
1014                    <Min>0</Min>
1015                    <Max>65535</Max>
1016                </Integer>
1017                <Integer Name="Gain" Address="0xFFFF">
1018                    <pAddress>RegAddr</pAddress>
1019                    <Length>4</Length>
1020                    <AccessMode>RW</AccessMode>
1021                    <Min>0</Min>
1022                    <Max>255</Max>
1023                </Integer>
1024            </RegisterDescription>
1025        "#;
1026
1027        let model = parse(XML).expect("parse indirect xml");
1028        assert_eq!(model.nodes.len(), 2);
1029        match &model.nodes[0] {
1030            NodeDecl::Integer {
1031                name, addressing, ..
1032            } => {
1033                assert_eq!(name, "RegAddr");
1034                assert!(
1035                    matches!(addressing, Some(Addressing::Fixed { address, len }) if *address == 0x2000 && *len == 4)
1036                );
1037            }
1038            other => panic!("unexpected node: {other:?}"),
1039        }
1040        match &model.nodes[1] {
1041            NodeDecl::Integer {
1042                name, addressing, ..
1043            } => {
1044                assert_eq!(name, "Gain");
1045                match addressing {
1046                    Some(Addressing::Indirect {
1047                        p_address_node,
1048                        len,
1049                    }) => {
1050                        assert_eq!(p_address_node, "RegAddr");
1051                        assert_eq!(*len, 4);
1052                    }
1053                    other => panic!("expected indirect addressing, got {other:?}"),
1054                }
1055            }
1056            other => panic!("unexpected node: {other:?}"),
1057        }
1058    }
1059
1060    #[test]
1061    fn parse_indirect_float_is_scaled_integer() {
1062        // Regression test: indirect <Float> with Length=4 and no
1063        // <Scale>/<Offset> must NOT be reclassified as IEEE 754. Pointer-
1064        // backed float features on real cameras are almost always scaled
1065        // integer registers; silently decoding their bytes as IEEE 754
1066        // corrupts the value.
1067        const XML: &str = r#"
1068            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1069                <Integer Name="RegAddr">
1070                    <Address>0x2000</Address>
1071                    <Length>4</Length>
1072                    <AccessMode>RW</AccessMode>
1073                </Integer>
1074                <Float Name="Exposure">
1075                    <pAddress>RegAddr</pAddress>
1076                    <Length>4</Length>
1077                    <AccessMode>RW</AccessMode>
1078                </Float>
1079            </RegisterDescription>
1080        "#;
1081
1082        let model = parse(XML).expect("parse indirect float");
1083        let float = model
1084            .nodes
1085            .iter()
1086            .find(|n| matches!(n, NodeDecl::Float { name, .. } if name == "Exposure"))
1087            .expect("Exposure node");
1088        match float {
1089            NodeDecl::Float {
1090                encoding,
1091                addressing,
1092                ..
1093            } => {
1094                assert!(
1095                    matches!(addressing, Some(Addressing::Indirect { .. })),
1096                    "expected indirect addressing"
1097                );
1098                assert_eq!(*encoding, FloatEncoding::ScaledInteger);
1099            }
1100            _ => unreachable!(),
1101        }
1102    }
1103
1104    #[test]
1105    fn parse_integer_bitfield_big_endian() {
1106        const XML: &str = r#"
1107            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1108                <Integer Name="Packed">
1109                    <Address>0x1000</Address>
1110                    <Length>4</Length>
1111                    <AccessMode>RW</AccessMode>
1112                    <Min>0</Min>
1113                    <Max>65535</Max>
1114                    <Lsb>8</Lsb>
1115                    <Msb>15</Msb>
1116                    <Endianness>BigEndian</Endianness>
1117                </Integer>
1118            </RegisterDescription>
1119        "#;
1120
1121        let model = parse(XML).expect("parse big-endian bitfield");
1122        assert_eq!(model.nodes.len(), 1);
1123        match &model.nodes[0] {
1124            NodeDecl::Integer { len, bitfield, .. } => {
1125                assert_eq!(*len, 4);
1126                let field = bitfield.as_ref().expect("bitfield present");
1127                assert_eq!(field.byte_order, ByteOrder::Big);
1128                assert_eq!(field.bit_length, 8);
1129                assert_eq!(field.bit_offset, 16);
1130            }
1131            other => panic!("unexpected node: {other:?}"),
1132        }
1133    }
1134
1135    #[test]
1136    fn parse_boolean_bitfield_default_length() {
1137        const XML: &str = r#"
1138            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1139                <Boolean Name="Flag">
1140                    <Address>0x2000</Address>
1141                    <Length>1</Length>
1142                    <AccessMode>RW</AccessMode>
1143                    <Bit>3</Bit>
1144                </Boolean>
1145            </RegisterDescription>
1146        "#;
1147
1148        let model = parse(XML).expect("parse boolean bitfield");
1149        assert_eq!(model.nodes.len(), 1);
1150        match &model.nodes[0] {
1151            NodeDecl::Boolean { len, bitfield, .. } => {
1152                assert_eq!(*len, 1);
1153                let bf = bitfield.as_ref().expect("bitfield present");
1154                assert_eq!(bf.byte_order, ByteOrder::Little);
1155                assert_eq!(bf.bit_length, 1);
1156                assert_eq!(bf.bit_offset, 3);
1157            }
1158            other => panic!("unexpected node: {other:?}"),
1159        }
1160    }
1161
1162    #[test]
1163    fn parse_integer_bitfield_mask() {
1164        const XML: &str = r#"
1165            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1166                <Integer Name="Masked">
1167                    <Address>0x3000</Address>
1168                    <Length>4</Length>
1169                    <AccessMode>RW</AccessMode>
1170                    <Min>0</Min>
1171                    <Max>65535</Max>
1172                    <Mask>0x0000FF00</Mask>
1173                </Integer>
1174            </RegisterDescription>
1175        "#;
1176
1177        let model = parse(XML).expect("parse mask bitfield");
1178        assert_eq!(model.nodes.len(), 1);
1179        match &model.nodes[0] {
1180            NodeDecl::Integer { bitfield, .. } => {
1181                let field = bitfield.as_ref().expect("bitfield present");
1182                assert_eq!(field.byte_order, ByteOrder::Little);
1183                assert_eq!(field.bit_length, 8);
1184                assert_eq!(field.bit_offset, 8);
1185            }
1186            other => panic!("unexpected node: {other:?}"),
1187        }
1188    }
1189
1190    #[test]
1191    fn parse_node_metadata() {
1192        const XML: &str = r#"
1193            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1194                <Integer Name="Width">
1195                    <Address>0x100</Address>
1196                    <Length>4</Length>
1197                    <AccessMode>RW</AccessMode>
1198                    <Min>16</Min>
1199                    <Max>4096</Max>
1200                    <Visibility>Expert</Visibility>
1201                    <Description>Image width in pixels.</Description>
1202                    <ToolTip>Width of the acquired image</ToolTip>
1203                    <DisplayName>Image Width</DisplayName>
1204                    <Representation>Linear</Representation>
1205                </Integer>
1206                <Float Name="Gain">
1207                    <Address>0x200</Address>
1208                    <Length>4</Length>
1209                    <AccessMode>RW</AccessMode>
1210                    <Min>0.0</Min>
1211                    <Max>48.0</Max>
1212                    <Unit>dB</Unit>
1213                    <Visibility>Beginner</Visibility>
1214                    <Representation>Logarithmic</Representation>
1215                </Float>
1216                <Category Name="Root">
1217                    <Visibility>Guru</Visibility>
1218                    <Description>Top-level category</Description>
1219                    <pFeature>Width</pFeature>
1220                    <pFeature>Gain</pFeature>
1221                </Category>
1222                <Enumeration Name="PixelFormat">
1223                    <Address>0x300</Address>
1224                    <Length>4</Length>
1225                    <AccessMode>RW</AccessMode>
1226                    <Visibility>Beginner</Visibility>
1227                    <ToolTip>Pixel format selector</ToolTip>
1228                    <EnumEntry Name="Mono8" Value="0" />
1229                </Enumeration>
1230            </RegisterDescription>
1231        "#;
1232
1233        let model = parse(XML).expect("parse metadata xml");
1234        assert_eq!(model.nodes.len(), 4);
1235
1236        // Integer with full metadata
1237        match &model.nodes[0] {
1238            NodeDecl::Integer { name, meta, .. } => {
1239                assert_eq!(name, "Width");
1240                assert_eq!(meta.visibility, Visibility::Expert);
1241                assert_eq!(meta.description.as_deref(), Some("Image width in pixels."));
1242                assert_eq!(meta.tooltip.as_deref(), Some("Width of the acquired image"));
1243                assert_eq!(meta.display_name.as_deref(), Some("Image Width"));
1244                assert_eq!(meta.representation, Some(Representation::Linear));
1245            }
1246            other => panic!("unexpected node: {other:?}"),
1247        }
1248
1249        // Float with visibility + representation
1250        match &model.nodes[1] {
1251            NodeDecl::Float { name, meta, .. } => {
1252                assert_eq!(name, "Gain");
1253                assert_eq!(meta.visibility, Visibility::Beginner);
1254                assert_eq!(meta.representation, Some(Representation::Logarithmic));
1255                assert!(meta.description.is_none());
1256            }
1257            other => panic!("unexpected node: {other:?}"),
1258        }
1259
1260        // Category with visibility + description
1261        match &model.nodes[2] {
1262            NodeDecl::Category { name, meta, .. } => {
1263                assert_eq!(name, "Root");
1264                assert_eq!(meta.visibility, Visibility::Guru);
1265                assert_eq!(meta.description.as_deref(), Some("Top-level category"));
1266            }
1267            other => panic!("unexpected node: {other:?}"),
1268        }
1269
1270        // Enum with visibility + tooltip
1271        match &model.nodes[3] {
1272            NodeDecl::Enum { name, meta, .. } => {
1273                assert_eq!(name, "PixelFormat");
1274                assert_eq!(meta.visibility, Visibility::Beginner);
1275                assert_eq!(meta.tooltip.as_deref(), Some("Pixel format selector"));
1276            }
1277            other => panic!("unexpected node: {other:?}"),
1278        }
1279    }
1280}