Skip to main content

zerodds_xml/
qos_parser.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Parser for the DDS-XML 1.0 §7.3.2 QoS profile library.
4//!
5//! Consumes raw XML, builds the foundation tree (see
6//! [`crate::parser::parse_xml_tree`]) and maps it onto the typed
7//! data model from [`crate::qos`].
8//!
9//! # Spec-conformant boolean semantics
10//!
11//! The foundation `parse_bool` from [`crate::types`] deliberately accepts
12//! `TRUE`/`FALSE` spellings for Cyclone/FastDDS compatibility.
13//! DDS-XML 1.0 §7.1.4, by contrast, requires **strict** `true`/`false`. Here
14//! we use the local [`parse_bool_strict`] helper, which only allows the
15//! spec values.
16//!
17//! # Single-QoS shortcut (§7.3.2.4.1)
18//!
19//! A `<qos_profile>` with only one `<datawriter_qos>` (or one of the
20//! other 6 entity containers) is treated like a fully specified profile —
21//! the direct container tag replaces the container-for-multiple-policies
22//! style. Other entity containers in the same profile
23//! are additionally accepted; that is a superset of the spec
24//! example 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/// Parses the top-level `<dds>` document and returns **all** contained
50/// QoS libraries (Spec §7.3.2.3 allows multiple libraries per
51/// document).
52///
53/// # Errors
54/// * [`XmlError::InvalidXml`] — XML not well-formed or no
55///   `<dds>` root.
56/// * Further errors from `parse_qos_library_element`, passed through.
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: parses the document and returns the **first** QoS library.
73/// Returns `MissingRequiredElement("qos_library")` if none is
74/// present.
75///
76/// # Errors
77/// As [`parse_qos_libraries`], plus `MissingRequiredElement` if the
78/// document contains no library.
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 for [`parse_qos_library_element`], so that
87/// the top-level loader (`crate::zerodds_xml::parse_dds_xml`) can decode a
88/// `<qos_library>` element sub-tree directly without duplicating the
89/// parser.
90///
91/// # Errors
92/// See `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." We
108    // recognize `<datawriter_qos name="…">` etc. directly under
109    // `<qos_library>` and wrap them in an implicit
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 — when a `<datawriter_qos>`
120/// (or `_reader`/`_topic`/`_publisher`/`_subscriber`/
121/// `_participant`) is directly under `<qos_library>` AND carries a
122/// `name` attribute, it is equivalent to a
123/// `<qos_profile name="…">` with exactly one QoS.
124///
125/// Returns `None` if the element is not a shortcut (e.g. a
126/// regular `<qos_profile>` child or a qos element without `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            // Unknown elements are ignored in "lax" mode
190            // (Spec §7.1.4 allows "lax" as a read strategy); strict
191            // validation can be added on the caller side via a dedicated
192            // whitelist.
193            _ => {}
194        }
195    }
196    Ok(profile)
197}
198
199/// Crate-internal `pub` wrapper for [`parse_entity_qos`], so that other
200/// building-block modules (domain, participant) can decode inline QoS
201/// containers without duplicating the parser.
202///
203/// # Errors
204/// See `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            // Unknown children: lax (see `parse_qos_profile_element`).
240            _ => {}
241        }
242    }
243    Ok(q)
244}
245
246// ============================================================================
247// Spec-strict boolean parser
248// ============================================================================
249
250/// DDS-XML 1.0 §7.1.4 requires strict `true`/`false` (case-sensitive).
251///
252/// # Errors
253/// [`XmlError::ValueOutOfRange`] on any other spelling (incl.
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/// Converts an XML `Duration_t` representation
271/// (`<sec>` + optional `<nanosec>`, or inline text `DURATION_INFINITY`)
272/// into a `zerodds_qos::Duration` (i32 seconds + u32 fraction).
273///
274/// Accepted XML formats:
275/// 1. `<duration><sec>1</sec><nanosec>0</nanosec></duration>` — full.
276/// 2. `<duration><sec>DURATION_INFINITY</sec></duration>` — sentinel
277///    (maps to [`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: when the `<sec>` value carries the spec sentinel
300    // `DURATION_INFINITE_SEC` (= 0x7FFFFFFF), we map to
301    // `QosDuration::INFINITE` (`{i32::MAX, u32::MAX}`). That is the
302    // wire convention from DDSI-RTPS §9.3.2 and handles both
303    // `<sec>DURATION_INFINITY</sec>` (with or without `<nanosec>`) and
304    // `<sec>2147483647</sec><nanosec>2147483647</nanosec>` correctly.
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    // Regular conversion: nanosec → fraction = nsec * 2^32 / 1e9.
312    // We compute in u64 to avoid overflow.
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 allows both the short IDL form and
345        // the long `_DURABILITY_QOS` suffix form from the 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    // An optional `<name_list>` with comma-separated entries is
522    // additionally accepted (Cyclone/FastDDS convention).
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/// Parses `<value>…</value>` as an octet sequence (Base64). Spec §7.2.4
633/// allows Base64 as the canonical form for `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 with `=` padding tolerance. Duplicated from
663/// `crates/security-permissions/src/governance.rs::base64_decode_anchor`
664/// to avoid the crate dependency (see the module 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: all containers 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="..."> directly under
815        // <qos_library> is equivalent to <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        // The spec requires a name attribute. Without `name` it would not be a
854        // shortcut → discarded (no 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        // Only the real qos_profile counts — no implicit 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        // Multiple shortcut kinds can co-exist.
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}