Skip to main content

zerodds_xml_wire/
codec.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! XML↔CDR-Codec — DDS-XML 1.0 §6.4.
4//!
5//! Spec §6.4: Topic-Sample-Mapping zu XML. Die Sample-Struktur wird
6//! als `<Sample><field-name>value</field-name>...</Sample>` codiert,
7//! mit einem Type-Discriminator als Root-Tag.
8
9use alloc::collections::BTreeMap;
10use alloc::format;
11use alloc::string::{String, ToString};
12use alloc::vec::Vec;
13
14use crate::emitter::{EmitError, XmlEmitter};
15use crate::parser::{Event, ParseError, XmlParser};
16
17/// Field-Kind (DDS-Builtin-Types Map zu DDS-XML §6.4 Tab.1).
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum FieldKind {
20    /// `int8`/`int16`/`int32`/`int64` — als XML-Integer.
21    Integer,
22    /// `float32`/`float64` — als XML-Double.
23    Float,
24    /// `bool` — als `true`/`false`.
25    Bool,
26    /// `string`/`wstring` — als XML-Text mit Entity-Encoding.
27    String,
28    /// `octet[]` — als Hex-encoded XML-Text.
29    Bytes,
30}
31
32/// Field-Value mit Kind-Info.
33#[derive(Debug, Clone, PartialEq)]
34pub enum FieldValue {
35    /// 64-bit signed integer (deckt alle DDS-Integer-Builtins ab).
36    Integer(i64),
37    /// 64-bit float.
38    Float(f64),
39    /// Boolean.
40    Bool(bool),
41    /// String.
42    String(String),
43    /// Bytes (Hex-codiert beim Emit).
44    Bytes(Vec<u8>),
45}
46
47impl FieldValue {
48    /// Field-Kind dieses Values.
49    #[must_use]
50    pub fn kind(&self) -> FieldKind {
51        match self {
52            Self::Integer(_) => FieldKind::Integer,
53            Self::Float(_) => FieldKind::Float,
54            Self::Bool(_) => FieldKind::Bool,
55            Self::String(_) => FieldKind::String,
56            Self::Bytes(_) => FieldKind::Bytes,
57        }
58    }
59}
60
61/// Codec-Fehler.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum CodecError {
64    /// XML-Parse-Fehler.
65    Parse(ParseError),
66    /// XML-Emit-Fehler.
67    Emit(EmitError),
68    /// Field hat einen fuer den `FieldKind` ungueltigen Text-Inhalt.
69    InvalidValue {
70        /// Field-Name.
71        field: String,
72        /// Erwarteter Kind.
73        kind: FieldKind,
74        /// Gelesener Wert.
75        got: String,
76    },
77    /// Kein Sample-Root-Element.
78    NoSampleRoot,
79    /// Hex-Decoding-Fehler.
80    InvalidHex,
81}
82
83impl core::fmt::Display for CodecError {
84    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
85        match self {
86            Self::Parse(e) => write!(f, "parse: {e}"),
87            Self::Emit(e) => write!(f, "emit: {e}"),
88            Self::InvalidValue { field, kind, got } => {
89                write!(f, "field `{field}`: expected {kind:?}, got `{got}`")
90            }
91            Self::NoSampleRoot => f.write_str("no sample root element"),
92            Self::InvalidHex => f.write_str("invalid hex string"),
93        }
94    }
95}
96
97#[cfg(feature = "std")]
98impl std::error::Error for CodecError {}
99
100impl From<ParseError> for CodecError {
101    fn from(e: ParseError) -> Self {
102        Self::Parse(e)
103    }
104}
105
106impl From<EmitError> for CodecError {
107    fn from(e: EmitError) -> Self {
108        Self::Emit(e)
109    }
110}
111
112/// Encode eine Sample-Map in XML. Spec §6.4.
113///
114/// # Errors
115/// `Emit` bei Tag-Namen-Validation.
116pub fn encode_to_xml(
117    type_name: &str,
118    fields: &BTreeMap<String, FieldValue>,
119) -> Result<String, CodecError> {
120    let mut e = XmlEmitter::new();
121    e.start_element(type_name, &[])?;
122    for (k, v) in fields {
123        e.start_element(k, &[("xsi:type", kind_xsi_name(v.kind()))])?;
124        match v {
125            FieldValue::Integer(i) => e.text(&format!("{i}")),
126            FieldValue::Float(x) => e.text(&format!("{x}")),
127            FieldValue::Bool(b) => e.text(if *b { "true" } else { "false" }),
128            FieldValue::String(s) => e.text(s),
129            FieldValue::Bytes(b) => e.text(&hex_encode(b)),
130        }
131        e.end_element()?;
132    }
133    e.end_element()?;
134    Ok(e.finish())
135}
136
137/// Decode XML zurueck in eine Sample-Map. Spec §6.4.
138///
139/// # Errors
140/// `Parse`/`InvalidValue`/`NoSampleRoot`/`InvalidHex`.
141pub fn decode_xml(xml: &str) -> Result<(String, BTreeMap<String, FieldValue>), CodecError> {
142    let parser = XmlParser::new(xml);
143    let mut events: Vec<Event> = Vec::new();
144    for ev in parser {
145        events.push(ev?);
146    }
147    let mut iter = events.into_iter().peekable();
148    // Skip declaration.
149    while let Some(Event::Declaration(_)) = iter.peek() {
150        iter.next();
151    }
152    let type_name = match iter.next() {
153        Some(Event::StartElement { name, .. }) => name,
154        _ => return Err(CodecError::NoSampleRoot),
155    };
156
157    let mut fields = BTreeMap::new();
158    while let Some(ev) = iter.next() {
159        match ev {
160            Event::StartElement { name, attrs } => {
161                let kind_attr = attrs
162                    .iter()
163                    .find(|(k, _)| k == "xsi:type")
164                    .map(|(_, v)| v.as_str())
165                    .unwrap_or("xs:string");
166                let text = match iter.next() {
167                    Some(Event::Text(t)) => t,
168                    Some(Event::EndElement(_)) => {
169                        // Empty body
170                        fields.insert(name.clone(), FieldValue::String(String::new()));
171                        continue;
172                    }
173                    _ => String::new(),
174                };
175                // Consume EndElement for this field
176                while let Some(next) = iter.peek() {
177                    if matches!(next, Event::EndElement(n) if n == &name) {
178                        iter.next();
179                        break;
180                    }
181                    iter.next();
182                }
183                let value = parse_value(&name, kind_attr, &text)?;
184                fields.insert(name, value);
185            }
186            Event::EndElement(n) if n == type_name => break,
187            _ => {}
188        }
189    }
190    Ok((type_name, fields))
191}
192
193fn kind_xsi_name(k: FieldKind) -> &'static str {
194    match k {
195        FieldKind::Integer => "xs:long",
196        FieldKind::Float => "xs:double",
197        FieldKind::Bool => "xs:boolean",
198        FieldKind::String => "xs:string",
199        FieldKind::Bytes => "xs:hexBinary",
200    }
201}
202
203fn parse_value(field: &str, xsi_type: &str, text: &str) -> Result<FieldValue, CodecError> {
204    match xsi_type {
205        "xs:long" | "xs:int" | "xs:short" | "xs:byte" => text
206            .parse::<i64>()
207            .map(FieldValue::Integer)
208            .map_err(|_| CodecError::InvalidValue {
209                field: field.into(),
210                kind: FieldKind::Integer,
211                got: text.into(),
212            }),
213        "xs:double" | "xs:float" => {
214            text.parse::<f64>()
215                .map(FieldValue::Float)
216                .map_err(|_| CodecError::InvalidValue {
217                    field: field.into(),
218                    kind: FieldKind::Float,
219                    got: text.into(),
220                })
221        }
222        "xs:boolean" => match text {
223            "true" | "1" => Ok(FieldValue::Bool(true)),
224            "false" | "0" => Ok(FieldValue::Bool(false)),
225            _ => Err(CodecError::InvalidValue {
226                field: field.into(),
227                kind: FieldKind::Bool,
228                got: text.into(),
229            }),
230        },
231        "xs:hexBinary" => hex_decode(text).map(FieldValue::Bytes),
232        _ => Ok(FieldValue::String(text.to_string())),
233    }
234}
235
236fn hex_encode(b: &[u8]) -> String {
237    let mut out = String::with_capacity(b.len() * 2);
238    for byte in b {
239        let hi = (byte >> 4) & 0x0f;
240        let lo = byte & 0x0f;
241        out.push(hex_digit(hi));
242        out.push(hex_digit(lo));
243    }
244    out
245}
246
247fn hex_digit(n: u8) -> char {
248    match n {
249        0..=9 => char::from(b'0' + n),
250        10..=15 => char::from(b'A' + n - 10),
251        _ => '?',
252    }
253}
254
255fn hex_decode(s: &str) -> Result<Vec<u8>, CodecError> {
256    if s.len() % 2 != 0 {
257        return Err(CodecError::InvalidHex);
258    }
259    let mut out = Vec::with_capacity(s.len() / 2);
260    let bytes = s.as_bytes();
261    let mut i = 0;
262    while i < bytes.len() {
263        let hi = hex_value(bytes[i])?;
264        let lo = hex_value(bytes[i + 1])?;
265        out.push((hi << 4) | lo);
266        i += 2;
267    }
268    Ok(out)
269}
270
271fn hex_value(c: u8) -> Result<u8, CodecError> {
272    match c {
273        b'0'..=b'9' => Ok(c - b'0'),
274        b'a'..=b'f' => Ok(c - b'a' + 10),
275        b'A'..=b'F' => Ok(c - b'A' + 10),
276        _ => Err(CodecError::InvalidHex),
277    }
278}
279
280#[cfg(test)]
281#[allow(
282    clippy::expect_used,
283    clippy::unwrap_used,
284    clippy::panic,
285    clippy::float_cmp
286)]
287mod tests {
288    use super::*;
289
290    fn sample() -> BTreeMap<String, FieldValue> {
291        let mut m = BTreeMap::new();
292        m.insert("id".into(), FieldValue::Integer(42));
293        m.insert("price".into(), FieldValue::Float(99.5));
294        m.insert("active".into(), FieldValue::Bool(true));
295        m.insert("symbol".into(), FieldValue::String("AAPL".into()));
296        m.insert("blob".into(), FieldValue::Bytes(alloc::vec![0xCA, 0xFE]));
297        m
298    }
299
300    #[test]
301    fn round_trip_preserves_all_fields() {
302        let s = sample();
303        let xml = encode_to_xml("Trade", &s).unwrap();
304        let (type_name, decoded) = decode_xml(&xml).unwrap();
305        assert_eq!(type_name, "Trade");
306        assert_eq!(decoded.get("id"), Some(&FieldValue::Integer(42)));
307        assert_eq!(decoded.get("price"), Some(&FieldValue::Float(99.5)));
308        assert_eq!(decoded.get("active"), Some(&FieldValue::Bool(true)));
309        assert_eq!(
310            decoded.get("symbol"),
311            Some(&FieldValue::String("AAPL".into()))
312        );
313        assert_eq!(
314            decoded.get("blob"),
315            Some(&FieldValue::Bytes(alloc::vec![0xCA, 0xFE]))
316        );
317    }
318
319    #[test]
320    fn encode_uses_type_name_as_root() {
321        let xml = encode_to_xml("MyTopic", &sample()).unwrap();
322        assert!(xml.starts_with("<MyTopic"));
323        assert!(xml.ends_with("</MyTopic>"));
324    }
325
326    #[test]
327    fn invalid_integer_rejected() {
328        let xml = r#"<T><id xsi:type="xs:long">notanumber</id></T>"#;
329        let err = decode_xml(xml).unwrap_err();
330        assert!(matches!(err, CodecError::InvalidValue { .. }));
331    }
332
333    #[test]
334    fn invalid_bool_rejected() {
335        let xml = r#"<T><b xsi:type="xs:boolean">maybe</b></T>"#;
336        let err = decode_xml(xml).unwrap_err();
337        assert!(matches!(
338            err,
339            CodecError::InvalidValue {
340                kind: FieldKind::Bool,
341                ..
342            }
343        ));
344    }
345
346    #[test]
347    fn missing_xsi_type_defaults_to_string() {
348        let xml = "<T><name>Alice</name></T>";
349        let (_, decoded) = decode_xml(xml).unwrap();
350        assert_eq!(
351            decoded.get("name"),
352            Some(&FieldValue::String("Alice".into()))
353        );
354    }
355
356    #[test]
357    fn empty_body_yields_empty_string() {
358        let xml = "<T><name></name></T>";
359        let (_, decoded) = decode_xml(xml).unwrap();
360        assert_eq!(
361            decoded.get("name"),
362            Some(&FieldValue::String(String::new()))
363        );
364    }
365
366    #[test]
367    fn hex_round_trip() {
368        let bytes = alloc::vec![0xDE, 0xAD, 0xBE, 0xEF];
369        let s = hex_encode(&bytes);
370        assert_eq!(s, "DEADBEEF");
371        let back = hex_decode(&s).unwrap();
372        assert_eq!(back, bytes);
373    }
374
375    #[test]
376    fn hex_decode_rejects_odd_length() {
377        assert!(hex_decode("ABC").is_err());
378    }
379
380    #[test]
381    fn hex_decode_rejects_non_hex_char() {
382        assert!(hex_decode("ZZ").is_err());
383    }
384
385    #[test]
386    fn boolean_accepts_zero_and_one() {
387        let xml = r#"<T><b xsi:type="xs:boolean">1</b></T>"#;
388        let (_, decoded) = decode_xml(xml).unwrap();
389        assert_eq!(decoded.get("b"), Some(&FieldValue::Bool(true)));
390    }
391}