Skip to main content

zerodds_corba_dnc/
xml.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! XML-Loader fuer D&C-Plan-Files — Spec D&C §10 (XML-Encoding).
5//!
6//! Spec §10 spezifiziert das XML-Schema (`Deployment.xsd`). Wir
7//! implementieren einen minimalen, robusten Parser fuer die
8//! Plan-Strukturen, die Caller typischerweise schreiben:
9//!
10//! ```xml
11//! <deploymentPlan>
12//!   <label>Plan1</label>
13//!   <UUID>uuid-1</UUID>
14//!   <implementation>
15//!     <label>EchoImpl</label>
16//!     <UUID>echo</UUID>
17//!     <artifact>lib/echo.so</artifact>
18//!     <realizes>IDL:demo/Echo:1.0</realizes>
19//!   </implementation>
20//!   <instance>
21//!     <name>echo1</name>
22//!     <implementation>EchoImpl</implementation>
23//!     <node>Node1</node>
24//!     <coLocateWith>echo2</coLocateWith>
25//!   </instance>
26//! </deploymentPlan>
27//! ```
28
29use alloc::collections::BTreeMap;
30use alloc::string::{String, ToString};
31use alloc::vec::Vec;
32
33use crate::plan::{
34    DeploymentPlan, ImplementationDescription, InstanceDeploymentDescription, PlanConnection,
35};
36
37/// XML-Parser-Fehler.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum ParseError {
40    /// Erwartetes Tag nicht gefunden.
41    ExpectedTag(String),
42    /// Tag ist nicht abgeschlossen.
43    UnterminatedTag,
44    /// Plan-Validation fehlgeschlagen.
45    ValidationFailed(String),
46}
47
48impl core::fmt::Display for ParseError {
49    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
50        match self {
51            Self::ExpectedTag(t) => write!(f, "expected tag <{t}>"),
52            Self::UnterminatedTag => f.write_str("unterminated tag"),
53            Self::ValidationFailed(msg) => write!(f, "validation: {msg}"),
54        }
55    }
56}
57
58#[cfg(feature = "std")]
59impl std::error::Error for ParseError {}
60
61/// Parst ein D&C `<deploymentPlan>`-XML in einen [`DeploymentPlan`].
62///
63/// Der Parser ist strikt sequenziell und erwartet die Spec-Reihenfolge
64/// `<label>` → `<UUID>` → `<realizes>?` → `<implementation>*` →
65/// `<instance>*` → `<connection>*`. Whitespace wird ignoriert.
66///
67/// # Errors
68/// Siehe [`ParseError`].
69pub fn parse_plan_xml(input: &str) -> Result<DeploymentPlan, ParseError> {
70    let body = inner_text(input, "deploymentPlan")?;
71    let label = inner_text(&body, "label").unwrap_or_default();
72    let uuid = inner_text(&body, "UUID").unwrap_or_default();
73    let realizes = inner_text(&body, "realizes").unwrap_or_default();
74
75    let mut plan = DeploymentPlan {
76        label,
77        uuid,
78        realizes,
79        ..DeploymentPlan::default()
80    };
81
82    // Spec §10 — Plan-Body hat `<implementation>`, `<instance>`,
83    // `<connection>` als Top-Level-Children. Da `<instance>` selbst
84    // ein `<implementation>`-Leaf-Element traegt (Reference auf eine
85    // ImplementationDescription per label), parsen wir Instances
86    // zuerst und entfernen ihre Bloecke aus dem Body, bevor wir nach
87    // weiteren Top-Level-Implementations suchen.
88    let instance_blocks = iter_blocks(&body, "instance");
89    let connection_blocks = iter_blocks(&body, "connection");
90    let body_without_instances = strip_blocks(&body, "instance");
91    let body_without_inst_conn = strip_blocks(&body_without_instances, "connection");
92
93    for impl_block in iter_blocks(&body_without_inst_conn, "implementation") {
94        plan.implementations.push(parse_implementation(&impl_block));
95    }
96    for inst_block in instance_blocks {
97        plan.instances.push(parse_instance(&inst_block));
98    }
99    for conn_block in connection_blocks {
100        plan.connections.push(parse_connection(&conn_block));
101    }
102
103    plan.validate()
104        .map_err(|e| ParseError::ValidationFailed(format_err(&e)))?;
105    Ok(plan)
106}
107
108fn strip_blocks(input: &str, tag: &str) -> String {
109    let open = alloc::format!("<{tag}>");
110    let close = alloc::format!("</{tag}>");
111    let mut out = String::new();
112    let mut cursor = 0;
113    while let Some(s) = input[cursor..].find(&open) {
114        let start = cursor + s;
115        out.push_str(&input[cursor..start]);
116        if let Some(e) = input[start..].find(&close) {
117            cursor = start + e + close.len();
118        } else {
119            cursor = input.len();
120            break;
121        }
122    }
123    out.push_str(&input[cursor..]);
124    out
125}
126
127fn parse_implementation(block: &str) -> ImplementationDescription {
128    let label = inner_text(block, "label").unwrap_or_default();
129    let uuid = inner_text(block, "UUID").unwrap_or_default();
130    let realizes = inner_text(block, "realizes").unwrap_or_default();
131    let artifacts: Vec<String> = collect_inner_texts(block, "artifact");
132    ImplementationDescription {
133        label,
134        uuid,
135        artifacts,
136        realizes,
137        exec_params: BTreeMap::new(),
138        depends_on: Vec::new(),
139    }
140}
141
142fn parse_instance(block: &str) -> InstanceDeploymentDescription {
143    let name = inner_text(block, "name").unwrap_or_default();
144    let implementation = inner_text(block, "implementation").unwrap_or_default();
145    let node = inner_text(block, "node").unwrap_or_default();
146    let co_locate_with = collect_inner_texts(block, "coLocateWith");
147    InstanceDeploymentDescription {
148        name,
149        implementation,
150        node,
151        config_props: BTreeMap::new(),
152        co_locate_with,
153    }
154}
155
156fn parse_connection(block: &str) -> PlanConnection {
157    PlanConnection {
158        name: inner_text(block, "name").unwrap_or_default(),
159        source_instance: inner_text(block, "sourceInstance").unwrap_or_default(),
160        source_port: inner_text(block, "sourcePort").unwrap_or_default(),
161        target_instance: inner_text(block, "targetInstance").unwrap_or_default(),
162        target_port: inner_text(block, "targetPort").unwrap_or_default(),
163    }
164}
165
166fn inner_text(input: &str, tag: &str) -> Result<String, ParseError> {
167    let open = alloc::format!("<{tag}>");
168    let close = alloc::format!("</{tag}>");
169    let start = input
170        .find(&open)
171        .ok_or_else(|| ParseError::ExpectedTag(tag.to_string()))?
172        + open.len();
173    let end = input[start..]
174        .find(&close)
175        .ok_or(ParseError::UnterminatedTag)?
176        + start;
177    Ok(input[start..end].trim().to_string())
178}
179
180fn collect_inner_texts(input: &str, tag: &str) -> Vec<String> {
181    let open = alloc::format!("<{tag}>");
182    let close = alloc::format!("</{tag}>");
183    let mut out = Vec::new();
184    let mut cursor = 0;
185    while let Some(s) = input[cursor..].find(&open) {
186        let start = cursor + s + open.len();
187        if let Some(e) = input[start..].find(&close) {
188            let end = start + e;
189            out.push(input[start..end].trim().to_string());
190            cursor = end + close.len();
191        } else {
192            break;
193        }
194    }
195    out
196}
197
198fn iter_blocks(input: &str, tag: &str) -> Vec<String> {
199    let open = alloc::format!("<{tag}>");
200    let close = alloc::format!("</{tag}>");
201    let mut blocks = Vec::new();
202    let mut cursor = 0;
203    while let Some(s) = input[cursor..].find(&open) {
204        let start = cursor + s + open.len();
205        if let Some(e) = input[start..].find(&close) {
206            let end = start + e;
207            blocks.push(input[start..end].to_string());
208            cursor = end + close.len();
209        } else {
210            break;
211        }
212    }
213    blocks
214}
215
216fn format_err(e: &crate::plan::PlanError) -> String {
217    alloc::format!("{e}")
218}
219
220#[cfg(test)]
221#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
222mod tests {
223    use super::*;
224
225    const SAMPLE: &str = r#"
226<deploymentPlan>
227  <label>Plan1</label>
228  <UUID>uuid-1</UUID>
229  <realizes>RootApp</realizes>
230  <implementation>
231    <label>EchoImpl</label>
232    <UUID>echo-uuid</UUID>
233    <artifact>lib/echo.so</artifact>
234    <realizes>IDL:demo/Echo:1.0</realizes>
235  </implementation>
236  <instance>
237    <name>echo1</name>
238    <implementation>EchoImpl</implementation>
239    <node>Node1</node>
240  </instance>
241</deploymentPlan>
242"#;
243
244    #[test]
245    fn parses_minimal_plan() {
246        let plan = parse_plan_xml(SAMPLE).unwrap();
247        assert_eq!(plan.label, "Plan1");
248        assert_eq!(plan.uuid, "uuid-1");
249        assert_eq!(plan.implementations.len(), 1);
250        assert_eq!(
251            plan.implementations[0].artifacts,
252            alloc::vec!["lib/echo.so".to_string()]
253        );
254        assert_eq!(plan.instances.len(), 1);
255        assert_eq!(plan.instances[0].node, "Node1");
256    }
257
258    #[test]
259    fn parses_multiple_artifacts() {
260        let xml = r#"
261<deploymentPlan>
262  <label>P</label>
263  <UUID>u</UUID>
264  <implementation>
265    <label>I1</label>
266    <UUID>i1</UUID>
267    <artifact>a.so</artifact>
268    <artifact>b.so</artifact>
269    <realizes>IDL:X:1.0</realizes>
270  </implementation>
271</deploymentPlan>
272"#;
273        let plan = parse_plan_xml(xml).unwrap();
274        assert_eq!(plan.implementations[0].artifacts.len(), 2);
275    }
276
277    #[test]
278    fn validation_fails_on_unknown_impl_reference() {
279        let xml = r#"
280<deploymentPlan>
281  <label>P</label>
282  <UUID>u</UUID>
283  <instance>
284    <name>x</name>
285    <implementation>NoSuchImpl</implementation>
286    <node>N</node>
287  </instance>
288</deploymentPlan>
289"#;
290        let err = parse_plan_xml(xml).unwrap_err();
291        match err {
292            ParseError::ValidationFailed(_) => {}
293            e => panic!("unexpected error variant: {e}"),
294        }
295    }
296
297    #[test]
298    fn missing_root_tag_returns_expected_tag() {
299        let err = parse_plan_xml("<empty/>").unwrap_err();
300        assert_eq!(err, ParseError::ExpectedTag("deploymentPlan".into()));
301    }
302
303    #[test]
304    fn co_locate_with_parsed() {
305        let xml = r#"
306<deploymentPlan>
307  <label>P</label>
308  <UUID>u</UUID>
309  <implementation>
310    <label>I</label>
311    <UUID>i</UUID>
312    <artifact>a.so</artifact>
313    <realizes>IDL:X:1.0</realizes>
314  </implementation>
315  <instance>
316    <name>a</name>
317    <implementation>I</implementation>
318    <node>N</node>
319  </instance>
320  <instance>
321    <name>b</name>
322    <implementation>I</implementation>
323    <node>N</node>
324    <coLocateWith>a</coLocateWith>
325  </instance>
326</deploymentPlan>
327"#;
328        let plan = parse_plan_xml(xml).unwrap();
329        assert_eq!(
330            plan.instances[1].co_locate_with,
331            alloc::vec!["a".to_string()]
332        );
333    }
334}