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