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        /// Selector nodes referencing this feature.
635        selectors: Vec<String>,
636        /// Selector gating rules in the form (selector name, allowed values).
637        selected_if: Vec<(String, Vec<String>)>,
638        /// Node providing the value (delegates read/write to another node).
639        pvalue: Option<String>,
640        /// Node providing the dynamic maximum.
641        p_max: Option<String>,
642        /// Node providing the dynamic minimum.
643        p_min: Option<String>,
644        /// Static value (for constant integer nodes with `<Value>`).
645        value: Option<i64>,
646        /// Predicate refs gating implementation / availability / lock state.
647        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
648        predicates: PredicateRefs,
649    },
650    /// Floating point feature backed by an integer register with scaling,
651    /// a native IEEE 754 register, or delegated via pValue.
652    Float {
653        name: String,
654        meta: NodeMeta,
655        /// Addressing metadata (absent when delegated via `pvalue`).
656        addressing: Option<Addressing>,
657        access: AccessMode,
658        min: f64,
659        max: f64,
660        unit: Option<String>,
661        /// Optional rational scale applied to the raw register value.
662        scale: Option<(i64, i64)>,
663        /// Optional additive offset applied after scaling.
664        offset: Option<f64>,
665        selectors: Vec<String>,
666        selected_if: Vec<(String, Vec<String>)>,
667        /// Node providing the value (delegates read/write to another node).
668        pvalue: Option<String>,
669        /// How the register payload should be interpreted — native IEEE 754
670        /// or scaled integer. Defaults to [`FloatEncoding::ScaledInteger`] to
671        /// preserve existing behaviour for XML that relied on it.
672        #[serde(default)]
673        encoding: FloatEncoding,
674        /// Byte order of the register payload. Defaults to [`ByteOrder::Big`]
675        /// (the GenICam default).
676        #[serde(default = "default_big_endian")]
677        byte_order: ByteOrder,
678        /// Predicate refs gating implementation / availability / lock state.
679        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
680        predicates: PredicateRefs,
681    },
682    /// Enumeration feature exposing a list of named integer values.
683    Enum {
684        name: String,
685        meta: NodeMeta,
686        /// Addressing metadata (absent when delegated via `pvalue`).
687        addressing: Option<Addressing>,
688        access: AccessMode,
689        entries: Vec<EnumEntryDecl>,
690        default: Option<String>,
691        selectors: Vec<String>,
692        selected_if: Vec<(String, Vec<String>)>,
693        /// Node providing the integer value (delegates register read/write).
694        pvalue: Option<String>,
695        /// Predicate refs gating implementation / availability / lock state.
696        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
697        predicates: PredicateRefs,
698    },
699    /// Boolean feature backed by a single bit/byte register or delegated via pValue.
700    Boolean {
701        name: String,
702        meta: NodeMeta,
703        /// Addressing metadata (absent when delegated via `pvalue`).
704        addressing: Option<Addressing>,
705        len: u32,
706        access: AccessMode,
707        bitfield: Option<BitField>,
708        selectors: Vec<String>,
709        selected_if: Vec<(String, Vec<String>)>,
710        /// Node providing the value (delegates read/write to another node).
711        pvalue: Option<String>,
712        /// On value for pValue-backed booleans.
713        on_value: Option<i64>,
714        /// Off value for pValue-backed booleans.
715        off_value: Option<i64>,
716        /// Predicate refs gating implementation / availability / lock state.
717        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
718        predicates: PredicateRefs,
719    },
720    /// Command feature that triggers an action when written.
721    Command {
722        name: String,
723        meta: NodeMeta,
724        /// Fixed register address (absent when delegated via `pvalue`).
725        address: Option<u64>,
726        len: u32,
727        /// Node providing the command register (delegates write).
728        pvalue: Option<String>,
729        /// Value to write when executing the command.
730        command_value: Option<i64>,
731        /// Predicate refs gating implementation / availability / lock state.
732        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
733        predicates: PredicateRefs,
734    },
735    /// Category used to organise features.
736    Category {
737        name: String,
738        meta: NodeMeta,
739        children: Vec<String>,
740        /// Predicate refs gating implementation / availability.
741        #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
742        predicates: PredicateRefs,
743    },
744    /// Computed value backed by an arithmetic expression referencing other nodes.
745    SwissKnife(SwissKnifeDecl),
746    /// Converter transforming raw values to/from user-facing floating-point values.
747    Converter(ConverterDecl),
748    /// IntConverter transforming raw values to/from user-facing integer values.
749    IntConverter(IntConverterDecl),
750    /// StringReg for string-typed register access.
751    String(StringDecl),
752    /// Raw byte-array register access.
753    Register(RegisterDecl),
754}
755
756impl NodeDecl {
757    /// Feature name declared by this node.
758    pub fn name(&self) -> &str {
759        match self {
760            NodeDecl::Integer { name, .. }
761            | NodeDecl::Float { name, .. }
762            | NodeDecl::Enum { name, .. }
763            | NodeDecl::Boolean { name, .. }
764            | NodeDecl::Command { name, .. }
765            | NodeDecl::Category { name, .. } => name,
766            NodeDecl::SwissKnife(decl) => &decl.name,
767            NodeDecl::Converter(decl) => &decl.name,
768            NodeDecl::IntConverter(decl) => &decl.name,
769            NodeDecl::String(decl) => &decl.name,
770            NodeDecl::Register(decl) => &decl.name,
771        }
772    }
773
774    /// Node kind, matching the variant name.
775    ///
776    /// This is the declaration kind rather than the originating XML tag: an
777    /// `<IntReg>` and a `<MaskedIntReg>` both report `Integer`. Useful for
778    /// grouping and for reporting nodes that were dropped downstream of
779    /// parsing.
780    pub fn kind(&self) -> &'static str {
781        match self {
782            NodeDecl::Integer { .. } => "Integer",
783            NodeDecl::Float { .. } => "Float",
784            NodeDecl::Enum { .. } => "Enum",
785            NodeDecl::Boolean { .. } => "Boolean",
786            NodeDecl::Command { .. } => "Command",
787            NodeDecl::Category { .. } => "Category",
788            NodeDecl::SwissKnife(_) => "SwissKnife",
789            NodeDecl::Converter(_) => "Converter",
790            NodeDecl::IntConverter(_) => "IntConverter",
791            NodeDecl::String(_) => "String",
792            NodeDecl::Register(_) => "Register",
793        }
794    }
795}
796
797/// A node element that could not be parsed and was left out of the model.
798///
799/// Parsing isolates each node, so a declaration we cannot make sense of costs
800/// that one feature instead of the whole document. These records exist so the
801/// loss is visible rather than silent — surface them in a UI, log them, or
802/// assert on them in tests.
803#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
804pub struct SkippedNode {
805    /// XML tag of the node element, e.g. `Integer`.
806    pub tag: String,
807    /// `Name` attribute, when the element had one.
808    pub name: Option<String>,
809    /// Rendered parse error explaining why the node was dropped.
810    pub error: String,
811}
812
813/// Full XML model describing the GenICam schema version and all declared nodes.
814#[derive(Debug, Clone, Serialize, Deserialize)]
815pub struct XmlModel {
816    /// Combined schema version extracted from the RegisterDescription attributes.
817    pub version: String,
818    /// Flat list of node declarations present in the document.
819    pub nodes: Vec<NodeDecl>,
820    /// Node elements that failed to parse and were skipped. Empty for a clean
821    /// document.
822    #[serde(default, skip_serializing_if = "Vec::is_empty")]
823    pub skipped: Vec<SkippedNode>,
824}
825
826/// Minimal metadata extracted from a quick XML scan.
827#[derive(Debug, Clone, PartialEq, Eq)]
828pub struct MinimalXmlInfo {
829    pub schema_version: Option<String>,
830    pub top_level_features: Vec<String>,
831}
832
833/// Parse a GenICam XML snippet and collect minimal metadata.
834pub fn parse_into_minimal_nodes(xml: &str) -> Result<MinimalXmlInfo, XmlError> {
835    let mut reader = Reader::from_str(xml);
836    reader.config_mut().trim_text(true);
837    // Vendor XML is not ours to fix: a lone `&` in a tooltip must not stop us
838    // from reading the document.
839    reader.config_mut().allow_dangling_amp = true;
840    let mut buf = Vec::new();
841    let mut depth = 0usize;
842    let mut schema_version: Option<String> = None;
843    let mut top_level_features = Vec::new();
844
845    loop {
846        match reader.read_event_into(&mut buf) {
847            Ok(Event::Start(e)) => {
848                depth += 1;
849                handle_start(&e, depth, &mut schema_version, &mut top_level_features)?;
850            }
851            Ok(Event::Empty(e)) => {
852                depth += 1;
853                handle_start(&e, depth, &mut schema_version, &mut top_level_features)?;
854                depth = depth.saturating_sub(1);
855            }
856            Ok(Event::End(_)) => {
857                depth = depth.saturating_sub(1);
858            }
859            Ok(Event::Eof) => break,
860            Err(err) => return Err(XmlError::Xml(err.to_string())),
861            _ => {}
862        }
863        buf.clear();
864    }
865
866    Ok(MinimalXmlInfo {
867        schema_version,
868        top_level_features,
869    })
870}
871
872/// `true` for tags this parser turns into one or more [`NodeDecl`]s.
873fn is_node_tag(tag: &[u8]) -> bool {
874    matches!(
875        tag,
876        b"Integer"
877            | b"IntReg"
878            | b"MaskedIntReg"
879            | b"IntSwissKnife"
880            | b"SwissKnife"
881            | b"Float"
882            | b"FloatReg"
883            | b"Enumeration"
884            | b"Boolean"
885            | b"Command"
886            | b"Category"
887            | b"Converter"
888            | b"IntConverter"
889            | b"Register"
890            | b"StringReg"
891            | b"String"
892            | b"StructReg"
893    )
894}
895
896/// Record an element this parser has no node type for, or `None` if it is not
897/// a node at all.
898///
899/// A node declaration is told from a structural element by its `Name`
900/// attribute, which the GenApi schema requires on every node and on nothing
901/// else at this level. Without this, an unlisted tag fell through to
902/// `skip_element` and vanished — no log line, no [`XmlModel::skipped`] entry,
903/// nothing for a corpus test to trip over. `<Register>`, 56 declarations
904/// across 14 corpus documents, disappeared exactly this way.
905fn unknown_node(tag: &[u8], start: &BytesStart<'_>) -> Result<Option<SkippedNode>, XmlError> {
906    let Some(name) = attribute_value(start, b"Name")? else {
907        return Ok(None);
908    };
909    let tag = String::from_utf8_lossy(tag).into_owned();
910    tracing::warn!(
911        tag = %tag,
912        node = %name,
913        "skipping node of an unsupported type"
914    );
915    Ok(Some(SkippedNode {
916        error: format!("unsupported node type <{tag}>"),
917        tag,
918        name: Some(name),
919    }))
920}
921
922/// Dispatch a node element to its parser. `start` must be the element's start
923/// event and `reader` must be positioned just after it.
924fn parse_node(
925    reader: &mut Reader<&[u8]>,
926    start: BytesStart<'_>,
927) -> Result<Vec<NodeDecl>, XmlError> {
928    let tag = start.name().as_ref().to_vec();
929    let node = match tag.as_slice() {
930        b"Integer" | b"IntReg" | b"MaskedIntReg" => parse_integer(reader, start)?,
931        b"IntSwissKnife" | b"SwissKnife" => parse_swissknife(reader, start)?,
932        b"Float" | b"FloatReg" => parse_float(reader, start)?,
933        b"Enumeration" => parse_enum(reader, start)?,
934        b"Boolean" => parse_boolean(reader, start)?,
935        b"Command" => parse_command(reader, start)?,
936        b"Category" => parse_category(reader, start)?,
937        b"Converter" => parse_converter(reader, start)?,
938        b"IntConverter" => parse_int_converter(reader, start)?,
939        b"Register" => parse_register(reader, start)?,
940        b"StringReg" | b"String" => parse_string(reader, start)?,
941        b"StructReg" => return parse_struct_reg(reader, start),
942        other => {
943            return Err(XmlError::Invalid(format!(
944                "not a node element: {}",
945                String::from_utf8_lossy(other)
946            )));
947        }
948    };
949    Ok(vec![node])
950}
951
952/// Parse one node element in isolation, from a slice spanning the whole element.
953///
954/// A fresh reader over just this element means a parse failure cannot leave the
955/// document-level reader at an unknown position — the caller has already
956/// consumed exactly this element and is correctly positioned either way.
957fn parse_isolated_node(element: &str) -> Result<Vec<NodeDecl>, XmlError> {
958    let mut reader = Reader::from_str(element);
959    reader.config_mut().trim_text(true);
960    reader.config_mut().allow_dangling_amp = true;
961    let mut buf = Vec::new();
962    loop {
963        match reader.read_event_into(&mut buf) {
964            Ok(Event::Start(e)) => {
965                let start = e.into_owned();
966                return parse_node(&mut reader, start);
967            }
968            Ok(Event::Eof) => {
969                return Err(XmlError::Invalid("node element is empty".into()));
970            }
971            Err(err) => return Err(XmlError::Xml(err.to_string())),
972            // Leading whitespace or a comment before the start tag.
973            _ => {}
974        }
975    }
976}
977
978/// Parse a GenICam XML document into an [`XmlModel`].
979///
980/// The parser only understands a practical subset of the schema. Unknown tags
981/// are skipped which keeps the implementation forward compatible with richer
982/// documents.
983///
984/// Each node element is parsed in isolation: one declaration we cannot make
985/// sense of costs that single feature and is recorded in [`XmlModel::skipped`],
986/// rather than failing the load and leaving the camera unopenable. Only errors
987/// that make the document as a whole unreadable are returned.
988pub fn parse(xml: &str) -> Result<XmlModel, XmlError> {
989    let mut reader = Reader::from_str(xml);
990    reader.config_mut().trim_text(true);
991    // Vendor XML is not ours to fix: a lone `&` in a tooltip must not stop us
992    // from opening the camera.
993    reader.config_mut().allow_dangling_amp = true;
994    let mut buf = Vec::new();
995    let mut version = String::from("0.0.0");
996    let mut nodes = Vec::new();
997    let mut skipped = Vec::new();
998
999    loop {
1000        // Captured before the read so a node element's slice can be recovered
1001        // verbatim, start tag included.
1002        let element_start = reader.buffer_position() as usize;
1003        match reader.read_event_into(&mut buf) {
1004            Ok(Event::Start(ref e)) => match e.name().as_ref() {
1005                b"RegisterDescription" => {
1006                    version = schema_version_from(e)?;
1007                }
1008                b"Group" => {
1009                    // Group is a transparent container wrapping feature nodes;
1010                    // let child events surface in the next loop iterations.
1011                }
1012                b"Port" => {
1013                    // Port nodes are transport-level abstractions; skip them.
1014                    skip_element(&mut reader, e.name().as_ref())?;
1015                }
1016                tag if is_node_tag(tag) => {
1017                    let tag = tag.to_vec();
1018                    let name = attribute_value(e, b"Name")?;
1019                    // Consume the element up front: whatever the node parser
1020                    // makes of it, the document reader stays in step.
1021                    reader
1022                        .read_to_end(QName(&tag))
1023                        .map_err(|err| XmlError::Xml(err.to_string()))?;
1024                    let element = &xml[element_start..reader.buffer_position() as usize];
1025                    match parse_isolated_node(element) {
1026                        Ok(parsed) => nodes.extend(parsed),
1027                        Err(err) => {
1028                            let tag = String::from_utf8_lossy(&tag).into_owned();
1029                            tracing::warn!(
1030                                tag = %tag,
1031                                node = name.as_deref().unwrap_or("<unnamed>"),
1032                                error = %err,
1033                                "skipping unparsable node"
1034                            );
1035                            skipped.push(SkippedNode {
1036                                tag,
1037                                name,
1038                                error: err.to_string(),
1039                            });
1040                        }
1041                    }
1042                }
1043                tag => {
1044                    let unknown = unknown_node(tag, e)?;
1045                    skip_element(&mut reader, e.name().as_ref())?;
1046                    if let Some(record) = unknown {
1047                        skipped.push(record);
1048                    }
1049                }
1050            },
1051            Ok(Event::Empty(ref e)) => match e.name().as_ref() {
1052                b"RegisterDescription" => {
1053                    version = schema_version_from(e)?;
1054                }
1055                b"Command" => {
1056                    let node = parse_command_empty(e)?;
1057                    nodes.push(node);
1058                }
1059                b"Category" => {
1060                    let node = parse_category_empty(e)?;
1061                    nodes.push(node);
1062                }
1063                // Same transport-level abstraction the Start arm skips; four
1064                // corpus documents declare it as `<Port Name="Device"/>`.
1065                b"Port" => {}
1066                tag => {
1067                    if let Some(record) = unknown_node(tag, e)? {
1068                        skipped.push(record);
1069                    }
1070                }
1071            },
1072            Ok(Event::Eof) => break,
1073            Err(err) => return Err(XmlError::Xml(err.to_string())),
1074            _ => {}
1075        }
1076        buf.clear();
1077    }
1078
1079    if !skipped.is_empty() {
1080        tracing::warn!(
1081            count = skipped.len(),
1082            total = nodes.len() + skipped.len(),
1083            "some GenApi nodes could not be parsed and were skipped"
1084        );
1085    }
1086
1087    Ok(XmlModel {
1088        version,
1089        nodes,
1090        skipped,
1091    })
1092}
1093
1094fn schema_version_from(event: &BytesStart<'_>) -> Result<String, XmlError> {
1095    let major = attribute_value(event, b"SchemaMajorVersion")?;
1096    let minor = attribute_value(event, b"SchemaMinorVersion")?;
1097    let sub = attribute_value(event, b"SchemaSubMinorVersion")?;
1098    let major = major.unwrap_or_else(|| "0".to_string());
1099    let minor = minor.unwrap_or_else(|| "0".to_string());
1100    let sub = sub.unwrap_or_else(|| "0".to_string());
1101    Ok(format!("{major}.{minor}.{sub}"))
1102}
1103
1104fn handle_start(
1105    event: &BytesStart<'_>,
1106    depth: usize,
1107    schema_version: &mut Option<String>,
1108    top_level: &mut Vec<String>,
1109) -> Result<(), XmlError> {
1110    if depth == 1 && schema_version.is_none() {
1111        *schema_version = extract_schema_version(event);
1112    } else if depth == 2 {
1113        if let Some(name) = attribute_value(event, b"Name")? {
1114            top_level.push(name);
1115        } else {
1116            top_level.push(String::from_utf8_lossy(event.name().as_ref()).to_string());
1117        }
1118    }
1119    Ok(())
1120}
1121
1122fn extract_schema_version(event: &BytesStart<'_>) -> Option<String> {
1123    let major = attribute_value(event, b"SchemaMajorVersion").ok().flatten();
1124    let minor = attribute_value(event, b"SchemaMinorVersion").ok().flatten();
1125    let sub = attribute_value(event, b"SchemaSubMinorVersion")
1126        .ok()
1127        .flatten();
1128    if major.is_none() && minor.is_none() && sub.is_none() {
1129        None
1130    } else {
1131        let major = major.unwrap_or_else(|| "0".to_string());
1132        let minor = minor.unwrap_or_else(|| "0".to_string());
1133        let sub = sub.unwrap_or_else(|| "0".to_string());
1134        Some(format!("{major}.{minor}.{sub}"))
1135    }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140    use super::*;
1141
1142    const FIXTURE: &str = r#"
1143        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="2" SchemaSubMinorVersion="3">
1144            <Category Name="Root">
1145                <pFeature>Gain</pFeature>
1146                <pFeature>GainSelector</pFeature>
1147            </Category>
1148            <Integer Name="Width">
1149                <Address>0x0000_0100</Address>
1150                <Length>4</Length>
1151                <AccessMode>RW</AccessMode>
1152                <Min>16</Min>
1153                <Max>4096</Max>
1154                <Inc>2</Inc>
1155            </Integer>
1156            <Float Name="ExposureTime">
1157                <Address>0x0000_0200</Address>
1158                <Length>4</Length>
1159                <AccessMode>RW</AccessMode>
1160                <Min>10.0</Min>
1161                <Max>200000.0</Max>
1162                <Scale>1/1000</Scale>
1163                <Offset>0.0</Offset>
1164            </Float>
1165            <Enumeration Name="GainSelector">
1166                <Address>0x0000_0300</Address>
1167                <Length>2</Length>
1168                <AccessMode>RW</AccessMode>
1169                <EnumEntry Name="AnalogAll" Value="0" />
1170                <EnumEntry Name="DigitalAll" Value="1" />
1171            </Enumeration>
1172            <Integer Name="Gain">
1173                <Address>0x0000_0304</Address>
1174                <Length>2</Length>
1175                <AccessMode>RW</AccessMode>
1176                <Min>0</Min>
1177                <Max>48</Max>
1178                <pSelected>GainSelector</pSelected>
1179                <Selected>AnalogAll</Selected>
1180            </Integer>
1181            <Boolean Name="GammaEnable">
1182                <Address>0x0000_0400</Address>
1183                <Length>1</Length>
1184                <AccessMode>RW</AccessMode>
1185            </Boolean>
1186            <Command Name="AcquisitionStart">
1187                <Address>0x0000_0500</Address>
1188                <Length>4</Length>
1189            </Command>
1190        </RegisterDescription>
1191    "#;
1192
1193    #[test]
1194    fn parse_minimal_xml() {
1195        let info = parse_into_minimal_nodes(FIXTURE).expect("parse xml");
1196        assert_eq!(info.schema_version.as_deref(), Some("1.2.3"));
1197        assert_eq!(info.top_level_features.len(), 7);
1198        assert_eq!(info.top_level_features[0], "Root");
1199    }
1200
1201    #[test]
1202    fn parse_fixture_model() {
1203        let model = parse(FIXTURE).expect("parse fixture");
1204        assert_eq!(model.version, "1.2.3");
1205        assert_eq!(model.nodes.len(), 7);
1206        match &model.nodes[0] {
1207            NodeDecl::Category { name, children, .. } => {
1208                assert_eq!(name, "Root");
1209                assert_eq!(
1210                    children,
1211                    &vec!["Gain".to_string(), "GainSelector".to_string()]
1212                );
1213            }
1214            other => panic!("unexpected node: {other:?}"),
1215        }
1216        match &model.nodes[1] {
1217            NodeDecl::Integer {
1218                name,
1219                min,
1220                max,
1221                inc,
1222                ..
1223            } => {
1224                assert_eq!(name, "Width");
1225                assert_eq!(*min, 16);
1226                assert_eq!(*max, 4096);
1227                assert_eq!(*inc, Some(2));
1228            }
1229            other => panic!("unexpected node: {other:?}"),
1230        }
1231        match &model.nodes[2] {
1232            NodeDecl::Float {
1233                name,
1234                scale,
1235                offset,
1236                ..
1237            } => {
1238                assert_eq!(name, "ExposureTime");
1239                assert_eq!(*scale, Some((1, 1000)));
1240                assert_eq!(*offset, Some(0.0));
1241            }
1242            other => panic!("unexpected node: {other:?}"),
1243        }
1244        match &model.nodes[3] {
1245            NodeDecl::Enum { name, entries, .. } => {
1246                assert_eq!(name, "GainSelector");
1247                assert_eq!(entries.len(), 2);
1248                assert!(matches!(entries[0].value, EnumValueSrc::Literal(0)));
1249                assert!(matches!(entries[1].value, EnumValueSrc::Literal(1)));
1250            }
1251            other => panic!("unexpected node: {other:?}"),
1252        }
1253        match &model.nodes[4] {
1254            NodeDecl::Integer {
1255                name, selected_if, ..
1256            } => {
1257                assert_eq!(name, "Gain");
1258                assert_eq!(selected_if.len(), 1);
1259                assert_eq!(selected_if[0].0, "GainSelector");
1260                assert_eq!(selected_if[0].1, vec!["AnalogAll".to_string()]);
1261            }
1262            other => panic!("unexpected node: {other:?}"),
1263        }
1264    }
1265
1266    #[test]
1267    fn parse_swissknife_node() {
1268        const XML: &str = r#"
1269            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1270                <Integer Name="GainRaw">
1271                    <Address>0x3000</Address>
1272                    <Length>4</Length>
1273                    <AccessMode>RW</AccessMode>
1274                    <Min>0</Min>
1275                    <Max>1000</Max>
1276                </Integer>
1277                <Float Name="Offset">
1278                    <Address>0x3008</Address>
1279                    <Length>4</Length>
1280                    <AccessMode>RW</AccessMode>
1281                    <Min>-100.0</Min>
1282                    <Max>100.0</Max>
1283                </Float>
1284                <SwissKnife Name="ComputedGain">
1285                    <Expression>(GainRaw * 0.5) + Offset</Expression>
1286                    <pVariable Name="GainRaw">GainRaw</pVariable>
1287                    <pVariable Name="Offset">Offset</pVariable>
1288                    <Output>Float</Output>
1289                </SwissKnife>
1290            </RegisterDescription>
1291        "#;
1292
1293        let model = parse(XML).expect("parse swissknife xml");
1294        assert_eq!(model.nodes.len(), 3);
1295        let swiss = model
1296            .nodes
1297            .iter()
1298            .find_map(|decl| match decl {
1299                NodeDecl::SwissKnife(node) => Some(node),
1300                _ => None,
1301            })
1302            .expect("swissknife present");
1303        assert_eq!(swiss.name, "ComputedGain");
1304        assert_eq!(swiss.expr, "(GainRaw * 0.5) + Offset");
1305        assert_eq!(swiss.output, SkOutput::Float);
1306        assert_eq!(swiss.variables.len(), 2);
1307        assert_eq!(
1308            swiss.variables[0],
1309            ("GainRaw".to_string(), "GainRaw".to_string())
1310        );
1311        assert_eq!(
1312            swiss.variables[1],
1313            ("Offset".to_string(), "Offset".to_string())
1314        );
1315    }
1316
1317    #[test]
1318    fn parse_int_swissknife_with_hex_and_ampersand() {
1319        // Test that &amp; is decoded to & and hex literals are supported.
1320        const XML: &str = r#"
1321            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1322                <IntSwissKnife Name="PayloadSize">
1323                    <pVariable Name="W">Width</pVariable>
1324                    <pVariable Name="H">Height</pVariable>
1325                    <pVariable Name="PF">PixelFormat</pVariable>
1326                    <Formula>W * H * ((PF>>16)&amp;0xFF) / 8</Formula>
1327                </IntSwissKnife>
1328            </RegisterDescription>
1329        "#;
1330
1331        let model = parse(XML).expect("parse intswissknife");
1332        assert_eq!(model.nodes.len(), 1);
1333        let swiss = model
1334            .nodes
1335            .iter()
1336            .find_map(|decl| match decl {
1337                NodeDecl::SwissKnife(node) => Some(node),
1338                _ => None,
1339            })
1340            .expect("swissknife present");
1341        assert_eq!(swiss.name, "PayloadSize");
1342        // &amp; should be decoded to &
1343        assert!(
1344            swiss.expr.contains('&'),
1345            "expression should contain decoded '&': {}",
1346            swiss.expr
1347        );
1348        assert!(
1349            swiss.expr.contains("0xFF"),
1350            "expression should contain hex literal: {}",
1351            swiss.expr
1352        );
1353    }
1354
1355    #[test]
1356    fn parse_enum_entry_with_pvalue() {
1357        const XML: &str = r#"
1358            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1359                <Enumeration Name="Mode">
1360                    <Address>0x0000_4000</Address>
1361                    <Length>4</Length>
1362                    <AccessMode>RW</AccessMode>
1363                    <EnumEntry Name="Fixed10">
1364                        <Value>10</Value>
1365                    </EnumEntry>
1366                    <EnumEntry Name="DynFromReg">
1367                        <pValue>RegModeVal</pValue>
1368                    </EnumEntry>
1369                </Enumeration>
1370                <Integer Name="RegModeVal">
1371                    <Address>0x0000_4100</Address>
1372                    <Length>4</Length>
1373                    <AccessMode>RW</AccessMode>
1374                    <Min>0</Min>
1375                    <Max>65535</Max>
1376                </Integer>
1377            </RegisterDescription>
1378        "#;
1379
1380        let model = parse(XML).expect("parse enum pvalue");
1381        assert_eq!(model.nodes.len(), 2);
1382        match &model.nodes[0] {
1383            NodeDecl::Enum { entries, .. } => {
1384                assert_eq!(entries.len(), 2);
1385                assert!(matches!(entries[0].value, EnumValueSrc::Literal(10)));
1386                match &entries[1].value {
1387                    EnumValueSrc::FromNode(node) => assert_eq!(node, "RegModeVal"),
1388                    other => panic!("unexpected entry value: {other:?}"),
1389                }
1390            }
1391            other => panic!("unexpected node: {other:?}"),
1392        }
1393    }
1394
1395    /// `<Address>` and `<pAddress>` on the same register are summed, in the
1396    /// order they were declared.
1397    ///
1398    /// FLIR and Point Grey write exactly this shape — `<pAddress>` for the
1399    /// block base, `<Address>` for the offset within it — and dropping either
1400    /// term reads the wrong register (issue #35).
1401    #[test]
1402    fn address_terms_are_summed() {
1403        const XML: &str = r#"
1404            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1405                <Integer Name="RegAddr">
1406                    <Address>0x2000</Address>
1407                    <Length>4</Length>
1408                    <AccessMode>RW</AccessMode>
1409                    <Min>0</Min>
1410                    <Max>65535</Max>
1411                </Integer>
1412                <Integer Name="Gain">
1413                    <pAddress>RegAddr</pAddress>
1414                    <Address>0x00000008</Address>
1415                    <Length>4</Length>
1416                    <AccessMode>RW</AccessMode>
1417                    <Min>0</Min>
1418                    <Max>255</Max>
1419                </Integer>
1420            </RegisterDescription>
1421        "#;
1422
1423        let model = parse(XML).expect("parse indirect xml");
1424        assert_eq!(model.nodes.len(), 2);
1425        match &model.nodes[0] {
1426            NodeDecl::Integer {
1427                name, addressing, ..
1428            } => {
1429                assert_eq!(name, "RegAddr");
1430                assert_eq!(addressing.as_ref(), Some(&Addressing::fixed(0x2000, 4)));
1431            }
1432            other => panic!("unexpected node: {other:?}"),
1433        }
1434        match &model.nodes[1] {
1435            NodeDecl::Integer {
1436                name, addressing, ..
1437            } => {
1438                assert_eq!(name, "Gain");
1439                match addressing {
1440                    Some(Addressing::Sum { terms, len }) => {
1441                        assert_eq!(
1442                            terms,
1443                            &[AddressTerm::Node("RegAddr".into()), AddressTerm::Fixed(0x8),]
1444                        );
1445                        assert_eq!(*len, 4);
1446                    }
1447                    other => panic!("expected summed addressing, got {other:?}"),
1448                }
1449            }
1450            other => panic!("unexpected node: {other:?}"),
1451        }
1452    }
1453
1454    /// `<pIndex>` contributes an index scaled by its stride.
1455    #[test]
1456    fn parse_p_index_term() {
1457        const XML: &str = r#"
1458            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1459                <MaskedIntReg Name="RegTriggerInqDelay">
1460                    <Address>0x13400</Address>
1461                    <pIndex Offset="64">IntTriggerSelector</pIndex>
1462                    <Length>4</Length>
1463                    <AccessMode>RO</AccessMode>
1464                    <Bit>30</Bit>
1465                </MaskedIntReg>
1466            </RegisterDescription>
1467        "#;
1468
1469        let model = parse(XML).expect("parse pIndex xml");
1470        match &model.nodes[0] {
1471            NodeDecl::Integer { addressing, .. } => match addressing {
1472                Some(Addressing::Sum { terms, .. }) => assert_eq!(
1473                    terms,
1474                    &[
1475                        AddressTerm::Fixed(0x13400),
1476                        AddressTerm::Index {
1477                            node: "IntTriggerSelector".into(),
1478                            offset: IndexOffset::Fixed(64),
1479                        },
1480                    ]
1481                ),
1482                other => panic!("expected summed addressing, got {other:?}"),
1483            },
1484            other => panic!("unexpected node: {other:?}"),
1485        }
1486    }
1487
1488    /// A `<StructReg>` shares its address terms with every `<StructEntry>`.
1489    ///
1490    /// Point Grey declares `<Address>` next to `<pAddress>` here; before the
1491    /// additive model the base was dropped and all 38 inquiry bits in the
1492    /// document read register 0.
1493    #[test]
1494    fn struct_reg_inherits_all_address_terms() {
1495        const XML: &str = r#"
1496            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1497                <StructReg Comment="Gain Inquiry Register">
1498                    <Address>0x520</Address>
1499                    <pAddress>CamRegBaseAddress</pAddress>
1500                    <Length>4</Length>
1501                    <AccessMode>RO</AccessMode>
1502                    <Endianess>BigEndian</Endianess>
1503                    <StructEntry Name="GainPresInq_Bit"><Bit>0</Bit></StructEntry>
1504                    <StructEntry Name="GainAutoInq_Bit"><Bit>6</Bit></StructEntry>
1505                </StructReg>
1506            </RegisterDescription>
1507        "#;
1508
1509        let model = parse(XML).expect("parse struct reg");
1510        assert_eq!(model.nodes.len(), 2);
1511        for node in &model.nodes {
1512            match node {
1513                NodeDecl::Integer { addressing, .. } => match addressing {
1514                    Some(Addressing::Sum { terms, len }) => {
1515                        assert_eq!(
1516                            terms,
1517                            &[
1518                                AddressTerm::Fixed(0x520),
1519                                AddressTerm::Node("CamRegBaseAddress".into()),
1520                            ]
1521                        );
1522                        assert_eq!(*len, 4);
1523                    }
1524                    other => panic!("expected summed addressing, got {other:?}"),
1525                },
1526                other => panic!("unexpected node: {other:?}"),
1527            }
1528        }
1529    }
1530
1531    /// A `<StructReg>` with no address at all is dropped rather than silently
1532    /// pointed at register zero.
1533    #[test]
1534    fn struct_reg_without_address_is_skipped() {
1535        const XML: &str = r#"
1536            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1537                <StructReg>
1538                    <Length>4</Length>
1539                    <AccessMode>RO</AccessMode>
1540                    <StructEntry Name="SomeInq_Bit"><Bit>0</Bit></StructEntry>
1541                </StructReg>
1542            </RegisterDescription>
1543        "#;
1544
1545        let model = parse(XML).expect("document still parses");
1546        assert!(model.nodes.is_empty());
1547        assert_eq!(model.skipped.len(), 1);
1548        assert!(model.skipped[0].error.contains("Address"));
1549    }
1550
1551    #[test]
1552    fn parse_indirect_float_is_scaled_integer() {
1553        // Regression test: indirect <Float> with Length=4 and no
1554        // <Scale>/<Offset> must NOT be reclassified as IEEE 754. Pointer-
1555        // backed float features on real cameras are almost always scaled
1556        // integer registers; silently decoding their bytes as IEEE 754
1557        // corrupts the value.
1558        const XML: &str = r#"
1559            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1560                <Integer Name="RegAddr">
1561                    <Address>0x2000</Address>
1562                    <Length>4</Length>
1563                    <AccessMode>RW</AccessMode>
1564                </Integer>
1565                <Float Name="Exposure">
1566                    <pAddress>RegAddr</pAddress>
1567                    <Length>4</Length>
1568                    <AccessMode>RW</AccessMode>
1569                </Float>
1570            </RegisterDescription>
1571        "#;
1572
1573        let model = parse(XML).expect("parse indirect float");
1574        let float = model
1575            .nodes
1576            .iter()
1577            .find(|n| matches!(n, NodeDecl::Float { name, .. } if name == "Exposure"))
1578            .expect("Exposure node");
1579        match float {
1580            NodeDecl::Float {
1581                encoding,
1582                addressing,
1583                ..
1584            } => {
1585                assert!(
1586                    matches!(
1587                        addressing,
1588                        Some(Addressing::Sum { terms, .. })
1589                            if terms.iter().any(|t| matches!(t, AddressTerm::Node(_)))
1590                    ),
1591                    "expected a pAddress term"
1592                );
1593                assert_eq!(*encoding, FloatEncoding::ScaledInteger);
1594            }
1595            _ => unreachable!(),
1596        }
1597    }
1598
1599    #[test]
1600    fn parse_integer_bitfield_big_endian() {
1601        const XML: &str = r#"
1602            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1603                <Integer Name="Packed">
1604                    <Address>0x1000</Address>
1605                    <Length>4</Length>
1606                    <AccessMode>RW</AccessMode>
1607                    <Min>0</Min>
1608                    <Max>65535</Max>
1609                    <Lsb>8</Lsb>
1610                    <Msb>15</Msb>
1611                    <Endianness>BigEndian</Endianness>
1612                </Integer>
1613            </RegisterDescription>
1614        "#;
1615
1616        let model = parse(XML).expect("parse big-endian bitfield");
1617        assert_eq!(model.nodes.len(), 1);
1618        match &model.nodes[0] {
1619            NodeDecl::Integer { len, bitfield, .. } => {
1620                assert_eq!(*len, 4);
1621                let field = bitfield.as_ref().expect("bitfield present");
1622                assert_eq!(field.byte_order, ByteOrder::Big);
1623                assert_eq!(field.bit_length, 8);
1624                assert_eq!(field.bit_offset, 16);
1625            }
1626            other => panic!("unexpected node: {other:?}"),
1627        }
1628    }
1629
1630    #[test]
1631    fn parse_boolean_bitfield_default_length() {
1632        const XML: &str = r#"
1633            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1634                <Boolean Name="Flag">
1635                    <Address>0x2000</Address>
1636                    <Length>1</Length>
1637                    <AccessMode>RW</AccessMode>
1638                    <Bit>3</Bit>
1639                </Boolean>
1640            </RegisterDescription>
1641        "#;
1642
1643        let model = parse(XML).expect("parse boolean bitfield");
1644        assert_eq!(model.nodes.len(), 1);
1645        match &model.nodes[0] {
1646            NodeDecl::Boolean { len, bitfield, .. } => {
1647                assert_eq!(*len, 1);
1648                let bf = bitfield.as_ref().expect("bitfield present");
1649                assert_eq!(bf.byte_order, ByteOrder::Little);
1650                assert_eq!(bf.bit_length, 1);
1651                assert_eq!(bf.bit_offset, 3);
1652            }
1653            other => panic!("unexpected node: {other:?}"),
1654        }
1655    }
1656
1657    #[test]
1658    fn parse_integer_bitfield_mask() {
1659        const XML: &str = r#"
1660            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1661                <Integer Name="Masked">
1662                    <Address>0x3000</Address>
1663                    <Length>4</Length>
1664                    <AccessMode>RW</AccessMode>
1665                    <Min>0</Min>
1666                    <Max>65535</Max>
1667                    <Mask>0x0000FF00</Mask>
1668                </Integer>
1669            </RegisterDescription>
1670        "#;
1671
1672        let model = parse(XML).expect("parse mask bitfield");
1673        assert_eq!(model.nodes.len(), 1);
1674        match &model.nodes[0] {
1675            NodeDecl::Integer { bitfield, .. } => {
1676                let field = bitfield.as_ref().expect("bitfield present");
1677                assert_eq!(field.byte_order, ByteOrder::Little);
1678                assert_eq!(field.bit_length, 8);
1679                assert_eq!(field.bit_offset, 8);
1680            }
1681            other => panic!("unexpected node: {other:?}"),
1682        }
1683    }
1684
1685    #[test]
1686    fn parse_node_metadata() {
1687        const XML: &str = r#"
1688            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1689                <Integer Name="Width">
1690                    <Address>0x100</Address>
1691                    <Length>4</Length>
1692                    <AccessMode>RW</AccessMode>
1693                    <Min>16</Min>
1694                    <Max>4096</Max>
1695                    <Visibility>Expert</Visibility>
1696                    <Description>Image width in pixels.</Description>
1697                    <ToolTip>Width of the acquired image</ToolTip>
1698                    <DisplayName>Image Width</DisplayName>
1699                    <Representation>Linear</Representation>
1700                </Integer>
1701                <Float Name="Gain">
1702                    <Address>0x200</Address>
1703                    <Length>4</Length>
1704                    <AccessMode>RW</AccessMode>
1705                    <Min>0.0</Min>
1706                    <Max>48.0</Max>
1707                    <Unit>dB</Unit>
1708                    <Visibility>Beginner</Visibility>
1709                    <Representation>Logarithmic</Representation>
1710                </Float>
1711                <Category Name="Root">
1712                    <Visibility>Guru</Visibility>
1713                    <Description>Top-level category</Description>
1714                    <pFeature>Width</pFeature>
1715                    <pFeature>Gain</pFeature>
1716                </Category>
1717                <Enumeration Name="PixelFormat">
1718                    <Address>0x300</Address>
1719                    <Length>4</Length>
1720                    <AccessMode>RW</AccessMode>
1721                    <Visibility>Beginner</Visibility>
1722                    <ToolTip>Pixel format selector</ToolTip>
1723                    <EnumEntry Name="Mono8" Value="0" />
1724                </Enumeration>
1725            </RegisterDescription>
1726        "#;
1727
1728        let model = parse(XML).expect("parse metadata xml");
1729        assert_eq!(model.nodes.len(), 4);
1730
1731        // Integer with full metadata
1732        match &model.nodes[0] {
1733            NodeDecl::Integer { name, meta, .. } => {
1734                assert_eq!(name, "Width");
1735                assert_eq!(meta.visibility, Visibility::Expert);
1736                assert_eq!(meta.description.as_deref(), Some("Image width in pixels."));
1737                assert_eq!(meta.tooltip.as_deref(), Some("Width of the acquired image"));
1738                assert_eq!(meta.display_name.as_deref(), Some("Image Width"));
1739                assert_eq!(meta.representation, Some(Representation::Linear));
1740            }
1741            other => panic!("unexpected node: {other:?}"),
1742        }
1743
1744        // Float with visibility + representation
1745        match &model.nodes[1] {
1746            NodeDecl::Float { name, meta, .. } => {
1747                assert_eq!(name, "Gain");
1748                assert_eq!(meta.visibility, Visibility::Beginner);
1749                assert_eq!(meta.representation, Some(Representation::Logarithmic));
1750                assert!(meta.description.is_none());
1751            }
1752            other => panic!("unexpected node: {other:?}"),
1753        }
1754
1755        // Category with visibility + description
1756        match &model.nodes[2] {
1757            NodeDecl::Category { name, meta, .. } => {
1758                assert_eq!(name, "Root");
1759                assert_eq!(meta.visibility, Visibility::Guru);
1760                assert_eq!(meta.description.as_deref(), Some("Top-level category"));
1761            }
1762            other => panic!("unexpected node: {other:?}"),
1763        }
1764
1765        // Enum with visibility + tooltip
1766        match &model.nodes[3] {
1767            NodeDecl::Enum { name, meta, .. } => {
1768                assert_eq!(name, "PixelFormat");
1769                assert_eq!(meta.visibility, Visibility::Beginner);
1770                assert_eq!(meta.tooltip.as_deref(), Some("Pixel format selector"));
1771            }
1772            other => panic!("unexpected node: {other:?}"),
1773        }
1774    }
1775
1776    /// Wrap a node fragment in a minimal document and parse it.
1777    fn parse_fragment(node: &str) -> XmlModel {
1778        let xml = format!(
1779            r#"<RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="0">{node}</RegisterDescription>"#
1780        );
1781        parse(&xml).expect("parse fragment")
1782    }
1783
1784    /// Extract the metadata of the single node in a parsed fragment.
1785    fn only_meta(model: &XmlModel) -> &NodeMeta {
1786        match model.nodes.first().expect("one node") {
1787            NodeDecl::Integer { meta, .. } => meta,
1788            other => panic!("unexpected node: {other:?}"),
1789        }
1790    }
1791
1792    /// Regression for issue #45: a FLIR BFS-PGE camera failed to open because a
1793    /// CDATA section in a text element was run through XML unescaping, where the
1794    /// literal `&` it legally contains has no `;` to terminate it.
1795    #[test]
1796    fn cdata_text_is_taken_literally() {
1797        let model = parse_fragment(
1798            r#"<Integer Name="Gain">
1799                 <Address>0x100</Address>
1800                 <ToolTip><![CDATA[Gain in dB & raw units, 0 < x < 10]]></ToolTip>
1801               </Integer>"#,
1802        );
1803        assert_eq!(
1804            only_meta(&model).tooltip.as_deref(),
1805            Some("Gain in dB & raw units, 0 < x < 10")
1806        );
1807    }
1808
1809    /// Non-conformant vendor XML with a lone `&` must not stop the document from
1810    /// loading — a cosmetic tooltip is never worth failing a camera connect over.
1811    #[test]
1812    fn dangling_ampersand_is_kept_verbatim() {
1813        let model = parse_fragment(
1814            r#"<Integer Name="Gain">
1815                 <Address>0x100</Address>
1816                 <ToolTip>Exposure & gain</ToolTip>
1817               </Integer>"#,
1818        );
1819        assert_eq!(
1820            only_meta(&model).tooltip.as_deref(),
1821            Some("Exposure & gain")
1822        );
1823    }
1824
1825    /// Comments are markup, not text: they are dropped, and a `&` inside one is
1826    /// not an entity reference.
1827    #[test]
1828    fn comments_inside_text_elements_are_dropped() {
1829        let model = parse_fragment(
1830            r#"<Integer Name="Gain">
1831                 <Address>0x100</Address>
1832                 <Description>Analog <!-- R&D note --> gain</Description>
1833               </Integer>"#,
1834        );
1835        assert_eq!(
1836            only_meta(&model).description.as_deref(),
1837            Some("Analog  gain")
1838        );
1839    }
1840
1841    /// Entity references still resolve, and the whitespace around them survives
1842    /// even though the reader splits character data at each reference.
1843    #[test]
1844    fn entity_references_resolve_and_preserve_spacing() {
1845        let model = parse_fragment(
1846            r#"<Integer Name="Gain">
1847                 <Address>0x100</Address>
1848                 <ToolTip>A &amp; B &lt; C &gt; D &quot;E&quot; &apos;F&apos;</ToolTip>
1849               </Integer>"#,
1850        );
1851        assert_eq!(
1852            only_meta(&model).tooltip.as_deref(),
1853            Some(r#"A & B < C > D "E" 'F'"#)
1854        );
1855    }
1856
1857    /// Numeric character references, both hexadecimal and decimal.
1858    #[test]
1859    fn character_references_resolve() {
1860        let model = parse_fragment(
1861            r#"<Integer Name="Gain">
1862                 <Address>0x100</Address>
1863                 <ToolTip>&#x2014; dash &#8212;</ToolTip>
1864               </Integer>"#,
1865        );
1866        assert_eq!(only_meta(&model).tooltip.as_deref(), Some("— dash —"));
1867    }
1868
1869    /// GenICam declares no DTD, so an entity we cannot resolve is kept as written
1870    /// rather than failing the document.
1871    #[test]
1872    fn unknown_entity_is_kept_as_written() {
1873        let model = parse_fragment(
1874            r#"<Integer Name="Gain">
1875                 <Address>0x100</Address>
1876                 <ToolTip>copyright &copy; vendor</ToolTip>
1877               </Integer>"#,
1878        );
1879        assert_eq!(
1880            only_meta(&model).tooltip.as_deref(),
1881            Some("copyright &copy; vendor")
1882        );
1883    }
1884
1885    /// SwissKnife formulas escape their bitwise/comparison operators; the parsed
1886    /// expression must contain the operators, not the entities.
1887    #[test]
1888    fn swissknife_formula_entities_resolve_to_operators() {
1889        let model = parse_fragment(
1890            r#"<IntSwissKnife Name="GainMask">
1891                 <pVariable Name="RAW">Gain</pVariable>
1892                 <Formula>(RAW &amp; 0xFF) &lt; 16</Formula>
1893               </IntSwissKnife>"#,
1894        );
1895        match model.nodes.first().expect("one node") {
1896            NodeDecl::SwissKnife(node) => assert_eq!(node.expr, "(RAW & 0xFF) < 16"),
1897            other => panic!("unexpected node: {other:?}"),
1898        }
1899    }
1900
1901    /// A SwissKnife whose formula is constant needs no `<pVariable>`. Rejecting
1902    /// these blocked a Hikrobot MV-CS050-10GC on `PixelDynamicRangeMin_Value`
1903    /// (reported in #35) and appears in the standard's own conformance document.
1904    #[test]
1905    fn swissknife_without_variables_is_accepted() {
1906        let model = parse_fragment(
1907            r#"<IntSwissKnife Name="PixelDynamicRangeMin_Value">
1908                 <Formula>0x1234</Formula>
1909               </IntSwissKnife>"#,
1910        );
1911        match model.nodes.first().expect("one node") {
1912            NodeDecl::SwissKnife(node) => {
1913                assert_eq!(node.expr, "0x1234");
1914                assert!(node.variables.is_empty());
1915            }
1916            other => panic!("unexpected node: {other:?}"),
1917        }
1918    }
1919
1920    /// `<AccessMode>` values beyond the standard's three spellings appear in
1921    /// third-party documents; an odd one must not cost the whole camera.
1922    #[test]
1923    fn access_mode_accepts_aliases_and_defaults_unknown_to_rw() {
1924        assert_eq!(AccessMode::parse("R").unwrap(), AccessMode::RO);
1925        assert_eq!(AccessMode::parse("W").unwrap(), AccessMode::WO);
1926        assert_eq!(AccessMode::parse("WR").unwrap(), AccessMode::RW);
1927        assert_eq!(AccessMode::parse(" ro ").unwrap(), AccessMode::RO);
1928        assert_eq!(AccessMode::parse("Bogus").unwrap(), AccessMode::RW);
1929    }
1930
1931    /// A node we cannot parse costs that one feature, not the whole document —
1932    /// and the reader stays in step, so nodes after it still load.
1933    #[test]
1934    fn unparsable_node_is_skipped_not_fatal() {
1935        let model = parse_fragment(
1936            r#"<Integer Name="Before">
1937                 <Address>0x100</Address>
1938                 <Length>4</Length>
1939               </Integer>
1940               <Integer Name="Broken">
1941                 <Address>0xZZZZ</Address>
1942                 <Length>4</Length>
1943               </Integer>
1944               <Integer Name="After">
1945                 <Address>0x200</Address>
1946                 <Length>4</Length>
1947               </Integer>"#,
1948        );
1949
1950        let names: Vec<&str> = model
1951            .nodes
1952            .iter()
1953            .map(|node| match node {
1954                NodeDecl::Integer { name, .. } => name.as_str(),
1955                other => panic!("unexpected node: {other:?}"),
1956            })
1957            .collect();
1958        assert_eq!(names, ["Before", "After"]);
1959
1960        assert_eq!(model.skipped.len(), 1);
1961        let skipped = &model.skipped[0];
1962        assert_eq!(skipped.tag, "Integer");
1963        assert_eq!(skipped.name.as_deref(), Some("Broken"));
1964        assert!(
1965            skipped.error.contains("invalid hex"),
1966            "unexpected error: {}",
1967            skipped.error
1968        );
1969    }
1970
1971    /// Isolation must survive a node whose failure happens before the parser has
1972    /// consumed any children — the outer reader is positioned by the caller, not
1973    /// by how far the node parser got.
1974    #[test]
1975    fn node_missing_required_name_is_skipped() {
1976        let model = parse_fragment(
1977            r#"<Integer>
1978                 <Address>0x100</Address>
1979               </Integer>
1980               <Integer Name="Good">
1981                 <Address>0x200</Address>
1982                 <Length>4</Length>
1983               </Integer>"#,
1984        );
1985        assert_eq!(model.nodes.len(), 1);
1986        assert_eq!(model.skipped.len(), 1);
1987        assert_eq!(model.skipped[0].name, None);
1988    }
1989
1990    /// A clean document reports nothing skipped.
1991    #[test]
1992    fn clean_document_skips_nothing() {
1993        let model = parse(FIXTURE).expect("parse fixture");
1994        assert!(model.skipped.is_empty());
1995    }
1996}