Skip to main content

zerodds_soap/
envelope.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! SOAP 1.2 Envelope — W3C SOAP 1.2 Part 1 §5.
5//!
6//! `<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">`
7//! mit `<soap:Header>?` und `<soap:Body>`.
8
9use alloc::format;
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12
13use zerodds_xml_wire::emitter::{EmitError, XmlEmitter};
14use zerodds_xml_wire::parser::{Event, ParseError, XmlParser};
15
16/// SOAP 1.2 Envelope-Namespace (W3C 2003).
17pub const SOAP_12_NS: &str = "http://www.w3.org/2003/05/soap-envelope";
18
19/// SOAP-Envelope (Header + Body).
20#[derive(Debug, Clone, PartialEq, Eq, Default)]
21pub struct Envelope {
22    /// Optional Header-Inhalt als Raw-XML.
23    pub header_xml: Option<String>,
24    /// Body-Inhalt als Raw-XML (z.B. ein Operation-Request-Element).
25    pub body_xml: String,
26}
27
28/// Envelope-Fehler.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum EnvelopeError {
31    /// XML-Parse-Fehler.
32    Parse(ParseError),
33    /// XML-Emit-Fehler.
34    Emit(EmitError),
35    /// Kein `<soap:Envelope>`-Root.
36    NoEnvelope,
37    /// Kein `<soap:Body>` im Envelope.
38    NoBody,
39}
40
41impl core::fmt::Display for EnvelopeError {
42    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
43        match self {
44            Self::Parse(e) => write!(f, "parse: {e}"),
45            Self::Emit(e) => write!(f, "emit: {e}"),
46            Self::NoEnvelope => f.write_str("no <soap:Envelope> root"),
47            Self::NoBody => f.write_str("no <soap:Body>"),
48        }
49    }
50}
51
52#[cfg(feature = "std")]
53impl std::error::Error for EnvelopeError {}
54
55impl From<ParseError> for EnvelopeError {
56    fn from(e: ParseError) -> Self {
57        Self::Parse(e)
58    }
59}
60impl From<EmitError> for EnvelopeError {
61    fn from(e: EmitError) -> Self {
62        Self::Emit(e)
63    }
64}
65
66/// Baut einen SOAP-1.2 Envelope-String. Spec §5.1.
67///
68/// # Errors
69/// `Emit` wenn der Body- oder Header-Tag-Name ungueltig ist.
70pub fn build_envelope(env: &Envelope) -> Result<String, EnvelopeError> {
71    let mut e = XmlEmitter::new();
72    e.declaration();
73    e.start_element("soap:Envelope", &[("xmlns:soap", SOAP_12_NS)])?;
74    if let Some(h) = &env.header_xml {
75        // Header is raw XML — caller is responsible for valid form.
76        // We emit a wrapping <soap:Header> and inject raw between.
77        e.start_element("soap:Header", &[])?;
78        // We can't push raw XML through XmlEmitter, so we drain it out
79        // to a String and concatenate manually.
80        let mut prefix = e.finish();
81        prefix.push_str(h);
82        prefix.push_str("</soap:Header>");
83        let mut e2 = XmlEmitter::new();
84        e2.start_element("soap:Body", &[])?;
85        prefix.push_str(&e2.finish());
86        prefix.push_str(&env.body_xml);
87        prefix.push_str("</soap:Body></soap:Envelope>");
88        return Ok(prefix);
89    }
90    // No header: emit Body directly.
91    e.start_element("soap:Body", &[])?;
92    let mut out = e.finish();
93    out.push_str(&env.body_xml);
94    out.push_str("</soap:Body></soap:Envelope>");
95    Ok(out)
96}
97
98/// Parst einen SOAP-1.2 Envelope-String zurueck zu [`Envelope`].
99///
100/// # Errors
101/// `NoEnvelope` / `NoBody` / `Parse`.
102pub fn parse_envelope(xml: &str) -> Result<Envelope, EnvelopeError> {
103    // Find <soap:Envelope> ... </soap:Envelope> bounds.
104    let env_open = xml
105        .find("<soap:Envelope")
106        .ok_or(EnvelopeError::NoEnvelope)?;
107    let env_inner_start =
108        xml[env_open..].find('>').ok_or(EnvelopeError::NoEnvelope)? + env_open + 1;
109    let env_close = xml
110        .rfind("</soap:Envelope>")
111        .ok_or(EnvelopeError::NoEnvelope)?;
112    let inner = &xml[env_inner_start..env_close];
113
114    // Extract header (optional) and body.
115    let header_xml = inner_block(inner, "soap:Header");
116    let body_xml = inner_block(inner, "soap:Body").ok_or(EnvelopeError::NoBody)?;
117
118    // Sanity-parse the body to make sure it's at least balanced XML
119    // (best-effort — we don't reject content that fails our minimal
120    // parser, since the spec allows arbitrary user-types).
121    let _ = XmlParser::new(&body_xml).collect::<Result<Vec<Event>, _>>();
122
123    Ok(Envelope {
124        header_xml,
125        body_xml,
126    })
127}
128
129fn inner_block(input: &str, tag: &str) -> Option<String> {
130    let open_marker = format!("<{tag}");
131    let close_marker = format!("</{tag}>");
132    let open = input.find(&open_marker)?;
133    let inner_start = input[open..].find('>')? + open + 1;
134    let close = input.find(&close_marker)?;
135    if close <= inner_start {
136        return None;
137    }
138    Some(input[inner_start..close].trim().to_string())
139}
140
141#[cfg(test)]
142#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn build_simple_body_only() {
148        let env = Envelope {
149            header_xml: None,
150            body_xml: "<m:Get xmlns:m=\"x\"/>".into(),
151        };
152        let out = build_envelope(&env).unwrap();
153        assert!(out.contains("<soap:Envelope"));
154        assert!(out.contains("<soap:Body>"));
155        assert!(out.contains("<m:Get"));
156        assert!(out.ends_with("</soap:Envelope>"));
157    }
158
159    #[test]
160    fn build_with_header() {
161        let env = Envelope {
162            header_xml: Some("<h:Token xmlns:h=\"y\"/>".into()),
163            body_xml: "<m:Op/>".into(),
164        };
165        let out = build_envelope(&env).unwrap();
166        assert!(out.contains("<soap:Header>"));
167        assert!(out.contains("<h:Token"));
168        assert!(out.contains("<soap:Body>"));
169    }
170
171    #[test]
172    fn parse_round_trip() {
173        let env = Envelope {
174            header_xml: Some("<h:Token xmlns:h=\"y\"/>".into()),
175            body_xml: "<m:Op xmlns:m=\"x\"/>".into(),
176        };
177        let xml = build_envelope(&env).unwrap();
178        let parsed = parse_envelope(&xml).unwrap();
179        assert!(parsed.body_xml.contains("m:Op"));
180        assert!(parsed.header_xml.unwrap().contains("h:Token"));
181    }
182
183    #[test]
184    fn parse_missing_envelope_rejected() {
185        let xml = "<foo/>";
186        assert!(matches!(
187            parse_envelope(xml),
188            Err(EnvelopeError::NoEnvelope)
189        ));
190    }
191
192    #[test]
193    fn parse_missing_body_rejected() {
194        let xml = format!("<soap:Envelope xmlns:soap=\"{SOAP_12_NS}\"></soap:Envelope>");
195        assert!(matches!(parse_envelope(&xml), Err(EnvelopeError::NoBody)));
196    }
197
198    #[test]
199    fn build_includes_namespace_declaration() {
200        let env = Envelope {
201            header_xml: None,
202            body_xml: String::new(),
203        };
204        let out = build_envelope(&env).unwrap();
205        assert!(out.contains(SOAP_12_NS));
206    }
207}