1use 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
49pub 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
72pub 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
86pub 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 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
119fn 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 _ => {}
194 }
195 }
196 Ok(profile)
197}
198
199pub 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 _ => {}
241 }
242 }
243 Ok(q)
244}
245
246pub 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
266fn parse_duration_element(el: &XmlElement) -> Result<QosDuration, XmlError> {
280 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 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 if sec == DURATION_INFINITE_SEC
306 && (nsec == DURATION_INFINITE_NSEC || el.child("nanosec").is_none())
307 {
308 return Ok(QosDuration::INFINITE);
309 }
310
311 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
330fn 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 "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 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 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
630fn 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
662fn 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 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 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 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 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 #[test]
813 fn single_qos_shortcut_datawriter_creates_implicit_profile() {
814 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 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 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 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 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}