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 serde::{Deserialize, Serialize};
19use thiserror::Error;
20
21use parsers::{
22    parse_boolean, parse_category, parse_category_empty, parse_command, parse_command_empty,
23    parse_converter, parse_enum, parse_float, parse_int_converter, parse_integer, parse_string,
24    parse_struct_reg, parse_swissknife,
25};
26use util::{attribute_value, skip_element};
27
28/// Source of the numeric value backing an enumeration entry.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub enum EnumValueSrc {
31    /// Numeric literal declared directly in the XML.
32    Literal(i64),
33    /// Value obtained from another node referenced via `<pValue>`.
34    FromNode(String),
35}
36
37/// Declaration for a single enumeration entry.
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct EnumEntryDecl {
40    /// Symbolic entry name exposed to clients.
41    pub name: String,
42    /// Source describing how to resolve the numeric value for this entry.
43    pub value: EnumValueSrc,
44    /// Optional user facing label.
45    pub display_name: Option<String>,
46}
47
48#[derive(Debug, Error)]
49#[non_exhaustive]
50pub enum XmlError {
51    #[error("xml: {0}")]
52    Xml(String),
53    #[error("invalid descriptor: {0}")]
54    Invalid(String),
55    #[error("transport: {0}")]
56    Transport(String),
57    #[error("unsupported URL: {0}")]
58    Unsupported(String),
59}
60
61/// Access privileges for a GenICam node as described in the XML.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63pub enum AccessMode {
64    /// Read-only node. The underlying register must not be modified by the client.
65    RO,
66    /// Write-only node. Reading the register is not permitted.
67    WO,
68    /// Read-write node. The register may be read and written by the client.
69    RW,
70}
71
72impl AccessMode {
73    pub(crate) fn parse(value: &str) -> Result<Self, XmlError> {
74        match value.trim().to_ascii_uppercase().as_str() {
75            "RO" => Ok(AccessMode::RO),
76            "WO" => Ok(AccessMode::WO),
77            "RW" => Ok(AccessMode::RW),
78            other => Err(XmlError::Invalid(format!("unknown access mode: {other}"))),
79        }
80    }
81}
82
83/// Register addressing metadata for a node.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub enum Addressing {
86    /// Node uses a fixed register block regardless of selector state.
87    Fixed { address: u64, len: u32 },
88    /// Node switches between register blocks based on a selector value.
89    BySelector {
90        /// Name of the selector node controlling the address.
91        selector: String,
92        /// Mapping of selector value to `(address, length)` pair.
93        map: Vec<(String, (u64, u32))>,
94    },
95    /// Node resolves its register block through another node providing the address.
96    Indirect {
97        /// Node providing the register address at runtime.
98        p_address_node: String,
99        /// Length of the target register block in bytes.
100        len: u32,
101    },
102}
103
104/// Byte order used to interpret a multi-byte register payload.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
106pub enum ByteOrder {
107    /// The first byte contains the least significant bits.
108    Little,
109    /// The first byte contains the most significant bits.
110    Big,
111}
112
113impl ByteOrder {
114    pub(crate) fn parse(tag: &str) -> Option<Self> {
115        match tag.trim().to_ascii_lowercase().as_str() {
116            "littleendian" => Some(ByteOrder::Little),
117            "bigendian" => Some(ByteOrder::Big),
118            _ => None,
119        }
120    }
121}
122
123/// Bitfield metadata describing a sub-range of a register payload.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125pub struct BitField {
126    /// Starting bit offset within the interpreted register value.
127    pub bit_offset: u16,
128    /// Number of bits covered by the field.
129    pub bit_length: u16,
130    /// Byte order used when interpreting the enclosing register.
131    pub byte_order: ByteOrder,
132}
133
134/// Output type of a SwissKnife expression node.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
136pub enum SkOutput {
137    /// Integer output. The runtime rounds the computed value to the nearest
138    /// integer with ties going towards zero.
139    Integer,
140    /// Floating point output. The runtime exposes the value as a `f64` without
141    /// any additional processing.
142    #[default]
143    Float,
144}
145
146impl SkOutput {
147    pub(crate) fn parse(tag: &str) -> Option<Self> {
148        match tag.trim().to_ascii_lowercase().as_str() {
149            "integer" => Some(SkOutput::Integer),
150            "float" => Some(SkOutput::Float),
151            _ => None,
152        }
153    }
154}
155
156/// Declaration of a SwissKnife node consisting of an arithmetic expression.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct SwissKnifeDecl {
159    /// Feature name exposed to clients.
160    pub name: String,
161    /// Raw expression string to be parsed by the runtime.
162    pub expr: String,
163    /// Mapping of variables used in the expression to provider node names.
164    pub variables: Vec<(String, String)>,
165    /// Desired output type (integer or float).
166    pub output: SkOutput,
167}
168
169/// Declaration of a Converter node for bidirectional value transformation.
170///
171/// Converters expose a floating-point value computed from an underlying
172/// register or node via a formula.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct ConverterDecl {
175    /// Feature name exposed to clients.
176    pub name: String,
177    /// Name of the node providing the raw register value.
178    pub p_value: String,
179    /// Expression converting raw register value to user-facing value (FROM direction).
180    pub formula_to: String,
181    /// Expression converting user-facing value back to raw register value (TO direction).
182    pub formula_from: String,
183    /// Mapping of expression variables to provider node names for `formula_to`.
184    pub variables_to: Vec<(String, String)>,
185    /// Mapping of expression variables to provider node names for `formula_from`.
186    pub variables_from: Vec<(String, String)>,
187    /// Engineering unit (if provided).
188    pub unit: Option<String>,
189    /// Desired output type.
190    pub output: SkOutput,
191}
192
193/// Declaration of an IntConverter node for integer-specific bidirectional conversion.
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct IntConverterDecl {
196    /// Feature name exposed to clients.
197    pub name: String,
198    /// Name of the node providing the raw register value.
199    pub p_value: String,
200    /// Expression converting raw register value to user-facing value (FROM direction).
201    pub formula_to: String,
202    /// Expression converting user-facing value back to raw register value (TO direction).
203    pub formula_from: String,
204    /// Mapping of expression variables to provider node names for `formula_to`.
205    pub variables_to: Vec<(String, String)>,
206    /// Mapping of expression variables to provider node names for `formula_from`.
207    pub variables_from: Vec<(String, String)>,
208    /// Engineering unit (if provided).
209    pub unit: Option<String>,
210}
211
212/// Declaration of a StringReg node for string-typed register access.
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct StringDecl {
215    /// Feature name exposed to clients.
216    pub name: String,
217    /// Addressing metadata for the register block.
218    pub addressing: Addressing,
219    /// Access privileges.
220    pub access: AccessMode,
221}
222
223/// Declaration of a node extracted from the GenICam XML description.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub enum NodeDecl {
226    /// Integer feature backed by a register block or delegated via pValue.
227    Integer {
228        /// Feature name.
229        name: String,
230        /// Addressing metadata (absent when delegated via `pvalue`).
231        addressing: Option<Addressing>,
232        /// Length in bytes of the register payload.
233        len: u32,
234        /// Access privileges.
235        access: AccessMode,
236        /// Minimum allowed user value.
237        min: i64,
238        /// Maximum allowed user value.
239        max: i64,
240        /// Optional increment step enforced by the device.
241        inc: Option<i64>,
242        /// Engineering unit (if provided).
243        unit: Option<String>,
244        /// Optional bitfield metadata describing the active bit range.
245        bitfield: Option<BitField>,
246        /// Selector nodes referencing this feature.
247        selectors: Vec<String>,
248        /// Selector gating rules in the form (selector name, allowed values).
249        selected_if: Vec<(String, Vec<String>)>,
250        /// Node providing the value (delegates read/write to another node).
251        pvalue: Option<String>,
252        /// Node providing the dynamic maximum.
253        p_max: Option<String>,
254        /// Node providing the dynamic minimum.
255        p_min: Option<String>,
256        /// Static value (for constant integer nodes with `<Value>`).
257        value: Option<i64>,
258    },
259    /// Floating point feature backed by an integer register with scaling
260    /// or delegated via pValue.
261    Float {
262        name: String,
263        /// Addressing metadata (absent when delegated via `pvalue`).
264        addressing: Option<Addressing>,
265        access: AccessMode,
266        min: f64,
267        max: f64,
268        unit: Option<String>,
269        /// Optional rational scale applied to the raw register value.
270        scale: Option<(i64, i64)>,
271        /// Optional additive offset applied after scaling.
272        offset: Option<f64>,
273        selectors: Vec<String>,
274        selected_if: Vec<(String, Vec<String>)>,
275        /// Node providing the value (delegates read/write to another node).
276        pvalue: Option<String>,
277    },
278    /// Enumeration feature exposing a list of named integer values.
279    Enum {
280        name: String,
281        /// Addressing metadata (absent when delegated via `pvalue`).
282        addressing: Option<Addressing>,
283        access: AccessMode,
284        entries: Vec<EnumEntryDecl>,
285        default: Option<String>,
286        selectors: Vec<String>,
287        selected_if: Vec<(String, Vec<String>)>,
288        /// Node providing the integer value (delegates register read/write).
289        pvalue: Option<String>,
290    },
291    /// Boolean feature backed by a single bit/byte register or delegated via pValue.
292    Boolean {
293        name: String,
294        /// Addressing metadata (absent when delegated via `pvalue`).
295        addressing: Option<Addressing>,
296        len: u32,
297        access: AccessMode,
298        bitfield: Option<BitField>,
299        selectors: Vec<String>,
300        selected_if: Vec<(String, Vec<String>)>,
301        /// Node providing the value (delegates read/write to another node).
302        pvalue: Option<String>,
303        /// On value for pValue-backed booleans.
304        on_value: Option<i64>,
305        /// Off value for pValue-backed booleans.
306        off_value: Option<i64>,
307    },
308    /// Command feature that triggers an action when written.
309    Command {
310        name: String,
311        /// Fixed register address (absent when delegated via `pvalue`).
312        address: Option<u64>,
313        len: u32,
314        /// Node providing the command register (delegates write).
315        pvalue: Option<String>,
316        /// Value to write when executing the command.
317        command_value: Option<i64>,
318    },
319    /// Category used to organise features.
320    Category { name: String, children: Vec<String> },
321    /// Computed value backed by an arithmetic expression referencing other nodes.
322    SwissKnife(SwissKnifeDecl),
323    /// Converter transforming raw values to/from user-facing floating-point values.
324    Converter(ConverterDecl),
325    /// IntConverter transforming raw values to/from user-facing integer values.
326    IntConverter(IntConverterDecl),
327    /// StringReg for string-typed register access.
328    String(StringDecl),
329}
330
331/// Full XML model describing the GenICam schema version and all declared nodes.
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct XmlModel {
334    /// Combined schema version extracted from the RegisterDescription attributes.
335    pub version: String,
336    /// Flat list of node declarations present in the document.
337    pub nodes: Vec<NodeDecl>,
338}
339
340/// Minimal metadata extracted from a quick XML scan.
341#[derive(Debug, Clone, PartialEq, Eq)]
342pub struct MinimalXmlInfo {
343    pub schema_version: Option<String>,
344    pub top_level_features: Vec<String>,
345}
346
347/// Parse a GenICam XML snippet and collect minimal metadata.
348pub fn parse_into_minimal_nodes(xml: &str) -> Result<MinimalXmlInfo, XmlError> {
349    let mut reader = Reader::from_str(xml);
350    reader.trim_text(true);
351    let mut buf = Vec::new();
352    let mut depth = 0usize;
353    let mut schema_version: Option<String> = None;
354    let mut top_level_features = Vec::new();
355
356    loop {
357        match reader.read_event_into(&mut buf) {
358            Ok(Event::Start(e)) => {
359                depth += 1;
360                handle_start(&e, depth, &mut schema_version, &mut top_level_features)?;
361            }
362            Ok(Event::Empty(e)) => {
363                depth += 1;
364                handle_start(&e, depth, &mut schema_version, &mut top_level_features)?;
365                if depth > 0 {
366                    depth = depth.saturating_sub(1);
367                }
368            }
369            Ok(Event::End(_)) => {
370                if depth > 0 {
371                    depth = depth.saturating_sub(1);
372                }
373            }
374            Ok(Event::Eof) => break,
375            Err(err) => return Err(XmlError::Xml(err.to_string())),
376            _ => {}
377        }
378        buf.clear();
379    }
380
381    Ok(MinimalXmlInfo {
382        schema_version,
383        top_level_features,
384    })
385}
386
387/// Parse a GenICam XML document into an [`XmlModel`].
388///
389/// The parser only understands a practical subset of the schema. Unknown tags
390/// are skipped which keeps the implementation forward compatible with richer
391/// documents.
392pub fn parse(xml: &str) -> Result<XmlModel, XmlError> {
393    let mut reader = Reader::from_str(xml);
394    reader.trim_text(true);
395    let mut buf = Vec::new();
396    let mut version = String::from("0.0.0");
397    let mut nodes = Vec::new();
398
399    loop {
400        match reader.read_event_into(&mut buf) {
401            Ok(Event::Start(ref e)) => match e.name().as_ref() {
402                b"RegisterDescription" => {
403                    version = schema_version_from(e)?;
404                }
405                b"Integer" | b"IntReg" | b"MaskedIntReg" => {
406                    let node = parse_integer(&mut reader, e.clone())?;
407                    nodes.push(node);
408                }
409                b"IntSwissKnife" => {
410                    let node = parse_swissknife(&mut reader, e.clone())?;
411                    nodes.push(node);
412                }
413                b"Float" | b"FloatReg" => {
414                    let node = parse_float(&mut reader, e.clone())?;
415                    nodes.push(node);
416                }
417                b"Enumeration" => {
418                    let node = parse_enum(&mut reader, e.clone())?;
419                    nodes.push(node);
420                }
421                b"Boolean" => {
422                    let node = parse_boolean(&mut reader, e.clone())?;
423                    nodes.push(node);
424                }
425                b"Command" => {
426                    let node = parse_command(&mut reader, e.clone())?;
427                    nodes.push(node);
428                }
429                b"Category" => {
430                    let node = parse_category(&mut reader, e.clone())?;
431                    nodes.push(node);
432                }
433                b"SwissKnife" => {
434                    let node = parse_swissknife(&mut reader, e.clone())?;
435                    nodes.push(node);
436                }
437                b"Converter" => {
438                    let node = parse_converter(&mut reader, e.clone())?;
439                    nodes.push(node);
440                }
441                b"IntConverter" => {
442                    let node = parse_int_converter(&mut reader, e.clone())?;
443                    nodes.push(node);
444                }
445                b"StringReg" | b"String" => {
446                    let node = parse_string(&mut reader, e.clone())?;
447                    nodes.push(node);
448                }
449                b"StructReg" => {
450                    let entries = parse_struct_reg(&mut reader, e.clone())?;
451                    nodes.extend(entries);
452                }
453                b"Port" => {
454                    // Port nodes are transport-level abstractions; skip them.
455                    skip_element(&mut reader, e.name().as_ref())?;
456                }
457                _ => {
458                    skip_element(&mut reader, e.name().as_ref())?;
459                }
460            },
461            Ok(Event::Empty(ref e)) => match e.name().as_ref() {
462                b"RegisterDescription" => {
463                    version = schema_version_from(e)?;
464                }
465                b"Command" => {
466                    let node = parse_command_empty(e)?;
467                    nodes.push(node);
468                }
469                b"Category" => {
470                    let node = parse_category_empty(e)?;
471                    nodes.push(node);
472                }
473                _ => {}
474            },
475            Ok(Event::Eof) => break,
476            Err(err) => return Err(XmlError::Xml(err.to_string())),
477            _ => {}
478        }
479        buf.clear();
480    }
481
482    Ok(XmlModel { version, nodes })
483}
484
485fn schema_version_from(event: &BytesStart<'_>) -> Result<String, XmlError> {
486    let major = attribute_value(event, b"SchemaMajorVersion")?;
487    let minor = attribute_value(event, b"SchemaMinorVersion")?;
488    let sub = attribute_value(event, b"SchemaSubMinorVersion")?;
489    let major = major.unwrap_or_else(|| "0".to_string());
490    let minor = minor.unwrap_or_else(|| "0".to_string());
491    let sub = sub.unwrap_or_else(|| "0".to_string());
492    Ok(format!("{major}.{minor}.{sub}"))
493}
494
495fn handle_start(
496    event: &BytesStart<'_>,
497    depth: usize,
498    schema_version: &mut Option<String>,
499    top_level: &mut Vec<String>,
500) -> Result<(), XmlError> {
501    if depth == 1 && schema_version.is_none() {
502        *schema_version = extract_schema_version(event);
503    } else if depth == 2 {
504        if let Some(name) = attribute_value(event, b"Name")? {
505            top_level.push(name);
506        } else {
507            top_level.push(String::from_utf8_lossy(event.name().as_ref()).to_string());
508        }
509    }
510    Ok(())
511}
512
513fn extract_schema_version(event: &BytesStart<'_>) -> Option<String> {
514    let major = attribute_value(event, b"SchemaMajorVersion").ok().flatten();
515    let minor = attribute_value(event, b"SchemaMinorVersion").ok().flatten();
516    let sub = attribute_value(event, b"SchemaSubMinorVersion")
517        .ok()
518        .flatten();
519    if major.is_none() && minor.is_none() && sub.is_none() {
520        None
521    } else {
522        let major = major.unwrap_or_else(|| "0".to_string());
523        let minor = minor.unwrap_or_else(|| "0".to_string());
524        let sub = sub.unwrap_or_else(|| "0".to_string());
525        Some(format!("{major}.{minor}.{sub}"))
526    }
527}
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532
533    const FIXTURE: &str = r#"
534        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="2" SchemaSubMinorVersion="3">
535            <Category Name="Root">
536                <pFeature>Gain</pFeature>
537                <pFeature>GainSelector</pFeature>
538            </Category>
539            <Integer Name="Width">
540                <Address>0x0000_0100</Address>
541                <Length>4</Length>
542                <AccessMode>RW</AccessMode>
543                <Min>16</Min>
544                <Max>4096</Max>
545                <Inc>2</Inc>
546            </Integer>
547            <Float Name="ExposureTime">
548                <Address>0x0000_0200</Address>
549                <Length>4</Length>
550                <AccessMode>RW</AccessMode>
551                <Min>10.0</Min>
552                <Max>200000.0</Max>
553                <Scale>1/1000</Scale>
554                <Offset>0.0</Offset>
555            </Float>
556            <Enumeration Name="GainSelector">
557                <Address>0x0000_0300</Address>
558                <Length>2</Length>
559                <AccessMode>RW</AccessMode>
560                <EnumEntry Name="AnalogAll" Value="0" />
561                <EnumEntry Name="DigitalAll" Value="1" />
562            </Enumeration>
563            <Integer Name="Gain">
564                <Address>0x0000_0304</Address>
565                <Length>2</Length>
566                <AccessMode>RW</AccessMode>
567                <Min>0</Min>
568                <Max>48</Max>
569                <pSelected>GainSelector</pSelected>
570                <Selected>AnalogAll</Selected>
571            </Integer>
572            <Boolean Name="GammaEnable">
573                <Address>0x0000_0400</Address>
574                <Length>1</Length>
575                <AccessMode>RW</AccessMode>
576            </Boolean>
577            <Command Name="AcquisitionStart">
578                <Address>0x0000_0500</Address>
579                <Length>4</Length>
580            </Command>
581        </RegisterDescription>
582    "#;
583
584    #[test]
585    fn parse_minimal_xml() {
586        let info = parse_into_minimal_nodes(FIXTURE).expect("parse xml");
587        assert_eq!(info.schema_version.as_deref(), Some("1.2.3"));
588        assert_eq!(info.top_level_features.len(), 7);
589        assert_eq!(info.top_level_features[0], "Root");
590    }
591
592    #[test]
593    fn parse_fixture_model() {
594        let model = parse(FIXTURE).expect("parse fixture");
595        assert_eq!(model.version, "1.2.3");
596        assert_eq!(model.nodes.len(), 7);
597        match &model.nodes[0] {
598            NodeDecl::Category { name, children } => {
599                assert_eq!(name, "Root");
600                assert_eq!(
601                    children,
602                    &vec!["Gain".to_string(), "GainSelector".to_string()]
603                );
604            }
605            other => panic!("unexpected node: {other:?}"),
606        }
607        match &model.nodes[1] {
608            NodeDecl::Integer {
609                name,
610                min,
611                max,
612                inc,
613                ..
614            } => {
615                assert_eq!(name, "Width");
616                assert_eq!(*min, 16);
617                assert_eq!(*max, 4096);
618                assert_eq!(*inc, Some(2));
619            }
620            other => panic!("unexpected node: {other:?}"),
621        }
622        match &model.nodes[2] {
623            NodeDecl::Float {
624                name,
625                scale,
626                offset,
627                ..
628            } => {
629                assert_eq!(name, "ExposureTime");
630                assert_eq!(*scale, Some((1, 1000)));
631                assert_eq!(*offset, Some(0.0));
632            }
633            other => panic!("unexpected node: {other:?}"),
634        }
635        match &model.nodes[3] {
636            NodeDecl::Enum { name, entries, .. } => {
637                assert_eq!(name, "GainSelector");
638                assert_eq!(entries.len(), 2);
639                assert!(matches!(entries[0].value, EnumValueSrc::Literal(0)));
640                assert!(matches!(entries[1].value, EnumValueSrc::Literal(1)));
641            }
642            other => panic!("unexpected node: {other:?}"),
643        }
644        match &model.nodes[4] {
645            NodeDecl::Integer {
646                name, selected_if, ..
647            } => {
648                assert_eq!(name, "Gain");
649                assert_eq!(selected_if.len(), 1);
650                assert_eq!(selected_if[0].0, "GainSelector");
651                assert_eq!(selected_if[0].1, vec!["AnalogAll".to_string()]);
652            }
653            other => panic!("unexpected node: {other:?}"),
654        }
655    }
656
657    #[test]
658    fn parse_swissknife_node() {
659        const XML: &str = r#"
660            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
661                <Integer Name="GainRaw">
662                    <Address>0x3000</Address>
663                    <Length>4</Length>
664                    <AccessMode>RW</AccessMode>
665                    <Min>0</Min>
666                    <Max>1000</Max>
667                </Integer>
668                <Float Name="Offset">
669                    <Address>0x3008</Address>
670                    <Length>4</Length>
671                    <AccessMode>RW</AccessMode>
672                    <Min>-100.0</Min>
673                    <Max>100.0</Max>
674                </Float>
675                <SwissKnife Name="ComputedGain">
676                    <Expression>(GainRaw * 0.5) + Offset</Expression>
677                    <pVariable Name="GainRaw">GainRaw</pVariable>
678                    <pVariable Name="Offset">Offset</pVariable>
679                    <Output>Float</Output>
680                </SwissKnife>
681            </RegisterDescription>
682        "#;
683
684        let model = parse(XML).expect("parse swissknife xml");
685        assert_eq!(model.nodes.len(), 3);
686        let swiss = model
687            .nodes
688            .iter()
689            .find_map(|decl| match decl {
690                NodeDecl::SwissKnife(node) => Some(node),
691                _ => None,
692            })
693            .expect("swissknife present");
694        assert_eq!(swiss.name, "ComputedGain");
695        assert_eq!(swiss.expr, "(GainRaw * 0.5) + Offset");
696        assert_eq!(swiss.output, SkOutput::Float);
697        assert_eq!(swiss.variables.len(), 2);
698        assert_eq!(
699            swiss.variables[0],
700            ("GainRaw".to_string(), "GainRaw".to_string())
701        );
702        assert_eq!(
703            swiss.variables[1],
704            ("Offset".to_string(), "Offset".to_string())
705        );
706    }
707
708    #[test]
709    fn parse_int_swissknife_with_hex_and_ampersand() {
710        // Test that &amp; is decoded to & and hex literals are supported.
711        const XML: &str = r#"
712            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
713                <IntSwissKnife Name="PayloadSize">
714                    <pVariable Name="W">Width</pVariable>
715                    <pVariable Name="H">Height</pVariable>
716                    <pVariable Name="PF">PixelFormat</pVariable>
717                    <Formula>W * H * ((PF>>16)&amp;0xFF) / 8</Formula>
718                </IntSwissKnife>
719            </RegisterDescription>
720        "#;
721
722        let model = parse(XML).expect("parse intswissknife");
723        assert_eq!(model.nodes.len(), 1);
724        let swiss = model
725            .nodes
726            .iter()
727            .find_map(|decl| match decl {
728                NodeDecl::SwissKnife(node) => Some(node),
729                _ => None,
730            })
731            .expect("swissknife present");
732        assert_eq!(swiss.name, "PayloadSize");
733        // &amp; should be decoded to &
734        assert!(
735            swiss.expr.contains('&'),
736            "expression should contain decoded '&': {}",
737            swiss.expr
738        );
739        assert!(
740            swiss.expr.contains("0xFF"),
741            "expression should contain hex literal: {}",
742            swiss.expr
743        );
744    }
745
746    #[test]
747    fn parse_enum_entry_with_pvalue() {
748        const XML: &str = r#"
749            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
750                <Enumeration Name="Mode">
751                    <Address>0x0000_4000</Address>
752                    <Length>4</Length>
753                    <AccessMode>RW</AccessMode>
754                    <EnumEntry Name="Fixed10">
755                        <Value>10</Value>
756                    </EnumEntry>
757                    <EnumEntry Name="DynFromReg">
758                        <pValue>RegModeVal</pValue>
759                    </EnumEntry>
760                </Enumeration>
761                <Integer Name="RegModeVal">
762                    <Address>0x0000_4100</Address>
763                    <Length>4</Length>
764                    <AccessMode>RW</AccessMode>
765                    <Min>0</Min>
766                    <Max>65535</Max>
767                </Integer>
768            </RegisterDescription>
769        "#;
770
771        let model = parse(XML).expect("parse enum pvalue");
772        assert_eq!(model.nodes.len(), 2);
773        match &model.nodes[0] {
774            NodeDecl::Enum { entries, .. } => {
775                assert_eq!(entries.len(), 2);
776                assert!(matches!(entries[0].value, EnumValueSrc::Literal(10)));
777                match &entries[1].value {
778                    EnumValueSrc::FromNode(node) => assert_eq!(node, "RegModeVal"),
779                    other => panic!("unexpected entry value: {other:?}"),
780                }
781            }
782            other => panic!("unexpected node: {other:?}"),
783        }
784    }
785
786    #[test]
787    fn parse_indirect_addressing() {
788        const XML: &str = r#"
789            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
790                <Integer Name="RegAddr">
791                    <Address>0x2000</Address>
792                    <Length>4</Length>
793                    <AccessMode>RW</AccessMode>
794                    <Min>0</Min>
795                    <Max>65535</Max>
796                </Integer>
797                <Integer Name="Gain" Address="0xFFFF">
798                    <pAddress>RegAddr</pAddress>
799                    <Length>4</Length>
800                    <AccessMode>RW</AccessMode>
801                    <Min>0</Min>
802                    <Max>255</Max>
803                </Integer>
804            </RegisterDescription>
805        "#;
806
807        let model = parse(XML).expect("parse indirect xml");
808        assert_eq!(model.nodes.len(), 2);
809        match &model.nodes[0] {
810            NodeDecl::Integer {
811                name, addressing, ..
812            } => {
813                assert_eq!(name, "RegAddr");
814                assert!(
815                    matches!(addressing, Some(Addressing::Fixed { address, len }) if *address == 0x2000 && *len == 4)
816                );
817            }
818            other => panic!("unexpected node: {other:?}"),
819        }
820        match &model.nodes[1] {
821            NodeDecl::Integer {
822                name, addressing, ..
823            } => {
824                assert_eq!(name, "Gain");
825                match addressing {
826                    Some(Addressing::Indirect {
827                        p_address_node,
828                        len,
829                    }) => {
830                        assert_eq!(p_address_node, "RegAddr");
831                        assert_eq!(*len, 4);
832                    }
833                    other => panic!("expected indirect addressing, got {other:?}"),
834                }
835            }
836            other => panic!("unexpected node: {other:?}"),
837        }
838    }
839
840    #[test]
841    fn parse_integer_bitfield_big_endian() {
842        const XML: &str = r#"
843            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
844                <Integer Name="Packed">
845                    <Address>0x1000</Address>
846                    <Length>4</Length>
847                    <AccessMode>RW</AccessMode>
848                    <Min>0</Min>
849                    <Max>65535</Max>
850                    <Lsb>8</Lsb>
851                    <Msb>15</Msb>
852                    <Endianness>BigEndian</Endianness>
853                </Integer>
854            </RegisterDescription>
855        "#;
856
857        let model = parse(XML).expect("parse big-endian bitfield");
858        assert_eq!(model.nodes.len(), 1);
859        match &model.nodes[0] {
860            NodeDecl::Integer { len, bitfield, .. } => {
861                assert_eq!(*len, 4);
862                let field = bitfield.as_ref().expect("bitfield present");
863                assert_eq!(field.byte_order, ByteOrder::Big);
864                assert_eq!(field.bit_length, 8);
865                assert_eq!(field.bit_offset, 16);
866            }
867            other => panic!("unexpected node: {other:?}"),
868        }
869    }
870
871    #[test]
872    fn parse_boolean_bitfield_default_length() {
873        const XML: &str = r#"
874            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
875                <Boolean Name="Flag">
876                    <Address>0x2000</Address>
877                    <Length>1</Length>
878                    <AccessMode>RW</AccessMode>
879                    <Bit>3</Bit>
880                </Boolean>
881            </RegisterDescription>
882        "#;
883
884        let model = parse(XML).expect("parse boolean bitfield");
885        assert_eq!(model.nodes.len(), 1);
886        match &model.nodes[0] {
887            NodeDecl::Boolean { len, bitfield, .. } => {
888                assert_eq!(*len, 1);
889                let bf = bitfield.as_ref().expect("bitfield present");
890                assert_eq!(bf.byte_order, ByteOrder::Little);
891                assert_eq!(bf.bit_length, 1);
892                assert_eq!(bf.bit_offset, 3);
893            }
894            other => panic!("unexpected node: {other:?}"),
895        }
896    }
897
898    #[test]
899    fn parse_integer_bitfield_mask() {
900        const XML: &str = r#"
901            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
902                <Integer Name="Masked">
903                    <Address>0x3000</Address>
904                    <Length>4</Length>
905                    <AccessMode>RW</AccessMode>
906                    <Min>0</Min>
907                    <Max>65535</Max>
908                    <Mask>0x0000FF00</Mask>
909                </Integer>
910            </RegisterDescription>
911        "#;
912
913        let model = parse(XML).expect("parse mask bitfield");
914        assert_eq!(model.nodes.len(), 1);
915        match &model.nodes[0] {
916            NodeDecl::Integer { bitfield, .. } => {
917                let field = bitfield.as_ref().expect("bitfield present");
918                assert_eq!(field.byte_order, ByteOrder::Little);
919                assert_eq!(field.bit_length, 8);
920                assert_eq!(field.bit_offset, 8);
921            }
922            other => panic!("unexpected node: {other:?}"),
923        }
924    }
925}