1use crate::xml_input as xml_sec_xml_input;
4use std::borrow::Cow;
5
6pub use crate::xml_input_shared::Error as XmlEncodingError;
10
11pub 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 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}