Skip to main content

soap_server/
xml_escape.rs

1//! XML character escaping helpers.
2//!
3//! `escape_text` and `escape_attr` are intentionally separate even though
4//! both currently delegate to `quick_xml::escape::escape` (which handles all
5//! five XML special characters).  Keeping them distinct lets callers
6//! communicate intent, and leaves room to differentiate behaviour in the
7//! future (e.g. skipping `"` / `'` in text-only contexts for smaller output).
8//!
9//! # Decision: `detail` escaping
10//! `SoapFault::detail` is always treated as **plain text** in this codebase.
11//! All call sites in soap-server construct detail from internal error messages
12//! or static strings; none embed pre-formed XML markup.  Therefore `detail`
13//! content is escaped with `escape_text` just like `reason`.  If a future
14//! caller needs to embed literal XML in a fault detail element it must
15//! construct a `SoapFault` whose `detail` field already contains the correct
16//! escaped representation, or it must supply the literal XML via a wrapper
17//! element and bypass this helper.
18
19/// Escape a string for use in XML text content (`&`, `<`, `>`, `"`, `'`).
20///
21/// Uses [`quick_xml::escape::escape`] which covers all five XML special chars,
22/// producing `&amp;`, `&lt;`, `&gt;`, `&quot;`, and `&apos;` as needed.
23pub fn escape_text(s: &str) -> String {
24    quick_xml::escape::escape(s).into_owned()
25}
26
27/// Escape a string for use in an XML attribute value (`&`, `<`, `>`, `"`, `'`).
28///
29/// Identical to [`escape_text`] — both `"` and `'` are escaped so the result
30/// is safe inside either single- or double-quoted attribute values.
31pub fn escape_attr(s: &str) -> String {
32    quick_xml::escape::escape(s).into_owned()
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38
39    #[test]
40    fn escape_text_all_special_chars() {
41        let input = r#"& < > " '"#;
42        let out = escape_text(input);
43        assert_eq!(out, "&amp; &lt; &gt; &quot; &apos;");
44    }
45
46    #[test]
47    fn escape_attr_all_special_chars() {
48        let input = r#"& < > " '"#;
49        let out = escape_attr(input);
50        assert_eq!(out, "&amp; &lt; &gt; &quot; &apos;");
51    }
52
53    #[test]
54    fn escape_text_plain_string_unchanged() {
55        let s = "hello world";
56        assert_eq!(escape_text(s), s);
57    }
58
59    #[test]
60    fn escape_attr_plain_string_unchanged() {
61        let s = "hello world";
62        assert_eq!(escape_attr(s), s);
63    }
64
65    #[test]
66    fn escape_text_ampersand_only() {
67        assert_eq!(escape_text("Acme & Sons"), "Acme &amp; Sons");
68    }
69
70    #[test]
71    fn escape_text_angle_brackets() {
72        assert_eq!(escape_text("<tag>"), "&lt;tag&gt;");
73    }
74
75    #[test]
76    fn escape_attr_double_quote() {
77        assert_eq!(escape_attr(r#"say "hi""#), "say &quot;hi&quot;");
78    }
79
80    #[test]
81    fn escape_attr_single_quote() {
82        assert_eq!(escape_attr("it's"), "it&apos;s");
83    }
84}