use alloc::{borrow::Cow, string::String};
use core::ops::Range;
#[doc(hidden)]
pub trait DecodeStorage {
type Text: AsRef<str>;
type Buffer;
type Error: From<Error>;
fn label_error(&self, kind: LabelError, label: &str) -> Self::Error;
fn charge(&self, bytes: usize) -> Result<(), Self::Error>;
fn buffer(&self) -> Self::Buffer;
fn len(&self, buffer: &Self::Buffer) -> usize;
fn append(&self, buffer: &mut Self::Buffer, text: &str) -> Result<(), Self::Error>;
fn finish(&self, buffer: Self::Buffer) -> Result<Self::Text, Self::Error>;
fn replace(
&self,
text: Self::Text,
range: Range<usize>,
replacement: &str,
) -> Result<Self::Text, Self::Error>;
}
#[doc(hidden)]
#[derive(Clone, Copy)]
pub enum LabelError {
Unsupported,
Forbidden,
Conflicting,
}
#[doc(hidden)]
pub enum Decoded<'a, T> {
Borrowed(&'a str),
Owned(T),
}
impl<T: AsRef<str>> AsRef<str> for Decoded<'_, T> {
fn as_ref(&self) -> &str {
match self {
Self::Borrowed(text) => text,
Self::Owned(text) => text.as_ref(),
}
}
}
pub(crate) struct NativeStorage;
impl DecodeStorage for NativeStorage {
type Text = String;
type Buffer = String;
type Error = Error;
fn label_error(&self, kind: LabelError, label: &str) -> Error {
match kind {
LabelError::Unsupported => Error::UnsupportedEncoding(label.into()),
LabelError::Forbidden => Error::ForbiddenEncoding(label.into()),
LabelError::Conflicting => Error::ConflictingEncoding(label.into()),
}
}
fn charge(&self, _: usize) -> Result<(), Error> {
Ok(())
}
fn buffer(&self) -> String {
String::new()
}
fn len(&self, buffer: &String) -> usize {
buffer.len()
}
fn append(&self, buffer: &mut String, text: &str) -> Result<(), Error> {
buffer.push_str(text);
Ok(())
}
fn finish(&self, buffer: String) -> Result<String, Error> {
Ok(buffer)
}
fn replace(
&self,
mut text: String,
range: Range<usize>,
replacement: &str,
) -> Result<String, Error> {
text.replace_range(range, replacement);
Ok(text)
}
}
fn native_text(text: Decoded<'_, String>) -> Cow<'_, str> {
match text {
Decoded::Borrowed(text) => Cow::Borrowed(text),
Decoded::Owned(text) => Cow::Owned(text),
}
}
#[path = "lexical.rs"]
pub mod lexical;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error<L: core::fmt::Display = String> {
#[error("unsupported XML byte encoding `{0}`")]
UnsupportedByteEncoding(&'static str),
#[error("unsupported XML encoding `{0}`")]
UnsupportedEncoding(L),
#[error("XML format does not permit encoding `{0}`")]
ForbiddenEncoding(L),
#[error("XML byte encoding conflicts with declared or selected encoding `{0}`")]
ConflictingEncoding(L),
#[error("malformed XML encoding declaration: {0}")]
MalformedDeclaration(&'static str),
#[error("XML input contains invalid {0} bytes")]
InvalidBytes(&'static str),
#[error("BOM-less UTF-16 XML input requires an explicit UTF-16LE or UTF-16BE declaration")]
MissingUtf16ByteOrder,
#[error("UTF-32 XML input requires a UTF-32LE/UTF-32BE encoding or byte-order signature")]
MissingUtf32ByteOrder,
#[error("{0} XML input requires an encoding declaration or trusted external encoding metadata")]
MissingEncodingDeclaration(&'static str),
#[error("{0} XML input has an odd byte length")]
InvalidUtf16Length(&'static str),
#[error("{0} XML input byte length is not divisible by four")]
InvalidUtf32Length(&'static str),
#[error("decoded XML exceeds the maximum size of {maximum} bytes: at least {actual} bytes")]
DecodedLimit {
maximum: usize,
actual: usize,
},
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum SelectedEncoding {
Standard(&'static encoding_rs::Encoding),
Utf32Le,
Utf32Be,
Ascii,
Registered(IanaSingleByteEncoding),
}
impl SelectedEncoding {
fn name(self) -> &'static str {
match self {
Self::Standard(encoding) => encoding.name(),
Self::Utf32Le => "UTF-32LE",
Self::Utf32Be => "UTF-32BE",
Self::Ascii => "US-ASCII",
Self::Registered(encoding) => encoding.name(),
}
}
fn is_utf8(self) -> bool {
matches!(self, Self::Standard(encoding) if encoding == encoding_rs::UTF_8)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IanaSingleByteEncoding {
Latin1,
Latin5,
Iso8859_11,
Tis620,
}
impl IanaSingleByteEncoding {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Latin1 => "ISO-8859-1",
Self::Latin5 => "ISO-8859-9",
Self::Iso8859_11 => "ISO-8859-11",
Self::Tis620 => "TIS-620",
}
}
#[must_use]
pub fn decode_byte(self, byte: u8) -> Option<char> {
match self {
Self::Latin1 => Some(char::from(byte)),
Self::Latin5 => Some(match byte {
0xD0 => '\u{011E}',
0xDD => '\u{0130}',
0xDE => '\u{015E}',
0xF0 => '\u{011F}',
0xFD => '\u{0131}',
0xFE => '\u{015F}',
_ => char::from(byte),
}),
Self::Iso8859_11 | Self::Tis620 => match byte {
0x00..=0x7F => Some(char::from(byte)),
0xA0 if self == Self::Iso8859_11 => Some('\u{00A0}'),
0xA1..=0xDA | 0xE0..=0xFB => char::from_u32(u32::from(byte) + 0x0D60),
0xDF => Some('\u{0E3F}'),
_ => None,
},
}
}
#[must_use]
pub fn encode_char(self, character: char) -> Option<u8> {
match self {
Self::Latin1 => u8::try_from(u32::from(character)).ok(),
Self::Latin5 => match character {
'\u{011E}' => Some(0xD0),
'\u{0130}' => Some(0xDD),
'\u{015E}' => Some(0xDE),
'\u{011F}' => Some(0xF0),
'\u{0131}' => Some(0xFD),
'\u{015F}' => Some(0xFE),
_ => u8::try_from(u32::from(character))
.ok()
.filter(|byte| !matches!(byte, 0xD0 | 0xDD | 0xDE | 0xF0 | 0xFD | 0xFE)),
},
Self::Iso8859_11 | Self::Tis620 => match u32::from(character) {
value @ 0x00..=0x7F => Some(value as u8),
0xA0 if self == Self::Iso8859_11 => Some(0xA0),
value @ 0x0E01..=0x0E3A | value @ 0x0E40..=0x0E5B => {
u8::try_from(value - 0x0D60).ok()
}
0x0E3F => Some(0xDF),
_ => None,
},
}
}
}
#[must_use]
pub fn registered_single_byte_encoding(label: &str) -> Option<IanaSingleByteEncoding> {
if is_latin1_encoding_label(label) {
return Some(IanaSingleByteEncoding::Latin1);
}
if matches_ascii_case(
label,
&[
"iso-ir-148",
"iso88599",
"iso-8859-9",
"iso_8859-9",
"latin5",
"csisolatin5",
"iso_8859-9:1989",
],
) {
return Some(IanaSingleByteEncoding::Latin5);
}
if matches_ascii_case(label, &["iso8859-11", "iso-8859-11"]) {
return Some(IanaSingleByteEncoding::Iso8859_11);
}
label
.eq_ignore_ascii_case("tis-620")
.then_some(IanaSingleByteEncoding::Tis620)
}
#[must_use]
pub fn legacy_label_matches_encoding(
label: &str,
encoding: &'static encoding_rs::Encoding,
) -> bool {
let canonical = encoding.name();
let Some(code_page) = canonical.strip_prefix("windows-") else {
return true;
};
label.eq_ignore_ascii_case(canonical)
|| label
.get(2..)
.is_some_and(|suffix| label[..2].eq_ignore_ascii_case("cp") && suffix == code_page)
|| label
.get(4..)
.is_some_and(|suffix| label[..4].eq_ignore_ascii_case("x-cp") && suffix == code_page)
}
pub fn decode_xml<'a>(
bytes: &'a [u8],
explicit_encoding: Option<&str>,
) -> Result<Cow<'a, str>, Error> {
decode_xml_bounded(bytes, explicit_encoding, usize::MAX)
}
pub fn decode_xml_bounded<'a>(
bytes: &'a [u8],
explicit_encoding: Option<&str>,
maximum_decoded_bytes: usize,
) -> Result<Cow<'a, str>, Error> {
decode_xml_bounded_inner(
bytes,
explicit_encoding,
maximum_decoded_bytes,
true,
|_, _| Ok(()),
&NativeStorage,
)
.map(native_text)
}
pub fn decode_xml_bounded_with_encoding_guard<'a>(
bytes: &'a [u8],
maximum_decoded_bytes: usize,
guard: impl Fn(&str, Option<&str>) -> Result<(), Error>,
) -> Result<Cow<'a, str>, Error> {
decode_xml_bounded_inner(
bytes,
None,
maximum_decoded_bytes,
true,
guard,
&NativeStorage,
)
.map(native_text)
}
#[doc(hidden)]
pub fn decode_xml_in<'a, S: DecodeStorage>(
bytes: &'a [u8],
maximum_decoded_bytes: usize,
guard: impl Fn(&str, Option<&str>) -> Result<(), S::Error>,
storage: &S,
) -> Result<Decoded<'a, S::Text>, S::Error> {
decode_xml_bounded_inner(bytes, None, maximum_decoded_bytes, true, guard, storage)
}
pub fn decode_xml_text_bounded<'a>(
bytes: &'a [u8],
explicit_encoding: Option<&str>,
maximum_decoded_bytes: usize,
) -> Result<Cow<'a, str>, Error> {
decode_xml_bounded_inner(
bytes,
explicit_encoding,
maximum_decoded_bytes,
false,
|_, _| Ok(()),
&NativeStorage,
)
.map(native_text)
}
fn decode_xml_bounded_inner<'a, S: DecodeStorage>(
bytes: &'a [u8],
explicit_encoding: Option<&str>,
maximum_decoded_bytes: usize,
normalize_declaration: bool,
encoding_guard: impl Fn(&str, Option<&str>) -> Result<(), S::Error>,
storage: &S,
) -> Result<Decoded<'a, S::Text>, S::Error> {
storage.charge(bytes.len())?;
let physical = physical_encoding(bytes)?;
let ascii_declaration = if physical.is_none() {
declaration_from_ascii_bytes(bytes)?
} else {
None
};
let explicit_utf16 = explicit_encoding.is_some_and(is_generic_utf16);
let explicit_utf32 = explicit_encoding.is_some_and(is_generic_utf32);
let explicit = explicit_encoding
.filter(|_| !explicit_utf16 && !explicit_utf32)
.map(|label| parse_encoding_in(label, storage))
.transpose()?;
if explicit_utf16
&& !physical.is_some_and(|(encoding, bom_len)| is_utf16_encoding(encoding) && bom_len > 0)
{
return Err(Error::MissingUtf16ByteOrder.into());
}
if explicit_utf32 && !physical.is_some_and(|(encoding, _)| is_utf32_encoding(encoding)) {
return Err(Error::MissingUtf32ByteOrder.into());
}
let declared_before_decode = ascii_declaration
.as_ref()
.map(|(_, label)| parse_encoding_in(label, storage))
.transpose()?;
let selected = explicit
.or(physical.map(|(encoding, _)| encoding))
.or(declared_before_decode)
.unwrap_or(SelectedEncoding::Standard(encoding_rs::UTF_8));
if let Some((physical, _)) = physical
&& !encodings_compatible(selected, physical, true)
{
return Err(storage.label_error(LabelError::Conflicting, selected.name()));
}
if let Some(declared) = declared_before_decode
&& !encodings_compatible(selected, declared, false)
{
return Err(storage.label_error(LabelError::Conflicting, declared.name()));
}
let bom_len = physical.map_or(0, |(_, bom_len)| bom_len);
encoding_guard(
selected.name(),
ascii_declaration.as_ref().map(|(_, label)| *label),
)?;
let mut decoded =
decode_selected_in(&bytes[bom_len..], selected, maximum_decoded_bytes, storage)?;
storage.charge(decoded.as_ref().len())?;
let declaration = declaration_from_text(decoded.as_ref())?;
if physical.is_some() {
encoding_guard(
selected.name(),
declaration
.as_ref()
.map(|range| &decoded.as_ref()[range.clone()]),
)?;
}
if explicit_encoding.is_none()
&& declaration.is_none()
&& physical.is_some_and(|(encoding, bom_len)| {
is_utf32_encoding(encoding) || (bom_len == 0 && is_utf16_encoding(encoding))
})
{
return Err(
if physical.is_some_and(|(encoding, _)| is_utf32_encoding(encoding)) {
Error::MissingEncodingDeclaration("UTF-32")
} else {
Error::MissingUtf16ByteOrder
}
.into(),
);
}
if let Some(range) = &declaration {
let label = &decoded.as_ref()[range.clone()];
if is_generic_utf16(label) {
let has_utf16_bom = physical.is_some_and(|(encoding, bom_len)| {
bom_len > 0
&& matches!(encoding, SelectedEncoding::Standard(value)
if value == encoding_rs::UTF_16LE || value == encoding_rs::UTF_16BE)
});
if !has_utf16_bom {
return Err(Error::MissingUtf16ByteOrder.into());
}
} else if is_generic_utf32(label) {
if !physical.is_some_and(|(encoding, _)| is_utf32_encoding(encoding)) {
return Err(Error::MissingUtf32ByteOrder.into());
}
} else {
let declared = parse_encoding_in(label, storage)?;
if !encodings_compatible(selected, declared, false) {
return Err(storage.label_error(LabelError::Conflicting, label));
}
}
}
if normalize_declaration
&& !selected.is_utf8()
&& let Some(range) = declaration
{
let normalized_len = decoded
.as_ref()
.len()
.saturating_sub(range.len())
.saturating_add("UTF-8".len());
if normalized_len > maximum_decoded_bytes {
return Err(Error::DecodedLimit {
maximum: maximum_decoded_bytes,
actual: normalized_len,
}
.into());
}
decoded = Decoded::Owned(match decoded {
Decoded::Owned(text) => storage.replace(text, range, "UTF-8")?,
Decoded::Borrowed(text) => {
let mut buffer = storage.buffer();
storage.append(&mut buffer, &text[..range.start])?;
storage.append(&mut buffer, "UTF-8")?;
storage.append(&mut buffer, &text[range.end..])?;
storage.finish(buffer)?
}
});
}
Ok(decoded)
}
pub fn decode_text<'a>(bytes: &'a [u8], encoding: &str) -> Result<Cow<'a, str>, Error> {
decode_text_bounded(bytes, encoding, usize::MAX)
}
pub fn decode_text_bounded<'a>(
bytes: &'a [u8],
encoding: &str,
maximum_decoded_bytes: usize,
) -> Result<Cow<'a, str>, Error> {
if is_generic_utf16(encoding) {
if let Some(payload) = bytes.strip_prefix(&[0xFF, 0xFE]) {
return decode_selected(
payload,
SelectedEncoding::Standard(encoding_rs::UTF_16LE),
maximum_decoded_bytes,
);
}
if let Some(payload) = bytes.strip_prefix(&[0xFE, 0xFF]) {
return decode_selected(
payload,
SelectedEncoding::Standard(encoding_rs::UTF_16BE),
maximum_decoded_bytes,
);
}
return Err(Error::MissingUtf16ByteOrder);
}
let selected = parse_encoding(encoding)?;
let bytes = match selected {
SelectedEncoding::Standard(value) if value == encoding_rs::UTF_16LE => {
if bytes.starts_with(&[0xFE, 0xFF]) {
return Err(Error::ConflictingEncoding(selected.name().into()));
}
bytes
}
SelectedEncoding::Standard(value) if value == encoding_rs::UTF_16BE => {
if bytes.starts_with(&[0xFF, 0xFE]) {
return Err(Error::ConflictingEncoding(selected.name().into()));
}
bytes
}
SelectedEncoding::Standard(value) if value == encoding_rs::UTF_8 => {
bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes)
}
_ => bytes,
};
decode_selected(bytes, selected, maximum_decoded_bytes)
}
fn physical_encoding(bytes: &[u8]) -> Result<Option<(SelectedEncoding, usize)>, Error> {
let prefix = bytes.get(..4).unwrap_or(bytes);
match prefix {
[0x00, 0x00, 0xFE, 0xFF] => return Ok(Some((SelectedEncoding::Utf32Be, 4))),
[0xFF, 0xFE, 0x00, 0x00] => return Ok(Some((SelectedEncoding::Utf32Le, 4))),
[0x00, 0x00, 0x00, b'<'] => return Ok(Some((SelectedEncoding::Utf32Be, 0))),
[b'<', 0x00, 0x00, 0x00] => return Ok(Some((SelectedEncoding::Utf32Le, 0))),
[0x00, 0x00, 0xFF, 0xFE]
| [0xFE, 0xFF, 0x00, 0x00]
| [0x00, 0x00, b'<', 0x00]
| [0x00, b'<', 0x00, 0x00] => {
return Err(Error::UnsupportedByteEncoding("UTF-32 unusual byte order"));
}
_ => {}
}
if prefix == [0x4C, 0x6F, 0xA7, 0x94] {
return Err(Error::UnsupportedByteEncoding("EBCDIC"));
}
if let Some((encoding, length)) = encoding_rs::Encoding::for_bom(bytes) {
return Ok(Some((SelectedEncoding::Standard(encoding), length)));
}
Ok(match prefix {
[0x00, b'<', 0x00, b'?'] => Some((SelectedEncoding::Standard(encoding_rs::UTF_16BE), 0)),
[b'<', 0x00, b'?', 0x00] => Some((SelectedEncoding::Standard(encoding_rs::UTF_16LE), 0)),
_ => None,
})
}
fn parse_encoding(label: &str) -> Result<SelectedEncoding, Error> {
parse_encoding_in(label, &NativeStorage)
}
fn parse_encoding_in<S: DecodeStorage>(
label: &str,
storage: &S,
) -> Result<SelectedEncoding, S::Error> {
if matches_ascii_case(label, &["utf-32le", "utf32le"]) {
return Ok(SelectedEncoding::Utf32Le);
}
if matches_ascii_case(label, &["utf-32be", "utf32be"]) {
return Ok(SelectedEncoding::Utf32Be);
}
if matches_ascii_case(label, &["us-ascii", "ascii"]) {
return Ok(SelectedEncoding::Ascii);
}
if let Some(encoding) = registered_single_byte_encoding(label) {
return Ok(SelectedEncoding::Registered(encoding));
}
let encoding = encoding_rs::Encoding::for_label_no_replacement(label.as_bytes())
.ok_or_else(|| storage.label_error(LabelError::Unsupported, label))?;
if !legacy_label_matches_encoding(label, encoding) {
return Err(storage.label_error(LabelError::Unsupported, label));
}
Ok(SelectedEncoding::Standard(encoding))
}
#[must_use]
pub fn is_latin1_encoding_label(label: &str) -> bool {
matches_ascii_case(
label,
&[
"iso_8859-1:1987",
"iso-ir-100",
"iso_8859-1",
"iso-8859-1",
"latin1",
"latin-1",
"l1",
"ibm819",
"cp819",
"csisolatin1",
],
)
}
fn decode_selected<'a>(
bytes: &'a [u8],
encoding: SelectedEncoding,
maximum: usize,
) -> Result<Cow<'a, str>, Error> {
decode_selected_in(bytes, encoding, maximum, &NativeStorage).map(native_text)
}
fn decode_selected_in<'a, S: DecodeStorage>(
bytes: &'a [u8],
encoding: SelectedEncoding,
maximum: usize,
storage: &S,
) -> Result<Decoded<'a, S::Text>, S::Error> {
if matches!(
encoding,
SelectedEncoding::Utf32Le | SelectedEncoding::Utf32Be
) {
return decode_utf32(bytes, encoding, maximum, storage).map(Decoded::Owned);
}
if encoding == SelectedEncoding::Ascii {
if bytes.iter().any(|byte| !byte.is_ascii()) {
return Err(Error::InvalidBytes("US-ASCII").into());
}
let decoded =
core::str::from_utf8(bytes).expect("seven-bit US-ASCII is always valid UTF-8");
if decoded.len() > maximum {
return Err(Error::DecodedLimit {
maximum,
actual: decoded.len(),
}
.into());
}
return Ok(Decoded::Borrowed(decoded));
}
if matches!(encoding, SelectedEncoding::Registered(_)) {
return decode_registered_single_byte(bytes, encoding, maximum, storage)
.map(Decoded::Owned);
}
let SelectedEncoding::Standard(encoding) = encoding else {
unreachable!("special-case encodings returned above")
};
if encoding == encoding_rs::UTF_8 {
let decoded = core::str::from_utf8(bytes).map_err(|_| Error::InvalidBytes("UTF-8"))?;
if decoded.len() > maximum {
return Err(Error::DecodedLimit {
maximum,
actual: decoded.len(),
}
.into());
}
return Ok(Decoded::Borrowed(decoded));
}
if (encoding == encoding_rs::UTF_16LE || encoding == encoding_rs::UTF_16BE)
&& !bytes.len().is_multiple_of(2)
{
return Err(Error::InvalidUtf16Length(encoding.name()).into());
}
let mut decoder = encoding.new_decoder_without_bom_handling();
let mut remaining = bytes;
let mut decoded = storage.buffer();
let mut buffer = [0_u8; 4096];
loop {
let input_length = remaining.len().min(buffer.len());
let last = input_length == remaining.len();
storage.charge(input_length)?;
let (result, read, written) = decoder.decode_to_utf8_without_replacement(
&remaining[..input_length],
&mut buffer,
last,
);
let actual = storage.len(&decoded).saturating_add(written);
if actual > maximum {
return Err(Error::DecodedLimit { maximum, actual }.into());
}
storage.charge(written)?;
storage.append(
&mut decoded,
core::str::from_utf8(&buffer[..written])
.expect("encoding_rs emits valid UTF-8 into the output buffer"),
)?;
remaining = &remaining[read..];
match result {
encoding_rs::DecoderResult::InputEmpty if last => {
return storage.finish(decoded).map(Decoded::Owned);
}
encoding_rs::DecoderResult::InputEmpty => {}
encoding_rs::DecoderResult::OutputFull => {}
encoding_rs::DecoderResult::Malformed(_, _) => {
return Err(Error::InvalidBytes(encoding.name()).into());
}
}
}
}
fn decode_registered_single_byte<S: DecodeStorage>(
bytes: &[u8],
encoding: SelectedEncoding,
maximum: usize,
storage: &S,
) -> Result<S::Text, S::Error> {
storage.charge(bytes.len())?;
let mut decoded = storage.buffer();
for &byte in bytes {
let character = match encoding {
SelectedEncoding::Registered(encoding) => encoding
.decode_byte(byte)
.ok_or_else(|| Error::InvalidBytes(encoding.name()))?,
_ => unreachable!("registered single-byte decoder receives a matching encoding"),
};
let actual = storage.len(&decoded).saturating_add(character.len_utf8());
if actual > maximum {
return Err(Error::DecodedLimit { maximum, actual }.into());
}
storage.append(&mut decoded, character.encode_utf8(&mut [0; 4]))?;
}
storage.finish(decoded)
}
fn decode_utf32<S: DecodeStorage>(
bytes: &[u8],
encoding: SelectedEncoding,
maximum: usize,
storage: &S,
) -> Result<S::Text, S::Error> {
if !bytes.len().is_multiple_of(4) {
return Err(Error::InvalidUtf32Length(encoding.name()).into());
}
storage.charge(bytes.len())?;
let mut decoded = storage.buffer();
let (units, remainder) = bytes.as_chunks::<4>();
debug_assert!(remainder.is_empty());
for &unit in units {
let scalar = match encoding {
SelectedEncoding::Utf32Le => u32::from_le_bytes(unit),
SelectedEncoding::Utf32Be => u32::from_be_bytes(unit),
_ => unreachable!("UTF-32 decoder receives an explicit byte order"),
};
let character = char::from_u32(scalar).ok_or(Error::InvalidBytes(encoding.name()))?;
let actual = storage.len(&decoded).saturating_add(character.len_utf8());
if actual > maximum {
return Err(Error::DecodedLimit { maximum, actual }.into());
}
storage.append(&mut decoded, character.encode_utf8(&mut [0; 4]))?;
}
storage.finish(decoded)
}
fn encodings_compatible(
selected: SelectedEncoding,
candidate: SelectedEncoding,
physical: bool,
) -> bool {
selected == candidate
|| (physical
&& matches!(selected, SelectedEncoding::Standard(value) if value == encoding_rs::UTF_8)
&& matches!(candidate, SelectedEncoding::Standard(value) if value == encoding_rs::UTF_8))
}
fn is_utf16_encoding(encoding: SelectedEncoding) -> bool {
matches!(encoding, SelectedEncoding::Standard(value)
if value == encoding_rs::UTF_16LE || value == encoding_rs::UTF_16BE)
}
fn is_utf32_encoding(encoding: SelectedEncoding) -> bool {
matches!(
encoding,
SelectedEncoding::Utf32Le | SelectedEncoding::Utf32Be
)
}
const fn is_xml_s_byte(byte: &u8) -> bool {
matches!(*byte, b' ' | b'\t' | b'\r' | b'\n')
}
fn declaration_from_ascii_bytes(bytes: &[u8]) -> Result<Option<(Range<usize>, &str)>, Error> {
let bytes = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes);
if !bytes.starts_with(b"<?xml") || !bytes.get(5).is_some_and(is_xml_s_byte) {
return Ok(None);
}
let end = bytes
.windows(2)
.position(|window| window == b"?>")
.map(|index| index + 2)
.ok_or(Error::MalformedDeclaration("unterminated declaration"))?;
let prefix = core::str::from_utf8(&bytes[..end])
.map_err(|_| Error::MalformedDeclaration("declaration is not ASCII-compatible"))?;
declaration_from_text(prefix).map(|range| {
range.map(|range| {
let label = &prefix[range.clone()];
(range, label)
})
})
}
fn declaration_from_text(xml: &str) -> Result<Option<Range<usize>>, Error> {
let Some(rest) = xml.strip_prefix("<?xml") else {
return Ok(None);
};
if !rest.as_bytes().first().is_some_and(is_xml_s_byte) {
return Ok(None);
}
let end = rest
.find("?>")
.ok_or(Error::MalformedDeclaration("unterminated declaration"))?;
let declaration = &rest.as_bytes()[..end];
let mut cursor = 0;
let mut encoding_range = None;
while cursor < declaration.len() {
while declaration.get(cursor).is_some_and(is_xml_s_byte) {
cursor += 1;
}
if cursor == declaration.len() {
break;
}
let name_start = cursor;
while declaration.get(cursor).is_some_and(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':' | b'-' | b'.')
}) {
cursor += 1;
}
if cursor == name_start {
return Err(Error::MalformedDeclaration("invalid pseudo-attribute"));
}
let name = &declaration[name_start..cursor];
while declaration.get(cursor).is_some_and(is_xml_s_byte) {
cursor += 1;
}
if declaration.get(cursor) != Some(&b'=') {
return Err(Error::MalformedDeclaration("missing `=`"));
}
cursor += 1;
while declaration.get(cursor).is_some_and(is_xml_s_byte) {
cursor += 1;
}
let "e @ (b'\'' | b'"') = declaration
.get(cursor)
.ok_or(Error::MalformedDeclaration("missing quoted value"))?
else {
return Err(Error::MalformedDeclaration("value is not quoted"));
};
let value_start = cursor + 1;
let value_end = value_start
+ declaration[value_start..]
.iter()
.position(|byte| *byte == quote)
.ok_or(Error::MalformedDeclaration("unterminated value"))?;
if name == b"encoding" {
let value = &declaration[value_start..value_end];
if !is_xml_encoding_name_bytes(value) {
return Err(Error::MalformedDeclaration("invalid encoding name"));
}
encoding_range = Some((5 + value_start)..(5 + value_end));
}
cursor = value_end + 1;
if declaration
.get(cursor)
.is_some_and(|byte| !is_xml_s_byte(byte))
{
return Err(Error::MalformedDeclaration("missing whitespace"));
}
}
Ok(encoding_range)
}
#[must_use]
pub fn is_xml_encoding_name(value: &str) -> bool {
is_xml_encoding_name_bytes(value.as_bytes())
}
fn is_xml_encoding_name_bytes(value: &[u8]) -> bool {
value.first().is_some_and(u8::is_ascii_alphabetic)
&& value[1..]
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
}
fn is_generic_utf16(label: &str) -> bool {
label.eq_ignore_ascii_case("UTF-16") || label.eq_ignore_ascii_case("UTF16")
}
fn is_generic_utf32(label: &str) -> bool {
label.eq_ignore_ascii_case("UTF-32") || label.eq_ignore_ascii_case("UTF32")
}
fn matches_ascii_case(value: &str, candidates: &[&str]) -> bool {
candidates
.iter()
.any(|candidate| value.eq_ignore_ascii_case(candidate))
}
#[cfg(all(test, feature = "std"))]
#[allow(clippy::unwrap_used)]
mod tests {
use std::borrow::Cow;
use super::{
Error, declaration_from_ascii_bytes, declaration_from_text, decode_text, decode_xml,
decode_xml_bounded, decode_xml_bounded_with_encoding_guard,
};
#[test]
fn encoding_guard_precedes_conversion_and_preserves_borrowing() {
let rejected = decode_xml_bounded_with_encoding_guard(
b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root/>",
0,
|physical, _| Err(Error::ForbiddenEncoding(physical.into())),
);
assert!(matches!(rejected, Err(Error::ForbiddenEncoding(_))));
let source = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>";
let accepted =
decode_xml_bounded_with_encoding_guard(source, source.len(), |physical, declared| {
assert_eq!(physical, "UTF-8");
assert_eq!(declared, Some("UTF-8"));
Ok(())
})
.unwrap();
assert!(matches!(accepted, Cow::Borrowed(_)));
}
fn encode_utf32(source: &str, little_endian: bool) -> Vec<u8> {
source
.chars()
.flat_map(|character| {
let value = u32::from(character);
if little_endian {
value.to_le_bytes()
} else {
value.to_be_bytes()
}
})
.collect()
}
#[test]
fn utf8_without_a_rewritten_declaration_stays_borrowed() {
let bytes = b"<root>ok</root>";
assert!(matches!(decode_xml(bytes, None), Ok(Cow::Borrowed(_))));
}
#[test]
fn generic_utf16_uses_the_bom_byte_order() {
let source = "<?xml version=\"1.0\" encoding=\"UTF-16\"?><root>lambda</root>";
let mut bytes = vec![0xFE, 0xFF];
bytes.extend(source.encode_utf16().flat_map(u16::to_be_bytes));
let decoded = decode_xml(&bytes, Some("UTF-16")).expect("BOM selects UTF-16BE");
assert!(decoded.contains("encoding=\"UTF-8\""));
assert!(decoded.contains("<root>lambda</root>"));
}
#[test]
fn xml_declaration_rejects_values_outside_the_enc_name_grammar() {
let bytes = b"<?xml version=\"1.0\" encoding=\"iso_8859-1:1987\"?><root/>";
assert!(matches!(
decode_xml(bytes, None),
Err(Error::MalformedDeclaration("invalid encoding name"))
));
}
#[test]
fn encoding_selection_requires_a_complete_well_formed_declaration() {
let malformed = "<?xml version=\"1.0\" encoding=\"UTF-8\" trailing?><root/>";
assert!(matches!(
declaration_from_text(malformed),
Err(Error::MalformedDeclaration("missing `=`"))
));
assert!(matches!(
declaration_from_ascii_bytes(malformed.as_bytes()),
Err(Error::MalformedDeclaration("missing `=`"))
));
let mut utf16 = vec![0xFF, 0xFE];
utf16.extend(malformed.encode_utf16().flat_map(u16::to_le_bytes));
assert!(matches!(
decode_xml(&utf16, None),
Err(Error::MalformedDeclaration("missing `=`"))
));
}
#[test]
fn xml_declaration_rejects_non_xml_ascii_whitespace() {
for whitespace in *b"\x0b\x0c" {
let source = [
b"<?xml".as_slice(),
&[whitespace],
b"version=\"1.0\" encoding=\"ISO-8859-1\"?><root>caf\xe9</root>".as_slice(),
]
.concat();
assert!(matches!(
decode_xml(&source, None),
Err(Error::InvalidBytes("UTF-8"))
));
}
}
#[test]
fn xml_prefixed_processing_instruction_is_not_a_declaration() {
let bytes = b"<?xml-stylesheet title='caf\xE9'?><root/>";
assert_eq!(
decode_xml(bytes, Some("windows-1252")).expect("PI follows resolver encoding"),
"<?xml-stylesheet title='caf\u{e9}'?><root/>"
);
}
#[test]
fn utf32_requires_a_bom_metadata_or_encoding_declaration() {
let source = "<?xml version=\"1.0\" encoding=\"UTF-32\"?><root>lambda</root>";
for (little_endian, bom) in [
(false, [0x00, 0x00, 0xFE, 0xFF]),
(true, [0xFF, 0xFE, 0x00, 0x00]),
] {
let mut bytes = bom.to_vec();
bytes.extend(encode_utf32(source, little_endian));
let decoded = decode_xml(&bytes, None).expect("UTF-32 BOM selects byte order");
assert!(decoded.contains("encoding=\"UTF-8\""));
assert!(decoded.contains("<root>lambda</root>"));
let mut declarationless = bom.to_vec();
declarationless.extend(encode_utf32("<root>lambda</root>", little_endian));
assert!(
decode_xml(&declarationless, None).is_err(),
"a UTF-32 BOM identifies byte order but does not replace the encoding declaration"
);
}
for little_endian in [false, true] {
let declaration = encode_utf32(
"<?xml version=\"1.0\" encoding=\"UTF-32\"?><root>lambda</root>",
little_endian,
);
assert!(
decode_xml(&declaration, None)
.expect("the initial signature and declaration identify UTF-32")
.contains("<root>lambda</root>")
);
let declarationless = encode_utf32("<root>lambda</root>", little_endian);
assert!(matches!(
decode_xml(&declarationless, None),
Err(Error::MissingEncodingDeclaration("UTF-32"))
));
assert_eq!(
decode_xml(
&declarationless,
Some(if little_endian {
"UTF-32LE"
} else {
"UTF-32BE"
})
)
.expect("trusted external metadata supplies the encoding"),
"<root>lambda</root>"
);
}
}
#[test]
fn utf32_rejects_truncation_invalid_scalars_and_conflicting_metadata() {
let mut truncated = vec![0x00, 0x00, 0xFE, 0xFF];
truncated.extend([0x00, 0x00, 0x00]);
assert!(decode_xml(&truncated, None).is_err());
let mut surrogate = vec![0x00, 0x00, 0xFE, 0xFF];
surrogate.extend(0xD800_u32.to_be_bytes());
assert!(matches!(
decode_xml(&surrogate, None),
Err(Error::InvalidBytes("UTF-32BE"))
));
let mut little_endian = vec![0xFF, 0xFE, 0x00, 0x00];
little_endian.extend(encode_utf32("<root/>", true));
assert!(matches!(
decode_xml(&little_endian, Some("UTF-32BE")),
Err(Error::ConflictingEncoding(_))
));
}
#[test]
fn utf32_decoding_obeys_the_utf8_materialization_limit() {
let mut bytes = vec![0x00, 0x00, 0xFE, 0xFF];
bytes.extend(encode_utf32("<root>lambda</root>", false));
assert!(matches!(
decode_xml_bounded(&bytes, None, 8),
Err(Error::DecodedLimit { maximum: 8, .. })
));
}
#[test]
fn bomless_generic_utf16_is_rejected_as_ambiguous() {
let bytes = "<?xml version=\"1.0\" encoding=\"UTF-16\"?><root/>"
.encode_utf16()
.flat_map(u16::to_le_bytes)
.collect::<Vec<_>>();
assert!(matches!(
decode_xml(&bytes, Some("UTF-16")),
Err(Error::MissingUtf16ByteOrder)
));
}
#[test]
fn generic_utf16_metadata_does_not_replace_the_required_bom() {
let bytes = "<?xml version=\"1.0\"?><root/>"
.encode_utf16()
.flat_map(u16::to_le_bytes)
.collect::<Vec<_>>();
assert!(matches!(
decode_xml(&bytes, Some("UTF-16")),
Err(Error::MissingUtf16ByteOrder)
));
}
#[test]
fn generic_utf16_metadata_cannot_bypass_the_bom_via_a_specific_declaration() {
let bytes = "<?xml version=\"1.0\" encoding=\"UTF-16LE\"?><root/>"
.encode_utf16()
.flat_map(u16::to_le_bytes)
.collect::<Vec<_>>();
assert!(matches!(
decode_xml(&bytes, Some("UTF-16")),
Err(Error::MissingUtf16ByteOrder)
));
}
#[test]
fn explicit_utf16_byte_order_decodes_a_declarationless_resource() {
let bytes = "<root>lambda</root>"
.encode_utf16()
.flat_map(u16::to_le_bytes)
.collect::<Vec<_>>();
assert_eq!(
decode_xml(&bytes, Some("UTF-16LE")).unwrap(),
"<root>lambda</root>"
);
}
#[test]
fn endian_specific_utf16_accepts_a_matching_byte_order_mark() {
let mut metadata = vec![0xFF, 0xFE];
metadata.extend("<root/>".encode_utf16().flat_map(u16::to_le_bytes));
assert_eq!(decode_xml(&metadata, Some("UTF-16LE")).unwrap(), "<root/>");
assert_eq!(
decode_text(&metadata, "UTF-16LE").unwrap(),
"\u{feff}<root/>"
);
let mut big_endian_text = vec![0xFE, 0xFF];
big_endian_text.extend("body".encode_utf16().flat_map(u16::to_be_bytes));
assert_eq!(
decode_text(&big_endian_text, "UTF-16BE").unwrap(),
"\u{feff}body"
);
let mut declaration = vec![0xFE, 0xFF];
declaration.extend(
"<?xml version=\"1.0\" encoding=\"UTF-16BE\"?><root/>"
.encode_utf16()
.flat_map(u16::to_be_bytes),
);
assert_eq!(
decode_xml(&declaration, None).unwrap(),
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>"
);
}
#[test]
fn generic_utf16_text_requires_and_consumes_a_byte_order_mark() {
let text = "lambda";
let mut little = vec![0xFF, 0xFE];
little.extend(text.encode_utf16().flat_map(u16::to_le_bytes));
let mut big = vec![0xFE, 0xFF];
big.extend(text.encode_utf16().flat_map(u16::to_be_bytes));
assert_eq!(decode_text(&little, "UTF-16").unwrap(), text);
assert_eq!(decode_text(&big, "UTF-16").unwrap(), text);
assert!(matches!(
decode_text(&little[2..], "UTF-16"),
Err(Error::MissingUtf16ByteOrder)
));
}
#[test]
fn utf8_text_consumes_its_signature_without_removing_content_fe_ff() {
assert_eq!(decode_text(b"\xef\xbb\xbfbody", "UTF-8").unwrap(), "body");
assert_eq!(decode_text(b"\xef\xbb\xbfbody", "utf8").unwrap(), "body");
assert_eq!(
decode_text(b"body\xef\xbb\xbftail", "UTF-8").unwrap(),
"body\u{feff}tail"
);
assert_eq!(decode_text(b"body", "UTF-8").unwrap(), "body");
}
#[test]
fn trusted_metadata_cannot_conflict_with_a_bom() {
assert!(matches!(
decode_xml(&[0xFF, 0xFE, b'A', 0], Some("UTF-8")),
Err(Error::ConflictingEncoding(_))
));
}
#[test]
fn latin1_and_windows_1252_keep_distinct_c1_semantics() {
for alias in [
"ISO_8859-1:1987",
"iso-ir-100",
"ISO_8859-1",
"ISO-8859-1",
"latin1",
"l1",
"IBM819",
"CP819",
"csISOLatin1",
] {
assert_eq!(
decode_text(&[0x80], alias).unwrap(),
"\u{80}",
"IANA alias {alias} must retain ISO-8859-1 C1 semantics"
);
}
assert_eq!(decode_text(&[0x80], "windows-1252").unwrap(), "€");
}
#[test]
fn iana_single_byte_encodings_do_not_inherit_windows_extensions() {
assert_eq!(decode_text(&[0x80], "ISO-8859-9").unwrap(), "\u{80}");
assert_eq!(decode_text(&[0x80], "iso88599").unwrap(), "\u{80}");
assert_eq!(decode_text(&[0xD0, 0xFD], "ISO-8859-9").unwrap(), "Ğı");
assert_eq!(decode_text(&[0x80], "windows-1254").unwrap(), "€");
assert_eq!(decode_text(&[0xA0], "ISO-8859-11").unwrap(), "\u{A0}");
assert!(matches!(
decode_text(&[0x80], "TIS-620"),
Err(Error::InvalidBytes("TIS-620"))
));
assert!(matches!(
decode_text(&[0xA0], "TIS-620"),
Err(Error::InvalidBytes("TIS-620"))
));
assert_eq!(decode_text(&[0xA1, 0xFB], "TIS-620").unwrap(), "ก๛");
assert_eq!(decode_text(&[0x80], "windows-874").unwrap(), "€");
}
#[test]
fn registered_iana_labels_retain_their_declared_repertoires() {
assert_eq!(decode_text(&[0x80], "ISO-8859-2").unwrap(), "\u{80}");
assert_eq!(decode_text(&[0xA1], "ISO-8859-2").unwrap(), "Ą");
assert_eq!(decode_text(&[0x80], "windows-1250").unwrap(), "€");
let source = b"<?xml version=\"1.0\" encoding=\"ISO-8859-2\"?><root>\xA1</root>";
assert!(decode_xml(source, None).unwrap().contains("<root>Ą</root>"));
}
#[test]
fn encoding_lookup_selects_the_strict_iso_8859_2_codec() {
assert_eq!(
encoding_rs::Encoding::for_label(b"ISO-8859-2"),
Some(encoding_rs::ISO_8859_2)
);
assert_eq!(encoding_rs::ISO_8859_2.name(), "ISO-8859-2");
}
#[test]
fn decoder_only_labels_are_reported_as_unsupported() {
for label in ["replacement", "ISO-2022-KR"] {
assert!(matches!(
decode_text(b"", label),
Err(Error::UnsupportedEncoding(rejected)) if rejected == label
));
}
}
#[test]
fn us_ascii_rejects_non_ascii_bytes() {
assert!(matches!(
decode_text(&[0x80], "US-ASCII"),
Err(Error::InvalidBytes("US-ASCII"))
));
assert_eq!(
decode_text(b"plain ASCII", "US-ASCII").unwrap(),
"plain ASCII"
);
}
#[test]
fn transcoded_and_normalized_representations_are_both_bounded() {
let bytes = b"<?xml version=\"1.0\" encoding=\"GBK\"?><root/>";
let exact = decode_xml(bytes, None).expect("GBK declaration is supported");
assert!(matches!(
decode_xml_bounded(bytes, None, exact.len() - 1),
Err(Error::DecodedLimit { .. })
));
assert_eq!(decode_xml_bounded(bytes, None, exact.len()).unwrap(), exact);
}
#[test]
fn declaration_detection_is_not_limited_to_a_short_prefix() {
let whitespace = " ".repeat(2_048);
let source = format!("<?xml{whitespace}encoding=\"ISO-8859-1\"?><root>caf\u{e9}</root>");
let bytes = source
.chars()
.map(|character| u8::try_from(u32::from(character)).unwrap())
.collect::<Vec<_>>();
assert!(decode_xml(&bytes, None).unwrap().contains("café"));
}
#[test]
fn declaration_allows_whitespace_before_its_terminator() {
let source = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?><root>caf\xe9</root>";
assert!(decode_xml(source, None).unwrap().contains("café"));
}
#[test]
fn unsupported_xml_signatures_fail_explicitly() {
assert!(matches!(
decode_xml(&[0x4C, 0x6F, 0xA7, 0x94], None),
Err(Error::UnsupportedByteEncoding("EBCDIC"))
));
}
#[test]
fn truncated_utf16_reports_the_code_unit_boundary() {
assert!(matches!(
decode_xml(&[0xff, 0xfe, 0], None),
Err(Error::InvalidUtf16Length("UTF-16LE"))
));
}
}