Skip to main content

zerodds_xrce/
xml_config.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! XRCE-XML-File-Configuration (Spec §7.7.3 + §9.3).
5//!
6//! Ein XRCE-Client/Agent kann seine ganze Object-Hierarchie aus einem
7//! XML-File laden. Das File-Format folgt der DDS-XRCE-Spec §9.3 Tab.15
8//! ("File-based Configuration"):
9//!
10//! ```xml
11//! <dds>
12//!   <type>
13//!     <module name="ShapesDemoTypes"><struct name="ShapeType">…</struct></module>
14//!   </type>
15//!   <participant object_id="0xCAFE" domain_id="0">
16//!     <topic object_id="0x0102" name="Square" type_name="ShapeType"
17//!            qos_profile="Lib::ShapeProfile"/>
18//!     <publisher object_id="0x0103">
19//!       <data_writer object_id="0x0105" topic_ref="0x0102"
20//!                    qos_profile="Lib::ShapeProfile"/>
21//!     </publisher>
22//!     <subscriber object_id="0x0104">
23//!       <data_reader object_id="0x0106" topic_ref="0x0102"/>
24//!     </subscriber>
25//!   </participant>
26//! </dds>
27//! ```
28//!
29//! ## Bridge zu DDS-XML-Loader
30//!
31//! * `<type><module>…</module></type>` wird ueber
32//!   [`zerodds_xml::parse_xml_tree`] eingelesen und kann an
33//!   [`zerodds_xml::xtypes_parser::parse_types_element`] weitergeleitet werden
34//!   (XML→TypeObject-Bridge ueber `zerodds_xml::typeobject_bridge`).
35//! * `qos_profile`-Attribute referenzieren Profile aus einer separat
36//!   geladenen `<qos_library>` (DDS-XML-Modul in `zerodds-xml`). Resolution
37//!   erfolgt via [`XrceConfig::resolve_qos_profile`] mit injiziertem
38//!   QoS-Loader.
39//!
40//! ## Mapping zu CREATE-Submessages (Spec §8.4.6)
41//!
42//! [`XrceConfig::to_create_messages`] generiert die CREATE-Submessage-
43//! Sequence in topologisch korrekter Reihenfolge:
44//!
45//! ```text
46//!   1. Type           (OBJK_TYPE,         REPRESENTATION_AS_XML_STRING)
47//!   2. Participant    (OBJK_PARTICIPANT,  REPRESENTATION_AS_XML_STRING)
48//!   3. Topic          (OBJK_TOPIC,        REPRESENTATION_AS_XML_STRING)
49//!   4. Publisher      (OBJK_PUBLISHER,    REPRESENTATION_AS_XML_STRING)
50//!   5. Subscriber     (OBJK_SUBSCRIBER,   REPRESENTATION_AS_XML_STRING)
51//!   6. DataWriter     (OBJK_DATAWRITER,   REPRESENTATION_AS_XML_STRING)
52//!   7. DataReader     (OBJK_DATAREADER,   REPRESENTATION_AS_XML_STRING)
53//! ```
54//!
55//! Out-of-scope :
56//! * `<application>`-Container, `<domain>`-Library — DDS-XML-Side.
57//! * Live-Reload / Hot-Swap — Stretch.
58//! * DTD-/XSD-Validation der Schema selbst — `xrce-config.xsd` ist
59//!   Doku-Skizze, nicht Validator-Input.
60
61extern 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
76/// Maximale Schachtelungs-Tiefe der XRCE-Object-Hierarchie. Spec §9.3
77/// erlaubt formal `<dds>/<participant>/<publisher>/<data_writer>` —
78/// Tiefe 4. Wir setzen ein DoS-Cap von 8 fuer Robustheit.
79pub const MAX_HIERARCHY_DEPTH: usize = 8;
80
81/// Maximale Anzahl Type-Definitionen pro File (DoS-Cap).
82pub const MAX_TYPES_PER_FILE: usize = 256;
83
84/// Fehler beim Laden eines XRCE-XML-Configuration-Files.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum XrceXmlError {
87    /// Underlying DDS-XML-Loader-Fehler (Wohlgeformtheit, DoS-Cap).
88    InvalidXml(String),
89    /// Wurzel-Element ist nicht `<dds>` (Spec §9.3).
90    UnexpectedRoot(String),
91    /// Pflicht-Attribut fehlt (z.B. `object_id`, `domain_id`, `name`).
92    MissingAttribute {
93        /// Element-Name.
94        element: String,
95        /// Attribut-Name.
96        attribute: String,
97    },
98    /// Attribut-Wert ist nicht parsebar (z.B. `object_id="0xZZZZ"`).
99    InvalidAttribute {
100        /// Element-Name.
101        element: String,
102        /// Attribut-Name.
103        attribute: String,
104        /// Beobachteter Wert.
105        value: String,
106    },
107    /// Doppelte ObjectId im Scope eines Participants (Spec §7.2.1
108    /// fordert Eindeutigkeit pro Client/Agent-Object-Store).
109    DuplicateObjectId(ObjectId),
110    /// `topic_ref` zeigt auf eine ObjectId, die nicht im selben
111    /// Participant deklariert ist.
112    UnresolvedTopicRef {
113        /// DataWriter-/DataReader-ObjectId.
114        endpoint: ObjectId,
115        /// Topic-ObjectId, auf die verwiesen wurde.
116        topic: ObjectId,
117    },
118    /// `type_name` referenziert einen Type, der nicht in der globalen
119    /// `<type>`-Section deklariert ist.
120    UnresolvedTypeName {
121        /// Topic-ObjectId, an der der Type-Name haengt.
122        topic: ObjectId,
123        /// Type-Name (z.B. `ShapeType` oder `Module::ShapeType`).
124        type_name: String,
125    },
126    /// `qos_profile`-String konnte nicht aufgeloest werden (kein
127    /// QoS-Loader injiziert oder Profile fehlt).
128    UnresolvedQosProfile(String),
129    /// Schachtelungs-Tiefe ueberschreitet [`MAX_HIERARCHY_DEPTH`].
130    HierarchyTooDeep(usize),
131    /// Anzahl Types ueberschreitet [`MAX_TYPES_PER_FILE`].
132    TooManyTypes(usize),
133    /// Innerhalb der Type-Definition referenziert sich ein Type selbst
134    /// (struct A enthaelt struct A) — direkt oder transitiv.
135    CircularType(String),
136    /// Domain-Id ist out-of-range (DDS-XRCE 1.0 §7.7.3.6: `long`, also
137    /// 0..=`i32::MAX`).
138    DomainIdOutOfRange(u64),
139    /// Eine ObjectId-/ObjectKind-Kombination passt nicht (z.B. eine
140    /// als `<topic>` deklarierte ObjectId hat im unteren Nibble nicht
141    /// `OBJK_TOPIC = 0x02`). Spec §7.2.1 verlangt diese Konsistenz.
142    ObjectKindMismatch {
143        /// ObjectId.
144        id: ObjectId,
145        /// Erwarteter Kind.
146        expected: u8,
147        /// Beobachteter Kind im unteren Nibble.
148        actual: u8,
149    },
150    /// XRCE-Wire-Encode-Fehler (Payload-Cap, etc.).
151    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/// Top-Level XRCE-File-Configuration (Spec §9.3 Tab.15).
231#[derive(Debug, Clone, Default, PartialEq, Eq)]
232pub struct XrceConfig {
233    /// Type-Definitionen (`<type><module>…</module></type>`). Speicher
234    /// ist der Original-XML-Substring pro Type — der DDS-XRCE-Wire-Pfad
235    /// uebergibt das ungeparste XML als `RepresentationByXmlString`.
236    pub types: Vec<TypeConfig>,
237    /// Participants. Pro Participant eigener ObjectStore-Scope.
238    pub participants: Vec<ParticipantConfig>,
239}
240
241/// Eine `<type>`-Section (Spec §7.7.3.3).
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub struct TypeConfig {
244    /// ObjectId fuer den OBJK_TYPE-Wrapper. Wird beim Aufrufer
245    /// deklariert oder mit `ObjectId::new(raw, ObjectKind::Type)`
246    /// generiert. Optional, weil §9.3 ObjectId-Ableitung via
247    /// MD5-Hash erlaubt — siehe `derive_object_id`.
248    pub object_id: ObjectId,
249    /// Type-Name (top-level struct/enum/union oder Module).
250    pub name: String,
251    /// Alle in dieser Section deklarierten Type-Namen (rekursiv ueber
252    /// `<module>`/`<struct>`/`<enum>`/`<union>`/`<typedef>`/
253    /// `<bitmask>`/`<bitset>`). Topics duerfen Namen aus dieser Liste
254    /// referenzieren — entspricht der DDS-XML 1.0 §7.3.3-Sicht.
255    pub declared_names: Vec<String>,
256    /// Original-XML der Type-Section (Substring zwischen
257    /// `<type>` und `</type>`). Wird im CREATE als
258    /// `RepresentationByXmlString` weitergegeben.
259    pub xml: String,
260}
261
262/// Ein `<participant>` (Spec §7.7.3.6).
263#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct ParticipantConfig {
265    /// ObjectId, kind=OBJK_PARTICIPANT.
266    pub object_id: ObjectId,
267    /// `domain_id`-Attribut.
268    pub domain_id: u32,
269    /// Topic-Definitionen.
270    pub topics: Vec<TopicConfig>,
271    /// Publisher-Definitionen.
272    pub publishers: Vec<PublisherConfig>,
273    /// Subscriber-Definitionen.
274    pub subscribers: Vec<SubscriberConfig>,
275}
276
277/// Ein `<topic>` (Spec §7.7.3.7).
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub struct TopicConfig {
280    /// ObjectId, kind=OBJK_TOPIC.
281    pub object_id: ObjectId,
282    /// `name`-Attribut.
283    pub name: String,
284    /// `type_name`-Attribut. Muss in der globalen `<type>`-Section
285    /// deklariert sein.
286    pub type_name: String,
287    /// Optional `qos_profile="Lib::Profile"`.
288    pub qos_profile: Option<String>,
289}
290
291/// Ein `<publisher>` (Spec §7.7.3.8).
292#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct PublisherConfig {
294    /// ObjectId, kind=OBJK_PUBLISHER.
295    pub object_id: ObjectId,
296    /// DataWriter-Endpoints.
297    pub data_writers: Vec<DataWriterConfig>,
298}
299
300/// Ein `<subscriber>` (Spec §7.7.3.9).
301#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct SubscriberConfig {
303    /// ObjectId, kind=OBJK_SUBSCRIBER.
304    pub object_id: ObjectId,
305    /// DataReader-Endpoints.
306    pub data_readers: Vec<DataReaderConfig>,
307}
308
309/// Ein `<data_writer>` (Spec §7.7.3.10).
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct DataWriterConfig {
312    /// ObjectId, kind=OBJK_DATAWRITER.
313    pub object_id: ObjectId,
314    /// Referenz auf ein Topic im selben Participant.
315    pub topic_ref: ObjectId,
316    /// Optional QoS-Profile.
317    pub qos_profile: Option<String>,
318}
319
320/// Ein `<data_reader>` (Spec §7.7.3.11).
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct DataReaderConfig {
323    /// ObjectId, kind=OBJK_DATAREADER.
324    pub object_id: ObjectId,
325    /// Referenz auf ein Topic im selben Participant.
326    pub topic_ref: ObjectId,
327    /// Optional QoS-Profile.
328    pub qos_profile: Option<String>,
329}
330
331/// Eine generierte CREATE-Submessage zusammen mit ihrer ObjectId und
332/// ObjectKind, damit der Caller sie an die richtige Stelle in der
333/// Submessage-Pipeline einfuegen kann.
334#[derive(Debug, Clone, PartialEq, Eq)]
335pub struct CreateMessage {
336    /// ObjectId, fuer die das CREATE gilt.
337    pub object_id: ObjectId,
338    /// Kind (Topic/Publisher/…) zur Sortier-Validation.
339    pub kind: ObjectKind,
340    /// Fertig konstruiertes [`CreatePayload`] mit
341    /// `RepresentationByXmlString`-Body. Der Aufrufer wandelt es ueber
342    /// [`CreatePayload::into_submessage`] in eine [`crate::Submessage`].
343    pub payload: CreatePayload,
344}
345
346/// Trait zum Auflosen von `qos_profile="Lib::Profile"`-Referenzen aus
347/// einer separat geladenen DDS-XML-`<qos_library>` (DDS-XML-Modul in
348/// `zerodds-xml`). Optional injiziert; wenn `None`, werden Referenzen
349/// strukturell mitgefuehrt aber nicht resolved.
350pub trait QosProfileResolver {
351    /// Liefert den `<qos_profile>…</qos_profile>`-XML-Substring fuer
352    /// einen Pfad `Library::Profile`. `None` bei nicht-existentem
353    /// Profile (Caller dann [`XrceXmlError::UnresolvedQosProfile`]).
354    fn resolve(&self, path: &str) -> Option<String>;
355}
356
357/// Convenience-Resolver, der direkt eine Map `Lib::Profile -> XML`
358/// haelt. Pure-Logik, kein DDS-XML-Layer noetig.
359#[derive(Debug, Default, Clone)]
360pub struct InMemoryQosResolver {
361    /// Lookup-Map.
362    pub profiles: alloc::collections::BTreeMap<String, String>,
363}
364
365impl InMemoryQosResolver {
366    /// Konstruiert einen leeren Resolver.
367    #[must_use]
368    pub fn new() -> Self {
369        Self::default()
370    }
371    /// Fuegt ein Profile hinzu.
372    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
383/// Parst einen XML-String in eine [`XrceConfig`].
384///
385/// Validation:
386/// * Wurzel muss `<dds>` sein.
387/// * Jede ObjectId ist eindeutig (ueber alle Sections + Participants).
388/// * Jede ObjectId hat im unteren Nibble den passenden ObjectKind.
389/// * `topic_ref` zeigt auf ein Topic im selben Participant.
390/// * `type_name` ist in der `<type>`-Section deklariert.
391/// * Type-Definitionen sind zyklus-frei.
392///
393/// # Errors
394/// [`XrceXmlError`] siehe einzelne Varianten.
395pub 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/// Liest und parst eine Datei (nur mit `feature = "std"`).
404///
405/// # Errors
406/// IO-Fehler werden als [`XrceXmlError::InvalidXml`] re-emittiert,
407/// XML-Fehler ueber [`load_xrce_config`].
408#[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    /// Baut die Config aus dem `<dds>`-Wurzel-Element auf.
418    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        // Cycle-Check ueber alle Type-Definitionen.
430        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    /// Generiert die CREATE-Submessage-Sequence. Reihenfolge:
448    /// Type → Participant → Topic → Publisher → Subscriber →
449    /// DataWriter → DataReader.
450    ///
451    /// `RepresentationByXmlString`-Body ist pro Object eine kompakte
452    /// XML-Snippet (kein voller File-Inhalt).
453    ///
454    /// # Errors
455    /// [`XrceXmlError::Wire`] bei Encode-Fehlern (Payload-Cap).
456    pub fn to_create_messages(&self) -> Result<Vec<CreateMessage>, XrceXmlError> {
457        let mut out = Vec::new();
458        // 1. Types
459        for tc in &self.types {
460            out.push(create_message(tc.object_id, ObjectKind::Type, &tc.xml)?);
461        }
462        // 2. Participants
463        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        // 3. Topics (pro Participant in Reihenfolge)
471        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        // 4. Publishers
488        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        // 5. Subscribers
498        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        // 6. DataWriters
508        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        // 7. DataReaders
526        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    /// Versucht alle `qos_profile`-Referenzen via Resolver aufzuloesen.
547    /// Liefert die Liste der gefundenen `(path, xml)`-Paare; fehlende
548    /// Profile werden als [`XrceXmlError::UnresolvedQosProfile`]
549    /// gesammelt (erstes Encounter).
550    ///
551    /// # Errors
552    /// [`XrceXmlError::UnresolvedQosProfile`] beim ersten fehlenden
553    /// Profile.
554    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    /// Sammelt alle in der Hierarchie referenzierten QoS-Profile-Pfade.
565    #[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        // Top-Level-Name: aus `name`-Attribut auf `<type>` selbst, oder
600        // vom ersten Type-tragenden Kind-Element (Modul/Struct/...).
601        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        // Alle deklarierten Type-Namen (rekursiv durch <module>).
618        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    /// Sammelt alle Type-Namen, die diese Definition referenziert
638    /// (fuer Cycle-Detection auf der Type-Ebene).
639    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
647/// Sammelt rekursiv alle Type-Namen unter einem Element (struct/enum/
648/// union/typedef/bitmask/bitset, in `<module>`-Wraps geschachtelt).
649///
650/// zerodds-lint: recursion-depth 32
651///
652/// XML-Module-Hierarchie ist mit `MAX_HIERARCHY_DEPTH=32` durch den
653/// Loader-Top-Level vorab gecappt; dieser Walk geht nicht tiefer.
654fn 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
674/// zerodds-lint: recursion-depth 32
675///
676/// Member-/Case-Walks innerhalb eines Type-Elements; tiefe Verschachtelung
677/// ist durch `MAX_HIERARCHY_DEPTH` im Loader bereits gecappt.
678fn 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    // DFS pro Knoten.
699    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        // type_name darf entweder gegen den Top-Level-Namen einer
786        // <type>-Section matchen oder gegen einen darunter geschachtelten
787        // Type (z.B. "ShapeType" innerhalb von <module>).
788        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("&lt;"),
986            '>' => out.push_str("&gt;"),
987            '&' => out.push_str("&amp;"),
988            '"' => out.push_str("&quot;"),
989            '\'' => out.push_str("&apos;"),
990            _ => out.push(c),
991        }
992    }
993    out
994}
995
996/// Serialisiert ein [`XmlElement`] in eine kompakte XML-String-Form.
997///
998/// Diese Reserialisierung ist nicht byte-identisch mit dem Eingabe-XML
999/// (Whitespace, Attribut-Reihenfolge), aber strukturell aequivalent —
1000/// genau das, was [`ObjectVariant::ByXmlString`] braucht.
1001fn serialize_element(el: &XmlElement) -> String {
1002    let mut out = String::new();
1003    write_element(&mut out, el);
1004    out
1005}
1006
1007/// zerodds-lint: recursion-depth 32
1008///
1009/// XML-Reserialisierung-Walk; Tiefe ist durch `MAX_HIERARCHY_DEPTH` beim
1010/// Loader bereits beschraenkt.
1011fn 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("&lt;"),
1030                '>' => out.push_str("&gt;"),
1031                '&' => out.push_str("&amp;"),
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    // ===== 5x XML-Roundtrips / Strukturelle Equivalenz =====
1071
1072    #[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    // ===== 5x Validation-Errors =====
1143
1144    #[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        // Beide Topics teilen ObjectId 0x0102 — Duplikat innerhalb des
1153        // Topic-Scopes des Participants.
1154        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        // ObjectId 0x0103 endet auf 3 = OBJK_PUBLISHER, wird aber als
1195        // Topic deklariert (erwartet ObjectKind::Topic = 0x02).
1196        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        // struct A enthaelt member type=A → Cycle
1209        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    // ===== 5x to_create_messages =====
1222
1223    #[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        // 1 Type + 1 Participant + 1 Topic + 1 Publisher + 1 Subscriber +
1228        // 1 DataWriter + 1 DataReader = 7
1229        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        // Discriminator-Byte muss BY_XML_STRING = 0x02 sein.
1261        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    // ===== 5x QoS-Profile-Resolution =====
1288
1289    #[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        // Bridge-Test: DDS-XML-Loader liefert ein parseable
1332        // <qos_library>-XML; daraus extrahieren wir den Profile-XML-Block
1333        // und stecken ihn in unseren Resolver. Hier simulieren wir das
1334        // mit `zerodds_xml::parse_xml_tree`.
1335        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    // ===== 5x Type-Reuse via xml-Crate-Bridge =====
1348
1349    #[test]
1350    fn type_reuse_xml_substring_parseable() {
1351        let cfg = load_xrce_config(cfg_basic()).unwrap();
1352        // Der gespeicherte XML-Substring muss vom zerodds-xml-Loader wieder
1353        // parseable sein (Bridge zur Type-Library).
1354        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        // member type=string steckt im Modul-Tree
1372        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    // ===== 5x Edge-Cases =====
1406
1407    #[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    // ===== Fmt + From-Konvertierung =====
1486
1487    #[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}