1extern crate alloc;
62use alloc::format;
63use alloc::string::{String, ToString};
64use alloc::vec;
65use alloc::vec::Vec;
66use core::fmt;
67
68use zerodds_xml::{DdsXmlDocument, XmlElement, XmlError, parse_xml_tree};
69
70use crate::error::XrceError;
71use crate::object_id::ObjectId;
72use crate::object_kind::ObjectKind;
73use crate::object_repr::ObjectVariant;
74use crate::submessages::create::CreatePayload;
75
76pub const MAX_HIERARCHY_DEPTH: usize = 8;
80
81pub const MAX_TYPES_PER_FILE: usize = 256;
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum XrceXmlError {
87 InvalidXml(String),
89 UnexpectedRoot(String),
91 MissingAttribute {
93 element: String,
95 attribute: String,
97 },
98 InvalidAttribute {
100 element: String,
102 attribute: String,
104 value: String,
106 },
107 DuplicateObjectId(ObjectId),
110 UnresolvedTopicRef {
113 endpoint: ObjectId,
115 topic: ObjectId,
117 },
118 UnresolvedTypeName {
121 topic: ObjectId,
123 type_name: String,
125 },
126 UnresolvedQosProfile(String),
129 HierarchyTooDeep(usize),
131 TooManyTypes(usize),
133 CircularType(String),
136 DomainIdOutOfRange(u64),
139 ObjectKindMismatch {
143 id: ObjectId,
145 expected: u8,
147 actual: u8,
149 },
150 Wire(XrceError),
152}
153
154impl fmt::Display for XrceXmlError {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 match self {
157 Self::InvalidXml(msg) => write!(f, "invalid XRCE-XML: {msg}"),
158 Self::UnexpectedRoot(name) => {
159 write!(f, "expected <dds> root, got <{name}>")
160 }
161 Self::MissingAttribute { element, attribute } => {
162 write!(f, "<{element}> missing required attribute `{attribute}`")
163 }
164 Self::InvalidAttribute {
165 element,
166 attribute,
167 value,
168 } => write!(
169 f,
170 "<{element}> attribute `{attribute}` has invalid value `{value}`"
171 ),
172 Self::DuplicateObjectId(id) => {
173 write!(f, "duplicate ObjectId 0x{:04X}", id.raw())
174 }
175 Self::UnresolvedTopicRef { endpoint, topic } => write!(
176 f,
177 "endpoint 0x{:04X} references undefined topic 0x{:04X}",
178 endpoint.raw(),
179 topic.raw()
180 ),
181 Self::UnresolvedTypeName { topic, type_name } => write!(
182 f,
183 "topic 0x{:04X} references undefined type `{type_name}`",
184 topic.raw()
185 ),
186 Self::UnresolvedQosProfile(name) => {
187 write!(f, "unresolved QoS-profile reference `{name}`")
188 }
189 Self::HierarchyTooDeep(depth) => {
190 write!(f, "XRCE hierarchy nesting exceeds limit (depth={depth})")
191 }
192 Self::TooManyTypes(count) => {
193 write!(f, "too many type definitions (count={count})")
194 }
195 Self::CircularType(name) => {
196 write!(f, "circular type definition involving `{name}`")
197 }
198 Self::DomainIdOutOfRange(v) => {
199 write!(f, "domain_id {v} out of range (must fit i32)")
200 }
201 Self::ObjectKindMismatch {
202 id,
203 expected,
204 actual,
205 } => write!(
206 f,
207 "ObjectId 0x{:04X}: expected kind 0x{expected:X}, got 0x{actual:X}",
208 id.raw()
209 ),
210 Self::Wire(e) => write!(f, "xrce wire error: {e}"),
211 }
212 }
213}
214
215#[cfg(feature = "std")]
216impl std::error::Error for XrceXmlError {}
217
218impl From<XmlError> for XrceXmlError {
219 fn from(e: XmlError) -> Self {
220 Self::InvalidXml(e.to_string())
221 }
222}
223
224impl From<XrceError> for XrceXmlError {
225 fn from(e: XrceError) -> Self {
226 Self::Wire(e)
227 }
228}
229
230#[derive(Debug, Clone, Default, PartialEq, Eq)]
232pub struct XrceConfig {
233 pub types: Vec<TypeConfig>,
237 pub participants: Vec<ParticipantConfig>,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
243pub struct TypeConfig {
244 pub object_id: ObjectId,
249 pub name: String,
251 pub declared_names: Vec<String>,
256 pub xml: String,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct ParticipantConfig {
265 pub object_id: ObjectId,
267 pub domain_id: u32,
269 pub topics: Vec<TopicConfig>,
271 pub publishers: Vec<PublisherConfig>,
273 pub subscribers: Vec<SubscriberConfig>,
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
279pub struct TopicConfig {
280 pub object_id: ObjectId,
282 pub name: String,
284 pub type_name: String,
287 pub qos_profile: Option<String>,
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct PublisherConfig {
294 pub object_id: ObjectId,
296 pub data_writers: Vec<DataWriterConfig>,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct SubscriberConfig {
303 pub object_id: ObjectId,
305 pub data_readers: Vec<DataReaderConfig>,
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct DataWriterConfig {
312 pub object_id: ObjectId,
314 pub topic_ref: ObjectId,
316 pub qos_profile: Option<String>,
318}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct DataReaderConfig {
323 pub object_id: ObjectId,
325 pub topic_ref: ObjectId,
327 pub qos_profile: Option<String>,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq)]
335pub struct CreateMessage {
336 pub object_id: ObjectId,
338 pub kind: ObjectKind,
340 pub payload: CreatePayload,
344}
345
346pub trait QosProfileResolver {
351 fn resolve(&self, path: &str) -> Option<String>;
355}
356
357#[derive(Debug, Default, Clone)]
360pub struct InMemoryQosResolver {
361 pub profiles: alloc::collections::BTreeMap<String, String>,
363}
364
365impl InMemoryQosResolver {
366 #[must_use]
368 pub fn new() -> Self {
369 Self::default()
370 }
371 pub fn add<P: Into<String>, X: Into<String>>(&mut self, path: P, xml: X) {
373 self.profiles.insert(path.into(), xml.into());
374 }
375}
376
377impl QosProfileResolver for InMemoryQosResolver {
378 fn resolve(&self, path: &str) -> Option<String> {
379 self.profiles.get(path).cloned()
380 }
381}
382
383pub fn load_xrce_config(xml: &str) -> Result<XrceConfig, XrceXmlError> {
396 let doc: DdsXmlDocument = parse_xml_tree(xml)?;
397 if doc.root.name != "dds" {
398 return Err(XrceXmlError::UnexpectedRoot(doc.root.name.clone()));
399 }
400 XrceConfig::from_root(&doc.root)
401}
402
403#[cfg(feature = "std")]
409pub fn load_xrce_config_from_file(path: &std::path::Path) -> Result<XrceConfig, XrceXmlError> {
410 let xml = std::fs::read_to_string(path).map_err(|e| {
411 XrceXmlError::InvalidXml(format!("io error reading `{}`: {}", path.display(), e))
412 })?;
413 load_xrce_config(&xml)
414}
415
416impl XrceConfig {
417 fn from_root(root: &XmlElement) -> Result<Self, XrceXmlError> {
419 let mut cfg = Self::default();
420 let mut seen_type_names: alloc::collections::BTreeSet<String> =
421 alloc::collections::BTreeSet::new();
422 for type_el in root.children_named("type") {
423 if cfg.types.len() >= MAX_TYPES_PER_FILE {
424 return Err(XrceXmlError::TooManyTypes(cfg.types.len() + 1));
425 }
426 let tc = TypeConfig::from_element(type_el, &mut seen_type_names)?;
427 cfg.types.push(tc);
428 }
429 check_type_cycles(&cfg.types)?;
431
432 let mut global_ids: alloc::collections::BTreeSet<ObjectId> =
433 alloc::collections::BTreeSet::new();
434 for tc in &cfg.types {
435 if !global_ids.insert(tc.object_id) {
436 return Err(XrceXmlError::DuplicateObjectId(tc.object_id));
437 }
438 }
439
440 for p_el in root.children_named("participant") {
441 let part = ParticipantConfig::from_element(p_el, 1, &cfg.types, &mut global_ids)?;
442 cfg.participants.push(part);
443 }
444 Ok(cfg)
445 }
446
447 pub fn to_create_messages(&self) -> Result<Vec<CreateMessage>, XrceXmlError> {
457 let mut out = Vec::new();
458 for tc in &self.types {
460 out.push(create_message(tc.object_id, ObjectKind::Type, &tc.xml)?);
461 }
462 for p in &self.participants {
464 let xml = format!(
465 "<participant><domain_id>{}</domain_id></participant>",
466 p.domain_id
467 );
468 out.push(create_message(p.object_id, ObjectKind::Participant, &xml)?);
469 }
470 for p in &self.participants {
472 for t in &p.topics {
473 let qos = t
474 .qos_profile
475 .as_deref()
476 .map(|q| format!(" qos_profile=\"{q}\""))
477 .unwrap_or_default();
478 let xml = format!(
479 "<topic name=\"{}\" type_name=\"{}\"{}/>",
480 escape_xml_attr(&t.name),
481 escape_xml_attr(&t.type_name),
482 qos
483 );
484 out.push(create_message(t.object_id, ObjectKind::Topic, &xml)?);
485 }
486 }
487 for p in &self.participants {
489 for pub_ in &p.publishers {
490 out.push(create_message(
491 pub_.object_id,
492 ObjectKind::Publisher,
493 "<publisher/>",
494 )?);
495 }
496 }
497 for p in &self.participants {
499 for sub in &p.subscribers {
500 out.push(create_message(
501 sub.object_id,
502 ObjectKind::Subscriber,
503 "<subscriber/>",
504 )?);
505 }
506 }
507 for p in &self.participants {
509 for pub_ in &p.publishers {
510 for dw in &pub_.data_writers {
511 let qos = dw
512 .qos_profile
513 .as_deref()
514 .map(|q| format!(" qos_profile=\"{q}\""))
515 .unwrap_or_default();
516 let xml = format!(
517 "<data_writer topic_ref=\"0x{:04X}\"{}/>",
518 dw.topic_ref.raw(),
519 qos
520 );
521 out.push(create_message(dw.object_id, ObjectKind::DataWriter, &xml)?);
522 }
523 }
524 }
525 for p in &self.participants {
527 for sub in &p.subscribers {
528 for dr in &sub.data_readers {
529 let qos = dr
530 .qos_profile
531 .as_deref()
532 .map(|q| format!(" qos_profile=\"{q}\""))
533 .unwrap_or_default();
534 let xml = format!(
535 "<data_reader topic_ref=\"0x{:04X}\"{}/>",
536 dr.topic_ref.raw(),
537 qos
538 );
539 out.push(create_message(dr.object_id, ObjectKind::DataReader, &xml)?);
540 }
541 }
542 }
543 Ok(out)
544 }
545
546 pub fn resolve_qos_profile<R: QosProfileResolver>(
555 &self,
556 path: &str,
557 resolver: &R,
558 ) -> Result<String, XrceXmlError> {
559 resolver
560 .resolve(path)
561 .ok_or_else(|| XrceXmlError::UnresolvedQosProfile(path.to_string()))
562 }
563
564 #[must_use]
566 pub fn qos_profile_refs(&self) -> Vec<String> {
567 let mut out: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
568 for p in &self.participants {
569 for t in &p.topics {
570 if let Some(q) = &t.qos_profile {
571 out.insert(q.clone());
572 }
573 }
574 for pub_ in &p.publishers {
575 for dw in &pub_.data_writers {
576 if let Some(q) = &dw.qos_profile {
577 out.insert(q.clone());
578 }
579 }
580 }
581 for sub in &p.subscribers {
582 for dr in &sub.data_readers {
583 if let Some(q) = &dr.qos_profile {
584 out.insert(q.clone());
585 }
586 }
587 }
588 }
589 out.into_iter().collect()
590 }
591}
592
593impl TypeConfig {
594 fn from_element(
595 el: &XmlElement,
596 seen: &mut alloc::collections::BTreeSet<String>,
597 ) -> Result<Self, XrceXmlError> {
598 let object_id = parse_object_id_attr(el, "object_id", ObjectKind::Type)?;
599 let name = if let Some(n) = el.attribute("name") {
602 n.to_string()
603 } else if let Some(child) = el.children.first() {
604 child
605 .attribute("name")
606 .ok_or_else(|| XrceXmlError::MissingAttribute {
607 element: "type".to_string(),
608 attribute: "name".to_string(),
609 })?
610 .to_string()
611 } else {
612 return Err(XrceXmlError::MissingAttribute {
613 element: "type".to_string(),
614 attribute: "name".to_string(),
615 });
616 };
617 let mut declared = Vec::new();
619 collect_declared_types(el, &mut declared);
620 if declared.is_empty() {
621 declared.push(name.clone());
622 }
623 for d in &declared {
624 if !seen.insert(d.clone()) {
625 return Err(XrceXmlError::CircularType(d.clone()));
626 }
627 }
628 let xml = serialize_element(el);
629 Ok(Self {
630 object_id,
631 name,
632 declared_names: declared,
633 xml,
634 })
635 }
636
637 fn referenced_types(&self) -> Vec<String> {
640 let Ok(doc) = parse_xml_tree(&self.xml) else {
641 return Vec::new();
642 };
643 collect_member_types(&doc.root)
644 }
645}
646
647fn collect_declared_types(el: &XmlElement, out: &mut Vec<String>) {
655 for child in &el.children {
656 match child.name.as_str() {
657 "struct" | "enum" | "union" | "typedef" | "bitmask" | "bitset" => {
658 if let Some(n) = child.attribute("name") {
659 out.push(n.to_string());
660 }
661 collect_declared_types(child, out);
662 }
663 "module" => {
664 if let Some(n) = child.attribute("name") {
665 out.push(n.to_string());
666 }
667 collect_declared_types(child, out);
668 }
669 _ => collect_declared_types(child, out),
670 }
671 }
672}
673
674fn collect_member_types(el: &XmlElement) -> Vec<String> {
679 let mut out = Vec::new();
680 for child in &el.children {
681 if child.name == "member" || child.name == "case" {
682 if let Some(t) = child.attribute("type") {
683 out.push(t.to_string());
684 }
685 }
686 out.extend(collect_member_types(child));
687 }
688 out
689}
690
691fn check_type_cycles(types: &[TypeConfig]) -> Result<(), XrceXmlError> {
692 use alloc::collections::BTreeMap;
693 use alloc::collections::BTreeSet;
694 let mut graph: BTreeMap<&str, Vec<String>> = BTreeMap::new();
695 for t in types {
696 graph.insert(t.name.as_str(), t.referenced_types());
697 }
698 for start in types.iter().map(|t| t.name.as_str()) {
700 let mut stack: Vec<&str> = vec![start];
701 let mut on_path: BTreeSet<&str> = BTreeSet::new();
702 let mut visited: BTreeSet<&str> = BTreeSet::new();
703 while let Some(node) = stack.last().copied() {
704 if !visited.contains(node) {
705 visited.insert(node);
706 on_path.insert(node);
707 }
708 let mut pushed = false;
709 if let Some(neigh) = graph.get(node) {
710 for n in neigh {
711 let n_str = n.as_str();
712 if on_path.contains(n_str) {
713 return Err(XrceXmlError::CircularType(n.clone()));
714 }
715 if !visited.contains(n_str) && graph.contains_key(n_str) {
716 stack.push(n_str);
717 pushed = true;
718 break;
719 }
720 }
721 }
722 if !pushed {
723 on_path.remove(node);
724 stack.pop();
725 }
726 }
727 }
728 Ok(())
729}
730
731impl ParticipantConfig {
732 fn from_element(
733 el: &XmlElement,
734 depth: usize,
735 types: &[TypeConfig],
736 global_ids: &mut alloc::collections::BTreeSet<ObjectId>,
737 ) -> Result<Self, XrceXmlError> {
738 if depth > MAX_HIERARCHY_DEPTH {
739 return Err(XrceXmlError::HierarchyTooDeep(depth));
740 }
741 let object_id = parse_object_id_attr(el, "object_id", ObjectKind::Participant)?;
742 if !global_ids.insert(object_id) {
743 return Err(XrceXmlError::DuplicateObjectId(object_id));
744 }
745 let domain_id = parse_domain_id_attr(el)?;
746
747 let mut topics = Vec::new();
748 let mut topic_ids: alloc::collections::BTreeSet<ObjectId> =
749 alloc::collections::BTreeSet::new();
750 for t_el in el.children_named("topic") {
751 let t = TopicConfig::from_element(t_el, types)?;
752 if !global_ids.insert(t.object_id) || !topic_ids.insert(t.object_id) {
753 return Err(XrceXmlError::DuplicateObjectId(t.object_id));
754 }
755 topics.push(t);
756 }
757
758 let mut publishers = Vec::new();
759 for p_el in el.children_named("publisher") {
760 let p = PublisherConfig::from_element(p_el, depth + 1, &topic_ids, global_ids)?;
761 publishers.push(p);
762 }
763
764 let mut subscribers = Vec::new();
765 for s_el in el.children_named("subscriber") {
766 let s = SubscriberConfig::from_element(s_el, depth + 1, &topic_ids, global_ids)?;
767 subscribers.push(s);
768 }
769
770 Ok(Self {
771 object_id,
772 domain_id,
773 topics,
774 publishers,
775 subscribers,
776 })
777 }
778}
779
780impl TopicConfig {
781 fn from_element(el: &XmlElement, types: &[TypeConfig]) -> Result<Self, XrceXmlError> {
782 let object_id = parse_object_id_attr(el, "object_id", ObjectKind::Topic)?;
783 let name = required_attr(el, "name")?;
784 let type_name = required_attr(el, "type_name")?;
785 let known = types
789 .iter()
790 .any(|t| t.name == type_name || t.declared_names.iter().any(|d| d == &type_name));
791 if !known {
792 return Err(XrceXmlError::UnresolvedTypeName {
793 topic: object_id,
794 type_name,
795 });
796 }
797 let qos_profile = el.attribute("qos_profile").map(ToString::to_string);
798 Ok(Self {
799 object_id,
800 name,
801 type_name,
802 qos_profile,
803 })
804 }
805}
806
807impl PublisherConfig {
808 fn from_element(
809 el: &XmlElement,
810 depth: usize,
811 topic_ids: &alloc::collections::BTreeSet<ObjectId>,
812 global_ids: &mut alloc::collections::BTreeSet<ObjectId>,
813 ) -> Result<Self, XrceXmlError> {
814 if depth > MAX_HIERARCHY_DEPTH {
815 return Err(XrceXmlError::HierarchyTooDeep(depth));
816 }
817 let object_id = parse_object_id_attr(el, "object_id", ObjectKind::Publisher)?;
818 if !global_ids.insert(object_id) {
819 return Err(XrceXmlError::DuplicateObjectId(object_id));
820 }
821 let mut data_writers = Vec::new();
822 for dw_el in el.children_named("data_writer") {
823 let dw = DataWriterConfig::from_element(dw_el, topic_ids)?;
824 if !global_ids.insert(dw.object_id) {
825 return Err(XrceXmlError::DuplicateObjectId(dw.object_id));
826 }
827 data_writers.push(dw);
828 }
829 Ok(Self {
830 object_id,
831 data_writers,
832 })
833 }
834}
835
836impl SubscriberConfig {
837 fn from_element(
838 el: &XmlElement,
839 depth: usize,
840 topic_ids: &alloc::collections::BTreeSet<ObjectId>,
841 global_ids: &mut alloc::collections::BTreeSet<ObjectId>,
842 ) -> Result<Self, XrceXmlError> {
843 if depth > MAX_HIERARCHY_DEPTH {
844 return Err(XrceXmlError::HierarchyTooDeep(depth));
845 }
846 let object_id = parse_object_id_attr(el, "object_id", ObjectKind::Subscriber)?;
847 if !global_ids.insert(object_id) {
848 return Err(XrceXmlError::DuplicateObjectId(object_id));
849 }
850 let mut data_readers = Vec::new();
851 for dr_el in el.children_named("data_reader") {
852 let dr = DataReaderConfig::from_element(dr_el, topic_ids)?;
853 if !global_ids.insert(dr.object_id) {
854 return Err(XrceXmlError::DuplicateObjectId(dr.object_id));
855 }
856 data_readers.push(dr);
857 }
858 Ok(Self {
859 object_id,
860 data_readers,
861 })
862 }
863}
864
865impl DataWriterConfig {
866 fn from_element(
867 el: &XmlElement,
868 topic_ids: &alloc::collections::BTreeSet<ObjectId>,
869 ) -> Result<Self, XrceXmlError> {
870 let object_id = parse_object_id_attr(el, "object_id", ObjectKind::DataWriter)?;
871 let topic_ref = parse_object_id_attr(el, "topic_ref", ObjectKind::Topic)?;
872 if !topic_ids.contains(&topic_ref) {
873 return Err(XrceXmlError::UnresolvedTopicRef {
874 endpoint: object_id,
875 topic: topic_ref,
876 });
877 }
878 let qos_profile = el.attribute("qos_profile").map(ToString::to_string);
879 Ok(Self {
880 object_id,
881 topic_ref,
882 qos_profile,
883 })
884 }
885}
886
887impl DataReaderConfig {
888 fn from_element(
889 el: &XmlElement,
890 topic_ids: &alloc::collections::BTreeSet<ObjectId>,
891 ) -> Result<Self, XrceXmlError> {
892 let object_id = parse_object_id_attr(el, "object_id", ObjectKind::DataReader)?;
893 let topic_ref = parse_object_id_attr(el, "topic_ref", ObjectKind::Topic)?;
894 if !topic_ids.contains(&topic_ref) {
895 return Err(XrceXmlError::UnresolvedTopicRef {
896 endpoint: object_id,
897 topic: topic_ref,
898 });
899 }
900 let qos_profile = el.attribute("qos_profile").map(ToString::to_string);
901 Ok(Self {
902 object_id,
903 topic_ref,
904 qos_profile,
905 })
906 }
907}
908
909fn required_attr(el: &XmlElement, name: &str) -> Result<String, XrceXmlError> {
910 el.attribute(name)
911 .map(ToString::to_string)
912 .ok_or_else(|| XrceXmlError::MissingAttribute {
913 element: el.name.clone(),
914 attribute: name.to_string(),
915 })
916}
917
918fn parse_object_id_attr(
919 el: &XmlElement,
920 attr: &str,
921 expected_kind: ObjectKind,
922) -> Result<ObjectId, XrceXmlError> {
923 let raw = required_attr(el, attr)?;
924 let parsed = parse_u16(&raw).ok_or_else(|| XrceXmlError::InvalidAttribute {
925 element: el.name.clone(),
926 attribute: attr.to_string(),
927 value: raw.clone(),
928 })?;
929 let id = ObjectId::from_raw(parsed);
930 let actual = (parsed & 0x000F) as u8;
931 if actual != expected_kind.to_u8() {
932 return Err(XrceXmlError::ObjectKindMismatch {
933 id,
934 expected: expected_kind.to_u8(),
935 actual,
936 });
937 }
938 Ok(id)
939}
940
941fn parse_domain_id_attr(el: &XmlElement) -> Result<u32, XrceXmlError> {
942 let raw = required_attr(el, "domain_id")?;
943 let parsed: u64 = raw.parse().map_err(|_| XrceXmlError::InvalidAttribute {
944 element: el.name.clone(),
945 attribute: "domain_id".to_string(),
946 value: raw.clone(),
947 })?;
948 if parsed > i32::MAX as u64 {
949 return Err(XrceXmlError::DomainIdOutOfRange(parsed));
950 }
951 Ok(parsed as u32)
952}
953
954fn parse_u16(s: &str) -> Option<u16> {
955 if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
956 u16::from_str_radix(rest, 16).ok()
957 } else {
958 s.parse().ok()
959 }
960}
961
962fn create_message(
963 object_id: ObjectId,
964 kind: ObjectKind,
965 xml: &str,
966) -> Result<CreateMessage, XrceXmlError> {
967 let variant = ObjectVariant::ByXmlString(xml.to_string());
968 let representation = variant.encode(crate::encoding::Endianness::Little)?;
969 let payload = CreatePayload {
970 representation,
971 reuse: false,
972 replace: false,
973 };
974 Ok(CreateMessage {
975 object_id,
976 kind,
977 payload,
978 })
979}
980
981fn escape_xml_attr(s: &str) -> String {
982 let mut out = String::with_capacity(s.len());
983 for c in s.chars() {
984 match c {
985 '<' => out.push_str("<"),
986 '>' => out.push_str(">"),
987 '&' => out.push_str("&"),
988 '"' => out.push_str("""),
989 '\'' => out.push_str("'"),
990 _ => out.push(c),
991 }
992 }
993 out
994}
995
996fn serialize_element(el: &XmlElement) -> String {
1002 let mut out = String::new();
1003 write_element(&mut out, el);
1004 out
1005}
1006
1007fn write_element(out: &mut String, el: &XmlElement) {
1012 out.push('<');
1013 out.push_str(&el.name);
1014 for (k, v) in &el.attributes {
1015 out.push(' ');
1016 out.push_str(k);
1017 out.push_str("=\"");
1018 out.push_str(&escape_xml_attr(v));
1019 out.push('"');
1020 }
1021 if el.children.is_empty() && el.text.is_empty() {
1022 out.push_str("/>");
1023 return;
1024 }
1025 out.push('>');
1026 if !el.text.is_empty() {
1027 for c in el.text.chars() {
1028 match c {
1029 '<' => out.push_str("<"),
1030 '>' => out.push_str(">"),
1031 '&' => out.push_str("&"),
1032 _ => out.push(c),
1033 }
1034 }
1035 }
1036 for child in &el.children {
1037 write_element(out, child);
1038 }
1039 out.push_str("</");
1040 out.push_str(&el.name);
1041 out.push('>');
1042}
1043
1044#[cfg(test)]
1045#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
1046mod tests {
1047 use super::*;
1048
1049 fn cfg_basic() -> &'static str {
1050 r#"<dds>
1051 <type object_id="0x000A">
1052 <module name="ShapesDemoTypes">
1053 <struct name="ShapeType">
1054 <member name="color" type="string"/>
1055 </struct>
1056 </module>
1057 </type>
1058 <participant object_id="0xCAF1" domain_id="0">
1059 <topic object_id="0x0102" name="Square" type_name="ShapeType" qos_profile="Lib::ShapeProfile"/>
1060 <publisher object_id="0x0103">
1061 <data_writer object_id="0x0105" topic_ref="0x0102" qos_profile="Lib::ShapeProfile"/>
1062 </publisher>
1063 <subscriber object_id="0x0104">
1064 <data_reader object_id="0x0106" topic_ref="0x0102"/>
1065 </subscriber>
1066 </participant>
1067 </dds>"#
1068 }
1069
1070 #[test]
1073 fn roundtrip_basic_hierarchy_parses() {
1074 let cfg = load_xrce_config(cfg_basic()).expect("parse");
1075 assert_eq!(cfg.types.len(), 1);
1076 assert_eq!(cfg.types[0].name, "ShapesDemoTypes");
1077 assert_eq!(cfg.participants.len(), 1);
1078 let p = &cfg.participants[0];
1079 assert_eq!(p.domain_id, 0);
1080 assert_eq!(p.topics.len(), 1);
1081 assert_eq!(p.publishers.len(), 1);
1082 assert_eq!(p.subscribers.len(), 1);
1083 assert_eq!(p.publishers[0].data_writers.len(), 1);
1084 assert_eq!(p.subscribers[0].data_readers.len(), 1);
1085 }
1086
1087 #[test]
1088 fn roundtrip_object_ids_preserved() {
1089 let cfg = load_xrce_config(cfg_basic()).unwrap();
1090 assert_eq!(cfg.participants[0].object_id, ObjectId::from_raw(0xCAF1));
1091 assert_eq!(
1092 cfg.participants[0].topics[0].object_id,
1093 ObjectId::from_raw(0x0102)
1094 );
1095 }
1096
1097 #[test]
1098 fn roundtrip_qos_profile_carries_string() {
1099 let cfg = load_xrce_config(cfg_basic()).unwrap();
1100 assert_eq!(
1101 cfg.participants[0].topics[0].qos_profile.as_deref(),
1102 Some("Lib::ShapeProfile")
1103 );
1104 assert_eq!(
1105 cfg.participants[0].publishers[0].data_writers[0]
1106 .qos_profile
1107 .as_deref(),
1108 Some("Lib::ShapeProfile")
1109 );
1110 assert!(
1111 cfg.participants[0].subscribers[0].data_readers[0]
1112 .qos_profile
1113 .is_none()
1114 );
1115 }
1116
1117 #[test]
1118 fn roundtrip_topic_ref_preserved() {
1119 let cfg = load_xrce_config(cfg_basic()).unwrap();
1120 assert_eq!(
1121 cfg.participants[0].publishers[0].data_writers[0].topic_ref,
1122 ObjectId::from_raw(0x0102)
1123 );
1124 assert_eq!(
1125 cfg.participants[0].subscribers[0].data_readers[0].topic_ref,
1126 ObjectId::from_raw(0x0102)
1127 );
1128 }
1129
1130 #[test]
1131 fn roundtrip_multiple_participants() {
1132 let xml = r#"<dds>
1133 <type object_id="0x000A" name="T1"/>
1134 <participant object_id="0xCAF1" domain_id="0"/>
1135 <participant object_id="0xBEE1" domain_id="42"/>
1136 </dds>"#;
1137 let cfg = load_xrce_config(xml).unwrap();
1138 assert_eq!(cfg.participants.len(), 2);
1139 assert_eq!(cfg.participants[1].domain_id, 42);
1140 }
1141
1142 #[test]
1145 fn err_unexpected_root() {
1146 let res = load_xrce_config("<not_dds/>");
1147 assert!(matches!(res, Err(XrceXmlError::UnexpectedRoot(_))));
1148 }
1149
1150 #[test]
1151 fn err_duplicate_object_id_two_topics() {
1152 let xml = r#"<dds>
1155 <type object_id="0x000A" name="T1"/>
1156 <participant object_id="0xCAF1" domain_id="0">
1157 <topic object_id="0x0102" name="A" type_name="T1"/>
1158 <topic object_id="0x0102" name="B" type_name="T1"/>
1159 </participant>
1160 </dds>"#;
1161 let res = load_xrce_config(xml);
1162 assert!(matches!(res, Err(XrceXmlError::DuplicateObjectId(_))));
1163 }
1164
1165 #[test]
1166 fn err_unresolved_topic_ref() {
1167 let xml = r#"<dds>
1168 <type object_id="0x000A" name="T1"/>
1169 <participant object_id="0xCAF1" domain_id="0">
1170 <topic object_id="0x0102" name="A" type_name="T1"/>
1171 <publisher object_id="0x0103">
1172 <data_writer object_id="0x0105" topic_ref="0x0FF2"/>
1173 </publisher>
1174 </participant>
1175 </dds>"#;
1176 let res = load_xrce_config(xml);
1177 assert!(matches!(res, Err(XrceXmlError::UnresolvedTopicRef { .. })));
1178 }
1179
1180 #[test]
1181 fn err_unresolved_type_name() {
1182 let xml = r#"<dds>
1183 <type object_id="0x000A" name="T1"/>
1184 <participant object_id="0xCAF1" domain_id="0">
1185 <topic object_id="0x0102" name="A" type_name="NotDeclared"/>
1186 </participant>
1187 </dds>"#;
1188 let res = load_xrce_config(xml);
1189 assert!(matches!(res, Err(XrceXmlError::UnresolvedTypeName { .. })));
1190 }
1191
1192 #[test]
1193 fn err_object_kind_mismatch() {
1194 let xml = r#"<dds>
1197 <type object_id="0x000A" name="T1"/>
1198 <participant object_id="0xCAF1" domain_id="0">
1199 <topic object_id="0x0103" name="A" type_name="T1"/>
1200 </participant>
1201 </dds>"#;
1202 let res = load_xrce_config(xml);
1203 assert!(matches!(res, Err(XrceXmlError::ObjectKindMismatch { .. })));
1204 }
1205
1206 #[test]
1207 fn err_circular_type_self_reference() {
1208 let xml = r#"<dds>
1210 <type object_id="0x000A" name="A">
1211 <struct name="A">
1212 <member name="self_ref" type="A"/>
1213 </struct>
1214 </type>
1215 <participant object_id="0xCAF1" domain_id="0"/>
1216 </dds>"#;
1217 let res = load_xrce_config(xml);
1218 assert!(matches!(res, Err(XrceXmlError::CircularType(_))));
1219 }
1220
1221 #[test]
1224 fn create_messages_has_correct_count() {
1225 let cfg = load_xrce_config(cfg_basic()).unwrap();
1226 let msgs = cfg.to_create_messages().unwrap();
1227 assert_eq!(msgs.len(), 7);
1230 }
1231
1232 #[test]
1233 fn create_messages_topological_order() {
1234 let cfg = load_xrce_config(cfg_basic()).unwrap();
1235 let msgs = cfg.to_create_messages().unwrap();
1236 let kinds: Vec<ObjectKind> = msgs.iter().map(|m| m.kind).collect();
1237 assert_eq!(
1238 kinds,
1239 vec![
1240 ObjectKind::Type,
1241 ObjectKind::Participant,
1242 ObjectKind::Topic,
1243 ObjectKind::Publisher,
1244 ObjectKind::Subscriber,
1245 ObjectKind::DataWriter,
1246 ObjectKind::DataReader,
1247 ]
1248 );
1249 }
1250
1251 #[test]
1252 fn create_messages_carry_xml_representation() {
1253 let cfg = load_xrce_config(cfg_basic()).unwrap();
1254 let msgs = cfg.to_create_messages().unwrap();
1255 let topic_msg = msgs
1256 .iter()
1257 .find(|m| m.kind == ObjectKind::Topic)
1258 .expect("topic");
1259 let body = &topic_msg.payload.representation;
1260 assert_eq!(body[0], crate::object_repr::repr_disc::AS_XML_STRING);
1262 }
1263
1264 #[test]
1265 fn create_messages_default_flags_have_no_reuse_replace() {
1266 let cfg = load_xrce_config(cfg_basic()).unwrap();
1267 let msgs = cfg.to_create_messages().unwrap();
1268 for m in &msgs {
1269 assert!(!m.payload.reuse);
1270 assert!(!m.payload.replace);
1271 }
1272 }
1273
1274 #[test]
1275 fn create_messages_for_empty_participant_only_participant_msg() {
1276 let xml = r#"<dds>
1277 <type object_id="0x000A" name="T1"/>
1278 <participant object_id="0xCAF1" domain_id="0"/>
1279 </dds>"#;
1280 let cfg = load_xrce_config(xml).unwrap();
1281 let msgs = cfg.to_create_messages().unwrap();
1282 assert_eq!(msgs.len(), 2);
1283 assert_eq!(msgs[0].kind, ObjectKind::Type);
1284 assert_eq!(msgs[1].kind, ObjectKind::Participant);
1285 }
1286
1287 #[test]
1290 fn qos_resolver_finds_profile() {
1291 let cfg = load_xrce_config(cfg_basic()).unwrap();
1292 let mut r = InMemoryQosResolver::new();
1293 r.add("Lib::ShapeProfile", "<qos_profile name=\"ShapeProfile\"/>");
1294 let xml = cfg.resolve_qos_profile("Lib::ShapeProfile", &r).unwrap();
1295 assert!(xml.contains("ShapeProfile"));
1296 }
1297
1298 #[test]
1299 fn qos_resolver_unresolved_returns_error() {
1300 let cfg = load_xrce_config(cfg_basic()).unwrap();
1301 let r = InMemoryQosResolver::new();
1302 let res = cfg.resolve_qos_profile("Lib::Missing", &r);
1303 assert!(matches!(res, Err(XrceXmlError::UnresolvedQosProfile(_))));
1304 }
1305
1306 #[test]
1307 fn qos_profile_refs_collects_all() {
1308 let cfg = load_xrce_config(cfg_basic()).unwrap();
1309 let refs = cfg.qos_profile_refs();
1310 assert_eq!(refs, vec!["Lib::ShapeProfile".to_string()]);
1311 }
1312
1313 #[test]
1314 fn qos_profile_refs_dedup() {
1315 let xml = r#"<dds>
1316 <type object_id="0x000A" name="T1"/>
1317 <participant object_id="0xCAF1" domain_id="0">
1318 <topic object_id="0x0102" name="A" type_name="T1" qos_profile="L::P"/>
1319 <publisher object_id="0x0103">
1320 <data_writer object_id="0x0105" topic_ref="0x0102" qos_profile="L::P"/>
1321 </publisher>
1322 </participant>
1323 </dds>"#;
1324 let cfg = load_xrce_config(xml).unwrap();
1325 let refs = cfg.qos_profile_refs();
1326 assert_eq!(refs, vec!["L::P".to_string()]);
1327 }
1328
1329 #[test]
1330 fn qos_resolver_via_phase7_dds_xml_loader_shape() {
1331 let lib_xml = r#"<dds><qos_library name="Lib"><qos_profile name="ShapeProfile"><datawriter_qos><reliability><kind>RELIABLE_RELIABILITY_QOS</kind></reliability></datawriter_qos></qos_profile></qos_library></dds>"#;
1336 let doc = parse_xml_tree(lib_xml).unwrap();
1337 let lib = doc.root.child("qos_library").unwrap();
1338 let profile = lib.child("qos_profile").unwrap();
1339 let mut r = InMemoryQosResolver::new();
1340 r.add("Lib::ShapeProfile", serialize_element(profile));
1341
1342 let cfg = load_xrce_config(cfg_basic()).unwrap();
1343 let xml = cfg.resolve_qos_profile("Lib::ShapeProfile", &r).unwrap();
1344 assert!(xml.contains("RELIABLE_RELIABILITY_QOS"));
1345 }
1346
1347 #[test]
1350 fn type_reuse_xml_substring_parseable() {
1351 let cfg = load_xrce_config(cfg_basic()).unwrap();
1352 let doc = parse_xml_tree(&cfg.types[0].xml).unwrap();
1355 assert_eq!(doc.root.name, "type");
1356 }
1357
1358 #[test]
1359 fn type_reuse_carries_module_struct() {
1360 let cfg = load_xrce_config(cfg_basic()).unwrap();
1361 let doc = parse_xml_tree(&cfg.types[0].xml).unwrap();
1362 let module = doc.root.child("module").expect("module");
1363 assert_eq!(module.attribute("name"), Some("ShapesDemoTypes"));
1364 assert!(module.child("struct").is_some());
1365 }
1366
1367 #[test]
1368 fn type_reuse_member_type_extraction() {
1369 let cfg = load_xrce_config(cfg_basic()).unwrap();
1370 let refs = cfg.types[0].referenced_types();
1371 assert!(refs.iter().any(|t| t == "string"));
1373 }
1374
1375 #[test]
1376 fn type_reuse_two_types_no_cycle() {
1377 let xml = r#"<dds>
1378 <type object_id="0x000A" name="A">
1379 <struct name="A"><member name="x" type="long"/></struct>
1380 </type>
1381 <type object_id="0x001A" name="B">
1382 <struct name="B"><member name="a" type="A"/></struct>
1383 </type>
1384 <participant object_id="0xCAF1" domain_id="0"/>
1385 </dds>"#;
1386 let cfg = load_xrce_config(xml).unwrap();
1387 assert_eq!(cfg.types.len(), 2);
1388 }
1389
1390 #[test]
1391 fn type_reuse_indirect_cycle_detected() {
1392 let xml = r#"<dds>
1393 <type object_id="0x000A" name="A">
1394 <struct name="A"><member name="b" type="B"/></struct>
1395 </type>
1396 <type object_id="0x001A" name="B">
1397 <struct name="B"><member name="a" type="A"/></struct>
1398 </type>
1399 <participant object_id="0xCAF1" domain_id="0"/>
1400 </dds>"#;
1401 let res = load_xrce_config(xml);
1402 assert!(matches!(res, Err(XrceXmlError::CircularType(_))));
1403 }
1404
1405 #[test]
1408 fn edge_empty_dds_root_is_valid() {
1409 let cfg = load_xrce_config("<dds/>").unwrap();
1410 assert_eq!(cfg.types.len(), 0);
1411 assert_eq!(cfg.participants.len(), 0);
1412 }
1413
1414 #[test]
1415 fn edge_missing_root_invalid_xml() {
1416 let res = load_xrce_config("");
1417 assert!(matches!(res, Err(XrceXmlError::InvalidXml(_))));
1418 }
1419
1420 #[test]
1421 fn edge_invalid_domain_id_string() {
1422 let xml = r#"<dds>
1423 <participant object_id="0xCAF1" domain_id="not_a_number"/>
1424 </dds>"#;
1425 let res = load_xrce_config(xml);
1426 assert!(matches!(res, Err(XrceXmlError::InvalidAttribute { .. })));
1427 }
1428
1429 #[test]
1430 fn edge_domain_id_overflow() {
1431 let xml = format!(
1432 "<dds><participant object_id=\"0xCAF1\" domain_id=\"{}\"/></dds>",
1433 (i32::MAX as u64) + 1
1434 );
1435 let res = load_xrce_config(&xml);
1436 assert!(matches!(res, Err(XrceXmlError::DomainIdOutOfRange(_))));
1437 }
1438
1439 #[test]
1440 fn edge_too_many_types_capped() {
1441 let mut xml = String::from("<dds>");
1442 for i in 0..(MAX_TYPES_PER_FILE + 1) {
1443 xml.push_str(&format!(
1444 "<type object_id=\"0x{:04X}\" name=\"T{}\"/>",
1445 ((i as u16 + 1) << 4) | ObjectKind::Type.to_u8() as u16,
1446 i
1447 ));
1448 }
1449 xml.push_str("</dds>");
1450 let res = load_xrce_config(&xml);
1451 assert!(matches!(res, Err(XrceXmlError::TooManyTypes(_))));
1452 }
1453
1454 #[test]
1455 fn edge_topic_missing_required_attr() {
1456 let xml = r#"<dds>
1457 <type object_id="0x000A" name="T1"/>
1458 <participant object_id="0xCAF1" domain_id="0">
1459 <topic object_id="0x0102" name="A"/>
1460 </participant>
1461 </dds>"#;
1462 let res = load_xrce_config(xml);
1463 assert!(matches!(res, Err(XrceXmlError::MissingAttribute { .. })));
1464 }
1465
1466 #[test]
1467 fn edge_invalid_object_id_format() {
1468 let xml = r#"<dds>
1469 <participant object_id="0xZZZZ" domain_id="0"/>
1470 </dds>"#;
1471 let res = load_xrce_config(xml);
1472 assert!(matches!(res, Err(XrceXmlError::InvalidAttribute { .. })));
1473 }
1474
1475 #[test]
1476 fn edge_decimal_object_id_supported() {
1477 let xml = r#"<dds>
1478 <type object_id="10" name="T1"/>
1479 <participant object_id="51953" domain_id="0"/>
1480 </dds>"#;
1481 let cfg = load_xrce_config(xml).unwrap();
1482 assert_eq!(cfg.participants[0].object_id, ObjectId::from_raw(51953));
1483 }
1484
1485 #[test]
1488 fn display_xrce_xml_error_messages() {
1489 let e = XrceXmlError::DuplicateObjectId(ObjectId::from_raw(0x1234));
1490 assert!(format!("{e}").contains("1234"));
1491 let e = XrceXmlError::HierarchyTooDeep(MAX_HIERARCHY_DEPTH + 1);
1492 assert!(format!("{e}").contains("limit"));
1493 let e = XrceXmlError::TooManyTypes(MAX_TYPES_PER_FILE + 1);
1494 assert!(format!("{e}").contains("count"));
1495 }
1496
1497 #[test]
1498 fn from_xrce_error_wraps_wire() {
1499 let e: XrceXmlError = XrceError::ValueOutOfRange { message: "x" }.into();
1500 assert!(matches!(e, XrceXmlError::Wire(_)));
1501 }
1502}