Skip to main content

zerodds_xml/
qos_parser.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Parser fuer DDS-XML 1.0 §7.3.2 QoS-Profile-Library.
4//!
5//! Konsumiert Roh-XML, baut den Foundation-Tree (siehe
6//! [`crate::parser::parse_xml_tree`]) auf und mappt ihn auf das typed
7//! Datenmodell aus [`crate::qos`].
8//!
9//! # Spec-konforme Boolean-Semantik
10//!
11//! Der Foundation-`parse_bool` aus [`crate::types`] akzeptiert bewusst
12//! `TRUE`/`FALSE`-Schreibweisen aus Cyclone/FastDDS-Kompatibilitaet.
13//! DDS-XML 1.0 §7.1.4 verlangt dagegen **strict** `true`/`false`. Wir
14//! verwenden hier den lokalen [`parse_bool_strict`] Helper, der nur die
15//! Spec-Werte zulaesst.
16//!
17//! # Single-QoS-Shortcut (§7.3.2.4.1)
18//!
19//! Ein `<qos_profile>` mit nur einem `<datawriter_qos>` (oder einem
20//! anderen 6 Entity-Containern) wird wie eine voll-spezifizierte Profile
21//! behandelt — der direkte Container-Tag ersetzt den Container-fuer-
22//! mehrere-Policies-Stil. Andere Entity-Container in derselben Profile
23//! werden zusaetzlich akzeptiert; das ist ein Superset des Spec-
24//! Examples in §7.3.2.4.1.
25
26use alloc::format;
27use alloc::string::{String, ToString};
28use alloc::vec::Vec;
29
30use zerodds_qos::{
31    DeadlineQosPolicy, DestinationOrderKind, DestinationOrderQosPolicy, DurabilityKind,
32    DurabilityQosPolicy, DurabilityServiceQosPolicy, Duration as QosDuration,
33    EntityFactoryQosPolicy, GroupDataQosPolicy, HistoryKind, HistoryQosPolicy,
34    LatencyBudgetQosPolicy, LifespanQosPolicy, LivelinessKind, LivelinessQosPolicy, OwnershipKind,
35    OwnershipQosPolicy, OwnershipStrengthQosPolicy, PartitionQosPolicy, PresentationAccessScope,
36    PresentationQosPolicy, ReaderDataLifecycleQosPolicy, ReliabilityKind, ReliabilityQosPolicy,
37    ResourceLimitsQosPolicy, TimeBasedFilterQosPolicy, TopicDataQosPolicy,
38    TransportPriorityQosPolicy, UserDataQosPolicy, WriterDataLifecycleQosPolicy,
39};
40
41use crate::errors::XmlError;
42use crate::parser::{XmlElement, parse_xml_tree};
43use crate::qos::{EntityQos, QosLibrary, QosProfile};
44use crate::types::{
45    DURATION_INFINITE_NSEC, DURATION_INFINITE_SEC, LENGTH_UNLIMITED, parse_duration_nsec,
46    parse_duration_sec, parse_long,
47};
48
49/// Parsed das oberste `<dds>`-Dokument und gibt **alle** enthaltenen
50/// QoS-Libraries zurueck (Spec §7.3.2.3 erlaubt mehrere Libraries pro
51/// Dokument).
52///
53/// # Errors
54/// * [`XmlError::InvalidXml`] — XML nicht wohlgeformt oder keine
55///   `<dds>`-Wurzel.
56/// * Weitere Fehler aus `parse_qos_library_element` durchgereicht.
57pub fn parse_qos_libraries(xml: &str) -> Result<Vec<QosLibrary>, XmlError> {
58    let doc = parse_xml_tree(xml)?;
59    if doc.root.name != "dds" {
60        return Err(XmlError::InvalidXml(format!(
61            "expected <dds> root, got <{}>",
62            doc.root.name
63        )));
64    }
65    let mut libs = Vec::new();
66    for lib_node in doc.root.children_named("qos_library") {
67        libs.push(parse_qos_library_element(lib_node)?);
68    }
69    Ok(libs)
70}
71
72/// Convenience: parsed das Dokument und gibt die **erste** QoS-Library
73/// zurueck. Liefert `MissingRequiredElement("qos_library")` wenn keine
74/// vorhanden.
75///
76/// # Errors
77/// Wie [`parse_qos_libraries`], plus `MissingRequiredElement` wenn das
78/// Dokument keine Library enthaelt.
79pub fn parse_qos_library(xml: &str) -> Result<QosLibrary, XmlError> {
80    parse_qos_libraries(xml)?
81        .into_iter()
82        .next()
83        .ok_or_else(|| XmlError::MissingRequiredElement("qos_library".into()))
84}
85
86/// Crate-internal `pub`-Wrapper fuer [`parse_qos_library_element`], damit
87/// der Top-Level-Loader (`crate::zerodds_xml::parse_dds_xml`) eine
88/// `<qos_library>`-Element-Sub-Tree direkt dekodieren kann ohne den
89/// Parser zu duplizieren.
90///
91/// # Errors
92/// Siehe `parse_qos_library_element`.
93pub fn parse_qos_library_element_public(el: &XmlElement) -> Result<QosLibrary, XmlError> {
94    parse_qos_library_element(el)
95}
96
97fn parse_qos_library_element(el: &XmlElement) -> Result<QosLibrary, XmlError> {
98    let name = el
99        .attribute("name")
100        .ok_or_else(|| XmlError::MissingRequiredElement("qos_library@name".into()))?
101        .to_string();
102    let mut profiles = Vec::new();
103    for prof_node in el.children_named("qos_profile") {
104        profiles.push(parse_qos_profile_element(prof_node)?);
105    }
106    // Spec §7.3.2.4.4: "The definition of an individual QoS is a
107    // shortcut for defining a QoS profile with a single QoS." Wir
108    // erkennen `<datawriter_qos name="…">` etc. direkt unter
109    // `<qos_library>` und wickeln sie in einen impliziten
110    // qos_profile-Wrapper.
111    for child in &el.children {
112        if let Some(profile) = parse_single_qos_shortcut(child)? {
113            profiles.push(profile);
114        }
115    }
116    Ok(QosLibrary { name, profiles })
117}
118
119/// Spec §7.3.2.4.4 Single-QoS-Shortcut — wenn ein `<datawriter_qos>`
120/// (oder `_reader`/`_topic`/`_publisher`/`_subscriber`/
121/// `_participant`) direkt unter `<qos_library>` steht UND ein
122/// `name`-Attribut traegt, ist das Aequivalent zu einem
123/// `<qos_profile name="…">` mit genau einer QoS.
124///
125/// Liefert `None` wenn das Element kein Shortcut ist (z.B. ein
126/// regulaeres `<qos_profile>`-Child oder ein qos-Element ohne `name`).
127fn parse_single_qos_shortcut(el: &XmlElement) -> Result<Option<QosProfile>, XmlError> {
128    let name = match el.attribute("name") {
129        Some(n) => n.to_string(),
130        None => return Ok(None),
131    };
132    let base_name = el.attribute("base_name").map(ToString::to_string);
133    let mut profile = QosProfile {
134        name,
135        base_name,
136        ..QosProfile::default()
137    };
138    let qos = parse_entity_qos(el)?;
139    match el.name.as_str() {
140        "datawriter_qos" => profile.datawriter_qos = Some(qos),
141        "datareader_qos" => profile.datareader_qos = Some(qos),
142        "topic_qos" => profile.topic_qos = Some(qos),
143        "publisher_qos" => profile.publisher_qos = Some(qos),
144        "subscriber_qos" => profile.subscriber_qos = Some(qos),
145        "domainparticipant_qos" | "participant_qos" => {
146            profile.domainparticipant_qos = Some(qos);
147        }
148        _ => return Ok(None),
149    }
150    Ok(Some(profile))
151}
152
153fn parse_qos_profile_element(el: &XmlElement) -> Result<QosProfile, XmlError> {
154    let name = el
155        .attribute("name")
156        .ok_or_else(|| XmlError::MissingRequiredElement("qos_profile@name".into()))?
157        .to_string();
158    let base_name = el.attribute("base_name").map(ToString::to_string);
159
160    let mut profile = QosProfile {
161        name,
162        base_name,
163        ..QosProfile::default()
164    };
165
166    for child in &el.children {
167        match child.name.as_str() {
168            "topic_filter" => {
169                profile.topic_filter = Some(child.text.clone());
170            }
171            "datawriter_qos" => {
172                profile.datawriter_qos = Some(parse_entity_qos(child)?);
173            }
174            "datareader_qos" => {
175                profile.datareader_qos = Some(parse_entity_qos(child)?);
176            }
177            "topic_qos" => {
178                profile.topic_qos = Some(parse_entity_qos(child)?);
179            }
180            "publisher_qos" => {
181                profile.publisher_qos = Some(parse_entity_qos(child)?);
182            }
183            "subscriber_qos" => {
184                profile.subscriber_qos = Some(parse_entity_qos(child)?);
185            }
186            "domainparticipant_qos" => {
187                profile.domainparticipant_qos = Some(parse_entity_qos(child)?);
188            }
189            // Unbekannte Elements werden im "lax"-Modus ignoriert
190            // (Spec §7.1.4 erlaubt "lax" als Lese-Strategie); strenge
191            // Validation kann auf Aufrufer-Seite via dedizierter
192            // Whitelist nachgezogen werden.
193            _ => {}
194        }
195    }
196    Ok(profile)
197}
198
199/// Crate-internal `pub`-Wrapper fuer [`parse_entity_qos`], damit andere
200/// Building-Block-Module (Domain, Participant) inline-QoS-Container
201/// dekodieren koennen ohne den Parser zu duplizieren.
202///
203/// # Errors
204/// Siehe `parse_entity_qos`.
205pub fn parse_entity_qos_public(el: &XmlElement) -> Result<EntityQos, XmlError> {
206    parse_entity_qos(el)
207}
208
209fn parse_entity_qos(el: &XmlElement) -> Result<EntityQos, XmlError> {
210    let mut q = EntityQos::default();
211    for child in &el.children {
212        match child.name.as_str() {
213            "durability" => q.durability = Some(parse_durability(child)?),
214            "durability_service" => q.durability_service = Some(parse_durability_service(child)?),
215            "presentation" => q.presentation = Some(parse_presentation(child)?),
216            "deadline" => q.deadline = Some(parse_deadline(child)?),
217            "latency_budget" => q.latency_budget = Some(parse_latency_budget(child)?),
218            "ownership" => q.ownership = Some(parse_ownership(child)?),
219            "ownership_strength" => q.ownership_strength = Some(parse_ownership_strength(child)?),
220            "liveliness" => q.liveliness = Some(parse_liveliness(child)?),
221            "time_based_filter" => q.time_based_filter = Some(parse_time_based_filter(child)?),
222            "partition" => q.partition = Some(parse_partition(child)?),
223            "reliability" => q.reliability = Some(parse_reliability(child)?),
224            "transport_priority" => q.transport_priority = Some(parse_transport_priority(child)?),
225            "lifespan" => q.lifespan = Some(parse_lifespan(child)?),
226            "destination_order" => q.destination_order = Some(parse_destination_order(child)?),
227            "history" => q.history = Some(parse_history(child)?),
228            "resource_limits" => q.resource_limits = Some(parse_resource_limits(child)?),
229            "entity_factory" => q.entity_factory = Some(parse_entity_factory(child)?),
230            "writer_data_lifecycle" => {
231                q.writer_data_lifecycle = Some(parse_writer_data_lifecycle(child)?);
232            }
233            "reader_data_lifecycle" => {
234                q.reader_data_lifecycle = Some(parse_reader_data_lifecycle(child)?);
235            }
236            "user_data" => q.user_data = Some(parse_user_data(child)?),
237            "topic_data" => q.topic_data = Some(parse_topic_data(child)?),
238            "group_data" => q.group_data = Some(parse_group_data(child)?),
239            // Unbekannte Children: lax (siehe `parse_qos_profile_element`).
240            _ => {}
241        }
242    }
243    Ok(q)
244}
245
246// ============================================================================
247// Spec-strikter Boolean-Parser
248// ============================================================================
249
250/// DDS-XML 1.0 §7.1.4 verlangt strict `true`/`false` (case-sensitive).
251///
252/// # Errors
253/// [`XmlError::ValueOutOfRange`] bei jeder anderen Schreibweise (auch
254/// `True`, `1`, `yes`).
255pub fn parse_bool_strict(s: &str) -> Result<bool, XmlError> {
256    let t = s.trim();
257    match t {
258        "true" => Ok(true),
259        "false" => Ok(false),
260        _ => Err(XmlError::ValueOutOfRange(format!(
261            "DDS-XML boolean must be `true` or `false` (case-sensitive), got `{t}`"
262        ))),
263    }
264}
265
266// ============================================================================
267// Duration-Helper: XML-`<sec>/<nanosec>` ↔ `zerodds_qos::Duration`
268// ============================================================================
269
270/// Konvertiert eine XML-`Duration_t`-Repraesentation
271/// (`<sec>` + optional `<nanosec>`, oder Inline-Text `DURATION_INFINITY`)
272/// zu einem `zerodds_qos::Duration` (i32 seconds + u32 fraction).
273///
274/// Akzeptierte XML-Formate:
275/// 1. `<duration><sec>1</sec><nanosec>0</nanosec></duration>` — voll.
276/// 2. `<duration><sec>DURATION_INFINITY</sec></duration>` — Sentinel
277///    (mappt auf [`QosDuration::INFINITE`]).
278/// 3. `<duration>DURATION_INFINITY</duration>` — Inline-Text-Sentinel.
279fn parse_duration_element(el: &XmlElement) -> Result<QosDuration, XmlError> {
280    // Inline-Sentinel-Text (Form 3).
281    let trimmed = el.text.trim();
282    if !trimmed.is_empty() && el.children.is_empty() {
283        return parse_duration_text_sentinel(trimmed);
284    }
285
286    let sec_el = el
287        .child("sec")
288        .ok_or_else(|| XmlError::MissingRequiredElement(format!("{}/sec", el.name)))?;
289    let sec_str = sec_el.text.trim();
290    let sec = parse_duration_sec(sec_str)?;
291
292    // nanosec optional; default 0.
293    let nsec = if let Some(n_el) = el.child("nanosec") {
294        parse_duration_nsec(n_el.text.trim())?
295    } else {
296        0
297    };
298
299    // Spec-Sentinel-Mapping: wenn der `<sec>`-Wert die Spec-Sentinel
300    // `DURATION_INFINITE_SEC` (= 0x7FFFFFFF) traegt, mappen wir auf
301    // `QosDuration::INFINITE` (`{i32::MAX, u32::MAX}`). Das ist die
302    // Wire-Konvention aus DDSI-RTPS §9.3.2 und behandelt sowohl
303    // `<sec>DURATION_INFINITY</sec>` (mit oder ohne `<nanosec>`) als
304    // auch `<sec>2147483647</sec><nanosec>2147483647</nanosec>` korrekt.
305    if sec == DURATION_INFINITE_SEC
306        && (nsec == DURATION_INFINITE_NSEC || el.child("nanosec").is_none())
307    {
308        return Ok(QosDuration::INFINITE);
309    }
310
311    // Regulaere Conversion: nanosec → fraction = nsec * 2^32 / 1e9.
312    // Wir berechnen in u64 um Overflow zu vermeiden.
313    let fraction = (u64::from(nsec) * (1u64 << 32) / 1_000_000_000) as u32;
314    Ok(QosDuration {
315        seconds: sec,
316        fraction,
317    })
318}
319
320fn parse_duration_text_sentinel(t: &str) -> Result<QosDuration, XmlError> {
321    if t == "DURATION_INFINITY" {
322        Ok(QosDuration::INFINITE)
323    } else {
324        Err(XmlError::ValueOutOfRange(format!(
325            "duration inline-text must be `DURATION_INFINITY`, got `{t}`"
326        )))
327    }
328}
329
330// ============================================================================
331// Per-Policy-Parser
332// ============================================================================
333
334fn parse_kind_text(el: &XmlElement) -> Result<&str, XmlError> {
335    let kind = el
336        .child("kind")
337        .ok_or_else(|| XmlError::MissingRequiredElement(format!("{}/kind", el.name)))?;
338    Ok(kind.text.trim())
339}
340
341fn parse_durability(el: &XmlElement) -> Result<DurabilityQosPolicy, XmlError> {
342    let kind_str = parse_kind_text(el)?;
343    let kind = match kind_str {
344        // DDS-XML 1.0 §7.3.2 erlaubt sowohl die kurze IDL-Form als auch
345        // die lange `_DURABILITY_QOS`-Suffix-Form aus DCPS-Spec.
346        "VOLATILE" | "VOLATILE_DURABILITY_QOS" => DurabilityKind::Volatile,
347        "TRANSIENT_LOCAL" | "TRANSIENT_LOCAL_DURABILITY_QOS" => DurabilityKind::TransientLocal,
348        "TRANSIENT" | "TRANSIENT_DURABILITY_QOS" => DurabilityKind::Transient,
349        "PERSISTENT" | "PERSISTENT_DURABILITY_QOS" => DurabilityKind::Persistent,
350        other => return Err(XmlError::BadEnum(other.to_string())),
351    };
352    Ok(DurabilityQosPolicy { kind })
353}
354
355fn parse_history(el: &XmlElement) -> Result<HistoryQosPolicy, XmlError> {
356    let kind_str = parse_kind_text(el).unwrap_or("");
357    let kind = match kind_str {
358        "" | "KEEP_LAST" | "KEEP_LAST_HISTORY_QOS" => HistoryKind::KeepLast,
359        "KEEP_ALL" | "KEEP_ALL_HISTORY_QOS" => HistoryKind::KeepAll,
360        other => return Err(XmlError::BadEnum(other.to_string())),
361    };
362    let depth = if let Some(d) = el.child("depth") {
363        parse_long(d.text.trim())?
364    } else {
365        // Spec-Default §2.2.3.17.3: 1.
366        1
367    };
368    Ok(HistoryQosPolicy { kind, depth })
369}
370
371fn parse_reliability(el: &XmlElement) -> Result<ReliabilityQosPolicy, XmlError> {
372    let kind_str = parse_kind_text(el)?;
373    let kind = match kind_str {
374        "BEST_EFFORT" | "BEST_EFFORT_RELIABILITY_QOS" => ReliabilityKind::BestEffort,
375        "RELIABLE" | "RELIABLE_RELIABILITY_QOS" => ReliabilityKind::Reliable,
376        other => return Err(XmlError::BadEnum(other.to_string())),
377    };
378    let max_blocking_time = if let Some(mbt) = el.child("max_blocking_time") {
379        parse_duration_element(mbt)?
380    } else {
381        QosDuration::from_millis(100)
382    };
383    Ok(ReliabilityQosPolicy {
384        kind,
385        max_blocking_time,
386    })
387}
388
389fn parse_deadline(el: &XmlElement) -> Result<DeadlineQosPolicy, XmlError> {
390    let period = el
391        .child("period")
392        .ok_or_else(|| XmlError::MissingRequiredElement("deadline/period".into()))?;
393    Ok(DeadlineQosPolicy {
394        period: parse_duration_element(period)?,
395    })
396}
397
398fn parse_latency_budget(el: &XmlElement) -> Result<LatencyBudgetQosPolicy, XmlError> {
399    let dur = el
400        .child("duration")
401        .ok_or_else(|| XmlError::MissingRequiredElement("latency_budget/duration".into()))?;
402    Ok(LatencyBudgetQosPolicy {
403        duration: parse_duration_element(dur)?,
404    })
405}
406
407fn parse_lifespan(el: &XmlElement) -> Result<LifespanQosPolicy, XmlError> {
408    let dur = el
409        .child("duration")
410        .ok_or_else(|| XmlError::MissingRequiredElement("lifespan/duration".into()))?;
411    Ok(LifespanQosPolicy {
412        duration: parse_duration_element(dur)?,
413    })
414}
415
416fn parse_time_based_filter(el: &XmlElement) -> Result<TimeBasedFilterQosPolicy, XmlError> {
417    let sep = el.child("minimum_separation").ok_or_else(|| {
418        XmlError::MissingRequiredElement("time_based_filter/minimum_separation".into())
419    })?;
420    Ok(TimeBasedFilterQosPolicy {
421        minimum_separation: parse_duration_element(sep)?,
422    })
423}
424
425fn parse_liveliness(el: &XmlElement) -> Result<LivelinessQosPolicy, XmlError> {
426    let kind = if let Some(k) = el.child("kind") {
427        match k.text.trim() {
428            "AUTOMATIC" | "AUTOMATIC_LIVELINESS_QOS" => LivelinessKind::Automatic,
429            "MANUAL_BY_PARTICIPANT" | "MANUAL_BY_PARTICIPANT_LIVELINESS_QOS" => {
430                LivelinessKind::ManualByParticipant
431            }
432            "MANUAL_BY_TOPIC" | "MANUAL_BY_TOPIC_LIVELINESS_QOS" => LivelinessKind::ManualByTopic,
433            other => return Err(XmlError::BadEnum(other.to_string())),
434        }
435    } else {
436        LivelinessKind::Automatic
437    };
438    let lease_duration = if let Some(ld) = el.child("lease_duration") {
439        parse_duration_element(ld)?
440    } else {
441        QosDuration::INFINITE
442    };
443    Ok(LivelinessQosPolicy {
444        kind,
445        lease_duration,
446    })
447}
448
449fn parse_destination_order(el: &XmlElement) -> Result<DestinationOrderQosPolicy, XmlError> {
450    let kind_str = parse_kind_text(el)?;
451    let kind = match kind_str {
452        "BY_RECEPTION_TIMESTAMP" | "BY_RECEPTION_TIMESTAMP_DESTINATIONORDER_QOS" => {
453            DestinationOrderKind::ByReceptionTimestamp
454        }
455        "BY_SOURCE_TIMESTAMP" | "BY_SOURCE_TIMESTAMP_DESTINATIONORDER_QOS" => {
456            DestinationOrderKind::BySourceTimestamp
457        }
458        other => return Err(XmlError::BadEnum(other.to_string())),
459    };
460    Ok(DestinationOrderQosPolicy { kind })
461}
462
463fn parse_ownership(el: &XmlElement) -> Result<OwnershipQosPolicy, XmlError> {
464    let kind_str = parse_kind_text(el)?;
465    let kind = match kind_str {
466        "SHARED" | "SHARED_OWNERSHIP_QOS" => OwnershipKind::Shared,
467        "EXCLUSIVE" | "EXCLUSIVE_OWNERSHIP_QOS" => OwnershipKind::Exclusive,
468        other => return Err(XmlError::BadEnum(other.to_string())),
469    };
470    Ok(OwnershipQosPolicy { kind })
471}
472
473fn parse_ownership_strength(el: &XmlElement) -> Result<OwnershipStrengthQosPolicy, XmlError> {
474    let val = el
475        .child("value")
476        .ok_or_else(|| XmlError::MissingRequiredElement("ownership_strength/value".into()))?;
477    Ok(OwnershipStrengthQosPolicy {
478        value: parse_long(val.text.trim())?,
479    })
480}
481
482fn parse_transport_priority(el: &XmlElement) -> Result<TransportPriorityQosPolicy, XmlError> {
483    let val = el
484        .child("value")
485        .ok_or_else(|| XmlError::MissingRequiredElement("transport_priority/value".into()))?;
486    Ok(TransportPriorityQosPolicy {
487        value: parse_long(val.text.trim())?,
488    })
489}
490
491fn parse_presentation(el: &XmlElement) -> Result<PresentationQosPolicy, XmlError> {
492    let access_scope = if let Some(s) = el.child("access_scope") {
493        match s.text.trim() {
494            "INSTANCE" | "INSTANCE_PRESENTATION_QOS" => PresentationAccessScope::Instance,
495            "TOPIC" | "TOPIC_PRESENTATION_QOS" => PresentationAccessScope::Topic,
496            "GROUP" | "GROUP_PRESENTATION_QOS" => PresentationAccessScope::Group,
497            other => return Err(XmlError::BadEnum(other.to_string())),
498        }
499    } else {
500        PresentationAccessScope::Instance
501    };
502    let coherent_access = if let Some(c) = el.child("coherent_access") {
503        parse_bool_strict(&c.text)?
504    } else {
505        false
506    };
507    let ordered_access = if let Some(o) = el.child("ordered_access") {
508        parse_bool_strict(&o.text)?
509    } else {
510        false
511    };
512    Ok(PresentationQosPolicy {
513        access_scope,
514        coherent_access,
515        ordered_access,
516    })
517}
518
519fn parse_partition(el: &XmlElement) -> Result<PartitionQosPolicy, XmlError> {
520    // §7.3.2: <partition><name>g1</name><name>g2</name></partition>.
521    // Ein optionales `<name_list>` mit Komma-separierten Eintraegen wird
522    // zusaetzlich akzeptiert (Cyclone-/FastDDS-Konvention).
523    let mut names: Vec<String> = Vec::new();
524    for child in &el.children {
525        match child.name.as_str() {
526            "name" => names.push(child.text.clone()),
527            "name_list" => {
528                for tok in child.text.split(',') {
529                    let t = tok.trim();
530                    if !t.is_empty() {
531                        names.push(t.to_string());
532                    }
533                }
534            }
535            _ => {}
536        }
537    }
538    Ok(PartitionQosPolicy { names })
539}
540
541fn parse_resource_limits(el: &XmlElement) -> Result<ResourceLimitsQosPolicy, XmlError> {
542    let max_samples = if let Some(c) = el.child("max_samples") {
543        parse_long(c.text.trim())?
544    } else {
545        LENGTH_UNLIMITED
546    };
547    let max_instances = if let Some(c) = el.child("max_instances") {
548        parse_long(c.text.trim())?
549    } else {
550        LENGTH_UNLIMITED
551    };
552    let max_samples_per_instance = if let Some(c) = el.child("max_samples_per_instance") {
553        parse_long(c.text.trim())?
554    } else {
555        LENGTH_UNLIMITED
556    };
557    Ok(ResourceLimitsQosPolicy {
558        max_samples,
559        max_instances,
560        max_samples_per_instance,
561    })
562}
563
564fn parse_entity_factory(el: &XmlElement) -> Result<EntityFactoryQosPolicy, XmlError> {
565    let auto = el.child("autoenable_created_entities").ok_or_else(|| {
566        XmlError::MissingRequiredElement("entity_factory/autoenable_created_entities".into())
567    })?;
568    Ok(EntityFactoryQosPolicy {
569        autoenable_created_entities: parse_bool_strict(&auto.text)?,
570    })
571}
572
573fn parse_writer_data_lifecycle(el: &XmlElement) -> Result<WriterDataLifecycleQosPolicy, XmlError> {
574    let auto = el
575        .child("autodispose_unregistered_instances")
576        .ok_or_else(|| {
577            XmlError::MissingRequiredElement(
578                "writer_data_lifecycle/autodispose_unregistered_instances".into(),
579            )
580        })?;
581    Ok(WriterDataLifecycleQosPolicy {
582        autodispose_unregistered_instances: parse_bool_strict(&auto.text)?,
583    })
584}
585
586fn parse_reader_data_lifecycle(el: &XmlElement) -> Result<ReaderDataLifecycleQosPolicy, XmlError> {
587    let nowriter = if let Some(c) = el.child("autopurge_nowriter_samples_delay") {
588        parse_duration_element(c)?
589    } else {
590        QosDuration::INFINITE
591    };
592    let disposed = if let Some(c) = el.child("autopurge_disposed_samples_delay") {
593        parse_duration_element(c)?
594    } else {
595        QosDuration::INFINITE
596    };
597    Ok(ReaderDataLifecycleQosPolicy {
598        autopurge_nowriter_samples_delay: nowriter,
599        autopurge_disposed_samples_delay: disposed,
600    })
601}
602
603fn parse_durability_service(el: &XmlElement) -> Result<DurabilityServiceQosPolicy, XmlError> {
604    let mut p = DurabilityServiceQosPolicy::default();
605    if let Some(c) = el.child("service_cleanup_delay") {
606        p.service_cleanup_delay = parse_duration_element(c)?;
607    }
608    if let Some(c) = el.child("history_kind") {
609        p.history_kind = match c.text.trim() {
610            "KEEP_LAST" | "KEEP_LAST_HISTORY_QOS" => HistoryKind::KeepLast,
611            "KEEP_ALL" | "KEEP_ALL_HISTORY_QOS" => HistoryKind::KeepAll,
612            other => return Err(XmlError::BadEnum(other.to_string())),
613        };
614    }
615    if let Some(c) = el.child("history_depth") {
616        p.history_depth = parse_long(c.text.trim())?;
617    }
618    if let Some(c) = el.child("max_samples") {
619        p.max_samples = parse_long(c.text.trim())?;
620    }
621    if let Some(c) = el.child("max_instances") {
622        p.max_instances = parse_long(c.text.trim())?;
623    }
624    if let Some(c) = el.child("max_samples_per_instance") {
625        p.max_samples_per_instance = parse_long(c.text.trim())?;
626    }
627    Ok(p)
628}
629
630// ----- Octet-Sequence Helpers (UserData / TopicData / GroupData) -----------
631
632/// Parsed `<value>…</value>` als Octet-Sequence (Base64). Spec §7.2.4
633/// erlaubt Base64 als kanonische Form fuer `sequence<octet>`.
634fn parse_octet_value(el: &XmlElement) -> Result<Vec<u8>, XmlError> {
635    let v = el
636        .child("value")
637        .ok_or_else(|| XmlError::MissingRequiredElement(format!("{}/value", el.name)))?;
638    let raw = v.text.trim();
639    if raw.is_empty() {
640        return Ok(Vec::new());
641    }
642    base64_decode(raw)
643        .ok_or_else(|| XmlError::ValueOutOfRange(format!("invalid base64 in <{}>", el.name)))
644}
645
646fn parse_user_data(el: &XmlElement) -> Result<UserDataQosPolicy, XmlError> {
647    Ok(UserDataQosPolicy {
648        value: parse_octet_value(el)?,
649    })
650}
651fn parse_topic_data(el: &XmlElement) -> Result<TopicDataQosPolicy, XmlError> {
652    Ok(TopicDataQosPolicy {
653        value: parse_octet_value(el)?,
654    })
655}
656fn parse_group_data(el: &XmlElement) -> Result<GroupDataQosPolicy, XmlError> {
657    Ok(GroupDataQosPolicy {
658        value: parse_octet_value(el)?,
659    })
660}
661
662/// Pure-Rust Base64-Decoder mit `=`-Padding-Toleranz. Dupliziert aus
663/// `crates/security-permissions/src/governance.rs::base64_decode_anchor`
664/// um die Crate-Dep zu vermeiden (siehe Modul-Doc).
665fn base64_decode(input: &str) -> Option<Vec<u8>> {
666    let cleaned: String = input.chars().filter(|c| !c.is_whitespace()).collect();
667    let bytes = cleaned.as_bytes();
668    if bytes.len() % 4 != 0 {
669        return None;
670    }
671    let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
672    for chunk in bytes.chunks_exact(4) {
673        let mut vals = [0u8; 4];
674        let mut pad = 0usize;
675        for (i, &c) in chunk.iter().enumerate() {
676            if c == b'=' {
677                pad += 1;
678                vals[i] = 0;
679            } else if pad > 0 {
680                return None;
681            } else {
682                vals[i] = match c {
683                    b'A'..=b'Z' => c - b'A',
684                    b'a'..=b'z' => c - b'a' + 26,
685                    b'0'..=b'9' => c - b'0' + 52,
686                    b'+' => 62,
687                    b'/' => 63,
688                    _ => return None,
689                };
690            }
691        }
692        let n = (u32::from(vals[0]) << 18)
693            | (u32::from(vals[1]) << 12)
694            | (u32::from(vals[2]) << 6)
695            | u32::from(vals[3]);
696        out.push(((n >> 16) & 0xFF) as u8);
697        if pad < 2 {
698            out.push(((n >> 8) & 0xFF) as u8);
699        }
700        if pad < 1 {
701            out.push((n & 0xFF) as u8);
702        }
703    }
704    Some(out)
705}
706
707#[cfg(test)]
708#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
709mod tests {
710    use super::*;
711
712    #[test]
713    fn parse_bool_strict_accepts_only_lowercase() {
714        assert!(parse_bool_strict("true").unwrap());
715        assert!(!parse_bool_strict("false").unwrap());
716        assert!(parse_bool_strict("True").is_err());
717        assert!(parse_bool_strict("TRUE").is_err());
718        assert!(parse_bool_strict("1").is_err());
719        assert!(parse_bool_strict("yes").is_err());
720    }
721
722    #[test]
723    fn base64_decode_basic() {
724        // "raw_bytes" -> base64 = "cmF3X2J5dGVz".
725        let v = base64_decode("cmF3X2J5dGVz").expect("decode");
726        assert_eq!(v, b"raw_bytes");
727    }
728
729    #[test]
730    fn base64_decode_with_padding() {
731        // "raw" -> "cmF3"
732        // "raw_" -> "cmF3Xw=="
733        let v = base64_decode("cmF3Xw==").expect("decode");
734        assert_eq!(v, b"raw_");
735    }
736
737    #[test]
738    fn base64_decode_invalid_returns_none() {
739        assert!(base64_decode("!!!").is_none());
740        // Length not multiple of 4.
741        assert!(base64_decode("abc").is_none());
742    }
743
744    #[test]
745    fn parse_minimal_library() {
746        let xml = r#"<?xml version="1.0"?>
747<dds>
748  <qos_library name="L1">
749    <qos_profile name="P1"/>
750  </qos_library>
751</dds>"#;
752        let lib = parse_qos_library(xml).expect("parse");
753        assert_eq!(lib.name, "L1");
754        assert_eq!(lib.profiles.len(), 1);
755        assert_eq!(lib.profiles[0].name, "P1");
756        // Spec: alle Container None.
757        let p = &lib.profiles[0];
758        assert!(p.datawriter_qos.is_none());
759        assert!(p.datareader_qos.is_none());
760    }
761
762    #[test]
763    fn parse_reliability_and_history() {
764        let xml = r#"<dds>
765  <qos_library name="L">
766    <qos_profile name="P">
767      <datawriter_qos>
768        <reliability>
769          <kind>RELIABLE</kind>
770          <max_blocking_time><sec>1</sec><nanosec>0</nanosec></max_blocking_time>
771        </reliability>
772        <history>
773          <kind>KEEP_LAST</kind>
774          <depth>10</depth>
775        </history>
776      </datawriter_qos>
777    </qos_profile>
778  </qos_library>
779</dds>"#;
780        let lib = parse_qos_library(xml).expect("parse");
781        let dw = lib.profiles[0].datawriter_qos.as_ref().expect("dw");
782        assert_eq!(
783            dw.reliability.unwrap().kind,
784            zerodds_qos::ReliabilityKind::Reliable
785        );
786        assert_eq!(dw.history.unwrap().depth, 10);
787    }
788
789    #[test]
790    fn rejects_non_dds_root() {
791        let xml = r#"<root/>"#;
792        let err = parse_qos_libraries(xml).expect_err("non-dds root");
793        assert!(matches!(err, XmlError::InvalidXml(_)));
794    }
795
796    #[test]
797    fn missing_library_name_rejected() {
798        let xml = r#"<dds><qos_library/></dds>"#;
799        let err = parse_qos_libraries(xml).expect_err("missing-name");
800        assert!(matches!(err, XmlError::MissingRequiredElement(_)));
801    }
802
803    #[test]
804    fn missing_profile_name_rejected() {
805        let xml = r#"<dds><qos_library name="L"><qos_profile/></qos_library></dds>"#;
806        let err = parse_qos_libraries(xml).expect_err("missing-name");
807        assert!(matches!(err, XmlError::MissingRequiredElement(_)));
808    }
809
810    // ---- §7.3.2.4.4 Single-QoS-Profile-Shortcut --------------------
811
812    #[test]
813    fn single_qos_shortcut_datawriter_creates_implicit_profile() {
814        // Spec §7.3.2.4.4: <datawriter_qos name="..."> direkt unter
815        // <qos_library> ist Aequivalent zu <qos_profile><datawriter_qos/>
816        // </qos_profile>.
817        let xml = r#"<dds>
818          <qos_library name="L">
819            <datawriter_qos name="ShortcutDW">
820              <reliability><kind>RELIABLE_RELIABILITY_QOS</kind></reliability>
821            </datawriter_qos>
822          </qos_library>
823        </dds>"#;
824        let libs = parse_qos_libraries(xml).expect("parse");
825        let lib = &libs[0];
826        let prof = lib
827            .profiles
828            .iter()
829            .find(|p| p.name == "ShortcutDW")
830            .expect("implicit profile");
831        assert!(prof.datawriter_qos.is_some());
832        assert!(prof.datareader_qos.is_none());
833    }
834
835    #[test]
836    fn single_qos_shortcut_topic_creates_implicit_profile() {
837        let xml = r#"<dds>
838          <qos_library name="L">
839            <topic_qos name="ShortcutT"/>
840          </qos_library>
841        </dds>"#;
842        let libs = parse_qos_libraries(xml).expect("parse");
843        let prof = libs[0]
844            .profiles
845            .iter()
846            .find(|p| p.name == "ShortcutT")
847            .expect("topic shortcut");
848        assert!(prof.topic_qos.is_some());
849    }
850
851    #[test]
852    fn single_qos_shortcut_without_name_is_ignored() {
853        // Spec verlangt name-Attribut. Ohne `name` waere das kein
854        // Shortcut → wird verworfen (kein Crash).
855        let xml = r#"<dds>
856          <qos_library name="L">
857            <qos_profile name="Real"/>
858            <datawriter_qos><reliability><kind>BEST_EFFORT_RELIABILITY_QOS</kind></reliability></datawriter_qos>
859          </qos_library>
860        </dds>"#;
861        let libs = parse_qos_libraries(xml).expect("parse");
862        // Nur das echte qos_profile zaehlt — kein impliziter Wrapper.
863        assert_eq!(libs[0].profiles.len(), 1);
864        assert_eq!(libs[0].profiles[0].name, "Real");
865    }
866
867    #[test]
868    fn single_qos_shortcut_multiple_kinds_in_same_library() {
869        // Mehrere Shortcut-Kinds koennen ko-existieren.
870        let xml = r#"<dds>
871          <qos_library name="L">
872            <datawriter_qos name="DW"/>
873            <datareader_qos name="DR"/>
874            <publisher_qos name="P"/>
875          </qos_library>
876        </dds>"#;
877        let libs = parse_qos_libraries(xml).expect("parse");
878        assert_eq!(libs[0].profiles.len(), 3);
879        let names: alloc::collections::BTreeSet<&str> =
880            libs[0].profiles.iter().map(|p| p.name.as_str()).collect();
881        assert!(names.contains("DW"));
882        assert!(names.contains("DR"));
883        assert!(names.contains("P"));
884    }
885
886    #[test]
887    fn boolean_case_sensitive_rejected() {
888        // §7.1.4: only `true`/`false`. `True` in entity_factory.
889        let xml = r#"<dds><qos_library name="L"><qos_profile name="P">
890          <domainparticipant_qos>
891            <entity_factory><autoenable_created_entities>True</autoenable_created_entities></entity_factory>
892          </domainparticipant_qos>
893        </qos_profile></qos_library></dds>"#;
894        let err = parse_qos_libraries(xml).expect_err("strict-bool");
895        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
896    }
897
898    #[test]
899    fn duration_inline_infinity_sentinel() {
900        let xml = r#"<dds><qos_library name="L"><qos_profile name="P">
901          <datawriter_qos>
902            <deadline><period><sec>DURATION_INFINITY</sec></period></deadline>
903          </datawriter_qos>
904        </qos_profile></qos_library></dds>"#;
905        let lib = parse_qos_library(xml).expect("parse");
906        let dw = lib.profiles[0].datawriter_qos.as_ref().expect("dw");
907        let p = dw.deadline.unwrap().period;
908        assert!(p.is_infinite());
909    }
910
911    #[test]
912    fn dtd_rejected_in_qos_context() {
913        let xml = r#"<?xml version="1.0"?>
914<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
915<dds><qos_library name="L"/></dds>"#;
916        let err = parse_qos_libraries(xml).expect_err("dtd");
917        assert!(matches!(err, XmlError::InvalidXml(_)));
918    }
919}