Skip to main content

xml_sec/
encoding.rs

1//! XML byte-encoding detection shared by process and transform boundaries.
2
3use crate::xml_input as xml_sec_xml_input;
4use std::borrow::Cow;
5
6/// Shared decoder errors, including unsupported encodings and decoded-size limits.
7/// This replaces the former XML-only error variants rather than collapsing new
8/// failure classes into misleading legacy variants.
9pub use crate::xml_input_shared::Error as XmlEncodingError;
10
11/// Decode XML 1.0 octets into the backend-neutral Unicode parser contract.
12///
13/// UTF-8 input remains borrowed. Other declared XML encodings are decoded
14/// strictly and their declaration is normalized to UTF-8 before parsing.
15/// Callers must supply a size ceiling; an implicit unbounded compatibility path
16/// would permit untrusted input to allocate without the operation's budget.
17pub fn decode_xml_octets(
18    bytes: &[u8],
19    maximum_decoded_bytes: usize,
20) -> Result<Cow<'_, str>, XmlEncodingError> {
21    xml_sec_xml_input::decode_xml_bounded(bytes, None, maximum_decoded_bytes)
22}
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27
28    fn utf16_bytes(xml: &str, little_endian: bool, bom: bool) -> Vec<u8> {
29        let mut bytes = if bom {
30            if little_endian {
31                vec![0xff, 0xfe]
32            } else {
33                vec![0xfe, 0xff]
34            }
35        } else {
36            Vec::new()
37        };
38        bytes.extend(xml.encode_utf16().flat_map(|value| {
39            if little_endian {
40                value.to_le_bytes()
41            } else {
42                value.to_be_bytes()
43            }
44        }));
45        bytes
46    }
47
48    #[test]
49    fn transcoding_normalizes_non_utf8_declarations() {
50        for bytes in [
51            utf16_bytes(
52                "<?xml version=\"1.0\" encoding=\"UTF-16\"?><root/>",
53                true,
54                true,
55            ),
56            b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root>caf\xe9</root>".to_vec(),
57        ] {
58            let decoded =
59                decode_xml_octets(&bytes, 1_024).expect("declared XML encoding must decode");
60            assert!(decoded.contains("encoding=\"UTF-8\"") || decoded.contains("encoding='UTF-8'"));
61        }
62    }
63
64    #[test]
65    fn declared_single_byte_xml_is_transcoded_strictly() {
66        // XML permits declared encodings beyond its mandatory UTF-8/UTF-16 baseline.
67        let latin1 = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root>caf\xe9</root>";
68        let decoded = decode_xml_octets(latin1, 1_024).expect("declared Latin-1 XML must decode");
69        assert!(decoded.contains("<root>café</root>"));
70        assert!(decoded.contains("encoding=\"UTF-8\""));
71    }
72
73    #[test]
74    fn byte_signatures_and_declarations_must_agree() {
75        let conflicting = utf16_bytes(
76            "<?xml version=\"1.0\" encoding=\"UTF-16BE\"?><root/>",
77            true,
78            true,
79        );
80        assert!(matches!(
81            decode_xml_octets(&conflicting, 1_024),
82            Err(XmlEncodingError::ConflictingEncoding(_))
83        ));
84
85        let matching_signature = utf16_bytes(
86            "<?xml version=\"1.0\" encoding=\"UTF-16BE\"?><root/>",
87            false,
88            true,
89        );
90        assert!(decode_xml_octets(&matching_signature, 1_024).is_ok());
91
92        let bomless_generic = utf16_bytes(
93            "<?xml version=\"1.0\" encoding=\"UTF-16\"?><root/>",
94            true,
95            false,
96        );
97        assert!(matches!(
98            decode_xml_octets(&bomless_generic, 1_024),
99            Err(XmlEncodingError::MissingUtf16ByteOrder)
100        ));
101    }
102
103    #[test]
104    fn utf8_is_borrowed_and_malformed_bytes_are_rejected() {
105        let xml = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>";
106        assert!(matches!(
107            decode_xml_octets(xml, xml.len()),
108            Ok(Cow::Borrowed(_))
109        ));
110        assert!(decode_xml_octets(b"<root>\xff</root>", 1_024).is_err());
111    }
112
113    #[test]
114    fn declaration_detection_ignores_other_processing_instructions() {
115        for xml in [
116            r#"<?xml-stylesheet encoding="ISO-8859-1"?><root/>"#,
117            r#"<root data-encoding="ISO-8859-1"/>"#,
118        ] {
119            decode_xml_octets(xml.as_bytes(), 1_024)
120                .expect("ordinary content does not select encoding");
121        }
122    }
123
124    #[test]
125    fn transcoding_stops_at_the_decoded_document_limit() {
126        let bytes = utf16_bytes("<?xml version=\"1.0\"?><root>expanded</root>", true, true);
127        assert!(matches!(
128            decode_xml_octets(&bytes, 16),
129            Err(XmlEncodingError::DecodedLimit { maximum: 16, .. })
130        ));
131    }
132}