use crate::xml_input as xml_sec_xml_input;
use std::borrow::Cow;
pub use crate::xml_input_shared::Error as XmlEncodingError;
pub fn decode_xml_octets(
bytes: &[u8],
maximum_decoded_bytes: usize,
) -> Result<Cow<'_, str>, XmlEncodingError> {
xml_sec_xml_input::decode_xml_bounded(bytes, None, maximum_decoded_bytes)
}
#[cfg(test)]
mod tests {
use super::*;
fn utf16_bytes(xml: &str, little_endian: bool, bom: bool) -> Vec<u8> {
let mut bytes = if bom {
if little_endian {
vec![0xff, 0xfe]
} else {
vec![0xfe, 0xff]
}
} else {
Vec::new()
};
bytes.extend(xml.encode_utf16().flat_map(|value| {
if little_endian {
value.to_le_bytes()
} else {
value.to_be_bytes()
}
}));
bytes
}
#[test]
fn transcoding_normalizes_non_utf8_declarations() {
for bytes in [
utf16_bytes(
"<?xml version=\"1.0\" encoding=\"UTF-16\"?><root/>",
true,
true,
),
b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root>caf\xe9</root>".to_vec(),
] {
let decoded =
decode_xml_octets(&bytes, 1_024).expect("declared XML encoding must decode");
assert!(decoded.contains("encoding=\"UTF-8\"") || decoded.contains("encoding='UTF-8'"));
}
}
#[test]
fn declared_single_byte_xml_is_transcoded_strictly() {
let latin1 = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root>caf\xe9</root>";
let decoded = decode_xml_octets(latin1, 1_024).expect("declared Latin-1 XML must decode");
assert!(decoded.contains("<root>café</root>"));
assert!(decoded.contains("encoding=\"UTF-8\""));
}
#[test]
fn byte_signatures_and_declarations_must_agree() {
let conflicting = utf16_bytes(
"<?xml version=\"1.0\" encoding=\"UTF-16BE\"?><root/>",
true,
true,
);
assert!(matches!(
decode_xml_octets(&conflicting, 1_024),
Err(XmlEncodingError::ConflictingEncoding(_))
));
let matching_signature = utf16_bytes(
"<?xml version=\"1.0\" encoding=\"UTF-16BE\"?><root/>",
false,
true,
);
assert!(decode_xml_octets(&matching_signature, 1_024).is_ok());
let bomless_generic = utf16_bytes(
"<?xml version=\"1.0\" encoding=\"UTF-16\"?><root/>",
true,
false,
);
assert!(matches!(
decode_xml_octets(&bomless_generic, 1_024),
Err(XmlEncodingError::MissingUtf16ByteOrder)
));
}
#[test]
fn utf8_is_borrowed_and_malformed_bytes_are_rejected() {
let xml = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>";
assert!(matches!(
decode_xml_octets(xml, xml.len()),
Ok(Cow::Borrowed(_))
));
assert!(decode_xml_octets(b"<root>\xff</root>", 1_024).is_err());
}
#[test]
fn declaration_detection_ignores_other_processing_instructions() {
for xml in [
r#"<?xml-stylesheet encoding="ISO-8859-1"?><root/>"#,
r#"<root data-encoding="ISO-8859-1"/>"#,
] {
decode_xml_octets(xml.as_bytes(), 1_024)
.expect("ordinary content does not select encoding");
}
}
#[test]
fn transcoding_stops_at_the_decoded_document_limit() {
let bytes = utf16_bytes("<?xml version=\"1.0\"?><root>expanded</root>", true, true);
assert!(matches!(
decode_xml_octets(&bytes, 16),
Err(XmlEncodingError::DecodedLimit { maximum: 16, .. })
));
}
}