Skip to main content

zerodds_xml/
qos.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Data model for the DDS-XML 1.0 §7.3.2 QoS profile library.
4//!
5//! Spec source: OMG DDS-XML 1.0 §7.3.2 (QoS Library Building Block) +
6//! DDS 1.4 §2.2.3 (22 standard QoS policies).
7//!
8//! # Layer discipline
9//!
10//! The individual policy structures (`DurabilityQosPolicy`, …) and the
11//! aggregate containers (`WriterQos`, `ReaderQos`) live in the crate
12//! [`zerodds_qos`] (wire-format source). `zerodds-xml` reuses them here
13//! directly — *no duplicates*.
14//!
15//! The XML-specific containers [`EntityQos`] (six variants
16//! for DataWriter/DataReader/Topic/Publisher/Subscriber/
17//! DomainParticipant) carry an `Option<…>` per policy. `None` =
18//! "spec default" (taken from `zerodds-qos::*::Default`); `Some(p)` =
19//! explicitly set in the XML. The override semantics of inheritance
20//! (see [`crate::qos_inheritance`]) operate on these options:
21//! a child profile with `Some(p)` overrides the inherited `Some/None`,
22//! a child profile with `None` inherits unchanged.
23//!
24//! # XML element to Rust type mapping (DDS-XML 1.0 §7.3.2 + DDS 1.4 §2.2.3)
25//!
26//! ```text
27//! XML element                            | Rust type
28//! ---------------------------------------+----------------------------------
29//! <qos_library name=…>                   | QosLibrary
30//! <qos_profile name=… base_name=…>       | QosProfile
31//! <topic_filter>                         | QosProfile.topic_filter (String)
32//! <datawriter_qos>                       | EntityQos (DataWriter)
33//! <datareader_qos>                       | EntityQos (DataReader)
34//! <topic_qos>                            | EntityQos (Topic)
35//! <publisher_qos>                        | EntityQos (Publisher)
36//! <subscriber_qos>                       | EntityQos (Subscriber)
37//! <domainparticipant_qos>                | EntityQos (DomainParticipant)
38//! <durability><kind>                     | DurabilityQosPolicy
39//! <durability_service>                   | DurabilityServiceQosPolicy
40//! <presentation>                         | PresentationQosPolicy
41//! <deadline><period>                     | DeadlineQosPolicy
42//! <latency_budget><duration>             | LatencyBudgetQosPolicy
43//! <ownership><kind>                      | OwnershipQosPolicy
44//! <ownership_strength><value>            | OwnershipStrengthQosPolicy
45//! <liveliness><kind><lease_duration>     | LivelinessQosPolicy
46//! <time_based_filter><minimum_separation>| TimeBasedFilterQosPolicy
47//! <partition><name>g1</name>…            | PartitionQosPolicy
48//! <reliability><kind><max_blocking_time> | ReliabilityQosPolicy
49//! <transport_priority><value>            | TransportPriorityQosPolicy
50//! <lifespan><duration>                   | LifespanQosPolicy
51//! <destination_order><kind>              | DestinationOrderQosPolicy
52//! <history><kind><depth>                 | HistoryQosPolicy
53//! <resource_limits>…                     | ResourceLimitsQosPolicy
54//! <entity_factory><autoenable_…>         | EntityFactoryQosPolicy
55//! <writer_data_lifecycle>                | WriterDataLifecycleQosPolicy
56//! <reader_data_lifecycle>                | ReaderDataLifecycleQosPolicy
57//! <user_data><value>BASE64</value>       | UserDataQosPolicy
58//! <topic_data><value>BASE64</value>      | TopicDataQosPolicy
59//! <group_data><value>BASE64</value>      | GroupDataQosPolicy
60//! ```
61
62use alloc::string::String;
63use alloc::vec::Vec;
64
65use zerodds_qos::{
66    DeadlineQosPolicy, DestinationOrderQosPolicy, DurabilityQosPolicy, DurabilityServiceQosPolicy,
67    EntityFactoryQosPolicy, GroupDataQosPolicy, HistoryQosPolicy, LatencyBudgetQosPolicy,
68    LifespanQosPolicy, LivelinessQosPolicy, OwnershipQosPolicy, OwnershipStrengthQosPolicy,
69    PartitionQosPolicy, PresentationQosPolicy, ReaderDataLifecycleQosPolicy, ReaderQos,
70    ReliabilityQosPolicy, ResourceLimitsQosPolicy, TimeBasedFilterQosPolicy, TopicDataQosPolicy,
71    TransportPriorityQosPolicy, UserDataQosPolicy, WriterDataLifecycleQosPolicy, WriterQos,
72};
73
74/// Container for 1+ QoS profiles per DDS-XML 1.0 §7.3.2.
75///
76/// Multiple libraries may appear per document; each library carries
77/// a `name` attribute. Lookups across libraries happen via the
78/// 2-segment path `library::profile`.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct QosLibrary {
81    /// Name of the library (`<qos_library name="…">` attribute).
82    pub name: String,
83    /// Profiles in this library, in document order.
84    pub profiles: Vec<QosProfile>,
85}
86
87impl QosLibrary {
88    /// Looks up a profile within this library by name.
89    #[must_use]
90    pub fn profile(&self, name: &str) -> Option<&QosProfile> {
91        self.profiles.iter().find(|p| p.name == name)
92    }
93}
94
95/// A single `<qos_profile>` element (§7.3.2.4).
96///
97/// Each `EntityQos` container is `Option<…>` — `None` means "not listed
98/// in the XML", which on resolve transitions to the spec-default aggregate
99/// type (see [`crate::qos_inheritance::resolve_profile`]).
100#[derive(Debug, Clone, Default, PartialEq, Eq)]
101pub struct QosProfile {
102    /// Name of the profile (`name` attribute).
103    pub name: String,
104    /// Optional `base_name` for inheritance (Spec §7.3.2.4.2). Format:
105    /// either `"profile"` (within the same library) or
106    /// `"library::profile"` (cross-library reference).
107    pub base_name: Option<String>,
108    /// Optional topic filter (glob, `*`/`?`) for profile-to-topic
109    /// binding. Multiple `<topic_filter>` are collapsed into **one** pattern
110    /// (last wins) — the spec only meaningfully covers one filter
111    /// per profile.
112    pub topic_filter: Option<String>,
113    /// `<datawriter_qos>` container.
114    pub datawriter_qos: Option<EntityQos>,
115    /// `<datareader_qos>` container.
116    pub datareader_qos: Option<EntityQos>,
117    /// `<topic_qos>` container.
118    pub topic_qos: Option<EntityQos>,
119    /// `<publisher_qos>` container.
120    pub publisher_qos: Option<EntityQos>,
121    /// `<subscriber_qos>` container.
122    pub subscriber_qos: Option<EntityQos>,
123    /// `<domainparticipant_qos>` container.
124    pub domainparticipant_qos: Option<EntityQos>,
125}
126
127/// Optional-per-policy container for all 22 OMG-DDS-1.4 QoS policies.
128///
129/// An unset `Option` means: not listed in the XML → spec
130/// default on resolve. *Reused as one container for all 6 entity types* —
131/// DDS-XML 1.0 in principle allows all policies in
132/// all 6 containers; semantic filtering (e.g. that
133/// `OwnershipStrength` only makes sense on the writer) happens when
134/// materializing into `WriterQos`/`ReaderQos` (see [`Self::into_writer_qos`] /
135/// [`Self::into_reader_qos`]).
136#[derive(Debug, Clone, Default, PartialEq, Eq)]
137pub struct EntityQos {
138    /// `<durability>` (DDS 1.4 §2.2.3.4).
139    pub durability: Option<DurabilityQosPolicy>,
140    /// `<durability_service>` (§2.2.3.5).
141    pub durability_service: Option<DurabilityServiceQosPolicy>,
142    /// `<presentation>` (§2.2.3.6).
143    pub presentation: Option<PresentationQosPolicy>,
144    /// `<deadline>` (§2.2.3.7).
145    pub deadline: Option<DeadlineQosPolicy>,
146    /// `<latency_budget>` (§2.2.3.10).
147    pub latency_budget: Option<LatencyBudgetQosPolicy>,
148    /// `<ownership>` (§2.2.3.23).
149    pub ownership: Option<OwnershipQosPolicy>,
150    /// `<ownership_strength>` (§2.2.3.24).
151    pub ownership_strength: Option<OwnershipStrengthQosPolicy>,
152    /// `<liveliness>` (§2.2.3.11).
153    pub liveliness: Option<LivelinessQosPolicy>,
154    /// `<time_based_filter>` (§2.2.3.12).
155    pub time_based_filter: Option<TimeBasedFilterQosPolicy>,
156    /// `<partition>` (§2.2.3.13).
157    pub partition: Option<PartitionQosPolicy>,
158    /// `<reliability>` (§2.2.3.14).
159    pub reliability: Option<ReliabilityQosPolicy>,
160    /// `<transport_priority>` (§2.2.3.15).
161    pub transport_priority: Option<TransportPriorityQosPolicy>,
162    /// `<lifespan>` (§2.2.3.16).
163    pub lifespan: Option<LifespanQosPolicy>,
164    /// `<destination_order>` (§2.2.3.18).
165    pub destination_order: Option<DestinationOrderQosPolicy>,
166    /// `<history>` (§2.2.3.17).
167    pub history: Option<HistoryQosPolicy>,
168    /// `<resource_limits>` (§2.2.3.19).
169    pub resource_limits: Option<ResourceLimitsQosPolicy>,
170    /// `<entity_factory>` (§2.2.3.27).
171    pub entity_factory: Option<EntityFactoryQosPolicy>,
172    /// `<writer_data_lifecycle>` (§2.2.3.21).
173    pub writer_data_lifecycle: Option<WriterDataLifecycleQosPolicy>,
174    /// `<reader_data_lifecycle>` (§2.2.3.20).
175    pub reader_data_lifecycle: Option<ReaderDataLifecycleQosPolicy>,
176    /// `<user_data>` (§2.2.3.1).
177    pub user_data: Option<UserDataQosPolicy>,
178    /// `<topic_data>` (§2.2.3.2).
179    pub topic_data: Option<TopicDataQosPolicy>,
180    /// `<group_data>` (§2.2.3.3).
181    pub group_data: Option<GroupDataQosPolicy>,
182}
183
184impl EntityQos {
185    /// Merges `override_` over `self`: each explicitly set policy in
186    /// `override_` overrides the one in `self`. The operation is **not**
187    /// commutative — order: `parent.merge(child)` means
188    /// "child wins where the child explicitly set a value".
189    #[must_use]
190    pub fn merge(mut self, override_: &Self) -> Self {
191        macro_rules! merge_field {
192            ($field:ident) => {
193                if let Some(v) = override_.$field.clone() {
194                    self.$field = Some(v);
195                }
196            };
197        }
198        merge_field!(durability);
199        merge_field!(durability_service);
200        merge_field!(presentation);
201        merge_field!(deadline);
202        merge_field!(latency_budget);
203        merge_field!(ownership);
204        merge_field!(ownership_strength);
205        merge_field!(liveliness);
206        merge_field!(time_based_filter);
207        merge_field!(partition);
208        merge_field!(reliability);
209        merge_field!(transport_priority);
210        merge_field!(lifespan);
211        merge_field!(destination_order);
212        merge_field!(history);
213        merge_field!(resource_limits);
214        merge_field!(entity_factory);
215        merge_field!(writer_data_lifecycle);
216        merge_field!(reader_data_lifecycle);
217        merge_field!(user_data);
218        merge_field!(topic_data);
219        merge_field!(group_data);
220        self
221    }
222
223    /// Materializes a `WriterQos` from the policies set in the `EntityQos`.
224    /// `None` entries are filled with the spec defaults from
225    /// `zerodds_qos::WriterQos::default()`.
226    ///
227    /// Policies that make no sense for a writer (e.g.
228    /// `time_based_filter`, `reader_data_lifecycle`) are
229    /// deliberately ignored here — they are only materialized for ReaderQos.
230    #[must_use]
231    pub fn into_writer_qos(&self) -> WriterQos {
232        let mut q = WriterQos::default();
233        if let Some(p) = self.durability {
234            q.durability = p;
235        }
236        if let Some(p) = self.durability_service {
237            q.durability_service = p;
238        }
239        if let Some(p) = self.deadline {
240            q.deadline = p;
241        }
242        if let Some(p) = self.latency_budget {
243            q.latency_budget = p;
244        }
245        if let Some(p) = self.liveliness {
246            q.liveliness = p;
247        }
248        if let Some(p) = self.reliability {
249            q.reliability = p;
250        }
251        if let Some(p) = self.destination_order {
252            q.destination_order = p;
253        }
254        if let Some(p) = self.history {
255            q.history = p;
256        }
257        if let Some(p) = self.resource_limits {
258            q.resource_limits = p;
259        }
260        if let Some(p) = self.transport_priority {
261            q.transport_priority = p;
262        }
263        if let Some(p) = self.lifespan {
264            q.lifespan = p;
265        }
266        if let Some(p) = self.ownership {
267            q.ownership = p;
268        }
269        if let Some(p) = self.ownership_strength {
270            q.ownership_strength = p;
271        }
272        if let Some(p) = self.presentation {
273            q.presentation = p;
274        }
275        if let Some(p) = self.partition.clone() {
276            q.partition = p;
277        }
278        if let Some(p) = self.writer_data_lifecycle {
279            q.writer_data_lifecycle = p;
280        }
281        if let Some(p) = self.user_data.clone() {
282            q.user_data = p;
283        }
284        if let Some(p) = self.topic_data.clone() {
285            q.topic_data = p;
286        }
287        if let Some(p) = self.group_data.clone() {
288            q.group_data = p;
289        }
290        q
291    }
292
293    /// Materializes a `ReaderQos` analogous to [`Self::into_writer_qos`].
294    #[must_use]
295    pub fn into_reader_qos(&self) -> ReaderQos {
296        let mut q = ReaderQos::default();
297        if let Some(p) = self.durability {
298            q.durability = p;
299        }
300        if let Some(p) = self.deadline {
301            q.deadline = p;
302        }
303        if let Some(p) = self.latency_budget {
304            q.latency_budget = p;
305        }
306        if let Some(p) = self.liveliness {
307            q.liveliness = p;
308        }
309        if let Some(p) = self.reliability {
310            q.reliability = p;
311        }
312        if let Some(p) = self.destination_order {
313            q.destination_order = p;
314        }
315        if let Some(p) = self.history {
316            q.history = p;
317        }
318        if let Some(p) = self.resource_limits {
319            q.resource_limits = p;
320        }
321        if let Some(p) = self.ownership {
322            q.ownership = p;
323        }
324        if let Some(p) = self.time_based_filter {
325            q.time_based_filter = p;
326        }
327        if let Some(p) = self.presentation {
328            q.presentation = p;
329        }
330        if let Some(p) = self.partition.clone() {
331            q.partition = p;
332        }
333        if let Some(p) = self.reader_data_lifecycle {
334            q.reader_data_lifecycle = p;
335        }
336        if let Some(p) = self.user_data.clone() {
337            q.user_data = p;
338        }
339        if let Some(p) = self.topic_data.clone() {
340            q.topic_data = p;
341        }
342        if let Some(p) = self.group_data.clone() {
343            q.group_data = p;
344        }
345        q
346    }
347}
348
349/// Topic filter match (DDS-XML 1.0 §7.3.2.5).
350///
351/// POSIX-fnmatch style with `*` (zero or more characters) and `?` (exactly
352/// one character). Used to check whether a
353/// `<topic_filter>foo_*</topic_filter>` covers a concrete topic name.
354///
355/// Implemented via dynamic programming (see the duplicate in
356/// `crates/security-permissions/src/topic_match.rs` — we duplicate
357/// deliberately, to avoid creating a `zerodds-xml → zerodds-security-permissions`
358/// dependency).
359#[must_use]
360pub fn topic_filter_matches(filter: &str, topic_name: &str) -> bool {
361    let p: Vec<char> = filter.chars().collect();
362    let n: Vec<char> = topic_name.chars().collect();
363    let (m, k) = (n.len(), p.len());
364    let mut dp = alloc::vec![alloc::vec![false; k + 1]; m + 1];
365    dp[0][0] = true;
366    for j in 1..=k {
367        if p[j - 1] == '*' {
368            dp[0][j] = dp[0][j - 1];
369        }
370    }
371    for i in 1..=m {
372        for j in 1..=k {
373            let pc = p[j - 1];
374            dp[i][j] = if pc == '*' {
375                dp[i - 1][j] || dp[i][j - 1]
376            } else if pc == '?' || pc == n[i - 1] {
377                dp[i - 1][j - 1]
378            } else {
379                false
380            };
381        }
382    }
383    dp[m][k]
384}
385
386#[cfg(test)]
387#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
388mod tests {
389    use super::*;
390    use zerodds_qos::{DurabilityKind, ReliabilityKind};
391
392    #[test]
393    fn entity_qos_default_is_all_none() {
394        let e = EntityQos::default();
395        assert!(e.durability.is_none());
396        assert!(e.reliability.is_none());
397        assert!(e.history.is_none());
398        assert!(e.partition.is_none());
399        assert!(e.user_data.is_none());
400    }
401
402    #[test]
403    fn entity_qos_into_writer_uses_defaults_for_unset() {
404        let e = EntityQos::default();
405        let wq = e.into_writer_qos();
406        // Writer-Default: Reliable.
407        assert_eq!(wq.reliability.kind, ReliabilityKind::Reliable);
408    }
409
410    #[test]
411    fn entity_qos_into_reader_uses_defaults_for_unset() {
412        let e = EntityQos::default();
413        let rq = e.into_reader_qos();
414        // Reader-Default: BestEffort.
415        assert_eq!(rq.reliability.kind, ReliabilityKind::BestEffort);
416    }
417
418    #[test]
419    fn merge_override_replaces_field() {
420        let parent = EntityQos {
421            durability: Some(DurabilityQosPolicy {
422                kind: DurabilityKind::Volatile,
423            }),
424            history: Some(HistoryQosPolicy::default()),
425            ..Default::default()
426        };
427        let child = EntityQos {
428            durability: Some(DurabilityQosPolicy {
429                kind: DurabilityKind::Persistent,
430            }),
431            ..Default::default()
432        };
433
434        let merged = parent.merge(&child);
435        assert_eq!(
436            merged.durability.unwrap().kind,
437            DurabilityKind::Persistent,
438            "child override should win"
439        );
440        assert!(merged.history.is_some(), "parent's history should be kept");
441    }
442
443    #[test]
444    fn merge_none_does_not_clobber() {
445        let parent = EntityQos {
446            deadline: Some(DeadlineQosPolicy::default()),
447            ..Default::default()
448        };
449        let child = EntityQos::default();
450        let merged = parent.merge(&child);
451        assert!(
452            merged.deadline.is_some(),
453            "child=None should not clobber parent"
454        );
455    }
456
457    #[test]
458    fn library_profile_lookup() {
459        let lib = QosLibrary {
460            name: "L".into(),
461            profiles: alloc::vec![
462                QosProfile {
463                    name: "A".into(),
464                    ..Default::default()
465                },
466                QosProfile {
467                    name: "B".into(),
468                    ..Default::default()
469                }
470            ],
471        };
472        assert!(lib.profile("A").is_some());
473        assert!(lib.profile("B").is_some());
474        assert!(lib.profile("Missing").is_none());
475    }
476
477    // ---- Topic-Filter-Match ------------------------------------------
478
479    #[test]
480    fn glob_star_matches_all() {
481        assert!(topic_filter_matches("*", "foo"));
482        assert!(topic_filter_matches("*", ""));
483    }
484
485    #[test]
486    fn glob_prefix() {
487        assert!(topic_filter_matches("foo_*", "foo_bar"));
488        assert!(topic_filter_matches("foo_*", "foo_"));
489        assert!(!topic_filter_matches("foo_*", "bar_foo"));
490    }
491
492    #[test]
493    fn glob_question_mark() {
494        assert!(topic_filter_matches("s?nsor", "sensor"));
495        assert!(!topic_filter_matches("s?nsor", "snsor"));
496        assert!(!topic_filter_matches("s?nsor", "sennsor"));
497    }
498
499    #[test]
500    fn glob_exact() {
501        assert!(topic_filter_matches("Chatter", "Chatter"));
502        assert!(!topic_filter_matches("Chatter", "ChatterX"));
503    }
504}