Skip to main content

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 quick_xml::name::QName;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22use parsers::{
23    parse_boolean, parse_category, parse_category_empty, parse_command, parse_command_empty,
24    parse_converter, parse_enum, parse_float, parse_int_converter, parse_integer, parse_register,
25    parse_string, parse_struct_reg, parse_swissknife,
26};
27use util::{attribute_value, skip_element};
28
29/// Source of the numeric value backing an enumeration entry.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub enum EnumValueSrc {
32    /// Numeric literal declared directly in the XML.
33    Literal(i64),
34    /// Value obtained from another node referenced via `<pValue>`.
35    FromNode(String),
36}
37
38/// References to predicate provider nodes used for runtime gating.
39///
40/// GenICam's `pIsImplemented`, `pIsAvailable` and `pIsLocked` each point at
41/// another node (typically an Integer, Boolean or IntSwissKnife) whose current
42/// value gates whether a feature is implemented, accessible, or writable.
43/// These three references are shared by most node variants, so we collect them
44/// into one struct to keep the variant fields small.
45///
46/// All three fields are optional; a node with no predicates has
47/// [`PredicateRefs::default()`]. Serde fields use the GenICam XML spelling so
48/// round-trip JSON matches the XML attribute names.
49#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
50pub struct PredicateRefs {
51    /// Name of a node evaluating to non-zero iff the feature is implemented.
52    #[serde(
53        default,
54        skip_serializing_if = "Option::is_none",
55        rename = "pIsImplemented"
56    )]
57    pub p_is_implemented: Option<String>,
58    /// Name of a node evaluating to non-zero iff the feature is accessible now.
59    #[serde(
60        default,
61        skip_serializing_if = "Option::is_none",
62        rename = "pIsAvailable"
63    )]
64    pub p_is_available: Option<String>,
65    /// Name of a node evaluating to non-zero iff the feature is locked (RW→RO).
66    #[serde(default, skip_serializing_if = "Option::is_none", rename = "pIsLocked")]
67    pub p_is_locked: Option<String>,
68}
69
70impl PredicateRefs {
71    /// Iterate over all referenced node names (for dependency-graph walks).
72    pub fn references(&self) -> impl Iterator<Item = &str> {
73        [
74            self.p_is_implemented.as_deref(),
75            self.p_is_available.as_deref(),
76            self.p_is_locked.as_deref(),
77        ]
78        .into_iter()
79        .flatten()
80    }
81
82    /// `true` when every field is `None`.
83    pub fn is_empty(&self) -> bool {
84        self.p_is_implemented.is_none()
85            && self.p_is_available.is_none()
86            && self.p_is_locked.is_none()
87    }
88}
89
90/// Declaration for a single enumeration entry.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct EnumEntryDecl {
93    /// Symbolic entry name exposed to clients.
94    pub name: String,
95    /// Source describing how to resolve the numeric value for this entry.
96    pub value: EnumValueSrc,
97    /// Optional user facing label.
98    pub display_name: Option<String>,
99    /// Predicate refs controlling whether this entry is implemented / available.
100    #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
101    pub predicates: PredicateRefs,
102}
103
104#[derive(Debug, Error)]
105#[non_exhaustive]
106pub enum XmlError {
107    #[error("xml: {0}")]
108    Xml(String),
109    #[error("invalid descriptor: {0}")]
110    Invalid(String),
111    #[error("transport: {0}")]
112    Transport(String),
113    #[error("unsupported URL: {0}")]
114    Unsupported(String),
115}
116
117/// Visibility level controlling which users see a feature.
118///
119/// GenICam defines four levels; features at a given level are visible to
120/// users at that level and above.
121#[derive(
122    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
123)]
124#[non_exhaustive]
125pub enum Visibility {
126    /// Shown to all users (default).
127    #[default]
128    Beginner,
129    /// Shown to experienced users.
130    Expert,
131    /// Shown only to advanced integrators.
132    Guru,
133    /// Hidden from all UI presentations.
134    Invisible,
135}
136
137impl Visibility {
138    pub(crate) fn parse(s: &str) -> Option<Self> {
139        match s.trim() {
140            "Beginner" => Some(Self::Beginner),
141            "Expert" => Some(Self::Expert),
142            "Guru" => Some(Self::Guru),
143            "Invisible" => Some(Self::Invisible),
144            _ => None,
145        }
146    }
147}
148
149/// Recommended UI representation for a numeric feature.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
151#[non_exhaustive]
152pub enum Representation {
153    Linear,
154    Logarithmic,
155    Boolean,
156    PureNumber,
157    HexNumber,
158    /// Display as dotted-quad IPv4 address.
159    IPV4Address,
160    /// Display as colon-separated MAC address.
161    MACAddress,
162}
163
164impl Representation {
165    pub(crate) fn parse(s: &str) -> Option<Self> {
166        match s.trim() {
167            "Linear" => Some(Self::Linear),
168            "Logarithmic" => Some(Self::Logarithmic),
169            "Boolean" => Some(Self::Boolean),
170            "PureNumber" => Some(Self::PureNumber),
171            "HexNumber" => Some(Self::HexNumber),
172            "IPV4Address" => Some(Self::IPV4Address),
173            "MACAddress" => Some(Self::MACAddress),
174            _ => None,
175        }
176    }
177}
178
179/// Shared metadata present on every GenICam node.
180#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
181pub struct NodeMeta {
182    /// Visibility level (Beginner, Expert, Guru, Invisible).
183    pub visibility: Visibility,
184    /// Long-form description of the feature.
185    pub description: Option<String>,
186    /// Short tooltip text for UI hover hints.
187    pub tooltip: Option<String>,
188    /// Human-readable label (may differ from the node name).
189    pub display_name: Option<String>,
190    /// Recommended UI representation for numeric features.
191    pub representation: Option<Representation>,
192}
193
194/// Access privileges for a GenICam node as described in the XML.
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
196pub enum AccessMode {
197    /// Read-only node. The underlying register must not be modified by the client.
198    RO,
199    /// Write-only node. Reading the register is not permitted.
200    WO,
201    /// Read-write node. The register may be read and written by the client.
202    RW,
203}
204
205impl AccessMode {
206    /// Parse an `<AccessMode>` value.
207    ///
208    /// Beyond the three spellings the standard defines, the single-letter forms
209    /// `R` / `W` are accepted: they appear in third-party GenICam documents
210    /// (including the standard's own conformance fixtures). An unrecognized
211    /// value falls back to `RW` — the same default as an absent `<AccessMode>` —
212    /// so one odd register cannot make a whole camera unusable. The runtime
213    /// still surfaces the device's own error if the access is genuinely denied.
214    pub(crate) fn parse(value: &str) -> Result<Self, XmlError> {
215        let trimmed = value.trim();
216        match trimmed.to_ascii_uppercase().as_str() {
217            "RO" | "R" => Ok(AccessMode::RO),
218            "WO" | "W" => Ok(AccessMode::WO),
219            "RW" | "WR" => Ok(AccessMode::RW),
220            other => {
221                tracing::warn!(
222                    access_mode = other,
223                    "unknown <AccessMode> value, assuming RW"
224                );
225                Ok(AccessMode::RW)
226            }
227        }
228    }
229}
230
231/// Register addressing metadata for a node.
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub enum Addressing {
234    /// Register address is the **sum** of every declared address term.
235    ///
236    /// This is the GenICam register address model: `<Address>`, `<pAddress>`
237    /// and `<pIndex>` may each appear any number of times on one register and
238    /// all of them add up. A node such as
239    ///
240    /// ```xml
241    /// <IntReg Name="SerialPortSource_Val">
242    ///   <pAddress>SerialPortSourceAddr</pAddress>
243    ///   <Address>0x00000008</Address>
244    /// </IntReg>
245    /// ```
246    ///
247    /// lives eight bytes past the block base, not at the base and not at
248    /// offset eight. Keeping only one term reads the wrong register and says
249    /// nothing about it — that was issue #35.
250    Sum {
251        /// Terms to add together, in declaration order.
252        terms: Vec<AddressTerm>,
253        /// Length of the register block in bytes.
254        len: u32,
255    },
256    /// Node switches between register blocks based on a selector value.
257    ///
258    /// This is `<Selected>`/`<pSelected>`, which picks one block rather than
259    /// contributing an offset, so it is not a term in the sum above.
260    BySelector {
261        /// Name of the selector node controlling the address.
262        selector: String,
263        /// Mapping of selector value to `(address, length)` pair.
264        map: Vec<(String, (u64, u32))>,
265    },
266}
267
268/// One contribution to a register address.
269#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270pub enum AddressTerm {
271    /// `<Address>` — a literal offset.
272    Fixed(u64),
273    /// `<pAddress>` — an offset read from another node at runtime.
274    Node(String),
275    /// `<pIndex>` — an index read from another node, scaled by a stride.
276    Index {
277        /// Node supplying the index.
278        node: String,
279        /// Stride each index step advances the address by.
280        offset: IndexOffset,
281    },
282}
283
284/// Stride applied to a `<pIndex>` value.
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286pub enum IndexOffset {
287    /// `<pIndex Offset="64">` — a literal stride.
288    Fixed(u64),
289    /// `<pIndex pOffset="Node">` — a stride read from another node.
290    Node(String),
291    /// `<pIndex>` with neither attribute: the stride is the register length.
292    Length,
293}
294
295impl Addressing {
296    /// A plain register at a literal address.
297    pub fn fixed(address: u64, len: u32) -> Self {
298        Addressing::Sum {
299            terms: vec![AddressTerm::Fixed(address)],
300            len,
301        }
302    }
303
304    /// Register block length in bytes, when it does not depend on a selector.
305    pub fn byte_len(&self) -> Option<u32> {
306        match self {
307            Addressing::Sum { len, .. } => Some(*len),
308            Addressing::BySelector { .. } => None,
309        }
310    }
311
312    /// Names of every node this addressing reads at resolution time.
313    ///
314    /// Used to build the invalidation graph: when one of these changes, the
315    /// address changes, so anything cached against it is stale.
316    pub fn referenced_nodes(&self) -> Vec<&str> {
317        match self {
318            Addressing::Sum { terms, .. } => terms
319                .iter()
320                .flat_map(|term| match term {
321                    AddressTerm::Fixed(_) => Vec::new(),
322                    AddressTerm::Node(node) => vec![node.as_str()],
323                    AddressTerm::Index { node, offset } => match offset {
324                        IndexOffset::Node(stride) => vec![node.as_str(), stride.as_str()],
325                        _ => vec![node.as_str()],
326                    },
327                })
328                .collect(),
329            Addressing::BySelector { selector, .. } => vec![selector.as_str()],
330        }
331    }
332}
333
334/// Whether a register's bits are interpreted as a signed or unsigned integer.
335///
336/// GenICam's default is `Unsigned`; `<Sign>Signed</Sign>` opts in. Getting
337/// this wrong is invisible until a register's top bit is set — a
338/// `GevCurrentIPAddress` of 192.168.1.160 then reads as a large negative
339/// number, and a mask comparison such as `(CTRL | 0xFDFFFFFF) = 0xFFFFFFFF`
340/// can never be true.
341#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
342pub enum Sign {
343    /// `<Sign>Unsigned</Sign>`, and the default when `<Sign>` is absent.
344    #[default]
345    Unsigned,
346    /// `<Sign>Signed</Sign>`: the payload is two's complement.
347    Signed,
348}
349
350impl Sign {
351    pub(crate) fn parse(tag: &str) -> Option<Self> {
352        match tag.trim().to_ascii_lowercase().as_str() {
353            "signed" => Some(Sign::Signed),
354            "unsigned" => Some(Sign::Unsigned),
355            _ => None,
356        }
357    }
358
359    /// Whether values should be sign-extended.
360    pub fn is_signed(self) -> bool {
361        matches!(self, Sign::Signed)
362    }
363}
364
365/// Byte order used to interpret a multi-byte register payload.
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
367pub enum ByteOrder {
368    /// The first byte contains the least significant bits.
369    Little,
370    /// The first byte contains the most significant bits.
371    Big,
372}
373
374impl ByteOrder {
375    pub(crate) fn parse(tag: &str) -> Option<Self> {
376        match tag.trim().to_ascii_lowercase().as_str() {
377            "littleendian" => Some(ByteOrder::Little),
378            "bigendian" => Some(ByteOrder::Big),
379            _ => None,
380        }
381    }
382}
383
384fn default_big_endian() -> ByteOrder {
385    ByteOrder::Big
386}
387
388/// Bitfield metadata describing a sub-range of a register payload.
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
390pub struct BitField {
391    /// Starting bit offset within the interpreted register value.
392    pub bit_offset: u16,
393    /// Number of bits covered by the field.
394    pub bit_length: u16,
395    /// Byte order used when interpreting the enclosing register.
396    pub byte_order: ByteOrder,
397}
398
399/// Byte-level encoding of the payload behind a `<Float>` / `<FloatReg>` node.
400///
401/// GenICam's XSD lets a float feature be backed by either:
402///
403/// - a native IEEE 754 register — a `<FloatReg>` element, or a `<Float>` whose
404///   addressing reads exactly 4 or 8 bytes and carries no `<Scale>`/`<Offset>`;
405/// - a scaled integer register — a `<Float>` that declares `<Scale>` and/or
406///   `<Offset>` and reads the register bytes as a signed integer.
407///
408/// Prior to this field, `get_float`/`set_float` always used the scaled-integer
409/// codec. That returned the bit pattern of the IEEE 754 value decoded as i64
410/// for fields such as `AcquisitionFrameRate` (e.g. `1106247680` for 30.0) —
411/// see `doc/2026-04-12-genapi-numeric-type-dispatch.md`.
412#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
413pub enum FloatEncoding {
414    /// Register bytes are an IEEE 754 value (4 bytes → f32, 8 bytes → f64).
415    Ieee754,
416    /// Register bytes are a signed integer; `Scale` and `Offset` map to the
417    /// user-facing value.
418    #[default]
419    ScaledInteger,
420}
421
422/// Output type of a SwissKnife expression node.
423#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
424pub enum SkOutput {
425    /// Integer output. The runtime rounds the computed value to the nearest
426    /// integer with ties going towards zero.
427    Integer,
428    /// Floating point output. The runtime exposes the value as a `f64` without
429    /// any additional processing.
430    #[default]
431    Float,
432}
433
434impl SkOutput {
435    pub(crate) fn parse(tag: &str) -> Option<Self> {
436        match tag.trim().to_ascii_lowercase().as_str() {
437            "integer" => Some(SkOutput::Integer),
438            "float" => Some(SkOutput::Float),
439            _ => None,
440        }
441    }
442}
443/// Named literals and sub-formulas declared alongside a formula.
444///
445/// GenApi lets a `<SwissKnife>`, `<IntSwissKnife>` or `<Converter>` name parts
446/// of its own formula:
447///
448/// ```xml
449/// <Constant Name="TEN">10</Constant>
450/// <Expression Name="XPLUS2">TEN + X</Expression>
451/// <Formula>TEN * XPLUS2</Formula>
452/// ```
453///
454/// Both are resolved by substitution when the runtime builds the node, so the
455/// evaluator never sees them. Declaration order matters: an `<Expression>` may
456/// refer to constants and to expressions declared before it.
457#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
458pub struct FormulaBindings {
459    /// `<Constant Name="..">literal</Constant>` pairs, in declaration order.
460    #[serde(default, skip_serializing_if = "Vec::is_empty")]
461    pub constants: Vec<(String, String)>,
462    /// `<Expression Name="..">formula</Expression>` pairs, in declaration order.
463    #[serde(default, skip_serializing_if = "Vec::is_empty")]
464    pub expressions: Vec<(String, String)>,
465}
466
467impl FormulaBindings {
468    /// Whether nothing was declared.
469    pub fn is_empty(&self) -> bool {
470        self.constants.is_empty() && self.expressions.is_empty()
471    }
472}
473
474/// Declaration of a SwissKnife node consisting of an arithmetic expression.
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct SwissKnifeDecl {
477    /// Feature name exposed to clients.
478    pub name: String,
479    /// Shared metadata.
480    pub meta: NodeMeta,
481    /// Raw expression string to be parsed by the runtime.
482    pub expr: String,
483    /// Mapping of variables used in the expression to provider node names.
484    pub variables: Vec<(String, String)>,
485    /// Desired output type (integer or float).
486    pub output: SkOutput,
487    /// Named literals and sub-formulas referenced by `expr`.
488    #[serde(default, skip_serializing_if = "FormulaBindings::is_empty")]
489    pub bindings: FormulaBindings,
490    /// Predicate refs gating implementation / availability.
491    #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
492    pub predicates: PredicateRefs,
493}
494
495/// Declaration of a Converter node for bidirectional value transformation.
496///
497/// Converters expose a floating-point value computed from an underlying
498/// register or node via a formula.
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct ConverterDecl {
501    /// Feature name exposed to clients.
502    pub name: String,
503    /// Shared metadata.
504    pub meta: NodeMeta,
505    /// Name of the node providing the raw register value.
506    pub p_value: String,
507    /// `<FormulaTo>`: converts the feature value to the raw register value.
508    /// This is the *write* direction; its `FROM` variable is the incoming
509    /// value.
510    pub formula_to: String,
511    /// `<FormulaFrom>`: converts the raw register value to the feature value.
512    /// This is the *read* direction; its `TO` variable is the raw value.
513    pub formula_from: String,
514    /// Mapping of formula variables to provider node names for `formula_to`.
515    pub variables_to: Vec<(String, String)>,
516    /// Mapping of formula variables to provider node names for `formula_from`.
517    pub variables_from: Vec<(String, String)>,
518    /// Named literals and sub-formulas referenced by either formula.
519    #[serde(default, skip_serializing_if = "FormulaBindings::is_empty")]
520    pub bindings: FormulaBindings,
521    /// Engineering unit (if provided).
522    pub unit: Option<String>,
523    /// Desired output type.
524    pub output: SkOutput,
525    /// Predicate refs gating implementation / availability / lock state.
526    #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
527    pub predicates: PredicateRefs,
528}
529
530/// Declaration of an IntConverter node for integer-specific bidirectional conversion.
531#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct IntConverterDecl {
533    /// Feature name exposed to clients.
534    pub name: String,
535    /// Shared metadata.
536    pub meta: NodeMeta,
537    /// Name of the node providing the raw register value.
538    pub p_value: String,
539    /// `<FormulaTo>`: converts the feature value to the raw register value.
540    /// This is the *write* direction; its `FROM` variable is the incoming
541    /// value.
542    pub formula_to: String,
543    /// `<FormulaFrom>`: converts the raw register value to the feature value.
544    /// This is the *read* direction; its `TO` variable is the raw value.
545    pub formula_from: String,
546    /// Mapping of formula variables to provider node names for `formula_to`.
547    pub variables_to: Vec<(String, String)>,
548    /// Mapping of formula variables to provider node names for `formula_from`.
549    pub variables_from: Vec<(String, String)>,
550    /// Named literals and sub-formulas referenced by either formula.
551    #[serde(default, skip_serializing_if = "FormulaBindings::is_empty")]
552    pub bindings: FormulaBindings,
553    /// Engineering unit (if provided).
554    pub unit: Option<String>,
555    /// Predicate refs gating implementation / availability / lock state.
556    #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
557    pub predicates: PredicateRefs,
558}
559
560/// Declaration of a StringReg node for string-typed register access.
561#[derive(Debug, Clone, Serialize, Deserialize)]
562pub struct StringDecl {
563    /// Feature name exposed to clients.
564    pub name: String,
565    /// Shared metadata.
566    pub meta: NodeMeta,
567    /// Addressing metadata for the register block.
568    pub addressing: Addressing,
569    /// Access privileges.
570    pub access: AccessMode,
571    /// Predicate refs gating implementation / availability / lock state.
572    #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
573    pub predicates: PredicateRefs,
574}
575
576/// Declaration of a `<Register>` node — raw byte-array register access.
577///
578/// The base register type: an address, a byte count, and no interpretation of
579/// the bytes at all. `<StringReg>` is this plus UTF-8/NUL decoding.
580///
581/// The length lives in [`Addressing`] rather than in a field here, because a
582/// bare `<pIndex>` strides by the register's length and address resolution
583/// already reads it from there. When `<pLength>` lands (GA-09 phase two) it
584/// gains a length that overrides that value *after* the address resolves.
585#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct RegisterDecl {
587    /// Feature name exposed to clients.
588    pub name: String,
589    /// Shared metadata.
590    pub meta: NodeMeta,
591    /// Addressing metadata for the register block, including its byte length.
592    pub addressing: Addressing,
593    /// Access privileges. An absent `<AccessMode>` means read-only.
594    pub access: AccessMode,
595    /// `<pPort>` target, verbatim; `None` when the element was absent.
596    ///
597    /// Anything other than `None` or `"Device"` is not routed yet — see GA-12.
598    #[serde(default, skip_serializing_if = "Option::is_none")]
599    pub port: Option<String>,
600    /// Predicate refs gating implementation / availability / lock state.
601    #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
602    pub predicates: PredicateRefs,
603}
604
605/// Declaration of a node extracted from the GenICam XML description.
606#[derive(Debug, Clone, Serialize, Deserialize)]
607#[non_exhaustive]
608pub enum NodeDecl {
609    /// Integer feature backed by a register block or delegated via pValue.
610    Integer {
611        /// Feature name.
612        name: String,
613        /// Shared metadata (visibility, description, tooltip, etc.).
614        meta: NodeMeta,
615        /// Addressing metadata (absent when delegated via `pvalue`).
616        addressing: Option<Addressing>,
617        /// Length in bytes of the register payload.
618        len: u32,
619        /// Access privileges.
620        access: AccessMode,
621        /// Minimum allowed user value.
622        min: i64,
623        /// Maximum allowed user value.
624        max: i64,
625        /// Optional increment step enforced by the device.
626        inc: Option<i64>,
627        /// Engineering unit (if provided).
628        unit: Option<String>,
629        /// Optional bitfield metadata describing the active bit range.
630        bitfield: Option<BitField>,
631        /// Whether the register payload is signed. Defaults to unsigned.
632        #[serde(default)]
633        sign: Sign,
634        /// Byte order of the register payload. Defaults to [`ByteOrder::Big`]
635        /// (the GenICam default). Recorded here as well as on `bitfield`,
636        /// because an unmasked register has no bitfield to carry it.
637        #[serde(default = "default_big_endian")]
638        byte_order: ByteOrder,
639        /// Selector nodes referencing this feature.
640        selectors: Vec<String>,
641        /// Selector gating rules in the form (selector name, allowed values).
642        selected_if: Vec<(String, Vec<String>)>,
643        /// Node providing the value (delegates read/write to another node).
644        pvalue: Option<String>,
645        /// Node providing the dynamic maximum.
646        p_max: Option<String>,
647        /// Node providing the dynamic minimum.
648        p_min: Option<String>,
649        /// Static value (for constant integer nodes with `<Value>`).
650        value: Option<i64>,
651        /// Predicate refs gating implementation / availability / lock state.
652        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
653        predicates: PredicateRefs,
654    },
655    /// Floating point feature backed by an integer register with scaling,
656    /// a native IEEE 754 register, or delegated via pValue.
657    Float {
658        name: String,
659        meta: NodeMeta,
660        /// Addressing metadata (absent when delegated via `pvalue`).
661        addressing: Option<Addressing>,
662        access: AccessMode,
663        min: f64,
664        max: f64,
665        unit: Option<String>,
666        /// Optional rational scale applied to the raw register value.
667        scale: Option<(i64, i64)>,
668        /// Optional additive offset applied after scaling.
669        offset: Option<f64>,
670        selectors: Vec<String>,
671        selected_if: Vec<(String, Vec<String>)>,
672        /// Node providing the value (delegates read/write to another node).
673        pvalue: Option<String>,
674        /// How the register payload should be interpreted — native IEEE 754
675        /// or scaled integer. Defaults to [`FloatEncoding::ScaledInteger`] to
676        /// preserve existing behaviour for XML that relied on it.
677        #[serde(default)]
678        encoding: FloatEncoding,
679        /// Byte order of the register payload. Defaults to [`ByteOrder::Big`]
680        /// (the GenICam default).
681        #[serde(default = "default_big_endian")]
682        byte_order: ByteOrder,
683        /// Predicate refs gating implementation / availability / lock state.
684        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
685        predicates: PredicateRefs,
686    },
687    /// Enumeration feature exposing a list of named integer values.
688    Enum {
689        name: String,
690        meta: NodeMeta,
691        /// Addressing metadata (absent when delegated via `pvalue`).
692        addressing: Option<Addressing>,
693        access: AccessMode,
694        entries: Vec<EnumEntryDecl>,
695        default: Option<String>,
696        selectors: Vec<String>,
697        selected_if: Vec<(String, Vec<String>)>,
698        /// Node providing the integer value (delegates register read/write).
699        pvalue: Option<String>,
700        /// Predicate refs gating implementation / availability / lock state.
701        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
702        predicates: PredicateRefs,
703    },
704    /// Boolean feature backed by a single bit/byte register or delegated via pValue.
705    Boolean {
706        name: String,
707        meta: NodeMeta,
708        /// Addressing metadata (absent when delegated via `pvalue`).
709        addressing: Option<Addressing>,
710        len: u32,
711        access: AccessMode,
712        bitfield: Option<BitField>,
713        selectors: Vec<String>,
714        selected_if: Vec<(String, Vec<String>)>,
715        /// Node providing the value (delegates read/write to another node).
716        pvalue: Option<String>,
717        /// On value for pValue-backed booleans.
718        on_value: Option<i64>,
719        /// Off value for pValue-backed booleans.
720        off_value: Option<i64>,
721        /// Predicate refs gating implementation / availability / lock state.
722        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
723        predicates: PredicateRefs,
724    },
725    /// Command feature that triggers an action when written.
726    Command {
727        name: String,
728        meta: NodeMeta,
729        /// Fixed register address (absent when delegated via `pvalue`).
730        address: Option<u64>,
731        len: u32,
732        /// Node providing the command register (delegates write).
733        pvalue: Option<String>,
734        /// Value to write when executing the command.
735        command_value: Option<i64>,
736        /// Predicate refs gating implementation / availability / lock state.
737        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
738        predicates: PredicateRefs,
739    },
740    /// Category used to organise features.
741    Category {
742        name: String,
743        meta: NodeMeta,
744        children: Vec<String>,
745        /// Predicate refs gating implementation / availability.
746        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
747        predicates: PredicateRefs,
748    },
749    /// Computed value backed by an arithmetic expression referencing other nodes.
750    SwissKnife(SwissKnifeDecl),
751    /// Converter transforming raw values to/from user-facing floating-point values.
752    Converter(ConverterDecl),
753    /// IntConverter transforming raw values to/from user-facing integer values.
754    IntConverter(IntConverterDecl),
755    /// StringReg for string-typed register access.
756    String(StringDecl),
757    /// Raw byte-array register access.
758    Register(RegisterDecl),
759}
760
761impl NodeDecl {
762    /// Feature name declared by this node.
763    pub fn name(&self) -> &str {
764        match self {
765            NodeDecl::Integer { name, .. }
766            | NodeDecl::Float { name, .. }
767            | NodeDecl::Enum { name, .. }
768            | NodeDecl::Boolean { name, .. }
769            | NodeDecl::Command { name, .. }
770            | NodeDecl::Category { name, .. } => name,
771            NodeDecl::SwissKnife(decl) => &decl.name,
772            NodeDecl::Converter(decl) => &decl.name,
773            NodeDecl::IntConverter(decl) => &decl.name,
774            NodeDecl::String(decl) => &decl.name,
775            NodeDecl::Register(decl) => &decl.name,
776        }
777    }
778
779    /// Node kind, matching the variant name.
780    ///
781    /// This is the declaration kind rather than the originating XML tag: an
782    /// `<IntReg>` and a `<MaskedIntReg>` both report `Integer`. Useful for
783    /// grouping and for reporting nodes that were dropped downstream of
784    /// parsing.
785    pub fn kind(&self) -> &'static str {
786        match self {
787            NodeDecl::Integer { .. } => "Integer",
788            NodeDecl::Float { .. } => "Float",
789            NodeDecl::Enum { .. } => "Enum",
790            NodeDecl::Boolean { .. } => "Boolean",
791            NodeDecl::Command { .. } => "Command",
792            NodeDecl::Category { .. } => "Category",
793            NodeDecl::SwissKnife(_) => "SwissKnife",
794            NodeDecl::Converter(_) => "Converter",
795            NodeDecl::IntConverter(_) => "IntConverter",
796            NodeDecl::String(_) => "String",
797            NodeDecl::Register(_) => "Register",
798        }
799    }
800}
801
802/// A node element that could not be parsed and was left out of the model.
803///
804/// Parsing isolates each node, so a declaration we cannot make sense of costs
805/// that one feature instead of the whole document. These records exist so the
806/// loss is visible rather than silent — surface them in a UI, log them, or
807/// assert on them in tests.
808#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
809pub struct SkippedNode {
810    /// XML tag of the node element, e.g. `Integer`.
811    pub tag: String,
812    /// `Name` attribute, when the element had one.
813    pub name: Option<String>,
814    /// Rendered parse error explaining why the node was dropped.
815    pub error: String,
816}
817
818/// Full XML model describing the GenICam schema version and all declared nodes.
819#[derive(Debug, Clone, Serialize, Deserialize)]
820pub struct XmlModel {
821    /// Combined schema version extracted from the RegisterDescription attributes.
822    pub version: String,
823    /// Flat list of node declarations present in the document.
824    pub nodes: Vec<NodeDecl>,
825    /// Node elements that failed to parse and were skipped. Empty for a clean
826    /// document.
827    #[serde(default, skip_serializing_if = "Vec::is_empty")]
828    pub skipped: Vec<SkippedNode>,
829}
830
831/// Minimal metadata extracted from a quick XML scan.
832#[derive(Debug, Clone, PartialEq, Eq)]
833pub struct MinimalXmlInfo {
834    pub schema_version: Option<String>,
835    pub top_level_features: Vec<String>,
836}
837
838/// Drop a leading UTF-8 byte-order mark.
839///
840/// A BOM is valid UTF-8 (`U+FEFF`), so it survives `String::from_utf8` and
841/// reaches the parser intact; The Imaging Source's DMK 33GP2000e ships one
842/// (issue #122). quick-xml removes it from its own view of the input but does
843/// **not** advance `Reader::buffer_position`, so every offset it reports is
844/// three bytes short of the true offset into `xml`. [`parse`] slices node
845/// elements out of `xml` by those offsets, so each slice lost its closing `>`
846/// and every node in the document was skipped. Stripping here keeps quick-xml's
847/// positions and our `&str` talking about the same bytes.
848fn strip_bom(xml: &str) -> &str {
849    xml.strip_prefix('\u{feff}').unwrap_or(xml)
850}
851
852/// Parse a GenICam XML snippet and collect minimal metadata.
853pub fn parse_into_minimal_nodes(xml: &str) -> Result<MinimalXmlInfo, XmlError> {
854    let xml = strip_bom(xml);
855    let mut reader = Reader::from_str(xml);
856    reader.config_mut().trim_text(true);
857    // Vendor XML is not ours to fix: a lone `&` in a tooltip must not stop us
858    // from reading the document.
859    reader.config_mut().allow_dangling_amp = true;
860    let mut buf = Vec::new();
861    let mut depth = 0usize;
862    let mut schema_version: Option<String> = None;
863    let mut top_level_features = Vec::new();
864
865    loop {
866        match reader.read_event_into(&mut buf) {
867            Ok(Event::Start(e)) => {
868                depth += 1;
869                handle_start(&e, depth, &mut schema_version, &mut top_level_features)?;
870            }
871            Ok(Event::Empty(e)) => {
872                depth += 1;
873                handle_start(&e, depth, &mut schema_version, &mut top_level_features)?;
874                depth = depth.saturating_sub(1);
875            }
876            Ok(Event::End(_)) => {
877                depth = depth.saturating_sub(1);
878            }
879            Ok(Event::Eof) => break,
880            Err(err) => return Err(XmlError::Xml(err.to_string())),
881            _ => {}
882        }
883        buf.clear();
884    }
885
886    Ok(MinimalXmlInfo {
887        schema_version,
888        top_level_features,
889    })
890}
891
892/// `true` for tags this parser turns into one or more [`NodeDecl`]s.
893fn is_node_tag(tag: &str) -> bool {
894    matches!(
895        tag,
896        "Integer"
897            | "IntReg"
898            | "MaskedIntReg"
899            | "IntSwissKnife"
900            | "SwissKnife"
901            | "Float"
902            | "FloatReg"
903            | "Enumeration"
904            | "Boolean"
905            | "Command"
906            | "Category"
907            | "Converter"
908            | "IntConverter"
909            | "Register"
910            | "StringReg"
911            | "String"
912            | "StructReg"
913    )
914}
915
916/// Record an element this parser has no node type for, or `None` if it is not
917/// a node at all.
918///
919/// A node declaration is told from a structural element by its `Name`
920/// attribute, which the GenApi schema requires on every node and on nothing
921/// else at this level. Without this, an unlisted tag fell through to
922/// `skip_element` and vanished — no log line, no [`XmlModel::skipped`] entry,
923/// nothing for a corpus test to trip over. `<Register>`, 56 declarations
924/// across 14 corpus documents, disappeared exactly this way.
925fn unknown_node(tag: &str, start: &BytesStart<'_>) -> Result<Option<SkippedNode>, XmlError> {
926    let Some(name) = attribute_value(start, "Name")? else {
927        return Ok(None);
928    };
929    let tag = tag.to_owned();
930    tracing::warn!(
931        tag = %tag,
932        node = %name,
933        "skipping node of an unsupported type"
934    );
935    Ok(Some(SkippedNode {
936        error: format!("unsupported node type <{tag}>"),
937        tag,
938        name: Some(name),
939    }))
940}
941
942/// Dispatch a node element to its parser. `start` must be the element's start
943/// event and `reader` must be positioned just after it.
944fn parse_node(
945    reader: &mut Reader<&[u8]>,
946    start: BytesStart<'_>,
947) -> Result<Vec<NodeDecl>, XmlError> {
948    let tag = start.name().as_ref().to_string();
949    let node = match tag.as_str() {
950        "Integer" | "IntReg" | "MaskedIntReg" => parse_integer(reader, start)?,
951        "IntSwissKnife" | "SwissKnife" => parse_swissknife(reader, start)?,
952        "Float" | "FloatReg" => parse_float(reader, start)?,
953        "Enumeration" => parse_enum(reader, start)?,
954        "Boolean" => parse_boolean(reader, start)?,
955        "Command" => parse_command(reader, start)?,
956        "Category" => parse_category(reader, start)?,
957        "Converter" => parse_converter(reader, start)?,
958        "IntConverter" => parse_int_converter(reader, start)?,
959        "Register" => parse_register(reader, start)?,
960        "StringReg" | "String" => parse_string(reader, start)?,
961        "StructReg" => return parse_struct_reg(reader, start),
962        other => {
963            return Err(XmlError::Invalid(format!("not a node element: {other}")));
964        }
965    };
966    Ok(vec![node])
967}
968
969/// Parse one node element in isolation, from a slice spanning the whole element.
970///
971/// A fresh reader over just this element means a parse failure cannot leave the
972/// document-level reader at an unknown position — the caller has already
973/// consumed exactly this element and is correctly positioned either way.
974fn parse_isolated_node(element: &str) -> Result<Vec<NodeDecl>, XmlError> {
975    let mut reader = Reader::from_str(element);
976    reader.config_mut().trim_text(true);
977    reader.config_mut().allow_dangling_amp = true;
978    let mut buf = Vec::new();
979    loop {
980        match reader.read_event_into(&mut buf) {
981            Ok(Event::Start(e)) => {
982                let start = e.into_owned();
983                return parse_node(&mut reader, start);
984            }
985            Ok(Event::Eof) => {
986                return Err(XmlError::Invalid("node element is empty".into()));
987            }
988            Err(err) => return Err(XmlError::Xml(err.to_string())),
989            // Leading whitespace or a comment before the start tag.
990            _ => {}
991        }
992    }
993}
994
995/// Parse a GenICam XML document into an [`XmlModel`].
996///
997/// The parser only understands a practical subset of the schema. Unknown tags
998/// are skipped which keeps the implementation forward compatible with richer
999/// documents.
1000///
1001/// Each node element is parsed in isolation: one declaration we cannot make
1002/// sense of costs that single feature and is recorded in [`XmlModel::skipped`],
1003/// rather than failing the load and leaving the camera unopenable. Only errors
1004/// that make the document as a whole unreadable are returned.
1005pub fn parse(xml: &str) -> Result<XmlModel, XmlError> {
1006    let xml = strip_bom(xml);
1007    let mut reader = Reader::from_str(xml);
1008    reader.config_mut().trim_text(true);
1009    // Vendor XML is not ours to fix: a lone `&` in a tooltip must not stop us
1010    // from opening the camera.
1011    reader.config_mut().allow_dangling_amp = true;
1012    let mut buf = Vec::new();
1013    let mut version = String::from("0.0.0");
1014    let mut nodes = Vec::new();
1015    let mut skipped = Vec::new();
1016
1017    loop {
1018        // Captured before the read so a node element's slice can be recovered
1019        // verbatim, start tag included.
1020        let element_start = reader.buffer_position() as usize;
1021        match reader.read_event_into(&mut buf) {
1022            Ok(Event::Start(ref e)) => match e.name().as_ref() {
1023                "RegisterDescription" => {
1024                    version = schema_version_from(e)?;
1025                }
1026                "Group" => {
1027                    // Group is a transparent container wrapping feature nodes;
1028                    // let child events surface in the next loop iterations.
1029                }
1030                "Port" => {
1031                    // Port nodes are transport-level abstractions; skip them.
1032                    skip_element(&mut reader, e.name().as_ref())?;
1033                }
1034                tag if is_node_tag(tag) => {
1035                    let tag = tag.to_owned();
1036                    let name = attribute_value(e, "Name")?;
1037                    // Consume the element up front: whatever the node parser
1038                    // makes of it, the document reader stays in step.
1039                    reader
1040                        .read_to_end(QName(&tag))
1041                        .map_err(|err| XmlError::Xml(err.to_string()))?;
1042                    // `get`, not `[..]`: this slice is built from reader
1043                    // offsets rather than from `xml` itself, and a byte-order
1044                    // mark used to desynchronise the two (#122). Indexing
1045                    // panics on a non-character-boundary index; `get` turns the
1046                    // same disagreement into one skipped feature, which is the
1047                    // right price to pay in the middle of a camera connect.
1048                    let element = xml
1049                        .get(element_start..reader.buffer_position() as usize)
1050                        .ok_or_else(|| {
1051                            XmlError::Invalid(format!(
1052                                "reader offset {element_start}..{} is not a character \
1053                                 boundary in the document",
1054                                reader.buffer_position()
1055                            ))
1056                        });
1057                    match element.and_then(parse_isolated_node) {
1058                        Ok(parsed) => nodes.extend(parsed),
1059                        Err(err) => {
1060                            tracing::warn!(
1061                                tag = %tag,
1062                                node = name.as_deref().unwrap_or("<unnamed>"),
1063                                error = %err,
1064                                "skipping unparsable node"
1065                            );
1066                            skipped.push(SkippedNode {
1067                                tag,
1068                                name,
1069                                error: err.to_string(),
1070                            });
1071                        }
1072                    }
1073                }
1074                tag => {
1075                    let unknown = unknown_node(tag, e)?;
1076                    skip_element(&mut reader, e.name().as_ref())?;
1077                    if let Some(record) = unknown {
1078                        skipped.push(record);
1079                    }
1080                }
1081            },
1082            Ok(Event::Empty(ref e)) => match e.name().as_ref() {
1083                "RegisterDescription" => {
1084                    version = schema_version_from(e)?;
1085                }
1086                "Command" => {
1087                    let node = parse_command_empty(e)?;
1088                    nodes.push(node);
1089                }
1090                "Category" => {
1091                    let node = parse_category_empty(e)?;
1092                    nodes.push(node);
1093                }
1094                // Same transport-level abstraction the Start arm skips; four
1095                // corpus documents declare it as `<Port Name="Device"/>`.
1096                "Port" => {}
1097                tag => {
1098                    if let Some(record) = unknown_node(tag, e)? {
1099                        skipped.push(record);
1100                    }
1101                }
1102            },
1103            Ok(Event::Eof) => break,
1104            Err(err) => return Err(XmlError::Xml(err.to_string())),
1105            _ => {}
1106        }
1107        buf.clear();
1108    }
1109
1110    if !skipped.is_empty() {
1111        tracing::warn!(
1112            count = skipped.len(),
1113            total = nodes.len() + skipped.len(),
1114            "some GenApi nodes could not be parsed and were skipped"
1115        );
1116    }
1117
1118    Ok(XmlModel {
1119        version,
1120        nodes,
1121        skipped,
1122    })
1123}
1124
1125fn schema_version_from(event: &BytesStart<'_>) -> Result<String, XmlError> {
1126    let major = attribute_value(event, "SchemaMajorVersion")?;
1127    let minor = attribute_value(event, "SchemaMinorVersion")?;
1128    let sub = attribute_value(event, "SchemaSubMinorVersion")?;
1129    let major = major.unwrap_or_else(|| "0".to_string());
1130    let minor = minor.unwrap_or_else(|| "0".to_string());
1131    let sub = sub.unwrap_or_else(|| "0".to_string());
1132    Ok(format!("{major}.{minor}.{sub}"))
1133}
1134
1135fn handle_start(
1136    event: &BytesStart<'_>,
1137    depth: usize,
1138    schema_version: &mut Option<String>,
1139    top_level: &mut Vec<String>,
1140) -> Result<(), XmlError> {
1141    if depth == 1 && schema_version.is_none() {
1142        *schema_version = extract_schema_version(event);
1143    } else if depth == 2 {
1144        if let Some(name) = attribute_value(event, "Name")? {
1145            top_level.push(name);
1146        } else {
1147            top_level.push(event.name().as_ref().to_string());
1148        }
1149    }
1150    Ok(())
1151}
1152
1153fn extract_schema_version(event: &BytesStart<'_>) -> Option<String> {
1154    let major = attribute_value(event, "SchemaMajorVersion").ok().flatten();
1155    let minor = attribute_value(event, "SchemaMinorVersion").ok().flatten();
1156    let sub = attribute_value(event, "SchemaSubMinorVersion")
1157        .ok()
1158        .flatten();
1159    if major.is_none() && minor.is_none() && sub.is_none() {
1160        None
1161    } else {
1162        let major = major.unwrap_or_else(|| "0".to_string());
1163        let minor = minor.unwrap_or_else(|| "0".to_string());
1164        let sub = sub.unwrap_or_else(|| "0".to_string());
1165        Some(format!("{major}.{minor}.{sub}"))
1166    }
1167}
1168
1169#[cfg(test)]
1170mod tests {
1171    use super::*;
1172
1173    const FIXTURE: &str = r#"
1174        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="2" SchemaSubMinorVersion="3">
1175            <Category Name="Root">
1176                <pFeature>Gain</pFeature>
1177                <pFeature>GainSelector</pFeature>
1178            </Category>
1179            <Integer Name="Width">
1180                <Address>0x0000_0100</Address>
1181                <Length>4</Length>
1182                <AccessMode>RW</AccessMode>
1183                <Min>16</Min>
1184                <Max>4096</Max>
1185                <Inc>2</Inc>
1186            </Integer>
1187            <Float Name="ExposureTime">
1188                <Address>0x0000_0200</Address>
1189                <Length>4</Length>
1190                <AccessMode>RW</AccessMode>
1191                <Min>10.0</Min>
1192                <Max>200000.0</Max>
1193                <Scale>1/1000</Scale>
1194                <Offset>0.0</Offset>
1195            </Float>
1196            <Enumeration Name="GainSelector">
1197                <Address>0x0000_0300</Address>
1198                <Length>2</Length>
1199                <AccessMode>RW</AccessMode>
1200                <EnumEntry Name="AnalogAll" Value="0" />
1201                <EnumEntry Name="DigitalAll" Value="1" />
1202            </Enumeration>
1203            <Integer Name="Gain">
1204                <Address>0x0000_0304</Address>
1205                <Length>2</Length>
1206                <AccessMode>RW</AccessMode>
1207                <Min>0</Min>
1208                <Max>48</Max>
1209                <pSelected>GainSelector</pSelected>
1210                <Selected>AnalogAll</Selected>
1211            </Integer>
1212            <Boolean Name="GammaEnable">
1213                <Address>0x0000_0400</Address>
1214                <Length>1</Length>
1215                <AccessMode>RW</AccessMode>
1216            </Boolean>
1217            <Command Name="AcquisitionStart">
1218                <Address>0x0000_0500</Address>
1219                <Length>4</Length>
1220            </Command>
1221        </RegisterDescription>
1222    "#;
1223
1224    #[test]
1225    fn parse_minimal_xml() {
1226        let info = parse_into_minimal_nodes(FIXTURE).expect("parse xml");
1227        assert_eq!(info.schema_version.as_deref(), Some("1.2.3"));
1228        assert_eq!(info.top_level_features.len(), 7);
1229        assert_eq!(info.top_level_features[0], "Root");
1230    }
1231
1232    #[test]
1233    fn parse_fixture_model() {
1234        let model = parse(FIXTURE).expect("parse fixture");
1235        assert_eq!(model.version, "1.2.3");
1236        assert_eq!(model.nodes.len(), 7);
1237        match &model.nodes[0] {
1238            NodeDecl::Category { name, children, .. } => {
1239                assert_eq!(name, "Root");
1240                assert_eq!(
1241                    children,
1242                    &vec!["Gain".to_string(), "GainSelector".to_string()]
1243                );
1244            }
1245            other => panic!("unexpected node: {other:?}"),
1246        }
1247        match &model.nodes[1] {
1248            NodeDecl::Integer {
1249                name,
1250                min,
1251                max,
1252                inc,
1253                ..
1254            } => {
1255                assert_eq!(name, "Width");
1256                assert_eq!(*min, 16);
1257                assert_eq!(*max, 4096);
1258                assert_eq!(*inc, Some(2));
1259            }
1260            other => panic!("unexpected node: {other:?}"),
1261        }
1262        match &model.nodes[2] {
1263            NodeDecl::Float {
1264                name,
1265                scale,
1266                offset,
1267                ..
1268            } => {
1269                assert_eq!(name, "ExposureTime");
1270                assert_eq!(*scale, Some((1, 1000)));
1271                assert_eq!(*offset, Some(0.0));
1272            }
1273            other => panic!("unexpected node: {other:?}"),
1274        }
1275        match &model.nodes[3] {
1276            NodeDecl::Enum { name, entries, .. } => {
1277                assert_eq!(name, "GainSelector");
1278                assert_eq!(entries.len(), 2);
1279                assert!(matches!(entries[0].value, EnumValueSrc::Literal(0)));
1280                assert!(matches!(entries[1].value, EnumValueSrc::Literal(1)));
1281            }
1282            other => panic!("unexpected node: {other:?}"),
1283        }
1284        match &model.nodes[4] {
1285            NodeDecl::Integer {
1286                name, selected_if, ..
1287            } => {
1288                assert_eq!(name, "Gain");
1289                assert_eq!(selected_if.len(), 1);
1290                assert_eq!(selected_if[0].0, "GainSelector");
1291                assert_eq!(selected_if[0].1, vec!["AnalogAll".to_string()]);
1292            }
1293            other => panic!("unexpected node: {other:?}"),
1294        }
1295    }
1296
1297    #[test]
1298    fn parse_swissknife_node() {
1299        const XML: &str = r#"
1300            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1301                <Integer Name="GainRaw">
1302                    <Address>0x3000</Address>
1303                    <Length>4</Length>
1304                    <AccessMode>RW</AccessMode>
1305                    <Min>0</Min>
1306                    <Max>1000</Max>
1307                </Integer>
1308                <Float Name="Offset">
1309                    <Address>0x3008</Address>
1310                    <Length>4</Length>
1311                    <AccessMode>RW</AccessMode>
1312                    <Min>-100.0</Min>
1313                    <Max>100.0</Max>
1314                </Float>
1315                <SwissKnife Name="ComputedGain">
1316                    <Expression>(GainRaw * 0.5) + Offset</Expression>
1317                    <pVariable Name="GainRaw">GainRaw</pVariable>
1318                    <pVariable Name="Offset">Offset</pVariable>
1319                    <Output>Float</Output>
1320                </SwissKnife>
1321            </RegisterDescription>
1322        "#;
1323
1324        let model = parse(XML).expect("parse swissknife xml");
1325        assert_eq!(model.nodes.len(), 3);
1326        let swiss = model
1327            .nodes
1328            .iter()
1329            .find_map(|decl| match decl {
1330                NodeDecl::SwissKnife(node) => Some(node),
1331                _ => None,
1332            })
1333            .expect("swissknife present");
1334        assert_eq!(swiss.name, "ComputedGain");
1335        assert_eq!(swiss.expr, "(GainRaw * 0.5) + Offset");
1336        assert_eq!(swiss.output, SkOutput::Float);
1337        assert_eq!(swiss.variables.len(), 2);
1338        assert_eq!(
1339            swiss.variables[0],
1340            ("GainRaw".to_string(), "GainRaw".to_string())
1341        );
1342        assert_eq!(
1343            swiss.variables[1],
1344            ("Offset".to_string(), "Offset".to_string())
1345        );
1346    }
1347
1348    #[test]
1349    fn parse_int_swissknife_with_hex_and_ampersand() {
1350        // Test that &amp; is decoded to & and hex literals are supported.
1351        const XML: &str = r#"
1352            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1353                <IntSwissKnife Name="PayloadSize">
1354                    <pVariable Name="W">Width</pVariable>
1355                    <pVariable Name="H">Height</pVariable>
1356                    <pVariable Name="PF">PixelFormat</pVariable>
1357                    <Formula>W * H * ((PF>>16)&amp;0xFF) / 8</Formula>
1358                </IntSwissKnife>
1359            </RegisterDescription>
1360        "#;
1361
1362        let model = parse(XML).expect("parse intswissknife");
1363        assert_eq!(model.nodes.len(), 1);
1364        let swiss = model
1365            .nodes
1366            .iter()
1367            .find_map(|decl| match decl {
1368                NodeDecl::SwissKnife(node) => Some(node),
1369                _ => None,
1370            })
1371            .expect("swissknife present");
1372        assert_eq!(swiss.name, "PayloadSize");
1373        // &amp; should be decoded to &
1374        assert!(
1375            swiss.expr.contains('&'),
1376            "expression should contain decoded '&': {}",
1377            swiss.expr
1378        );
1379        assert!(
1380            swiss.expr.contains("0xFF"),
1381            "expression should contain hex literal: {}",
1382            swiss.expr
1383        );
1384    }
1385
1386    #[test]
1387    fn parse_enum_entry_with_pvalue() {
1388        const XML: &str = r#"
1389            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1390                <Enumeration Name="Mode">
1391                    <Address>0x0000_4000</Address>
1392                    <Length>4</Length>
1393                    <AccessMode>RW</AccessMode>
1394                    <EnumEntry Name="Fixed10">
1395                        <Value>10</Value>
1396                    </EnumEntry>
1397                    <EnumEntry Name="DynFromReg">
1398                        <pValue>RegModeVal</pValue>
1399                    </EnumEntry>
1400                </Enumeration>
1401                <Integer Name="RegModeVal">
1402                    <Address>0x0000_4100</Address>
1403                    <Length>4</Length>
1404                    <AccessMode>RW</AccessMode>
1405                    <Min>0</Min>
1406                    <Max>65535</Max>
1407                </Integer>
1408            </RegisterDescription>
1409        "#;
1410
1411        let model = parse(XML).expect("parse enum pvalue");
1412        assert_eq!(model.nodes.len(), 2);
1413        match &model.nodes[0] {
1414            NodeDecl::Enum { entries, .. } => {
1415                assert_eq!(entries.len(), 2);
1416                assert!(matches!(entries[0].value, EnumValueSrc::Literal(10)));
1417                match &entries[1].value {
1418                    EnumValueSrc::FromNode(node) => assert_eq!(node, "RegModeVal"),
1419                    other => panic!("unexpected entry value: {other:?}"),
1420                }
1421            }
1422            other => panic!("unexpected node: {other:?}"),
1423        }
1424    }
1425
1426    /// `<Address>` and `<pAddress>` on the same register are summed, in the
1427    /// order they were declared.
1428    ///
1429    /// FLIR and Point Grey write exactly this shape — `<pAddress>` for the
1430    /// block base, `<Address>` for the offset within it — and dropping either
1431    /// term reads the wrong register (issue #35).
1432    #[test]
1433    fn address_terms_are_summed() {
1434        const XML: &str = r#"
1435            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1436                <Integer Name="RegAddr">
1437                    <Address>0x2000</Address>
1438                    <Length>4</Length>
1439                    <AccessMode>RW</AccessMode>
1440                    <Min>0</Min>
1441                    <Max>65535</Max>
1442                </Integer>
1443                <Integer Name="Gain">
1444                    <pAddress>RegAddr</pAddress>
1445                    <Address>0x00000008</Address>
1446                    <Length>4</Length>
1447                    <AccessMode>RW</AccessMode>
1448                    <Min>0</Min>
1449                    <Max>255</Max>
1450                </Integer>
1451            </RegisterDescription>
1452        "#;
1453
1454        let model = parse(XML).expect("parse indirect xml");
1455        assert_eq!(model.nodes.len(), 2);
1456        match &model.nodes[0] {
1457            NodeDecl::Integer {
1458                name, addressing, ..
1459            } => {
1460                assert_eq!(name, "RegAddr");
1461                assert_eq!(addressing.as_ref(), Some(&Addressing::fixed(0x2000, 4)));
1462            }
1463            other => panic!("unexpected node: {other:?}"),
1464        }
1465        match &model.nodes[1] {
1466            NodeDecl::Integer {
1467                name, addressing, ..
1468            } => {
1469                assert_eq!(name, "Gain");
1470                match addressing {
1471                    Some(Addressing::Sum { terms, len }) => {
1472                        assert_eq!(
1473                            terms,
1474                            &[AddressTerm::Node("RegAddr".into()), AddressTerm::Fixed(0x8),]
1475                        );
1476                        assert_eq!(*len, 4);
1477                    }
1478                    other => panic!("expected summed addressing, got {other:?}"),
1479                }
1480            }
1481            other => panic!("unexpected node: {other:?}"),
1482        }
1483    }
1484
1485    /// `<pIndex>` contributes an index scaled by its stride.
1486    #[test]
1487    fn parse_p_index_term() {
1488        const XML: &str = r#"
1489            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1490                <MaskedIntReg Name="RegTriggerInqDelay">
1491                    <Address>0x13400</Address>
1492                    <pIndex Offset="64">IntTriggerSelector</pIndex>
1493                    <Length>4</Length>
1494                    <AccessMode>RO</AccessMode>
1495                    <Bit>30</Bit>
1496                </MaskedIntReg>
1497            </RegisterDescription>
1498        "#;
1499
1500        let model = parse(XML).expect("parse pIndex xml");
1501        match &model.nodes[0] {
1502            NodeDecl::Integer { addressing, .. } => match addressing {
1503                Some(Addressing::Sum { terms, .. }) => assert_eq!(
1504                    terms,
1505                    &[
1506                        AddressTerm::Fixed(0x13400),
1507                        AddressTerm::Index {
1508                            node: "IntTriggerSelector".into(),
1509                            offset: IndexOffset::Fixed(64),
1510                        },
1511                    ]
1512                ),
1513                other => panic!("expected summed addressing, got {other:?}"),
1514            },
1515            other => panic!("unexpected node: {other:?}"),
1516        }
1517    }
1518
1519    /// A `<StructReg>` shares its address terms with every `<StructEntry>`.
1520    ///
1521    /// Point Grey declares `<Address>` next to `<pAddress>` here; before the
1522    /// additive model the base was dropped and all 38 inquiry bits in the
1523    /// document read register 0.
1524    #[test]
1525    fn struct_reg_inherits_all_address_terms() {
1526        const XML: &str = r#"
1527            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1528                <StructReg Comment="Gain Inquiry Register">
1529                    <Address>0x520</Address>
1530                    <pAddress>CamRegBaseAddress</pAddress>
1531                    <Length>4</Length>
1532                    <AccessMode>RO</AccessMode>
1533                    <Endianess>BigEndian</Endianess>
1534                    <StructEntry Name="GainPresInq_Bit"><Bit>0</Bit></StructEntry>
1535                    <StructEntry Name="GainAutoInq_Bit"><Bit>6</Bit></StructEntry>
1536                </StructReg>
1537            </RegisterDescription>
1538        "#;
1539
1540        let model = parse(XML).expect("parse struct reg");
1541        assert_eq!(model.nodes.len(), 2);
1542        for node in &model.nodes {
1543            match node {
1544                NodeDecl::Integer { addressing, .. } => match addressing {
1545                    Some(Addressing::Sum { terms, len }) => {
1546                        assert_eq!(
1547                            terms,
1548                            &[
1549                                AddressTerm::Fixed(0x520),
1550                                AddressTerm::Node("CamRegBaseAddress".into()),
1551                            ]
1552                        );
1553                        assert_eq!(*len, 4);
1554                    }
1555                    other => panic!("expected summed addressing, got {other:?}"),
1556                },
1557                other => panic!("unexpected node: {other:?}"),
1558            }
1559        }
1560    }
1561
1562    /// A `<StructReg>` with no address at all is dropped rather than silently
1563    /// pointed at register zero.
1564    #[test]
1565    fn struct_reg_without_address_is_skipped() {
1566        const XML: &str = r#"
1567            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1568                <StructReg>
1569                    <Length>4</Length>
1570                    <AccessMode>RO</AccessMode>
1571                    <StructEntry Name="SomeInq_Bit"><Bit>0</Bit></StructEntry>
1572                </StructReg>
1573            </RegisterDescription>
1574        "#;
1575
1576        let model = parse(XML).expect("document still parses");
1577        assert!(model.nodes.is_empty());
1578        assert_eq!(model.skipped.len(), 1);
1579        assert!(model.skipped[0].error.contains("Address"));
1580    }
1581
1582    #[test]
1583    fn parse_indirect_float_is_scaled_integer() {
1584        // Regression test: indirect <Float> with Length=4 and no
1585        // <Scale>/<Offset> must NOT be reclassified as IEEE 754. Pointer-
1586        // backed float features on real cameras are almost always scaled
1587        // integer registers; silently decoding their bytes as IEEE 754
1588        // corrupts the value.
1589        const XML: &str = r#"
1590            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1591                <Integer Name="RegAddr">
1592                    <Address>0x2000</Address>
1593                    <Length>4</Length>
1594                    <AccessMode>RW</AccessMode>
1595                </Integer>
1596                <Float Name="Exposure">
1597                    <pAddress>RegAddr</pAddress>
1598                    <Length>4</Length>
1599                    <AccessMode>RW</AccessMode>
1600                </Float>
1601            </RegisterDescription>
1602        "#;
1603
1604        let model = parse(XML).expect("parse indirect float");
1605        let float = model
1606            .nodes
1607            .iter()
1608            .find(|n| matches!(n, NodeDecl::Float { name, .. } if name == "Exposure"))
1609            .expect("Exposure node");
1610        match float {
1611            NodeDecl::Float {
1612                encoding,
1613                addressing,
1614                ..
1615            } => {
1616                assert!(
1617                    matches!(
1618                        addressing,
1619                        Some(Addressing::Sum { terms, .. })
1620                            if terms.iter().any(|t| matches!(t, AddressTerm::Node(_)))
1621                    ),
1622                    "expected a pAddress term"
1623                );
1624                assert_eq!(*encoding, FloatEncoding::ScaledInteger);
1625            }
1626            _ => unreachable!(),
1627        }
1628    }
1629
1630    /// Big-endian `<LSB>`/`<MSB>` are counted **from the MSB**, so a
1631    /// conformant document has `<LSB>` >= `<MSB>` and the pair's *minimum* is
1632    /// the offset of the field's most significant bit.
1633    ///
1634    /// The fixture is real: `AVT_Manta_G125B.xml` declares GigE Vision's
1635    /// bootstrap `GevSCPSPacketSize` at `0xD04` exactly this way, and the
1636    /// standard fixes that register's layout — the packet size is the *low*
1637    /// 16 bits. `viva_genapi::bitops` turns `bit_offset = 16` over 4 bytes into
1638    /// `shift = 32 - 16 - 16 = 0`, i.e. the low half. The end-to-end decode is
1639    /// asserted in `viva-genapi`, which owns the extraction.
1640    ///
1641    /// This test previously used `<Lsb>8</Lsb><Msb>15</Msb>` with `BigEndian` —
1642    /// a shape that appears **zero** times in the 38-document vendor corpus and
1643    /// is inverted against its own byte order. It encoded the issue-#120 defect
1644    /// rather than catching it.
1645    #[test]
1646    fn parse_integer_bitfield_big_endian() {
1647        const XML: &str = r#"
1648            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1649                <Integer Name="RegSCPSPacketSize">
1650                    <Address>0xD04</Address>
1651                    <Length>4</Length>
1652                    <AccessMode>RW</AccessMode>
1653                    <LSB>31</LSB>
1654                    <MSB>16</MSB>
1655                    <Endianess>BigEndian</Endianess>
1656                </Integer>
1657            </RegisterDescription>
1658        "#;
1659
1660        let model = parse(XML).expect("parse big-endian bitfield");
1661        assert_eq!(model.nodes.len(), 1);
1662        match &model.nodes[0] {
1663            NodeDecl::Integer { len, bitfield, .. } => {
1664                assert_eq!(*len, 4);
1665                let field = bitfield.as_ref().expect("bitfield present");
1666                assert_eq!(field.byte_order, ByteOrder::Big);
1667                assert_eq!(field.bit_length, 16);
1668                assert_eq!(field.bit_offset, 16);
1669            }
1670            other => panic!("unexpected node: {other:?}"),
1671        }
1672    }
1673
1674    /// `<Bit>` is `<LSB>` and `<MSB>` at the same index, so it inherits the same
1675    /// orientation: on a big-endian register `<Bit>0</Bit>` is the *most*
1676    /// significant bit.
1677    ///
1678    /// This is the exact shape behind issue #120 — FLIR gates `ExposureTime` on
1679    /// three such registers sharing one 32-bit word at `0x000C1000`.
1680    #[test]
1681    fn parse_big_endian_single_bit_is_counted_from_the_msb() {
1682        const XML: &str = r#"
1683            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1684                <Integer Name="ExposureTime_Imp">
1685                    <Address>0x000C1000</Address>
1686                    <Length>4</Length>
1687                    <AccessMode>RO</AccessMode>
1688                    <Bit>0</Bit>
1689                    <Endianess>BigEndian</Endianess>
1690                </Integer>
1691            </RegisterDescription>
1692        "#;
1693
1694        let model = parse(XML).expect("parse big-endian single bit");
1695        match &model.nodes[0] {
1696            NodeDecl::Integer { bitfield, .. } => {
1697                let field = bitfield.as_ref().expect("bitfield present");
1698                assert_eq!(field.bit_length, 1);
1699                // Offset from the MSB, so `bitops` shifts by 31 and reads the
1700                // top bit. Before #120 this was 31, which shifted by 0.
1701                assert_eq!(field.bit_offset, 0);
1702            }
1703            other => panic!("unexpected node: {other:?}"),
1704        }
1705    }
1706
1707    /// The GenICam schema spells the bit-range elements `LSB` and `MSB`, and
1708    /// every one of the 1 419 declarations inside register nodes across the
1709    /// vendor corpus uses that spelling — none uses `Lsb`.
1710    ///
1711    /// `parsers::numeric` matched only the mixed-case form, so it dropped the
1712    /// bit range from every `<MaskedIntReg>` in every real document and read the
1713    /// whole register instead. `parsers::struct_reg` already accepted both,
1714    /// which is another way the two bitfield paths had drifted apart. Both
1715    /// spellings must parse, and to the same field.
1716    #[test]
1717    fn both_spellings_of_lsb_and_msb_are_accepted() {
1718        fn bitfield_of(lsb_tag: &str, msb_tag: &str) -> BitField {
1719            let xml = format!(
1720                r#"<RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="1">
1721                     <MaskedIntReg Name="Field">
1722                       <Address>0xD04</Address><Length>4</Length><AccessMode>RW</AccessMode>
1723                       <{lsb_tag}>31</{lsb_tag}><{msb_tag}>16</{msb_tag}>
1724                       <Endianess>BigEndian</Endianess>
1725                     </MaskedIntReg>
1726                   </RegisterDescription>"#
1727            );
1728            let model = parse(&xml).expect("parse");
1729            match &model.nodes[0] {
1730                NodeDecl::Integer { bitfield, .. } => *bitfield.as_ref().expect("bitfield present"),
1731                other => panic!("unexpected node: {other:?}"),
1732            }
1733        }
1734
1735        let schema = bitfield_of("LSB", "MSB");
1736        let mixed = bitfield_of("Lsb", "Msb");
1737        assert_eq!(schema.bit_offset, 16);
1738        assert_eq!(schema.bit_length, 16);
1739        assert_eq!(schema, mixed);
1740    }
1741
1742    /// Both spellings must reach the `<Boolean>` parser too, not only the
1743    /// numeric one.
1744    ///
1745    /// `<Boolean>` is parsed by `parsers::symbolic`, a different function from
1746    /// the `<Integer>`/`<MaskedIntReg>` path, and it matched the shared
1747    /// `TAG_LSB`/`TAG_MSB` constants. Renaming those to the schema spelling for
1748    /// GA-22 therefore *swapped* which spelling worked here instead of
1749    /// accepting both — and a register-backed Boolean wider than one byte whose
1750    /// bit range is dropped is not merely misread, it fails
1751    /// `Boolean node {name} requires explicit bitfield metadata` and is skipped
1752    /// outright. Caught in review on
1753    /// [#124](https://github.com/VitalyVorobyev/viva-genicam/pull/124).
1754    #[test]
1755    fn boolean_accepts_both_spellings_of_lsb_and_msb() {
1756        fn flag_bitfield(lsb_tag: &str, msb_tag: &str) -> BitField {
1757            let xml = format!(
1758                r#"<RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="1">
1759                     <Boolean Name="Flag">
1760                       <Address>0x2000</Address><Length>4</Length><AccessMode>RW</AccessMode>
1761                       <{lsb_tag}>31</{lsb_tag}><{msb_tag}>31</{msb_tag}>
1762                       <Endianess>BigEndian</Endianess>
1763                     </Boolean>
1764                   </RegisterDescription>"#
1765            );
1766            let model = parse(&xml).expect("parse");
1767            assert!(model.skipped.is_empty(), "skipped: {:?}", model.skipped);
1768            match &model.nodes[0] {
1769                NodeDecl::Boolean { bitfield, .. } => *bitfield.as_ref().expect("bitfield present"),
1770                other => panic!("unexpected node: {other:?}"),
1771            }
1772        }
1773
1774        let schema = flag_bitfield("LSB", "MSB");
1775        let mixed = flag_bitfield("Lsb", "Msb");
1776        // Index 31 from the MSB of a 4-byte register is the least significant
1777        // bit, so `bitops` shifts by zero.
1778        assert_eq!(schema.bit_offset, 31);
1779        assert_eq!(schema.bit_length, 1);
1780        assert_eq!(schema, mixed);
1781    }
1782
1783    /// `<Mask>` is the one bitfield source that stays LSB-relative under `Big`,
1784    /// because it is a literal register value rather than a GenICam bit index.
1785    ///
1786    /// It has zero corpus occurrences, so only this test keeps the distinction
1787    /// honest: the #120 fix removes the endianness conversion for `<LSB>`/
1788    /// `<MSB>`/`<Bit>` and must **keep** it here.
1789    #[test]
1790    fn big_endian_mask_stays_lsb_relative() {
1791        const XML: &str = r#"
1792            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1793                <Integer Name="Masked">
1794                    <Address>0x3000</Address>
1795                    <Length>4</Length>
1796                    <AccessMode>RW</AccessMode>
1797                    <Mask>0x0000FF00</Mask>
1798                    <Endianess>BigEndian</Endianess>
1799                </Integer>
1800            </RegisterDescription>
1801        "#;
1802
1803        let model = parse(XML).expect("parse big-endian mask");
1804        match &model.nodes[0] {
1805            NodeDecl::Integer { bitfield, .. } => {
1806                let field = bitfield.as_ref().expect("bitfield present");
1807                assert_eq!(field.byte_order, ByteOrder::Big);
1808                assert_eq!(field.bit_length, 8);
1809                // Bits 8..15 counted from the LSB are bits 16..23 from the MSB.
1810                assert_eq!(field.bit_offset, 16);
1811            }
1812            other => panic!("unexpected node: {other:?}"),
1813        }
1814    }
1815    #[test]
1816    fn parse_boolean_bitfield_default_length() {
1817        const XML: &str = r#"
1818            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1819                <Boolean Name="Flag">
1820                    <Address>0x2000</Address>
1821                    <Length>1</Length>
1822                    <AccessMode>RW</AccessMode>
1823                    <Bit>3</Bit>
1824                </Boolean>
1825            </RegisterDescription>
1826        "#;
1827
1828        let model = parse(XML).expect("parse boolean bitfield");
1829        assert_eq!(model.nodes.len(), 1);
1830        match &model.nodes[0] {
1831            NodeDecl::Boolean { len, bitfield, .. } => {
1832                assert_eq!(*len, 1);
1833                let bf = bitfield.as_ref().expect("bitfield present");
1834                assert_eq!(bf.byte_order, ByteOrder::Little);
1835                assert_eq!(bf.bit_length, 1);
1836                assert_eq!(bf.bit_offset, 3);
1837            }
1838            other => panic!("unexpected node: {other:?}"),
1839        }
1840    }
1841
1842    /// GA-28: `<Endianess>` on a plain `<IntReg>` used to go only into the
1843    /// bitfield builder, which discards it when nothing sets a bit range — so
1844    /// 311 declarations across 16 of the 38 corpus documents decoded
1845    /// byte-swapped. All three spellings the parser accepts are checked,
1846    /// because vendors use all three.
1847    #[test]
1848    fn plain_integer_records_its_declared_byte_order() {
1849        for tag in ["Endianess", "Endianness", "ByteOrder"] {
1850            let xml = format!(
1851                r#"
1852            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1853                <IntReg Name="Reg">
1854                    <Address>0x3000</Address>
1855                    <Length>4</Length>
1856                    <AccessMode>RO</AccessMode>
1857                    <{tag}>LittleEndian</{tag}>
1858                </IntReg>
1859            </RegisterDescription>
1860        "#
1861            );
1862
1863            let model = parse(&xml).unwrap_or_else(|err| panic!("parse <{tag}>: {err}"));
1864            match &model.nodes[0] {
1865                NodeDecl::Integer {
1866                    byte_order,
1867                    bitfield,
1868                    ..
1869                } => {
1870                    assert_eq!(*byte_order, ByteOrder::Little, "<{tag}>");
1871                    // No <LSB>/<MSB>/<Bit>/<Mask>, so there is no bitfield to
1872                    // have carried the order — which is the whole defect.
1873                    assert!(bitfield.is_none(), "<{tag}>");
1874                }
1875                other => panic!("unexpected node: {other:?}"),
1876            }
1877        }
1878    }
1879
1880    /// GenICam's default is big-endian, and a document that says nothing must
1881    /// keep getting it.
1882    #[test]
1883    fn plain_integer_without_endianness_defaults_to_big() {
1884        const XML: &str = r#"
1885            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1886                <IntReg Name="Reg">
1887                    <Address>0x3000</Address>
1888                    <Length>4</Length>
1889                    <AccessMode>RO</AccessMode>
1890                </IntReg>
1891            </RegisterDescription>
1892        "#;
1893
1894        let model = parse(XML).expect("parse");
1895        match &model.nodes[0] {
1896            NodeDecl::Integer { byte_order, .. } => assert_eq!(*byte_order, ByteOrder::Big),
1897            other => panic!("unexpected node: {other:?}"),
1898        }
1899    }
1900
1901    /// Negative control. A masked register takes the bitfield path, which
1902    /// already handled byte order and must keep doing so — the decl field is
1903    /// recorded but not consulted. Asserting both stops a later refactor
1904    /// quietly routing masked registers through the new path.
1905    #[test]
1906    fn masked_integer_keeps_the_bitfield_byte_order_path() {
1907        const XML: &str = r#"
1908            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1909                <IntReg Name="Reg">
1910                    <Address>0x3000</Address>
1911                    <Length>4</Length>
1912                    <AccessMode>RO</AccessMode>
1913                    <Endianess>LittleEndian</Endianess>
1914                    <LSB>15</LSB>
1915                    <MSB>0</MSB>
1916                </IntReg>
1917            </RegisterDescription>
1918        "#;
1919
1920        let model = parse(XML).expect("parse");
1921        match &model.nodes[0] {
1922            NodeDecl::Integer {
1923                byte_order,
1924                bitfield,
1925                ..
1926            } => {
1927                assert_eq!(*byte_order, ByteOrder::Little);
1928                let field = bitfield.as_ref().expect("bitfield present");
1929                assert_eq!(field.byte_order, ByteOrder::Little);
1930            }
1931            other => panic!("unexpected node: {other:?}"),
1932        }
1933    }
1934
1935    #[test]
1936    fn parse_integer_bitfield_mask() {
1937        const XML: &str = r#"
1938            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1939                <Integer Name="Masked">
1940                    <Address>0x3000</Address>
1941                    <Length>4</Length>
1942                    <AccessMode>RW</AccessMode>
1943                    <Min>0</Min>
1944                    <Max>65535</Max>
1945                    <Mask>0x0000FF00</Mask>
1946                </Integer>
1947            </RegisterDescription>
1948        "#;
1949
1950        let model = parse(XML).expect("parse mask bitfield");
1951        assert_eq!(model.nodes.len(), 1);
1952        match &model.nodes[0] {
1953            NodeDecl::Integer { bitfield, .. } => {
1954                let field = bitfield.as_ref().expect("bitfield present");
1955                assert_eq!(field.byte_order, ByteOrder::Little);
1956                assert_eq!(field.bit_length, 8);
1957                assert_eq!(field.bit_offset, 8);
1958            }
1959            other => panic!("unexpected node: {other:?}"),
1960        }
1961    }
1962
1963    #[test]
1964    fn parse_node_metadata() {
1965        const XML: &str = r#"
1966            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1967                <Integer Name="Width">
1968                    <Address>0x100</Address>
1969                    <Length>4</Length>
1970                    <AccessMode>RW</AccessMode>
1971                    <Min>16</Min>
1972                    <Max>4096</Max>
1973                    <Visibility>Expert</Visibility>
1974                    <Description>Image width in pixels.</Description>
1975                    <ToolTip>Width of the acquired image</ToolTip>
1976                    <DisplayName>Image Width</DisplayName>
1977                    <Representation>Linear</Representation>
1978                </Integer>
1979                <Float Name="Gain">
1980                    <Address>0x200</Address>
1981                    <Length>4</Length>
1982                    <AccessMode>RW</AccessMode>
1983                    <Min>0.0</Min>
1984                    <Max>48.0</Max>
1985                    <Unit>dB</Unit>
1986                    <Visibility>Beginner</Visibility>
1987                    <Representation>Logarithmic</Representation>
1988                </Float>
1989                <Category Name="Root">
1990                    <Visibility>Guru</Visibility>
1991                    <Description>Top-level category</Description>
1992                    <pFeature>Width</pFeature>
1993                    <pFeature>Gain</pFeature>
1994                </Category>
1995                <Enumeration Name="PixelFormat">
1996                    <Address>0x300</Address>
1997                    <Length>4</Length>
1998                    <AccessMode>RW</AccessMode>
1999                    <Visibility>Beginner</Visibility>
2000                    <ToolTip>Pixel format selector</ToolTip>
2001                    <EnumEntry Name="Mono8" Value="0" />
2002                </Enumeration>
2003            </RegisterDescription>
2004        "#;
2005
2006        let model = parse(XML).expect("parse metadata xml");
2007        assert_eq!(model.nodes.len(), 4);
2008
2009        // Integer with full metadata
2010        match &model.nodes[0] {
2011            NodeDecl::Integer { name, meta, .. } => {
2012                assert_eq!(name, "Width");
2013                assert_eq!(meta.visibility, Visibility::Expert);
2014                assert_eq!(meta.description.as_deref(), Some("Image width in pixels."));
2015                assert_eq!(meta.tooltip.as_deref(), Some("Width of the acquired image"));
2016                assert_eq!(meta.display_name.as_deref(), Some("Image Width"));
2017                assert_eq!(meta.representation, Some(Representation::Linear));
2018            }
2019            other => panic!("unexpected node: {other:?}"),
2020        }
2021
2022        // Float with visibility + representation
2023        match &model.nodes[1] {
2024            NodeDecl::Float { name, meta, .. } => {
2025                assert_eq!(name, "Gain");
2026                assert_eq!(meta.visibility, Visibility::Beginner);
2027                assert_eq!(meta.representation, Some(Representation::Logarithmic));
2028                assert!(meta.description.is_none());
2029            }
2030            other => panic!("unexpected node: {other:?}"),
2031        }
2032
2033        // Category with visibility + description
2034        match &model.nodes[2] {
2035            NodeDecl::Category { name, meta, .. } => {
2036                assert_eq!(name, "Root");
2037                assert_eq!(meta.visibility, Visibility::Guru);
2038                assert_eq!(meta.description.as_deref(), Some("Top-level category"));
2039            }
2040            other => panic!("unexpected node: {other:?}"),
2041        }
2042
2043        // Enum with visibility + tooltip
2044        match &model.nodes[3] {
2045            NodeDecl::Enum { name, meta, .. } => {
2046                assert_eq!(name, "PixelFormat");
2047                assert_eq!(meta.visibility, Visibility::Beginner);
2048                assert_eq!(meta.tooltip.as_deref(), Some("Pixel format selector"));
2049            }
2050            other => panic!("unexpected node: {other:?}"),
2051        }
2052    }
2053
2054    /// Wrap a node fragment in a minimal document and parse it.
2055    fn parse_fragment(node: &str) -> XmlModel {
2056        let xml = format!(
2057            r#"<RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="0">{node}</RegisterDescription>"#
2058        );
2059        parse(&xml).expect("parse fragment")
2060    }
2061
2062    /// Extract the metadata of the single node in a parsed fragment.
2063    fn only_meta(model: &XmlModel) -> &NodeMeta {
2064        match model.nodes.first().expect("one node") {
2065            NodeDecl::Integer { meta, .. } => meta,
2066            other => panic!("unexpected node: {other:?}"),
2067        }
2068    }
2069
2070    /// Regression for issue #122: The Imaging Source's DMK 33GP2000e ships its
2071    /// GenApi XML with a UTF-8 byte-order mark.
2072    ///
2073    /// The BOM is valid UTF-8, so nothing upstream rejected it and `parse`
2074    /// returned `Ok` — with every node in the document skipped, because
2075    /// quick-xml strips the BOM from its own view without advancing
2076    /// `buffer_position`, leaving each sliced element three bytes short of its
2077    /// closing `>`. Assert the node count *and* an empty skip list: asserting
2078    /// `parse(..).is_ok()` alone passed throughout the bug.
2079    #[test]
2080    fn byte_order_mark_does_not_shift_node_slices() {
2081        let body = r#"<Integer Name="Width"><Address>0x100</Address><Length>4</Length></Integer><Integer Name="Height"><Address>0x104</Address><Length>4</Length></Integer>"#;
2082        let doc = format!(
2083            r#"<RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="1">{body}</RegisterDescription>"#
2084        );
2085        let with_bom = format!("\u{feff}{doc}");
2086
2087        let plain = parse(&doc).expect("parse without BOM");
2088        let bom = parse(&with_bom).expect("parse with BOM");
2089
2090        assert!(
2091            bom.skipped.is_empty(),
2092            "BOM document skipped nodes: {:?}",
2093            bom.skipped
2094        );
2095        assert_eq!(bom.nodes.len(), plain.nodes.len());
2096        assert_eq!(bom.version, plain.version);
2097    }
2098
2099    /// The same document, through the offset-free scan `viva-camctl` and the
2100    /// `fetch_xml` example use first. This half always worked — which is why
2101    /// issue #122 reported 291 top-level features listed correctly and then
2102    /// every node failing. Pin it so the two entry points cannot diverge again.
2103    #[test]
2104    fn byte_order_mark_does_not_disturb_the_minimal_scan() {
2105        let doc = r#"<RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="1"><Category Name="Root"><pFeature>Width</pFeature></Category></RegisterDescription>"#;
2106        let with_bom = format!("\u{feff}{doc}");
2107
2108        let info = parse_into_minimal_nodes(&with_bom).expect("minimal scan with BOM");
2109        assert_eq!(info.schema_version.as_deref(), Some("1.1.1"));
2110        assert_eq!(info.top_level_features, vec!["Root".to_string()]);
2111    }
2112
2113    /// A BOM in a document that also carries multi-byte text.
2114    ///
2115    /// The three-byte shift lands on ASCII markup for any XML we have seen, so
2116    /// this is not a second failure mode — it is the same one, checked on
2117    /// content where a wrong slice would corrupt text rather than only truncate
2118    /// a tag. The `xml.get(..)` guard in [`parse`] is defensive on top of that:
2119    /// no document in the corpus reaches a non-character-boundary index, and it
2120    /// exists so that if one ever does the cost is one skipped feature instead
2121    /// of a panic in the middle of a camera connect.
2122    #[test]
2123    fn byte_order_mark_before_multibyte_text_keeps_the_text_intact() {
2124        let doc = r#"<RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="1"><Integer Name="Gain"><Address>0x100</Address><Length>4</Length><ToolTip>Verstärkung in dB — Meßwert</ToolTip></Integer></RegisterDescription>"#;
2125        let with_bom = format!("\u{feff}{doc}");
2126
2127        let model = parse(&with_bom).expect("parse with BOM and multi-byte text");
2128        assert!(model.skipped.is_empty(), "skipped: {:?}", model.skipped);
2129        assert_eq!(
2130            only_meta(&model).tooltip.as_deref(),
2131            Some("Verstärkung in dB — Meßwert")
2132        );
2133    }
2134
2135    /// Regression for issue #45: a FLIR BFS-PGE camera failed to open because a
2136    /// CDATA section in a text element was run through XML unescaping, where the
2137    /// literal `&` it legally contains has no `;` to terminate it.
2138    #[test]
2139    fn cdata_text_is_taken_literally() {
2140        let model = parse_fragment(
2141            r#"<Integer Name="Gain">
2142                 <Address>0x100</Address>
2143                 <ToolTip><![CDATA[Gain in dB & raw units, 0 < x < 10]]></ToolTip>
2144               </Integer>"#,
2145        );
2146        assert_eq!(
2147            only_meta(&model).tooltip.as_deref(),
2148            Some("Gain in dB & raw units, 0 < x < 10")
2149        );
2150    }
2151
2152    /// Non-conformant vendor XML with a lone `&` must not stop the document from
2153    /// loading — a cosmetic tooltip is never worth failing a camera connect over.
2154    #[test]
2155    fn dangling_ampersand_is_kept_verbatim() {
2156        let model = parse_fragment(
2157            r#"<Integer Name="Gain">
2158                 <Address>0x100</Address>
2159                 <ToolTip>Exposure & gain</ToolTip>
2160               </Integer>"#,
2161        );
2162        assert_eq!(
2163            only_meta(&model).tooltip.as_deref(),
2164            Some("Exposure & gain")
2165        );
2166    }
2167
2168    /// Comments are markup, not text: they are dropped, and a `&` inside one is
2169    /// not an entity reference.
2170    #[test]
2171    fn comments_inside_text_elements_are_dropped() {
2172        let model = parse_fragment(
2173            r#"<Integer Name="Gain">
2174                 <Address>0x100</Address>
2175                 <Description>Analog <!-- R&D note --> gain</Description>
2176               </Integer>"#,
2177        );
2178        assert_eq!(
2179            only_meta(&model).description.as_deref(),
2180            Some("Analog  gain")
2181        );
2182    }
2183
2184    /// Entity references still resolve, and the whitespace around them survives
2185    /// even though the reader splits character data at each reference.
2186    #[test]
2187    fn entity_references_resolve_and_preserve_spacing() {
2188        let model = parse_fragment(
2189            r#"<Integer Name="Gain">
2190                 <Address>0x100</Address>
2191                 <ToolTip>A &amp; B &lt; C &gt; D &quot;E&quot; &apos;F&apos;</ToolTip>
2192               </Integer>"#,
2193        );
2194        assert_eq!(
2195            only_meta(&model).tooltip.as_deref(),
2196            Some(r#"A & B < C > D "E" 'F'"#)
2197        );
2198    }
2199
2200    /// Numeric character references, both hexadecimal and decimal.
2201    #[test]
2202    fn character_references_resolve() {
2203        let model = parse_fragment(
2204            r#"<Integer Name="Gain">
2205                 <Address>0x100</Address>
2206                 <ToolTip>&#x2014; dash &#8212;</ToolTip>
2207               </Integer>"#,
2208        );
2209        assert_eq!(only_meta(&model).tooltip.as_deref(), Some("— dash —"));
2210    }
2211
2212    /// GenICam declares no DTD, so an entity we cannot resolve is kept as written
2213    /// rather than failing the document.
2214    #[test]
2215    fn unknown_entity_is_kept_as_written() {
2216        let model = parse_fragment(
2217            r#"<Integer Name="Gain">
2218                 <Address>0x100</Address>
2219                 <ToolTip>copyright &copy; vendor</ToolTip>
2220               </Integer>"#,
2221        );
2222        assert_eq!(
2223            only_meta(&model).tooltip.as_deref(),
2224            Some("copyright &copy; vendor")
2225        );
2226    }
2227
2228    /// SwissKnife formulas escape their bitwise/comparison operators; the parsed
2229    /// expression must contain the operators, not the entities.
2230    #[test]
2231    fn swissknife_formula_entities_resolve_to_operators() {
2232        let model = parse_fragment(
2233            r#"<IntSwissKnife Name="GainMask">
2234                 <pVariable Name="RAW">Gain</pVariable>
2235                 <Formula>(RAW &amp; 0xFF) &lt; 16</Formula>
2236               </IntSwissKnife>"#,
2237        );
2238        match model.nodes.first().expect("one node") {
2239            NodeDecl::SwissKnife(node) => assert_eq!(node.expr, "(RAW & 0xFF) < 16"),
2240            other => panic!("unexpected node: {other:?}"),
2241        }
2242    }
2243
2244    /// A SwissKnife whose formula is constant needs no `<pVariable>`. Rejecting
2245    /// these blocked a Hikrobot MV-CS050-10GC on `PixelDynamicRangeMin_Value`
2246    /// (reported in #35) and appears in the standard's own conformance document.
2247    #[test]
2248    fn swissknife_without_variables_is_accepted() {
2249        let model = parse_fragment(
2250            r#"<IntSwissKnife Name="PixelDynamicRangeMin_Value">
2251                 <Formula>0x1234</Formula>
2252               </IntSwissKnife>"#,
2253        );
2254        match model.nodes.first().expect("one node") {
2255            NodeDecl::SwissKnife(node) => {
2256                assert_eq!(node.expr, "0x1234");
2257                assert!(node.variables.is_empty());
2258            }
2259            other => panic!("unexpected node: {other:?}"),
2260        }
2261    }
2262
2263    /// `<AccessMode>` values beyond the standard's three spellings appear in
2264    /// third-party documents; an odd one must not cost the whole camera.
2265    #[test]
2266    fn access_mode_accepts_aliases_and_defaults_unknown_to_rw() {
2267        assert_eq!(AccessMode::parse("R").unwrap(), AccessMode::RO);
2268        assert_eq!(AccessMode::parse("W").unwrap(), AccessMode::WO);
2269        assert_eq!(AccessMode::parse("WR").unwrap(), AccessMode::RW);
2270        assert_eq!(AccessMode::parse(" ro ").unwrap(), AccessMode::RO);
2271        assert_eq!(AccessMode::parse("Bogus").unwrap(), AccessMode::RW);
2272    }
2273
2274    /// A node we cannot parse costs that one feature, not the whole document —
2275    /// and the reader stays in step, so nodes after it still load.
2276    #[test]
2277    fn unparsable_node_is_skipped_not_fatal() {
2278        let model = parse_fragment(
2279            r#"<Integer Name="Before">
2280                 <Address>0x100</Address>
2281                 <Length>4</Length>
2282               </Integer>
2283               <Integer Name="Broken">
2284                 <Address>0xZZZZ</Address>
2285                 <Length>4</Length>
2286               </Integer>
2287               <Integer Name="After">
2288                 <Address>0x200</Address>
2289                 <Length>4</Length>
2290               </Integer>"#,
2291        );
2292
2293        let names: Vec<&str> = model
2294            .nodes
2295            .iter()
2296            .map(|node| match node {
2297                NodeDecl::Integer { name, .. } => name.as_str(),
2298                other => panic!("unexpected node: {other:?}"),
2299            })
2300            .collect();
2301        assert_eq!(names, ["Before", "After"]);
2302
2303        assert_eq!(model.skipped.len(), 1);
2304        let skipped = &model.skipped[0];
2305        assert_eq!(skipped.tag, "Integer");
2306        assert_eq!(skipped.name.as_deref(), Some("Broken"));
2307        assert!(
2308            skipped.error.contains("invalid hex"),
2309            "unexpected error: {}",
2310            skipped.error
2311        );
2312    }
2313
2314    /// Isolation must survive a node whose failure happens before the parser has
2315    /// consumed any children — the outer reader is positioned by the caller, not
2316    /// by how far the node parser got.
2317    #[test]
2318    fn node_missing_required_name_is_skipped() {
2319        let model = parse_fragment(
2320            r#"<Integer>
2321                 <Address>0x100</Address>
2322               </Integer>
2323               <Integer Name="Good">
2324                 <Address>0x200</Address>
2325                 <Length>4</Length>
2326               </Integer>"#,
2327        );
2328        assert_eq!(model.nodes.len(), 1);
2329        assert_eq!(model.skipped.len(), 1);
2330        assert_eq!(model.skipped[0].name, None);
2331    }
2332
2333    /// A clean document reports nothing skipped.
2334    #[test]
2335    fn clean_document_skips_nothing() {
2336        let model = parse(FIXTURE).expect("parse fixture");
2337        assert!(model.skipped.is_empty());
2338    }
2339}