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 `&`, `<`, `>`, `"`, and `'` 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, "& < > " '");
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, "& < > " '");
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 & Sons");
68 }
69
70 #[test]
71 fn escape_text_angle_brackets() {
72 assert_eq!(escape_text("<tag>"), "<tag>");
73 }
74
75 #[test]
76 fn escape_attr_double_quote() {
77 assert_eq!(escape_attr(r#"say "hi""#), "say "hi"");
78 }
79
80 #[test]
81 fn escape_attr_single_quote() {
82 assert_eq!(escape_attr("it's"), "it's");
83 }
84}