#![allow(dead_code)]
use super::DocumentEncoding;
#[doc(hidden)]
pub trait EncodingEngine: Send + Sync + std::fmt::Debug {
fn encoding(&self) -> DocumentEncoding;
fn step(&self, bytes: &[u8], offset: usize, end: usize) -> usize;
fn step_backward(&self, _bytes: &[u8], offset: usize, start: usize) -> usize {
if offset > start {
1
} else {
0
}
}
fn next_line_start(&self, bytes: &[u8], file_len: usize, line_start: usize) -> usize;
fn count_columns_exact(&self, bytes: &[u8]) -> usize;
fn count_columns_bounded(&self, bytes: &[u8], max_cols: usize) -> usize;
fn advance_offset_by_text_units(
&self,
bytes: &[u8],
file_len: usize,
start: usize,
text_units: usize,
) -> usize;
fn trim_trailing_line_break(&self, bytes: &[u8], start: usize, end: usize) -> usize {
let mut end = end.min(bytes.len()).max(start);
if end > start && bytes[end - 1] == b'\n' {
end -= 1;
}
if end > start && bytes[end - 1] == b'\r' {
end -= 1;
}
end
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub struct Utf8Engine;
impl EncodingEngine for Utf8Engine {
fn encoding(&self) -> DocumentEncoding {
DocumentEncoding::utf8()
}
fn step(&self, bytes: &[u8], offset: usize, end: usize) -> usize {
super::utf8_step(bytes, offset, end)
}
fn step_backward(&self, bytes: &[u8], offset: usize, start: usize) -> usize {
let len = bytes.len();
let offset = offset.min(len);
if offset <= start {
return 0;
}
if offset >= start + 2 && bytes[offset - 1] == b'\n' && bytes[offset - 2] == b'\r' {
return 2;
}
let mut p = offset - 1;
while p > start && super::is_utf8_continuation(bytes[p]) {
p -= 1;
}
offset - p
}
fn next_line_start(&self, bytes: &[u8], file_len: usize, line_start: usize) -> usize {
super::search::next_line_start_exact(bytes, file_len, line_start)
}
fn count_columns_exact(&self, bytes: &[u8]) -> usize {
super::count_text_columns_exact(bytes)
}
fn count_columns_bounded(&self, bytes: &[u8], max_cols: usize) -> usize {
super::count_text_columns(bytes, max_cols)
}
fn advance_offset_by_text_units(
&self,
bytes: &[u8],
file_len: usize,
start: usize,
text_units: usize,
) -> usize {
super::advance_offset_by_text_units_in_bytes(bytes, file_len, start, text_units)
}
}
#[doc(hidden)]
pub const UTF8_ENGINE: &Utf8Engine = &Utf8Engine;
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub struct SingleByteEngine {
encoding: DocumentEncoding,
}
impl SingleByteEngine {
pub(crate) const fn new(encoding: DocumentEncoding) -> Self {
Self { encoding }
}
pub fn supports(encoding: DocumentEncoding) -> bool {
matches!(
encoding.name(),
"windows-1250"
| "windows-1251"
| "windows-1252"
| "windows-1253"
| "windows-1254"
| "windows-1255"
| "windows-1256"
| "windows-1257"
| "windows-1258"
| "windows-874"
| "ISO-8859-2"
| "ISO-8859-3"
| "ISO-8859-4"
| "ISO-8859-5"
| "ISO-8859-6"
| "ISO-8859-7"
| "ISO-8859-8"
| "ISO-8859-8-I"
| "ISO-8859-10"
| "ISO-8859-13"
| "ISO-8859-14"
| "ISO-8859-15"
| "ISO-8859-16"
| "KOI8-R"
| "KOI8-U"
| "IBM866"
| "macintosh"
| "x-mac-cyrillic"
)
}
}
impl EncodingEngine for SingleByteEngine {
fn encoding(&self) -> DocumentEncoding {
self.encoding
}
#[inline]
fn step(&self, _bytes: &[u8], offset: usize, end: usize) -> usize {
if offset >= end {
0
} else {
1
}
}
#[inline]
fn step_backward(&self, bytes: &[u8], offset: usize, start: usize) -> usize {
if offset <= start {
return 0;
}
let len = bytes.len();
let offset = offset.min(len);
if offset <= start {
return 0;
}
if offset >= start + 2 && bytes[offset - 1] == b'\n' && bytes[offset - 2] == b'\r' {
return 2;
}
1
}
fn next_line_start(&self, bytes: &[u8], file_len: usize, line_start: usize) -> usize {
super::search::next_line_start_exact(bytes, file_len, line_start)
}
fn count_columns_exact(&self, bytes: &[u8]) -> usize {
match memchr::memchr2(b'\n', b'\r', bytes) {
Some(idx) => idx,
None => bytes.len(),
}
}
fn count_columns_bounded(&self, bytes: &[u8], max_cols: usize) -> usize {
let scan_end = max_cols.min(bytes.len());
match memchr::memchr2(b'\n', b'\r', &bytes[..scan_end]) {
Some(idx) => idx,
None => scan_end,
}
}
fn advance_offset_by_text_units(
&self,
bytes: &[u8],
file_len: usize,
start: usize,
text_units: usize,
) -> usize {
let start = start.min(file_len);
if text_units == 0 || start >= file_len {
return start;
}
let mut remaining = text_units;
let mut offset = start;
let mut pending_cr = false;
while offset < file_len && (remaining > 0 || pending_cr) {
if pending_cr {
pending_cr = false;
if bytes[offset] == b'\n' {
offset += 1;
continue;
}
}
if remaining == 0 {
break;
}
match bytes[offset] {
b'\r' => {
remaining -= 1;
offset += 1;
pending_cr = true;
}
_ => {
remaining -= 1;
offset += 1;
}
}
}
offset.min(file_len)
}
}
#[doc(hidden)]
pub fn engine_for_encoding(encoding: DocumentEncoding) -> &'static dyn EncodingEngine {
if SingleByteEngine::supports(encoding) {
return single_byte_engine_for(encoding);
}
if let Some(engine) = utf16::utf16_engine_for(encoding) {
return engine;
}
if let Some(engine) = multibyte::multibyte_engine_for(encoding) {
return engine;
}
UTF8_ENGINE
}
fn single_byte_engine_for(encoding: DocumentEncoding) -> &'static dyn EncodingEngine {
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
static CACHE: OnceLock<Mutex<HashMap<&'static str, &'static SingleByteEngine>>> =
OnceLock::new();
let map = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let mut guard = map.lock().expect("encoding-engine cache poisoned");
let name: &'static str = encoding.name();
*guard
.entry(name)
.or_insert_with(|| Box::leak(Box::new(SingleByteEngine::new(encoding))))
}
pub(crate) mod utf16 {
use super::{DocumentEncoding, EncodingEngine};
use std::marker::PhantomData;
use std::sync::OnceLock;
pub(crate) trait Endian: Send + Sync + std::fmt::Debug + Copy + 'static {
const NAME: &'static str;
const LF: [u8; 2];
const CR: [u8; 2];
fn read_u16(bytes: &[u8; 2]) -> u16;
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct LittleEndian;
#[derive(Debug, Clone, Copy)]
pub(crate) struct BigEndian;
impl Endian for LittleEndian {
const NAME: &'static str = "UTF-16LE";
const LF: [u8; 2] = [0x0A, 0x00];
const CR: [u8; 2] = [0x0D, 0x00];
#[inline]
fn read_u16(bytes: &[u8; 2]) -> u16 {
u16::from_le_bytes(*bytes)
}
}
impl Endian for BigEndian {
const NAME: &'static str = "UTF-16BE";
const LF: [u8; 2] = [0x00, 0x0A];
const CR: [u8; 2] = [0x00, 0x0D];
#[inline]
fn read_u16(bytes: &[u8; 2]) -> u16 {
u16::from_be_bytes(*bytes)
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Utf16Engine<E: Endian>(PhantomData<E>);
impl<E: Endian> Utf16Engine<E> {
pub(crate) const fn new() -> Self {
Self(PhantomData)
}
}
impl<E: Endian> EncodingEngine for Utf16Engine<E> {
fn encoding(&self) -> DocumentEncoding {
DocumentEncoding::from_label(E::NAME)
.unwrap_or_else(|| panic!("encoding_rs should know {}", E::NAME))
}
fn step(&self, bytes: &[u8], offset: usize, end: usize) -> usize {
let end = end.min(bytes.len());
if offset >= end {
return 0;
}
let remaining = end - offset;
if remaining < 2 {
return 0;
}
let unit0 = E::read_u16(&[bytes[offset], bytes[offset + 1]]);
if (0xD800..=0xDBFF).contains(&unit0) && remaining >= 4 {
let unit1 = E::read_u16(&[bytes[offset + 2], bytes[offset + 3]]);
if (0xDC00..=0xDFFF).contains(&unit1) {
return 4;
}
}
2
}
fn step_backward(&self, bytes: &[u8], offset: usize, start: usize) -> usize {
let len = bytes.len();
let offset = offset.min(len);
if offset.saturating_sub(start) < 2 {
return 0;
}
let p_last = offset - 2;
if p_last + 1 < len {
let unit_last = E::read_u16(&[bytes[p_last], bytes[p_last + 1]]);
if (0xDC00..=0xDFFF).contains(&unit_last) && offset.saturating_sub(start) >= 4 {
let p_prev = offset - 4;
let unit_prev = E::read_u16(&[bytes[p_prev], bytes[p_prev + 1]]);
if (0xD800..=0xDBFF).contains(&unit_prev) {
return 4;
}
}
}
2
}
fn next_line_start(&self, bytes: &[u8], file_len: usize, line_start: usize) -> usize {
let file_len = file_len.min(bytes.len());
let mut p = if line_start & 1 == 1 {
line_start.saturating_add(1)
} else {
line_start
};
while p + 1 < file_len {
let unit = [bytes[p], bytes[p + 1]];
if unit == E::LF {
return (p + 2).min(file_len);
}
if unit == E::CR {
let q = p + 2;
if q + 1 < file_len {
let next = [bytes[q], bytes[q + 1]];
if next == E::LF {
return (q + 2).min(file_len);
}
}
return (p + 2).min(file_len);
}
p += 2;
}
file_len
}
fn count_columns_exact(&self, bytes: &[u8]) -> usize {
self.count_columns_bounded(bytes, usize::MAX)
}
fn count_columns_bounded(&self, bytes: &[u8], max_cols: usize) -> usize {
let len = bytes.len();
let mut p = 0usize;
let mut cols = 0usize;
while cols < max_cols && p + 1 < len {
let unit = [bytes[p], bytes[p + 1]];
if unit == E::LF || unit == E::CR {
return cols;
}
let step = self.step(bytes, p, len);
if step == 0 {
break;
}
p += step;
cols += 1;
}
cols
}
fn advance_offset_by_text_units(
&self,
bytes: &[u8],
file_len: usize,
start: usize,
text_units: usize,
) -> usize {
let file_len = file_len.min(bytes.len());
let mut p = if start & 1 == 1 {
start.saturating_add(1)
} else {
start
};
if p >= file_len || text_units == 0 {
return p.min(file_len);
}
let mut remaining = text_units;
while remaining > 0 && p + 1 < file_len {
let unit = [bytes[p], bytes[p + 1]];
let step = self.step(bytes, p, file_len);
if step == 0 {
break;
}
p += step;
remaining -= 1;
if unit == E::CR && p + 1 < file_len {
let next = [bytes[p], bytes[p + 1]];
if next == E::LF {
p += 2;
}
}
}
p.min(file_len)
}
fn trim_trailing_line_break(&self, bytes: &[u8], start: usize, end: usize) -> usize {
let len = bytes.len();
let mut end = end.min(len).max(start);
if end.saturating_sub(start) < 2 || (end - start) & 1 == 1 {
return end;
}
let cell = [bytes[end - 2], bytes[end - 1]];
if cell == E::LF {
end -= 2;
if end.saturating_sub(start) >= 2 {
let prev = [bytes[end - 2], bytes[end - 1]];
if prev == E::CR {
end -= 2;
}
}
} else if cell == E::CR {
end -= 2;
}
end
}
}
pub(super) fn utf16_engine_for(
encoding: DocumentEncoding,
) -> Option<&'static dyn EncodingEngine> {
static LE: OnceLock<Utf16Engine<LittleEndian>> = OnceLock::new();
static BE: OnceLock<Utf16Engine<BigEndian>> = OnceLock::new();
match encoding.name() {
"UTF-16LE" => Some(LE.get_or_init(Utf16Engine::<LittleEndian>::new)),
"UTF-16BE" => Some(BE.get_or_init(Utf16Engine::<BigEndian>::new)),
_ => None,
}
}
}
pub(crate) mod multibyte {
use super::{DocumentEncoding, EncodingEngine};
use std::sync::OnceLock;
pub(super) const APPROX_LINE_BACKTRACK_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CjkKind {
ShiftJis,
Gb18030,
EucKr,
}
impl CjkKind {
const fn label(self) -> &'static str {
match self {
CjkKind::ShiftJis => "Shift_JIS",
CjkKind::Gb18030 => "gb18030",
CjkKind::EucKr => "EUC-KR",
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct MultiByteEngine {
kind: CjkKind,
encoding: DocumentEncoding,
}
impl MultiByteEngine {
pub(crate) fn new(kind: CjkKind) -> Self {
let encoding = DocumentEncoding::from_label(kind.label())
.unwrap_or_else(|| panic!("encoding_rs should know {}", kind.label()));
Self { kind, encoding }
}
#[inline]
pub(crate) fn char_len(&self, bytes: &[u8], offset: usize, end: usize) -> usize {
let end = end.min(bytes.len());
if offset >= end {
return 0;
}
let remaining = end - offset;
let b = bytes[offset];
match self.kind {
CjkKind::ShiftJis => match b {
0x81..=0x9F | 0xE0..=0xFC if remaining >= 2 => 2,
_ => 1,
},
CjkKind::Gb18030 => match b {
0x00..=0x7F => 1,
0x81..=0xFE if remaining >= 2 => {
let t = bytes[offset + 1];
match t {
0x40..=0x7E | 0x80..=0xFE => 2,
0x30..=0x39 if remaining >= 4 => 4,
_ => 1,
}
}
_ => 1,
},
CjkKind::EucKr => match b {
0x81..=0xFE if remaining >= 2 => 2,
_ => 1,
},
}
}
}
impl EncodingEngine for MultiByteEngine {
fn encoding(&self) -> DocumentEncoding {
self.encoding
}
fn step(&self, bytes: &[u8], offset: usize, end: usize) -> usize {
self.char_len(bytes, offset, end)
}
fn step_backward(&self, bytes: &[u8], offset: usize, start: usize) -> usize {
let offset = offset.min(bytes.len());
if offset <= start {
return 0;
}
let start = start.min(offset);
let scan_floor = offset
.saturating_sub(APPROX_LINE_BACKTRACK_BYTES)
.max(start);
let window = &bytes[scan_floor..offset];
let anchor = match window.iter().rposition(|b| matches!(*b, b'\n' | b'\r')) {
Some(rel) => {
let idx = scan_floor + rel;
if bytes[idx] == b'\r' && idx + 1 < offset && bytes[idx + 1] == b'\n' {
idx + 2
} else {
idx + 1
}
}
None => scan_floor, };
if anchor == offset {
return 1;
}
let mut cursor = anchor;
let mut last_step = 0usize;
while cursor < offset {
let step = self.char_len(bytes, cursor, offset);
if step == 0 {
return 1;
}
last_step = step;
cursor += step;
}
if cursor == offset && last_step > 0 {
last_step
} else {
1
}
}
fn next_line_start(&self, bytes: &[u8], file_len: usize, line_start: usize) -> usize {
let file_len = file_len.min(bytes.len());
let mut p = line_start.min(file_len);
while p < file_len {
let step = self.char_len(bytes, p, file_len);
if step == 0 {
break;
}
if step == 1 {
let b = bytes[p];
if b == b'\n' {
return (p + 1).min(file_len);
}
if b == b'\r' {
let q = p + 1;
if q < file_len {
let next_step = self.char_len(bytes, q, file_len);
if next_step == 1 && bytes[q] == b'\n' {
return (q + 1).min(file_len);
}
}
return (p + 1).min(file_len);
}
}
p += step;
}
file_len
}
fn count_columns_exact(&self, bytes: &[u8]) -> usize {
self.count_columns_bounded(bytes, usize::MAX)
}
fn count_columns_bounded(&self, bytes: &[u8], max_cols: usize) -> usize {
let len = bytes.len();
let mut p = 0usize;
let mut cols = 0usize;
while cols < max_cols && p < len {
let step = self.char_len(bytes, p, len);
if step == 0 {
break;
}
if step == 1 {
let b = bytes[p];
if b == b'\n' || b == b'\r' {
return cols;
}
}
p += step;
cols += 1;
}
cols
}
fn advance_offset_by_text_units(
&self,
bytes: &[u8],
file_len: usize,
start: usize,
text_units: usize,
) -> usize {
let file_len = file_len.min(bytes.len());
let mut p = start.min(file_len);
if p >= file_len || text_units == 0 {
return p;
}
let mut remaining = text_units;
while remaining > 0 && p < file_len {
let step = self.char_len(bytes, p, file_len);
if step == 0 {
break;
}
let was_cr = step == 1 && bytes[p] == b'\r';
p += step;
remaining -= 1;
if was_cr && p < file_len {
let next_step = self.char_len(bytes, p, file_len);
if next_step == 1 && bytes[p] == b'\n' {
p += 1;
}
}
}
p.min(file_len)
}
}
pub(super) fn multibyte_engine_for(
encoding: DocumentEncoding,
) -> Option<&'static dyn EncodingEngine> {
static SJIS: OnceLock<MultiByteEngine> = OnceLock::new();
static GB: OnceLock<MultiByteEngine> = OnceLock::new();
static EUCK: OnceLock<MultiByteEngine> = OnceLock::new();
match encoding.name() {
"Shift_JIS" => Some(SJIS.get_or_init(|| MultiByteEngine::new(CjkKind::ShiftJis))),
"gb18030" => Some(GB.get_or_init(|| MultiByteEngine::new(CjkKind::Gb18030))),
"EUC-KR" => Some(EUCK.get_or_init(|| MultiByteEngine::new(CjkKind::EucKr))),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn utf8_engine_step_matches_free_function_for_ascii() {
let bytes = b"abc\n";
let engine = Utf8Engine;
for i in 0..bytes.len() {
assert_eq!(
engine.step(bytes, i, bytes.len()),
super::super::utf8_step(bytes, i, bytes.len()),
"step disagreement at offset {i}",
);
}
}
#[test]
fn utf8_engine_step_matches_free_function_for_multibyte() {
let bytes = "А".as_bytes();
let engine = Utf8Engine;
assert_eq!(engine.step(bytes, 0, bytes.len()), 2);
}
#[test]
fn utf8_engine_next_line_start_handles_crlf() {
let bytes = b"line1\r\nline2\n";
let engine = Utf8Engine;
let after_first = engine.next_line_start(bytes, bytes.len(), 0);
assert_eq!(after_first, 7, "CRLF must collapse to a single 2-byte step");
}
#[test]
fn utf8_engine_count_columns_exact_excludes_trailing_newline() {
let bytes = b"abc\n";
let engine = Utf8Engine;
assert_eq!(engine.count_columns_exact(b"abc"), 3);
assert_eq!(engine.count_columns_exact(bytes), 3);
}
#[test]
fn utf8_engine_count_columns_bounded_caps_at_max_cols() {
let bytes = b"abcdef";
let engine = Utf8Engine;
assert_eq!(engine.count_columns_bounded(bytes, 3), 3);
assert_eq!(engine.count_columns_bounded(bytes, 100), 6);
}
#[test]
fn utf8_engine_advance_by_text_units_handles_crlf() {
let bytes = b"a\r\nb";
let engine = Utf8Engine;
let after = engine.advance_offset_by_text_units(bytes, bytes.len(), 0, 2);
assert_eq!(after, 3);
}
fn cp1251_engine() -> SingleByteEngine {
let encoding =
DocumentEncoding::from_label("windows-1251").expect("windows-1251 is a known encoding");
SingleByteEngine::new(encoding)
}
#[test]
fn single_byte_engine_supports_known_class_a_encodings() {
for label in [
"windows-1251",
"windows-1252",
"ISO-8859-15",
"KOI8-R",
"IBM866",
] {
let encoding = DocumentEncoding::from_label(label)
.unwrap_or_else(|| panic!("encoding_rs should know {label}"));
assert!(
SingleByteEngine::supports(encoding),
"{label} should be supported"
);
}
for label in ["UTF-8", "UTF-16LE", "UTF-16BE", "Shift_JIS", "GB18030"] {
let encoding = DocumentEncoding::from_label(label)
.unwrap_or_else(|| panic!("encoding_rs should know {label}"));
assert!(
!SingleByteEngine::supports(encoding),
"{label} must not be claimed by SingleByteEngine"
);
}
}
#[test]
fn single_byte_engine_step_is_one_byte_per_unit() {
let bytes = [0xC0u8, 0xC1, 0xC2, b'\n'];
let engine = cp1251_engine();
for i in 0..bytes.len() {
assert_eq!(engine.step(&bytes, i, bytes.len()), 1, "step at {i}");
}
assert_eq!(engine.step(&bytes, bytes.len(), bytes.len()), 0);
}
#[test]
fn single_byte_engine_next_line_start_collapses_crlf() {
let bytes = b"\xC0\xC1\r\n\xC2\xC3\n";
let engine = cp1251_engine();
assert_eq!(
engine.next_line_start(bytes, bytes.len(), 0),
4,
"CRLF after two cp1251 bytes must skip both"
);
assert_eq!(
engine.next_line_start(bytes, bytes.len(), 4),
7,
"trailing LF on line 2"
);
}
#[test]
fn single_byte_engine_count_columns_exact_excludes_newline() {
let bytes = b"\xC0\xC1\xC2\n";
let engine = cp1251_engine();
assert_eq!(engine.count_columns_exact(bytes), 3);
assert_eq!(engine.count_columns_exact(b"\xC0\xC1"), 2);
}
#[test]
fn single_byte_engine_count_columns_bounded_caps_at_max_cols() {
let bytes = b"\xC0\xC1\xC2\xC3\xC4";
let engine = cp1251_engine();
assert_eq!(engine.count_columns_bounded(bytes, 3), 3);
assert_eq!(engine.count_columns_bounded(bytes, 100), 5);
let bytes_nl = b"\xC0\xC1\n\xC3\xC4";
assert_eq!(engine.count_columns_bounded(bytes_nl, 100), 2);
}
#[test]
fn single_byte_engine_advance_by_text_units_handles_crlf() {
let bytes = b"\xC0\r\n\xC1";
let engine = cp1251_engine();
let after = engine.advance_offset_by_text_units(bytes, bytes.len(), 0, 2);
assert_eq!(after, 3);
}
#[test]
fn single_byte_engine_advance_treats_high_bytes_as_one_unit() {
let bytes = b"\xC0\xC1\xC2\xC3\xC4";
let engine = cp1251_engine();
let after = engine.advance_offset_by_text_units(bytes, bytes.len(), 0, 4);
assert_eq!(after, 4);
}
#[test]
fn utf8_engine_step_backward_walks_to_char_boundary() {
let bytes = "А".as_bytes();
let engine = Utf8Engine;
assert_eq!(engine.step_backward(bytes, bytes.len(), 0), 2);
let bytes = b"abc";
assert_eq!(engine.step_backward(bytes, 3, 0), 1);
assert_eq!(engine.step_backward(bytes, 0, 0), 0);
}
#[test]
fn utf8_engine_step_backward_collapses_crlf() {
let bytes = b"a\r\nb";
let engine = Utf8Engine;
assert_eq!(engine.step_backward(bytes, 3, 0), 2);
}
#[test]
fn single_byte_engine_step_backward_is_one_byte_per_unit() {
let bytes = [0xC0u8, 0xC1, 0xC2];
let engine = cp1251_engine();
assert_eq!(engine.step_backward(&bytes, 0, 0), 0);
for i in 1..=bytes.len() {
assert_eq!(
engine.step_backward(&bytes, i, 0),
1,
"step_backward at {i}"
);
}
}
#[test]
fn single_byte_engine_step_backward_collapses_crlf() {
let bytes = b"\xC0\r\n\xC1";
let engine = cp1251_engine();
assert_eq!(engine.step_backward(bytes, 3, 0), 2);
let cr_only = b"\xC0\r\xC1";
assert_eq!(engine.step_backward(cr_only, 2, 0), 1);
}
fn utf16le_engine() -> &'static dyn EncodingEngine {
let encoding =
DocumentEncoding::from_label("UTF-16LE").expect("UTF-16LE is a known encoding");
engine_for_encoding(encoding)
}
fn utf16be_engine() -> &'static dyn EncodingEngine {
let encoding =
DocumentEncoding::from_label("UTF-16BE").expect("UTF-16BE is a known encoding");
engine_for_encoding(encoding)
}
#[test]
fn utf16_le_step_distinguishes_bmp_from_supplementary() {
let bytes = [0x41, 0x00, 0x01, 0xD8, 0x37, 0xDC, 0x42, 0x00];
let engine = utf16le_engine();
assert_eq!(engine.step(&bytes, 0, bytes.len()), 2, "BMP 'A'");
assert_eq!(engine.step(&bytes, 2, bytes.len()), 4, "supplementary pair");
assert_eq!(engine.step(&bytes, 6, bytes.len()), 2, "BMP 'B'");
assert_eq!(engine.step(&bytes, bytes.len(), bytes.len()), 0, "at end");
}
#[test]
fn utf16_le_step_handles_lone_high_surrogate() {
let bytes = [0x01, 0xD8, 0x41, 0x00];
let engine = utf16le_engine();
assert_eq!(engine.step(&bytes, 0, bytes.len()), 2);
assert_eq!(engine.step(&bytes, 0, 1), 0);
}
#[test]
fn utf16_be_step_distinguishes_bmp_from_supplementary() {
let bytes = [0x00, 0x41, 0xD8, 0x01, 0xDC, 0x37, 0x00, 0x42];
let engine = utf16be_engine();
assert_eq!(engine.step(&bytes, 0, bytes.len()), 2);
assert_eq!(engine.step(&bytes, 2, bytes.len()), 4);
assert_eq!(engine.step(&bytes, 6, bytes.len()), 2);
}
#[test]
fn utf16_le_step_backward_distinguishes_bmp_from_supplementary() {
let bytes = [0x41, 0x00, 0x01, 0xD8, 0x37, 0xDC, 0x42, 0x00];
let engine = utf16le_engine();
assert_eq!(engine.step_backward(&bytes, 8, 0), 2, "back over 'B'");
assert_eq!(engine.step_backward(&bytes, 6, 0), 4, "back over pair");
assert_eq!(engine.step_backward(&bytes, 2, 0), 2, "back over 'A'");
assert_eq!(engine.step_backward(&bytes, 0, 0), 0, "at start");
}
#[test]
fn utf16_be_step_backward_distinguishes_bmp_from_supplementary() {
let bytes = [0x00, 0x41, 0xD8, 0x01, 0xDC, 0x37, 0x00, 0x42];
let engine = utf16be_engine();
assert_eq!(engine.step_backward(&bytes, 8, 0), 2);
assert_eq!(engine.step_backward(&bytes, 6, 0), 4);
assert_eq!(engine.step_backward(&bytes, 2, 0), 2);
assert_eq!(engine.step_backward(&bytes, 0, 0), 0);
}
#[test]
fn utf16_step_forward_backward_round_trip() {
let bytes_le = [0x41, 0x00, 0x01, 0xD8, 0x37, 0xDC, 0x42, 0x00];
for engine in [utf16le_engine()] {
let mut p = 0usize;
while p < bytes_le.len() {
let fwd = engine.step(&bytes_le, p, bytes_le.len());
assert!(fwd > 0);
let next = p + fwd;
let back = engine.step_backward(&bytes_le, next, 0);
assert_eq!(fwd, back, "round-trip mismatch at p={p}");
p = next;
}
}
let bytes_be = [0x00, 0x41, 0xD8, 0x01, 0xDC, 0x37, 0x00, 0x42];
for engine in [utf16be_engine()] {
let mut p = 0usize;
while p < bytes_be.len() {
let fwd = engine.step(&bytes_be, p, bytes_be.len());
assert!(fwd > 0);
let next = p + fwd;
let back = engine.step_backward(&bytes_be, next, 0);
assert_eq!(fwd, back, "round-trip mismatch at p={p}");
p = next;
}
}
}
#[test]
fn utf16_le_count_columns_exact_bmp_only() {
let bytes = [
0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x0A, 0x00, ];
let engine = utf16le_engine();
assert_eq!(engine.count_columns_exact(&bytes), 3);
let bytes_no_nl = [0x41, 0x00, 0x42, 0x00, 0x43, 0x00];
assert_eq!(engine.count_columns_exact(&bytes_no_nl), 3);
}
#[test]
fn utf16_be_count_columns_exact_bmp_only() {
let bytes = [0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x0A];
let engine = utf16be_engine();
assert_eq!(engine.count_columns_exact(&bytes), 3);
}
#[test]
fn utf16_le_count_columns_counts_supplementary_as_one() {
let bytes = [0x41, 0x00, 0x01, 0xD8, 0x37, 0xDC, 0x42, 0x00];
let engine = utf16le_engine();
assert_eq!(engine.count_columns_exact(&bytes), 3);
}
#[test]
fn utf16_be_count_columns_counts_supplementary_as_one() {
let bytes = [0x00, 0x41, 0xD8, 0x01, 0xDC, 0x37, 0x00, 0x42];
let engine = utf16be_engine();
assert_eq!(engine.count_columns_exact(&bytes), 3);
}
#[test]
fn utf16_le_count_columns_bounded_caps_at_max_cols() {
let bytes = [0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x45, 0x00];
let engine = utf16le_engine();
assert_eq!(engine.count_columns_bounded(&bytes, 3), 3);
assert_eq!(engine.count_columns_bounded(&bytes, 100), 5);
let bytes_nl = [0x41, 0x00, 0x42, 0x00, 0x0A, 0x00, 0x43, 0x00];
assert_eq!(engine.count_columns_bounded(&bytes_nl, 100), 2);
}
#[test]
fn utf16_le_count_columns_ignores_misaligned_lf_inside_bmp_cell() {
let bytes = [0x01, 0x0A, 0x42, 0x00];
let engine = utf16le_engine();
assert_eq!(engine.count_columns_exact(&bytes), 2);
}
#[test]
fn utf16_le_count_columns_ignores_misaligned_cr_inside_bmp_cell() {
let bytes = [0x01, 0x0D, 0x42, 0x00];
let engine = utf16le_engine();
assert_eq!(engine.count_columns_exact(&bytes), 2);
}
#[test]
fn utf16_le_advance_offset_by_text_units_bmp() {
let bytes = [0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00];
let engine = utf16le_engine();
assert_eq!(
engine.advance_offset_by_text_units(&bytes, bytes.len(), 0, 0),
0
);
assert_eq!(
engine.advance_offset_by_text_units(&bytes, bytes.len(), 0, 1),
2
);
assert_eq!(
engine.advance_offset_by_text_units(&bytes, bytes.len(), 0, 3),
6
);
assert_eq!(
engine.advance_offset_by_text_units(&bytes, bytes.len(), 0, 99),
bytes.len()
);
}
#[test]
fn utf16_le_advance_offset_collapses_crlf() {
let bytes = [0x41, 0x00, 0x0D, 0x00, 0x0A, 0x00, 0x42, 0x00];
let engine = utf16le_engine();
let after = engine.advance_offset_by_text_units(&bytes, bytes.len(), 0, 2);
assert_eq!(after, 6);
let after_three = engine.advance_offset_by_text_units(&bytes, bytes.len(), 0, 3);
assert_eq!(after_three, 8);
}
#[test]
fn utf16_be_advance_offset_collapses_crlf() {
let bytes = [0x00, 0x41, 0x00, 0x0D, 0x00, 0x0A, 0x00, 0x42];
let engine = utf16be_engine();
let after = engine.advance_offset_by_text_units(&bytes, bytes.len(), 0, 2);
assert_eq!(after, 6);
}
#[test]
fn utf16_le_advance_offset_lf_only_is_one_unit() {
let bytes = [0x41, 0x00, 0x0A, 0x00, 0x42, 0x00];
let engine = utf16le_engine();
let after = engine.advance_offset_by_text_units(&bytes, bytes.len(), 0, 2);
assert_eq!(after, 4);
}
#[test]
fn utf16_le_advance_offset_cr_only_is_one_unit() {
let bytes = [0x41, 0x00, 0x0D, 0x00, 0x42, 0x00];
let engine = utf16le_engine();
let after = engine.advance_offset_by_text_units(&bytes, bytes.len(), 0, 2);
assert_eq!(after, 4);
}
#[test]
fn utf16_le_advance_offset_supplementary_pair_is_one_unit() {
let bytes = [0x41, 0x00, 0x01, 0xD8, 0x37, 0xDC, 0x42, 0x00];
let engine = utf16le_engine();
let after = engine.advance_offset_by_text_units(&bytes, bytes.len(), 0, 2);
assert_eq!(after, 6);
let after_three = engine.advance_offset_by_text_units(&bytes, bytes.len(), 0, 3);
assert_eq!(after_three, bytes.len());
}
#[test]
fn utf16_le_advance_offset_aligns_odd_start() {
let bytes = [0x41, 0x00, 0x42, 0x00];
let engine = utf16le_engine();
let after = engine.advance_offset_by_text_units(&bytes, bytes.len(), 1, 1);
assert_eq!(after, 4);
}
#[test]
fn utf16_le_next_line_start_rejects_misaligned_lf() {
let bytes = [0x01, 0x0A, 0x42, 0x00, 0x0A, 0x00];
let engine = utf16le_engine();
let next = engine.next_line_start(&bytes, bytes.len(), 0);
assert_eq!(next, 6, "misaligned 0x0A must not terminate the line");
}
#[test]
fn utf16_le_next_line_start_rejects_misaligned_cr() {
let bytes = [0x01, 0x0D, 0x42, 0x00, 0x0D, 0x00, 0x0A, 0x00];
let engine = utf16le_engine();
let next = engine.next_line_start(&bytes, bytes.len(), 0);
assert_eq!(next, 8);
}
#[test]
fn utf16_le_next_line_start_handles_lf_only() {
let bytes = [0x41, 0x00, 0x0A, 0x00, 0x42, 0x00];
let engine = utf16le_engine();
assert_eq!(engine.next_line_start(&bytes, bytes.len(), 0), 4);
}
#[test]
fn utf16_le_next_line_start_handles_cr_only() {
let bytes = [0x41, 0x00, 0x0D, 0x00, 0x42, 0x00];
let engine = utf16le_engine();
assert_eq!(engine.next_line_start(&bytes, bytes.len(), 0), 4);
}
#[test]
fn utf16_be_next_line_start_handles_crlf() {
let bytes = [0x00, 0x41, 0x00, 0x0D, 0x00, 0x0A, 0x00, 0x42];
let engine = utf16be_engine();
assert_eq!(engine.next_line_start(&bytes, bytes.len(), 0), 6);
}
#[test]
fn utf16_le_next_line_start_no_break_returns_file_len() {
let bytes = [0x41, 0x00, 0x42, 0x00, 0x43, 0x00];
let engine = utf16le_engine();
assert_eq!(engine.next_line_start(&bytes, bytes.len(), 0), bytes.len());
}
fn shift_jis_engine() -> &'static dyn EncodingEngine {
let encoding =
DocumentEncoding::from_label("Shift_JIS").expect("Shift_JIS is a known encoding");
engine_for_encoding(encoding)
}
fn gb18030_engine() -> &'static dyn EncodingEngine {
let encoding =
DocumentEncoding::from_label("gb18030").expect("gb18030 is a known encoding");
engine_for_encoding(encoding)
}
fn euc_kr_engine() -> &'static dyn EncodingEngine {
let encoding = DocumentEncoding::from_label("EUC-KR").expect("EUC-KR is a known encoding");
engine_for_encoding(encoding)
}
#[test]
fn engine_for_encoding_routes_cjk_to_multibyte() {
assert_eq!(shift_jis_engine().encoding().name(), "Shift_JIS");
assert_eq!(gb18030_engine().encoding().name(), "gb18030");
assert_eq!(euc_kr_engine().encoding().name(), "EUC-KR");
}
#[test]
fn engine_for_encoding_returns_pointer_stable_cjk_engine() {
let sjis_label =
DocumentEncoding::from_label("Shift_JIS").expect("Shift_JIS is a known encoding");
let a = engine_for_encoding(sjis_label) as *const _ as *const ();
let b = engine_for_encoding(sjis_label) as *const _ as *const ();
assert_eq!(a, b, "Shift_JIS engine pointer must be stable");
}
#[test]
fn multibyte_engine_step_for_shift_jis_two_byte_lead() {
let bytes = [0x82u8, 0xA0];
let engine = shift_jis_engine();
assert_eq!(engine.step(&bytes, 0, bytes.len()), 2);
let mixed = [b'A', 0x82, 0xA0];
assert_eq!(engine.step(&mixed, 0, mixed.len()), 1);
assert_eq!(engine.step(&mixed, 1, mixed.len()), 2);
assert_eq!(engine.step(&bytes, bytes.len(), bytes.len()), 0);
}
#[test]
fn multibyte_engine_step_for_shift_jis_truncated_lead_is_one_byte() {
let bytes = [0x82u8];
let engine = shift_jis_engine();
assert_eq!(engine.step(&bytes, 0, bytes.len()), 1);
}
#[test]
fn multibyte_engine_step_for_gb18030_two_byte_sequence() {
let bytes = [0xD6u8, 0xD0];
let engine = gb18030_engine();
assert_eq!(engine.step(&bytes, 0, bytes.len()), 2);
let ascii = [b'A'];
assert_eq!(engine.step(&ascii, 0, ascii.len()), 1);
}
#[test]
fn multibyte_engine_step_for_gb18030_four_byte_sequence() {
let bytes = [0x81u8, 0x30, 0x84, 0x31];
let engine = gb18030_engine();
assert_eq!(engine.step(&bytes, 0, bytes.len()), 4);
let truncated = [0x81u8, 0x30, 0x84];
assert_eq!(engine.step(&truncated, 0, truncated.len()), 1);
}
#[test]
fn multibyte_engine_step_for_euc_kr_two_byte_sequence() {
let bytes = [0xB0u8, 0xA1];
let engine = euc_kr_engine();
assert_eq!(engine.step(&bytes, 0, bytes.len()), 2);
let ascii = [b'A'];
assert_eq!(engine.step(&ascii, 0, ascii.len()), 1);
let lone = [0x80u8, 0x41];
assert_eq!(engine.step(&lone, 0, lone.len()), 1);
let uhc = [0x90u8, 0x45];
assert_eq!(engine.step(&uhc, 0, uhc.len()), 2);
}
#[test]
fn multibyte_engine_step_at_end_returns_zero() {
let engine = shift_jis_engine();
let bytes: [u8; 0] = [];
assert_eq!(engine.step(&bytes, 0, 0), 0);
}
#[test]
fn multibyte_engine_next_line_start_skips_multibyte_lead_for_lf() {
let engine = shift_jis_engine();
let bytes = [0x82u8, 0xA0, b'\n', b'A'];
assert_eq!(engine.next_line_start(&bytes, bytes.len(), 0), 3);
}
#[test]
fn multibyte_engine_next_line_start_collapses_crlf_at_ascii() {
let engine = shift_jis_engine();
let bytes = [0x82u8, 0xA0, b'\r', b'\n', b'A'];
assert_eq!(engine.next_line_start(&bytes, bytes.len(), 0), 4);
}
#[test]
fn multibyte_engine_next_line_start_rejects_lf_in_shift_jis_trail_position() {
let engine = shift_jis_engine();
let bytes = [0x82u8, 0x0A, b'A', b'\n', b'B'];
assert_eq!(engine.next_line_start(&bytes, bytes.len(), 0), 4);
}
#[test]
fn multibyte_engine_next_line_start_rejects_cr_in_euc_kr_trail_position() {
let engine = euc_kr_engine();
let bytes = [0xA1u8, 0x0D, b'A', b'\r', b'\n', b'B'];
assert_eq!(engine.next_line_start(&bytes, bytes.len(), 0), 5);
}
#[test]
fn multibyte_engine_next_line_start_does_not_split_gb18030_four_byte_sequence() {
let engine = gb18030_engine();
let bytes = [0x81u8, 0x30, 0x84, 0x31, b'\n', b'A'];
assert_eq!(engine.next_line_start(&bytes, bytes.len(), 0), 5);
}
#[test]
fn multibyte_engine_next_line_start_returns_file_len_without_line_break() {
let engine = shift_jis_engine();
let bytes = [0x82u8, 0xA0, 0x82, 0xA1, b'A', b'B'];
assert_eq!(engine.next_line_start(&bytes, bytes.len(), 0), bytes.len());
}
#[test]
fn multibyte_engine_next_line_start_starts_from_line_start_offset() {
let engine = shift_jis_engine();
let bytes = [0x82u8, 0xA0, b'\n', 0x82, 0xA1, b'\n', b'A'];
assert_eq!(engine.next_line_start(&bytes, bytes.len(), 3), 6);
}
#[test]
fn multibyte_engine_advance_offset_walks_multibyte_chars() {
let engine = shift_jis_engine();
let bytes = [0x82u8, 0xA0, 0x82, 0xA1];
let after = engine.advance_offset_by_text_units(&bytes, bytes.len(), 0, 2);
assert_eq!(after, 4);
}
#[test]
fn multibyte_engine_count_columns_exact_counts_per_character() {
let engine = shift_jis_engine();
let bytes = [b'A', 0x82, 0xA0, b'B', b'\n'];
assert_eq!(engine.count_columns_exact(&bytes), 3);
}
#[test]
fn step_backward_walks_to_two_byte_boundary_in_shift_jis() {
let engine = shift_jis_engine();
let bytes = [b'A', 0x82, 0xA0, b'B'];
assert_eq!(engine.step_backward(&bytes, 0, 0), 0);
assert_eq!(engine.step_backward(&bytes, 1, 0), 1);
assert_eq!(engine.step_backward(&bytes, 3, 0), 2);
assert_eq!(engine.step_backward(&bytes, 4, 0), 1);
}
#[test]
fn step_backward_round_trip_for_gb18030_two_and_four_byte() {
let engine = gb18030_engine();
let bytes = [b'A', 0xD6, 0xD0, 0x81, 0x30, 0x84, 0x31, b'B'];
let boundaries = [0usize, 1, 3, 7, 8];
for window in boundaries.windows(2) {
let (a, b) = (window[0], window[1]);
let fwd = engine.step(&bytes, a, bytes.len());
assert_eq!(a + fwd, b, "step_forward({a}) should reach {b}");
let back = engine.step_backward(&bytes, b, 0);
assert_eq!(
back, fwd,
"step_backward({b}) should mirror step_forward({a})"
);
}
assert_eq!(engine.step_backward(&bytes, bytes.len(), 0), 1);
}
#[test]
fn step_backward_uses_line_anchor_when_present() {
let engine = shift_jis_engine();
let bytes = [b'\n', 0x82, 0xA0, 0x82, 0xA1, b'B'];
assert_eq!(engine.step_backward(&bytes, 1, 0), 1); assert_eq!(engine.step_backward(&bytes, 3, 0), 2); assert_eq!(engine.step_backward(&bytes, 5, 0), 2); assert_eq!(engine.step_backward(&bytes, 6, 0), 1); }
#[test]
fn step_backward_falls_back_for_unanchored_long_scan() {
let engine = shift_jis_engine();
let len = multibyte::APPROX_LINE_BACKTRACK_BYTES + 4096;
let bytes = vec![b'A'; len];
let result = engine.step_backward(&bytes, len, 0);
assert!(
(1..=4).contains(&result),
"step_backward must return a valid byte length, got {result}"
);
}
}
#[cfg(test)]
mod prop_dispatch_tests {
use crate::document::{Document, DocumentEncoding, DocumentOpenOptions, DocumentSaveOptions};
use proptest::prelude::*;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
fn fresh_test_dir(name: &str) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let base = std::env::var_os("TMP")
.or_else(|| std::env::var_os("TEMP"))
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(r"D:\qem_test_tmp"));
let pid = std::process::id();
let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = base.join(format!("qem-prop-dispatch-{pid}-{counter}-{name}"));
std::fs::create_dir_all(&dir).expect("fresh_test_dir: create_dir_all");
dir
}
fn supported_encoding_strategy() -> impl Strategy<Value = DocumentEncoding> {
let labels: &[&str] = &[
"UTF-8",
"windows-1250",
"windows-1251",
"windows-1252",
"windows-1253",
"windows-1254",
"windows-1255",
"windows-1256",
"windows-1257",
"windows-1258",
"windows-874",
"ISO-8859-2",
"ISO-8859-3",
"ISO-8859-4",
"ISO-8859-5",
"ISO-8859-7",
"ISO-8859-10",
"ISO-8859-13",
"ISO-8859-14",
"ISO-8859-15",
"ISO-8859-16",
"KOI8-R",
"KOI8-U",
"IBM866",
"macintosh",
"x-mac-cyrillic",
];
prop::sample::select(labels.to_vec()).prop_map(|label| {
DocumentEncoding::from_label(label)
.unwrap_or_else(|| panic!("encoding_rs should know {label}"))
})
}
#[derive(Debug, Clone)]
enum DispatchOp {
New,
OpenWithEncoding(DocumentEncoding),
OpenWithOptions(DocumentEncoding),
SaveWithEncoding(DocumentEncoding),
}
fn op_strategy() -> impl Strategy<Value = DispatchOp> {
prop_oneof![
1 => Just(DispatchOp::New),
2 => supported_encoding_strategy().prop_map(DispatchOp::OpenWithEncoding),
2 => supported_encoding_strategy().prop_map(DispatchOp::OpenWithOptions),
2 => supported_encoding_strategy().prop_map(DispatchOp::SaveWithEncoding),
]
}
fn assert_engine_matches_encoding(doc: &Document, context: &str) -> Result<(), TestCaseError> {
let doc_encoding = doc.encoding();
let engine_encoding = doc.encoding_engine().encoding();
prop_assert_eq!(
engine_encoding,
doc_encoding,
"Property 1 violated after {}: engine encoding {} != document encoding {}",
context,
engine_encoding.name(),
doc_encoding.name()
);
Ok(())
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn property_1_engine_matches_encoding_through_lifecycle(
ops in prop::collection::vec(op_strategy(), 1..12),
) {
let dir = fresh_test_dir("property_1");
let mut doc = Document::new();
assert_engine_matches_encoding(&doc, "Document::new()")?;
for (idx, op) in ops.iter().enumerate() {
match op {
DispatchOp::New => {
doc = Document::new();
assert_engine_matches_encoding(
&doc,
&format!("step {idx}: Document::new"),
)?;
}
DispatchOp::OpenWithEncoding(encoding) => {
let path = dir.join(format!("open-{idx}.bin"));
std::fs::write(&path, b"hello world\nsecond line\n")
.expect("write fixture");
match Document::open_with_encoding(&path, *encoding) {
Ok(opened) => {
doc = opened;
assert_engine_matches_encoding(
&doc,
&format!(
"step {idx}: open_with_encoding({})",
encoding.name()
),
)?;
}
Err(_) => continue,
}
}
DispatchOp::OpenWithOptions(encoding) => {
let path = dir.join(format!("open-opts-{idx}.bin"));
std::fs::write(&path, b"abc\r\ndef\r\n")
.expect("write fixture");
let options =
DocumentOpenOptions::new().with_reinterpretation(*encoding);
match Document::open_with_options(&path, options) {
Ok(opened) => {
doc = opened;
assert_engine_matches_encoding(
&doc,
&format!(
"step {idx}: open_with_options({})",
encoding.name()
),
)?;
}
Err(_) => continue,
}
}
DispatchOp::SaveWithEncoding(encoding) => {
let path = dir.join(format!("save-{idx}.bin"));
let options =
DocumentSaveOptions::new().with_encoding(*encoding);
match doc.save_to_with_options(&path, options) {
Ok(()) => {
assert_engine_matches_encoding(
&doc,
&format!(
"step {idx}: save_to_with_options({})",
encoding.name()
),
)?;
}
Err(_) => {
assert_engine_matches_encoding(
&doc,
&format!(
"step {idx}: failed save_to_with_options({})",
encoding.name()
),
)?;
}
}
}
}
}
drop(doc);
let _ = std::fs::remove_dir_all(&dir);
}
}
}
#[cfg(test)]
mod prop_step_tests {
use super::{engine_for_encoding, SingleByteEngine};
use crate::document::DocumentEncoding;
use proptest::prelude::*;
fn class_a_label_strategy() -> impl Strategy<Value = &'static str> {
let labels: Vec<&'static str> = vec![
"windows-1251",
"windows-1252",
"koi8-r",
"ibm866",
"iso-8859-1",
"iso-8859-15",
];
prop::sample::select(labels)
}
fn step_input_strategy() -> impl Strategy<Value = (Vec<u8>, usize, usize)> {
prop::collection::vec(any::<u8>(), 0..=256)
.prop_flat_map(|bytes| {
let len = bytes.len();
(Just(bytes), 0..=len, 0..=len)
})
.prop_map(|(bytes, a, b)| {
let (offset, end) = if a <= b { (a, b) } else { (b, a) };
(bytes, offset, end)
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn property_2_single_byte_engine_step_is_one_or_zero(
label in class_a_label_strategy(),
(bytes, offset, end) in step_input_strategy(),
) {
let encoding = DocumentEncoding::from_label(label)
.unwrap_or_else(|| panic!("encoding_rs should know {label}"));
prop_assert!(
SingleByteEngine::supports(encoding),
"label {label} (canonical {}) must route to SingleByteEngine",
encoding.name(),
);
let engine = engine_for_encoding(encoding);
let step = engine.step(&bytes, offset, end);
if offset < end {
prop_assert_eq!(
step, 1,
"step must be 1 when offset < end (encoding {}, offset {}, end {}, len {})",
encoding.name(), offset, end, bytes.len(),
);
} else {
prop_assert_eq!(
step, 0,
"step must be 0 when offset == end (encoding {}, offset {}, end {}, len {})",
encoding.name(), offset, end, bytes.len(),
);
}
}
}
}