Skip to main content

cadmpeg_core/decode/
utf16.rs

1// SPDX-License-Identifier: Apache-2.0
2//! UTF-16LE text decoding.
3
4use crate::CodecError;
5
6use super::{DecodeContext, View};
7
8#[derive(Clone, Copy)]
9enum Surrogates {
10    Reject,
11    Replace,
12}
13
14impl DecodeContext<'_> {
15    /// Decodes `units` UTF-16LE code units, rejecting unpaired surrogates.
16    pub fn utf16le_text(
17        &self,
18        bytes: &[u8],
19        units: usize,
20        trim_nul: bool,
21        _operation: &'static str,
22    ) -> Result<String, CodecError> {
23        decode_text(bytes, units, trim_nul, Surrogates::Reject)
24    }
25
26    /// Decodes `units` UTF-16LE code units, replacing unpaired surrogates with
27    /// U+FFFD.
28    pub fn utf16le_lossy_text(
29        &self,
30        bytes: &[u8],
31        units: usize,
32        trim_nul: bool,
33        _operation: &'static str,
34    ) -> Result<String, CodecError> {
35        decode_text(bytes, units, trim_nul, Surrogates::Replace)
36    }
37}
38
39fn decode_text(
40    bytes: &[u8],
41    units: usize,
42    trim_nul: bool,
43    surrogates: Surrogates,
44) -> Result<String, CodecError> {
45    let byte_len = units
46        .checked_mul(2)
47        .ok_or_else(|| CodecError::malformed("UTF-16LE byte length overflow"))?;
48    let bytes = bytes
49        .get(..byte_len)
50        .ok_or_else(|| CodecError::malformed("truncated UTF-16LE text"))?;
51    characters(bytes, trim_nul, surrogates)
52        .collect::<Result<String, _>>()
53        .map_err(|_| CodecError::malformed("invalid UTF-16LE surrogate sequence"))
54}
55
56fn characters(
57    bytes: &[u8],
58    trim_nul: bool,
59    surrogates: Surrogates,
60) -> impl Iterator<Item = Result<char, std::char::DecodeUtf16Error>> + '_ {
61    let mut view = View::over_retained(bytes);
62    char::decode_utf16(
63        std::iter::from_fn(move || view.u16_le()).take_while(move |unit| !trim_nul || *unit != 0),
64    )
65    .map(move |character| match (surrogates, character) {
66        (Surrogates::Replace, Err(_)) => Ok(char::REPLACEMENT_CHARACTER),
67        (_, character) => character,
68    })
69}
70
71#[cfg(test)]
72mod tests {
73    use crate::decode::{DecodeArena, DecodeContext, DecodePolicy};
74    use crate::CodecError;
75
76    #[test]
77    fn utf16le_lossy_text_trims_first_nul() {
78        let arena = DecodeArena::new();
79        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::default())
80            .expect("empty root");
81        assert_eq!(
82            ctx.utf16le_lossy_text(&[0, 0xd8, 0, 0, 0, 0xdc], 3, true, "replacement test")
83                .expect("terminated replacement text"),
84            "�"
85        );
86    }
87
88    #[test]
89    fn utf16le_text_rejects_odd_byte_window() {
90        let arena = DecodeArena::new();
91        let (ctx, _) = DecodeContext::from_root_bytes(b"", &arena, &DecodePolicy::default())
92            .expect("empty root context");
93        assert!(matches!(
94            ctx.utf16le_text(&[0x41], 1, false, "UTF-16 test"),
95            Err(CodecError::Malformed(_))
96        ));
97        assert!(matches!(
98            ctx.utf16le_text(&[], usize::MAX, false, "UTF-16 test"),
99            Err(CodecError::Malformed(_))
100        ));
101    }
102
103    #[test]
104    fn utf16le_text_rejects_invalid_surrogates() {
105        let arena = DecodeArena::new();
106        let (ctx, _) = DecodeContext::from_root_bytes(b"", &arena, &DecodePolicy::default())
107            .expect("empty root context");
108        for bytes in [&[0, 0xd8][..], &[0, 0xdc][..], &[0, 0xd8, 0x41, 0][..]] {
109            assert!(matches!(
110                ctx.utf16le_text(bytes, bytes.len() / 2, false, "UTF-16 test"),
111                Err(CodecError::Malformed(_))
112            ));
113        }
114    }
115}