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}