Skip to main content

zerodds_xml/
types.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! IDL-PSM data type mapping per DDS-XML 1.0 §7.2.
4//!
5//! Element value parsers for boolean, hex/decimal long, Duration_t,
6//! and the symbol constants `LENGTH_UNLIMITED`, `DURATION_INFINITE_SEC`
7//! and `DURATION_INFINITE_NSEC` from §7.2.2.
8//!
9//! All helpers are pure string -> typed-value conversions without
10//! allocation, where possible.
11
12use crate::errors::XmlError;
13use alloc::format;
14use alloc::string::ToString;
15
16/// Spec §7.2.2: `LENGTH_UNLIMITED` as a signed long, value `-1`.
17///
18/// Used in QoS policies (e.g. `<resource_limits><max_samples>`) as a
19/// "no limit" sentinel.
20pub const LENGTH_UNLIMITED: i32 = -1;
21
22/// Spec §7.2.2: `DURATION_INFINITE_SEC` = `0x7FFFFFFF` (max signed long).
23pub const DURATION_INFINITE_SEC: i32 = 0x7FFF_FFFF;
24
25/// Spec §7.2.2: `DURATION_INFINITE_NSEC` = `0x7FFFFFFF`.
26pub const DURATION_INFINITE_NSEC: u32 = 0x7FFF_FFFF;
27
28/// Spec §7.2.2: `DURATION_ZERO_SEC` = `0`.
29pub const DURATION_ZERO_SEC: i32 = 0;
30
31/// Spec §7.2.2: `DURATION_ZERO_NSEC` = `0`.
32pub const DURATION_ZERO_NSEC: u32 = 0;
33
34/// Spec §7.2.2.6: `TIME_INVALID_SEC = -1`.
35///
36/// Sentinel value for an "invalid" `Time_t.sec`. Used only for
37/// XML sample encoding (DDS timestamps in XML form).
38pub const TIME_INVALID_SEC: i32 = -1;
39
40/// Spec §7.2.2.7: `TIME_INVALID_NSEC = 0xFFFFFFFF`.
41pub const TIME_INVALID_NSEC: u32 = 0xFFFF_FFFF;
42
43/// `Duration_t` per DDS 1.4 §2.2.1.2 + DDS-XML 1.0 §7.2.6.
44///
45/// XML-Mapping:
46///
47/// ```xml
48/// <duration>
49///   <sec>5</sec>
50///   <nanosec>0</nanosec>
51/// </duration>
52/// ```
53///
54/// Sentinel values: `<sec>DURATION_INFINITE_SEC</sec>` and
55/// `<nanosec>DURATION_INFINITE_NSEC</nanosec>` are mapped via
56/// [`Self::INFINITE`]/[`Self::ZERO`] to the spec constants.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct Duration {
59    /// Seconds part (signed, since the spec `nonNegativeInteger_Duration_SEC`
60    /// allows the symbol `DURATION_INFINITE_SEC`, which maps to a
61    /// signed-long sentinel).
62    pub sec: i32,
63    /// Nanoseconds part (`0..=999_999_999`, or the sentinel
64    /// [`DURATION_INFINITE_NSEC`]).
65    pub nanosec: u32,
66}
67
68impl Duration {
69    /// Sentinel "infinity" — both fields set to the spec sentinel.
70    pub const INFINITE: Self = Self {
71        sec: DURATION_INFINITE_SEC,
72        nanosec: DURATION_INFINITE_NSEC,
73    };
74
75    /// Zero duration (spec default for many QoS policies).
76    pub const ZERO: Self = Self {
77        sec: DURATION_ZERO_SEC,
78        nanosec: DURATION_ZERO_NSEC,
79    };
80
81    /// `true` if both fields carry the infinite sentinel.
82    #[must_use]
83    pub fn is_infinite(&self) -> bool {
84        self.sec == DURATION_INFINITE_SEC && self.nanosec == DURATION_INFINITE_NSEC
85    }
86}
87
88/// Boolean parser per Spec §7.1.4 Tab.7.1.
89///
90/// Accepted values (all case-sensitive in the spec, we additionally
91/// accept the common uppercase spelling — Cyclone
92/// and FastDDS are tolerant here too):
93///
94/// * `true`, `TRUE`, `1` -> `true`
95/// * `false`, `FALSE`, `0` -> `false`
96///
97/// # Errors
98/// [`XmlError::ValueOutOfRange`] for any other string.
99pub fn parse_bool(s: &str) -> Result<bool, XmlError> {
100    let t = s.trim();
101    match t {
102        "1" | "true" | "TRUE" => Ok(true),
103        "0" | "false" | "FALSE" => Ok(false),
104        _ => Err(XmlError::ValueOutOfRange(format!(
105            "boolean expected (true/false/1/0), got `{t}`"
106        ))),
107    }
108}
109
110/// Long parser (signed 32-bit) per Spec §7.1.4 Tab.7.1.
111///
112/// Accepts:
113/// * Decimal: `-2147483648..=2147483647`
114/// * Hex (prefix `0x` / `0X`): `0..=0xFFFFFFFF` (bit pattern,
115///   reinterpreted as `i32`).
116/// * Symbol `LENGTH_UNLIMITED` -> `-1`.
117///
118/// # Errors
119/// [`XmlError::ValueOutOfRange`] on a value range violation or a
120/// parser error.
121pub fn parse_long(s: &str) -> Result<i32, XmlError> {
122    let t = s.trim();
123    if t == "LENGTH_UNLIMITED" {
124        return Ok(LENGTH_UNLIMITED);
125    }
126    if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
127        let v = u32::from_str_radix(hex, 16)
128            .map_err(|e| XmlError::ValueOutOfRange(format!("hex long `{t}`: {e}")))?;
129        // Bit-pattern reinterpret: §7.1.4 Tab.7.1 allows
130        // 0x80000000..0xFFFFFFFF as a negative i32 (signed).
131        return Ok(i32::from_ne_bytes(v.to_ne_bytes()));
132    }
133    t.parse::<i32>()
134        .map_err(|e| XmlError::ValueOutOfRange(format!("long `{t}`: {e}")))
135}
136
137/// Unsigned long parser (32-bit) per Spec §7.1.4 Tab.7.1.
138///
139/// Accepts decimal `0..=4294967295` and hex `0x0..=0xFFFFFFFF`.
140///
141/// # Errors
142/// [`XmlError::ValueOutOfRange`] on a value range violation.
143pub fn parse_ulong(s: &str) -> Result<u32, XmlError> {
144    let t = s.trim();
145    if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
146        return u32::from_str_radix(hex, 16)
147            .map_err(|e| XmlError::ValueOutOfRange(format!("hex ulong `{t}`: {e}")));
148    }
149    t.parse::<u32>()
150        .map_err(|e| XmlError::ValueOutOfRange(format!("ulong `{t}`: {e}")))
151}
152
153/// Seconds value for `Duration_t.sec` per Spec §7.2.2 +
154/// the `nonNegativeInteger_Duration_SEC` pattern.
155///
156/// Accepts:
157/// * Decimal `0..=0x7FFFFFFF`.
158/// * Symbols `DURATION_INFINITY` and `DURATION_INFINITE_SEC` -> sentinel
159///   [`DURATION_INFINITE_SEC`].
160///
161/// # Errors
162/// [`XmlError::ValueOutOfRange`] on negative values or overflow.
163pub fn parse_duration_sec(s: &str) -> Result<i32, XmlError> {
164    let t = s.trim();
165    if t == "DURATION_INFINITY" || t == "DURATION_INFINITE_SEC" {
166        return Ok(DURATION_INFINITE_SEC);
167    }
168    let v = t
169        .parse::<i64>()
170        .map_err(|e| XmlError::ValueOutOfRange(format!("duration_sec `{t}`: {e}")))?;
171    if !(0..=i64::from(DURATION_INFINITE_SEC)).contains(&v) {
172        return Err(XmlError::ValueOutOfRange(format!(
173            "duration_sec `{t}` outside 0..=0x7FFFFFFF"
174        )));
175    }
176    // Safe: due to the range check above.
177    Ok(i32::try_from(v).unwrap_or(0))
178}
179
180/// Nanoseconds value for `Duration_t.nanosec` per Spec §7.2.2 +
181/// the `nonNegativeInteger_Duration_NSEC` pattern.
182///
183/// Accepts:
184/// * Decimal `0..=999_999_999` (regular sub-second range).
185/// * Symbols `DURATION_INFINITY` and `DURATION_INFINITE_NSEC` -> sentinel
186///   [`DURATION_INFINITE_NSEC`].
187///
188/// # Errors
189/// [`XmlError::ValueOutOfRange`] on a value > 999_999_999 (unless it is a
190/// sentinel).
191pub fn parse_duration_nsec(s: &str) -> Result<u32, XmlError> {
192    let t = s.trim();
193    if t == "DURATION_INFINITY" || t == "DURATION_INFINITE_NSEC" {
194        return Ok(DURATION_INFINITE_NSEC);
195    }
196    let v = t
197        .parse::<u32>()
198        .map_err(|e| XmlError::ValueOutOfRange(format!("duration_nsec `{t}`: {e}")))?;
199    // Spec range for regular values: 0..=999_999_999.
200    // Values above that are only allowed via the sentinel.
201    if v > 999_999_999 {
202        return Err(XmlError::ValueOutOfRange(format!(
203            "duration_nsec `{t}` exceeds 999_999_999"
204        )));
205    }
206    Ok(v)
207}
208
209/// `positiveInteger_UNLIMITED` parser per Spec §7.2.2.9.
210///
211/// Pattern from the OMG spec: `(LENGTH_UNLIMITED|[1-9]([0-9])*)?`.
212/// Unlike [`parse_long`] / [`parse_ulong`], `0` is
213/// **not** an allowed value — the spec requires `[1-9]` as the first
214/// digit char.
215///
216/// Accepts:
217/// * `LENGTH_UNLIMITED` -> `-1` (sentinel; spec-conformant "unlimited").
218/// * Decimal `1..=2147483647` (positive i32 values without a leading `0`).
219///
220/// Rejected:
221/// * `0` (the spec forbids it via the `[1-9]` prefix).
222/// * Negative values (except the sentinel).
223/// * Hex values (the spec pattern allows only decimal).
224/// * Leading zeros (e.g. `01`, `001`).
225///
226/// # Errors
227/// [`XmlError::ValueOutOfRange`] on a violation of the constraints
228/// named above.
229pub fn parse_positive_long_unlimited(s: &str) -> Result<i32, XmlError> {
230    let t = s.trim();
231    if t == "LENGTH_UNLIMITED" {
232        return Ok(LENGTH_UNLIMITED);
233    }
234    // Spec pattern `[1-9]([0-9])*` — first digit 1..=9, no hex,
235    // no leading zeros.
236    let mut chars = t.chars();
237    let first = chars.next().ok_or_else(|| {
238        XmlError::ValueOutOfRange("positiveInteger_UNLIMITED: empty input".to_string())
239    })?;
240    if !('1'..='9').contains(&first) {
241        return Err(XmlError::ValueOutOfRange(format!(
242            "positiveInteger_UNLIMITED `{t}`: first digit must be 1..9 (spec pattern)"
243        )));
244    }
245    if !chars.all(|c| c.is_ascii_digit()) {
246        return Err(XmlError::ValueOutOfRange(format!(
247            "positiveInteger_UNLIMITED `{t}`: only ASCII decimal digits allowed"
248        )));
249    }
250    t.parse::<i32>()
251        .map_err(|e| XmlError::ValueOutOfRange(format!("positiveInteger_UNLIMITED `{t}`: {e}")))
252}
253
254/// Octet sequence parser per Spec §7.2.4.2 (comma-separated
255/// decimal/hex). Each element is an octet (`u8`).
256///
257/// Accepts:
258/// * Comma-separated decimal: `0,1,2,255`.
259/// * Comma-separated hex (prefix `0x` / `0X`): `0x00,0xFF`.
260/// * Mixed allowed: `1,0x02,3` (each element is parsed individually).
261/// * Whitespace around commas is trimmed.
262/// * Empty string -> empty sequence.
263///
264/// Rejected:
265/// * Values outside `0..=255`.
266/// * Trailing comma (e.g. `1,2,`).
267/// * Non-numeric tokens.
268///
269/// For Base64-encoded octet sequences see `qos_parser::base64_decode`
270/// — the spec allows **either** a comma list **or** Base64,
271/// distinguished by the element name (`<value>` vs. `<valueB64>`).
272///
273/// # Errors
274/// [`XmlError::ValueOutOfRange`] on range/format errors.
275pub fn parse_octet_sequence(s: &str) -> Result<alloc::vec::Vec<u8>, XmlError> {
276    let trimmed = s.trim();
277    if trimmed.is_empty() {
278        return Ok(alloc::vec::Vec::new());
279    }
280    // DoS cap: 1 MiB raw ≈ 256k values.
281    if trimmed.len() > MAX_STRING_BYTES * 16 {
282        return Err(XmlError::LimitExceeded(format!(
283            "octet sequence ({} bytes) exceeds cap",
284            trimmed.len()
285        )));
286    }
287    let mut out = alloc::vec::Vec::new();
288    for token in trimmed.split(',') {
289        let tok = token.trim();
290        if tok.is_empty() {
291            return Err(XmlError::ValueOutOfRange(
292                "octet sequence: empty element (e.g. `1,,2` or trailing comma)".to_string(),
293            ));
294        }
295        let v = if let Some(hex) = tok.strip_prefix("0x").or_else(|| tok.strip_prefix("0X")) {
296            u16::from_str_radix(hex, 16)
297                .map_err(|e| XmlError::ValueOutOfRange(format!("octet hex `{tok}`: {e}")))?
298        } else {
299            tok.parse::<u16>()
300                .map_err(|e| XmlError::ValueOutOfRange(format!("octet decimal `{tok}`: {e}")))?
301        };
302        let byte = u8::try_from(v)
303            .map_err(|_| XmlError::ValueOutOfRange(format!("octet `{tok}` outside 0..=255")))?;
304        out.push(byte);
305    }
306    Ok(out)
307}
308
309/// Enum whitelist check per Spec §7.1.4 Tab.7.1 (enum values are
310/// string literals from DCPS-IDL, *not* numeric).
311///
312/// # Errors
313/// [`XmlError::BadEnum`] if the value is not contained in the whitelist.
314pub fn parse_enum<'a>(s: &str, whitelist: &[&'a str]) -> Result<&'a str, XmlError> {
315    let t = s.trim();
316    whitelist
317        .iter()
318        .find(|allowed| **allowed == t)
319        .copied()
320        .ok_or_else(|| XmlError::BadEnum(t.to_string()))
321}
322
323/// String-DoS-Cap: 64 KiB.
324pub const MAX_STRING_BYTES: usize = 64 * 1024;
325
326/// String parser with a DoS cap per the ZeroDDS security posture.
327///
328/// XML escaping (`&lt;`, `&amp;` etc.) is already decoded by roxmltree.
329///
330/// # Errors
331/// [`XmlError::LimitExceeded`] on strings over [`MAX_STRING_BYTES`].
332pub fn parse_string(s: &str) -> Result<&str, XmlError> {
333    if s.len() > MAX_STRING_BYTES {
334        return Err(XmlError::LimitExceeded(format!(
335            "string ({} bytes) exceeds {MAX_STRING_BYTES}",
336            s.len()
337        )));
338    }
339    Ok(s)
340}
341
342#[cfg(test)]
343#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
344mod tests {
345    use super::*;
346
347    // ---- Boolean ------------------------------------------------------
348
349    #[test]
350    fn bool_true_variants() {
351        assert!(parse_bool("true").expect("true"));
352        assert!(parse_bool("TRUE").expect("TRUE"));
353        assert!(parse_bool("1").expect("1"));
354        assert!(parse_bool("  true  ").expect("trim"));
355    }
356
357    #[test]
358    fn bool_false_variants() {
359        assert!(!parse_bool("false").expect("false"));
360        assert!(!parse_bool("FALSE").expect("FALSE"));
361        assert!(!parse_bool("0").expect("0"));
362    }
363
364    #[test]
365    fn bool_invalid() {
366        let err = parse_bool("yes").expect_err("invalid");
367        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
368    }
369
370    // ---- Long --------------------------------------------------------
371
372    #[test]
373    fn long_decimal() {
374        assert_eq!(parse_long("42").expect("dec"), 42);
375        assert_eq!(parse_long("-1").expect("neg"), -1);
376        assert_eq!(parse_long("2147483647").expect("max"), i32::MAX);
377        assert_eq!(parse_long("-2147483648").expect("min"), i32::MIN);
378    }
379
380    #[test]
381    fn long_hex() {
382        assert_eq!(parse_long("0xFF").expect("hex"), 0xFF);
383        assert_eq!(parse_long("0X80").expect("upper-X"), 0x80);
384        // 0x80000000 -> reinterpreted as i32 = i32::MIN
385        assert_eq!(parse_long("0x80000000").expect("hex-msb"), i32::MIN);
386        assert_eq!(parse_long("0x7FFFFFFF").expect("hex-max"), i32::MAX);
387    }
388
389    #[test]
390    fn long_length_unlimited_symbol() {
391        assert_eq!(parse_long("LENGTH_UNLIMITED").expect("symbol"), -1);
392    }
393
394    #[test]
395    fn long_invalid() {
396        assert!(parse_long("not-a-number").is_err());
397        assert!(parse_long("0xZZ").is_err());
398    }
399
400    // ---- ULong -------------------------------------------------------
401
402    #[test]
403    fn ulong_decimal_and_hex() {
404        assert_eq!(parse_ulong("0").expect("0"), 0);
405        assert_eq!(parse_ulong("4294967295").expect("max"), u32::MAX);
406        assert_eq!(parse_ulong("0xFFFFFFFF").expect("hex-max"), u32::MAX);
407    }
408
409    #[test]
410    fn ulong_invalid() {
411        assert!(parse_ulong("-1").is_err());
412        assert!(parse_ulong("4294967296").is_err());
413    }
414
415    // ---- Duration ----------------------------------------------------
416
417    #[test]
418    fn duration_sec_normal() {
419        assert_eq!(parse_duration_sec("0").expect("zero"), 0);
420        assert_eq!(parse_duration_sec("123").expect("normal"), 123);
421    }
422
423    #[test]
424    fn duration_sec_infinite_symbols() {
425        assert_eq!(
426            parse_duration_sec("DURATION_INFINITY").expect("infinity"),
427            DURATION_INFINITE_SEC
428        );
429        assert_eq!(
430            parse_duration_sec("DURATION_INFINITE_SEC").expect("infinite_sec"),
431            DURATION_INFINITE_SEC
432        );
433    }
434
435    #[test]
436    fn duration_sec_overflow() {
437        // 0x80000000 ueberschreitet 0x7FFFFFFF.
438        let err = parse_duration_sec("2147483648").expect_err("overflow");
439        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
440    }
441
442    #[test]
443    fn duration_nsec_normal() {
444        assert_eq!(
445            parse_duration_nsec("999999999").expect("max-nsec"),
446            999_999_999
447        );
448        assert_eq!(parse_duration_nsec("0").expect("zero"), 0);
449    }
450
451    #[test]
452    fn duration_nsec_infinite() {
453        assert_eq!(
454            parse_duration_nsec("DURATION_INFINITE_NSEC").expect("infinite"),
455            DURATION_INFINITE_NSEC
456        );
457        assert_eq!(
458            parse_duration_nsec("DURATION_INFINITY").expect("infinity"),
459            DURATION_INFINITE_NSEC
460        );
461    }
462
463    #[test]
464    fn duration_nsec_out_of_range() {
465        let err = parse_duration_nsec("1000000000").expect_err("oor");
466        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
467    }
468
469    #[test]
470    fn duration_constants() {
471        assert!(Duration::INFINITE.is_infinite());
472        assert!(!Duration::ZERO.is_infinite());
473        assert_eq!(Duration::ZERO.sec, 0);
474        assert_eq!(Duration::ZERO.nanosec, 0);
475    }
476
477    // ---- Enum --------------------------------------------------------
478
479    #[test]
480    fn enum_match() {
481        let wl = ["KEEP_LAST_HISTORY_QOS", "KEEP_ALL_HISTORY_QOS"];
482        assert_eq!(
483            parse_enum("KEEP_LAST_HISTORY_QOS", &wl).expect("match"),
484            "KEEP_LAST_HISTORY_QOS"
485        );
486    }
487
488    #[test]
489    fn enum_no_match() {
490        let wl = ["A", "B"];
491        let err = parse_enum("C", &wl).expect_err("no-match");
492        assert!(matches!(err, XmlError::BadEnum(_)));
493    }
494
495    // ---- String ------------------------------------------------------
496
497    #[test]
498    fn string_short_passes() {
499        assert_eq!(parse_string("hello").expect("ok"), "hello");
500    }
501
502    #[test]
503    fn string_too_long_rejected() {
504        let big = "x".repeat(MAX_STRING_BYTES + 1);
505        let err = parse_string(&big).expect_err("too-big");
506        assert!(matches!(err, XmlError::LimitExceeded(_)));
507    }
508
509    // ---- Spec-Konstanten ---------------------------------------------
510
511    #[test]
512    fn spec_constants() {
513        assert_eq!(LENGTH_UNLIMITED, -1);
514        assert_eq!(DURATION_INFINITE_SEC, 0x7FFF_FFFF);
515        assert_eq!(DURATION_INFINITE_NSEC, 0x7FFF_FFFF);
516    }
517
518    #[test]
519    fn time_invalid_constants() {
520        // Spec §7.2.2.6 + §7.2.2.7.
521        assert_eq!(TIME_INVALID_SEC, -1);
522        assert_eq!(TIME_INVALID_NSEC, 0xFFFF_FFFF);
523        // TIME_INVALID must be distinguishable from DURATION_INFINITE and
524        // DURATION_ZERO (different sentinel values).
525        assert_ne!(TIME_INVALID_SEC, DURATION_INFINITE_SEC);
526        assert_ne!(TIME_INVALID_NSEC, DURATION_INFINITE_NSEC);
527        assert_ne!(TIME_INVALID_SEC, DURATION_ZERO_SEC);
528    }
529
530    // ---- §7.2.2.9 positiveInteger_UNLIMITED --------------------------
531
532    #[test]
533    fn positive_unlimited_symbol_passes() {
534        assert_eq!(
535            parse_positive_long_unlimited("LENGTH_UNLIMITED").expect("symbol"),
536            LENGTH_UNLIMITED
537        );
538    }
539
540    #[test]
541    fn positive_unlimited_one_to_max_passes() {
542        assert_eq!(parse_positive_long_unlimited("1").expect("1"), 1);
543        assert_eq!(parse_positive_long_unlimited("42").expect("42"), 42);
544        assert_eq!(
545            parse_positive_long_unlimited("2147483647").expect("max"),
546            i32::MAX
547        );
548    }
549
550    #[test]
551    fn positive_unlimited_zero_rejected() {
552        // Spec pattern `[1-9]([0-9])*` forbids 0 explicitly.
553        let err = parse_positive_long_unlimited("0").expect_err("zero");
554        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
555    }
556
557    #[test]
558    fn positive_unlimited_negative_rejected() {
559        let err = parse_positive_long_unlimited("-1").expect_err("negative");
560        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
561    }
562
563    #[test]
564    fn positive_unlimited_leading_zero_rejected() {
565        // `01` does not match the spec pattern.
566        let err = parse_positive_long_unlimited("01").expect_err("leading-zero");
567        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
568    }
569
570    #[test]
571    fn positive_unlimited_hex_rejected() {
572        // Pattern allows only decimal.
573        let err = parse_positive_long_unlimited("0x10").expect_err("hex");
574        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
575    }
576
577    #[test]
578    fn positive_unlimited_overflow_rejected() {
579        let err = parse_positive_long_unlimited("2147483648").expect_err("overflow");
580        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
581    }
582
583    #[test]
584    fn positive_unlimited_empty_rejected() {
585        let err = parse_positive_long_unlimited("").expect_err("empty");
586        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
587    }
588
589    // ---- §7.2.4.2 octet sequences ------------------------------------
590
591    #[test]
592    fn octet_sequence_decimal_basic() {
593        let bytes = parse_octet_sequence("0,1,2,255").expect("ok");
594        assert_eq!(bytes, alloc::vec![0, 1, 2, 255]);
595    }
596
597    #[test]
598    fn octet_sequence_hex_basic() {
599        let bytes = parse_octet_sequence("0x00,0xFF,0x42").expect("ok");
600        assert_eq!(bytes, alloc::vec![0x00, 0xFF, 0x42]);
601    }
602
603    #[test]
604    fn octet_sequence_mixed_decimal_and_hex() {
605        // Spec: "decimal or hexadecimal" — pro Element entscheidbar.
606        let bytes = parse_octet_sequence("1,0x02,3,0x04").expect("mixed");
607        assert_eq!(bytes, alloc::vec![1, 2, 3, 4]);
608    }
609
610    #[test]
611    fn octet_sequence_whitespace_around_commas() {
612        let bytes = parse_octet_sequence(" 1 , 2 , 3 ").expect("trim");
613        assert_eq!(bytes, alloc::vec![1, 2, 3]);
614    }
615
616    #[test]
617    fn octet_sequence_empty_string_returns_empty_vec() {
618        let bytes = parse_octet_sequence("").expect("empty");
619        assert!(bytes.is_empty());
620    }
621
622    #[test]
623    fn octet_sequence_value_above_255_rejected() {
624        let err = parse_octet_sequence("0,256").expect_err("over");
625        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
626    }
627
628    #[test]
629    fn octet_sequence_negative_rejected() {
630        let err = parse_octet_sequence("0,-1").expect_err("negative");
631        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
632    }
633
634    #[test]
635    fn octet_sequence_trailing_comma_rejected() {
636        let err = parse_octet_sequence("1,2,").expect_err("trailing");
637        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
638    }
639
640    #[test]
641    fn octet_sequence_double_comma_rejected() {
642        let err = parse_octet_sequence("1,,2").expect_err("double");
643        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
644    }
645
646    #[test]
647    fn octet_sequence_non_numeric_token_rejected() {
648        let err = parse_octet_sequence("1,abc,3").expect_err("non-numeric");
649        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
650    }
651
652    #[test]
653    fn octet_sequence_hex_above_255_rejected() {
654        let err = parse_octet_sequence("0x100").expect_err("hex over");
655        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
656    }
657}