1use std::borrow::Cow;
27
28use crate::error::{ErrorDomain, ErrorLevel, Result, XmlError};
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum Encoding {
33 Utf8,
35 Ascii,
37 Latin1,
39 Windows1252,
43 Utf16Le,
46 Utf16Be,
49 Utf32Le,
53 Utf32Be,
57 Ebcdic037,
62 Ebcdic500,
67 Ebcdic1047,
72 Ebcdic1140,
76 Other(String),
79}
80
81impl Encoding {
82 pub fn name(&self) -> &str {
84 match self {
85 Encoding::Utf8 => "UTF-8",
86 Encoding::Ascii => "US-ASCII",
87 Encoding::Latin1 => "ISO-8859-1",
88 Encoding::Windows1252 => "windows-1252",
89 Encoding::Utf16Le => "UTF-16LE",
90 Encoding::Utf16Be => "UTF-16BE",
91 Encoding::Utf32Le => "UTF-32LE",
92 Encoding::Utf32Be => "UTF-32BE",
93 Encoding::Ebcdic037 => "IBM037",
94 Encoding::Ebcdic500 => "IBM500",
95 Encoding::Ebcdic1047 => "IBM1047",
96 Encoding::Ebcdic1140 => "IBM1140",
97 Encoding::Other(s) => s,
98 }
99 }
100}
101
102pub fn detect(bytes: &[u8]) -> Encoding {
115 if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
117 return Encoding::Utf8;
118 }
119 if bytes.starts_with(&[0x00, 0x00, 0xFE, 0xFF]) {
122 return Encoding::Utf32Be;
123 }
124 if bytes.starts_with(&[0xFF, 0xFE, 0x00, 0x00]) {
125 return Encoding::Utf32Le;
126 }
127 if bytes.starts_with(&[0xFE, 0xFF]) {
128 return Encoding::Utf16Be;
129 }
130 if bytes.starts_with(&[0xFF, 0xFE]) {
131 return Encoding::Utf16Le;
132 }
133
134 if bytes.starts_with(&[0x4C, 0x6F, 0xA7, 0x94]) {
150 let head_len = bytes.len().min(200);
151 let head_utf8 = transcode_single_byte(&bytes[..head_len], &IBM037_TO_UTF8);
152 if let Some(name) = read_xml_decl_encoding(&head_utf8) {
153 let parsed = encoding_from_name(&name);
154 if matches!(parsed,
155 Encoding::Ebcdic037 | Encoding::Ebcdic500
156 | Encoding::Ebcdic1047 | Encoding::Ebcdic1140)
157 {
158 return parsed;
159 }
160 }
161 return Encoding::Ebcdic037;
162 }
163
164 if bytes.len() >= 4 {
177 if bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0x00 && bytes[3] == 0x3C {
178 return Encoding::Utf32Be;
179 }
180 if bytes[0] == 0x3C && bytes[1] == 0x00 && bytes[2] == 0x00 && bytes[3] == 0x00 {
181 return Encoding::Utf32Le;
182 }
183 if bytes[0] == 0x00 && bytes[1] == 0x3C && bytes[2] == 0x00 && bytes[3] != 0x00 {
184 return Encoding::Utf16Be;
185 }
186 if bytes[0] == 0x3C && bytes[1] == 0x00 && bytes[2] != 0x00 && bytes[3] == 0x00 {
187 return Encoding::Utf16Le;
188 }
189 }
190
191 let head = &bytes[..bytes.len().min(200)];
193 if let Some(name) = read_xml_decl_encoding(head) {
194 return encoding_from_name(&name);
195 }
196
197 Encoding::Utf8
199}
200
201pub fn encoding_from_name(name: &str) -> Encoding {
204 let lower = name.to_ascii_lowercase();
205 match lower.as_str() {
206 "utf-8" | "utf8" => Encoding::Utf8,
207 "us-ascii" | "ascii" | "ansi_x3.4-1968" => Encoding::Ascii,
208 "iso-8859-1" | "iso_8859-1" | "latin1" | "latin-1" | "l1" | "8859-1"
209 => Encoding::Latin1,
210 "windows-1252" | "cp1252" | "cp-1252" | "1252"
211 => Encoding::Windows1252,
212 "utf-16le" | "utf16le" | "utf-16 le" => Encoding::Utf16Le,
213 "utf-16be" | "utf16be" | "utf-16 be" => Encoding::Utf16Be,
214 "utf-32le" | "utf32le" | "utf-32 le" | "ucs-4le" | "ucs4le" | "ucs-4-le"
215 => Encoding::Utf32Le,
216 "utf-32be" | "utf32be" | "utf-32 be" | "ucs-4be" | "ucs4be" | "ucs-4-be"
217 => Encoding::Utf32Be,
218 "ibm037" | "ibm-037" | "cp037" | "cp-037"
219 | "037" | "csibm037"
220 | "ebcdic-cp-us" | "ebcdic-cp-ca"
221 | "ebcdic-cp-wt" | "ebcdic-cp-nl" => Encoding::Ebcdic037,
222 "ibm500" | "ibm-500" | "cp500" | "cp-500"
223 | "500" | "csibm500"
224 | "ebcdic-cp-be" | "ebcdic-cp-ch" => Encoding::Ebcdic500,
225 "ibm1047" | "ibm-1047" | "cp1047" | "cp-1047"
226 | "1047" | "csibm1047" => Encoding::Ebcdic1047,
227 "ibm1140" | "ibm-1140" | "cp1140" | "cp-1140"
228 | "1140" | "csibm1140"
229 | "ibm01140" | "cp01140"
230 | "ebcdic-us-37+euro" => Encoding::Ebcdic1140,
231 _ => Encoding::Other(name.to_string()),
236 }
237}
238
239pub fn declared_encoding_name(bytes: &[u8]) -> Option<String> {
244 read_xml_decl_encoding(bytes)
245}
246
247fn read_xml_decl_encoding(head: &[u8]) -> Option<String> {
251 let start = find_bytes(head, b"<?xml")?;
252 let after = &head[start + 5..];
253 if !matches!(after.first()?, b' ' | b'\t' | b'\r' | b'\n') {
255 return None;
256 }
257 let end = find_bytes(after, b"?>").unwrap_or(after.len());
259 let decl = &after[..end];
260 let kw = find_bytes(decl, b"encoding")?;
261 let mut i = kw + 8;
262 while i < decl.len() && matches!(decl[i], b' ' | b'\t' | b'\r' | b'\n') { i += 1; }
263 if i >= decl.len() || decl[i] != b'=' { return None; }
264 i += 1;
265 while i < decl.len() && matches!(decl[i], b' ' | b'\t' | b'\r' | b'\n') { i += 1; }
266 let quote = match decl.get(i)? {
267 b'"' | b'\'' => decl[i],
268 _ => return None,
269 };
270 i += 1;
271 let val_start = i;
272 while i < decl.len() && decl[i] != quote { i += 1; }
273 if i >= decl.len() { return None; }
274 std::str::from_utf8(&decl[val_start..i]).ok().map(|s| s.to_string())
277}
278
279fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
280 if needle.is_empty() || haystack.len() < needle.len() { return None; }
281 haystack.windows(needle.len()).position(|w| w == needle)
282}
283
284pub fn transcode_to_utf8(bytes: &[u8]) -> Result<Cow<'_, [u8]>> {
296 let enc = detect(bytes);
297 transcode_to_utf8_as(bytes, enc)
298}
299
300pub fn transcode_to_utf8_strict(bytes: &[u8]) -> Result<Cow<'_, [u8]>> {
311 let enc = detect(bytes);
312 let transcoded = transcode_to_utf8_as(bytes, enc.clone())?;
313 verify_declaration_matches(&enc, &transcoded)?;
314 Ok(transcoded)
315}
316
317fn verify_declaration_matches(detected: &Encoding, transcoded: &[u8]) -> Result<()> {
320 let head = &transcoded[..transcoded.len().min(200)];
321 let declared = match read_xml_decl_encoding(head) {
322 Some(s) => s,
323 None => return Ok(()), };
325 if encodings_match(detected, &declared) {
326 return Ok(());
327 }
328 Err(XmlError::new(
329 ErrorDomain::Encoding,
330 ErrorLevel::Fatal,
331 format!(
332 "encoding declaration {declared:?} contradicts the encoding detected \
333 from the byte stream ({})",
334 detected.name(),
335 ),
336 ))
337}
338
339fn encodings_match(detected: &Encoding, declared: &str) -> bool {
342 let parsed = encoding_from_name(declared);
343 match (detected, &parsed) {
344 (a, b) if a == b => true,
345 (Encoding::Utf8, Encoding::Ascii) | (Encoding::Ascii, Encoding::Utf8) => true,
347 (Encoding::Utf16Le | Encoding::Utf16Be, Encoding::Other(s))
350 if s.eq_ignore_ascii_case("UTF-16") || s.eq_ignore_ascii_case("UTF16") => true,
351 (Encoding::Utf32Le | Encoding::Utf32Be, Encoding::Other(s))
354 if s.eq_ignore_ascii_case("UTF-32") || s.eq_ignore_ascii_case("UTF32")
355 || s.eq_ignore_ascii_case("UCS-4") || s.eq_ignore_ascii_case("UCS4") => true,
356 (Encoding::Other(a), Encoding::Other(b)) if a.eq_ignore_ascii_case(b) => true,
358 _ => false,
359 }
360}
361
362pub fn transcode_to_utf8_as(bytes: &[u8], encoding: Encoding) -> Result<Cow<'_, [u8]>> {
368 match encoding {
369 Encoding::Utf8 | Encoding::Ascii => Ok(Cow::Borrowed(strip_bom(bytes))),
370 Encoding::Latin1 => Ok(Cow::Owned(transcode_latin1(strip_bom(bytes)))),
371 Encoding::Windows1252 => Ok(Cow::Owned(transcode_windows1252(strip_bom(bytes)))),
372 Encoding::Utf16Le => Ok(Cow::Owned(transcode_utf16(strip_utf16_bom(bytes, false), false)?)),
373 Encoding::Utf16Be => Ok(Cow::Owned(transcode_utf16(strip_utf16_bom(bytes, true), true)?)),
374 Encoding::Utf32Le => Ok(Cow::Owned(transcode_utf32(strip_utf32_bom(bytes, false), false)?)),
375 Encoding::Utf32Be => Ok(Cow::Owned(transcode_utf32(strip_utf32_bom(bytes, true), true)?)),
376 Encoding::Ebcdic037 => Ok(Cow::Owned(transcode_single_byte(bytes, &IBM037_TO_UTF8))),
377 Encoding::Ebcdic500 => Ok(Cow::Owned(transcode_single_byte(bytes, &IBM500_TO_UTF8))),
378 Encoding::Ebcdic1047 => Ok(Cow::Owned(transcode_single_byte(bytes, &IBM1047_TO_UTF8))),
379 Encoding::Ebcdic1140 => Ok(Cow::Owned(transcode_single_byte(bytes, &IBM1140_TO_UTF8))),
380 Encoding::Other(name) => Ok(Cow::Owned(transcode_other(bytes, &name)?)),
381 }
382}
383
384pub const IBM037_TO_UNICODE: [u16; 256] = [
393 0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
395 0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
396 0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
398 0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
399 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
401 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
402 0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
404 0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
405 0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
407 0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
408 0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
410 0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
411 0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
413 0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
414 0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
416 0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
417 0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
419 0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
420 0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
422 0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
423 0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
425 0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
426 0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
428 0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
429 0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
431 0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
432 0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
434 0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
435 0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
437 0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
438 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
440 0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
441];
442
443pub const IBM1140_TO_UNICODE: [u16; 256] = {
452 let mut t = IBM037_TO_UNICODE;
453 t[0x9F] = 0x20AC; t
455};
456
457pub const IBM500_TO_UNICODE: [u16; 256] = {
467 let mut t = IBM037_TO_UNICODE;
468 t[0x4A] = 0x005B; t[0x4F] = 0x0021; t[0x5A] = 0x005D; t[0x5F] = 0x005E; t[0xB0] = 0x00A2; t[0xBA] = 0x00AC; t[0xBB] = 0x007C; t
477};
478
479pub const IBM1047_TO_UNICODE: [u16; 256] = {
490 let mut t = IBM037_TO_UNICODE;
491 t[0x15] = 0x000A; t[0x25] = 0x0085; t[0x4A] = 0x005B; t[0x4F] = 0x0021; t[0x5A] = 0x005D; t[0x5F] = 0x005E; t[0xB0] = 0x00A2; t[0xBA] = 0x00AC; t[0xBB] = 0x007C; t
503};
504
505
506type SingleByteUtf8Table = [[u8; 4]; 256];
514
515const fn build_utf8_table(unicode_table: &[u16; 256]) -> SingleByteUtf8Table {
520 let mut t = [[0u8; 4]; 256];
521 let mut i = 0;
522 while i < 256 {
523 let cp = unicode_table[i] as u32;
524 if cp < 0x80 {
525 t[i] = [1, cp as u8, 0, 0];
526 } else if cp < 0x800 {
527 t[i] = [
528 2,
529 0xC0 | (cp >> 6) as u8,
530 0x80 | (cp & 0x3F) as u8,
531 0,
532 ];
533 } else {
534 t[i] = [
535 3,
536 0xE0 | (cp >> 12) as u8,
537 0x80 | ((cp >> 6) & 0x3F) as u8,
538 0x80 | (cp & 0x3F) as u8,
539 ];
540 }
541 i += 1;
542 }
543 t
544}
545
546const IBM037_TO_UTF8: SingleByteUtf8Table = build_utf8_table(&IBM037_TO_UNICODE);
547const IBM500_TO_UTF8: SingleByteUtf8Table = build_utf8_table(&IBM500_TO_UNICODE);
548const IBM1047_TO_UTF8: SingleByteUtf8Table = build_utf8_table(&IBM1047_TO_UNICODE);
549const IBM1140_TO_UTF8: SingleByteUtf8Table = build_utf8_table(&IBM1140_TO_UNICODE);
550
551fn transcode_single_byte(bytes: &[u8], table: &SingleByteUtf8Table) -> Vec<u8> {
559 let mut out: Vec<u8> = Vec::with_capacity(bytes.len() * 3);
561 let ptr = out.as_mut_ptr();
562 let mut len = 0usize;
563
564 for &b in bytes {
565 let entry = table[b as usize];
566 unsafe {
571 ptr.add(len ).write(entry[1]);
572 ptr.add(len + 1).write(entry[2]);
573 ptr.add(len + 2).write(entry[3]);
574 }
575 len += entry[0] as usize;
576 }
577
578 unsafe { out.set_len(len); }
581 out
582}
583
584#[cfg(feature = "full-encodings")]
591fn transcode_other(bytes: &[u8], name: &str) -> Result<Vec<u8>> {
592 let enc = encoding_rs::Encoding::for_label(name.as_bytes())
593 .ok_or_else(|| XmlError::new(
594 ErrorDomain::Encoding,
595 ErrorLevel::Fatal,
596 format!("encoding {name:?} is not recognized by encoding_rs"),
597 ))?;
598 let (decoded, _had_errors) = enc.decode_without_bom_handling(bytes);
600 Ok(decoded.into_owned().into_bytes())
605}
606
607#[cfg(not(feature = "full-encodings"))]
608fn transcode_other(_bytes: &[u8], name: &str) -> Result<Vec<u8>> {
609 Err(XmlError::new(
610 ErrorDomain::Encoding,
611 ErrorLevel::Fatal,
612 format!(
613 "encoding {name:?} is not supported — rebuild sup-xml-core with \
614 the default 'full-encodings' feature enabled to pull in encoding_rs"
615 ),
616 ))
617}
618
619fn strip_bom(bytes: &[u8]) -> &[u8] {
621 if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) { &bytes[3..] } else { bytes }
622}
623
624fn strip_utf16_bom(bytes: &[u8], big_endian: bool) -> &[u8] {
626 let bom: [u8; 2] = if big_endian { [0xFE, 0xFF] } else { [0xFF, 0xFE] };
627 if bytes.starts_with(&bom) { &bytes[2..] } else { bytes }
628}
629
630fn strip_utf32_bom(bytes: &[u8], big_endian: bool) -> &[u8] {
632 let bom: [u8; 4] = if big_endian {
633 [0x00, 0x00, 0xFE, 0xFF]
634 } else {
635 [0xFF, 0xFE, 0x00, 0x00]
636 };
637 if bytes.starts_with(&bom) { &bytes[4..] } else { bytes }
638}
639
640fn transcode_utf16(bytes: &[u8], big_endian: bool) -> Result<Vec<u8>> {
642 if bytes.len() % 2 != 0 {
643 return Err(XmlError::new(
644 ErrorDomain::Encoding,
645 ErrorLevel::Fatal,
646 "UTF-16 input length must be even",
647 ));
648 }
649 let decode_u16 = |i: usize| -> u16 {
650 if big_endian {
651 u16::from_be_bytes([bytes[i], bytes[i + 1]])
652 } else {
653 u16::from_le_bytes([bytes[i], bytes[i + 1]])
654 }
655 };
656
657 let mut out = Vec::with_capacity(bytes.len());
661 let mut i = 0;
662 while i < bytes.len() {
663 let u = decode_u16(i);
664 i += 2;
665
666 let cp: u32 = if (0xD800..=0xDBFF).contains(&u) {
667 if i + 2 > bytes.len() {
669 return Err(XmlError::new(
670 ErrorDomain::Encoding,
671 ErrorLevel::Fatal,
672 "lone UTF-16 high surrogate at end of input",
673 ));
674 }
675 let low = decode_u16(i);
676 if !(0xDC00..=0xDFFF).contains(&low) {
677 return Err(XmlError::new(
678 ErrorDomain::Encoding,
679 ErrorLevel::Fatal,
680 format!("UTF-16 high surrogate U+{u:04X} not followed by low surrogate (got U+{low:04X})"),
681 ));
682 }
683 i += 2;
684 0x10000 + (((u as u32 - 0xD800) << 10) | (low as u32 - 0xDC00))
685 } else if (0xDC00..=0xDFFF).contains(&u) {
686 return Err(XmlError::new(
687 ErrorDomain::Encoding,
688 ErrorLevel::Fatal,
689 format!("lone UTF-16 low surrogate U+{u:04X}"),
690 ));
691 } else {
692 u as u32
693 };
694
695 encode_utf8_codepoint(cp, &mut out);
696 }
697 Ok(out)
698}
699
700fn transcode_utf32(bytes: &[u8], big_endian: bool) -> Result<Vec<u8>> {
707 if bytes.len() % 4 != 0 {
708 return Err(XmlError::new(
709 ErrorDomain::Encoding,
710 ErrorLevel::Fatal,
711 "UTF-32 input length must be a multiple of 4",
712 ));
713 }
714 let decode_u32 = |i: usize| -> u32 {
715 if big_endian {
716 u32::from_be_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]])
717 } else {
718 u32::from_le_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]])
719 }
720 };
721
722 let mut out = Vec::with_capacity(bytes.len());
727 let mut i = 0;
728 while i < bytes.len() {
729 let cp = decode_u32(i);
730 i += 4;
731
732 if cp > 0x10FFFF {
733 return Err(XmlError::new(
734 ErrorDomain::Encoding,
735 ErrorLevel::Fatal,
736 format!("UTF-32 code point U+{cp:08X} is outside the Unicode range"),
737 ));
738 }
739 if (0xD800..=0xDFFF).contains(&cp) {
740 return Err(XmlError::new(
741 ErrorDomain::Encoding,
742 ErrorLevel::Fatal,
743 format!("UTF-32 code point U+{cp:04X} is a surrogate (not a valid scalar)"),
744 ));
745 }
746 encode_utf8_codepoint(cp, &mut out);
747 }
748 Ok(out)
749}
750
751fn transcode_latin1(bytes: &[u8]) -> Vec<u8> {
765 const ASCII_MASK: u64 = 0x8080_8080_8080_8080;
766 let mut out = Vec::with_capacity(bytes.len() * 2);
768 let mut pos = 0;
769
770 while pos + 8 <= bytes.len() {
771 let chunk = u64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
774 let hi = chunk & ASCII_MASK;
775 if hi == 0 {
776 out.extend_from_slice(&bytes[pos..pos + 8]);
778 pos += 8;
779 } else {
780 let off = (hi.trailing_zeros() / 8) as usize;
782 if off > 0 {
783 out.extend_from_slice(&bytes[pos..pos + off]);
784 }
785 let b = bytes[pos + off];
786 out.push(0xC0 | (b >> 6));
787 out.push(0x80 | (b & 0x3F));
788 pos += off + 1;
789 }
790 }
791
792 while pos < bytes.len() {
794 let b = bytes[pos];
795 if b < 0x80 {
796 out.push(b);
797 } else {
798 out.push(0xC0 | (b >> 6));
799 out.push(0x80 | (b & 0x3F));
800 }
801 pos += 1;
802 }
803
804 out
805}
806
807const WIN1252_HI: [u32; 32] = [
813 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
814 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F,
815 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
816 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178,
817];
818
819fn transcode_windows1252(bytes: &[u8]) -> Vec<u8> {
826 const ASCII_MASK: u64 = 0x8080_8080_8080_8080;
827 let mut out = Vec::with_capacity(bytes.len() * 2);
829 let mut pos = 0;
830
831 #[inline]
832 fn emit_non_ascii(out: &mut Vec<u8>, b: u8) {
833 if (0x80..0xA0).contains(&b) {
834 let cp = WIN1252_HI[(b - 0x80) as usize];
835 if cp < 0x80 {
836 out.push(cp as u8);
837 } else if cp < 0x800 {
838 out.push(0xC0 | (cp >> 6) as u8);
839 out.push(0x80 | (cp & 0x3F) as u8);
840 } else {
841 out.push(0xE0 | (cp >> 12) as u8);
842 out.push(0x80 | ((cp >> 6) & 0x3F) as u8);
843 out.push(0x80 | (cp & 0x3F) as u8);
844 }
845 } else {
846 out.push(0xC0 | (b >> 6));
847 out.push(0x80 | (b & 0x3F));
848 }
849 }
850
851 while pos + 8 <= bytes.len() {
852 let chunk = u64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
853 let hi = chunk & ASCII_MASK;
854 if hi == 0 {
855 out.extend_from_slice(&bytes[pos..pos + 8]);
856 pos += 8;
857 } else {
858 let off = (hi.trailing_zeros() / 8) as usize;
859 if off > 0 {
860 out.extend_from_slice(&bytes[pos..pos + off]);
861 }
862 emit_non_ascii(&mut out, bytes[pos + off]);
863 pos += off + 1;
864 }
865 }
866
867 while pos < bytes.len() {
868 let b = bytes[pos];
869 if b < 0x80 {
870 out.push(b);
871 } else {
872 emit_non_ascii(&mut out, b);
873 }
874 pos += 1;
875 }
876
877 out
878}
879
880fn encode_utf8_codepoint(cp: u32, out: &mut Vec<u8>) {
883 if cp < 0x80 {
884 out.push(cp as u8);
885 } else if cp < 0x800 {
886 out.push(0xC0 | (cp >> 6) as u8);
887 out.push(0x80 | (cp & 0x3F) as u8);
888 } else if cp < 0x10000 {
889 out.push(0xE0 | (cp >> 12) as u8);
890 out.push(0x80 | ((cp >> 6) & 0x3F) as u8);
891 out.push(0x80 | (cp & 0x3F) as u8);
892 } else {
893 out.push(0xF0 | (cp >> 18) as u8);
894 out.push(0x80 | ((cp >> 12) & 0x3F) as u8);
895 out.push(0x80 | ((cp >> 6) & 0x3F) as u8);
896 out.push(0x80 | (cp & 0x3F) as u8);
897 }
898}
899
900#[cfg(test)]
903mod tests {
904 use super::*;
905
906 #[test]
907 fn detect_defaults_to_utf8() {
908 assert_eq!(detect(b"<r/>"), Encoding::Utf8);
909 }
910
911 #[test]
912 fn detect_utf8_bom() {
913 let bytes = [0xEF, 0xBB, 0xBF, b'<', b'r', b'/', b'>'];
914 assert_eq!(detect(&bytes), Encoding::Utf8);
915 }
916
917 #[test]
918 fn detect_utf16_bom() {
919 assert_eq!(detect(&[0xFE, 0xFF, 0, b'<']), Encoding::Utf16Be);
920 assert_eq!(detect(&[0xFF, 0xFE, b'<', 0]), Encoding::Utf16Le);
921 }
922
923 #[test]
924 fn detect_utf16_named_in_declaration() {
925 let be = br#"<?xml version="1.0" encoding="UTF-16BE"?><r/>"#;
926 let le = br#"<?xml version="1.0" encoding="UTF-16LE"?><r/>"#;
927 assert_eq!(detect(be), Encoding::Utf16Be);
928 assert_eq!(detect(le), Encoding::Utf16Le);
929 }
930
931 #[test]
932 fn detect_generic_utf16_no_bom_is_other() {
933 let bytes = br#"<?xml version="1.0" encoding="UTF-16"?><r/>"#;
936 assert!(matches!(detect(bytes), Encoding::Other(s) if s == "UTF-16"));
937 }
938
939 #[test]
940 fn transcode_utf16_le_with_bom() {
941 let bytes: &[u8] = &[
944 0xFF, 0xFE,
945 0x3C, 0x00, 0x72, 0x00, 0x2F, 0x00, 0x3E, 0x00,
946 ];
947 let out = transcode_to_utf8(bytes).unwrap();
948 assert_eq!(std::str::from_utf8(&out).unwrap(), "<r/>");
949 }
950
951 #[test]
952 fn transcode_utf16_be_with_bom() {
953 let bytes: &[u8] = &[
954 0xFE, 0xFF,
955 0x00, 0x3C, 0x00, 0x72, 0x00, 0x2F, 0x00, 0x3E,
956 ];
957 let out = transcode_to_utf8(bytes).unwrap();
958 assert_eq!(std::str::from_utf8(&out).unwrap(), "<r/>");
959 }
960
961 #[test]
962 fn transcode_utf16_le_surrogate_pair() {
963 let bytes: &[u8] = &[0xFF, 0xFE, 0x3D, 0xD8, 0x00, 0xDE];
966 let out = transcode_to_utf8(bytes).unwrap();
967 assert_eq!(&*out, &[0xF0, 0x9F, 0x98, 0x80]);
969 assert_eq!(std::str::from_utf8(&out).unwrap(), "😀");
970 }
971
972 #[test]
973 fn transcode_utf16_lone_high_surrogate_errors() {
974 let bytes: &[u8] = &[0xFF, 0xFE, 0x3D, 0xD8];
976 let err = transcode_to_utf8(bytes).unwrap_err();
977 assert!(err.message.contains("high surrogate"), "got: {:?}", err.message);
978 }
979
980 #[test]
981 fn transcode_utf16_lone_low_surrogate_errors() {
982 let bytes: &[u8] = &[0xFF, 0xFE, 0x00, 0xDE];
984 let err = transcode_to_utf8(bytes).unwrap_err();
985 assert!(err.message.contains("low surrogate"), "got: {:?}", err.message);
986 }
987
988 #[test]
989 fn transcode_utf16_odd_length_errors() {
990 let bytes: &[u8] = &[0xFF, 0xFE, 0x3C];
991 let err = transcode_to_utf8(bytes).unwrap_err();
992 assert!(err.message.contains("even"), "got: {:?}", err.message);
993 }
994
995 #[test]
996 fn detect_utf16_be_without_bom() {
997 let bytes: &[u8] = &[0x00, 0x3C, 0x00, 0x3F, 0x00, 0x78];
999 assert_eq!(detect(bytes), Encoding::Utf16Be);
1000 }
1001
1002 #[test]
1003 fn detect_utf16_le_without_bom() {
1004 let bytes: &[u8] = &[0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00];
1006 assert_eq!(detect(bytes), Encoding::Utf16Le);
1007 }
1008
1009 #[test]
1010 fn transcode_utf16_be_without_bom_resilient() {
1011 let bytes: &[u8] = &[0x00, 0x3C, 0x00, 0x72, 0x00, 0x2F, 0x00, 0x3E];
1014 let out = transcode_to_utf8(bytes).unwrap();
1015 assert_eq!(std::str::from_utf8(&out).unwrap(), "<r/>");
1016 }
1017
1018 #[test]
1019 fn detect_utf32_be_bom() {
1020 let bytes: &[u8] = &[0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, 0x3C];
1021 assert_eq!(detect(bytes), Encoding::Utf32Be);
1022 }
1023
1024 #[test]
1025 fn detect_utf32_le_bom() {
1026 let bytes: &[u8] = &[0xFF, 0xFE, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00];
1029 assert_eq!(detect(bytes), Encoding::Utf32Le);
1030 }
1031
1032 #[test]
1033 fn detect_utf32_be_without_bom() {
1034 let bytes: &[u8] = &[0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x3F];
1036 assert_eq!(detect(bytes), Encoding::Utf32Be);
1037 }
1038
1039 #[test]
1040 fn detect_utf32_le_without_bom() {
1041 let bytes: &[u8] = &[0x3C, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00];
1043 assert_eq!(detect(bytes), Encoding::Utf32Le);
1044 }
1045
1046 #[test]
1047 fn detect_utf32_named_in_declaration() {
1048 let be = br#"<?xml version="1.0" encoding="UTF-32BE"?><r/>"#;
1049 let le = br#"<?xml version="1.0" encoding="UTF-32LE"?><r/>"#;
1050 assert_eq!(detect(be), Encoding::Utf32Be);
1051 assert_eq!(detect(le), Encoding::Utf32Le);
1052 }
1053
1054 #[test]
1055 fn detect_ucs4_aliases() {
1056 let be = br#"<?xml version="1.0" encoding="UCS-4BE"?><r/>"#;
1057 let le = br#"<?xml version="1.0" encoding="UCS-4LE"?><r/>"#;
1058 assert_eq!(detect(be), Encoding::Utf32Be);
1059 assert_eq!(detect(le), Encoding::Utf32Le);
1060 }
1061
1062 #[test]
1063 fn detect_generic_utf32_no_bom_is_other() {
1064 let bytes = br#"<?xml version="1.0" encoding="UTF-32"?><r/>"#;
1067 assert!(matches!(detect(bytes), Encoding::Other(s) if s == "UTF-32"));
1068 }
1069
1070 #[test]
1071 fn transcode_utf32_le_with_bom() {
1072 let bytes: &[u8] = &[
1074 0xFF, 0xFE, 0x00, 0x00,
1075 0x3C, 0x00, 0x00, 0x00,
1076 0x72, 0x00, 0x00, 0x00,
1077 0x2F, 0x00, 0x00, 0x00,
1078 0x3E, 0x00, 0x00, 0x00,
1079 ];
1080 let out = transcode_to_utf8(bytes).unwrap();
1081 assert_eq!(std::str::from_utf8(&out).unwrap(), "<r/>");
1082 }
1083
1084 #[test]
1085 fn transcode_utf32_be_with_bom() {
1086 let bytes: &[u8] = &[
1087 0x00, 0x00, 0xFE, 0xFF,
1088 0x00, 0x00, 0x00, 0x3C,
1089 0x00, 0x00, 0x00, 0x72,
1090 0x00, 0x00, 0x00, 0x2F,
1091 0x00, 0x00, 0x00, 0x3E,
1092 ];
1093 let out = transcode_to_utf8(bytes).unwrap();
1094 assert_eq!(std::str::from_utf8(&out).unwrap(), "<r/>");
1095 }
1096
1097 #[test]
1098 fn transcode_utf32_be_without_bom_resilient() {
1099 let bytes: &[u8] = &[
1101 0x00, 0x00, 0x00, 0x3C,
1102 0x00, 0x00, 0x00, 0x72,
1103 0x00, 0x00, 0x00, 0x2F,
1104 0x00, 0x00, 0x00, 0x3E,
1105 ];
1106 let out = transcode_to_utf8(bytes).unwrap();
1107 assert_eq!(std::str::from_utf8(&out).unwrap(), "<r/>");
1108 }
1109
1110 #[test]
1111 fn transcode_utf32_smp_codepoint() {
1112 let bytes: &[u8] = &[0xFF, 0xFE, 0x00, 0x00, 0x00, 0xF6, 0x01, 0x00];
1115 let out = transcode_to_utf8(bytes).unwrap();
1116 assert_eq!(&*out, &[0xF0, 0x9F, 0x98, 0x80]);
1118 assert_eq!(std::str::from_utf8(&out).unwrap(), "😀");
1119 }
1120
1121 #[test]
1122 fn transcode_utf32_surrogate_errors() {
1123 let bytes: &[u8] = &[0xFF, 0xFE, 0x00, 0x00, 0x3D, 0xD8, 0x00, 0x00];
1125 let err = transcode_to_utf8(bytes).unwrap_err();
1126 assert!(err.message.contains("surrogate"), "got: {:?}", err.message);
1127 }
1128
1129 #[test]
1130 fn transcode_utf32_out_of_range_errors() {
1131 let bytes: &[u8] = &[0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00];
1133 let err = transcode_to_utf8(bytes).unwrap_err();
1134 assert!(err.message.contains("Unicode range"), "got: {:?}", err.message);
1135 }
1136
1137 #[test]
1138 fn transcode_utf32_misaligned_length_errors() {
1139 let bytes: &[u8] = &[0xFF, 0xFE, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x72, 0x00];
1141 let err = transcode_to_utf8(bytes).unwrap_err();
1142 assert!(err.message.contains("multiple of 4"), "got: {:?}", err.message);
1143 }
1144
1145 #[test]
1146 fn transcode_utf32_strips_bom_only_once() {
1147 let bytes: &[u8] = &[0xFF, 0xFE, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00];
1150 let out = transcode_to_utf8(bytes).unwrap();
1151 assert_eq!(out.as_ref(), b"<");
1152 }
1153
1154 #[test]
1155 fn detect_ebcdic_signature() {
1156 let bytes: &[u8] = &[0x4C, 0x6F, 0xA7, 0x94];
1158 assert_eq!(detect(bytes), Encoding::Ebcdic037);
1159 }
1160
1161 #[test]
1162 fn detect_ebcdic_named() {
1163 let bytes = br#"<?xml version="1.0" encoding="IBM037"?><r/>"#;
1164 assert_eq!(detect(bytes), Encoding::Ebcdic037);
1165 let bytes = br#"<?xml version="1.0" encoding="CP037"?><r/>"#;
1166 assert_eq!(detect(bytes), Encoding::Ebcdic037);
1167 let bytes = br#"<?xml version="1.0" encoding="EBCDIC-CP-US"?><r/>"#;
1168 assert_eq!(detect(bytes), Encoding::Ebcdic037);
1169 }
1170
1171 #[test]
1172 fn transcode_ebcdic_minimal_via_explicit_encoding() {
1173 let bytes: &[u8] = &[0x4C, 0x99, 0x61, 0x6E];
1177 let out = transcode_to_utf8_as(bytes, Encoding::Ebcdic037).unwrap();
1178 assert_eq!(std::str::from_utf8(&out).unwrap(), "<r/>");
1179 }
1180
1181 #[test]
1182 fn transcode_ebcdic_xml_declaration() {
1183 let bytes: &[u8] = &[
1196 0x4C, 0x6F, 0xA7, 0x94, 0x93, 0x40,
1197 0xA5, 0x85, 0x99, 0xA2, 0x89, 0x96, 0x95, 0x7E, 0x7D, 0xF1, 0x4B, 0xF0, 0x7D, 0x40,
1198 0x85, 0x95, 0x83, 0x96, 0x84, 0x89, 0x95, 0x87, 0x7E, 0x7D,
1199 0xC9, 0xC2, 0xD4, 0xF0, 0xF3, 0xF7, 0x7D, 0x6F, 0x6E,
1200 0x4C, 0x99, 0x6E,
1201 0x83, 0x81, 0x86, 0x51,
1202 0x4C, 0x61, 0x99, 0x6E,
1203 ];
1204 let out = transcode_to_utf8(bytes).unwrap();
1205 let s = std::str::from_utf8(&out).unwrap();
1206 assert!(s.contains("café"), "expected 'café' in output, got: {s:?}");
1207 assert!(s.contains("encoding='IBM037'"), "expected encoding decl preserved, got: {s:?}");
1208 }
1209
1210 #[test]
1213 fn ibm1140_differs_from_ibm037_at_byte_9f_only() {
1214 for i in 0..256 {
1218 if i == 0x9F {
1219 assert_eq!(IBM1140_TO_UNICODE[i], 0x20AC,
1220 "IBM1140 0x9F must map to Euro sign");
1221 assert_eq!(IBM037_TO_UNICODE[i], 0x00A4,
1222 "IBM037 0x9F must remain currency sign ¤");
1223 } else {
1224 assert_eq!(IBM1140_TO_UNICODE[i], IBM037_TO_UNICODE[i],
1225 "IBM1140 should match IBM037 at byte 0x{i:02X}");
1226 }
1227 }
1228 }
1229
1230 #[test]
1231 fn ibm500_seven_byte_punctuation_swap_from_ibm037() {
1232 let deltas: &[(u8, u16, u16)] = &[
1235 (0x4A, 0x00A2, 0x005B), (0x4F, 0x007C, 0x0021), (0x5A, 0x0021, 0x005D), (0x5F, 0x00AC, 0x005E), (0xB0, 0x005E, 0x00A2), (0xBA, 0x005B, 0x00AC), (0xBB, 0x005D, 0x007C), ];
1243 for &(byte, ibm037_cp, ibm500_cp) in deltas {
1244 assert_eq!(IBM037_TO_UNICODE[byte as usize], ibm037_cp);
1245 assert_eq!(IBM500_TO_UNICODE[byte as usize], ibm500_cp);
1246 }
1247 let changed: std::collections::HashSet<u8> = deltas.iter().map(|(b, _, _)| *b).collect();
1248 for i in 0..=255u8 {
1249 if !changed.contains(&i) {
1250 assert_eq!(IBM500_TO_UNICODE[i as usize], IBM037_TO_UNICODE[i as usize],
1251 "IBM500 should match IBM037 at byte 0x{i:02X}");
1252 }
1253 }
1254 }
1255
1256 #[test]
1257 fn ibm1047_swaps_lf_and_nel_in_addition_to_ibm500_deltas() {
1258 assert_eq!(IBM1047_TO_UNICODE[0x15], 0x000A, "IBM1047 0x15 = LF");
1260 assert_eq!(IBM1047_TO_UNICODE[0x25], 0x0085, "IBM1047 0x25 = NEL");
1261 assert_eq!(IBM037_TO_UNICODE [0x15], 0x0085, "IBM037 0x15 = NEL");
1262 assert_eq!(IBM037_TO_UNICODE [0x25], 0x000A, "IBM037 0x25 = LF");
1263 assert_eq!(IBM1047_TO_UNICODE[0x4A], IBM500_TO_UNICODE[0x4A]);
1265 assert_eq!(IBM1047_TO_UNICODE[0xBB], IBM500_TO_UNICODE[0xBB]);
1266 assert_eq!(IBM1047_TO_UNICODE[0xC1], IBM037_TO_UNICODE[0xC1]); assert_eq!(IBM1047_TO_UNICODE[0xF0], IBM037_TO_UNICODE[0xF0]); }
1270
1271 #[test]
1272 fn detect_ibm1140_via_declaration_after_ebcdic_signature() {
1273 let bytes: &[u8] = &[
1277 0x4C, 0x6F, 0xA7, 0x94, 0x93, 0x40, 0xA5, 0x85, 0x99, 0xA2, 0x89, 0x96, 0x95, 0x7E, 0x7D, 0xF1, 0x4B, 0xF0, 0x7D, 0x40, 0x85, 0x95, 0x83, 0x96, 0x84, 0x89, 0x95, 0x87, 0x7E, 0x7D, 0xC9, 0xC2, 0xD4, 0xF1, 0xF1, 0xF4, 0xF0, 0x7D, 0x6F, 0x6E, 0x4C, 0x99, 0x61, 0x6E, ];
1285 assert_eq!(detect(bytes), Encoding::Ebcdic1140);
1286 }
1287
1288 #[test]
1289 fn detect_ibm500_via_declaration_after_ebcdic_signature() {
1290 let bytes: &[u8] = &[
1291 0x4C, 0x6F, 0xA7, 0x94, 0x93, 0x40, 0xA5, 0x85, 0x99, 0xA2, 0x89, 0x96, 0x95, 0x7E, 0x7D, 0xF1, 0x4B, 0xF0, 0x7D, 0x40,
1294 0x85, 0x95, 0x83, 0x96, 0x84, 0x89, 0x95, 0x87, 0x7E, 0x7D, 0xC9, 0xC2, 0xD4, 0xF5, 0xF0, 0xF0, 0x7D, 0x6F, 0x6E,
1297 0x4C, 0x99, 0x61, 0x6E,
1298 ];
1299 assert_eq!(detect(bytes), Encoding::Ebcdic500);
1300 }
1301
1302 #[test]
1303 fn detect_ibm1047_via_declaration_after_ebcdic_signature() {
1304 let bytes: &[u8] = &[
1305 0x4C, 0x6F, 0xA7, 0x94, 0x93, 0x40,
1306 0xA5, 0x85, 0x99, 0xA2, 0x89, 0x96, 0x95, 0x7E, 0x7D,
1307 0xF1, 0x4B, 0xF0, 0x7D, 0x40,
1308 0x85, 0x95, 0x83, 0x96, 0x84, 0x89, 0x95, 0x87, 0x7E, 0x7D,
1309 0xC9, 0xC2, 0xD4, 0xF1, 0xF0, 0xF4, 0xF7, 0x7D, 0x6F, 0x6E,
1311 0x4C, 0x99, 0x61, 0x6E,
1312 ];
1313 assert_eq!(detect(bytes), Encoding::Ebcdic1047);
1314 }
1315
1316 #[test]
1317 fn ibm1140_euro_sign_round_trips() {
1318 let bytes: &[u8] = &[0x4C, 0x99, 0x6E, 0x9F, 0x4C, 0x61, 0x99, 0x6E];
1320 let out = transcode_to_utf8_as(bytes, Encoding::Ebcdic1140).unwrap();
1321 let s = std::str::from_utf8(&out).unwrap();
1322 assert_eq!(s, "<r>€</r>", "got: {s:?}");
1323 }
1324
1325 #[test]
1326 fn ibm1140_byte_9f_is_currency_under_ibm037() {
1327 let bytes: &[u8] = &[0x4C, 0x99, 0x6E, 0x9F, 0x4C, 0x61, 0x99, 0x6E];
1331 let out = transcode_to_utf8_as(bytes, Encoding::Ebcdic037).unwrap();
1332 let s = std::str::from_utf8(&out).unwrap();
1333 assert_eq!(s, "<r>¤</r>", "got: {s:?}");
1334 }
1335
1336 #[test]
1337 fn ibm500_left_bracket_round_trips() {
1338 let bytes: &[u8] = &[0x4A]; let out = transcode_to_utf8_as(bytes, Encoding::Ebcdic500).unwrap();
1341 assert_eq!(&*out, b"[", "IBM500 0x4A must decode to '['");
1342 let out2 = transcode_to_utf8_as(bytes, Encoding::Ebcdic037).unwrap();
1344 assert_eq!(std::str::from_utf8(&out2).unwrap(), "¢",
1345 "IBM037 0x4A must decode to ¢");
1346 }
1347
1348 #[test]
1349 fn ibm1047_lf_and_punctuation_round_trip() {
1350 let bytes: &[u8] = &[0x15, 0x4A, 0xBB];
1353 let out = transcode_to_utf8_as(bytes, Encoding::Ebcdic1047).unwrap();
1354 assert_eq!(&*out, b"\n[|", "IBM1047 line/bracket/pipe round-trip");
1355 let out2 = transcode_to_utf8_as(bytes, Encoding::Ebcdic037).unwrap();
1357 let s2 = std::str::from_utf8(&out2).unwrap();
1358 assert!(s2.starts_with("\u{0085}"), "IBM037 0x15 must be NEL (U+0085)");
1359 assert!(s2.contains('¢'), "IBM037 0x4A must be ¢");
1360 assert!(s2.ends_with(']'), "IBM037 0xBB must be ]");
1361 }
1362
1363 #[test]
1364 fn ibm1140_aliases_resolve() {
1365 for name in ["IBM1140", "ibm1140", "CP1140", "cp01140", "IBM01140",
1367 "csibm1140", "ebcdic-us-37+euro"]
1368 {
1369 let bytes = format!("<?xml version=\"1.0\" encoding=\"{name}\"?><r/>");
1370 assert_eq!(detect(bytes.as_bytes()), Encoding::Ebcdic1140,
1371 "{name} should resolve to Ebcdic1140");
1372 }
1373 }
1374
1375 #[test]
1376 fn detect_xml_decl_iso_8859_1() {
1377 let bytes = br#"<?xml version="1.0" encoding="ISO-8859-1"?><r/>"#;
1378 assert_eq!(detect(bytes), Encoding::Latin1);
1379 }
1380
1381 #[test]
1382 fn detect_xml_decl_windows_1252() {
1383 let bytes = br#"<?xml version="1.0" encoding="Windows-1252"?><r/>"#;
1384 assert_eq!(detect(bytes), Encoding::Windows1252);
1385 }
1386
1387 #[test]
1388 fn detect_xml_decl_us_ascii() {
1389 let bytes = br#"<?xml version="1.0" encoding="US-ASCII"?><r/>"#;
1390 assert_eq!(detect(bytes), Encoding::Ascii);
1391 }
1392
1393 #[test]
1394 fn detect_xml_decl_utf_8_explicit() {
1395 let bytes = br#"<?xml version="1.0" encoding="UTF-8"?><r/>"#;
1396 assert_eq!(detect(bytes), Encoding::Utf8);
1397 }
1398
1399 #[test]
1400 fn detect_xml_decl_single_quoted() {
1401 let bytes = br#"<?xml version='1.0' encoding='ISO-8859-1'?><r/>"#;
1402 assert_eq!(detect(bytes), Encoding::Latin1);
1403 }
1404
1405 #[test]
1406 fn detect_xml_decl_unknown_encoding_is_other() {
1407 let bytes = br#"<?xml version="1.0" encoding="ISO-8859-2"?><r/>"#;
1408 assert!(matches!(detect(bytes), Encoding::Other(s) if s == "ISO-8859-2"));
1409 }
1410
1411 #[test]
1412 fn transcode_utf8_is_zero_copy() {
1413 let bytes: &[u8] = b"<r>plain ascii</r>";
1414 let out = transcode_to_utf8(bytes).unwrap();
1415 assert!(matches!(out, Cow::Borrowed(_)));
1416 assert_eq!(out.as_ref(), bytes);
1417 }
1418
1419 #[test]
1420 fn transcode_latin1_caf_e_acute() {
1421 let bytes: &[u8] = b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><r>caf\xe9</r>";
1423 let out = transcode_to_utf8(bytes).unwrap();
1424 assert!(matches!(out, Cow::Owned(_)));
1425 let s = std::str::from_utf8(&out).expect("output is valid UTF-8");
1426 assert!(s.contains("café"), "expected 'café' in output, got: {s:?}");
1427 }
1428
1429 #[test]
1430 fn transcode_windows1252_ellipsis() {
1431 let bytes: &[u8] = b"<?xml version=\"1.0\" encoding=\"Windows-1252\"?><r>and\x85</r>";
1434 let out = transcode_to_utf8(bytes).unwrap();
1435 let s = std::str::from_utf8(&out).expect("output is valid UTF-8");
1436 assert!(s.contains("and…"), "expected 'and…' (U+2026 ellipsis), got: {s:?}");
1437 }
1438
1439 #[cfg(feature = "full-encodings")]
1442 #[test]
1443 fn transcode_other_encoding_decodes_via_encoding_rs() {
1444 let bytes: &[u8] = b"<?xml version=\"1.0\" encoding=\"GB2312\"?><r>\xD6\xD0</r>";
1446 let out = transcode_to_utf8(bytes).expect("encoding_rs decodes GB2312");
1447 let s = std::str::from_utf8(&out).expect("decoded bytes are valid UTF-8");
1448 assert!(s.contains("中"), "expected '中' (U+4E2D) in decoded text, got: {s:?}");
1449 }
1450
1451 #[cfg(not(feature = "full-encodings"))]
1452 #[test]
1453 fn transcode_other_encoding_errors_without_feature() {
1454 let bytes: &[u8] = b"<?xml version=\"1.0\" encoding=\"GB2312\"?><r/>";
1455 let err = transcode_to_utf8(bytes).unwrap_err();
1456 assert_eq!(err.domain, ErrorDomain::Encoding);
1457 assert!(err.message.contains("GB2312"), "expected error to mention GB2312, got: {:?}", err.message);
1458 }
1459
1460 #[cfg(feature = "full-encodings")]
1461 #[test]
1462 fn transcode_other_encoding_unknown_label_errors() {
1463 let bytes: &[u8] = b"<?xml version=\"1.0\" encoding=\"not-a-real-encoding-name\"?><r/>";
1464 let err = transcode_to_utf8(bytes).unwrap_err();
1465 assert_eq!(err.domain, ErrorDomain::Encoding);
1466 assert!(
1467 err.message.contains("not recognized"),
1468 "expected message to flag the label as unrecognized, got: {:?}", err.message,
1469 );
1470 }
1471
1472 #[cfg(feature = "full-encodings")]
1473 #[test]
1474 fn transcode_iso_8859_2_via_encoding_rs() {
1475 let bytes: &[u8] = b"<?xml version=\"1.0\" encoding=\"ISO-8859-2\"?><r>\xB1</r>";
1478 let out = transcode_to_utf8(bytes).expect("encoding_rs decodes Latin-2");
1479 let s = std::str::from_utf8(&out).unwrap();
1480 assert!(s.contains("ą"), "expected 'ą' (U+0105) in decoded text, got: {s:?}");
1481 }
1482
1483 #[cfg(feature = "full-encodings")]
1484 #[test]
1485 fn transcode_shift_jis_via_encoding_rs() {
1486 let bytes: &[u8] = b"<?xml version=\"1.0\" encoding=\"Shift_JIS\"?><r>\x82\xA0</r>";
1488 let out = transcode_to_utf8(bytes).expect("encoding_rs decodes Shift_JIS");
1489 let s = std::str::from_utf8(&out).unwrap();
1490 assert!(s.contains("あ"), "expected 'あ' (U+3042) in decoded text, got: {s:?}");
1491 }
1492
1493 #[test]
1494 fn transcode_strips_utf8_bom() {
1495 let bytes: &[u8] = &[0xEF, 0xBB, 0xBF, b'<', b'r', b'/', b'>'];
1496 let out = transcode_to_utf8(bytes).unwrap();
1497 assert_eq!(out.as_ref(), b"<r/>");
1498 }
1499
1500 #[test]
1501 fn transcode_to_utf8_as_explicit() {
1502 let bytes: &[u8] = b"caf\xe9";
1503 let out = transcode_to_utf8_as(bytes, Encoding::Latin1).unwrap();
1504 assert_eq!(std::str::from_utf8(&out).unwrap(), "café");
1505 }
1506}