1use alloc::{borrow::Cow, string::String};
7use core::ops::Range;
8
9#[path = "lexical.rs"]
10pub mod lexical;
11
12#[derive(Debug, thiserror::Error)]
14#[non_exhaustive]
15pub enum Error {
16 #[error("unsupported XML byte encoding `{0}`")]
18 UnsupportedByteEncoding(&'static str),
19 #[error("unsupported XML encoding `{0}`")]
21 UnsupportedEncoding(String),
22 #[error("XML format does not permit encoding `{0}`")]
24 ForbiddenEncoding(String),
25 #[error("XML byte encoding conflicts with declared or selected encoding `{0}`")]
27 ConflictingEncoding(String),
28 #[error("malformed XML encoding declaration: {0}")]
30 MalformedDeclaration(&'static str),
31 #[error("XML input contains invalid {0} bytes")]
33 InvalidBytes(&'static str),
34 #[error("BOM-less UTF-16 XML input requires an explicit UTF-16LE or UTF-16BE declaration")]
36 MissingUtf16ByteOrder,
37 #[error("UTF-32 XML input requires a UTF-32LE/UTF-32BE encoding or byte-order signature")]
39 MissingUtf32ByteOrder,
40 #[error("{0} XML input requires an encoding declaration or trusted external encoding metadata")]
42 MissingEncodingDeclaration(&'static str),
43 #[error("{0} XML input has an odd byte length")]
45 InvalidUtf16Length(&'static str),
46 #[error("{0} XML input byte length is not divisible by four")]
48 InvalidUtf32Length(&'static str),
49 #[error("decoded XML exceeds the maximum size of {maximum} bytes: at least {actual} bytes")]
51 DecodedLimit {
52 maximum: usize,
54 actual: usize,
56 },
57}
58
59#[derive(Clone, Copy, PartialEq, Eq)]
60enum SelectedEncoding {
61 Standard(&'static encoding_rs::Encoding),
62 Utf32Le,
63 Utf32Be,
64 Ascii,
65 Registered(IanaSingleByteEncoding),
66}
67
68impl SelectedEncoding {
69 fn name(self) -> &'static str {
70 match self {
71 Self::Standard(encoding) => encoding.name(),
72 Self::Utf32Le => "UTF-32LE",
73 Self::Utf32Be => "UTF-32BE",
74 Self::Ascii => "US-ASCII",
75 Self::Registered(encoding) => encoding.name(),
76 }
77 }
78
79 fn is_utf8(self) -> bool {
80 matches!(self, Self::Standard(encoding) if encoding == encoding_rs::UTF_8)
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum IanaSingleByteEncoding {
87 Latin1,
89 Latin5,
91 Iso8859_11,
93 Tis620,
95}
96
97impl IanaSingleByteEncoding {
98 #[must_use]
99 pub const fn name(self) -> &'static str {
101 match self {
102 Self::Latin1 => "ISO-8859-1",
103 Self::Latin5 => "ISO-8859-9",
104 Self::Iso8859_11 => "ISO-8859-11",
105 Self::Tis620 => "TIS-620",
106 }
107 }
108
109 #[must_use]
110 pub fn decode_byte(self, byte: u8) -> Option<char> {
112 match self {
113 Self::Latin1 => Some(char::from(byte)),
114 Self::Latin5 => Some(match byte {
115 0xD0 => '\u{011E}',
116 0xDD => '\u{0130}',
117 0xDE => '\u{015E}',
118 0xF0 => '\u{011F}',
119 0xFD => '\u{0131}',
120 0xFE => '\u{015F}',
121 _ => char::from(byte),
122 }),
123 Self::Iso8859_11 | Self::Tis620 => match byte {
124 0x00..=0x7F => Some(char::from(byte)),
125 0xA0 if self == Self::Iso8859_11 => Some('\u{00A0}'),
126 0xA1..=0xDA | 0xE0..=0xFB => char::from_u32(u32::from(byte) + 0x0D60),
127 0xDF => Some('\u{0E3F}'),
128 _ => None,
129 },
130 }
131 }
132
133 #[must_use]
134 pub fn encode_char(self, character: char) -> Option<u8> {
136 match self {
137 Self::Latin1 => u8::try_from(u32::from(character)).ok(),
138 Self::Latin5 => match character {
139 '\u{011E}' => Some(0xD0),
140 '\u{0130}' => Some(0xDD),
141 '\u{015E}' => Some(0xDE),
142 '\u{011F}' => Some(0xF0),
143 '\u{0131}' => Some(0xFD),
144 '\u{015F}' => Some(0xFE),
145 _ => u8::try_from(u32::from(character))
146 .ok()
147 .filter(|byte| !matches!(byte, 0xD0 | 0xDD | 0xDE | 0xF0 | 0xFD | 0xFE)),
148 },
149 Self::Iso8859_11 | Self::Tis620 => match u32::from(character) {
150 value @ 0x00..=0x7F => Some(value as u8),
151 0xA0 if self == Self::Iso8859_11 => Some(0xA0),
152 value @ 0x0E01..=0x0E3A | value @ 0x0E40..=0x0E5B => {
153 u8::try_from(value - 0x0D60).ok()
154 }
155 0x0E3F => Some(0xDF),
156 _ => None,
157 },
158 }
159 }
160}
161
162#[must_use]
164pub fn registered_single_byte_encoding(label: &str) -> Option<IanaSingleByteEncoding> {
165 if is_latin1_encoding_label(label) {
166 return Some(IanaSingleByteEncoding::Latin1);
167 }
168 if matches_ascii_case(
169 label,
170 &[
171 "iso-ir-148",
172 "iso88599",
173 "iso-8859-9",
174 "iso_8859-9",
175 "latin5",
176 "csisolatin5",
177 "iso_8859-9:1989",
178 ],
179 ) {
180 return Some(IanaSingleByteEncoding::Latin5);
181 }
182 if matches_ascii_case(label, &["iso8859-11", "iso-8859-11"]) {
183 return Some(IanaSingleByteEncoding::Iso8859_11);
184 }
185 label
186 .eq_ignore_ascii_case("tis-620")
187 .then_some(IanaSingleByteEncoding::Tis620)
188}
189
190#[must_use]
195pub fn legacy_label_matches_encoding(
196 label: &str,
197 encoding: &'static encoding_rs::Encoding,
198) -> bool {
199 let canonical = encoding.name();
200 let Some(code_page) = canonical.strip_prefix("windows-") else {
201 return true;
202 };
203 label.eq_ignore_ascii_case(canonical)
204 || label
205 .get(2..)
206 .is_some_and(|suffix| label[..2].eq_ignore_ascii_case("cp") && suffix == code_page)
207 || label
208 .get(4..)
209 .is_some_and(|suffix| label[..4].eq_ignore_ascii_case("x-cp") && suffix == code_page)
210}
211
212pub fn decode_xml<'a>(
219 bytes: &'a [u8],
220 explicit_encoding: Option<&str>,
221) -> Result<Cow<'a, str>, Error> {
222 decode_xml_bounded(bytes, explicit_encoding, usize::MAX)
223}
224
225pub fn decode_xml_bounded<'a>(
228 bytes: &'a [u8],
229 explicit_encoding: Option<&str>,
230 maximum_decoded_bytes: usize,
231) -> Result<Cow<'a, str>, Error> {
232 decode_xml_bounded_inner(
233 bytes,
234 explicit_encoding,
235 maximum_decoded_bytes,
236 true,
237 |_, _| Ok(()),
238 )
239}
240
241pub fn decode_xml_bounded_with_encoding_guard<'a>(
248 bytes: &'a [u8],
249 maximum_decoded_bytes: usize,
250 guard: impl Fn(&str, Option<&str>) -> Result<(), Error>,
251) -> Result<Cow<'a, str>, Error> {
252 decode_xml_bounded_inner(bytes, None, maximum_decoded_bytes, true, guard)
253}
254
255pub fn decode_xml_text_bounded<'a>(
260 bytes: &'a [u8],
261 explicit_encoding: Option<&str>,
262 maximum_decoded_bytes: usize,
263) -> Result<Cow<'a, str>, Error> {
264 decode_xml_bounded_inner(
265 bytes,
266 explicit_encoding,
267 maximum_decoded_bytes,
268 false,
269 |_, _| Ok(()),
270 )
271}
272
273fn decode_xml_bounded_inner<'a>(
274 bytes: &'a [u8],
275 explicit_encoding: Option<&str>,
276 maximum_decoded_bytes: usize,
277 normalize_declaration: bool,
278 encoding_guard: impl Fn(&str, Option<&str>) -> Result<(), Error>,
279) -> Result<Cow<'a, str>, Error> {
280 let physical = physical_encoding(bytes)?;
281 let ascii_declaration = if physical.is_none() {
282 declaration_from_ascii_bytes(bytes)?
283 } else {
284 None
285 };
286 let explicit_utf16 = explicit_encoding.is_some_and(is_generic_utf16);
287 let explicit_utf32 = explicit_encoding.is_some_and(is_generic_utf32);
288 let explicit = explicit_encoding
289 .filter(|_| !explicit_utf16 && !explicit_utf32)
290 .map(parse_encoding)
291 .transpose()?;
292 if explicit_utf16
293 && !physical.is_some_and(|(encoding, bom_len)| is_utf16_encoding(encoding) && bom_len > 0)
294 {
295 return Err(Error::MissingUtf16ByteOrder);
299 }
300 if explicit_utf32 && !physical.is_some_and(|(encoding, _)| is_utf32_encoding(encoding)) {
301 return Err(Error::MissingUtf32ByteOrder);
302 }
303 let declared_before_decode = ascii_declaration
304 .as_ref()
305 .map(|(_, label)| parse_encoding(label))
306 .transpose()?;
307 let selected = explicit
308 .or(physical.map(|(encoding, _)| encoding))
309 .or(declared_before_decode)
310 .unwrap_or(SelectedEncoding::Standard(encoding_rs::UTF_8));
311
312 if let Some((physical, _)) = physical
313 && !encodings_compatible(selected, physical, true)
314 {
315 return Err(Error::ConflictingEncoding(selected.name().into()));
316 }
317 if let Some(declared) = declared_before_decode
318 && !encodings_compatible(selected, declared, false)
319 {
320 return Err(Error::ConflictingEncoding(declared.name().into()));
321 }
322
323 let bom_len = physical.map_or(0, |(_, bom_len)| bom_len);
324 encoding_guard(
325 selected.name(),
326 ascii_declaration.as_ref().map(|(_, label)| *label),
327 )?;
328 let mut decoded = decode_selected(&bytes[bom_len..], selected, maximum_decoded_bytes)?;
329 let declaration = declaration_from_text(&decoded)?;
330 if physical.is_some() {
331 encoding_guard(
332 selected.name(),
333 declaration.as_ref().map(|range| &decoded[range.clone()]),
334 )?;
335 }
336 if explicit_encoding.is_none()
337 && declaration.is_none()
338 && physical.is_some_and(|(encoding, bom_len)| {
339 is_utf32_encoding(encoding) || (bom_len == 0 && is_utf16_encoding(encoding))
340 })
341 {
342 return Err(
346 if physical.is_some_and(|(encoding, _)| is_utf32_encoding(encoding)) {
347 Error::MissingEncodingDeclaration("UTF-32")
348 } else {
349 Error::MissingUtf16ByteOrder
350 },
351 );
352 }
353 if let Some(range) = &declaration {
354 let label = &decoded[range.clone()];
355 if is_generic_utf16(label) {
356 let has_utf16_bom = physical.is_some_and(|(encoding, bom_len)| {
357 bom_len > 0
358 && matches!(encoding, SelectedEncoding::Standard(value)
359 if value == encoding_rs::UTF_16LE || value == encoding_rs::UTF_16BE)
360 });
361 if !has_utf16_bom {
362 return Err(Error::MissingUtf16ByteOrder);
363 }
364 } else if is_generic_utf32(label) {
365 if !physical.is_some_and(|(encoding, _)| is_utf32_encoding(encoding)) {
366 return Err(Error::MissingUtf32ByteOrder);
367 }
368 } else {
369 let declared = parse_encoding(label)?;
370 if !encodings_compatible(selected, declared, false) {
371 return Err(Error::ConflictingEncoding(label.into()));
372 }
373 }
374 }
375
376 if normalize_declaration
377 && !selected.is_utf8()
378 && let Some(range) = declaration
379 {
380 let normalized_len = decoded
381 .len()
382 .saturating_sub(range.len())
383 .saturating_add("UTF-8".len());
384 if normalized_len > maximum_decoded_bytes {
385 return Err(Error::DecodedLimit {
386 maximum: maximum_decoded_bytes,
387 actual: normalized_len,
388 });
389 }
390 decoded.to_mut().replace_range(range, "UTF-8");
391 }
392 Ok(decoded)
393}
394
395pub fn decode_text<'a>(bytes: &'a [u8], encoding: &str) -> Result<Cow<'a, str>, Error> {
397 decode_text_bounded(bytes, encoding, usize::MAX)
398}
399
400pub fn decode_text_bounded<'a>(
402 bytes: &'a [u8],
403 encoding: &str,
404 maximum_decoded_bytes: usize,
405) -> Result<Cow<'a, str>, Error> {
406 if is_generic_utf16(encoding) {
407 if let Some(payload) = bytes.strip_prefix(&[0xFF, 0xFE]) {
411 return decode_selected(
412 payload,
413 SelectedEncoding::Standard(encoding_rs::UTF_16LE),
414 maximum_decoded_bytes,
415 );
416 }
417 if let Some(payload) = bytes.strip_prefix(&[0xFE, 0xFF]) {
418 return decode_selected(
419 payload,
420 SelectedEncoding::Standard(encoding_rs::UTF_16BE),
421 maximum_decoded_bytes,
422 );
423 }
424 return Err(Error::MissingUtf16ByteOrder);
425 }
426 let selected = parse_encoding(encoding)?;
427 let bytes = match selected {
428 SelectedEncoding::Standard(value) if value == encoding_rs::UTF_16LE => {
432 if bytes.starts_with(&[0xFE, 0xFF]) {
433 return Err(Error::ConflictingEncoding(selected.name().into()));
434 }
435 bytes
436 }
437 SelectedEncoding::Standard(value) if value == encoding_rs::UTF_16BE => {
438 if bytes.starts_with(&[0xFF, 0xFE]) {
439 return Err(Error::ConflictingEncoding(selected.name().into()));
440 }
441 bytes
442 }
443 SelectedEncoding::Standard(value) if value == encoding_rs::UTF_8 => {
446 bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes)
447 }
448 _ => bytes,
449 };
450 decode_selected(bytes, selected, maximum_decoded_bytes)
451}
452
453fn physical_encoding(bytes: &[u8]) -> Result<Option<(SelectedEncoding, usize)>, Error> {
454 let prefix = bytes.get(..4).unwrap_or(bytes);
455 match prefix {
459 [0x00, 0x00, 0xFE, 0xFF] => return Ok(Some((SelectedEncoding::Utf32Be, 4))),
460 [0xFF, 0xFE, 0x00, 0x00] => return Ok(Some((SelectedEncoding::Utf32Le, 4))),
461 [0x00, 0x00, 0x00, b'<'] => return Ok(Some((SelectedEncoding::Utf32Be, 0))),
462 [b'<', 0x00, 0x00, 0x00] => return Ok(Some((SelectedEncoding::Utf32Le, 0))),
463 [0x00, 0x00, 0xFF, 0xFE]
464 | [0xFE, 0xFF, 0x00, 0x00]
465 | [0x00, 0x00, b'<', 0x00]
466 | [0x00, b'<', 0x00, 0x00] => {
467 return Err(Error::UnsupportedByteEncoding("UTF-32 unusual byte order"));
468 }
469 _ => {}
470 }
471 if prefix == [0x4C, 0x6F, 0xA7, 0x94] {
472 return Err(Error::UnsupportedByteEncoding("EBCDIC"));
473 }
474 if let Some((encoding, length)) = encoding_rs::Encoding::for_bom(bytes) {
475 return Ok(Some((SelectedEncoding::Standard(encoding), length)));
476 }
477 Ok(match prefix {
478 [0x00, b'<', 0x00, b'?'] => Some((SelectedEncoding::Standard(encoding_rs::UTF_16BE), 0)),
479 [b'<', 0x00, b'?', 0x00] => Some((SelectedEncoding::Standard(encoding_rs::UTF_16LE), 0)),
480 _ => None,
481 })
482}
483
484fn parse_encoding(label: &str) -> Result<SelectedEncoding, Error> {
485 if matches_ascii_case(label, &["utf-32le", "utf32le"]) {
486 return Ok(SelectedEncoding::Utf32Le);
487 }
488 if matches_ascii_case(label, &["utf-32be", "utf32be"]) {
489 return Ok(SelectedEncoding::Utf32Be);
490 }
491 if matches_ascii_case(label, &["us-ascii", "ascii"]) {
492 return Ok(SelectedEncoding::Ascii);
493 }
494 if let Some(encoding) = registered_single_byte_encoding(label) {
499 return Ok(SelectedEncoding::Registered(encoding));
500 }
501 let encoding = encoding_rs::Encoding::for_label_no_replacement(label.as_bytes())
510 .ok_or_else(|| Error::UnsupportedEncoding(label.into()))?;
511 if !legacy_label_matches_encoding(label, encoding) {
512 return Err(Error::UnsupportedEncoding(label.into()));
513 }
514 Ok(SelectedEncoding::Standard(encoding))
515}
516
517#[must_use]
522pub fn is_latin1_encoding_label(label: &str) -> bool {
523 matches_ascii_case(
524 label,
525 &[
526 "iso_8859-1:1987",
527 "iso-ir-100",
528 "iso_8859-1",
529 "iso-8859-1",
530 "latin1",
531 "latin-1",
532 "l1",
533 "ibm819",
534 "cp819",
535 "csisolatin1",
536 ],
537 )
538}
539
540fn decode_selected<'a>(
541 bytes: &'a [u8],
542 encoding: SelectedEncoding,
543 maximum: usize,
544) -> Result<Cow<'a, str>, Error> {
545 if matches!(
546 encoding,
547 SelectedEncoding::Utf32Le | SelectedEncoding::Utf32Be
548 ) {
549 return decode_utf32(bytes, encoding, maximum).map(Cow::Owned);
550 }
551 if encoding == SelectedEncoding::Ascii {
552 if bytes.iter().any(|byte| !byte.is_ascii()) {
553 return Err(Error::InvalidBytes("US-ASCII"));
554 }
555 let decoded =
556 core::str::from_utf8(bytes).expect("seven-bit US-ASCII is always valid UTF-8");
557 if decoded.len() > maximum {
558 return Err(Error::DecodedLimit {
559 maximum,
560 actual: decoded.len(),
561 });
562 }
563 return Ok(Cow::Borrowed(decoded));
564 }
565 if matches!(encoding, SelectedEncoding::Registered(_)) {
566 return decode_registered_single_byte(bytes, encoding, maximum).map(Cow::Owned);
567 }
568 let SelectedEncoding::Standard(encoding) = encoding else {
569 unreachable!("special-case encodings returned above")
570 };
571 if encoding == encoding_rs::UTF_8 {
572 let decoded = core::str::from_utf8(bytes).map_err(|_| Error::InvalidBytes("UTF-8"))?;
573 if decoded.len() > maximum {
574 return Err(Error::DecodedLimit {
575 maximum,
576 actual: decoded.len(),
577 });
578 }
579 return Ok(Cow::Borrowed(decoded));
580 }
581 if (encoding == encoding_rs::UTF_16LE || encoding == encoding_rs::UTF_16BE)
582 && !bytes.len().is_multiple_of(2)
583 {
584 return Err(Error::InvalidUtf16Length(encoding.name()));
585 }
586
587 let mut decoder = encoding.new_decoder_without_bom_handling();
588 let mut remaining = bytes;
589 let mut decoded = String::with_capacity(bytes.len().min(maximum));
590 let mut buffer = [0_u8; 4096];
591 loop {
592 let (result, read, written) =
593 decoder.decode_to_utf8_without_replacement(remaining, &mut buffer, true);
594 let actual = decoded.len().saturating_add(written);
595 if actual > maximum {
596 return Err(Error::DecodedLimit { maximum, actual });
597 }
598 decoded.push_str(
599 core::str::from_utf8(&buffer[..written])
600 .expect("encoding_rs emits valid UTF-8 into the output buffer"),
601 );
602 remaining = &remaining[read..];
603 match result {
604 encoding_rs::DecoderResult::InputEmpty => return Ok(Cow::Owned(decoded)),
605 encoding_rs::DecoderResult::OutputFull => {}
606 encoding_rs::DecoderResult::Malformed(_, _) => {
607 return Err(Error::InvalidBytes(encoding.name()));
608 }
609 }
610 }
611}
612
613fn decode_registered_single_byte(
614 bytes: &[u8],
615 encoding: SelectedEncoding,
616 maximum: usize,
617) -> Result<String, Error> {
618 let mut decoded = String::with_capacity(bytes.len().min(maximum));
619 for &byte in bytes {
620 let character = match encoding {
621 SelectedEncoding::Registered(encoding) => encoding
622 .decode_byte(byte)
623 .ok_or_else(|| Error::InvalidBytes(encoding.name()))?,
624 _ => unreachable!("registered single-byte decoder receives a matching encoding"),
625 };
626 let actual = decoded.len().saturating_add(character.len_utf8());
627 if actual > maximum {
628 return Err(Error::DecodedLimit { maximum, actual });
629 }
630 decoded.push(character);
631 }
632 Ok(decoded)
633}
634
635fn decode_utf32(bytes: &[u8], encoding: SelectedEncoding, maximum: usize) -> Result<String, Error> {
636 if !bytes.len().is_multiple_of(4) {
637 return Err(Error::InvalidUtf32Length(encoding.name()));
638 }
639 let mut decoded = String::with_capacity(bytes.len().min(maximum));
640 let (units, remainder) = bytes.as_chunks::<4>();
641 debug_assert!(remainder.is_empty());
642 for &unit in units {
643 let scalar = match encoding {
644 SelectedEncoding::Utf32Le => u32::from_le_bytes(unit),
645 SelectedEncoding::Utf32Be => u32::from_be_bytes(unit),
646 _ => unreachable!("UTF-32 decoder receives an explicit byte order"),
647 };
648 let character = char::from_u32(scalar).ok_or(Error::InvalidBytes(encoding.name()))?;
649 let actual = decoded.len().saturating_add(character.len_utf8());
650 if actual > maximum {
651 return Err(Error::DecodedLimit { maximum, actual });
652 }
653 decoded.push(character);
654 }
655 Ok(decoded)
656}
657
658fn encodings_compatible(
659 selected: SelectedEncoding,
660 candidate: SelectedEncoding,
661 physical: bool,
662) -> bool {
663 selected == candidate
664 || (physical
665 && matches!(selected, SelectedEncoding::Standard(value) if value == encoding_rs::UTF_8)
666 && matches!(candidate, SelectedEncoding::Standard(value) if value == encoding_rs::UTF_8))
667}
668
669fn is_utf16_encoding(encoding: SelectedEncoding) -> bool {
670 matches!(encoding, SelectedEncoding::Standard(value)
671 if value == encoding_rs::UTF_16LE || value == encoding_rs::UTF_16BE)
672}
673
674fn is_utf32_encoding(encoding: SelectedEncoding) -> bool {
675 matches!(
676 encoding,
677 SelectedEncoding::Utf32Le | SelectedEncoding::Utf32Be
678 )
679}
680
681const fn is_xml_s_byte(byte: &u8) -> bool {
684 matches!(*byte, b' ' | b'\t' | b'\r' | b'\n')
685}
686
687fn declaration_from_ascii_bytes(bytes: &[u8]) -> Result<Option<(Range<usize>, &str)>, Error> {
688 let bytes = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes);
689 if !bytes.starts_with(b"<?xml") || !bytes.get(5).is_some_and(is_xml_s_byte) {
690 return Ok(None);
691 }
692 let end = bytes
693 .windows(2)
694 .position(|window| window == b"?>")
695 .map(|index| index + 2)
696 .ok_or(Error::MalformedDeclaration("unterminated declaration"))?;
697 let prefix = core::str::from_utf8(&bytes[..end])
700 .map_err(|_| Error::MalformedDeclaration("declaration is not ASCII-compatible"))?;
701 declaration_from_text(prefix).map(|range| {
702 range.map(|range| {
703 let label = &prefix[range.clone()];
704 (range, label)
705 })
706 })
707}
708
709fn declaration_from_text(xml: &str) -> Result<Option<Range<usize>>, Error> {
710 let Some(rest) = xml.strip_prefix("<?xml") else {
711 return Ok(None);
712 };
713 if !rest.as_bytes().first().is_some_and(is_xml_s_byte) {
714 return Ok(None);
715 }
716 let end = rest
717 .find("?>")
718 .ok_or(Error::MalformedDeclaration("unterminated declaration"))?;
719 let declaration = &rest.as_bytes()[..end];
720 let mut cursor = 0;
721 let mut encoding_range = None;
722 while cursor < declaration.len() {
723 while declaration.get(cursor).is_some_and(is_xml_s_byte) {
724 cursor += 1;
725 }
726 if cursor == declaration.len() {
727 break;
728 }
729 let name_start = cursor;
730 while declaration.get(cursor).is_some_and(|byte| {
731 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':' | b'-' | b'.')
732 }) {
733 cursor += 1;
734 }
735 if cursor == name_start {
736 return Err(Error::MalformedDeclaration("invalid pseudo-attribute"));
737 }
738 let name = &declaration[name_start..cursor];
739 while declaration.get(cursor).is_some_and(is_xml_s_byte) {
740 cursor += 1;
741 }
742 if declaration.get(cursor) != Some(&b'=') {
743 return Err(Error::MalformedDeclaration("missing `=`"));
744 }
745 cursor += 1;
746 while declaration.get(cursor).is_some_and(is_xml_s_byte) {
747 cursor += 1;
748 }
749 let "e @ (b'\'' | b'"') = declaration
750 .get(cursor)
751 .ok_or(Error::MalformedDeclaration("missing quoted value"))?
752 else {
753 return Err(Error::MalformedDeclaration("value is not quoted"));
754 };
755 let value_start = cursor + 1;
756 let value_end = value_start
757 + declaration[value_start..]
758 .iter()
759 .position(|byte| *byte == quote)
760 .ok_or(Error::MalformedDeclaration("unterminated value"))?;
761 if name == b"encoding" {
762 let value = &declaration[value_start..value_end];
763 if !is_xml_encoding_name_bytes(value) {
767 return Err(Error::MalformedDeclaration("invalid encoding name"));
768 }
769 encoding_range = Some((5 + value_start)..(5 + value_end));
770 }
771 cursor = value_end + 1;
772 if declaration
773 .get(cursor)
774 .is_some_and(|byte| !is_xml_s_byte(byte))
775 {
776 return Err(Error::MalformedDeclaration("missing whitespace"));
777 }
778 }
779 Ok(encoding_range)
783}
784
785#[must_use]
790pub fn is_xml_encoding_name(value: &str) -> bool {
791 is_xml_encoding_name_bytes(value.as_bytes())
792}
793
794fn is_xml_encoding_name_bytes(value: &[u8]) -> bool {
795 value.first().is_some_and(u8::is_ascii_alphabetic)
796 && value[1..]
797 .iter()
798 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
799}
800
801fn is_generic_utf16(label: &str) -> bool {
802 label.eq_ignore_ascii_case("UTF-16") || label.eq_ignore_ascii_case("UTF16")
803}
804
805fn is_generic_utf32(label: &str) -> bool {
806 label.eq_ignore_ascii_case("UTF-32") || label.eq_ignore_ascii_case("UTF32")
807}
808
809fn matches_ascii_case(value: &str, candidates: &[&str]) -> bool {
810 candidates
811 .iter()
812 .any(|candidate| value.eq_ignore_ascii_case(candidate))
813}
814
815#[cfg(all(test, feature = "std"))]
816#[allow(clippy::unwrap_used)]
817mod tests {
818 use std::borrow::Cow;
819
820 use super::{
821 Error, declaration_from_ascii_bytes, declaration_from_text, decode_text, decode_xml,
822 decode_xml_bounded, decode_xml_bounded_with_encoding_guard,
823 };
824
825 #[test]
826 fn encoding_guard_precedes_conversion_and_preserves_borrowing() {
827 let rejected = decode_xml_bounded_with_encoding_guard(
829 b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root/>",
830 0,
831 |physical, _| Err(Error::ForbiddenEncoding(physical.into())),
832 );
833 assert!(matches!(rejected, Err(Error::ForbiddenEncoding(_))));
834 let source = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>";
835 let accepted =
836 decode_xml_bounded_with_encoding_guard(source, source.len(), |physical, declared| {
837 assert_eq!(physical, "UTF-8");
838 assert_eq!(declared, Some("UTF-8"));
839 Ok(())
840 })
841 .unwrap();
842 assert!(matches!(accepted, Cow::Borrowed(_)));
843 }
844
845 fn encode_utf32(source: &str, little_endian: bool) -> Vec<u8> {
846 source
847 .chars()
848 .flat_map(|character| {
849 let value = u32::from(character);
850 if little_endian {
851 value.to_le_bytes()
852 } else {
853 value.to_be_bytes()
854 }
855 })
856 .collect()
857 }
858
859 #[test]
860 fn utf8_without_a_rewritten_declaration_stays_borrowed() {
861 let bytes = b"<root>ok</root>";
862 assert!(matches!(decode_xml(bytes, None), Ok(Cow::Borrowed(_))));
863 }
864
865 #[test]
866 fn generic_utf16_uses_the_bom_byte_order() {
867 let source = "<?xml version=\"1.0\" encoding=\"UTF-16\"?><root>lambda</root>";
868 let mut bytes = vec![0xFE, 0xFF];
869 bytes.extend(source.encode_utf16().flat_map(u16::to_be_bytes));
870 let decoded = decode_xml(&bytes, Some("UTF-16")).expect("BOM selects UTF-16BE");
871 assert!(decoded.contains("encoding=\"UTF-8\""));
872 assert!(decoded.contains("<root>lambda</root>"));
873 }
874
875 #[test]
876 fn xml_declaration_rejects_values_outside_the_enc_name_grammar() {
877 let bytes = b"<?xml version=\"1.0\" encoding=\"iso_8859-1:1987\"?><root/>";
880 assert!(matches!(
881 decode_xml(bytes, None),
882 Err(Error::MalformedDeclaration("invalid encoding name"))
883 ));
884 }
885
886 #[test]
887 fn encoding_selection_requires_a_complete_well_formed_declaration() {
888 let malformed = "<?xml version=\"1.0\" encoding=\"UTF-8\" trailing?><root/>";
892 assert!(matches!(
893 declaration_from_text(malformed),
894 Err(Error::MalformedDeclaration("missing `=`"))
895 ));
896 assert!(matches!(
897 declaration_from_ascii_bytes(malformed.as_bytes()),
898 Err(Error::MalformedDeclaration("missing `=`"))
899 ));
900
901 let mut utf16 = vec![0xFF, 0xFE];
902 utf16.extend(malformed.encode_utf16().flat_map(u16::to_le_bytes));
903 assert!(matches!(
904 decode_xml(&utf16, None),
905 Err(Error::MalformedDeclaration("missing `=`"))
906 ));
907 }
908
909 #[test]
910 fn xml_declaration_rejects_non_xml_ascii_whitespace() {
911 for whitespace in *b"\x0b\x0c" {
914 let source = [
915 b"<?xml".as_slice(),
916 &[whitespace],
917 b"version=\"1.0\" encoding=\"ISO-8859-1\"?><root>caf\xe9</root>".as_slice(),
918 ]
919 .concat();
920 assert!(matches!(
921 decode_xml(&source, None),
922 Err(Error::InvalidBytes("UTF-8"))
923 ));
924 }
925 }
926
927 #[test]
928 fn xml_prefixed_processing_instruction_is_not_a_declaration() {
929 let bytes = b"<?xml-stylesheet title='caf\xE9'?><root/>";
932 assert_eq!(
933 decode_xml(bytes, Some("windows-1252")).expect("PI follows resolver encoding"),
934 "<?xml-stylesheet title='caf\u{e9}'?><root/>"
935 );
936 }
937
938 #[test]
939 fn utf32_requires_a_bom_metadata_or_encoding_declaration() {
940 let source = "<?xml version=\"1.0\" encoding=\"UTF-32\"?><root>lambda</root>";
943 for (little_endian, bom) in [
944 (false, [0x00, 0x00, 0xFE, 0xFF]),
945 (true, [0xFF, 0xFE, 0x00, 0x00]),
946 ] {
947 let mut bytes = bom.to_vec();
948 bytes.extend(encode_utf32(source, little_endian));
949 let decoded = decode_xml(&bytes, None).expect("UTF-32 BOM selects byte order");
950 assert!(decoded.contains("encoding=\"UTF-8\""));
951 assert!(decoded.contains("<root>lambda</root>"));
952
953 let mut declarationless = bom.to_vec();
954 declarationless.extend(encode_utf32("<root>lambda</root>", little_endian));
955 assert!(
956 decode_xml(&declarationless, None).is_err(),
957 "a UTF-32 BOM identifies byte order but does not replace the encoding declaration"
958 );
959 }
960
961 for little_endian in [false, true] {
962 let declaration = encode_utf32(
963 "<?xml version=\"1.0\" encoding=\"UTF-32\"?><root>lambda</root>",
964 little_endian,
965 );
966 assert!(
967 decode_xml(&declaration, None)
968 .expect("the initial signature and declaration identify UTF-32")
969 .contains("<root>lambda</root>")
970 );
971 let declarationless = encode_utf32("<root>lambda</root>", little_endian);
972 assert!(matches!(
973 decode_xml(&declarationless, None),
974 Err(Error::MissingEncodingDeclaration("UTF-32"))
975 ));
976 assert_eq!(
977 decode_xml(
978 &declarationless,
979 Some(if little_endian {
980 "UTF-32LE"
981 } else {
982 "UTF-32BE"
983 })
984 )
985 .expect("trusted external metadata supplies the encoding"),
986 "<root>lambda</root>"
987 );
988 }
989 }
990
991 #[test]
992 fn utf32_rejects_truncation_invalid_scalars_and_conflicting_metadata() {
993 let mut truncated = vec![0x00, 0x00, 0xFE, 0xFF];
994 truncated.extend([0x00, 0x00, 0x00]);
995 assert!(decode_xml(&truncated, None).is_err());
996
997 let mut surrogate = vec![0x00, 0x00, 0xFE, 0xFF];
998 surrogate.extend(0xD800_u32.to_be_bytes());
999 assert!(matches!(
1000 decode_xml(&surrogate, None),
1001 Err(Error::InvalidBytes("UTF-32BE"))
1002 ));
1003
1004 let mut little_endian = vec![0xFF, 0xFE, 0x00, 0x00];
1005 little_endian.extend(encode_utf32("<root/>", true));
1006 assert!(matches!(
1007 decode_xml(&little_endian, Some("UTF-32BE")),
1008 Err(Error::ConflictingEncoding(_))
1009 ));
1010 }
1011
1012 #[test]
1013 fn utf32_decoding_obeys_the_utf8_materialization_limit() {
1014 let mut bytes = vec![0x00, 0x00, 0xFE, 0xFF];
1015 bytes.extend(encode_utf32("<root>lambda</root>", false));
1016 assert!(matches!(
1017 decode_xml_bounded(&bytes, None, 8),
1018 Err(Error::DecodedLimit { maximum: 8, .. })
1019 ));
1020 }
1021
1022 #[test]
1023 fn bomless_generic_utf16_is_rejected_as_ambiguous() {
1024 let bytes = "<?xml version=\"1.0\" encoding=\"UTF-16\"?><root/>"
1025 .encode_utf16()
1026 .flat_map(u16::to_le_bytes)
1027 .collect::<Vec<_>>();
1028 assert!(matches!(
1029 decode_xml(&bytes, Some("UTF-16")),
1030 Err(Error::MissingUtf16ByteOrder)
1031 ));
1032 }
1033
1034 #[test]
1035 fn generic_utf16_metadata_does_not_replace_the_required_bom() {
1036 let bytes = "<?xml version=\"1.0\"?><root/>"
1040 .encode_utf16()
1041 .flat_map(u16::to_le_bytes)
1042 .collect::<Vec<_>>();
1043 assert!(matches!(
1044 decode_xml(&bytes, Some("UTF-16")),
1045 Err(Error::MissingUtf16ByteOrder)
1046 ));
1047 }
1048
1049 #[test]
1050 fn generic_utf16_metadata_cannot_bypass_the_bom_via_a_specific_declaration() {
1051 let bytes = "<?xml version=\"1.0\" encoding=\"UTF-16LE\"?><root/>"
1054 .encode_utf16()
1055 .flat_map(u16::to_le_bytes)
1056 .collect::<Vec<_>>();
1057 assert!(matches!(
1058 decode_xml(&bytes, Some("UTF-16")),
1059 Err(Error::MissingUtf16ByteOrder)
1060 ));
1061 }
1062
1063 #[test]
1064 fn explicit_utf16_byte_order_decodes_a_declarationless_resource() {
1065 let bytes = "<root>lambda</root>"
1066 .encode_utf16()
1067 .flat_map(u16::to_le_bytes)
1068 .collect::<Vec<_>>();
1069 assert_eq!(
1070 decode_xml(&bytes, Some("UTF-16LE")).unwrap(),
1071 "<root>lambda</root>"
1072 );
1073 }
1074
1075 #[test]
1076 fn endian_specific_utf16_accepts_a_matching_byte_order_mark() {
1077 let mut metadata = vec![0xFF, 0xFE];
1080 metadata.extend("<root/>".encode_utf16().flat_map(u16::to_le_bytes));
1081 assert_eq!(decode_xml(&metadata, Some("UTF-16LE")).unwrap(), "<root/>");
1082 assert_eq!(
1083 decode_text(&metadata, "UTF-16LE").unwrap(),
1084 "\u{feff}<root/>"
1085 );
1086
1087 let mut big_endian_text = vec![0xFE, 0xFF];
1088 big_endian_text.extend("body".encode_utf16().flat_map(u16::to_be_bytes));
1089 assert_eq!(
1090 decode_text(&big_endian_text, "UTF-16BE").unwrap(),
1091 "\u{feff}body"
1092 );
1093
1094 let mut declaration = vec![0xFE, 0xFF];
1095 declaration.extend(
1096 "<?xml version=\"1.0\" encoding=\"UTF-16BE\"?><root/>"
1097 .encode_utf16()
1098 .flat_map(u16::to_be_bytes),
1099 );
1100 assert_eq!(
1101 decode_xml(&declaration, None).unwrap(),
1102 "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>"
1103 );
1104 }
1105
1106 #[test]
1107 fn generic_utf16_text_requires_and_consumes_a_byte_order_mark() {
1108 let text = "lambda";
1109 let mut little = vec![0xFF, 0xFE];
1110 little.extend(text.encode_utf16().flat_map(u16::to_le_bytes));
1111 let mut big = vec![0xFE, 0xFF];
1112 big.extend(text.encode_utf16().flat_map(u16::to_be_bytes));
1113
1114 assert_eq!(decode_text(&little, "UTF-16").unwrap(), text);
1115 assert_eq!(decode_text(&big, "UTF-16").unwrap(), text);
1116 assert!(matches!(
1117 decode_text(&little[2..], "UTF-16"),
1118 Err(Error::MissingUtf16ByteOrder)
1119 ));
1120 }
1121
1122 #[test]
1123 fn utf8_text_consumes_its_signature_without_removing_content_fe_ff() {
1124 assert_eq!(decode_text(b"\xef\xbb\xbfbody", "UTF-8").unwrap(), "body");
1127 assert_eq!(decode_text(b"\xef\xbb\xbfbody", "utf8").unwrap(), "body");
1128 assert_eq!(
1129 decode_text(b"body\xef\xbb\xbftail", "UTF-8").unwrap(),
1130 "body\u{feff}tail"
1131 );
1132 assert_eq!(decode_text(b"body", "UTF-8").unwrap(), "body");
1133 }
1134
1135 #[test]
1136 fn trusted_metadata_cannot_conflict_with_a_bom() {
1137 assert!(matches!(
1138 decode_xml(&[0xFF, 0xFE, b'A', 0], Some("UTF-8")),
1139 Err(Error::ConflictingEncoding(_))
1140 ));
1141 }
1142
1143 #[test]
1144 fn latin1_and_windows_1252_keep_distinct_c1_semantics() {
1145 for alias in [
1148 "ISO_8859-1:1987",
1149 "iso-ir-100",
1150 "ISO_8859-1",
1151 "ISO-8859-1",
1152 "latin1",
1153 "l1",
1154 "IBM819",
1155 "CP819",
1156 "csISOLatin1",
1157 ] {
1158 assert_eq!(
1159 decode_text(&[0x80], alias).unwrap(),
1160 "\u{80}",
1161 "IANA alias {alias} must retain ISO-8859-1 C1 semantics"
1162 );
1163 }
1164 assert_eq!(decode_text(&[0x80], "windows-1252").unwrap(), "€");
1165 }
1166
1167 #[test]
1168 fn iana_single_byte_encodings_do_not_inherit_windows_extensions() {
1169 assert_eq!(decode_text(&[0x80], "ISO-8859-9").unwrap(), "\u{80}");
1172 assert_eq!(decode_text(&[0x80], "iso88599").unwrap(), "\u{80}");
1173 assert_eq!(decode_text(&[0xD0, 0xFD], "ISO-8859-9").unwrap(), "Ğı");
1174 assert_eq!(decode_text(&[0x80], "windows-1254").unwrap(), "€");
1175
1176 assert_eq!(decode_text(&[0xA0], "ISO-8859-11").unwrap(), "\u{A0}");
1177 assert!(matches!(
1178 decode_text(&[0x80], "TIS-620"),
1179 Err(Error::InvalidBytes("TIS-620"))
1180 ));
1181 assert!(matches!(
1182 decode_text(&[0xA0], "TIS-620"),
1183 Err(Error::InvalidBytes("TIS-620"))
1184 ));
1185 assert_eq!(decode_text(&[0xA1, 0xFB], "TIS-620").unwrap(), "ก๛");
1186 assert_eq!(decode_text(&[0x80], "windows-874").unwrap(), "€");
1187 }
1188
1189 #[test]
1190 fn registered_iana_labels_retain_their_declared_repertoires() {
1191 assert_eq!(decode_text(&[0x80], "ISO-8859-2").unwrap(), "\u{80}");
1195 assert_eq!(decode_text(&[0xA1], "ISO-8859-2").unwrap(), "Ą");
1196 assert_eq!(decode_text(&[0x80], "windows-1250").unwrap(), "€");
1197
1198 let source = b"<?xml version=\"1.0\" encoding=\"ISO-8859-2\"?><root>\xA1</root>";
1199 assert!(decode_xml(source, None).unwrap().contains("<root>Ą</root>"));
1200 }
1201
1202 #[test]
1203 fn encoding_lookup_selects_the_strict_iso_8859_2_codec() {
1204 assert_eq!(
1207 encoding_rs::Encoding::for_label(b"ISO-8859-2"),
1208 Some(encoding_rs::ISO_8859_2)
1209 );
1210 assert_eq!(encoding_rs::ISO_8859_2.name(), "ISO-8859-2");
1211 }
1212
1213 #[test]
1214 fn decoder_only_labels_are_reported_as_unsupported() {
1215 for label in ["replacement", "ISO-2022-KR"] {
1219 assert!(matches!(
1220 decode_text(b"", label),
1221 Err(Error::UnsupportedEncoding(rejected)) if rejected == label
1222 ));
1223 }
1224 }
1225
1226 #[test]
1227 fn us_ascii_rejects_non_ascii_bytes() {
1228 assert!(matches!(
1231 decode_text(&[0x80], "US-ASCII"),
1232 Err(Error::InvalidBytes("US-ASCII"))
1233 ));
1234 assert_eq!(
1235 decode_text(b"plain ASCII", "US-ASCII").unwrap(),
1236 "plain ASCII"
1237 );
1238 }
1239
1240 #[test]
1241 fn transcoded_and_normalized_representations_are_both_bounded() {
1242 let bytes = b"<?xml version=\"1.0\" encoding=\"GBK\"?><root/>";
1243 let exact = decode_xml(bytes, None).expect("GBK declaration is supported");
1244 assert!(matches!(
1245 decode_xml_bounded(bytes, None, exact.len() - 1),
1246 Err(Error::DecodedLimit { .. })
1247 ));
1248 assert_eq!(decode_xml_bounded(bytes, None, exact.len()).unwrap(), exact);
1249 }
1250
1251 #[test]
1252 fn declaration_detection_is_not_limited_to_a_short_prefix() {
1253 let whitespace = " ".repeat(2_048);
1254 let source = format!("<?xml{whitespace}encoding=\"ISO-8859-1\"?><root>caf\u{e9}</root>");
1255 let bytes = source
1256 .chars()
1257 .map(|character| u8::try_from(u32::from(character)).unwrap())
1258 .collect::<Vec<_>>();
1259 assert!(decode_xml(&bytes, None).unwrap().contains("café"));
1260 }
1261
1262 #[test]
1263 fn declaration_allows_whitespace_before_its_terminator() {
1264 let source = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?><root>caf\xe9</root>";
1265 assert!(decode_xml(source, None).unwrap().contains("café"));
1266 }
1267
1268 #[test]
1269 fn unsupported_xml_signatures_fail_explicitly() {
1270 assert!(matches!(
1271 decode_xml(&[0x4C, 0x6F, 0xA7, 0x94], None),
1272 Err(Error::UnsupportedByteEncoding("EBCDIC"))
1273 ));
1274 }
1275
1276 #[test]
1277 fn truncated_utf16_reports_the_code_unit_boundary() {
1278 assert!(matches!(
1279 decode_xml(&[0xff, 0xfe, 0], None),
1280 Err(Error::InvalidUtf16Length("UTF-16LE"))
1281 ));
1282 }
1283}