#![allow(dead_code)]
use super::encoding_engine::{EncodingEngine, SingleByteEngine};
use super::{align_utf8_boundary_backward, align_utf8_boundary_forward, Document};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AlignDirection {
Backward,
Forward,
}
impl Document {
pub(crate) fn align_byte_offset(&self, offset: usize, dir: AlignDirection) -> usize {
let file_len = self.file_len();
let offset = offset.min(file_len);
if offset == 0 || offset == file_len {
return offset;
}
let encoding = self.encoding();
let name = encoding.name();
if encoding.is_utf8() {
let bytes = self.bytes_for_alignment();
return match dir {
AlignDirection::Backward => align_utf8_boundary_backward(&bytes, offset),
AlignDirection::Forward => align_utf8_boundary_forward(&bytes, offset),
};
}
if SingleByteEngine::supports(encoding) {
return offset;
}
if matches!(name, "UTF-16LE" | "UTF-16BE") {
return match dir {
AlignDirection::Backward => offset & !1usize,
AlignDirection::Forward => ((offset + 1) & !1usize).min(file_len),
};
}
let bytes = self.bytes_for_alignment();
let bytes_len = bytes.len();
let target = offset.min(bytes_len);
if target == 0 || target == bytes_len {
return target;
}
let engine = self.encoding_engine();
align_class_b(
engine,
&bytes,
&self.line_anchor_before(target, &bytes),
target,
dir,
)
}
pub(crate) fn bytes_for_alignment(&self) -> Vec<u8> {
if let Some(piece_table) = &self.piece_table {
return piece_table.read_range(0, piece_table.total_len());
}
if let Some(rope) = &self.rope {
return rope.bytes().collect();
}
self.mmap_bytes().to_vec()
}
fn line_anchor_before(&self, target: usize, bytes: &[u8]) -> usize {
let scan_floor = target.saturating_sub(super::APPROX_LINE_BACKTRACK_BYTES);
let window = &bytes[scan_floor..target];
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 < target && bytes[idx + 1] == b'\n' {
idx + 2
} else {
idx + 1
}
}
None => scan_floor,
}
}
}
fn align_class_b(
engine: &dyn EncodingEngine,
bytes: &[u8],
anchor: &usize,
target: usize,
dir: AlignDirection,
) -> usize {
let bytes_len = bytes.len();
let target = target.min(bytes_len);
let mut cursor = (*anchor).min(target);
while cursor < target {
let step = engine.step(bytes, cursor, bytes_len);
if step == 0 {
return cursor;
}
let next = cursor.saturating_add(step).min(bytes_len);
if next == target {
return target;
}
if next > target {
return match dir {
AlignDirection::Backward => cursor,
AlignDirection::Forward => next.min(bytes_len),
};
}
cursor = next;
}
cursor
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::DocumentEncoding;
use std::io::Write;
use tempfile::tempdir;
fn write_fixture(name: &str, bytes: &[u8]) -> std::path::PathBuf {
let dir = tempdir().expect("tempdir");
let dir = Box::leak(Box::new(dir));
let path = dir.path().join(name);
let mut file = std::fs::File::create(&path).expect("create fixture");
file.write_all(bytes).expect("write fixture");
path
}
fn open_doc(bytes: &[u8], encoding: DocumentEncoding, name: &str) -> Document {
let path = write_fixture(name, bytes);
Document::open_with_encoding(path, encoding).expect("open fixture")
}
#[test]
fn align_utf8_backward_walks_to_char_boundary() {
let bytes = "Привет".as_bytes();
let doc = open_doc(bytes, DocumentEncoding::utf8(), "utf8_back.txt");
let aligned = doc.align_byte_offset(3, AlignDirection::Backward);
assert_eq!(aligned, 2, "backward must land on the start of П's pair");
}
#[test]
fn align_utf8_forward_walks_to_next_char_boundary() {
let bytes = "Привет".as_bytes();
let doc = open_doc(bytes, DocumentEncoding::utf8(), "utf8_forward.txt");
let aligned = doc.align_byte_offset(3, AlignDirection::Forward);
assert_eq!(aligned, 4, "forward must land at the end of П's pair");
}
#[test]
fn align_utf8_clamps_to_file_len() {
let bytes = b"hello\n";
let doc = open_doc(bytes, DocumentEncoding::utf8(), "utf8_clamp.txt");
let aligned = doc.align_byte_offset(usize::MAX, AlignDirection::Backward);
assert_eq!(aligned, doc.file_len());
}
#[test]
fn align_class_a_is_noop_clamp() {
let bytes = b"abc\xDF\n"; let encoding = DocumentEncoding::from_label("windows-1251").expect("known");
let doc = open_doc(bytes, encoding, "win1251_class_a.txt");
for offset in [0usize, 1, 2, 3, 4, 5] {
assert_eq!(
doc.align_byte_offset(offset, AlignDirection::Backward),
offset.min(doc.file_len()),
"Class A backward must be a no-op clamp at offset {offset}",
);
assert_eq!(
doc.align_byte_offset(offset, AlignDirection::Forward),
offset.min(doc.file_len()),
"Class A forward must be a no-op clamp at offset {offset}",
);
}
}
#[test]
fn align_utf16_le_rounds_to_even_byte() {
let bytes: Vec<u8> = "AB\n"
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
let doc = open_doc(&bytes, DocumentEncoding::utf16le(), "utf16_le.txt");
assert_eq!(doc.align_byte_offset(3, AlignDirection::Backward), 2);
assert_eq!(doc.align_byte_offset(3, AlignDirection::Forward), 4);
assert_eq!(doc.align_byte_offset(2, AlignDirection::Backward), 2);
assert_eq!(doc.align_byte_offset(2, AlignDirection::Forward), 2);
}
#[test]
fn align_utf16_be_rounds_to_even_byte() {
let bytes: Vec<u8> = "AB\n"
.encode_utf16()
.flat_map(|u| u.to_be_bytes())
.collect();
let doc = open_doc(&bytes, DocumentEncoding::utf16be(), "utf16_be.txt");
assert_eq!(doc.align_byte_offset(1, AlignDirection::Backward), 0);
assert_eq!(doc.align_byte_offset(1, AlignDirection::Forward), 2);
assert_eq!(doc.align_byte_offset(4, AlignDirection::Forward), 4);
}
#[test]
fn align_utf16_forward_clamps_to_file_len() {
let bytes: Vec<u8> = "A\n".encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
let doc = open_doc(&bytes, DocumentEncoding::utf16le(), "utf16_clamp.txt");
let file_len = doc.file_len();
assert_eq!(file_len, 4);
assert_eq!(
doc.align_byte_offset(file_len, AlignDirection::Forward),
file_len,
);
}
#[test]
fn align_class_b_shift_jis_backward_walks_to_char_start() {
let bytes = [0x82u8, 0xA0, 0x82, 0xA0, 0x0A];
let encoding = DocumentEncoding::from_label("Shift_JIS").expect("known");
let doc = open_doc(&bytes, encoding, "shift_jis_back.txt");
assert_eq!(doc.align_byte_offset(1, AlignDirection::Backward), 0);
assert_eq!(doc.align_byte_offset(3, AlignDirection::Backward), 2);
}
#[test]
fn align_class_b_shift_jis_forward_walks_to_next_char_start() {
let bytes = [0x82u8, 0xA0, 0x82, 0xA0, 0x0A];
let encoding = DocumentEncoding::from_label("Shift_JIS").expect("known");
let doc = open_doc(&bytes, encoding, "shift_jis_forward.txt");
assert_eq!(doc.align_byte_offset(1, AlignDirection::Forward), 2);
assert_eq!(doc.align_byte_offset(3, AlignDirection::Forward), 4);
assert_eq!(doc.align_byte_offset(4, AlignDirection::Forward), 4);
}
#[test]
fn align_class_b_class_a_byte_already_aligned() {
let bytes = [b'A', 0x82u8, 0xA0, b'\n'];
let encoding = DocumentEncoding::from_label("Shift_JIS").expect("known");
let doc = open_doc(&bytes, encoding, "shift_jis_aligned.txt");
assert_eq!(doc.align_byte_offset(1, AlignDirection::Backward), 1);
assert_eq!(doc.align_byte_offset(1, AlignDirection::Forward), 1);
assert_eq!(doc.align_byte_offset(3, AlignDirection::Backward), 3);
assert_eq!(doc.align_byte_offset(3, AlignDirection::Forward), 3);
}
}