1use alloc::{borrow::Cow, string::String};
7use core::ops::Range;
8
9#[doc(hidden)]
12pub trait DecodeStorage {
13 type Text: AsRef<str>;
14 type Buffer;
15 type Error: From<Error>;
16 fn label_error(&self, kind: LabelError, label: &str) -> Self::Error;
17 fn charge(&self, bytes: usize) -> Result<(), Self::Error>;
18 fn buffer(&self) -> Self::Buffer;
19 fn len(&self, buffer: &Self::Buffer) -> usize;
20 fn append(&self, buffer: &mut Self::Buffer, text: &str) -> Result<(), Self::Error>;
21 fn finish(&self, buffer: Self::Buffer) -> Result<Self::Text, Self::Error>;
22 fn replace(
23 &self,
24 text: Self::Text,
25 range: Range<usize>,
26 replacement: &str,
27 ) -> Result<Self::Text, Self::Error>;
28}
29
30#[doc(hidden)]
32#[derive(Clone, Copy)]
33pub enum LabelError {
34 Unsupported,
35 Forbidden,
36 Conflicting,
37}
38
39#[doc(hidden)]
41pub enum Decoded<'a, T> {
42 Borrowed(&'a str),
43 Owned(T),
44}
45
46impl<T: AsRef<str>> AsRef<str> for Decoded<'_, T> {
47 fn as_ref(&self) -> &str {
48 match self {
49 Self::Borrowed(text) => text,
50 Self::Owned(text) => text.as_ref(),
51 }
52 }
53}
54
55pub(crate) struct NativeStorage;
56
57impl DecodeStorage for NativeStorage {
58 type Text = String;
59 type Buffer = String;
60 type Error = Error;
61 fn label_error(&self, kind: LabelError, label: &str) -> Error {
62 match kind {
63 LabelError::Unsupported => Error::UnsupportedEncoding(label.into()),
64 LabelError::Forbidden => Error::ForbiddenEncoding(label.into()),
65 LabelError::Conflicting => Error::ConflictingEncoding(label.into()),
66 }
67 }
68 fn charge(&self, _: usize) -> Result<(), Error> {
69 Ok(())
70 }
71 fn buffer(&self) -> String {
72 String::new()
73 }
74 fn len(&self, buffer: &String) -> usize {
75 buffer.len()
76 }
77 fn append(&self, buffer: &mut String, text: &str) -> Result<(), Error> {
78 buffer.push_str(text);
79 Ok(())
80 }
81 fn finish(&self, buffer: String) -> Result<String, Error> {
82 Ok(buffer)
83 }
84 fn replace(
85 &self,
86 mut text: String,
87 range: Range<usize>,
88 replacement: &str,
89 ) -> Result<String, Error> {
90 text.replace_range(range, replacement);
91 Ok(text)
92 }
93}
94
95fn native_text(text: Decoded<'_, String>) -> Cow<'_, str> {
96 match text {
97 Decoded::Borrowed(text) => Cow::Borrowed(text),
98 Decoded::Owned(text) => Cow::Owned(text),
99 }
100}
101
102#[path = "lexical.rs"]
103pub mod lexical;
104
105#[derive(Debug, thiserror::Error)]
107#[non_exhaustive]
108pub enum Error<L: core::fmt::Display = String> {
109 #[error("unsupported XML byte encoding `{0}`")]
111 UnsupportedByteEncoding(&'static str),
112 #[error("unsupported XML encoding `{0}`")]
114 UnsupportedEncoding(L),
115 #[error("XML format does not permit encoding `{0}`")]
117 ForbiddenEncoding(L),
118 #[error("XML byte encoding conflicts with declared or selected encoding `{0}`")]
120 ConflictingEncoding(L),
121 #[error("malformed XML encoding declaration: {0}")]
123 MalformedDeclaration(&'static str),
124 #[error("XML input contains invalid {0} bytes")]
126 InvalidBytes(&'static str),
127 #[error("BOM-less UTF-16 XML input requires an explicit UTF-16LE or UTF-16BE declaration")]
129 MissingUtf16ByteOrder,
130 #[error("UTF-32 XML input requires a UTF-32LE/UTF-32BE encoding or byte-order signature")]
132 MissingUtf32ByteOrder,
133 #[error("{0} XML input requires an encoding declaration or trusted external encoding metadata")]
135 MissingEncodingDeclaration(&'static str),
136 #[error("{0} XML input has an odd byte length")]
138 InvalidUtf16Length(&'static str),
139 #[error("{0} XML input byte length is not divisible by four")]
141 InvalidUtf32Length(&'static str),
142 #[error("decoded XML exceeds the maximum size of {maximum} bytes: at least {actual} bytes")]
144 DecodedLimit {
145 maximum: usize,
147 actual: usize,
149 },
150}
151
152#[derive(Clone, Copy, PartialEq, Eq)]
153enum SelectedEncoding {
154 Standard(&'static encoding_rs::Encoding),
155 Utf32Le,
156 Utf32Be,
157 Ascii,
158 Registered(IanaSingleByteEncoding),
159}
160
161impl SelectedEncoding {
162 fn name(self) -> &'static str {
163 match self {
164 Self::Standard(encoding) => encoding.name(),
165 Self::Utf32Le => "UTF-32LE",
166 Self::Utf32Be => "UTF-32BE",
167 Self::Ascii => "US-ASCII",
168 Self::Registered(encoding) => encoding.name(),
169 }
170 }
171
172 fn is_utf8(self) -> bool {
173 matches!(self, Self::Standard(encoding) if encoding == encoding_rs::UTF_8)
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum IanaSingleByteEncoding {
180 Latin1,
182 Latin5,
184 Iso8859_11,
186 Tis620,
188}
189
190impl IanaSingleByteEncoding {
191 #[must_use]
192 pub const fn name(self) -> &'static str {
194 match self {
195 Self::Latin1 => "ISO-8859-1",
196 Self::Latin5 => "ISO-8859-9",
197 Self::Iso8859_11 => "ISO-8859-11",
198 Self::Tis620 => "TIS-620",
199 }
200 }
201
202 #[must_use]
203 pub fn decode_byte(self, byte: u8) -> Option<char> {
205 match self {
206 Self::Latin1 => Some(char::from(byte)),
207 Self::Latin5 => Some(match byte {
208 0xD0 => '\u{011E}',
209 0xDD => '\u{0130}',
210 0xDE => '\u{015E}',
211 0xF0 => '\u{011F}',
212 0xFD => '\u{0131}',
213 0xFE => '\u{015F}',
214 _ => char::from(byte),
215 }),
216 Self::Iso8859_11 | Self::Tis620 => match byte {
217 0x00..=0x7F => Some(char::from(byte)),
218 0xA0 if self == Self::Iso8859_11 => Some('\u{00A0}'),
219 0xA1..=0xDA | 0xE0..=0xFB => char::from_u32(u32::from(byte) + 0x0D60),
220 0xDF => Some('\u{0E3F}'),
221 _ => None,
222 },
223 }
224 }
225
226 #[must_use]
227 pub fn encode_char(self, character: char) -> Option<u8> {
229 match self {
230 Self::Latin1 => u8::try_from(u32::from(character)).ok(),
231 Self::Latin5 => match character {
232 '\u{011E}' => Some(0xD0),
233 '\u{0130}' => Some(0xDD),
234 '\u{015E}' => Some(0xDE),
235 '\u{011F}' => Some(0xF0),
236 '\u{0131}' => Some(0xFD),
237 '\u{015F}' => Some(0xFE),
238 _ => u8::try_from(u32::from(character))
239 .ok()
240 .filter(|byte| !matches!(byte, 0xD0 | 0xDD | 0xDE | 0xF0 | 0xFD | 0xFE)),
241 },
242 Self::Iso8859_11 | Self::Tis620 => match u32::from(character) {
243 value @ 0x00..=0x7F => Some(value as u8),
244 0xA0 if self == Self::Iso8859_11 => Some(0xA0),
245 value @ 0x0E01..=0x0E3A | value @ 0x0E40..=0x0E5B => {
246 u8::try_from(value - 0x0D60).ok()
247 }
248 0x0E3F => Some(0xDF),
249 _ => None,
250 },
251 }
252 }
253}
254
255#[must_use]
257pub fn registered_single_byte_encoding(label: &str) -> Option<IanaSingleByteEncoding> {
258 if is_latin1_encoding_label(label) {
259 return Some(IanaSingleByteEncoding::Latin1);
260 }
261 if matches_ascii_case(
262 label,
263 &[
264 "iso-ir-148",
265 "iso88599",
266 "iso-8859-9",
267 "iso_8859-9",
268 "latin5",
269 "csisolatin5",
270 "iso_8859-9:1989",
271 ],
272 ) {
273 return Some(IanaSingleByteEncoding::Latin5);
274 }
275 if matches_ascii_case(label, &["iso8859-11", "iso-8859-11"]) {
276 return Some(IanaSingleByteEncoding::Iso8859_11);
277 }
278 label
279 .eq_ignore_ascii_case("tis-620")
280 .then_some(IanaSingleByteEncoding::Tis620)
281}
282
283#[must_use]
288pub fn legacy_label_matches_encoding(
289 label: &str,
290 encoding: &'static encoding_rs::Encoding,
291) -> bool {
292 let canonical = encoding.name();
293 let Some(code_page) = canonical.strip_prefix("windows-") else {
294 return true;
295 };
296 label.eq_ignore_ascii_case(canonical)
297 || label
298 .get(2..)
299 .is_some_and(|suffix| label[..2].eq_ignore_ascii_case("cp") && suffix == code_page)
300 || label
301 .get(4..)
302 .is_some_and(|suffix| label[..4].eq_ignore_ascii_case("x-cp") && suffix == code_page)
303}
304
305pub fn decode_xml<'a>(
312 bytes: &'a [u8],
313 explicit_encoding: Option<&str>,
314) -> Result<Cow<'a, str>, Error> {
315 decode_xml_bounded(bytes, explicit_encoding, usize::MAX)
316}
317
318pub fn decode_xml_bounded<'a>(
321 bytes: &'a [u8],
322 explicit_encoding: Option<&str>,
323 maximum_decoded_bytes: usize,
324) -> Result<Cow<'a, str>, Error> {
325 decode_xml_bounded_inner(
326 bytes,
327 explicit_encoding,
328 maximum_decoded_bytes,
329 true,
330 |_, _| Ok(()),
331 &NativeStorage,
332 )
333 .map(native_text)
334}
335
336pub fn decode_xml_bounded_with_encoding_guard<'a>(
343 bytes: &'a [u8],
344 maximum_decoded_bytes: usize,
345 guard: impl Fn(&str, Option<&str>) -> Result<(), Error>,
346) -> Result<Cow<'a, str>, Error> {
347 decode_xml_bounded_inner(
348 bytes,
349 None,
350 maximum_decoded_bytes,
351 true,
352 guard,
353 &NativeStorage,
354 )
355 .map(native_text)
356}
357
358#[doc(hidden)]
360pub fn decode_xml_in<'a, S: DecodeStorage>(
361 bytes: &'a [u8],
362 maximum_decoded_bytes: usize,
363 guard: impl Fn(&str, Option<&str>) -> Result<(), S::Error>,
364 storage: &S,
365) -> Result<Decoded<'a, S::Text>, S::Error> {
366 decode_xml_bounded_inner(bytes, None, maximum_decoded_bytes, true, guard, storage)
367}
368
369pub fn decode_xml_text_bounded<'a>(
374 bytes: &'a [u8],
375 explicit_encoding: Option<&str>,
376 maximum_decoded_bytes: usize,
377) -> Result<Cow<'a, str>, Error> {
378 decode_xml_bounded_inner(
379 bytes,
380 explicit_encoding,
381 maximum_decoded_bytes,
382 false,
383 |_, _| Ok(()),
384 &NativeStorage,
385 )
386 .map(native_text)
387}
388
389fn decode_xml_bounded_inner<'a, S: DecodeStorage>(
390 bytes: &'a [u8],
391 explicit_encoding: Option<&str>,
392 maximum_decoded_bytes: usize,
393 normalize_declaration: bool,
394 encoding_guard: impl Fn(&str, Option<&str>) -> Result<(), S::Error>,
395 storage: &S,
396) -> Result<Decoded<'a, S::Text>, S::Error> {
397 storage.charge(bytes.len())?;
400 let physical = physical_encoding(bytes)?;
401 let ascii_declaration = if physical.is_none() {
402 declaration_from_ascii_bytes(bytes)?
403 } else {
404 None
405 };
406 let explicit_utf16 = explicit_encoding.is_some_and(is_generic_utf16);
407 let explicit_utf32 = explicit_encoding.is_some_and(is_generic_utf32);
408 let explicit = explicit_encoding
409 .filter(|_| !explicit_utf16 && !explicit_utf32)
410 .map(|label| parse_encoding_in(label, storage))
411 .transpose()?;
412 if explicit_utf16
413 && !physical.is_some_and(|(encoding, bom_len)| is_utf16_encoding(encoding) && bom_len > 0)
414 {
415 return Err(Error::MissingUtf16ByteOrder.into());
419 }
420 if explicit_utf32 && !physical.is_some_and(|(encoding, _)| is_utf32_encoding(encoding)) {
421 return Err(Error::MissingUtf32ByteOrder.into());
422 }
423 let declared_before_decode = ascii_declaration
424 .as_ref()
425 .map(|(_, label)| parse_encoding_in(label, storage))
426 .transpose()?;
427 let selected = explicit
428 .or(physical.map(|(encoding, _)| encoding))
429 .or(declared_before_decode)
430 .unwrap_or(SelectedEncoding::Standard(encoding_rs::UTF_8));
431
432 if let Some((physical, _)) = physical
433 && !encodings_compatible(selected, physical, true)
434 {
435 return Err(storage.label_error(LabelError::Conflicting, selected.name()));
436 }
437 if let Some(declared) = declared_before_decode
438 && !encodings_compatible(selected, declared, false)
439 {
440 return Err(storage.label_error(LabelError::Conflicting, declared.name()));
441 }
442
443 let bom_len = physical.map_or(0, |(_, bom_len)| bom_len);
444 encoding_guard(
445 selected.name(),
446 ascii_declaration.as_ref().map(|(_, label)| *label),
447 )?;
448 let mut decoded =
449 decode_selected_in(&bytes[bom_len..], selected, maximum_decoded_bytes, storage)?;
450 storage.charge(decoded.as_ref().len())?;
451 let declaration = declaration_from_text(decoded.as_ref())?;
452 if physical.is_some() {
453 encoding_guard(
454 selected.name(),
455 declaration
456 .as_ref()
457 .map(|range| &decoded.as_ref()[range.clone()]),
458 )?;
459 }
460 if explicit_encoding.is_none()
461 && declaration.is_none()
462 && physical.is_some_and(|(encoding, bom_len)| {
463 is_utf32_encoding(encoding) || (bom_len == 0 && is_utf16_encoding(encoding))
464 })
465 {
466 return Err(
470 if physical.is_some_and(|(encoding, _)| is_utf32_encoding(encoding)) {
471 Error::MissingEncodingDeclaration("UTF-32")
472 } else {
473 Error::MissingUtf16ByteOrder
474 }
475 .into(),
476 );
477 }
478 if let Some(range) = &declaration {
479 let label = &decoded.as_ref()[range.clone()];
480 if is_generic_utf16(label) {
481 let has_utf16_bom = physical.is_some_and(|(encoding, bom_len)| {
482 bom_len > 0
483 && matches!(encoding, SelectedEncoding::Standard(value)
484 if value == encoding_rs::UTF_16LE || value == encoding_rs::UTF_16BE)
485 });
486 if !has_utf16_bom {
487 return Err(Error::MissingUtf16ByteOrder.into());
488 }
489 } else if is_generic_utf32(label) {
490 if !physical.is_some_and(|(encoding, _)| is_utf32_encoding(encoding)) {
491 return Err(Error::MissingUtf32ByteOrder.into());
492 }
493 } else {
494 let declared = parse_encoding_in(label, storage)?;
495 if !encodings_compatible(selected, declared, false) {
496 return Err(storage.label_error(LabelError::Conflicting, label));
497 }
498 }
499 }
500
501 if normalize_declaration
502 && !selected.is_utf8()
503 && let Some(range) = declaration
504 {
505 let normalized_len = decoded
506 .as_ref()
507 .len()
508 .saturating_sub(range.len())
509 .saturating_add("UTF-8".len());
510 if normalized_len > maximum_decoded_bytes {
511 return Err(Error::DecodedLimit {
512 maximum: maximum_decoded_bytes,
513 actual: normalized_len,
514 }
515 .into());
516 }
517 decoded = Decoded::Owned(match decoded {
518 Decoded::Owned(text) => storage.replace(text, range, "UTF-8")?,
519 Decoded::Borrowed(text) => {
520 let mut buffer = storage.buffer();
521 storage.append(&mut buffer, &text[..range.start])?;
522 storage.append(&mut buffer, "UTF-8")?;
523 storage.append(&mut buffer, &text[range.end..])?;
524 storage.finish(buffer)?
525 }
526 });
527 }
528 Ok(decoded)
529}
530
531pub fn decode_text<'a>(bytes: &'a [u8], encoding: &str) -> Result<Cow<'a, str>, Error> {
533 decode_text_bounded(bytes, encoding, usize::MAX)
534}
535
536pub fn decode_text_bounded<'a>(
538 bytes: &'a [u8],
539 encoding: &str,
540 maximum_decoded_bytes: usize,
541) -> Result<Cow<'a, str>, Error> {
542 if is_generic_utf16(encoding) {
543 if let Some(payload) = bytes.strip_prefix(&[0xFF, 0xFE]) {
547 return decode_selected(
548 payload,
549 SelectedEncoding::Standard(encoding_rs::UTF_16LE),
550 maximum_decoded_bytes,
551 );
552 }
553 if let Some(payload) = bytes.strip_prefix(&[0xFE, 0xFF]) {
554 return decode_selected(
555 payload,
556 SelectedEncoding::Standard(encoding_rs::UTF_16BE),
557 maximum_decoded_bytes,
558 );
559 }
560 return Err(Error::MissingUtf16ByteOrder);
561 }
562 let selected = parse_encoding(encoding)?;
563 let bytes = match selected {
564 SelectedEncoding::Standard(value) if value == encoding_rs::UTF_16LE => {
568 if bytes.starts_with(&[0xFE, 0xFF]) {
569 return Err(Error::ConflictingEncoding(selected.name().into()));
570 }
571 bytes
572 }
573 SelectedEncoding::Standard(value) if value == encoding_rs::UTF_16BE => {
574 if bytes.starts_with(&[0xFF, 0xFE]) {
575 return Err(Error::ConflictingEncoding(selected.name().into()));
576 }
577 bytes
578 }
579 SelectedEncoding::Standard(value) if value == encoding_rs::UTF_8 => {
582 bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes)
583 }
584 _ => bytes,
585 };
586 decode_selected(bytes, selected, maximum_decoded_bytes)
587}
588
589fn physical_encoding(bytes: &[u8]) -> Result<Option<(SelectedEncoding, usize)>, Error> {
590 let prefix = bytes.get(..4).unwrap_or(bytes);
591 match prefix {
595 [0x00, 0x00, 0xFE, 0xFF] => return Ok(Some((SelectedEncoding::Utf32Be, 4))),
596 [0xFF, 0xFE, 0x00, 0x00] => return Ok(Some((SelectedEncoding::Utf32Le, 4))),
597 [0x00, 0x00, 0x00, b'<'] => return Ok(Some((SelectedEncoding::Utf32Be, 0))),
598 [b'<', 0x00, 0x00, 0x00] => return Ok(Some((SelectedEncoding::Utf32Le, 0))),
599 [0x00, 0x00, 0xFF, 0xFE]
600 | [0xFE, 0xFF, 0x00, 0x00]
601 | [0x00, 0x00, b'<', 0x00]
602 | [0x00, b'<', 0x00, 0x00] => {
603 return Err(Error::UnsupportedByteEncoding("UTF-32 unusual byte order"));
604 }
605 _ => {}
606 }
607 if prefix == [0x4C, 0x6F, 0xA7, 0x94] {
608 return Err(Error::UnsupportedByteEncoding("EBCDIC"));
609 }
610 if let Some((encoding, length)) = encoding_rs::Encoding::for_bom(bytes) {
611 return Ok(Some((SelectedEncoding::Standard(encoding), length)));
612 }
613 Ok(match prefix {
614 [0x00, b'<', 0x00, b'?'] => Some((SelectedEncoding::Standard(encoding_rs::UTF_16BE), 0)),
615 [b'<', 0x00, b'?', 0x00] => Some((SelectedEncoding::Standard(encoding_rs::UTF_16LE), 0)),
616 _ => None,
617 })
618}
619
620fn parse_encoding(label: &str) -> Result<SelectedEncoding, Error> {
621 parse_encoding_in(label, &NativeStorage)
622}
623
624fn parse_encoding_in<S: DecodeStorage>(
625 label: &str,
626 storage: &S,
627) -> Result<SelectedEncoding, S::Error> {
628 if matches_ascii_case(label, &["utf-32le", "utf32le"]) {
629 return Ok(SelectedEncoding::Utf32Le);
630 }
631 if matches_ascii_case(label, &["utf-32be", "utf32be"]) {
632 return Ok(SelectedEncoding::Utf32Be);
633 }
634 if matches_ascii_case(label, &["us-ascii", "ascii"]) {
635 return Ok(SelectedEncoding::Ascii);
636 }
637 if let Some(encoding) = registered_single_byte_encoding(label) {
642 return Ok(SelectedEncoding::Registered(encoding));
643 }
644 let encoding = encoding_rs::Encoding::for_label_no_replacement(label.as_bytes())
653 .ok_or_else(|| storage.label_error(LabelError::Unsupported, label))?;
654 if !legacy_label_matches_encoding(label, encoding) {
655 return Err(storage.label_error(LabelError::Unsupported, label));
656 }
657 Ok(SelectedEncoding::Standard(encoding))
658}
659
660#[must_use]
665pub fn is_latin1_encoding_label(label: &str) -> bool {
666 matches_ascii_case(
667 label,
668 &[
669 "iso_8859-1:1987",
670 "iso-ir-100",
671 "iso_8859-1",
672 "iso-8859-1",
673 "latin1",
674 "latin-1",
675 "l1",
676 "ibm819",
677 "cp819",
678 "csisolatin1",
679 ],
680 )
681}
682
683fn decode_selected<'a>(
684 bytes: &'a [u8],
685 encoding: SelectedEncoding,
686 maximum: usize,
687) -> Result<Cow<'a, str>, Error> {
688 decode_selected_in(bytes, encoding, maximum, &NativeStorage).map(native_text)
689}
690
691fn decode_selected_in<'a, S: DecodeStorage>(
692 bytes: &'a [u8],
693 encoding: SelectedEncoding,
694 maximum: usize,
695 storage: &S,
696) -> Result<Decoded<'a, S::Text>, S::Error> {
697 if matches!(
698 encoding,
699 SelectedEncoding::Utf32Le | SelectedEncoding::Utf32Be
700 ) {
701 return decode_utf32(bytes, encoding, maximum, storage).map(Decoded::Owned);
702 }
703 if encoding == SelectedEncoding::Ascii {
704 if bytes.iter().any(|byte| !byte.is_ascii()) {
705 return Err(Error::InvalidBytes("US-ASCII").into());
706 }
707 let decoded =
708 core::str::from_utf8(bytes).expect("seven-bit US-ASCII is always valid UTF-8");
709 if decoded.len() > maximum {
710 return Err(Error::DecodedLimit {
711 maximum,
712 actual: decoded.len(),
713 }
714 .into());
715 }
716 return Ok(Decoded::Borrowed(decoded));
717 }
718 if matches!(encoding, SelectedEncoding::Registered(_)) {
719 return decode_registered_single_byte(bytes, encoding, maximum, storage)
720 .map(Decoded::Owned);
721 }
722 let SelectedEncoding::Standard(encoding) = encoding else {
723 unreachable!("special-case encodings returned above")
724 };
725 if encoding == encoding_rs::UTF_8 {
726 let decoded = core::str::from_utf8(bytes).map_err(|_| Error::InvalidBytes("UTF-8"))?;
727 if decoded.len() > maximum {
728 return Err(Error::DecodedLimit {
729 maximum,
730 actual: decoded.len(),
731 }
732 .into());
733 }
734 return Ok(Decoded::Borrowed(decoded));
735 }
736 if (encoding == encoding_rs::UTF_16LE || encoding == encoding_rs::UTF_16BE)
737 && !bytes.len().is_multiple_of(2)
738 {
739 return Err(Error::InvalidUtf16Length(encoding.name()).into());
740 }
741
742 let mut decoder = encoding.new_decoder_without_bom_handling();
743 let mut remaining = bytes;
744 let mut decoded = storage.buffer();
745 let mut buffer = [0_u8; 4096];
746 loop {
747 let input_length = remaining.len().min(buffer.len());
751 let last = input_length == remaining.len();
752 storage.charge(input_length)?;
753 let (result, read, written) = decoder.decode_to_utf8_without_replacement(
754 &remaining[..input_length],
755 &mut buffer,
756 last,
757 );
758 let actual = storage.len(&decoded).saturating_add(written);
759 if actual > maximum {
760 return Err(Error::DecodedLimit { maximum, actual }.into());
761 }
762 storage.charge(written)?;
763 storage.append(
764 &mut decoded,
765 core::str::from_utf8(&buffer[..written])
766 .expect("encoding_rs emits valid UTF-8 into the output buffer"),
767 )?;
768 remaining = &remaining[read..];
769 match result {
770 encoding_rs::DecoderResult::InputEmpty if last => {
771 return storage.finish(decoded).map(Decoded::Owned);
772 }
773 encoding_rs::DecoderResult::InputEmpty => {}
774 encoding_rs::DecoderResult::OutputFull => {}
775 encoding_rs::DecoderResult::Malformed(_, _) => {
776 return Err(Error::InvalidBytes(encoding.name()).into());
777 }
778 }
779 }
780}
781
782fn decode_registered_single_byte<S: DecodeStorage>(
783 bytes: &[u8],
784 encoding: SelectedEncoding,
785 maximum: usize,
786 storage: &S,
787) -> Result<S::Text, S::Error> {
788 storage.charge(bytes.len())?;
789 let mut decoded = storage.buffer();
790 for &byte in bytes {
791 let character = match encoding {
792 SelectedEncoding::Registered(encoding) => encoding
793 .decode_byte(byte)
794 .ok_or_else(|| Error::InvalidBytes(encoding.name()))?,
795 _ => unreachable!("registered single-byte decoder receives a matching encoding"),
796 };
797 let actual = storage.len(&decoded).saturating_add(character.len_utf8());
798 if actual > maximum {
799 return Err(Error::DecodedLimit { maximum, actual }.into());
800 }
801 storage.append(&mut decoded, character.encode_utf8(&mut [0; 4]))?;
802 }
803 storage.finish(decoded)
804}
805
806fn decode_utf32<S: DecodeStorage>(
807 bytes: &[u8],
808 encoding: SelectedEncoding,
809 maximum: usize,
810 storage: &S,
811) -> Result<S::Text, S::Error> {
812 if !bytes.len().is_multiple_of(4) {
813 return Err(Error::InvalidUtf32Length(encoding.name()).into());
814 }
815 storage.charge(bytes.len())?;
816 let mut decoded = storage.buffer();
817 let (units, remainder) = bytes.as_chunks::<4>();
818 debug_assert!(remainder.is_empty());
819 for &unit in units {
820 let scalar = match encoding {
821 SelectedEncoding::Utf32Le => u32::from_le_bytes(unit),
822 SelectedEncoding::Utf32Be => u32::from_be_bytes(unit),
823 _ => unreachable!("UTF-32 decoder receives an explicit byte order"),
824 };
825 let character = char::from_u32(scalar).ok_or(Error::InvalidBytes(encoding.name()))?;
826 let actual = storage.len(&decoded).saturating_add(character.len_utf8());
827 if actual > maximum {
828 return Err(Error::DecodedLimit { maximum, actual }.into());
829 }
830 storage.append(&mut decoded, character.encode_utf8(&mut [0; 4]))?;
831 }
832 storage.finish(decoded)
833}
834
835fn encodings_compatible(
836 selected: SelectedEncoding,
837 candidate: SelectedEncoding,
838 physical: bool,
839) -> bool {
840 selected == candidate
841 || (physical
842 && matches!(selected, SelectedEncoding::Standard(value) if value == encoding_rs::UTF_8)
843 && matches!(candidate, SelectedEncoding::Standard(value) if value == encoding_rs::UTF_8))
844}
845
846fn is_utf16_encoding(encoding: SelectedEncoding) -> bool {
847 matches!(encoding, SelectedEncoding::Standard(value)
848 if value == encoding_rs::UTF_16LE || value == encoding_rs::UTF_16BE)
849}
850
851fn is_utf32_encoding(encoding: SelectedEncoding) -> bool {
852 matches!(
853 encoding,
854 SelectedEncoding::Utf32Le | SelectedEncoding::Utf32Be
855 )
856}
857
858const fn is_xml_s_byte(byte: &u8) -> bool {
861 matches!(*byte, b' ' | b'\t' | b'\r' | b'\n')
862}
863
864fn declaration_from_ascii_bytes(bytes: &[u8]) -> Result<Option<(Range<usize>, &str)>, Error> {
865 let bytes = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes);
866 if !bytes.starts_with(b"<?xml") || !bytes.get(5).is_some_and(is_xml_s_byte) {
867 return Ok(None);
868 }
869 let end = bytes
870 .windows(2)
871 .position(|window| window == b"?>")
872 .map(|index| index + 2)
873 .ok_or(Error::MalformedDeclaration("unterminated declaration"))?;
874 let prefix = core::str::from_utf8(&bytes[..end])
877 .map_err(|_| Error::MalformedDeclaration("declaration is not ASCII-compatible"))?;
878 declaration_from_text(prefix).map(|range| {
879 range.map(|range| {
880 let label = &prefix[range.clone()];
881 (range, label)
882 })
883 })
884}
885
886fn declaration_from_text(xml: &str) -> Result<Option<Range<usize>>, Error> {
887 let Some(rest) = xml.strip_prefix("<?xml") else {
888 return Ok(None);
889 };
890 if !rest.as_bytes().first().is_some_and(is_xml_s_byte) {
891 return Ok(None);
892 }
893 let end = rest
894 .find("?>")
895 .ok_or(Error::MalformedDeclaration("unterminated declaration"))?;
896 let declaration = &rest.as_bytes()[..end];
897 let mut cursor = 0;
898 let mut encoding_range = None;
899 while cursor < declaration.len() {
900 while declaration.get(cursor).is_some_and(is_xml_s_byte) {
901 cursor += 1;
902 }
903 if cursor == declaration.len() {
904 break;
905 }
906 let name_start = cursor;
907 while declaration.get(cursor).is_some_and(|byte| {
908 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':' | b'-' | b'.')
909 }) {
910 cursor += 1;
911 }
912 if cursor == name_start {
913 return Err(Error::MalformedDeclaration("invalid pseudo-attribute"));
914 }
915 let name = &declaration[name_start..cursor];
916 while declaration.get(cursor).is_some_and(is_xml_s_byte) {
917 cursor += 1;
918 }
919 if declaration.get(cursor) != Some(&b'=') {
920 return Err(Error::MalformedDeclaration("missing `=`"));
921 }
922 cursor += 1;
923 while declaration.get(cursor).is_some_and(is_xml_s_byte) {
924 cursor += 1;
925 }
926 let "e @ (b'\'' | b'"') = declaration
927 .get(cursor)
928 .ok_or(Error::MalformedDeclaration("missing quoted value"))?
929 else {
930 return Err(Error::MalformedDeclaration("value is not quoted"));
931 };
932 let value_start = cursor + 1;
933 let value_end = value_start
934 + declaration[value_start..]
935 .iter()
936 .position(|byte| *byte == quote)
937 .ok_or(Error::MalformedDeclaration("unterminated value"))?;
938 if name == b"encoding" {
939 let value = &declaration[value_start..value_end];
940 if !is_xml_encoding_name_bytes(value) {
944 return Err(Error::MalformedDeclaration("invalid encoding name"));
945 }
946 encoding_range = Some((5 + value_start)..(5 + value_end));
947 }
948 cursor = value_end + 1;
949 if declaration
950 .get(cursor)
951 .is_some_and(|byte| !is_xml_s_byte(byte))
952 {
953 return Err(Error::MalformedDeclaration("missing whitespace"));
954 }
955 }
956 Ok(encoding_range)
960}
961
962#[must_use]
967pub fn is_xml_encoding_name(value: &str) -> bool {
968 is_xml_encoding_name_bytes(value.as_bytes())
969}
970
971fn is_xml_encoding_name_bytes(value: &[u8]) -> bool {
972 value.first().is_some_and(u8::is_ascii_alphabetic)
973 && value[1..]
974 .iter()
975 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
976}
977
978fn is_generic_utf16(label: &str) -> bool {
979 label.eq_ignore_ascii_case("UTF-16") || label.eq_ignore_ascii_case("UTF16")
980}
981
982fn is_generic_utf32(label: &str) -> bool {
983 label.eq_ignore_ascii_case("UTF-32") || label.eq_ignore_ascii_case("UTF32")
984}
985
986fn matches_ascii_case(value: &str, candidates: &[&str]) -> bool {
987 candidates
988 .iter()
989 .any(|candidate| value.eq_ignore_ascii_case(candidate))
990}
991
992#[cfg(all(test, feature = "std"))]
993#[allow(clippy::unwrap_used)]
994mod tests {
995 use std::borrow::Cow;
996
997 use super::{
998 Error, declaration_from_ascii_bytes, declaration_from_text, decode_text, decode_xml,
999 decode_xml_bounded, decode_xml_bounded_with_encoding_guard,
1000 };
1001
1002 #[test]
1003 fn encoding_guard_precedes_conversion_and_preserves_borrowing() {
1004 let rejected = decode_xml_bounded_with_encoding_guard(
1006 b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><root/>",
1007 0,
1008 |physical, _| Err(Error::ForbiddenEncoding(physical.into())),
1009 );
1010 assert!(matches!(rejected, Err(Error::ForbiddenEncoding(_))));
1011 let source = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>";
1012 let accepted =
1013 decode_xml_bounded_with_encoding_guard(source, source.len(), |physical, declared| {
1014 assert_eq!(physical, "UTF-8");
1015 assert_eq!(declared, Some("UTF-8"));
1016 Ok(())
1017 })
1018 .unwrap();
1019 assert!(matches!(accepted, Cow::Borrowed(_)));
1020 }
1021
1022 fn encode_utf32(source: &str, little_endian: bool) -> Vec<u8> {
1023 source
1024 .chars()
1025 .flat_map(|character| {
1026 let value = u32::from(character);
1027 if little_endian {
1028 value.to_le_bytes()
1029 } else {
1030 value.to_be_bytes()
1031 }
1032 })
1033 .collect()
1034 }
1035
1036 #[test]
1037 fn utf8_without_a_rewritten_declaration_stays_borrowed() {
1038 let bytes = b"<root>ok</root>";
1039 assert!(matches!(decode_xml(bytes, None), Ok(Cow::Borrowed(_))));
1040 }
1041
1042 #[test]
1043 fn generic_utf16_uses_the_bom_byte_order() {
1044 let source = "<?xml version=\"1.0\" encoding=\"UTF-16\"?><root>lambda</root>";
1045 let mut bytes = vec![0xFE, 0xFF];
1046 bytes.extend(source.encode_utf16().flat_map(u16::to_be_bytes));
1047 let decoded = decode_xml(&bytes, Some("UTF-16")).expect("BOM selects UTF-16BE");
1048 assert!(decoded.contains("encoding=\"UTF-8\""));
1049 assert!(decoded.contains("<root>lambda</root>"));
1050 }
1051
1052 #[test]
1053 fn xml_declaration_rejects_values_outside_the_enc_name_grammar() {
1054 let bytes = b"<?xml version=\"1.0\" encoding=\"iso_8859-1:1987\"?><root/>";
1057 assert!(matches!(
1058 decode_xml(bytes, None),
1059 Err(Error::MalformedDeclaration("invalid encoding name"))
1060 ));
1061 }
1062
1063 #[test]
1064 fn encoding_selection_requires_a_complete_well_formed_declaration() {
1065 let malformed = "<?xml version=\"1.0\" encoding=\"UTF-8\" trailing?><root/>";
1069 assert!(matches!(
1070 declaration_from_text(malformed),
1071 Err(Error::MalformedDeclaration("missing `=`"))
1072 ));
1073 assert!(matches!(
1074 declaration_from_ascii_bytes(malformed.as_bytes()),
1075 Err(Error::MalformedDeclaration("missing `=`"))
1076 ));
1077
1078 let mut utf16 = vec![0xFF, 0xFE];
1079 utf16.extend(malformed.encode_utf16().flat_map(u16::to_le_bytes));
1080 assert!(matches!(
1081 decode_xml(&utf16, None),
1082 Err(Error::MalformedDeclaration("missing `=`"))
1083 ));
1084 }
1085
1086 #[test]
1087 fn xml_declaration_rejects_non_xml_ascii_whitespace() {
1088 for whitespace in *b"\x0b\x0c" {
1091 let source = [
1092 b"<?xml".as_slice(),
1093 &[whitespace],
1094 b"version=\"1.0\" encoding=\"ISO-8859-1\"?><root>caf\xe9</root>".as_slice(),
1095 ]
1096 .concat();
1097 assert!(matches!(
1098 decode_xml(&source, None),
1099 Err(Error::InvalidBytes("UTF-8"))
1100 ));
1101 }
1102 }
1103
1104 #[test]
1105 fn xml_prefixed_processing_instruction_is_not_a_declaration() {
1106 let bytes = b"<?xml-stylesheet title='caf\xE9'?><root/>";
1109 assert_eq!(
1110 decode_xml(bytes, Some("windows-1252")).expect("PI follows resolver encoding"),
1111 "<?xml-stylesheet title='caf\u{e9}'?><root/>"
1112 );
1113 }
1114
1115 #[test]
1116 fn utf32_requires_a_bom_metadata_or_encoding_declaration() {
1117 let source = "<?xml version=\"1.0\" encoding=\"UTF-32\"?><root>lambda</root>";
1120 for (little_endian, bom) in [
1121 (false, [0x00, 0x00, 0xFE, 0xFF]),
1122 (true, [0xFF, 0xFE, 0x00, 0x00]),
1123 ] {
1124 let mut bytes = bom.to_vec();
1125 bytes.extend(encode_utf32(source, little_endian));
1126 let decoded = decode_xml(&bytes, None).expect("UTF-32 BOM selects byte order");
1127 assert!(decoded.contains("encoding=\"UTF-8\""));
1128 assert!(decoded.contains("<root>lambda</root>"));
1129
1130 let mut declarationless = bom.to_vec();
1131 declarationless.extend(encode_utf32("<root>lambda</root>", little_endian));
1132 assert!(
1133 decode_xml(&declarationless, None).is_err(),
1134 "a UTF-32 BOM identifies byte order but does not replace the encoding declaration"
1135 );
1136 }
1137
1138 for little_endian in [false, true] {
1139 let declaration = encode_utf32(
1140 "<?xml version=\"1.0\" encoding=\"UTF-32\"?><root>lambda</root>",
1141 little_endian,
1142 );
1143 assert!(
1144 decode_xml(&declaration, None)
1145 .expect("the initial signature and declaration identify UTF-32")
1146 .contains("<root>lambda</root>")
1147 );
1148 let declarationless = encode_utf32("<root>lambda</root>", little_endian);
1149 assert!(matches!(
1150 decode_xml(&declarationless, None),
1151 Err(Error::MissingEncodingDeclaration("UTF-32"))
1152 ));
1153 assert_eq!(
1154 decode_xml(
1155 &declarationless,
1156 Some(if little_endian {
1157 "UTF-32LE"
1158 } else {
1159 "UTF-32BE"
1160 })
1161 )
1162 .expect("trusted external metadata supplies the encoding"),
1163 "<root>lambda</root>"
1164 );
1165 }
1166 }
1167
1168 #[test]
1169 fn utf32_rejects_truncation_invalid_scalars_and_conflicting_metadata() {
1170 let mut truncated = vec![0x00, 0x00, 0xFE, 0xFF];
1171 truncated.extend([0x00, 0x00, 0x00]);
1172 assert!(decode_xml(&truncated, None).is_err());
1173
1174 let mut surrogate = vec![0x00, 0x00, 0xFE, 0xFF];
1175 surrogate.extend(0xD800_u32.to_be_bytes());
1176 assert!(matches!(
1177 decode_xml(&surrogate, None),
1178 Err(Error::InvalidBytes("UTF-32BE"))
1179 ));
1180
1181 let mut little_endian = vec![0xFF, 0xFE, 0x00, 0x00];
1182 little_endian.extend(encode_utf32("<root/>", true));
1183 assert!(matches!(
1184 decode_xml(&little_endian, Some("UTF-32BE")),
1185 Err(Error::ConflictingEncoding(_))
1186 ));
1187 }
1188
1189 #[test]
1190 fn utf32_decoding_obeys_the_utf8_materialization_limit() {
1191 let mut bytes = vec![0x00, 0x00, 0xFE, 0xFF];
1192 bytes.extend(encode_utf32("<root>lambda</root>", false));
1193 assert!(matches!(
1194 decode_xml_bounded(&bytes, None, 8),
1195 Err(Error::DecodedLimit { maximum: 8, .. })
1196 ));
1197 }
1198
1199 #[test]
1200 fn bomless_generic_utf16_is_rejected_as_ambiguous() {
1201 let bytes = "<?xml version=\"1.0\" encoding=\"UTF-16\"?><root/>"
1202 .encode_utf16()
1203 .flat_map(u16::to_le_bytes)
1204 .collect::<Vec<_>>();
1205 assert!(matches!(
1206 decode_xml(&bytes, Some("UTF-16")),
1207 Err(Error::MissingUtf16ByteOrder)
1208 ));
1209 }
1210
1211 #[test]
1212 fn generic_utf16_metadata_does_not_replace_the_required_bom() {
1213 let bytes = "<?xml version=\"1.0\"?><root/>"
1217 .encode_utf16()
1218 .flat_map(u16::to_le_bytes)
1219 .collect::<Vec<_>>();
1220 assert!(matches!(
1221 decode_xml(&bytes, Some("UTF-16")),
1222 Err(Error::MissingUtf16ByteOrder)
1223 ));
1224 }
1225
1226 #[test]
1227 fn generic_utf16_metadata_cannot_bypass_the_bom_via_a_specific_declaration() {
1228 let bytes = "<?xml version=\"1.0\" encoding=\"UTF-16LE\"?><root/>"
1231 .encode_utf16()
1232 .flat_map(u16::to_le_bytes)
1233 .collect::<Vec<_>>();
1234 assert!(matches!(
1235 decode_xml(&bytes, Some("UTF-16")),
1236 Err(Error::MissingUtf16ByteOrder)
1237 ));
1238 }
1239
1240 #[test]
1241 fn explicit_utf16_byte_order_decodes_a_declarationless_resource() {
1242 let bytes = "<root>lambda</root>"
1243 .encode_utf16()
1244 .flat_map(u16::to_le_bytes)
1245 .collect::<Vec<_>>();
1246 assert_eq!(
1247 decode_xml(&bytes, Some("UTF-16LE")).unwrap(),
1248 "<root>lambda</root>"
1249 );
1250 }
1251
1252 #[test]
1253 fn endian_specific_utf16_accepts_a_matching_byte_order_mark() {
1254 let mut metadata = vec![0xFF, 0xFE];
1257 metadata.extend("<root/>".encode_utf16().flat_map(u16::to_le_bytes));
1258 assert_eq!(decode_xml(&metadata, Some("UTF-16LE")).unwrap(), "<root/>");
1259 assert_eq!(
1260 decode_text(&metadata, "UTF-16LE").unwrap(),
1261 "\u{feff}<root/>"
1262 );
1263
1264 let mut big_endian_text = vec![0xFE, 0xFF];
1265 big_endian_text.extend("body".encode_utf16().flat_map(u16::to_be_bytes));
1266 assert_eq!(
1267 decode_text(&big_endian_text, "UTF-16BE").unwrap(),
1268 "\u{feff}body"
1269 );
1270
1271 let mut declaration = vec![0xFE, 0xFF];
1272 declaration.extend(
1273 "<?xml version=\"1.0\" encoding=\"UTF-16BE\"?><root/>"
1274 .encode_utf16()
1275 .flat_map(u16::to_be_bytes),
1276 );
1277 assert_eq!(
1278 decode_xml(&declaration, None).unwrap(),
1279 "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>"
1280 );
1281 }
1282
1283 #[test]
1284 fn generic_utf16_text_requires_and_consumes_a_byte_order_mark() {
1285 let text = "lambda";
1286 let mut little = vec![0xFF, 0xFE];
1287 little.extend(text.encode_utf16().flat_map(u16::to_le_bytes));
1288 let mut big = vec![0xFE, 0xFF];
1289 big.extend(text.encode_utf16().flat_map(u16::to_be_bytes));
1290
1291 assert_eq!(decode_text(&little, "UTF-16").unwrap(), text);
1292 assert_eq!(decode_text(&big, "UTF-16").unwrap(), text);
1293 assert!(matches!(
1294 decode_text(&little[2..], "UTF-16"),
1295 Err(Error::MissingUtf16ByteOrder)
1296 ));
1297 }
1298
1299 #[test]
1300 fn utf8_text_consumes_its_signature_without_removing_content_fe_ff() {
1301 assert_eq!(decode_text(b"\xef\xbb\xbfbody", "UTF-8").unwrap(), "body");
1304 assert_eq!(decode_text(b"\xef\xbb\xbfbody", "utf8").unwrap(), "body");
1305 assert_eq!(
1306 decode_text(b"body\xef\xbb\xbftail", "UTF-8").unwrap(),
1307 "body\u{feff}tail"
1308 );
1309 assert_eq!(decode_text(b"body", "UTF-8").unwrap(), "body");
1310 }
1311
1312 #[test]
1313 fn trusted_metadata_cannot_conflict_with_a_bom() {
1314 assert!(matches!(
1315 decode_xml(&[0xFF, 0xFE, b'A', 0], Some("UTF-8")),
1316 Err(Error::ConflictingEncoding(_))
1317 ));
1318 }
1319
1320 #[test]
1321 fn latin1_and_windows_1252_keep_distinct_c1_semantics() {
1322 for alias in [
1325 "ISO_8859-1:1987",
1326 "iso-ir-100",
1327 "ISO_8859-1",
1328 "ISO-8859-1",
1329 "latin1",
1330 "l1",
1331 "IBM819",
1332 "CP819",
1333 "csISOLatin1",
1334 ] {
1335 assert_eq!(
1336 decode_text(&[0x80], alias).unwrap(),
1337 "\u{80}",
1338 "IANA alias {alias} must retain ISO-8859-1 C1 semantics"
1339 );
1340 }
1341 assert_eq!(decode_text(&[0x80], "windows-1252").unwrap(), "€");
1342 }
1343
1344 #[test]
1345 fn iana_single_byte_encodings_do_not_inherit_windows_extensions() {
1346 assert_eq!(decode_text(&[0x80], "ISO-8859-9").unwrap(), "\u{80}");
1349 assert_eq!(decode_text(&[0x80], "iso88599").unwrap(), "\u{80}");
1350 assert_eq!(decode_text(&[0xD0, 0xFD], "ISO-8859-9").unwrap(), "Ğı");
1351 assert_eq!(decode_text(&[0x80], "windows-1254").unwrap(), "€");
1352
1353 assert_eq!(decode_text(&[0xA0], "ISO-8859-11").unwrap(), "\u{A0}");
1354 assert!(matches!(
1355 decode_text(&[0x80], "TIS-620"),
1356 Err(Error::InvalidBytes("TIS-620"))
1357 ));
1358 assert!(matches!(
1359 decode_text(&[0xA0], "TIS-620"),
1360 Err(Error::InvalidBytes("TIS-620"))
1361 ));
1362 assert_eq!(decode_text(&[0xA1, 0xFB], "TIS-620").unwrap(), "ก๛");
1363 assert_eq!(decode_text(&[0x80], "windows-874").unwrap(), "€");
1364 }
1365
1366 #[test]
1367 fn registered_iana_labels_retain_their_declared_repertoires() {
1368 assert_eq!(decode_text(&[0x80], "ISO-8859-2").unwrap(), "\u{80}");
1372 assert_eq!(decode_text(&[0xA1], "ISO-8859-2").unwrap(), "Ą");
1373 assert_eq!(decode_text(&[0x80], "windows-1250").unwrap(), "€");
1374
1375 let source = b"<?xml version=\"1.0\" encoding=\"ISO-8859-2\"?><root>\xA1</root>";
1376 assert!(decode_xml(source, None).unwrap().contains("<root>Ą</root>"));
1377 }
1378
1379 #[test]
1380 fn encoding_lookup_selects_the_strict_iso_8859_2_codec() {
1381 assert_eq!(
1384 encoding_rs::Encoding::for_label(b"ISO-8859-2"),
1385 Some(encoding_rs::ISO_8859_2)
1386 );
1387 assert_eq!(encoding_rs::ISO_8859_2.name(), "ISO-8859-2");
1388 }
1389
1390 #[test]
1391 fn decoder_only_labels_are_reported_as_unsupported() {
1392 for label in ["replacement", "ISO-2022-KR"] {
1396 assert!(matches!(
1397 decode_text(b"", label),
1398 Err(Error::UnsupportedEncoding(rejected)) if rejected == label
1399 ));
1400 }
1401 }
1402
1403 #[test]
1404 fn us_ascii_rejects_non_ascii_bytes() {
1405 assert!(matches!(
1408 decode_text(&[0x80], "US-ASCII"),
1409 Err(Error::InvalidBytes("US-ASCII"))
1410 ));
1411 assert_eq!(
1412 decode_text(b"plain ASCII", "US-ASCII").unwrap(),
1413 "plain ASCII"
1414 );
1415 }
1416
1417 #[test]
1418 fn transcoded_and_normalized_representations_are_both_bounded() {
1419 let bytes = b"<?xml version=\"1.0\" encoding=\"GBK\"?><root/>";
1420 let exact = decode_xml(bytes, None).expect("GBK declaration is supported");
1421 assert!(matches!(
1422 decode_xml_bounded(bytes, None, exact.len() - 1),
1423 Err(Error::DecodedLimit { .. })
1424 ));
1425 assert_eq!(decode_xml_bounded(bytes, None, exact.len()).unwrap(), exact);
1426 }
1427
1428 #[test]
1429 fn declaration_detection_is_not_limited_to_a_short_prefix() {
1430 let whitespace = " ".repeat(2_048);
1431 let source = format!("<?xml{whitespace}encoding=\"ISO-8859-1\"?><root>caf\u{e9}</root>");
1432 let bytes = source
1433 .chars()
1434 .map(|character| u8::try_from(u32::from(character)).unwrap())
1435 .collect::<Vec<_>>();
1436 assert!(decode_xml(&bytes, None).unwrap().contains("café"));
1437 }
1438
1439 #[test]
1440 fn declaration_allows_whitespace_before_its_terminator() {
1441 let source = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?><root>caf\xe9</root>";
1442 assert!(decode_xml(source, None).unwrap().contains("café"));
1443 }
1444
1445 #[test]
1446 fn unsupported_xml_signatures_fail_explicitly() {
1447 assert!(matches!(
1448 decode_xml(&[0x4C, 0x6F, 0xA7, 0x94], None),
1449 Err(Error::UnsupportedByteEncoding("EBCDIC"))
1450 ));
1451 }
1452
1453 #[test]
1454 fn truncated_utf16_reports_the_code_unit_boundary() {
1455 assert!(matches!(
1456 decode_xml(&[0xff, 0xfe, 0], None),
1457 Err(Error::InvalidUtf16Length("UTF-16LE"))
1458 ));
1459 }
1460}