Skip to main content

oxixml_encoding/decoder/
mod.rs

1//! Incremental, chunk-safe decoding from bytes to [`String`]s.
2
3mod single_byte;
4mod utf16;
5mod utf8;
6
7use crate::jis::{euc_jp, iso2022jp, shift_jis};
8use crate::label::Encoding;
9
10/// An error reported by [`Decoder::decode_to_string`].
11///
12/// All positional information (`offset`, `length`) is expressed in bytes and is relative to the
13/// `input` slice passed to the specific [`decode_to_string`](Decoder::decode_to_string) call
14/// that returned this error — *not* to the logical start of the overall byte stream. A caller
15/// tracking a running total offset across chunks should add the sum of the lengths of all
16/// previously-consumed chunks to `offset` to obtain a stream-relative position.
17///
18/// When the malformed data spans a chunk boundary (part of it was buffered internally from a
19/// previous call), `offset` is clamped to `0`: the problem became visible only once the new
20/// bytes arrived, so `0` is the most useful position a caller can act on (for example, to slice
21/// `input[..0]` off before the error and report the rest as unconsumed).
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct DecodeError {
24    /// Byte offset within the `input` slice of the call that failed.
25    pub offset: usize,
26    /// The number of bytes that make up the malformed/incomplete unit, where known. This is a
27    /// hint for error-recovery strategies (e.g. "skip `length` bytes and retry"); it may be `0`
28    /// when no specific byte range is at fault (e.g. an unpaired surrogate detected only once a
29    /// *following* code unit failed to complete the pair).
30    pub length: usize,
31    /// What kind of problem was found.
32    pub kind: DecodeErrorKind,
33}
34
35/// The kind of problem a [`DecodeError`] reports.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum DecodeErrorKind {
38    /// A byte sequence that can never be valid in this encoding (e.g. a UTF-8 continuation byte
39    /// in a position where a lead byte was expected, or an overlong encoding).
40    MalformedSequence,
41    /// The input ended in the middle of a multi-byte sequence and the caller signaled `last =
42    /// true`, so no further bytes will arrive to complete it.
43    TruncatedSequence,
44    /// A UTF-16 high surrogate was not followed by a low surrogate (or vice versa).
45    UnpairedSurrogate,
46    /// A byte outside the encoding's repertoire was found (e.g. `0x80..=0xFF` in
47    /// [`Encoding::Ascii`]).
48    DisallowedByte,
49}
50
51impl std::fmt::Display for DecodeError {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        let what = match self.kind {
54            DecodeErrorKind::MalformedSequence => "malformed byte sequence",
55            DecodeErrorKind::TruncatedSequence => "truncated byte sequence at end of input",
56            DecodeErrorKind::UnpairedSurrogate => "unpaired UTF-16 surrogate",
57            DecodeErrorKind::DisallowedByte => "byte outside the encoding's repertoire",
58        };
59        write!(
60            f,
61            "{what} at offset {} (length {})",
62            self.offset, self.length
63        )
64    }
65}
66
67impl std::error::Error for DecodeError {}
68
69/// An incremental decoder for one [`Encoding`].
70///
71/// A `Decoder` is created via [`Encoding::new_decoder`] and is fed the byte stream in one or
72/// more calls to [`decode_to_string`](Decoder::decode_to_string) (or its lossy counterpart,
73/// [`decode_to_string_lossy`](Decoder::decode_to_string_lossy)). Chunks may be split at any
74/// byte boundary — including in the middle of a multi-byte sequence or a UTF-16 surrogate pair
75/// — and the decoder carries the necessary state between calls to produce output identical to
76/// decoding the concatenation of all chunks in one call.
77///
78/// The final chunk (which may be empty) must be passed with `last = true` so the decoder can
79/// detect a sequence left truncated at the true end of the stream.
80///
81/// # Examples
82///
83/// Feeding a UTF-8 string one byte at a time still reassembles multi-byte characters correctly:
84///
85/// ```
86/// use oxixml_encoding::Encoding;
87///
88/// let text = "a→b🎉c"; // mixes ASCII, a 3-byte BMP arrow, and a 4-byte astral emoji.
89/// let mut decoder = Encoding::Utf8.new_decoder();
90/// let mut out = String::new();
91/// let bytes = text.as_bytes();
92/// for i in 0..bytes.len() {
93///     let last = i + 1 == bytes.len();
94///     decoder.decode_to_string(&bytes[i..=i], &mut out, last).expect("valid utf-8");
95/// }
96/// assert_eq!(out, text);
97/// ```
98#[derive(Debug, Clone)]
99pub struct Decoder {
100    kind: DecoderKind,
101}
102
103#[derive(Debug, Clone)]
104enum DecoderKind {
105    Utf8(utf8::Utf8State),
106    Utf16Le(utf16::Utf16State),
107    Utf16Be(utf16::Utf16State),
108    Latin1,
109    Windows1252,
110    Ascii,
111    /// Any of the 26 legacy single-byte encodings backed by a generated 128-entry upper-half
112    /// table (see [`Encoding::is_legacy_single_byte_table`]). Stateless, like the three variants
113    /// above, so the specific [`Encoding`] is carried directly rather than via a per-encoding
114    /// unit-struct variant.
115    LegacySingleByte(Encoding),
116    ShiftJis(shift_jis::ShiftJisState),
117    EucJp(euc_jp::EucJpState),
118    Iso2022Jp(iso2022jp::Iso2022JpState),
119}
120
121impl Decoder {
122    /// Create a new decoder for `encoding`. Prefer [`Encoding::new_decoder`], which forwards
123    /// here.
124    #[must_use]
125    pub(crate) const fn new(encoding: Encoding) -> Decoder {
126        let kind = match encoding {
127            Encoding::Utf8 => DecoderKind::Utf8(utf8::Utf8State::new()),
128            Encoding::Utf16Le => DecoderKind::Utf16Le(utf16::Utf16State::new()),
129            Encoding::Utf16Be => DecoderKind::Utf16Be(utf16::Utf16State::new()),
130            Encoding::Latin1 => DecoderKind::Latin1,
131            Encoding::Windows1252 => DecoderKind::Windows1252,
132            Encoding::Ascii => DecoderKind::Ascii,
133            Encoding::ShiftJis => DecoderKind::ShiftJis(shift_jis::ShiftJisState::new()),
134            Encoding::EucJp => DecoderKind::EucJp(euc_jp::EucJpState::new()),
135            Encoding::Iso2022Jp => DecoderKind::Iso2022Jp(iso2022jp::Iso2022JpState::new()),
136            // Every remaining variant is one of the 26 legacy single-byte encodings; verified in
137            // debug/test builds (this function must stay `const`, so a full `match` guard on
138            // `is_legacy_single_byte_table` is not usable here without also enumerating all 26
139            // variants a second time, which `crate::tables::legacy_single_byte_table`'s own
140            // exhaustive `match` already does and enforces at first use).
141            other => {
142                debug_assert!(
143                    other.is_legacy_single_byte_table(),
144                    "Decoder::new: unhandled Encoding variant"
145                );
146                DecoderKind::LegacySingleByte(other)
147            }
148        };
149        Decoder { kind }
150    }
151
152    /// The encoding this decoder was created for.
153    #[must_use]
154    pub const fn encoding(&self) -> Encoding {
155        match &self.kind {
156            DecoderKind::Utf8(_) => Encoding::Utf8,
157            DecoderKind::Utf16Le(_) => Encoding::Utf16Le,
158            DecoderKind::Utf16Be(_) => Encoding::Utf16Be,
159            DecoderKind::Latin1 => Encoding::Latin1,
160            DecoderKind::Windows1252 => Encoding::Windows1252,
161            DecoderKind::Ascii => Encoding::Ascii,
162            DecoderKind::LegacySingleByte(encoding) => *encoding,
163            DecoderKind::ShiftJis(_) => Encoding::ShiftJis,
164            DecoderKind::EucJp(_) => Encoding::EucJp,
165            DecoderKind::Iso2022Jp(_) => Encoding::Iso2022Jp,
166        }
167    }
168
169    /// Whether this decoder is currently holding onto part of an incomplete multi-byte sequence
170    /// (or an unpaired UTF-16 high surrogate) carried over from a previous
171    /// [`decode_to_string`](Decoder::decode_to_string) call.
172    ///
173    /// Useful for a caller that wants to detect a stream that ends mid-sequence *without*
174    /// making a final `last = true` call (for example, when a network connection drops).
175    #[must_use]
176    pub const fn has_pending_state(&self) -> bool {
177        match &self.kind {
178            DecoderKind::Utf8(state) => state.has_pending(),
179            DecoderKind::Utf16Le(state) | DecoderKind::Utf16Be(state) => state.has_pending(),
180            DecoderKind::Latin1
181            | DecoderKind::Windows1252
182            | DecoderKind::Ascii
183            | DecoderKind::LegacySingleByte(_) => false,
184            DecoderKind::ShiftJis(state) => state.has_pending(),
185            DecoderKind::EucJp(state) => state.has_pending(),
186            DecoderKind::Iso2022Jp(state) => state.has_pending(),
187        }
188    }
189
190    /// Decode as much of `input` as possible, appending the result to `dst`.
191    ///
192    /// `input` need not end on a character boundary: an incomplete trailing sequence is saved
193    /// internally and completed by the start of the next call. Pass `last = true` for the final
194    /// chunk of the stream (an empty final chunk is fine) so that a sequence left dangling at
195    /// true end-of-input is reported as [`DecodeErrorKind::TruncatedSequence`] rather than
196    /// silently held onto forever.
197    ///
198    /// Returns the number of bytes of `input` incorporated into the decode (either emitted as
199    /// characters or buffered as a pending partial sequence). On success this is always
200    /// `input.len()`. On error, see [`DecodeError`] for how `offset`/`length` are interpreted;
201    /// any valid characters that appeared before the error have already been appended to `dst`,
202    /// and the decoder's carried state is reset to empty so the `Decoder` remains usable for a
203    /// fresh call (e.g. after the caller skips the offending bytes).
204    ///
205    /// # Errors
206    ///
207    /// Returns [`DecodeError`] if `input` (combined with any carried-over state) contains a
208    /// byte sequence that is not valid in this decoder's encoding, or ends in a sequence that
209    /// `last = true` asserts will never be completed.
210    pub fn decode_to_string(
211        &mut self,
212        input: &[u8],
213        dst: &mut String,
214        last: bool,
215    ) -> Result<usize, DecodeError> {
216        match &mut self.kind {
217            DecoderKind::Utf8(state) => utf8::decode(state, input, dst, last),
218            DecoderKind::Utf16Le(state) => utf16::decode(state, input, dst, last, false),
219            DecoderKind::Utf16Be(state) => utf16::decode(state, input, dst, last, true),
220            DecoderKind::Latin1 => single_byte::decode_latin1(input, dst),
221            DecoderKind::Windows1252 => single_byte::decode_windows1252(input, dst),
222            DecoderKind::Ascii => single_byte::decode_ascii(input, dst),
223            DecoderKind::LegacySingleByte(encoding) => {
224                single_byte::decode_legacy_single_byte(*encoding, input, dst)
225            }
226            DecoderKind::ShiftJis(state) => shift_jis::decode(state, input, dst, last),
227            DecoderKind::EucJp(state) => euc_jp::decode(state, input, dst, last),
228            DecoderKind::Iso2022Jp(state) => iso2022jp::decode(state, input, dst, last),
229        }
230    }
231
232    /// Like [`decode_to_string`](Decoder::decode_to_string), but never fails: any malformed or
233    /// disallowed byte sequence is replaced by one `U+FFFD REPLACEMENT CHARACTER` per the
234    /// [Unicode "maximal subpart" recovery algorithm][u5-22], and decoding continues from the
235    /// byte immediately after the replaced sequence.
236    ///
237    /// Always consumes (and returns) the full length of `input`.
238    ///
239    /// [u5-22]: https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf#page=22
240    pub fn decode_to_string_lossy(&mut self, input: &[u8], dst: &mut String, last: bool) -> usize {
241        match &mut self.kind {
242            DecoderKind::Utf8(state) => utf8::decode_lossy(state, input, dst, last),
243            DecoderKind::Utf16Le(state) => utf16::decode_lossy(state, input, dst, last, false),
244            DecoderKind::Utf16Be(state) => utf16::decode_lossy(state, input, dst, last, true),
245            DecoderKind::Latin1 => single_byte::decode_latin1_lossy(input, dst),
246            DecoderKind::Windows1252 => single_byte::decode_windows1252_lossy(input, dst),
247            DecoderKind::Ascii => single_byte::decode_ascii_lossy(input, dst),
248            DecoderKind::LegacySingleByte(encoding) => {
249                single_byte::decode_legacy_single_byte_lossy(*encoding, input, dst)
250            }
251            DecoderKind::ShiftJis(state) => shift_jis::decode_lossy(state, input, dst, last),
252            DecoderKind::EucJp(state) => euc_jp::decode_lossy(state, input, dst, last),
253            DecoderKind::Iso2022Jp(state) => iso2022jp::decode_lossy(state, input, dst, last),
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn encoding_round_trips_for_every_variant() {
264        for &enc in Encoding::ALL {
265            assert_eq!(enc.new_decoder().encoding(), enc);
266        }
267    }
268
269    #[test]
270    fn fresh_decoder_has_no_pending_state() {
271        for &enc in Encoding::ALL {
272            assert!(!enc.new_decoder().has_pending_state(), "{enc}");
273        }
274    }
275
276    #[test]
277    fn shift_jis_euc_jp_iso2022jp_decoders_report_pending_state_mid_sequence()
278    -> Result<(), DecodeError> {
279        let mut decoder = Encoding::ShiftJis.new_decoder();
280        let mut out = String::new();
281        decoder.decode_to_string(&[0x82], &mut out, false)?;
282        assert!(decoder.has_pending_state());
283        decoder.decode_to_string(&[0xA0], &mut out, true)?;
284        assert!(!decoder.has_pending_state());
285
286        let mut decoder = Encoding::EucJp.new_decoder();
287        let mut out = String::new();
288        decoder.decode_to_string(&[0xA4], &mut out, false)?;
289        assert!(decoder.has_pending_state());
290        decoder.decode_to_string(&[0xA2], &mut out, true)?;
291        assert!(!decoder.has_pending_state());
292
293        let mut decoder = Encoding::Iso2022Jp.new_decoder();
294        let mut out = String::new();
295        decoder.decode_to_string(&[0x1B], &mut out, false)?;
296        assert!(decoder.has_pending_state());
297        decoder.decode_to_string(&[0x28, 0x42], &mut out, true)?;
298        assert!(!decoder.has_pending_state());
299        Ok(())
300    }
301
302    #[test]
303    fn legacy_single_byte_decoders_never_report_pending_state() {
304        for &enc in Encoding::ALL {
305            if !enc.is_legacy_single_byte_table() {
306                continue;
307            }
308            let mut decoder = enc.new_decoder();
309            let mut out = String::new();
310            let _ = decoder.decode_to_string(&[0x80], &mut out, false);
311            assert!(!decoder.has_pending_state(), "{enc}");
312        }
313    }
314
315    #[test]
316    fn utf8_decoder_reports_pending_state_mid_sequence() -> Result<(), DecodeError> {
317        let mut decoder = Encoding::Utf8.new_decoder();
318        let mut out = String::new();
319        // 0xE2 0x86 0x92 is '→'; feed only the first byte.
320        decoder.decode_to_string(&[0xE2], &mut out, false)?;
321        assert!(decoder.has_pending_state());
322        decoder.decode_to_string(&[0x86, 0x92], &mut out, true)?;
323        assert!(!decoder.has_pending_state());
324        assert_eq!(out, "→");
325        Ok(())
326    }
327
328    #[test]
329    fn decode_error_display_is_human_readable() {
330        let err = DecodeError {
331            offset: 3,
332            length: 1,
333            kind: DecodeErrorKind::DisallowedByte,
334        };
335        assert_eq!(
336            err.to_string(),
337            "byte outside the encoding's repertoire at offset 3 (length 1)"
338        );
339    }
340}