Skip to main content

xml_sec/xml_input/
shared.rs

1//! Backend-neutral XML byte encoding detection and strict transcoding.
2//!
3//! Disable the default `std` feature for an alloc-only decoder and lexical scanner. The
4//! `std::io::Write`-based lexical writer is available only when `std` is enabled.
5
6use alloc::{borrow::Cow, string::String};
7use core::ops::Range;
8
9/// Storage strategy for the shared decoder. Resource-aware callers retain their
10/// actual allocator and execution domain; ordinary callers retain `Cow<str>`.
11#[doc(hidden)]
12pub trait DecodeStorage {
13    type Text: AsRef<str>;
14    type Buffer;
15    type Error: From<Error>;
16    fn label_error(&self, kind: LabelError, label: &str) -> Self::Error;
17    fn charge(&self, bytes: usize) -> Result<(), Self::Error>;
18    fn buffer(&self) -> Self::Buffer;
19    fn len(&self, buffer: &Self::Buffer) -> usize;
20    fn append(&self, buffer: &mut Self::Buffer, text: &str) -> Result<(), Self::Error>;
21    fn finish(&self, buffer: Self::Buffer) -> Result<Self::Text, Self::Error>;
22    fn replace(
23        &self,
24        text: Self::Text,
25        range: Range<usize>,
26        replacement: &str,
27    ) -> Result<Self::Text, Self::Error>;
28}
29
30/// Encoding failures whose diagnostic retains an input label.
31#[doc(hidden)]
32#[derive(Clone, Copy)]
33pub enum LabelError {
34    Unsupported,
35    Forbidden,
36    Conflicting,
37}
38
39/// Borrowed UTF-8 or a caller-owned transcoding buffer.
40#[doc(hidden)]
41pub enum Decoded<'a, T> {
42    Borrowed(&'a str),
43    Owned(T),
44}
45
46impl<T: AsRef<str>> AsRef<str> for Decoded<'_, T> {
47    fn as_ref(&self) -> &str {
48        match self {
49            Self::Borrowed(text) => text,
50            Self::Owned(text) => text.as_ref(),
51        }
52    }
53}
54
55pub(crate) struct NativeStorage;
56
57impl DecodeStorage for NativeStorage {
58    type Text = String;
59    type Buffer = String;
60    type Error = Error;
61    fn label_error(&self, kind: LabelError, label: &str) -> Error {
62        match kind {
63            LabelError::Unsupported => Error::UnsupportedEncoding(label.into()),
64            LabelError::Forbidden => Error::ForbiddenEncoding(label.into()),
65            LabelError::Conflicting => Error::ConflictingEncoding(label.into()),
66        }
67    }
68    fn charge(&self, _: usize) -> Result<(), Error> {
69        Ok(())
70    }
71    fn buffer(&self) -> String {
72        String::new()
73    }
74    fn len(&self, buffer: &String) -> usize {
75        buffer.len()
76    }
77    fn append(&self, buffer: &mut String, text: &str) -> Result<(), Error> {
78        buffer.push_str(text);
79        Ok(())
80    }
81    fn finish(&self, buffer: String) -> Result<String, Error> {
82        Ok(buffer)
83    }
84    fn replace(
85        &self,
86        mut text: String,
87        range: Range<usize>,
88        replacement: &str,
89    ) -> Result<String, Error> {
90        text.replace_range(range, replacement);
91        Ok(text)
92    }
93}
94
95fn native_text(text: Decoded<'_, String>) -> Cow<'_, str> {
96    match text {
97        Decoded::Borrowed(text) => Cow::Borrowed(text),
98        Decoded::Owned(text) => Cow::Owned(text),
99    }
100}
101
102#[path = "lexical.rs"]
103pub mod lexical;
104
105/// Failure while converting external bytes into the Unicode XML parser contract.
106#[derive(Debug, thiserror::Error)]
107#[non_exhaustive]
108pub enum Error<L: core::fmt::Display = String> {
109    /// The byte signature selected an encoding that this implementation cannot decode.
110    #[error("unsupported XML byte encoding `{0}`")]
111    UnsupportedByteEncoding(&'static str),
112    /// An encoding label was not recognized.
113    #[error("unsupported XML encoding `{0}`")]
114    UnsupportedEncoding(L),
115    /// The consuming XML format disallows the detected or declared encoding.
116    #[error("XML format does not permit encoding `{0}`")]
117    ForbiddenEncoding(L),
118    /// Resolver metadata, a byte signature, and the XML declaration disagreed.
119    #[error("XML byte encoding conflicts with declared or selected encoding `{0}`")]
120    ConflictingEncoding(L),
121    /// The XML declaration was malformed before parsing could begin.
122    #[error("malformed XML encoding declaration: {0}")]
123    MalformedDeclaration(&'static str),
124    /// The selected decoder rejected malformed input instead of replacing it.
125    #[error("XML input contains invalid {0} bytes")]
126    InvalidBytes(&'static str),
127    /// A BOM-less UTF-16 document did not identify its byte order.
128    #[error("BOM-less UTF-16 XML input requires an explicit UTF-16LE or UTF-16BE declaration")]
129    MissingUtf16ByteOrder,
130    /// A UTF-32 document did not identify its byte order through metadata or its signature.
131    #[error("UTF-32 XML input requires a UTF-32LE/UTF-32BE encoding or byte-order signature")]
132    MissingUtf32ByteOrder,
133    /// A non-UTF-8/UTF-16 entity omitted both external encoding metadata and its declaration.
134    #[error("{0} XML input requires an encoding declaration or trusted external encoding metadata")]
135    MissingEncodingDeclaration(&'static str),
136    /// A UTF-16 code unit was truncated.
137    #[error("{0} XML input has an odd byte length")]
138    InvalidUtf16Length(&'static str),
139    /// A UTF-32 code unit was truncated.
140    #[error("{0} XML input byte length is not divisible by four")]
141    InvalidUtf32Length(&'static str),
142    /// Decoded UTF-8 would exceed the caller's materialization ceiling.
143    #[error("decoded XML exceeds the maximum size of {maximum} bytes: at least {actual} bytes")]
144    DecodedLimit {
145        /// Configured maximum decoded size.
146        maximum: usize,
147        /// Minimum decoded size required by the input.
148        actual: usize,
149    },
150}
151
152#[derive(Clone, Copy, PartialEq, Eq)]
153enum SelectedEncoding {
154    Standard(&'static encoding_rs::Encoding),
155    Utf32Le,
156    Utf32Be,
157    Ascii,
158    Registered(IanaSingleByteEncoding),
159}
160
161impl SelectedEncoding {
162    fn name(self) -> &'static str {
163        match self {
164            Self::Standard(encoding) => encoding.name(),
165            Self::Utf32Le => "UTF-32LE",
166            Self::Utf32Be => "UTF-32BE",
167            Self::Ascii => "US-ASCII",
168            Self::Registered(encoding) => encoding.name(),
169        }
170    }
171
172    fn is_utf8(self) -> bool {
173        matches!(self, Self::Standard(encoding) if encoding == encoding_rs::UTF_8)
174    }
175}
176
177/// Strict IANA single-byte repertoire shared by XML input and XSLT output.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum IanaSingleByteEncoding {
180    /// ISO-8859-1 (Latin-1).
181    Latin1,
182    /// ISO-8859-9 (Latin-5).
183    Latin5,
184    /// ISO-8859-11 Thai encoding.
185    Iso8859_11,
186    /// TIS-620 Thai encoding.
187    Tis620,
188}
189
190impl IanaSingleByteEncoding {
191    #[must_use]
192    /// Return the registered encoding name.
193    pub const fn name(self) -> &'static str {
194        match self {
195            Self::Latin1 => "ISO-8859-1",
196            Self::Latin5 => "ISO-8859-9",
197            Self::Iso8859_11 => "ISO-8859-11",
198            Self::Tis620 => "TIS-620",
199        }
200    }
201
202    #[must_use]
203    /// Decode one byte, or return `None` for an undefined mapping.
204    pub fn decode_byte(self, byte: u8) -> Option<char> {
205        match self {
206            Self::Latin1 => Some(char::from(byte)),
207            Self::Latin5 => Some(match byte {
208                0xD0 => '\u{011E}',
209                0xDD => '\u{0130}',
210                0xDE => '\u{015E}',
211                0xF0 => '\u{011F}',
212                0xFD => '\u{0131}',
213                0xFE => '\u{015F}',
214                _ => char::from(byte),
215            }),
216            Self::Iso8859_11 | Self::Tis620 => match byte {
217                0x00..=0x7F => Some(char::from(byte)),
218                0xA0 if self == Self::Iso8859_11 => Some('\u{00A0}'),
219                0xA1..=0xDA | 0xE0..=0xFB => char::from_u32(u32::from(byte) + 0x0D60),
220                0xDF => Some('\u{0E3F}'),
221                _ => None,
222            },
223        }
224    }
225
226    #[must_use]
227    /// Encode one character, or return `None` if it is not representable.
228    pub fn encode_char(self, character: char) -> Option<u8> {
229        match self {
230            Self::Latin1 => u8::try_from(u32::from(character)).ok(),
231            Self::Latin5 => match character {
232                '\u{011E}' => Some(0xD0),
233                '\u{0130}' => Some(0xDD),
234                '\u{015E}' => Some(0xDE),
235                '\u{011F}' => Some(0xF0),
236                '\u{0131}' => Some(0xFD),
237                '\u{015F}' => Some(0xFE),
238                _ => u8::try_from(u32::from(character))
239                    .ok()
240                    .filter(|byte| !matches!(byte, 0xD0 | 0xDD | 0xDE | 0xF0 | 0xFD | 0xFE)),
241            },
242            Self::Iso8859_11 | Self::Tis620 => match u32::from(character) {
243                value @ 0x00..=0x7F => Some(value as u8),
244                0xA0 if self == Self::Iso8859_11 => Some(0xA0),
245                value @ 0x0E01..=0x0E3A | value @ 0x0E40..=0x0E5B => {
246                    u8::try_from(value - 0x0D60).ok()
247                }
248                0x0E3F => Some(0xDF),
249                _ => None,
250            },
251        }
252    }
253}
254
255/// Resolve labels whose IANA meaning differs from WHATWG-compatible decoders.
256#[must_use]
257pub fn registered_single_byte_encoding(label: &str) -> Option<IanaSingleByteEncoding> {
258    if is_latin1_encoding_label(label) {
259        return Some(IanaSingleByteEncoding::Latin1);
260    }
261    if matches_ascii_case(
262        label,
263        &[
264            "iso-ir-148",
265            "iso88599",
266            "iso-8859-9",
267            "iso_8859-9",
268            "latin5",
269            "csisolatin5",
270            "iso_8859-9:1989",
271        ],
272    ) {
273        return Some(IanaSingleByteEncoding::Latin5);
274    }
275    if matches_ascii_case(label, &["iso8859-11", "iso-8859-11"]) {
276        return Some(IanaSingleByteEncoding::Iso8859_11);
277    }
278    label
279        .eq_ignore_ascii_case("tis-620")
280        .then_some(IanaSingleByteEncoding::Tis620)
281}
282
283/// Return whether a WHATWG label lookup preserves the caller's requested legacy encoding.
284///
285/// WHATWG redirects many ISO labels to Windows code pages. Callers that promise exact IANA
286/// semantics must either implement those repertoires explicitly or reject the redirected label.
287#[must_use]
288pub fn legacy_label_matches_encoding(
289    label: &str,
290    encoding: &'static encoding_rs::Encoding,
291) -> bool {
292    let canonical = encoding.name();
293    let Some(code_page) = canonical.strip_prefix("windows-") else {
294        return true;
295    };
296    label.eq_ignore_ascii_case(canonical)
297        || label
298            .get(2..)
299            .is_some_and(|suffix| label[..2].eq_ignore_ascii_case("cp") && suffix == code_page)
300        || label
301            .get(4..)
302            .is_some_and(|suffix| label[..4].eq_ignore_ascii_case("x-cp") && suffix == code_page)
303}
304
305/// Decode XML bytes according to XML 1.0 encoding detection rules.
306///
307/// `explicit_encoding` is trusted resolver metadata. It is checked against the
308/// byte signature and XML declaration rather than silently overriding either.
309/// UTF-8 input is borrowed when no declaration rewrite is required; other
310/// encodings are strictly transcoded and their declaration is normalized.
311pub fn decode_xml<'a>(
312    bytes: &'a [u8],
313    explicit_encoding: Option<&str>,
314) -> Result<Cow<'a, str>, Error> {
315    decode_xml_bounded(bytes, explicit_encoding, usize::MAX)
316}
317
318/// Decode XML while preventing either the transcoded or normalized UTF-8
319/// representation from growing beyond `maximum_decoded_bytes`.
320pub fn decode_xml_bounded<'a>(
321    bytes: &'a [u8],
322    explicit_encoding: Option<&str>,
323    maximum_decoded_bytes: usize,
324) -> Result<Cow<'a, str>, Error> {
325    decode_xml_bounded_inner(
326        bytes,
327        explicit_encoding,
328        maximum_decoded_bytes,
329        true,
330        |_, _| Ok(()),
331        &NativeStorage,
332    )
333    .map(native_text)
334}
335
336/// Decode XML with a format-specific check of detected and declared encodings.
337///
338/// The guard receives the selected physical decoder name and the declaration
339/// label, when present, as borrowed strings. It runs before transcoding and again
340/// when a non-ASCII declaration becomes readable, before declaration rewriting.
341/// Returning an error prevents parsing; the ordinary decoder remains unrestricted.
342pub fn decode_xml_bounded_with_encoding_guard<'a>(
343    bytes: &'a [u8],
344    maximum_decoded_bytes: usize,
345    guard: impl Fn(&str, Option<&str>) -> Result<(), Error>,
346) -> Result<Cow<'a, str>, Error> {
347    decode_xml_bounded_inner(
348        bytes,
349        None,
350        maximum_decoded_bytes,
351        true,
352        guard,
353        &NativeStorage,
354    )
355    .map(native_text)
356}
357
358/// Decode through the same grammar using caller-owned fallible storage.
359#[doc(hidden)]
360pub fn decode_xml_in<'a, S: DecodeStorage>(
361    bytes: &'a [u8],
362    maximum_decoded_bytes: usize,
363    guard: impl Fn(&str, Option<&str>) -> Result<(), S::Error>,
364    storage: &S,
365) -> Result<Decoded<'a, S::Text>, S::Error> {
366    decode_xml_bounded_inner(bytes, None, maximum_decoded_bytes, true, guard, storage)
367}
368
369/// Detect XML-media-type text encoding without changing the included text's declaration.
370///
371/// XInclude `parse="text"` uses XML encoding detection for XML media types, but includes the
372/// decoded characters as text rather than reparsing or rewriting an XML declaration.
373pub fn decode_xml_text_bounded<'a>(
374    bytes: &'a [u8],
375    explicit_encoding: Option<&str>,
376    maximum_decoded_bytes: usize,
377) -> Result<Cow<'a, str>, Error> {
378    decode_xml_bounded_inner(
379        bytes,
380        explicit_encoding,
381        maximum_decoded_bytes,
382        false,
383        |_, _| Ok(()),
384        &NativeStorage,
385    )
386    .map(native_text)
387}
388
389fn decode_xml_bounded_inner<'a, S: DecodeStorage>(
390    bytes: &'a [u8],
391    explicit_encoding: Option<&str>,
392    maximum_decoded_bytes: usize,
393    normalize_declaration: bool,
394    encoding_guard: impl Fn(&str, Option<&str>) -> Result<(), S::Error>,
395    storage: &S,
396) -> Result<Decoded<'a, S::Text>, S::Error> {
397    // Admission precedes declaration detection, label lookup and UTF validation,
398    // including their malformed-input paths. Transcoding/output work is separate.
399    storage.charge(bytes.len())?;
400    let physical = physical_encoding(bytes)?;
401    let ascii_declaration = if physical.is_none() {
402        declaration_from_ascii_bytes(bytes)?
403    } else {
404        None
405    };
406    let explicit_utf16 = explicit_encoding.is_some_and(is_generic_utf16);
407    let explicit_utf32 = explicit_encoding.is_some_and(is_generic_utf32);
408    let explicit = explicit_encoding
409        .filter(|_| !explicit_utf16 && !explicit_utf32)
410        .map(|label| parse_encoding_in(label, storage))
411        .transpose()?;
412    if explicit_utf16
413        && !physical.is_some_and(|(encoding, bom_len)| is_utf16_encoding(encoding) && bom_len > 0)
414    {
415        // XML 1.0 section 4.3.3 requires an entity labeled as generic UTF-16 to begin with a BOM;
416        // a declaration discovered after decoding cannot replace that byte-order signature.
417        // https://www.w3.org/TR/xml/#charencoding
418        return Err(Error::MissingUtf16ByteOrder.into());
419    }
420    if explicit_utf32 && !physical.is_some_and(|(encoding, _)| is_utf32_encoding(encoding)) {
421        return Err(Error::MissingUtf32ByteOrder.into());
422    }
423    let declared_before_decode = ascii_declaration
424        .as_ref()
425        .map(|(_, label)| parse_encoding_in(label, storage))
426        .transpose()?;
427    let selected = explicit
428        .or(physical.map(|(encoding, _)| encoding))
429        .or(declared_before_decode)
430        .unwrap_or(SelectedEncoding::Standard(encoding_rs::UTF_8));
431
432    if let Some((physical, _)) = physical
433        && !encodings_compatible(selected, physical, true)
434    {
435        return Err(storage.label_error(LabelError::Conflicting, selected.name()));
436    }
437    if let Some(declared) = declared_before_decode
438        && !encodings_compatible(selected, declared, false)
439    {
440        return Err(storage.label_error(LabelError::Conflicting, declared.name()));
441    }
442
443    let bom_len = physical.map_or(0, |(_, bom_len)| bom_len);
444    encoding_guard(
445        selected.name(),
446        ascii_declaration.as_ref().map(|(_, label)| *label),
447    )?;
448    let mut decoded =
449        decode_selected_in(&bytes[bom_len..], selected, maximum_decoded_bytes, storage)?;
450    storage.charge(decoded.as_ref().len())?;
451    let declaration = declaration_from_text(decoded.as_ref())?;
452    if physical.is_some() {
453        encoding_guard(
454            selected.name(),
455            declaration
456                .as_ref()
457                .map(|range| &decoded.as_ref()[range.clone()]),
458        )?;
459    }
460    if explicit_encoding.is_none()
461        && declaration.is_none()
462        && physical.is_some_and(|(encoding, bom_len)| {
463            is_utf32_encoding(encoding) || (bom_len == 0 && is_utf16_encoding(encoding))
464        })
465    {
466        // XML 1.0 section 4.3.3 permits declarationless entities only for UTF-8 and UTF-16. A
467        // UTF-32 BOM identifies byte order but does not make UTF-32 one of those two exceptions.
468        // https://www.w3.org/TR/xml/#charencoding
469        return Err(
470            if physical.is_some_and(|(encoding, _)| is_utf32_encoding(encoding)) {
471                Error::MissingEncodingDeclaration("UTF-32")
472            } else {
473                Error::MissingUtf16ByteOrder
474            }
475            .into(),
476        );
477    }
478    if let Some(range) = &declaration {
479        let label = &decoded.as_ref()[range.clone()];
480        if is_generic_utf16(label) {
481            let has_utf16_bom = physical.is_some_and(|(encoding, bom_len)| {
482                bom_len > 0
483                    && matches!(encoding, SelectedEncoding::Standard(value)
484                        if value == encoding_rs::UTF_16LE || value == encoding_rs::UTF_16BE)
485            });
486            if !has_utf16_bom {
487                return Err(Error::MissingUtf16ByteOrder.into());
488            }
489        } else if is_generic_utf32(label) {
490            if !physical.is_some_and(|(encoding, _)| is_utf32_encoding(encoding)) {
491                return Err(Error::MissingUtf32ByteOrder.into());
492            }
493        } else {
494            let declared = parse_encoding_in(label, storage)?;
495            if !encodings_compatible(selected, declared, false) {
496                return Err(storage.label_error(LabelError::Conflicting, label));
497            }
498        }
499    }
500
501    if normalize_declaration
502        && !selected.is_utf8()
503        && let Some(range) = declaration
504    {
505        let normalized_len = decoded
506            .as_ref()
507            .len()
508            .saturating_sub(range.len())
509            .saturating_add("UTF-8".len());
510        if normalized_len > maximum_decoded_bytes {
511            return Err(Error::DecodedLimit {
512                maximum: maximum_decoded_bytes,
513                actual: normalized_len,
514            }
515            .into());
516        }
517        decoded = Decoded::Owned(match decoded {
518            Decoded::Owned(text) => storage.replace(text, range, "UTF-8")?,
519            Decoded::Borrowed(text) => {
520                let mut buffer = storage.buffer();
521                storage.append(&mut buffer, &text[..range.start])?;
522                storage.append(&mut buffer, "UTF-8")?;
523                storage.append(&mut buffer, &text[range.end..])?;
524                storage.finish(buffer)?
525            }
526        });
527    }
528    Ok(decoded)
529}
530
531/// Decode a non-XML text resource using an explicit character encoding.
532pub fn decode_text<'a>(bytes: &'a [u8], encoding: &str) -> Result<Cow<'a, str>, Error> {
533    decode_text_bounded(bytes, encoding, usize::MAX)
534}
535
536/// Decode a non-XML text resource under a retained-byte ceiling.
537pub fn decode_text_bounded<'a>(
538    bytes: &'a [u8],
539    encoding: &str,
540    maximum_decoded_bytes: usize,
541) -> Result<Cow<'a, str>, Error> {
542    if is_generic_utf16(encoding) {
543        // RFC 2781 section 3.3 requires a byte-order signature when the generic UTF-16
544        // label is used. Consume that signature before exposing text to the caller.
545        // https://www.rfc-editor.org/rfc/rfc2781.html#section-3.3
546        if let Some(payload) = bytes.strip_prefix(&[0xFF, 0xFE]) {
547            return decode_selected(
548                payload,
549                SelectedEncoding::Standard(encoding_rs::UTF_16LE),
550                maximum_decoded_bytes,
551            );
552        }
553        if let Some(payload) = bytes.strip_prefix(&[0xFE, 0xFF]) {
554            return decode_selected(
555                payload,
556                SelectedEncoding::Standard(encoding_rs::UTF_16BE),
557                maximum_decoded_bytes,
558            );
559        }
560        return Err(Error::MissingUtf16ByteOrder);
561    }
562    let selected = parse_encoding(encoding)?;
563    let bytes = match selected {
564        // XInclude 1.0 section 4.3 retains an initial U+FEFF for explicit-endian text;
565        // only generic UTF-16 treats it as a BOM. The opposite-order signature is invalid.
566        // https://www.w3.org/TR/2006/REC-xinclude-20061115/#text
567        SelectedEncoding::Standard(value) if value == encoding_rs::UTF_16LE => {
568            if bytes.starts_with(&[0xFE, 0xFF]) {
569                return Err(Error::ConflictingEncoding(selected.name().into()));
570            }
571            bytes
572        }
573        SelectedEncoding::Standard(value) if value == encoding_rs::UTF_16BE => {
574            if bytes.starts_with(&[0xFF, 0xFE]) {
575                return Err(Error::ConflictingEncoding(selected.name().into()));
576            }
577            bytes
578        }
579        // XInclude 1.0 section 4.3 uses this signature to identify UTF-8, not as included text.
580        // https://www.w3.org/TR/2006/REC-xinclude-20061115/#text
581        SelectedEncoding::Standard(value) if value == encoding_rs::UTF_8 => {
582            bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes)
583        }
584        _ => bytes,
585    };
586    decode_selected(bytes, selected, maximum_decoded_bytes)
587}
588
589fn physical_encoding(bytes: &[u8]) -> Result<Option<(SelectedEncoding, usize)>, Error> {
590    let prefix = bytes.get(..4).unwrap_or(bytes);
591    // XML 1.0 Appendix F defines UCS-4 BOMs and initial `<` signatures. The two
592    // unusual octet orders are recognized but intentionally unsupported.
593    // https://www.w3.org/TR/xml/#sec-guessing
594    match prefix {
595        [0x00, 0x00, 0xFE, 0xFF] => return Ok(Some((SelectedEncoding::Utf32Be, 4))),
596        [0xFF, 0xFE, 0x00, 0x00] => return Ok(Some((SelectedEncoding::Utf32Le, 4))),
597        [0x00, 0x00, 0x00, b'<'] => return Ok(Some((SelectedEncoding::Utf32Be, 0))),
598        [b'<', 0x00, 0x00, 0x00] => return Ok(Some((SelectedEncoding::Utf32Le, 0))),
599        [0x00, 0x00, 0xFF, 0xFE]
600        | [0xFE, 0xFF, 0x00, 0x00]
601        | [0x00, 0x00, b'<', 0x00]
602        | [0x00, b'<', 0x00, 0x00] => {
603            return Err(Error::UnsupportedByteEncoding("UTF-32 unusual byte order"));
604        }
605        _ => {}
606    }
607    if prefix == [0x4C, 0x6F, 0xA7, 0x94] {
608        return Err(Error::UnsupportedByteEncoding("EBCDIC"));
609    }
610    if let Some((encoding, length)) = encoding_rs::Encoding::for_bom(bytes) {
611        return Ok(Some((SelectedEncoding::Standard(encoding), length)));
612    }
613    Ok(match prefix {
614        [0x00, b'<', 0x00, b'?'] => Some((SelectedEncoding::Standard(encoding_rs::UTF_16BE), 0)),
615        [b'<', 0x00, b'?', 0x00] => Some((SelectedEncoding::Standard(encoding_rs::UTF_16LE), 0)),
616        _ => None,
617    })
618}
619
620fn parse_encoding(label: &str) -> Result<SelectedEncoding, Error> {
621    parse_encoding_in(label, &NativeStorage)
622}
623
624fn parse_encoding_in<S: DecodeStorage>(
625    label: &str,
626    storage: &S,
627) -> Result<SelectedEncoding, S::Error> {
628    if matches_ascii_case(label, &["utf-32le", "utf32le"]) {
629        return Ok(SelectedEncoding::Utf32Le);
630    }
631    if matches_ascii_case(label, &["utf-32be", "utf32be"]) {
632        return Ok(SelectedEncoding::Utf32Be);
633    }
634    if matches_ascii_case(label, &["us-ascii", "ascii"]) {
635        return Ok(SelectedEncoding::Ascii);
636    }
637    // The IANA-registered labels below name the same ISO-8859-1 repertoire;
638    // WHATWG-style lookup would incorrectly map them to Windows-1252. `latin-1`
639    // is retained as the already-supported punctuation variant.
640    // https://www.iana.org/assignments/character-sets/character-sets.xhtml
641    if let Some(encoding) = registered_single_byte_encoding(label) {
642        return Ok(SelectedEncoding::Registered(encoding));
643    }
644    // `encoding_rs` exposes some registered ISO repertoires directly (including
645    // ISO-8859-2). Reject only lookups whose canonical result proves that the
646    // requested IANA label was redirected to a Windows extension with different C1 bytes.
647    // XML 1.0 section 4.3.3 requires registered labels to retain their IANA meaning.
648    // https://www.w3.org/TR/xml/#charencoding
649    // XInclude 1.0 sections 4.2-4.3 make an unsupported text encoding a resource error.
650    // Decoder-only WHATWG labels must therefore not select the replacement decoder.
651    // https://www.w3.org/TR/xinclude/#text_included
652    let encoding = encoding_rs::Encoding::for_label_no_replacement(label.as_bytes())
653        .ok_or_else(|| storage.label_error(LabelError::Unsupported, label))?;
654    if !legacy_label_matches_encoding(label, encoding) {
655        return Err(storage.label_error(LabelError::Unsupported, label));
656    }
657    Ok(SelectedEncoding::Standard(encoding))
658}
659
660/// Return whether `label` selects strict ISO-8859-1 semantics.
661///
662/// This intentionally does not use WHATWG label matching, which maps these
663/// XML encoding names to Windows-1252 instead of the registered repertoire.
664#[must_use]
665pub fn is_latin1_encoding_label(label: &str) -> bool {
666    matches_ascii_case(
667        label,
668        &[
669            "iso_8859-1:1987",
670            "iso-ir-100",
671            "iso_8859-1",
672            "iso-8859-1",
673            "latin1",
674            "latin-1",
675            "l1",
676            "ibm819",
677            "cp819",
678            "csisolatin1",
679        ],
680    )
681}
682
683fn decode_selected<'a>(
684    bytes: &'a [u8],
685    encoding: SelectedEncoding,
686    maximum: usize,
687) -> Result<Cow<'a, str>, Error> {
688    decode_selected_in(bytes, encoding, maximum, &NativeStorage).map(native_text)
689}
690
691fn decode_selected_in<'a, S: DecodeStorage>(
692    bytes: &'a [u8],
693    encoding: SelectedEncoding,
694    maximum: usize,
695    storage: &S,
696) -> Result<Decoded<'a, S::Text>, S::Error> {
697    if matches!(
698        encoding,
699        SelectedEncoding::Utf32Le | SelectedEncoding::Utf32Be
700    ) {
701        return decode_utf32(bytes, encoding, maximum, storage).map(Decoded::Owned);
702    }
703    if encoding == SelectedEncoding::Ascii {
704        if bytes.iter().any(|byte| !byte.is_ascii()) {
705            return Err(Error::InvalidBytes("US-ASCII").into());
706        }
707        let decoded =
708            core::str::from_utf8(bytes).expect("seven-bit US-ASCII is always valid UTF-8");
709        if decoded.len() > maximum {
710            return Err(Error::DecodedLimit {
711                maximum,
712                actual: decoded.len(),
713            }
714            .into());
715        }
716        return Ok(Decoded::Borrowed(decoded));
717    }
718    if matches!(encoding, SelectedEncoding::Registered(_)) {
719        return decode_registered_single_byte(bytes, encoding, maximum, storage)
720            .map(Decoded::Owned);
721    }
722    let SelectedEncoding::Standard(encoding) = encoding else {
723        unreachable!("special-case encodings returned above")
724    };
725    if encoding == encoding_rs::UTF_8 {
726        let decoded = core::str::from_utf8(bytes).map_err(|_| Error::InvalidBytes("UTF-8"))?;
727        if decoded.len() > maximum {
728            return Err(Error::DecodedLimit {
729                maximum,
730                actual: decoded.len(),
731            }
732            .into());
733        }
734        return Ok(Decoded::Borrowed(decoded));
735    }
736    if (encoding == encoding_rs::UTF_16LE || encoding == encoding_rs::UTF_16BE)
737        && !bytes.len().is_multiple_of(2)
738    {
739        return Err(Error::InvalidUtf16Length(encoding.name()).into());
740    }
741
742    let mut decoder = encoding.new_decoder_without_bom_handling();
743    let mut remaining = bytes;
744    let mut decoded = storage.buffer();
745    let mut buffer = [0_u8; 4096];
746    loop {
747        // Bound input as well as output: stateful escapes can consume a long
748        // input without output. Charging the entire tail on each output chunk
749        // would turn a linear decoder into quadratic admitted work.
750        let input_length = remaining.len().min(buffer.len());
751        let last = input_length == remaining.len();
752        storage.charge(input_length)?;
753        let (result, read, written) = decoder.decode_to_utf8_without_replacement(
754            &remaining[..input_length],
755            &mut buffer,
756            last,
757        );
758        let actual = storage.len(&decoded).saturating_add(written);
759        if actual > maximum {
760            return Err(Error::DecodedLimit { maximum, actual }.into());
761        }
762        storage.charge(written)?;
763        storage.append(
764            &mut decoded,
765            core::str::from_utf8(&buffer[..written])
766                .expect("encoding_rs emits valid UTF-8 into the output buffer"),
767        )?;
768        remaining = &remaining[read..];
769        match result {
770            encoding_rs::DecoderResult::InputEmpty if last => {
771                return storage.finish(decoded).map(Decoded::Owned);
772            }
773            encoding_rs::DecoderResult::InputEmpty => {}
774            encoding_rs::DecoderResult::OutputFull => {}
775            encoding_rs::DecoderResult::Malformed(_, _) => {
776                return Err(Error::InvalidBytes(encoding.name()).into());
777            }
778        }
779    }
780}
781
782fn decode_registered_single_byte<S: DecodeStorage>(
783    bytes: &[u8],
784    encoding: SelectedEncoding,
785    maximum: usize,
786    storage: &S,
787) -> Result<S::Text, S::Error> {
788    storage.charge(bytes.len())?;
789    let mut decoded = storage.buffer();
790    for &byte in bytes {
791        let character = match encoding {
792            SelectedEncoding::Registered(encoding) => encoding
793                .decode_byte(byte)
794                .ok_or_else(|| Error::InvalidBytes(encoding.name()))?,
795            _ => unreachable!("registered single-byte decoder receives a matching encoding"),
796        };
797        let actual = storage.len(&decoded).saturating_add(character.len_utf8());
798        if actual > maximum {
799            return Err(Error::DecodedLimit { maximum, actual }.into());
800        }
801        storage.append(&mut decoded, character.encode_utf8(&mut [0; 4]))?;
802    }
803    storage.finish(decoded)
804}
805
806fn decode_utf32<S: DecodeStorage>(
807    bytes: &[u8],
808    encoding: SelectedEncoding,
809    maximum: usize,
810    storage: &S,
811) -> Result<S::Text, S::Error> {
812    if !bytes.len().is_multiple_of(4) {
813        return Err(Error::InvalidUtf32Length(encoding.name()).into());
814    }
815    storage.charge(bytes.len())?;
816    let mut decoded = storage.buffer();
817    let (units, remainder) = bytes.as_chunks::<4>();
818    debug_assert!(remainder.is_empty());
819    for &unit in units {
820        let scalar = match encoding {
821            SelectedEncoding::Utf32Le => u32::from_le_bytes(unit),
822            SelectedEncoding::Utf32Be => u32::from_be_bytes(unit),
823            _ => unreachable!("UTF-32 decoder receives an explicit byte order"),
824        };
825        let character = char::from_u32(scalar).ok_or(Error::InvalidBytes(encoding.name()))?;
826        let actual = storage.len(&decoded).saturating_add(character.len_utf8());
827        if actual > maximum {
828            return Err(Error::DecodedLimit { maximum, actual }.into());
829        }
830        storage.append(&mut decoded, character.encode_utf8(&mut [0; 4]))?;
831    }
832    storage.finish(decoded)
833}
834
835fn encodings_compatible(
836    selected: SelectedEncoding,
837    candidate: SelectedEncoding,
838    physical: bool,
839) -> bool {
840    selected == candidate
841        || (physical
842            && matches!(selected, SelectedEncoding::Standard(value) if value == encoding_rs::UTF_8)
843            && matches!(candidate, SelectedEncoding::Standard(value) if value == encoding_rs::UTF_8))
844}
845
846fn is_utf16_encoding(encoding: SelectedEncoding) -> bool {
847    matches!(encoding, SelectedEncoding::Standard(value)
848        if value == encoding_rs::UTF_16LE || value == encoding_rs::UTF_16BE)
849}
850
851fn is_utf32_encoding(encoding: SelectedEncoding) -> bool {
852    matches!(
853        encoding,
854        SelectedEncoding::Utf32Le | SelectedEncoding::Utf32Be
855    )
856}
857
858// XML 1.0 section 2.3 production [3] defines S as exactly these four bytes.
859// https://www.w3.org/TR/xml/#NT-S
860const fn is_xml_s_byte(byte: &u8) -> bool {
861    matches!(*byte, b' ' | b'\t' | b'\r' | b'\n')
862}
863
864fn declaration_from_ascii_bytes(bytes: &[u8]) -> Result<Option<(Range<usize>, &str)>, Error> {
865    let bytes = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes);
866    if !bytes.starts_with(b"<?xml") || !bytes.get(5).is_some_and(is_xml_s_byte) {
867        return Ok(None);
868    }
869    let end = bytes
870        .windows(2)
871        .position(|window| window == b"?>")
872        .map(|index| index + 2)
873        .ok_or(Error::MalformedDeclaration("unterminated declaration"))?;
874    // XML encoding declarations are ASCII for every supported
875    // ASCII-compatible encoding, regardless of the following document bytes.
876    let prefix = core::str::from_utf8(&bytes[..end])
877        .map_err(|_| Error::MalformedDeclaration("declaration is not ASCII-compatible"))?;
878    declaration_from_text(prefix).map(|range| {
879        range.map(|range| {
880            let label = &prefix[range.clone()];
881            (range, label)
882        })
883    })
884}
885
886fn declaration_from_text(xml: &str) -> Result<Option<Range<usize>>, Error> {
887    let Some(rest) = xml.strip_prefix("<?xml") else {
888        return Ok(None);
889    };
890    if !rest.as_bytes().first().is_some_and(is_xml_s_byte) {
891        return Ok(None);
892    }
893    let end = rest
894        .find("?>")
895        .ok_or(Error::MalformedDeclaration("unterminated declaration"))?;
896    let declaration = &rest.as_bytes()[..end];
897    let mut cursor = 0;
898    let mut encoding_range = None;
899    while cursor < declaration.len() {
900        while declaration.get(cursor).is_some_and(is_xml_s_byte) {
901            cursor += 1;
902        }
903        if cursor == declaration.len() {
904            break;
905        }
906        let name_start = cursor;
907        while declaration.get(cursor).is_some_and(|byte| {
908            byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':' | b'-' | b'.')
909        }) {
910            cursor += 1;
911        }
912        if cursor == name_start {
913            return Err(Error::MalformedDeclaration("invalid pseudo-attribute"));
914        }
915        let name = &declaration[name_start..cursor];
916        while declaration.get(cursor).is_some_and(is_xml_s_byte) {
917            cursor += 1;
918        }
919        if declaration.get(cursor) != Some(&b'=') {
920            return Err(Error::MalformedDeclaration("missing `=`"));
921        }
922        cursor += 1;
923        while declaration.get(cursor).is_some_and(is_xml_s_byte) {
924            cursor += 1;
925        }
926        let &quote @ (b'\'' | b'"') = declaration
927            .get(cursor)
928            .ok_or(Error::MalformedDeclaration("missing quoted value"))?
929        else {
930            return Err(Error::MalformedDeclaration("value is not quoted"));
931        };
932        let value_start = cursor + 1;
933        let value_end = value_start
934            + declaration[value_start..]
935                .iter()
936                .position(|byte| *byte == quote)
937                .ok_or(Error::MalformedDeclaration("unterminated value"))?;
938        if name == b"encoding" {
939            let value = &declaration[value_start..value_end];
940            // XML 1.0 section 4.3.3 production [81] requires EncName to start with an ASCII
941            // letter and limits the remaining characters. Validate before normalization erases
942            // the declaration. https://www.w3.org/TR/xml/#NT-EncName
943            if !is_xml_encoding_name_bytes(value) {
944                return Err(Error::MalformedDeclaration("invalid encoding name"));
945            }
946            encoding_range = Some((5 + value_start)..(5 + value_end));
947        }
948        cursor = value_end + 1;
949        if declaration
950            .get(cursor)
951            .is_some_and(|byte| !is_xml_s_byte(byte))
952        {
953            return Err(Error::MalformedDeclaration("missing whitespace"));
954        }
955    }
956    // XML 1.0 section 2.8 production [23] makes EncodingDecl part of one complete XMLDecl;
957    // selection is valid only after every following pseudo-attribute has been checked.
958    // https://www.w3.org/TR/xml/#NT-XMLDecl
959    Ok(encoding_range)
960}
961
962/// Return whether a label satisfies XML 1.0's `EncName` production.
963///
964/// See XML 1.0 section 4.3.3, production [81]:
965/// https://www.w3.org/TR/xml/#NT-EncName
966#[must_use]
967pub fn is_xml_encoding_name(value: &str) -> bool {
968    is_xml_encoding_name_bytes(value.as_bytes())
969}
970
971fn is_xml_encoding_name_bytes(value: &[u8]) -> bool {
972    value.first().is_some_and(u8::is_ascii_alphabetic)
973        && value[1..]
974            .iter()
975            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
976}
977
978fn is_generic_utf16(label: &str) -> bool {
979    label.eq_ignore_ascii_case("UTF-16") || label.eq_ignore_ascii_case("UTF16")
980}
981
982fn is_generic_utf32(label: &str) -> bool {
983    label.eq_ignore_ascii_case("UTF-32") || label.eq_ignore_ascii_case("UTF32")
984}
985
986fn matches_ascii_case(value: &str, candidates: &[&str]) -> bool {
987    candidates
988        .iter()
989        .any(|candidate| value.eq_ignore_ascii_case(candidate))
990}
991
992#[cfg(all(test, feature = "std"))]
993#[allow(clippy::unwrap_used)]
994mod tests {
995    use std::borrow::Cow;
996
997    use super::{
998        Error, declaration_from_ascii_bytes, declaration_from_text, decode_text, decode_xml,
999        decode_xml_bounded, decode_xml_bounded_with_encoding_guard,
1000    };
1001
1002    #[test]
1003    fn encoding_guard_precedes_conversion_and_preserves_borrowing() {
1004        // Format rejection must precede conversion, and accepted UTF-8 needs no copy.
1005        let rejected = decode_xml_bounded_with_encoding_guard(
1006            b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root/>",
1007            0,
1008            |physical, _| Err(Error::ForbiddenEncoding(physical.into())),
1009        );
1010        assert!(matches!(rejected, Err(Error::ForbiddenEncoding(_))));
1011        let source = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>";
1012        let accepted =
1013            decode_xml_bounded_with_encoding_guard(source, source.len(), |physical, declared| {
1014                assert_eq!(physical, "UTF-8");
1015                assert_eq!(declared, Some("UTF-8"));
1016                Ok(())
1017            })
1018            .unwrap();
1019        assert!(matches!(accepted, Cow::Borrowed(_)));
1020    }
1021
1022    fn encode_utf32(source: &str, little_endian: bool) -> Vec<u8> {
1023        source
1024            .chars()
1025            .flat_map(|character| {
1026                let value = u32::from(character);
1027                if little_endian {
1028                    value.to_le_bytes()
1029                } else {
1030                    value.to_be_bytes()
1031                }
1032            })
1033            .collect()
1034    }
1035
1036    #[test]
1037    fn utf8_without_a_rewritten_declaration_stays_borrowed() {
1038        let bytes = b"<root>ok</root>";
1039        assert!(matches!(decode_xml(bytes, None), Ok(Cow::Borrowed(_))));
1040    }
1041
1042    #[test]
1043    fn generic_utf16_uses_the_bom_byte_order() {
1044        let source = "<?xml version=\"1.0\" encoding=\"UTF-16\"?><root>lambda</root>";
1045        let mut bytes = vec![0xFE, 0xFF];
1046        bytes.extend(source.encode_utf16().flat_map(u16::to_be_bytes));
1047        let decoded = decode_xml(&bytes, Some("UTF-16")).expect("BOM selects UTF-16BE");
1048        assert!(decoded.contains("encoding=\"UTF-8\""));
1049        assert!(decoded.contains("<root>lambda</root>"));
1050    }
1051
1052    #[test]
1053    fn xml_declaration_rejects_values_outside_the_enc_name_grammar() {
1054        // XML 1.0 section 4.3.3 production [81] excludes `:` from EncName.
1055        // https://www.w3.org/TR/xml/#NT-EncName
1056        let bytes = b"<?xml version=\"1.0\" encoding=\"iso_8859-1:1987\"?><root/>";
1057        assert!(matches!(
1058            decode_xml(bytes, None),
1059            Err(Error::MalformedDeclaration("invalid encoding name"))
1060        ));
1061    }
1062
1063    #[test]
1064    fn encoding_selection_requires_a_complete_well_formed_declaration() {
1065        // XML 1.0 section 2.8 production [23] requires the declaration to match XMLDecl in full;
1066        // finding EncodingDecl is not permission to ignore malformed trailing pseudo-attributes.
1067        // https://www.w3.org/TR/xml/#NT-XMLDecl
1068        let malformed = "<?xml version=\"1.0\" encoding=\"UTF-8\" trailing?><root/>";
1069        assert!(matches!(
1070            declaration_from_text(malformed),
1071            Err(Error::MalformedDeclaration("missing `=`"))
1072        ));
1073        assert!(matches!(
1074            declaration_from_ascii_bytes(malformed.as_bytes()),
1075            Err(Error::MalformedDeclaration("missing `=`"))
1076        ));
1077
1078        let mut utf16 = vec![0xFF, 0xFE];
1079        utf16.extend(malformed.encode_utf16().flat_map(u16::to_le_bytes));
1080        assert!(matches!(
1081            decode_xml(&utf16, None),
1082            Err(Error::MalformedDeclaration("missing `=`"))
1083        ));
1084    }
1085
1086    #[test]
1087    fn xml_declaration_rejects_non_xml_ascii_whitespace() {
1088        // XML 1.0 section 2.3 production [3] limits S to space, tab, CR, and LF.
1089        // https://www.w3.org/TR/xml/#NT-S
1090        for whitespace in *b"\x0b\x0c" {
1091            let source = [
1092                b"<?xml".as_slice(),
1093                &[whitespace],
1094                b"version=\"1.0\" encoding=\"ISO-8859-1\"?><root>caf\xe9</root>".as_slice(),
1095            ]
1096            .concat();
1097            assert!(matches!(
1098                decode_xml(&source, None),
1099                Err(Error::InvalidBytes("UTF-8"))
1100            ));
1101        }
1102    }
1103
1104    #[test]
1105    fn xml_prefixed_processing_instruction_is_not_a_declaration() {
1106        // XML 1.0 sections 2.6 and 2.8 distinguish PI targets from XMLDecl by the mandatory
1107        // whitespace after `xml`: https://www.w3.org/TR/xml/#sec-prolog-dtd
1108        let bytes = b"<?xml-stylesheet title='caf\xE9'?><root/>";
1109        assert_eq!(
1110            decode_xml(bytes, Some("windows-1252")).expect("PI follows resolver encoding"),
1111            "<?xml-stylesheet title='caf\u{e9}'?><root/>"
1112        );
1113    }
1114
1115    #[test]
1116    fn utf32_requires_a_bom_metadata_or_encoding_declaration() {
1117        // XML 1.0 Appendix F defines both UCS-4 BOMs and the BOM-less `<` signatures.
1118        // https://www.w3.org/TR/xml/#sec-guessing
1119        let source = "<?xml version=\"1.0\" encoding=\"UTF-32\"?><root>lambda</root>";
1120        for (little_endian, bom) in [
1121            (false, [0x00, 0x00, 0xFE, 0xFF]),
1122            (true, [0xFF, 0xFE, 0x00, 0x00]),
1123        ] {
1124            let mut bytes = bom.to_vec();
1125            bytes.extend(encode_utf32(source, little_endian));
1126            let decoded = decode_xml(&bytes, None).expect("UTF-32 BOM selects byte order");
1127            assert!(decoded.contains("encoding=\"UTF-8\""));
1128            assert!(decoded.contains("<root>lambda</root>"));
1129
1130            let mut declarationless = bom.to_vec();
1131            declarationless.extend(encode_utf32("<root>lambda</root>", little_endian));
1132            assert!(
1133                decode_xml(&declarationless, None).is_err(),
1134                "a UTF-32 BOM identifies byte order but does not replace the encoding declaration"
1135            );
1136        }
1137
1138        for little_endian in [false, true] {
1139            let declaration = encode_utf32(
1140                "<?xml version=\"1.0\" encoding=\"UTF-32\"?><root>lambda</root>",
1141                little_endian,
1142            );
1143            assert!(
1144                decode_xml(&declaration, None)
1145                    .expect("the initial signature and declaration identify UTF-32")
1146                    .contains("<root>lambda</root>")
1147            );
1148            let declarationless = encode_utf32("<root>lambda</root>", little_endian);
1149            assert!(matches!(
1150                decode_xml(&declarationless, None),
1151                Err(Error::MissingEncodingDeclaration("UTF-32"))
1152            ));
1153            assert_eq!(
1154                decode_xml(
1155                    &declarationless,
1156                    Some(if little_endian {
1157                        "UTF-32LE"
1158                    } else {
1159                        "UTF-32BE"
1160                    })
1161                )
1162                .expect("trusted external metadata supplies the encoding"),
1163                "<root>lambda</root>"
1164            );
1165        }
1166    }
1167
1168    #[test]
1169    fn utf32_rejects_truncation_invalid_scalars_and_conflicting_metadata() {
1170        let mut truncated = vec![0x00, 0x00, 0xFE, 0xFF];
1171        truncated.extend([0x00, 0x00, 0x00]);
1172        assert!(decode_xml(&truncated, None).is_err());
1173
1174        let mut surrogate = vec![0x00, 0x00, 0xFE, 0xFF];
1175        surrogate.extend(0xD800_u32.to_be_bytes());
1176        assert!(matches!(
1177            decode_xml(&surrogate, None),
1178            Err(Error::InvalidBytes("UTF-32BE"))
1179        ));
1180
1181        let mut little_endian = vec![0xFF, 0xFE, 0x00, 0x00];
1182        little_endian.extend(encode_utf32("<root/>", true));
1183        assert!(matches!(
1184            decode_xml(&little_endian, Some("UTF-32BE")),
1185            Err(Error::ConflictingEncoding(_))
1186        ));
1187    }
1188
1189    #[test]
1190    fn utf32_decoding_obeys_the_utf8_materialization_limit() {
1191        let mut bytes = vec![0x00, 0x00, 0xFE, 0xFF];
1192        bytes.extend(encode_utf32("<root>lambda</root>", false));
1193        assert!(matches!(
1194            decode_xml_bounded(&bytes, None, 8),
1195            Err(Error::DecodedLimit { maximum: 8, .. })
1196        ));
1197    }
1198
1199    #[test]
1200    fn bomless_generic_utf16_is_rejected_as_ambiguous() {
1201        let bytes = "<?xml version=\"1.0\" encoding=\"UTF-16\"?><root/>"
1202            .encode_utf16()
1203            .flat_map(u16::to_le_bytes)
1204            .collect::<Vec<_>>();
1205        assert!(matches!(
1206            decode_xml(&bytes, Some("UTF-16")),
1207            Err(Error::MissingUtf16ByteOrder)
1208        ));
1209    }
1210
1211    #[test]
1212    fn generic_utf16_metadata_does_not_replace_the_required_bom() {
1213        // XML 1.0 section 4.3.3 requires every UTF-16 entity to begin with a BOM; a generic
1214        // transport label supplies no byte order and cannot relax that document constraint.
1215        // https://www.w3.org/TR/xml/#charencoding
1216        let bytes = "<?xml version=\"1.0\"?><root/>"
1217            .encode_utf16()
1218            .flat_map(u16::to_le_bytes)
1219            .collect::<Vec<_>>();
1220        assert!(matches!(
1221            decode_xml(&bytes, Some("UTF-16")),
1222            Err(Error::MissingUtf16ByteOrder)
1223        ));
1224    }
1225
1226    #[test]
1227    fn generic_utf16_metadata_cannot_bypass_the_bom_via_a_specific_declaration() {
1228        // XML 1.0 section 4.3.3 requires generic UTF-16 entities to begin with a BOM even when
1229        // their declaration reveals the byte order: https://www.w3.org/TR/xml/#charencoding
1230        let bytes = "<?xml version=\"1.0\" encoding=\"UTF-16LE\"?><root/>"
1231            .encode_utf16()
1232            .flat_map(u16::to_le_bytes)
1233            .collect::<Vec<_>>();
1234        assert!(matches!(
1235            decode_xml(&bytes, Some("UTF-16")),
1236            Err(Error::MissingUtf16ByteOrder)
1237        ));
1238    }
1239
1240    #[test]
1241    fn explicit_utf16_byte_order_decodes_a_declarationless_resource() {
1242        let bytes = "<root>lambda</root>"
1243            .encode_utf16()
1244            .flat_map(u16::to_le_bytes)
1245            .collect::<Vec<_>>();
1246        assert_eq!(
1247            decode_xml(&bytes, Some("UTF-16LE")).unwrap(),
1248            "<root>lambda</root>"
1249        );
1250    }
1251
1252    #[test]
1253    fn endian_specific_utf16_accepts_a_matching_byte_order_mark() {
1254        // RFC 2781 sections 4.1 and 4.2 require a matching signature to be consumed without
1255        // affecting deserialization. https://www.rfc-editor.org/rfc/rfc2781#section-4.1
1256        let mut metadata = vec![0xFF, 0xFE];
1257        metadata.extend("<root/>".encode_utf16().flat_map(u16::to_le_bytes));
1258        assert_eq!(decode_xml(&metadata, Some("UTF-16LE")).unwrap(), "<root/>");
1259        assert_eq!(
1260            decode_text(&metadata, "UTF-16LE").unwrap(),
1261            "\u{feff}<root/>"
1262        );
1263
1264        let mut big_endian_text = vec![0xFE, 0xFF];
1265        big_endian_text.extend("body".encode_utf16().flat_map(u16::to_be_bytes));
1266        assert_eq!(
1267            decode_text(&big_endian_text, "UTF-16BE").unwrap(),
1268            "\u{feff}body"
1269        );
1270
1271        let mut declaration = vec![0xFE, 0xFF];
1272        declaration.extend(
1273            "<?xml version=\"1.0\" encoding=\"UTF-16BE\"?><root/>"
1274                .encode_utf16()
1275                .flat_map(u16::to_be_bytes),
1276        );
1277        assert_eq!(
1278            decode_xml(&declaration, None).unwrap(),
1279            "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>"
1280        );
1281    }
1282
1283    #[test]
1284    fn generic_utf16_text_requires_and_consumes_a_byte_order_mark() {
1285        let text = "lambda";
1286        let mut little = vec![0xFF, 0xFE];
1287        little.extend(text.encode_utf16().flat_map(u16::to_le_bytes));
1288        let mut big = vec![0xFE, 0xFF];
1289        big.extend(text.encode_utf16().flat_map(u16::to_be_bytes));
1290
1291        assert_eq!(decode_text(&little, "UTF-16").unwrap(), text);
1292        assert_eq!(decode_text(&big, "UTF-16").unwrap(), text);
1293        assert!(matches!(
1294            decode_text(&little[2..], "UTF-16"),
1295            Err(Error::MissingUtf16ByteOrder)
1296        ));
1297    }
1298
1299    #[test]
1300    fn utf8_text_consumes_its_signature_without_removing_content_fe_ff() {
1301        // XInclude 1.0 section 4.3 treats the BOM as an encoding signature, not included text.
1302        // https://www.w3.org/TR/2006/REC-xinclude-20061115/#text
1303        assert_eq!(decode_text(b"\xef\xbb\xbfbody", "UTF-8").unwrap(), "body");
1304        assert_eq!(decode_text(b"\xef\xbb\xbfbody", "utf8").unwrap(), "body");
1305        assert_eq!(
1306            decode_text(b"body\xef\xbb\xbftail", "UTF-8").unwrap(),
1307            "body\u{feff}tail"
1308        );
1309        assert_eq!(decode_text(b"body", "UTF-8").unwrap(), "body");
1310    }
1311
1312    #[test]
1313    fn trusted_metadata_cannot_conflict_with_a_bom() {
1314        assert!(matches!(
1315            decode_xml(&[0xFF, 0xFE, b'A', 0], Some("UTF-8")),
1316            Err(Error::ConflictingEncoding(_))
1317        ));
1318    }
1319
1320    #[test]
1321    fn latin1_and_windows_1252_keep_distinct_c1_semantics() {
1322        // Every IANA label must select the exact registered repertoire rather than the
1323        // WHATWG replacement decoder used for HTML compatibility.
1324        for alias in [
1325            "ISO_8859-1:1987",
1326            "iso-ir-100",
1327            "ISO_8859-1",
1328            "ISO-8859-1",
1329            "latin1",
1330            "l1",
1331            "IBM819",
1332            "CP819",
1333            "csISOLatin1",
1334        ] {
1335            assert_eq!(
1336                decode_text(&[0x80], alias).unwrap(),
1337                "\u{80}",
1338                "IANA alias {alias} must retain ISO-8859-1 C1 semantics"
1339            );
1340        }
1341        assert_eq!(decode_text(&[0x80], "windows-1252").unwrap(), "€");
1342    }
1343
1344    #[test]
1345    fn iana_single_byte_encodings_do_not_inherit_windows_extensions() {
1346        // XML 1.0 section 4.3.3 requires IANA labels to retain their registered meaning.
1347        // https://www.w3.org/TR/xml/#charencoding
1348        assert_eq!(decode_text(&[0x80], "ISO-8859-9").unwrap(), "\u{80}");
1349        assert_eq!(decode_text(&[0x80], "iso88599").unwrap(), "\u{80}");
1350        assert_eq!(decode_text(&[0xD0, 0xFD], "ISO-8859-9").unwrap(), "Ğı");
1351        assert_eq!(decode_text(&[0x80], "windows-1254").unwrap(), "€");
1352
1353        assert_eq!(decode_text(&[0xA0], "ISO-8859-11").unwrap(), "\u{A0}");
1354        assert!(matches!(
1355            decode_text(&[0x80], "TIS-620"),
1356            Err(Error::InvalidBytes("TIS-620"))
1357        ));
1358        assert!(matches!(
1359            decode_text(&[0xA0], "TIS-620"),
1360            Err(Error::InvalidBytes("TIS-620"))
1361        ));
1362        assert_eq!(decode_text(&[0xA1, 0xFB], "TIS-620").unwrap(), "ก๛");
1363        assert_eq!(decode_text(&[0x80], "windows-874").unwrap(), "€");
1364    }
1365
1366    #[test]
1367    fn registered_iana_labels_retain_their_declared_repertoires() {
1368        // XML 1.0 section 4.3.3 requires an IANA encoding name to retain its registered
1369        // semantics; ISO-8859-2 therefore must not be confused with Windows-1250.
1370        // https://www.w3.org/TR/xml/#charencoding
1371        assert_eq!(decode_text(&[0x80], "ISO-8859-2").unwrap(), "\u{80}");
1372        assert_eq!(decode_text(&[0xA1], "ISO-8859-2").unwrap(), "Ą");
1373        assert_eq!(decode_text(&[0x80], "windows-1250").unwrap(), "€");
1374
1375        let source = b"<?xml version=\"1.0\" encoding=\"ISO-8859-2\"?><root>\xA1</root>";
1376        assert!(decode_xml(source, None).unwrap().contains("<root>Ą</root>"));
1377    }
1378
1379    #[test]
1380    fn encoding_lookup_selects_the_strict_iso_8859_2_codec() {
1381        // Keep the dependency contract explicit: this label is not a WHATWG redirect in the
1382        // encoding_rs release used by the parser, so the standard decoder is the strict codec.
1383        assert_eq!(
1384            encoding_rs::Encoding::for_label(b"ISO-8859-2"),
1385            Some(encoding_rs::ISO_8859_2)
1386        );
1387        assert_eq!(encoding_rs::ISO_8859_2.name(), "ISO-8859-2");
1388    }
1389
1390    #[test]
1391    fn decoder_only_labels_are_reported_as_unsupported() {
1392        // XInclude 1.0 sections 4.2-4.3 classify an unsupported text encoding as a resource
1393        // error, so decoder-only WHATWG labels must not reach the replacement decoder.
1394        // https://www.w3.org/TR/xinclude/#text_included
1395        for label in ["replacement", "ISO-2022-KR"] {
1396            assert!(matches!(
1397                decode_text(b"", label),
1398                Err(Error::UnsupportedEncoding(rejected)) if rejected == label
1399            ));
1400        }
1401    }
1402
1403    #[test]
1404    fn us_ascii_rejects_non_ascii_bytes() {
1405        // WHATWG aliases US-ASCII to Windows-1252, but XML's declared encoding
1406        // contract permits only seven-bit bytes for this label.
1407        assert!(matches!(
1408            decode_text(&[0x80], "US-ASCII"),
1409            Err(Error::InvalidBytes("US-ASCII"))
1410        ));
1411        assert_eq!(
1412            decode_text(b"plain ASCII", "US-ASCII").unwrap(),
1413            "plain ASCII"
1414        );
1415    }
1416
1417    #[test]
1418    fn transcoded_and_normalized_representations_are_both_bounded() {
1419        let bytes = b"<?xml version=\"1.0\" encoding=\"GBK\"?><root/>";
1420        let exact = decode_xml(bytes, None).expect("GBK declaration is supported");
1421        assert!(matches!(
1422            decode_xml_bounded(bytes, None, exact.len() - 1),
1423            Err(Error::DecodedLimit { .. })
1424        ));
1425        assert_eq!(decode_xml_bounded(bytes, None, exact.len()).unwrap(), exact);
1426    }
1427
1428    #[test]
1429    fn declaration_detection_is_not_limited_to_a_short_prefix() {
1430        let whitespace = " ".repeat(2_048);
1431        let source = format!("<?xml{whitespace}encoding=\"ISO-8859-1\"?><root>caf\u{e9}</root>");
1432        let bytes = source
1433            .chars()
1434            .map(|character| u8::try_from(u32::from(character)).unwrap())
1435            .collect::<Vec<_>>();
1436        assert!(decode_xml(&bytes, None).unwrap().contains("café"));
1437    }
1438
1439    #[test]
1440    fn declaration_allows_whitespace_before_its_terminator() {
1441        let source = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?><root>caf\xe9</root>";
1442        assert!(decode_xml(source, None).unwrap().contains("café"));
1443    }
1444
1445    #[test]
1446    fn unsupported_xml_signatures_fail_explicitly() {
1447        assert!(matches!(
1448            decode_xml(&[0x4C, 0x6F, 0xA7, 0x94], None),
1449            Err(Error::UnsupportedByteEncoding("EBCDIC"))
1450        ));
1451    }
1452
1453    #[test]
1454    fn truncated_utf16_reports_the_code_unit_boundary() {
1455        assert!(matches!(
1456            decode_xml(&[0xff, 0xfe, 0], None),
1457            Err(Error::InvalidUtf16Length("UTF-16LE"))
1458        ));
1459    }
1460}