Skip to main content

pptxboss_core/
encoding.rs

1//! Transcoding of UTF-16 XML parts to UTF-8 (ECMA-376 Part 2 allows both
2//! encodings for XML parts; every parser in this crate reads UTF-8).
3
4/// The UTF-8 bytes of `bytes` when they are a UTF-16 XML part (a byte
5/// order mark or `<?` in either byte order), None when they are not.
6/// Unpaired surrogates become U+FFFD.
7pub fn utf16_xml_to_utf8(bytes: &[u8]) -> Option<Vec<u8>> {
8    let (little_endian, start) = match bytes {
9        [0xff, 0xfe, ..] => (true, 2),
10        [0xfe, 0xff, ..] => (false, 2),
11        [b'<', 0, b'?', 0, ..] => (true, 0),
12        [0, b'<', 0, b'?', ..] => (false, 0),
13        _ => return None,
14    };
15    let (pairs, _) = bytes[start..].as_chunks::<2>();
16    let units = pairs.iter().map(|pair| match little_endian {
17        true => u16::from_le_bytes(*pair),
18        false => u16::from_be_bytes(*pair),
19    });
20    let mut out = String::with_capacity(bytes.len() / 2);
21    for ch in char::decode_utf16(units) {
22        out.push(ch.unwrap_or(char::REPLACEMENT_CHARACTER));
23    }
24    Some(out.into_bytes())
25}
26
27/// Whether `bytes` look like a UTF-16 XML part; see [`utf16_xml_to_utf8`].
28pub fn is_utf16_xml(bytes: &[u8]) -> bool {
29    matches!(
30        bytes,
31        [0xff, 0xfe, ..] | [0xfe, 0xff, ..] | [b'<', 0, b'?', 0, ..] | [0, b'<', 0, b'?', ..]
32    )
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38
39    fn utf16le(text: &str, bom: bool) -> Vec<u8> {
40        let mut out = Vec::new();
41        if bom {
42            out.extend([0xff, 0xfe]);
43        }
44        for unit in text.encode_utf16() {
45            out.extend(unit.to_le_bytes());
46        }
47        out
48    }
49
50    #[test]
51    fn transcodes_both_byte_orders_with_and_without_a_mark() {
52        let xml = "<?xml version=\"1.0\" encoding=\"UTF-16\"?><a>héllo 😀</a>";
53        assert_eq!(
54            utf16_xml_to_utf8(&utf16le(xml, true)).unwrap(),
55            xml.as_bytes()
56        );
57        assert_eq!(
58            utf16_xml_to_utf8(&utf16le(xml, false)).unwrap(),
59            xml.as_bytes()
60        );
61        let mut big_endian = vec![0xfe, 0xff];
62        for unit in xml.encode_utf16() {
63            big_endian.extend(unit.to_be_bytes());
64        }
65        assert_eq!(utf16_xml_to_utf8(&big_endian).unwrap(), xml.as_bytes());
66    }
67
68    #[test]
69    fn leaves_utf8_and_binaries_alone() {
70        assert!(utf16_xml_to_utf8(b"<?xml version=\"1.0\"?><a/>").is_none());
71        assert!(utf16_xml_to_utf8(&[0x89, b'P', b'N', b'G']).is_none());
72        assert!(utf16_xml_to_utf8(&[0xff, 0xd8, 0xff, 0xe0]).is_none());
73        assert!(!is_utf16_xml(b""));
74    }
75
76    #[test]
77    fn a_lone_surrogate_becomes_the_replacement_character() {
78        let mut bytes = utf16le("<a>", true);
79        bytes.extend(0xd800u16.to_le_bytes());
80        bytes.extend(utf16le("</a>", false));
81        assert_eq!(
82            utf16_xml_to_utf8(&bytes).unwrap(),
83            "<a>\u{fffd}</a>".as_bytes()
84        );
85    }
86}