Skip to main content

zerodds_xml/
qos.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Datenmodell fuer die DDS-XML 1.0 §7.3.2 QoS-Profile-Library.
4//!
5//! Spec-Quelle: 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//! # Schicht-Disziplin
9//!
10//! Die einzelnen Policy-Strukturen (`DurabilityQosPolicy`, …) und die
11//! Aggregat-Container (`WriterQos`, `ReaderQos`) leben im Crate
12//! [`zerodds_qos`] (Wire-Format-Quelle). `zerodds-xml` re-nutzt sie hier
13//! direkt — *keine Duplikate*.
14//!
15//! Die XML-spezifischen Container [`EntityQos`] (sechs Auspraegungen
16//! fuer DataWriter/DataReader/Topic/Publisher/Subscriber/
17//! DomainParticipant) tragen pro Policy ein `Option<…>`. `None` =
18//! "Spec-Default" (uebernommen aus `zerodds-qos::*::Default`); `Some(p)` =
19//! im XML explizit gesetzt. Die Override-Semantik der Inheritance
20//! (siehe [`crate::qos_inheritance`]) operiert auf diesen Optionen:
21//! ein Kind-Profile mit `Some(p)` ueberschreibt das geerbte `Some/None`,
22//! ein Kind-Profile mit `None` erbt unveraendert.
23//!
24//! # XML-Element zu 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 fuer 1+ QoS-Profile gemaess DDS-XML 1.0 §7.3.2.
75///
76/// Mehrere Libraries duerfen pro Dokument vorkommen; jede Library traegt
77/// einen `name`-Attribut. Lookups quer ueber Bibliotheken erfolgen via
78/// 2-Segment-Pfad `library::profile`.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct QosLibrary {
81    /// Name der Library (`<qos_library name="…">`-Attribut).
82    pub name: String,
83    /// Profile in dieser Library, in Dokument-Reihenfolge.
84    pub profiles: Vec<QosProfile>,
85}
86
87impl QosLibrary {
88    /// Sucht ein Profile innerhalb dieser Library nach Namen.
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/// Ein einzelnes `<qos_profile>`-Element (§7.3.2.4).
96///
97/// Jeder `EntityQos`-Container ist `Option<…>` — `None` heisst "im XML
98/// nicht aufgefuehrt", was beim Resolve in den Spec-Default-Aggregat-Typ
99/// uebergeht (siehe [`crate::qos_inheritance::resolve_profile`]).
100#[derive(Debug, Clone, Default, PartialEq, Eq)]
101pub struct QosProfile {
102    /// Name des Profile (`name`-Attribut).
103    pub name: String,
104    /// Optionaler `base_name` fuer Inheritance (Spec §7.3.2.4.2). Format:
105    /// entweder `"profile"` (innerhalb derselben Library) oder
106    /// `"library::profile"` (cross-Library-Referenz).
107    pub base_name: Option<String>,
108    /// Optionaler Topic-Filter (Glob, `*`/`?`) zur Profile-zu-Topic-
109    /// Bindung. Mehrere `<topic_filter>` werden zu **einem** Pattern
110    /// zusammengezogen (letztes gewinnt) — Spec deckt nur einen Filter
111    /// pro Profile sinnvoll ab.
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-pro-Policy-Container fuer alle 22 OMG-DDS-1.4-QoS-Policies.
128///
129/// Ein nicht-gesetztes `Option` heisst: im XML nicht aufgefuehrt → Spec-
130/// Default beim Resolve. Wird *als ein Container fuer alle 6 Entity-Typen
131/// wiederverwendet* — DDS-XML 1.0 erlaubt grundsaetzlich alle Policies in
132/// allen 6 Containern; semantische Filterung (z.B. dass
133/// `OwnershipStrength` nur am Writer Sinn ergibt) findet beim Materialisieren
134/// in `WriterQos`/`ReaderQos` statt (siehe [`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    /// Mergt `override_` ueber `self`: jede explizit gesetzte Policy in
186    /// `override_` ueberschreibt die in `self`. Kommutativ ist die
187    /// Operation **nicht** — Reihenfolge: `parent.merge(child)` heisst
188    /// "Child gewinnt, wo Child explizit gesetzt".
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    /// Materialisiert einen `WriterQos` aus den im `EntityQos` gesetzten
224    /// Policies. `None`-Eintraege werden mit den Spec-Defaults aus
225    /// `zerodds_qos::WriterQos::default()` aufgefuellt.
226    ///
227    /// Policies, die fuer Writer keinen Sinn ergeben (z.B.
228    /// `time_based_filter`, `reader_data_lifecycle`), werden hier
229    /// bewusst ignoriert — Sie werden nur fuer ReaderQos materialisiert.
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    /// Materialisiert einen `ReaderQos` analog zu [`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 mit `*` (null oder mehr Zeichen) und `?` (genau
352/// ein Zeichen). Wird genutzt um zu pruefen ob ein
353/// `<topic_filter>foo_*</topic_filter>` einen konkreten Topic-Namen
354/// abdeckt.
355///
356/// Implementiert per dynamischer Programmierung (siehe Duplikat in
357/// `crates/security-permissions/src/topic_match.rs` — wir duplizieren
358/// bewusst, um keine `zerodds-xml → zerodds-security-permissions`-Dep zu
359/// erzeugen).
360#[must_use]
361pub fn topic_filter_matches(filter: &str, topic_name: &str) -> bool {
362    let p: Vec<char> = filter.chars().collect();
363    let n: Vec<char> = topic_name.chars().collect();
364    let (m, k) = (n.len(), p.len());
365    let mut dp = alloc::vec![alloc::vec![false; k + 1]; m + 1];
366    dp[0][0] = true;
367    for j in 1..=k {
368        if p[j - 1] == '*' {
369            dp[0][j] = dp[0][j - 1];
370        }
371    }
372    for i in 1..=m {
373        for j in 1..=k {
374            let pc = p[j - 1];
375            dp[i][j] = if pc == '*' {
376                dp[i - 1][j] || dp[i][j - 1]
377            } else if pc == '?' || pc == n[i - 1] {
378                dp[i - 1][j - 1]
379            } else {
380                false
381            };
382        }
383    }
384    dp[m][k]
385}
386
387#[cfg(test)]
388#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
389mod tests {
390    use super::*;
391    use zerodds_qos::{DurabilityKind, ReliabilityKind};
392
393    #[test]
394    fn entity_qos_default_is_all_none() {
395        let e = EntityQos::default();
396        assert!(e.durability.is_none());
397        assert!(e.reliability.is_none());
398        assert!(e.history.is_none());
399        assert!(e.partition.is_none());
400        assert!(e.user_data.is_none());
401    }
402
403    #[test]
404    fn entity_qos_into_writer_uses_defaults_for_unset() {
405        let e = EntityQos::default();
406        let wq = e.into_writer_qos();
407        // Writer-Default: Reliable.
408        assert_eq!(wq.reliability.kind, ReliabilityKind::Reliable);
409    }
410
411    #[test]
412    fn entity_qos_into_reader_uses_defaults_for_unset() {
413        let e = EntityQos::default();
414        let rq = e.into_reader_qos();
415        // Reader-Default: BestEffort.
416        assert_eq!(rq.reliability.kind, ReliabilityKind::BestEffort);
417    }
418
419    #[test]
420    fn merge_override_replaces_field() {
421        let parent = EntityQos {
422            durability: Some(DurabilityQosPolicy {
423                kind: DurabilityKind::Volatile,
424            }),
425            history: Some(HistoryQosPolicy::default()),
426            ..Default::default()
427        };
428        let child = EntityQos {
429            durability: Some(DurabilityQosPolicy {
430                kind: DurabilityKind::Persistent,
431            }),
432            ..Default::default()
433        };
434
435        let merged = parent.merge(&child);
436        assert_eq!(
437            merged.durability.unwrap().kind,
438            DurabilityKind::Persistent,
439            "child override should win"
440        );
441        assert!(merged.history.is_some(), "parent's history should be kept");
442    }
443
444    #[test]
445    fn merge_none_does_not_clobber() {
446        let parent = EntityQos {
447            deadline: Some(DeadlineQosPolicy::default()),
448            ..Default::default()
449        };
450        let child = EntityQos::default();
451        let merged = parent.merge(&child);
452        assert!(
453            merged.deadline.is_some(),
454            "child=None should not clobber parent"
455        );
456    }
457
458    #[test]
459    fn library_profile_lookup() {
460        let lib = QosLibrary {
461            name: "L".into(),
462            profiles: alloc::vec![
463                QosProfile {
464                    name: "A".into(),
465                    ..Default::default()
466                },
467                QosProfile {
468                    name: "B".into(),
469                    ..Default::default()
470                }
471            ],
472        };
473        assert!(lib.profile("A").is_some());
474        assert!(lib.profile("B").is_some());
475        assert!(lib.profile("Missing").is_none());
476    }
477
478    // ---- Topic-Filter-Match ------------------------------------------
479
480    #[test]
481    fn glob_star_matches_all() {
482        assert!(topic_filter_matches("*", "foo"));
483        assert!(topic_filter_matches("*", ""));
484    }
485
486    #[test]
487    fn glob_prefix() {
488        assert!(topic_filter_matches("foo_*", "foo_bar"));
489        assert!(topic_filter_matches("foo_*", "foo_"));
490        assert!(!topic_filter_matches("foo_*", "bar_foo"));
491    }
492
493    #[test]
494    fn glob_question_mark() {
495        assert!(topic_filter_matches("s?nsor", "sensor"));
496        assert!(!topic_filter_matches("s?nsor", "snsor"));
497        assert!(!topic_filter_matches("s?nsor", "sennsor"));
498    }
499
500    #[test]
501    fn glob_exact() {
502        assert!(topic_filter_matches("Chatter", "Chatter"));
503        assert!(!topic_filter_matches("Chatter", "ChatterX"));
504    }
505}