quick_xml/encoding.rs
1//! A module for wrappers that encode / decode data.
2
3use std::str::Utf8Error;
4
5#[cfg(feature = "encoding")]
6use encoding_rs;
7#[cfg(feature = "encoding")]
8use std::borrow::Cow;
9#[cfg(feature = "encoding")]
10use std::io::{self, BufRead, Read};
11
12/// Unicode "byte order mark" (\u{FEFF}) encoded as UTF-8.
13/// See <https://unicode.org/faq/utf_bom.html#bom1>
14pub(crate) const UTF8_BOM: &[u8] = &[0xEF, 0xBB, 0xBF];
15/// Unicode "byte order mark" (\u{FEFF}) encoded as UTF-16 with little-endian byte order.
16/// See <https://unicode.org/faq/utf_bom.html#bom1>
17pub(crate) const UTF16_LE_BOM: &[u8] = &[0xFF, 0xFE];
18/// Unicode "byte order mark" (\u{FEFF}) encoded as UTF-16 with big-endian byte order.
19/// See <https://unicode.org/faq/utf_bom.html#bom1>
20pub(crate) const UTF16_BE_BOM: &[u8] = &[0xFE, 0xFF];
21
22/// An error when decoding or encoding
23///
24/// If feature [`encoding`] is disabled, the [`EncodingError`] is always [`EncodingError::Utf8`]
25///
26/// [`encoding`]: ../index.html#encoding
27#[derive(Clone, Debug, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum EncodingError {
30 /// Input was not valid UTF-8
31 Utf8(Utf8Error),
32 /// Input did not adhere to the given encoding
33 #[cfg(feature = "encoding")]
34 Other(&'static encoding_rs::Encoding),
35}
36
37impl From<Utf8Error> for EncodingError {
38 #[inline]
39 fn from(e: Utf8Error) -> Self {
40 Self::Utf8(e)
41 }
42}
43
44impl std::error::Error for EncodingError {
45 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
46 match self {
47 Self::Utf8(e) => Some(e),
48 #[cfg(feature = "encoding")]
49 Self::Other(_) => None,
50 }
51 }
52}
53
54impl std::fmt::Display for EncodingError {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 Self::Utf8(e) => write!(f, "cannot decode input using UTF-8: {}", e),
58 #[cfg(feature = "encoding")]
59 Self::Other(encoding) => write!(f, "cannot decode input using {}", encoding.name()),
60 }
61 }
62}
63
64/// Decoder of byte slices into strings.
65///
66/// If feature [`encoding`] is enabled, this encoding taken from the `"encoding"`
67/// XML declaration or assumes UTF-8, if XML has no <?xml ?> declaration, encoding
68/// key is not defined or contains unknown encoding.
69///
70/// The library supports any UTF-8 compatible encodings that crate `encoding_rs`
71/// is supported. [*UTF-16 and ISO-2022-JP are not supported at the present*][utf16].
72///
73/// If feature [`encoding`] is disabled, the decoder is always UTF-8 decoder:
74/// any XML declarations are ignored.
75///
76/// [utf16]: https://github.com/tafia/quick-xml/issues/158
77/// [`encoding`]: ../index.html#encoding
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub struct Decoder {
80 #[cfg(feature = "encoding")]
81 pub(crate) encoding: &'static encoding_rs::Encoding,
82}
83
84/// Decodes the provided bytes using the specified encoding.
85///
86/// Returns an error in case of malformed or non-representable sequences in the `bytes`.
87#[cfg(feature = "encoding")]
88pub fn decode<'b>(
89 bytes: &'b [u8],
90 encoding: &'static encoding_rs::Encoding,
91) -> Result<Cow<'b, str>, EncodingError> {
92 encoding
93 .decode_without_bom_handling_and_without_replacement(bytes)
94 .ok_or(EncodingError::Other(encoding))
95}
96
97/// Like [`decode`] but using a pre-allocated buffer.
98#[cfg(feature = "encoding")]
99pub fn decode_into(
100 bytes: &[u8],
101 encoding: &'static encoding_rs::Encoding,
102 buf: &mut String,
103) -> Result<(), EncodingError> {
104 if encoding == encoding_rs::UTF_8 {
105 buf.push_str(std::str::from_utf8(bytes)?);
106 return Ok(());
107 }
108
109 let mut decoder = encoding.new_decoder_without_bom_handling();
110 buf.reserve(
111 decoder
112 .max_utf8_buffer_length_without_replacement(bytes.len())
113 // SAFETY: None can be returned only if required size will overflow usize,
114 // but in that case String::reserve also panics
115 .unwrap(),
116 );
117 let (result, read) = decoder.decode_to_string_without_replacement(bytes, buf, true);
118 match result {
119 encoding_rs::DecoderResult::InputEmpty => {
120 debug_assert_eq!(read, bytes.len());
121 Ok(())
122 }
123 encoding_rs::DecoderResult::Malformed(_, _) => Err(EncodingError::Other(encoding)),
124 // SAFETY: We allocate enough space above
125 encoding_rs::DecoderResult::OutputFull => unreachable!(),
126 }
127}
128
129/// Automatic encoding detection of XML files based using the
130/// [recommended algorithm](https://www.w3.org/TR/xml11/#sec-guessing).
131///
132/// If encoding is detected, `Some` is returned with a [`DetectedEncoding`] that provides
133/// the BOM size in bytes (or zero if no BOM was present).
134///
135/// IF encoding was not recognized, `None` is returned.
136///
137/// Because the [`encoding_rs`] crate supports only subset of those encodings, only
138/// the supported subset are detected, which is UTF-8, UTF-16 BE and UTF-16 LE.
139///
140/// The algorithm suggests examine up to the first 4 bytes to determine encoding
141/// according to the following table:
142///
143/// | Bytes |Detected encoding
144/// |-------------|------------------------------------------
145/// | **BOM**
146/// |`FE_FF_##_##`|UTF-16, big-endian
147/// |`FF FE ## ##`|UTF-16, little-endian
148/// |`EF BB BF` |UTF-8
149/// | **No BOM**
150/// |`00 3C 00 3F`|UTF-16 BE or ISO-10646-UCS-2 BE or similar 16-bit BE (use declared encoding to find the exact one)
151/// |`3C 00 3F 00`|UTF-16 LE or ISO-10646-UCS-2 LE or similar 16-bit LE (use declared encoding to find the exact one)
152/// |`3C 3F 78 6D`|UTF-8, ISO 646, ASCII, some part of ISO 8859, Shift-JIS, EUC, or any other 7-bit, 8-bit, or mixed-width encoding which ensures that the characters of ASCII have their normal positions, width, and values; the actual encoding declaration must be read to detect which of these applies, but since all of these encodings use the same bit patterns for the relevant ASCII characters, the encoding declaration itself may be read reliably
153pub fn detect_encoding(bytes: &[u8]) -> Option<DetectedEncoding> {
154 // Prevent suggesting "<?xm". We want to have the same formatted lines for all arms.
155 #[allow(clippy::byte_char_slices)]
156 match bytes {
157 // with BOM
158 _ if bytes.starts_with(UTF16_BE_BOM) => Some(DetectedEncoding::Utf16BeBom),
159 _ if bytes.starts_with(UTF16_LE_BOM) => Some(DetectedEncoding::Utf16LeBom),
160 _ if bytes.starts_with(UTF8_BOM) => Some(DetectedEncoding::Utf8Bom),
161
162 // without BOM
163 _ if bytes.starts_with(&[0x00, b'<', 0x00, b'?']) => Some(DetectedEncoding::Utf16BeLike), // Some BE encoding, for example, UTF-16 or ISO-10646-UCS-2
164 _ if bytes.starts_with(&[b'<', 0x00, b'?', 0x00]) => Some(DetectedEncoding::Utf16LeLike), // Some LE encoding, for example, UTF-16 or ISO-10646-UCS-2
165 _ if bytes.starts_with(&[b'<', b'?', b'x', b'm']) => {
166 Some(DetectedEncoding::AsciiCompatible)
167 } // Some ASCII compatible
168
169 _ => None,
170 }
171}
172
173/// Possible scenarios for start-of-xml detection of encoding
174///
175/// See the documentation of [`detect_encoding`]
176pub enum DetectedEncoding {
177 /// Matches UTF-8 or some other ascii-compatible encoding
178 AsciiCompatible,
179 /// We saw a UTF-8 BOM
180 Utf8Bom,
181 /// Matches UTF-16-LE or some other UTF-16 compatible encoding (e.g. ISO-10646-UCS-2)
182 Utf16LeLike,
183 /// We saw a UTF-16 BOM in little-endian orientation
184 Utf16LeBom,
185 /// Matches UTF-16-BE or some other UTF-16 compatible encoding (e.g. ISO-10646-UCS-2)
186 Utf16BeLike,
187 /// We saw a UTF-16 BOM in big-endian orientation
188 Utf16BeBom,
189}
190
191impl DetectedEncoding {
192 /// Return an Encoding object appropriate for the detected encoding
193 #[cfg(feature = "encoding")]
194 pub const fn encoding(&self) -> &'static encoding_rs::Encoding {
195 match self {
196 DetectedEncoding::AsciiCompatible | DetectedEncoding::Utf8Bom => encoding_rs::UTF_8,
197 DetectedEncoding::Utf16LeLike | DetectedEncoding::Utf16LeBom => encoding_rs::UTF_16LE,
198 DetectedEncoding::Utf16BeLike | DetectedEncoding::Utf16BeBom => encoding_rs::UTF_16BE,
199 }
200 }
201
202 /// Length of the BOM, which may need to be stripped from the input
203 pub const fn bom_len(&self) -> usize {
204 match self {
205 DetectedEncoding::Utf8Bom => 3,
206 DetectedEncoding::Utf16LeBom | DetectedEncoding::Utf16BeBom => 2,
207 DetectedEncoding::AsciiCompatible
208 | DetectedEncoding::Utf16LeLike
209 | DetectedEncoding::Utf16BeLike => 0,
210 }
211 }
212}
213
214// Bytes read upfront so `set_encoding()` can be called before the main
215// decode loop. Kept small (just enough for an XML declaration) to limit
216// bytes decoded with a potentially wrong initial encoding.
217#[cfg(feature = "encoding")]
218const PREFIX_CAP: usize = 64;
219
220#[cfg(feature = "encoding")]
221struct Prefix {
222 buf: [u8; PREFIX_CAP],
223 len: usize,
224 detected: bool,
225}
226
227/// A reader wrapper that decodes a byte stream from any encoding into UTF-8.
228///
229/// This reader wraps a [`BufRead`] source and uses [`encoding_rs::Decoder`] to
230/// transcode the input into valid UTF-8. On first access, it detects the encoding
231/// from BOM or XML declaration byte patterns and configures the appropriate decoder.
232///
233/// For UTF-8 input, this acts as a validating passthrough. For UTF-16 or other
234/// encodings, the bytes are transcoded into UTF-8 in an internal buffer.
235///
236/// # Examples
237///
238/// ```
239/// use std::io::Read;
240/// use quick_xml::encoding::DecodingReader;
241///
242/// // UTF-8 input passes through:
243/// let data = b"Hello, World!";
244/// let mut reader = DecodingReader::new(&data[..]);
245/// let mut buf = Vec::new();
246/// reader.read_to_end(&mut buf).unwrap();
247/// assert_eq!(buf, data);
248/// ```
249///
250/// The example below shows how you can read documents using `DecodingReader`:
251/// ```
252/// use quick_xml::encoding::DecodingReader;
253/// use quick_xml::events::Event;
254/// use quick_xml::reader::Reader;
255///
256/// # fn to_utf16le_with_bom(string: &str) -> Vec<u8> {
257/// # let mut bytes = Vec::new();
258/// # bytes.extend_from_slice(&[0xFF, 0xFE]); // UTF-16 LE BOM
259/// # for ch in string.encode_utf16() {
260/// # bytes.extend_from_slice(&ch.to_le_bytes());
261/// # }
262/// # bytes
263/// # }
264/// let xml = to_utf16le_with_bom("<?xml encoding='UTF-16'?><element/>");
265/// let mut decoder = DecodingReader::new(xml.as_ref());
266/// let mut reader = Reader::from_reader(decoder);
267///
268/// let mut buf = Vec::new();
269/// loop {
270/// buf.clear();
271/// match reader.read_event_into(&mut buf).unwrap() {
272/// Event::Decl(e) => {
273/// // If XML declaration contains unknown encoding name, None is returned
274/// match e.encoder() {
275/// Some(encoding) => reader.get_mut().set_encoding(encoding),
276/// None => panic!("Unsupported encoding {:?}", e.encoding()),
277/// }
278/// }
279/// Event::Eof => break,
280/// _ => {}
281/// }
282/// }
283/// ```
284#[cfg(feature = "encoding")]
285pub struct DecodingReader<R> {
286 inner: R,
287 decoder: encoding_rs::Decoder,
288 /// `encoding_rs::Decoder` panics if called after finalization (`last=true`).
289 /// This flag prevents that by short-circuiting `fill_buf` after completion.
290 decoder_finished: bool,
291 /// Decoded UTF-8 output buffer
292 out_buf: Box<[u8]>,
293 /// Start of unconsumed data in out_buf
294 out_pos: usize,
295 /// End of valid data in out_buf
296 out_len: usize,
297 /// Bytes read upfront for encoding detection and XML declaration buffering.
298 /// `Some` until the prefix is fully drained; `None` afterward (main decode
299 /// path takes over and the allocation is freed).
300 prefix: Option<Box<Prefix>>,
301 /// Whether the inner reader has reached EOF
302 inner_eof: bool,
303}
304
305#[cfg(feature = "encoding")]
306impl<R: std::fmt::Debug> std::fmt::Debug for DecodingReader<R> {
307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308 f.debug_struct("DecodingReader")
309 .field("inner", &self.inner)
310 .field("encoding", &self.decoder.encoding())
311 .field("out_pos", &self.out_pos)
312 .field("out_len", &self.out_len)
313 .field("inner_eof", &self.inner_eof)
314 .field("prefix_active", &self.prefix.is_some())
315 .finish()
316 }
317}
318
319#[cfg(feature = "encoding")]
320impl<R> DecodingReader<R> {
321 /// Creates a new decoding reader.
322 ///
323 /// The encoding is auto-detected from BOM or XML declaration patterns on
324 /// first access. Defaults to UTF-8 if no pattern is recognized.
325 pub fn new(inner: R) -> Self {
326 Self {
327 inner,
328 decoder: encoding_rs::UTF_8.new_decoder_without_bom_handling(),
329 decoder_finished: false,
330 out_buf: vec![0u8; 8192].into_boxed_slice(),
331 out_pos: 0,
332 out_len: 0,
333 prefix: Some(Box::new(Prefix {
334 buf: [0; PREFIX_CAP],
335 len: 0,
336 detected: false,
337 })),
338 inner_eof: false,
339 }
340 }
341
342 /// Returns a reference to the underlying reader
343 pub const fn get_ref(&self) -> &R {
344 &self.inner
345 }
346
347 /// Returns a mutable reference to the underlying reader
348 pub const fn get_mut(&mut self) -> &mut R {
349 &mut self.inner
350 }
351
352 /// Consumes this reader and returns the underlying reader
353 pub fn into_inner(self) -> R {
354 self.inner
355 }
356
357 /// Returns the encoding currently used by the decoder.
358 ///
359 /// Before the first read, this is always UTF-8. After encoding detection
360 /// it reflects the detected (or overridden) encoding.
361 pub fn encoding(&self) -> &'static encoding_rs::Encoding {
362 self.decoder.encoding()
363 }
364
365 /// Replaces the decoder with one for the given encoding. The encoding
366 /// must be ASCII-compatible (the parser cannot read the declaration otherwise).
367 ///
368 /// # Panics
369 ///
370 /// Panics if the prefix buffer has already been drained. Must be called
371 /// before the prefix is exhausted — in practice, right after parsing
372 /// the XML declaration.
373 pub fn set_encoding(&mut self, encoding: &'static encoding_rs::Encoding) {
374 // No-op when the encoding matches - replacing the decoder would discard
375 // its internal state (e.g. a partial multi-byte sequence), corrupting output.
376 // This check is safe regardless of prefix state since nothing changes.
377 if self.decoder.encoding() == encoding {
378 return;
379 }
380 assert!(
381 self.prefix.is_some(),
382 "set_encoding() called after prefix buffer was drained; \
383 encoding can only be changed while the prefix is still active"
384 );
385 self.decoder = encoding.new_decoder_without_bom_handling();
386 self.decoder_finished = false;
387 }
388}
389
390#[cfg(feature = "encoding")]
391impl<R: BufRead> BufRead for DecodingReader<R> {
392 fn fill_buf(&mut self) -> io::Result<&[u8]> {
393 // Fast path: serve already-decoded data
394 if self.out_pos < self.out_len {
395 return Ok(&self.out_buf[self.out_pos..self.out_len]);
396 }
397
398 // Reset output buffer
399 self.out_pos = 0;
400 self.out_len = 0;
401
402 if let Some(prefix) = &mut self.prefix {
403 // On first access, fill the prefix buffer and detect encoding.
404 // The prefix is large enough to hold an entire XML declaration,
405 // ensuring set_encoding() can be called before the greedy main
406 // decode path consumes from inner.
407 if !prefix.detected {
408 prefix.detected = true;
409
410 while prefix.len < PREFIX_CAP {
411 match self.inner.read(&mut prefix.buf[prefix.len..]) {
412 Ok(0) => {
413 self.inner_eof = true;
414 break;
415 }
416 Ok(n) => prefix.len += n,
417 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
418 Err(e) => return Err(e),
419 }
420 }
421
422 let detection_bytes = &prefix.buf[..prefix.len];
423 if let Some(detected) = detect_encoding(detection_bytes) {
424 let bom_len = detected.bom_len();
425 if bom_len > 0 {
426 prefix.buf.copy_within(bom_len..prefix.len, 0);
427 prefix.len -= bom_len;
428 }
429 let encoding = detected.encoding();
430 if encoding != encoding_rs::UTF_8 {
431 self.decoder = encoding.new_decoder_without_bom_handling();
432 }
433 }
434 }
435
436 if self.decoder_finished {
437 return Ok(&[]);
438 }
439
440 // Prefix fully decoded on a previous call - drop it and fall
441 // through to the main decode path.
442 if prefix.len == 0 {
443 self.prefix = None;
444 } else {
445 // Decode from prefix buffer
446 let src = &prefix.buf[..prefix.len];
447 let (result, read, written) = self.decoder.decode_to_utf8_without_replacement(
448 src,
449 &mut self.out_buf[..],
450 false,
451 );
452 prefix.buf.copy_within(read..prefix.len, 0);
453 prefix.len -= read;
454 self.out_len = written;
455
456 match result {
457 encoding_rs::DecoderResult::InputEmpty if written > 0 => {
458 return Ok(&self.out_buf[..self.out_len]);
459 }
460 encoding_rs::DecoderResult::InputEmpty => {
461 // prefix.len is now 0; keep prefix alive for
462 // set_encoding() - it will be dropped on the next call.
463 }
464 encoding_rs::DecoderResult::OutputFull => {
465 return Ok(&self.out_buf[..self.out_len]);
466 }
467 encoding_rs::DecoderResult::Malformed(_, _) => {
468 return Err(io::Error::new(
469 io::ErrorKind::InvalidData,
470 EncodingError::Other(self.decoder.encoding()),
471 ));
472 }
473 }
474 // InputEmpty with written == 0: prefix drained, decoder may
475 // hold partial internal state (e.g. a lone byte of UTF-16).
476 // Drop prefix and fall through to the main decode path.
477 if prefix.len == 0 {
478 self.prefix = None;
479 }
480 }
481 }
482
483 if self.decoder_finished {
484 return Ok(&[]);
485 }
486
487 // Loop until we produce output, hit EOF, or get an error.
488 // The decoder may consume input into internal state (e.g., partial
489 // UTF-16 code unit) without producing output - we must keep feeding
490 // it more input rather than returning an empty slice (which signals EOF).
491 loop {
492 // EOF flush path: tell decoder this is the last chunk
493 if self.inner_eof {
494 let (result, _, written) = self.decoder.decode_to_utf8_without_replacement(
495 b"",
496 &mut self.out_buf[..],
497 true,
498 );
499 self.out_len = written;
500 match result {
501 encoding_rs::DecoderResult::InputEmpty => {
502 self.decoder_finished = true;
503 return Ok(&self.out_buf[..self.out_len]);
504 }
505 encoding_rs::DecoderResult::OutputFull => {
506 return Ok(&self.out_buf[..self.out_len]);
507 }
508 encoding_rs::DecoderResult::Malformed(_, _) => {
509 return Err(io::Error::new(
510 io::ErrorKind::InvalidData,
511 EncodingError::Other(self.decoder.encoding()),
512 ));
513 }
514 }
515 }
516
517 // Main decode path: read from inner, decode into out_buf
518 let (result, read, written) = {
519 let src = self.inner.fill_buf()?;
520 if src.is_empty() {
521 self.inner_eof = true;
522 continue; // will hit EOF flush path on next iteration
523 }
524 self.decoder
525 .decode_to_utf8_without_replacement(src, &mut self.out_buf[..], false)
526 };
527 self.inner.consume(read);
528 self.out_len = written;
529
530 match result {
531 encoding_rs::DecoderResult::InputEmpty if written > 0 => {
532 return Ok(&self.out_buf[..self.out_len]);
533 }
534 encoding_rs::DecoderResult::InputEmpty => {
535 // Decoder consumed all input but produced no output
536 // (e.g., 1 byte of a 2-byte UTF-16 code unit stored
537 // in decoder internal state). Loop to get more input.
538 }
539 encoding_rs::DecoderResult::OutputFull => {
540 // Output buffer full; return what we have. Remaining
541 // input will be decoded on the next fill_buf call.
542 return Ok(&self.out_buf[..self.out_len]);
543 }
544 encoding_rs::DecoderResult::Malformed(_, _) => {
545 return Err(io::Error::new(
546 io::ErrorKind::InvalidData,
547 EncodingError::Other(self.decoder.encoding()),
548 ));
549 }
550 }
551 }
552 }
553
554 fn consume(&mut self, amt: usize) {
555 debug_assert!(
556 self.out_pos + amt <= self.out_len,
557 "consume({amt}) out of range: out_pos={}, out_len={}",
558 self.out_pos,
559 self.out_len,
560 );
561 self.out_pos += amt;
562 }
563}
564
565#[cfg(feature = "encoding")]
566impl<R: BufRead> Read for DecodingReader<R> {
567 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
568 if buf.is_empty() {
569 return Ok(0);
570 }
571 let available = self.fill_buf()?;
572 if available.is_empty() {
573 return Ok(0);
574 }
575 let len = available.len().min(buf.len());
576 buf[..len].copy_from_slice(&available[..len]);
577 self.consume(len);
578 Ok(len)
579 }
580}
581
582#[cfg(all(test, feature = "encoding"))]
583mod decoding_reader {
584 use super::*;
585 use std::io::{BufReader, Read};
586
587 /// Helper reader that returns data in fixed-size chunks
588 struct ChunkedReader<'a> {
589 data: &'a [u8],
590 pos: usize,
591 chunk_size: usize,
592 }
593
594 impl<'a> ChunkedReader<'a> {
595 fn new(data: &'a [u8], chunk_size: usize) -> Self {
596 Self {
597 data,
598 pos: 0,
599 chunk_size,
600 }
601 }
602 }
603
604 impl<'a> Read for ChunkedReader<'a> {
605 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
606 if self.pos >= self.data.len() {
607 return Ok(0);
608 }
609 let len = self
610 .chunk_size
611 .min(buf.len())
612 .min(self.data.len() - self.pos);
613 buf[..len].copy_from_slice(&self.data[self.pos..self.pos + len]);
614 self.pos += len;
615 Ok(len)
616 }
617 }
618
619 /// Encode a string as UTF-16 LE bytes with BOM
620 fn utf16le_with_bom(s: &str) -> Vec<u8> {
621 let mut out = vec![0xFF, 0xFE]; // UTF-16 LE BOM
622 for code_unit in s.encode_utf16() {
623 out.extend_from_slice(&code_unit.to_le_bytes());
624 }
625 out
626 }
627
628 /// Encode a string as UTF-16 BE bytes with BOM
629 fn utf16be_with_bom(s: &str) -> Vec<u8> {
630 let mut out = vec![0xFE, 0xFF]; // UTF-16 BE BOM
631 for code_unit in s.encode_utf16() {
632 out.extend_from_slice(&code_unit.to_be_bytes());
633 }
634 out
635 }
636
637 /// Encode a string as UTF-16 LE bytes without BOM
638 fn utf16le_no_bom(s: &str) -> Vec<u8> {
639 let mut out = Vec::new();
640 for code_unit in s.encode_utf16() {
641 out.extend_from_slice(&code_unit.to_le_bytes());
642 }
643 out
644 }
645
646 /// Encode a string as UTF-16 BE bytes without BOM
647 fn utf16be_no_bom(s: &str) -> Vec<u8> {
648 let mut out = Vec::new();
649 for code_unit in s.encode_utf16() {
650 out.extend_from_slice(&code_unit.to_be_bytes());
651 }
652 out
653 }
654
655 /// Read all bytes from a reader into a String
656 fn read_all(reader: &mut DecodingReader<impl BufRead>) -> io::Result<String> {
657 let mut result = Vec::new();
658 reader.read_to_end(&mut result)?;
659 Ok(String::from_utf8(result).expect("DecodingReader should produce valid UTF-8"))
660 }
661
662 /// Simple edge cases and degenerate inputs
663 mod edge_cases {
664 use super::*;
665 use pretty_assertions::assert_eq;
666
667 /// Zero-length input should immediately return EOF (n == 0).
668 #[test]
669 fn empty_input() {
670 let data = b"";
671 let mut reader = DecodingReader::new(&data[..]);
672 let mut buf = [0u8; 10];
673 let n = reader.read(&mut buf).unwrap();
674 assert_eq!(n, 0);
675 }
676
677 /// A UTF-8 BOM with no payload should decode to an empty string.
678 #[test]
679 fn utf8_bom_only() {
680 let data = b"\xEF\xBB\xBF";
681 let mut reader = DecodingReader::new(&data[..]);
682 assert_eq!(read_all(&mut reader).unwrap(), "");
683 }
684
685 /// A UTF-16 LE BOM with no payload should decode to an empty string.
686 #[test]
687 fn utf16le_bom_only() {
688 let data = &[0xFF, 0xFE];
689 let mut reader = DecodingReader::new(&data[..]);
690 assert_eq!(read_all(&mut reader).unwrap(), "");
691 }
692
693 /// A UTF-16 BE BOM with no payload should decode to an empty string.
694 #[test]
695 fn utf16be_bom_only() {
696 let data = &[0xFE, 0xFF];
697 let mut reader = DecodingReader::new(&data[..]);
698 assert_eq!(read_all(&mut reader).unwrap(), "");
699 }
700
701 /// Invalid UTF-8 (no BOM, so treated as UTF-8) must produce an error.
702 #[test]
703 fn invalid_utf8_is_rejected() {
704 let data: &[u8] = &[0x48, 0x65, 0x6C, 0xFF, 0xFE];
705 let mut reader = DecodingReader::new(&data[..]);
706 let err = read_all(&mut reader).unwrap_err();
707 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
708 }
709
710 /// An odd trailing byte in UTF-16 is malformed and must produce an error.
711 #[test]
712 fn truncated_utf16_at_eof() {
713 // UTF-16 LE BOM + one valid code unit + one incomplete byte
714 let data: &[u8] = &[0xFF, 0xFE, 0x48, 0x00, 0x65];
715 let mut reader = DecodingReader::new(&data[..]);
716 let err = read_all(&mut reader).unwrap_err();
717 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
718 }
719
720 /// A 1-byte output buffer forces one byte per read() call; verifies
721 /// multi-byte UTF-8 sequences are still assembled correctly.
722 #[test]
723 fn read_with_one_byte_buffer() {
724 let data = "Hello, 世界!".as_bytes();
725 let mut reader = DecodingReader::new(&data[..]);
726 let mut result = Vec::new();
727 let mut buf = [0u8; 1];
728 loop {
729 let n = reader.read(&mut buf).unwrap();
730 if n == 0 {
731 break;
732 }
733 result.extend_from_slice(&buf[..n]);
734 }
735 assert_eq!(String::from_utf8(result).unwrap(), "Hello, 世界!");
736 }
737 }
738
739 /// Tests that exercise the BufRead contract (fill_buf + consume) directly,
740 /// as opposed to the Read-based helpers used elsewhere.
741 mod bufread_interface {
742 use super::*;
743 use pretty_assertions::assert_eq;
744 use std::io::BufRead;
745
746 /// Basic fill_buf/consume cycle: partial consume leaves remaining
747 /// data available on the next fill_buf call.
748 #[test]
749 fn fill_buf_and_consume() {
750 let data = b"Hello, World!";
751 let mut reader = DecodingReader::new(&data[..]);
752
753 let buf = reader.fill_buf().unwrap();
754 assert!(!buf.is_empty());
755 assert_eq!(buf[0], b'H');
756
757 // Consume only part of the buffer
758 reader.consume(5);
759
760 let buf = reader.fill_buf().unwrap();
761 assert!(!buf.is_empty());
762 assert_eq!(buf[0], b',');
763 }
764
765 /// Drain the reader via fill_buf/consume, then confirm it stays at EOF.
766 #[test]
767 fn partial_consume_then_read_more() {
768 let data = b"Hello, World!";
769 let mut reader = DecodingReader::new(&data[..]);
770
771 // Collect all output via fill_buf/consume
772 let mut result = Vec::new();
773 loop {
774 let buf = reader.fill_buf().unwrap();
775 if buf.is_empty() {
776 break;
777 }
778 result.extend_from_slice(buf);
779 let len = buf.len();
780 reader.consume(len);
781 }
782 assert_eq!(std::str::from_utf8(&result).unwrap(), "Hello, World!");
783
784 // Should remain at EOF
785 let buf = reader.fill_buf().unwrap();
786 assert!(buf.is_empty());
787 }
788
789 /// Calling fill_buf() repeatedly after EOF must keep returning empty
790 /// (and not panic - encoding_rs::Decoder panics if called after finalization).
791 #[test]
792 fn fill_buf_after_eof_is_idempotent() {
793 let data = b"Hello";
794 let mut reader = DecodingReader::new(&data[..]);
795
796 loop {
797 let buf = reader.fill_buf().unwrap();
798 if buf.is_empty() {
799 break;
800 }
801 let len = buf.len();
802 reader.consume(len);
803 }
804
805 for _ in 0..3 {
806 let buf = reader.fill_buf().unwrap();
807 assert!(buf.is_empty());
808 }
809 }
810
811 /// consume() past the buffered length must trigger a debug_assert panic.
812 #[test]
813 #[should_panic(expected = "consume")]
814 fn consume_overflow_panics_in_debug() {
815 let data = b"Hi";
816 let mut reader = DecodingReader::new(&data[..]);
817 let _ = reader.fill_buf().unwrap();
818 reader.consume(100);
819 }
820 }
821
822 mod accessors {
823 use super::*;
824 use pretty_assertions::assert_eq;
825 use std::io::Cursor;
826
827 #[test]
828 fn get_ref() {
829 let data = b"Hello";
830 let cursor = Cursor::new(data.to_vec());
831 let reader = DecodingReader::new(cursor);
832 assert_eq!(reader.get_ref().get_ref(), data);
833 }
834
835 #[test]
836 fn get_mut() {
837 let data = b"Hello";
838 let cursor = Cursor::new(data.to_vec());
839 let mut reader = DecodingReader::new(cursor);
840 reader.get_mut().set_position(2);
841 assert_eq!(reader.get_ref().position(), 2);
842 }
843
844 #[test]
845 fn into_inner() {
846 let data = b"Hello";
847 let cursor = Cursor::new(data.to_vec());
848 let reader = DecodingReader::new(cursor);
849 let inner = reader.into_inner();
850 assert_eq!(inner.get_ref(), data);
851 }
852
853 /// Default encoding before any reads is UTF-8.
854 #[test]
855 fn encoding_default_is_utf8() {
856 let reader = DecodingReader::new(&b"Hello"[..]);
857 assert_eq!(reader.encoding(), encoding_rs::UTF_8);
858 }
859 }
860
861 // TODO: These tests emulate the updating of the internal decoder after reading the XML decl.
862 // Since `Reader` currently only speaks the `BufRead` trait, we can't test that directly.
863 // Eventually once `Reader` knows about the underlying `DecodingReader` we should test
864 // that directly.
865
866 /// Tests for encoding() and set_encoding(): detection, switching,
867 /// same-encoding no-op safety, and mid-stream override behavior.
868 mod encoding_switching {
869 use super::*;
870 use pretty_assertions::assert_eq;
871 use std::io::BufRead;
872
873 /// Encoding reflects BOM detection after first read.
874 #[test]
875 fn encoding_reflects_detection() {
876 let data = utf16le_with_bom("Hello");
877 let mut reader = DecodingReader::new(&data[..]);
878 let _ = read_all(&mut reader).unwrap();
879 assert_eq!(reader.encoding(), encoding_rs::UTF_16LE);
880 }
881
882 /// set_encoding switches the active decoder.
883 #[test]
884 fn set_encoding_changes_encoding() {
885 let mut reader = DecodingReader::new(&b"Hello"[..]);
886 assert_eq!(reader.encoding(), encoding_rs::UTF_8);
887 reader.set_encoding(encoding_rs::UTF_16LE);
888 assert_eq!(reader.encoding(), encoding_rs::UTF_16LE);
889 }
890
891 /// set_encoding after reading preserves already-buffered output.
892 #[test]
893 fn set_encoding_preserves_buffered_output() {
894 let data = b"Hello";
895 let mut reader = DecodingReader::new(&data[..]);
896
897 let buf = reader.fill_buf().unwrap();
898 assert_eq!(buf, b"Hello");
899
900 reader.set_encoding(encoding_rs::WINDOWS_1252);
901 assert_eq!(reader.encoding(), encoding_rs::WINDOWS_1252);
902
903 // Buffered data is unchanged
904 let buf = reader.fill_buf().unwrap();
905 assert_eq!(buf, b"Hello");
906 }
907
908 /// Calling set_encoding with the already-active encoding is a no-op:
909 /// the decoder's internal state is preserved and decoding continues
910 /// without corruption.
911 #[test]
912 fn set_encoding_same_as_detected_is_noop() {
913 let data = b"Hello, World!";
914 let mut reader = DecodingReader::new(&data[..]);
915
916 // Trigger detection and consume the first chunk
917 let first_chunk;
918 {
919 let buf = reader.fill_buf().unwrap();
920 assert!(buf.len() > 0);
921 first_chunk = std::str::from_utf8(buf).unwrap().to_string();
922 let n = buf.len();
923 reader.consume(n);
924 }
925 assert_eq!(reader.encoding(), encoding_rs::UTF_8);
926
927 // "Re-set" to the same encoding - must not reset decoder state
928 reader.set_encoding(encoding_rs::UTF_8);
929 assert_eq!(reader.encoding(), encoding_rs::UTF_8);
930
931 // Read the rest - combined output must equal the original string
932 let rest = read_all(&mut reader).unwrap();
933 assert_eq!(format!("{first_chunk}{rest}"), "Hello, World!");
934 }
935
936 /// set_encoding mid-stream: read some UTF-8 data, switch encoding,
937 /// then verify the encoding accessor reflects the change.
938 #[test]
939 fn set_encoding_mid_stream() {
940 let data = b"Hello, World!";
941 let mut reader = DecodingReader::new(&data[..]);
942
943 // Read a few bytes under UTF-8
944 let buf = reader.fill_buf().unwrap();
945 let n = std::cmp::min(buf.len(), 5);
946 reader.consume(n);
947
948 assert_eq!(reader.encoding(), encoding_rs::UTF_8);
949 reader.set_encoding(encoding_rs::WINDOWS_1252);
950 assert_eq!(reader.encoding(), encoding_rs::WINDOWS_1252);
951
952 // Remaining data still readable (ASCII is identical in both encodings)
953 let rest = read_all(&mut reader).unwrap();
954 assert_eq!(rest, ", World!");
955 }
956 }
957
958 /// Tests exercised across a matrix of (input text x encoding x read strategy).
959 /// Each test encodes a string, feeds it through DecodingReader, and asserts the
960 /// decoded output matches the original. This covers BOM detection, UTF-16
961 /// transcoding, surrogate pairs, and multi-byte UTF-8 characters in one sweep.
962 ///
963 /// Examples:
964 ///
965 /// - UTF-8 passthrough (ASCII and multibyte) with and without BOM
966 /// - UTF-16 LE/BE decoding with and without BOM
967 /// - BOM-less UTF-16 detection via `<?xml` byte pattern
968 /// - UTF-16 surrogate pairs (astral plane characters)
969 /// - Chunked input at misaligned boundaries (odd chunk sizes vs 2-byte code units)
970 /// - One-byte-at-a-time delivery for all encodings
971 /// - Inputs larger than the 8192-byte internal output buffer
972 /// - Empty and single-character inputs (prefix-only decode path)
973 mod matrix_decoding_tests {
974 use super::*;
975 use pretty_assertions::assert_eq;
976
977 struct TestCase {
978 label: &'static str,
979 text: &'static str,
980 }
981
982 /// Short inputs that exercise different Unicode categories.
983 const CASES: &[TestCase] = &[
984 TestCase {
985 label: "empty",
986 text: "",
987 },
988 TestCase {
989 label: "single_multibyte",
990 // Single 3-byte character - entire content fits in the prefix buffer
991 text: "€",
992 },
993 TestCase {
994 label: "ascii",
995 text: "Hello",
996 },
997 TestCase {
998 label: "multibyte",
999 // 3-byte CJK + 4-byte emoji
1000 text: "Hello, 世界! 😀",
1001 },
1002 TestCase {
1003 label: "surrogate_pairs",
1004 // U+1D11E and U+1F3B5 require surrogate pairs in UTF-16
1005 text: "Music: 𝄞🎵",
1006 },
1007 TestCase {
1008 label: "xml_declaration",
1009 // Enables BOM-less UTF-16 detection via the <?xml byte pattern
1010 text: "<?xml version=\"1.0\"?><root/>",
1011 },
1012 ];
1013
1014 /// Inputs larger than the 8192-byte internal output buffer.
1015 fn large_cases() -> Vec<(&'static str, String)> {
1016 vec![
1017 ("large_ascii", "abcdefghij".repeat(1000)),
1018 ("large_multibyte", "Hello, 世界! 😀 ".repeat(500)),
1019 ]
1020 }
1021
1022 enum Encoding {
1023 Utf8,
1024 Utf8Bom,
1025 Utf16Le,
1026 Utf16Be,
1027 Utf16LeNoBom,
1028 Utf16BeNoBom,
1029 }
1030
1031 impl Encoding {
1032 fn encode(&self, text: &str) -> Vec<u8> {
1033 match self {
1034 Encoding::Utf8 => text.as_bytes().to_vec(),
1035 Encoding::Utf8Bom => {
1036 let mut out = vec![0xEF, 0xBB, 0xBF];
1037 out.extend_from_slice(text.as_bytes());
1038 out
1039 }
1040 Encoding::Utf16Le => utf16le_with_bom(text),
1041 Encoding::Utf16Be => utf16be_with_bom(text),
1042 Encoding::Utf16LeNoBom => utf16le_no_bom(text),
1043 Encoding::Utf16BeNoBom => utf16be_no_bom(text),
1044 }
1045 }
1046
1047 fn label(&self) -> &'static str {
1048 match self {
1049 Encoding::Utf8 => "utf8",
1050 Encoding::Utf8Bom => "utf8_bom",
1051 Encoding::Utf16Le => "utf16le",
1052 Encoding::Utf16Be => "utf16be",
1053 Encoding::Utf16LeNoBom => "utf16le_no_bom",
1054 Encoding::Utf16BeNoBom => "utf16be_no_bom",
1055 }
1056 }
1057
1058 /// BOM-less UTF-16 detection requires a `<?xml` prefix, so those
1059 /// encodings are only included for inputs that start with one.
1060 fn all_for(text: &str) -> Vec<Encoding> {
1061 let mut encs = vec![
1062 Encoding::Utf8,
1063 Encoding::Utf8Bom,
1064 Encoding::Utf16Le,
1065 Encoding::Utf16Be,
1066 ];
1067 if text.starts_with("<?xml") {
1068 encs.push(Encoding::Utf16LeNoBom);
1069 encs.push(Encoding::Utf16BeNoBom);
1070 }
1071 encs
1072 }
1073 }
1074
1075 /// Encode -> decode with the entire input available at once.
1076 #[test]
1077 fn bulk_read() {
1078 for case in CASES {
1079 for enc in Encoding::all_for(case.text) {
1080 let data = enc.encode(case.text);
1081 let mut reader = DecodingReader::new(&data[..]);
1082 assert_eq!(
1083 read_all(&mut reader).unwrap(),
1084 case.text,
1085 "bulk_read failed: case={}, encoding={}",
1086 case.label,
1087 enc.label(),
1088 );
1089 }
1090 }
1091 for (label, text) in large_cases() {
1092 for enc in Encoding::all_for(&text) {
1093 let data = enc.encode(&text);
1094 let mut reader = DecodingReader::new(&data[..]);
1095 assert_eq!(
1096 read_all(&mut reader).unwrap(),
1097 text,
1098 "bulk_read failed: case={}, encoding={}",
1099 label,
1100 enc.label(),
1101 );
1102 }
1103 }
1104 }
1105
1106 /// Encode -> decode with the input delivered in fixed-size chunks via
1107 /// ChunkedReader, testing that the decoder handles arbitrary byte
1108 /// boundaries (mid-BOM, mid-code-unit, mid-surrogate-pair).
1109 #[test]
1110 fn chunked_read() {
1111 for case in CASES {
1112 for enc in Encoding::all_for(case.text) {
1113 for chunk_size in [1, 2, 3, 4, 5] {
1114 let data = enc.encode(case.text);
1115 let mut reader = DecodingReader::new(BufReader::new(ChunkedReader::new(
1116 &data, chunk_size,
1117 )));
1118 assert_eq!(
1119 read_all(&mut reader).unwrap(),
1120 case.text,
1121 "chunked_read failed: case={}, encoding={}, chunk_size={}",
1122 case.label,
1123 enc.label(),
1124 chunk_size,
1125 );
1126 }
1127 }
1128 }
1129 }
1130
1131 /// Same as chunked_read but with inputs exceeding the 8192-byte
1132 /// internal output buffer, exercising the multi-fill_buf decode loop.
1133 #[test]
1134 fn large_chunked_read() {
1135 for (label, text) in large_cases() {
1136 for enc in Encoding::all_for(&text) {
1137 for chunk_size in [1, 2, 3, 4, 5] {
1138 let data = enc.encode(&text);
1139 let mut reader = DecodingReader::new(BufReader::new(ChunkedReader::new(
1140 &data, chunk_size,
1141 )));
1142 assert_eq!(
1143 read_all(&mut reader).unwrap(),
1144 text,
1145 "large_chunked_read failed: case={}, encoding={}, chunk_size={}",
1146 label,
1147 enc.label(),
1148 chunk_size,
1149 );
1150 }
1151 }
1152 }
1153 }
1154 }
1155}