aws_smithy_xml/
escape.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use std::borrow::Cow;
7use std::fmt::Write;
8
9const ESCAPES: &[char] = &[
10    '&', '\'', '\"', '<', '>', '\u{00D}', '\u{00A}', '\u{0085}', '\u{2028}',
11];
12
13pub(crate) fn escape(s: &str) -> Cow<'_, str> {
14    let mut remaining = s;
15    if !s.contains(ESCAPES) {
16        return Cow::Borrowed(s);
17    }
18    let mut out = String::new();
19    while let Some(idx) = remaining.find(ESCAPES) {
20        out.push_str(&remaining[..idx]);
21        remaining = &remaining[idx..];
22        let mut idxs = remaining.char_indices();
23        let (_, chr) = idxs.next().expect("must not be none");
24        match chr {
25            '>' => out.push_str("&gt;"),
26            '<' => out.push_str("&lt;"),
27            '\'' => out.push_str("&apos;"),
28            '"' => out.push_str("&quot;"),
29            '&' => out.push_str("&amp;"),
30            // push a hex escape sequence
31            other => {
32                write!(&mut out, "&#x{:X};", other as u32).expect("write to string cannot fail")
33            }
34        };
35        match idxs.next() {
36            None => remaining = "",
37            Some((idx, _)) => remaining = &remaining[idx..],
38        }
39    }
40    out.push_str(remaining);
41    Cow::Owned(out)
42}
43
44#[cfg(test)]
45mod test {
46    #[test]
47    fn escape_basic() {
48        let inp = "<helo>&\"'";
49        assert_eq!(escape(inp), "&lt;helo&gt;&amp;&quot;&apos;");
50    }
51
52    #[test]
53    fn escape_eol_encoding_sep() {
54        let test_cases = vec![
55            ("CiAK", "&#xA; &#xA;"),                                      // '\n \n'
56            ("YQ0KIGIKIGMN", "a&#xD;&#xA; b&#xA; c&#xD;"),                // 'a\r\n b\n c\r'
57            ("YQ3ChSBiwoU", "a&#xD;&#x85; b&#x85;"),                      // 'a\r\u0085 b\u0085'
58            ("YQ3igKggYsKFIGPigKg=", "a&#xD;&#x2028; b&#x85; c&#x2028;"), // 'a\r\u2028 b\u0085 c\u2028'
59        ];
60        for (base64_encoded, expected_xml_output) in test_cases {
61            let bytes = base64::decode(base64_encoded).expect("valid base64");
62            let input = String::from_utf8(bytes).expect("valid utf-8");
63            assert_eq!(escape(&input), expected_xml_output);
64        }
65    }
66
67    use crate::escape::escape;
68    use proptest::proptest;
69    proptest! {
70        /// Test that arbitrary strings round trip after being escaped and unescaped
71        #[test]
72        fn round_trip(s: String) {
73            let encoded = escape(&s);
74            let decoded = crate::unescape::unescape(&encoded).expect("encoded should be valid decoded");
75            assert_eq!(decoded, s);
76        }
77    }
78}