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}