Skip to main content

zerodds_xml/
types.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! IDL-PSM-Datentyp-Mapping nach DDS-XML 1.0 §7.2.
4//!
5//! Element-Werte-Parser fuer Boolean, Hex-/Dezimal-Long, Duration_t,
6//! sowie die Symbol-Konstanten `LENGTH_UNLIMITED`, `DURATION_INFINITE_SEC`
7//! und `DURATION_INFINITE_NSEC` aus §7.2.2.
8//!
9//! Alle Helper sind reine String -> typed-Value-Konvertierungen ohne
10//! Allokation, soweit moeglich.
11
12use crate::errors::XmlError;
13use alloc::format;
14use alloc::string::ToString;
15
16/// Spec §7.2.2: `LENGTH_UNLIMITED` als signed long, Wert `-1`.
17///
18/// Wird in QoS-Policies (z.B. `<resource_limits><max_samples>`) als
19/// "no limit"-Sentinel benutzt.
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-Wert fuer ein "ungueltiges" `Time_t.sec`. Wird nur fuer
37/// XML-Sample-Encoding genutzt (DDS Time-Stamps 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` gemaess 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-Werte: `<sec>DURATION_INFINITE_SEC</sec>` und
55/// `<nanosec>DURATION_INFINITE_NSEC</nanosec>` werden via
56/// [`Self::INFINITE`]/[`Self::ZERO`] mit den Spec-Konstanten gemappt.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct Duration {
59    /// Sekunden-Anteil (signed, da Spec `nonNegativeInteger_Duration_SEC`
60    /// das Symbol `DURATION_INFINITE_SEC` zulaesst, das auf einen
61    /// signed-long-Sentinel mappt).
62    pub sec: i32,
63    /// Nanosekunden-Anteil (`0..=999_999_999`, oder Sentinel
64    /// [`DURATION_INFINITE_NSEC`]).
65    pub nanosec: u32,
66}
67
68impl Duration {
69    /// Sentinel "infinity" — beide Felder auf den Spec-Sentinel.
70    pub const INFINITE: Self = Self {
71        sec: DURATION_INFINITE_SEC,
72        nanosec: DURATION_INFINITE_NSEC,
73    };
74
75    /// Null-Duration (Spec-Default fuer viele QoS-Policies).
76    pub const ZERO: Self = Self {
77        sec: DURATION_ZERO_SEC,
78        nanosec: DURATION_ZERO_NSEC,
79    };
80
81    /// `true` wenn beide Felder den Infinite-Sentinel tragen.
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 gemaess Spec §7.1.4 Tab.7.1.
89///
90/// Akzeptierte Werte (alle case-sensitive in der Spec, wir akzeptieren
91/// zusaetzlich die haeufige Schreibweise mit Grossbuchstaben — Cyclone
92/// und FastDDS sind hier ebenfalls tolerant):
93///
94/// * `true`, `TRUE`, `1` -> `true`
95/// * `false`, `FALSE`, `0` -> `false`
96///
97/// # Errors
98/// [`XmlError::ValueOutOfRange`] bei jedem anderen 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) gemaess Spec §7.1.4 Tab.7.1.
111///
112/// Akzeptiert:
113/// * Dezimal: `-2147483648..=2147483647`
114/// * Hex (Prefix `0x` / `0X`): `0..=0xFFFFFFFF` (Bit-Pattern, wird in
115///   `i32` reinterpretiert).
116/// * Symbol `LENGTH_UNLIMITED` -> `-1`.
117///
118/// # Errors
119/// [`XmlError::ValueOutOfRange`] bei Wertbereich-Verletzung oder
120/// Parser-Fehler.
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 erlaubt
130        // 0x80000000..0xFFFFFFFF als 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) gemaess Spec §7.1.4 Tab.7.1.
138///
139/// Akzeptiert Dezimal `0..=4294967295` und Hex `0x0..=0xFFFFFFFF`.
140///
141/// # Errors
142/// [`XmlError::ValueOutOfRange`] bei Wertbereich-Verletzung.
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/// Sekunden-Wert fuer `Duration_t.sec` gemaess Spec §7.2.2 +
154/// `nonNegativeInteger_Duration_SEC` Pattern.
155///
156/// Akzeptiert:
157/// * Dezimal `0..=0x7FFFFFFF`.
158/// * Symbol `DURATION_INFINITY` und `DURATION_INFINITE_SEC` -> Sentinel
159///   [`DURATION_INFINITE_SEC`].
160///
161/// # Errors
162/// [`XmlError::ValueOutOfRange`] bei negativen Werten oder 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: durch Range-Check oben.
177    Ok(i32::try_from(v).unwrap_or(0))
178}
179
180/// Nanosekunden-Wert fuer `Duration_t.nanosec` gemaess Spec §7.2.2 +
181/// `nonNegativeInteger_Duration_NSEC` Pattern.
182///
183/// Akzeptiert:
184/// * Dezimal `0..=999_999_999` (regulaerer Sub-Sekunden-Bereich).
185/// * Symbol `DURATION_INFINITY` und `DURATION_INFINITE_NSEC` -> Sentinel
186///   [`DURATION_INFINITE_NSEC`].
187///
188/// # Errors
189/// [`XmlError::ValueOutOfRange`] bei Wert > 999_999_999 (sofern kein
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 fuer regulaere Werte: 0..=999_999_999.
200    // Werte daueber sind nur via Sentinel zulaessig.
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 gemaess Spec §7.2.2.9.
210///
211/// Pattern aus der OMG-Spec: `(LENGTH_UNLIMITED|[1-9]([0-9])*)?`.
212/// Im Unterschied zu [`parse_long`] / [`parse_ulong`] ist `0`
213/// **kein** zulaessiger Wert — die Spec verlangt `[1-9]` als ersten
214/// Ziffer-Char.
215///
216/// Akzeptiert:
217/// * `LENGTH_UNLIMITED` -> `-1` (Sentinel; spec-konform "unlimited").
218/// * Dezimal `1..=2147483647` (positive i32-Werte ohne fuehrende `0`).
219///
220/// Rejected:
221/// * `0` (Spec verbietet via `[1-9]`-Praefix).
222/// * Negative Werte (ausser dem Sentinel).
223/// * Hex-Werte (Spec-Pattern erlaubt nur Dezimal).
224/// * Fuehrende Nullen (z.B. `01`, `001`).
225///
226/// # Errors
227/// [`XmlError::ValueOutOfRange`] bei Verletzung der oben genannten
228/// Constraints.
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])*` — erste Ziffer 1..=9, keine Hex,
235    // keine fuehrenden Nullen.
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}`: erste Ziffer muss 1..9 sein (Spec-Pattern)"
243        )));
244    }
245    if !chars.all(|c| c.is_ascii_digit()) {
246        return Err(XmlError::ValueOutOfRange(format!(
247            "positiveInteger_UNLIMITED `{t}`: nur ASCII-Dezimalziffern erlaubt"
248        )));
249    }
250    t.parse::<i32>()
251        .map_err(|e| XmlError::ValueOutOfRange(format!("positiveInteger_UNLIMITED `{t}`: {e}")))
252}
253
254/// Octet-Sequenz-Parser gemaess Spec §7.2.4.2 (Comma-separated
255/// decimal/hex). Jedes Element ist ein Octet (`u8`).
256///
257/// Akzeptiert:
258/// * Comma-separated Dezimal: `0,1,2,255`.
259/// * Comma-separated Hex (Prefix `0x` / `0X`): `0x00,0xFF`.
260/// * Gemischt erlaubt: `1,0x02,3` (jedes Element wird einzeln geparst).
261/// * Whitespace um Kommas wird getrimmt.
262/// * Leerer String -> leere Sequenz.
263///
264/// Rejected:
265/// * Werte ausserhalb `0..=255`.
266/// * Trailing-Komma (z.B. `1,2,`).
267/// * Nicht-numerische Tokens.
268///
269/// Fuer Base64-encoded Octet-Sequenzen siehe `qos_parser::base64_decode`
270/// — die Spec erlaubt **entweder** Comma-Liste **oder** Base64,
271/// unterschieden durch Element-Namen (`<value>` vs. `<valueB64>`).
272///
273/// # Errors
274/// [`XmlError::ValueOutOfRange`] bei Range-/Format-Fehlern.
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 roh ≈ 256k Werte.
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: leeres Element (z.B. `1,,2` oder 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-Pruefung gemaess Spec §7.1.4 Tab.7.1 (enum-Werte sind
310/// String-Literale aus DCPS-IDL, *nicht* numerisch).
311///
312/// # Errors
313/// [`XmlError::BadEnum`] wenn der Wert nicht in der Whitelist enthalten
314/// ist.
315pub fn parse_enum<'a>(s: &str, whitelist: &[&'a str]) -> Result<&'a str, XmlError> {
316    let t = s.trim();
317    whitelist
318        .iter()
319        .find(|allowed| **allowed == t)
320        .copied()
321        .ok_or_else(|| XmlError::BadEnum(t.to_string()))
322}
323
324/// String-DoS-Cap: 64 KiB.
325pub const MAX_STRING_BYTES: usize = 64 * 1024;
326
327/// String-Parser mit DoS-Cap gemaess ZeroDDS-Security-Posture.
328///
329/// XML-Escaping (`&lt;`, `&amp;` etc.) ist bereits durch roxmltree
330/// dekodiert.
331///
332/// # Errors
333/// [`XmlError::LimitExceeded`] bei Strings ueber [`MAX_STRING_BYTES`].
334pub fn parse_string(s: &str) -> Result<&str, XmlError> {
335    if s.len() > MAX_STRING_BYTES {
336        return Err(XmlError::LimitExceeded(format!(
337            "string ({} bytes) exceeds {MAX_STRING_BYTES}",
338            s.len()
339        )));
340    }
341    Ok(s)
342}
343
344#[cfg(test)]
345#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
346mod tests {
347    use super::*;
348
349    // ---- Boolean ------------------------------------------------------
350
351    #[test]
352    fn bool_true_variants() {
353        assert!(parse_bool("true").expect("true"));
354        assert!(parse_bool("TRUE").expect("TRUE"));
355        assert!(parse_bool("1").expect("1"));
356        assert!(parse_bool("  true  ").expect("trim"));
357    }
358
359    #[test]
360    fn bool_false_variants() {
361        assert!(!parse_bool("false").expect("false"));
362        assert!(!parse_bool("FALSE").expect("FALSE"));
363        assert!(!parse_bool("0").expect("0"));
364    }
365
366    #[test]
367    fn bool_invalid() {
368        let err = parse_bool("yes").expect_err("invalid");
369        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
370    }
371
372    // ---- Long --------------------------------------------------------
373
374    #[test]
375    fn long_decimal() {
376        assert_eq!(parse_long("42").expect("dec"), 42);
377        assert_eq!(parse_long("-1").expect("neg"), -1);
378        assert_eq!(parse_long("2147483647").expect("max"), i32::MAX);
379        assert_eq!(parse_long("-2147483648").expect("min"), i32::MIN);
380    }
381
382    #[test]
383    fn long_hex() {
384        assert_eq!(parse_long("0xFF").expect("hex"), 0xFF);
385        assert_eq!(parse_long("0X80").expect("upper-X"), 0x80);
386        // 0x80000000 -> reinterpreted as i32 = i32::MIN
387        assert_eq!(parse_long("0x80000000").expect("hex-msb"), i32::MIN);
388        assert_eq!(parse_long("0x7FFFFFFF").expect("hex-max"), i32::MAX);
389    }
390
391    #[test]
392    fn long_length_unlimited_symbol() {
393        assert_eq!(parse_long("LENGTH_UNLIMITED").expect("symbol"), -1);
394    }
395
396    #[test]
397    fn long_invalid() {
398        assert!(parse_long("not-a-number").is_err());
399        assert!(parse_long("0xZZ").is_err());
400    }
401
402    // ---- ULong -------------------------------------------------------
403
404    #[test]
405    fn ulong_decimal_and_hex() {
406        assert_eq!(parse_ulong("0").expect("0"), 0);
407        assert_eq!(parse_ulong("4294967295").expect("max"), u32::MAX);
408        assert_eq!(parse_ulong("0xFFFFFFFF").expect("hex-max"), u32::MAX);
409    }
410
411    #[test]
412    fn ulong_invalid() {
413        assert!(parse_ulong("-1").is_err());
414        assert!(parse_ulong("4294967296").is_err());
415    }
416
417    // ---- Duration ----------------------------------------------------
418
419    #[test]
420    fn duration_sec_normal() {
421        assert_eq!(parse_duration_sec("0").expect("zero"), 0);
422        assert_eq!(parse_duration_sec("123").expect("normal"), 123);
423    }
424
425    #[test]
426    fn duration_sec_infinite_symbols() {
427        assert_eq!(
428            parse_duration_sec("DURATION_INFINITY").expect("infinity"),
429            DURATION_INFINITE_SEC
430        );
431        assert_eq!(
432            parse_duration_sec("DURATION_INFINITE_SEC").expect("infinite_sec"),
433            DURATION_INFINITE_SEC
434        );
435    }
436
437    #[test]
438    fn duration_sec_overflow() {
439        // 0x80000000 ueberschreitet 0x7FFFFFFF.
440        let err = parse_duration_sec("2147483648").expect_err("overflow");
441        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
442    }
443
444    #[test]
445    fn duration_nsec_normal() {
446        assert_eq!(
447            parse_duration_nsec("999999999").expect("max-nsec"),
448            999_999_999
449        );
450        assert_eq!(parse_duration_nsec("0").expect("zero"), 0);
451    }
452
453    #[test]
454    fn duration_nsec_infinite() {
455        assert_eq!(
456            parse_duration_nsec("DURATION_INFINITE_NSEC").expect("infinite"),
457            DURATION_INFINITE_NSEC
458        );
459        assert_eq!(
460            parse_duration_nsec("DURATION_INFINITY").expect("infinity"),
461            DURATION_INFINITE_NSEC
462        );
463    }
464
465    #[test]
466    fn duration_nsec_out_of_range() {
467        let err = parse_duration_nsec("1000000000").expect_err("oor");
468        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
469    }
470
471    #[test]
472    fn duration_constants() {
473        assert!(Duration::INFINITE.is_infinite());
474        assert!(!Duration::ZERO.is_infinite());
475        assert_eq!(Duration::ZERO.sec, 0);
476        assert_eq!(Duration::ZERO.nanosec, 0);
477    }
478
479    // ---- Enum --------------------------------------------------------
480
481    #[test]
482    fn enum_match() {
483        let wl = ["KEEP_LAST_HISTORY_QOS", "KEEP_ALL_HISTORY_QOS"];
484        assert_eq!(
485            parse_enum("KEEP_LAST_HISTORY_QOS", &wl).expect("match"),
486            "KEEP_LAST_HISTORY_QOS"
487        );
488    }
489
490    #[test]
491    fn enum_no_match() {
492        let wl = ["A", "B"];
493        let err = parse_enum("C", &wl).expect_err("no-match");
494        assert!(matches!(err, XmlError::BadEnum(_)));
495    }
496
497    // ---- String ------------------------------------------------------
498
499    #[test]
500    fn string_short_passes() {
501        assert_eq!(parse_string("hello").expect("ok"), "hello");
502    }
503
504    #[test]
505    fn string_too_long_rejected() {
506        let big = "x".repeat(MAX_STRING_BYTES + 1);
507        let err = parse_string(&big).expect_err("too-big");
508        assert!(matches!(err, XmlError::LimitExceeded(_)));
509    }
510
511    // ---- Spec-Konstanten ---------------------------------------------
512
513    #[test]
514    fn spec_constants() {
515        assert_eq!(LENGTH_UNLIMITED, -1);
516        assert_eq!(DURATION_INFINITE_SEC, 0x7FFF_FFFF);
517        assert_eq!(DURATION_INFINITE_NSEC, 0x7FFF_FFFF);
518    }
519
520    #[test]
521    fn time_invalid_constants() {
522        // Spec §7.2.2.6 + §7.2.2.7.
523        assert_eq!(TIME_INVALID_SEC, -1);
524        assert_eq!(TIME_INVALID_NSEC, 0xFFFF_FFFF);
525        // TIME_INVALID muss von DURATION_INFINITE und DURATION_ZERO
526        // unterscheidbar sein (verschiedene Sentinel-Werte).
527        assert_ne!(TIME_INVALID_SEC, DURATION_INFINITE_SEC);
528        assert_ne!(TIME_INVALID_NSEC, DURATION_INFINITE_NSEC);
529        assert_ne!(TIME_INVALID_SEC, DURATION_ZERO_SEC);
530    }
531
532    // ---- §7.2.2.9 positiveInteger_UNLIMITED --------------------------
533
534    #[test]
535    fn positive_unlimited_symbol_passes() {
536        assert_eq!(
537            parse_positive_long_unlimited("LENGTH_UNLIMITED").expect("symbol"),
538            LENGTH_UNLIMITED
539        );
540    }
541
542    #[test]
543    fn positive_unlimited_one_to_max_passes() {
544        assert_eq!(parse_positive_long_unlimited("1").expect("1"), 1);
545        assert_eq!(parse_positive_long_unlimited("42").expect("42"), 42);
546        assert_eq!(
547            parse_positive_long_unlimited("2147483647").expect("max"),
548            i32::MAX
549        );
550    }
551
552    #[test]
553    fn positive_unlimited_zero_rejected() {
554        // Spec-Pattern `[1-9]([0-9])*` verbietet 0 explizit.
555        let err = parse_positive_long_unlimited("0").expect_err("zero");
556        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
557    }
558
559    #[test]
560    fn positive_unlimited_negative_rejected() {
561        let err = parse_positive_long_unlimited("-1").expect_err("negative");
562        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
563    }
564
565    #[test]
566    fn positive_unlimited_leading_zero_rejected() {
567        // `01` matcht nicht das Spec-Pattern.
568        let err = parse_positive_long_unlimited("01").expect_err("leading-zero");
569        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
570    }
571
572    #[test]
573    fn positive_unlimited_hex_rejected() {
574        // Pattern erlaubt nur Dezimal.
575        let err = parse_positive_long_unlimited("0x10").expect_err("hex");
576        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
577    }
578
579    #[test]
580    fn positive_unlimited_overflow_rejected() {
581        let err = parse_positive_long_unlimited("2147483648").expect_err("overflow");
582        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
583    }
584
585    #[test]
586    fn positive_unlimited_empty_rejected() {
587        let err = parse_positive_long_unlimited("").expect_err("empty");
588        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
589    }
590
591    // ---- §7.2.4.2 Octet-Sequenzen ------------------------------------
592
593    #[test]
594    fn octet_sequence_decimal_basic() {
595        let bytes = parse_octet_sequence("0,1,2,255").expect("ok");
596        assert_eq!(bytes, alloc::vec![0, 1, 2, 255]);
597    }
598
599    #[test]
600    fn octet_sequence_hex_basic() {
601        let bytes = parse_octet_sequence("0x00,0xFF,0x42").expect("ok");
602        assert_eq!(bytes, alloc::vec![0x00, 0xFF, 0x42]);
603    }
604
605    #[test]
606    fn octet_sequence_mixed_decimal_and_hex() {
607        // Spec: "decimal or hexadecimal" — pro Element entscheidbar.
608        let bytes = parse_octet_sequence("1,0x02,3,0x04").expect("mixed");
609        assert_eq!(bytes, alloc::vec![1, 2, 3, 4]);
610    }
611
612    #[test]
613    fn octet_sequence_whitespace_around_commas() {
614        let bytes = parse_octet_sequence(" 1 , 2 , 3 ").expect("trim");
615        assert_eq!(bytes, alloc::vec![1, 2, 3]);
616    }
617
618    #[test]
619    fn octet_sequence_empty_string_returns_empty_vec() {
620        let bytes = parse_octet_sequence("").expect("empty");
621        assert!(bytes.is_empty());
622    }
623
624    #[test]
625    fn octet_sequence_value_above_255_rejected() {
626        let err = parse_octet_sequence("0,256").expect_err("over");
627        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
628    }
629
630    #[test]
631    fn octet_sequence_negative_rejected() {
632        let err = parse_octet_sequence("0,-1").expect_err("negative");
633        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
634    }
635
636    #[test]
637    fn octet_sequence_trailing_comma_rejected() {
638        let err = parse_octet_sequence("1,2,").expect_err("trailing");
639        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
640    }
641
642    #[test]
643    fn octet_sequence_double_comma_rejected() {
644        let err = parse_octet_sequence("1,,2").expect_err("double");
645        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
646    }
647
648    #[test]
649    fn octet_sequence_non_numeric_token_rejected() {
650        let err = parse_octet_sequence("1,abc,3").expect_err("non-numeric");
651        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
652    }
653
654    #[test]
655    fn octet_sequence_hex_above_255_rejected() {
656        let err = parse_octet_sequence("0x100").expect_err("hex over");
657        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
658    }
659}