use oxixml_encoding::{
ByteOrderGuess, DecodeError, Encoding, detect_bom, encode_from_utf8, sniff_xml_encoding,
};
fn decode_in_chunks(
encoding: Encoding,
bytes: &[u8],
chunk_size: usize,
) -> Result<String, DecodeError> {
assert!(chunk_size > 0, "chunk_size must be positive");
let mut decoder = encoding.new_decoder();
let mut out = String::new();
let mut offset = 0;
loop {
let end = (offset + chunk_size).min(bytes.len());
let last = end == bytes.len();
decoder.decode_to_string(&bytes[offset..end], &mut out, last)?;
offset = end;
if last {
break;
}
}
Ok(out)
}
fn assert_round_trips_at_every_chunk_size(text: &str, encoding: Encoding) {
let mut bytes = Vec::new();
encode_from_utf8(text, encoding, &mut bytes)
.unwrap_or_else(|e| panic!("{encoding}: {text:?} must be representable: {e}"));
for chunk_size in [1usize, 2, 3] {
let decoded = decode_in_chunks(encoding, &bytes, chunk_size).unwrap_or_else(|e| {
panic!("{encoding}: decode at chunk_size={chunk_size} failed: {e}")
});
assert_eq!(
decoded, text,
"{encoding}: mismatch at chunk_size={chunk_size}"
);
}
let whole = decode_in_chunks(encoding, &bytes, bytes.len().max(1))
.unwrap_or_else(|e| panic!("{encoding}: whole-buffer decode failed: {e}"));
assert_eq!(whole, text, "{encoding}: whole-buffer mismatch");
}
#[test]
fn utf8_chunk_boundary_torture() {
let fixture = "The quick brown fox ๐ฆ jumps over โ the lazy dog. ใใใซใกใฏ ๐๐ cafรฉ ๐๐ซ๐ฆ๐ ๐ฌ๐ก๐ข";
assert_round_trips_at_every_chunk_size(fixture, Encoding::Utf8);
}
#[test]
fn utf16_le_chunk_boundary_torture() {
let fixture = "The quick brown fox ๐ฆ jumps over โ the lazy dog. ใใใซใกใฏ ๐๐ cafรฉ ๐๐ซ๐ฆ๐ ๐ฌ๐ก๐ข";
assert_round_trips_at_every_chunk_size(fixture, Encoding::Utf16Le);
}
#[test]
fn utf16_be_chunk_boundary_torture() {
let fixture = "The quick brown fox ๐ฆ jumps over โ the lazy dog. ใใใซใกใฏ ๐๐ cafรฉ ๐๐ซ๐ฆ๐ ๐ฌ๐ก๐ข";
assert_round_trips_at_every_chunk_size(fixture, Encoding::Utf16Be);
}
#[test]
fn latin1_chunk_boundary_torture() {
let fixture = "Fa\u{E7}ade na\u{EF}ve r\u{E9}sum\u{E9} caf\u{E9} \u{BD} \u{A9} \u{DF} \u{C6}";
assert_round_trips_at_every_chunk_size(fixture, Encoding::Latin1);
}
#[test]
fn windows1252_chunk_boundary_torture() {
let fixture =
"\u{201C}Hello\u{201D} \u{2014} caf\u{E9} costs \u{20AC}3.50\u{2026} r\u{E9}sum\u{E9}";
assert_round_trips_at_every_chunk_size(fixture, Encoding::Windows1252);
}
#[test]
fn ascii_chunk_boundary_torture() {
let fixture = "Hello, World! 123 #$%^&*() The quick brown fox jumps over the lazy dog.";
assert_round_trips_at_every_chunk_size(fixture, Encoding::Ascii);
}
#[test]
fn legacy_single_byte_chunk_boundary_torture() {
let cases: &[(oxixml_encoding::Encoding, &str)] = &[
(
oxixml_encoding::Encoding::Iso8859_2,
"Za\u{017C}\u{00F3}\u{0142}\u{0107} g\u{0119}\u{015B}l\u{0105} ja\u{017A}\u{0144}",
),
(
oxixml_encoding::Encoding::Iso8859_5,
"\u{0417}\u{0434}\u{0440}\u{0430}\u{0432}\u{0441}\u{0442}\u{0432}\u{0443}\u{0439}\u{0442}\u{0435}",
),
(
oxixml_encoding::Encoding::Iso8859_7,
"\u{0393}\u{03B5}\u{03B9}\u{03AC} \u{03C3}\u{03B1}\u{03C2}",
),
(
oxixml_encoding::Encoding::Iso8859_9,
"\u{011E}\u{0130}\u{015E} T\u{00FC}rk\u{00E7}e",
),
(
oxixml_encoding::Encoding::Windows1251,
"\u{041F}\u{0440}\u{0438}\u{0432}\u{0435}\u{0442}",
),
(
oxixml_encoding::Encoding::Koi8R,
"\u{041F}\u{0440}\u{0438}\u{0432}\u{0435}\u{0442}",
),
(
oxixml_encoding::Encoding::Macintosh,
"Caf\u{00E9} na\u{00EF}ve",
),
];
for &(encoding, fixture) in cases {
assert_round_trips_at_every_chunk_size(fixture, encoding);
}
}
#[test]
fn shift_jis_chunk_boundary_torture() {
let fixture = "Hello, \u{4E16}\u{754C}! \u{65E5}\u{672C}\u{8A9E}\u{30C6}\u{30B9}\u{30C8} \u{FF71}\u{FF72}\u{FF73} 123";
assert_round_trips_at_every_chunk_size(fixture, Encoding::ShiftJis);
}
#[test]
fn euc_jp_chunk_boundary_torture() {
let fixture = "Hello, \u{4E16}\u{754C}! \u{65E5}\u{672C}\u{8A9E}\u{30C6}\u{30B9}\u{30C8} \u{FF71}\u{FF72}\u{FF73} 123";
assert_round_trips_at_every_chunk_size(fixture, Encoding::EucJp);
}
#[test]
fn iso_2022_jp_chunk_boundary_torture() {
let fixture = "Hello, \u{4E16}\u{754C}! \u{65E5}\u{672C}\u{8A9E}\u{30C6}\u{30B9}\u{30C8} end";
assert_round_trips_at_every_chunk_size(fixture, Encoding::Iso2022Jp);
}
#[test]
fn gbk_chunk_boundary_torture() {
let fixture = "Hello, \u{4E2D}\u{6587}! \u{4F60}\u{597D}\u{4E16}\u{754C} \u{20AC} 123";
assert_round_trips_at_every_chunk_size(fixture, Encoding::Gbk);
}
#[test]
fn gb18030_chunk_boundary_torture() {
let fixture =
"Hello, \u{4E2D}\u{6587}! \u{4F60}\u{597D}\u{4E16}\u{754C} \u{20AC} \u{20000} 123";
assert_round_trips_at_every_chunk_size(fixture, Encoding::Gb18030);
}
#[test]
fn big5_chunk_boundary_torture() {
let fixture = "Hello, \u{4E2D}\u{6587}! \u{4F60}\u{597D}\u{4E16}\u{754C} 123";
assert_round_trips_at_every_chunk_size(fixture, Encoding::Big5);
}
#[test]
fn euc_kr_chunk_boundary_torture() {
let fixture = "Hello, \u{D55C}\u{AD6D}\u{C5B4} \u{C548}\u{B155}\u{D558}\u{C138}\u{C694} 123";
assert_round_trips_at_every_chunk_size(fixture, Encoding::EucKr);
}
fn decode_in_chunks_expect_error(
encoding: Encoding,
bytes: &[u8],
chunk_size: usize,
) -> (DecodeError, usize) {
let mut decoder = encoding.new_decoder();
let mut out = String::new();
let mut offset = 0usize;
loop {
let end = (offset + chunk_size).min(bytes.len());
let last = end == bytes.len();
match decoder.decode_to_string(&bytes[offset..end], &mut out, last) {
Ok(_) => {
assert!(
!last,
"expected an error but the whole buffer decoded successfully"
);
offset = end;
}
Err(err) => return (err, offset),
}
}
}
#[test]
fn utf8_malformed_sequence_reports_correct_offset_regardless_of_chunking() {
let bytes: &[u8] = b"ok\x80rest";
for chunk_size in [1usize, 2, 3, 100] {
let (err, chunk_start) = decode_in_chunks_expect_error(Encoding::Utf8, bytes, chunk_size);
assert_eq!(chunk_start + err.offset, 2, "chunk_size={chunk_size}");
}
}
#[test]
fn ascii_disallowed_byte_reports_correct_offset_regardless_of_chunking() {
let bytes: &[u8] = b"abc\xFFdef";
for chunk_size in [1usize, 2, 3, 100] {
let (err, chunk_start) = decode_in_chunks_expect_error(Encoding::Ascii, bytes, chunk_size);
assert_eq!(chunk_start + err.offset, 3, "chunk_size={chunk_size}");
}
}
#[test]
fn utf16_unpaired_surrogate_is_rejected_regardless_of_chunking() {
let bytes: &[u8] = &[0x00, 0xD8, b'x', 0x00];
for chunk_size in [1usize, 2, 3, 100] {
decode_in_chunks(Encoding::Utf16Le, bytes, chunk_size)
.expect_err(&format!("chunk_size={chunk_size} must fail"));
}
}
#[test]
fn label_round_trip_for_every_encoding() {
for &encoding in Encoding::ALL {
assert_eq!(Encoding::for_label(encoding.name()), Some(encoding));
assert_eq!(
Encoding::for_label(&encoding.name().to_lowercase()),
Some(encoding)
);
assert_eq!(
Encoding::for_label(&encoding.name().to_uppercase()),
Some(encoding)
);
}
}
#[test]
fn bom_detection_matches_sniff_result_for_all_three_signatures() {
let cases: &[(&[u8], Encoding, usize)] = &[
(&[0xEF, 0xBB, 0xBF, b'<'], Encoding::Utf8, 3),
(&[0xFE, 0xFF, 0x00, b'<'], Encoding::Utf16Be, 2),
(&[0xFF, 0xFE, b'<', 0x00], Encoding::Utf16Le, 2),
];
for &(bytes, expected_encoding, expected_len) in cases {
let (encoding, len) = detect_bom(bytes).expect("BOM must be detected");
assert_eq!(encoding, expected_encoding);
assert_eq!(len, expected_len);
let sniffed = sniff_xml_encoding(bytes);
assert_eq!(sniffed.bom_len, expected_len);
}
}
#[test]
fn sniff_appendix_f_patterns_end_to_end() {
let result = sniff_xml_encoding(b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><r/>");
assert_eq!(result.byte_order_guess, Some(ByteOrderGuess::Utf8));
assert_eq!(result.best_effort_encoding(), Encoding::Latin1);
let mut utf16be = Vec::new();
for unit in "<?xml version=\"1.0\"?><r/>".encode_utf16() {
utf16be.extend_from_slice(&unit.to_be_bytes());
}
let result = sniff_xml_encoding(&utf16be);
assert_eq!(result.byte_order_guess, Some(ByteOrderGuess::Utf16Be));
assert_eq!(result.best_effort_encoding(), Encoding::Utf16Be);
let mut utf16le = Vec::new();
for unit in "<?xml version=\"1.0\"?><r/>".encode_utf16() {
utf16le.extend_from_slice(&unit.to_le_bytes());
}
let result = sniff_xml_encoding(&utf16le);
assert_eq!(result.byte_order_guess, Some(ByteOrderGuess::Utf16Le));
assert_eq!(result.best_effort_encoding(), Encoding::Utf16Le);
}
#[test]
fn lossy_decode_never_errors_on_arbitrary_bytes() {
let all_bytes: Vec<u8> = (0..=255).collect();
for &encoding in Encoding::ALL {
let mut decoder = encoding.new_decoder();
let mut out = String::new();
let mut total_consumed = 0usize;
for (i, byte) in all_bytes.iter().enumerate() {
let last = i + 1 == all_bytes.len();
total_consumed +=
decoder.decode_to_string_lossy(std::slice::from_ref(byte), &mut out, last);
}
assert_eq!(total_consumed, all_bytes.len(), "{encoding}");
}
}