#[derive(Debug, Clone)]
pub struct Utf16Map {
line_utf16_starts: Vec<u32>,
line_byte_starts: Vec<u32>,
is_ascii: bool,
pub total_utf16_len: u32,
}
impl Utf16Map {
pub fn build(source: &[u8]) -> Self {
let mut line_utf16_starts = Vec::with_capacity(source.len() / 40 + 1);
let mut line_byte_starts = Vec::with_capacity(source.len() / 40 + 1);
let mut is_ascii = true;
line_utf16_starts.push(0);
line_byte_starts.push(0);
let mut utf16_pos: u32 = 0;
let mut i = 0;
while i < source.len() {
let b = source[i];
if b == b'\n' {
utf16_pos += 1; i += 1;
line_utf16_starts.push(utf16_pos);
line_byte_starts.push(i as u32);
} else if b < 0x80 {
utf16_pos += 1;
i += 1;
} else if b < 0xC0 {
utf16_pos += 1;
i += 1;
is_ascii = false;
} else if b < 0xE0 {
utf16_pos += 1;
i += 2.min(source.len());
is_ascii = false;
} else if b < 0xF0 {
utf16_pos += 1;
i += 3.min(source.len());
is_ascii = false;
} else {
utf16_pos += 2;
i += 4.min(source.len());
is_ascii = false;
}
}
Utf16Map {
line_utf16_starts,
line_byte_starts,
is_ascii,
total_utf16_len: utf16_pos,
}
}
#[allow(clippy::if_same_then_else)]
pub fn byte_to_utf16(&self, byte_offset: u32, source: &[u8]) -> u32 {
if byte_offset >= source.len() as u32 {
return self.total_utf16_len;
}
if self.is_ascii {
return byte_offset;
}
let line = match self.line_byte_starts.binary_search(&byte_offset) {
Ok(exact) => exact,
Err(insert) => insert.saturating_sub(1),
};
let line_byte_start = self.line_byte_starts[line] as usize;
let line_utf16_start = self.line_utf16_starts[line];
let target = byte_offset as usize;
let mut utf16_offset = line_utf16_start;
let mut pos = line_byte_start;
while pos < target && pos < source.len() {
let b = source[pos];
if b < 0x80 {
utf16_offset += 1;
pos += 1;
} else if b < 0xC0 {
utf16_offset += 1;
pos += 1;
} else if b < 0xE0 {
utf16_offset += 1;
pos += 2;
} else if b < 0xF0 {
utf16_offset += 1;
pos += 3;
} else {
utf16_offset += 2;
pos += 4;
}
}
utf16_offset
}
#[allow(clippy::if_same_then_else)]
pub fn utf16_to_byte(&self, utf16_offset: u32, source: &[u8]) -> u32 {
if self.is_ascii {
return utf16_offset;
}
let line = match self.line_utf16_starts.binary_search(&utf16_offset) {
Ok(exact) => exact,
Err(insert) => insert.saturating_sub(1),
};
let line_byte_start = self.line_byte_starts[line] as usize;
let line_utf16_start = self.line_utf16_starts[line];
let target = utf16_offset;
let mut current_utf16 = line_utf16_start;
let mut pos = line_byte_start;
while current_utf16 < target && pos < source.len() {
let b = source[pos];
if b < 0x80 {
current_utf16 += 1;
pos += 1;
} else if b < 0xC0 {
current_utf16 += 1;
pos += 1;
} else if b < 0xE0 {
current_utf16 += 1;
pos += 2;
} else if b < 0xF0 {
current_utf16 += 1;
pos += 3;
} else {
current_utf16 += 2;
pos += 4;
}
}
pos as u32
}
pub fn line_at_byte(&self, byte_offset: u32) -> u32 {
match self.line_byte_starts.binary_search(&byte_offset) {
Ok(exact) => exact as u32,
Err(insert) => insert.saturating_sub(1) as u32,
}
}
pub fn line_byte_start(&self, line: u32) -> u32 {
self.line_byte_starts
.get(line as usize)
.copied()
.unwrap_or(0)
}
pub fn line_utf16_start(&self, line: u32) -> u32 {
self.line_utf16_starts
.get(line as usize)
.copied()
.unwrap_or(0)
}
pub fn line_count(&self) -> u32 {
self.line_byte_starts.len() as u32
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_identity() {
let src = b"hello\nworld\n";
let map = Utf16Map::build(src);
assert!(map.is_ascii);
assert_eq!(map.byte_to_utf16(0, src), 0);
assert_eq!(map.byte_to_utf16(5, src), 5);
assert_eq!(map.byte_to_utf16(6, src), 6);
assert_eq!(map.total_utf16_len, 12);
}
#[test]
fn two_byte_utf8() {
let src = "señor\n".as_bytes();
let map = Utf16Map::build(src);
assert!(!map.is_ascii);
assert_eq!(map.byte_to_utf16(0, src), 0);
assert_eq!(map.byte_to_utf16(1, src), 1);
assert_eq!(map.byte_to_utf16(2, src), 2);
assert_eq!(map.byte_to_utf16(4, src), 3);
assert_eq!(map.total_utf16_len, 6);
}
#[test]
fn four_byte_emoji() {
let src = "a🟢b\n".as_bytes();
let map = Utf16Map::build(src);
assert!(!map.is_ascii);
assert_eq!(map.byte_to_utf16(0, src), 0); assert_eq!(map.byte_to_utf16(1, src), 1); assert_eq!(map.byte_to_utf16(5, src), 3); assert_eq!(map.total_utf16_len, 5); }
#[test]
fn multiline_offsets() {
let src = "line1\nline2\nline3\n".as_bytes();
let map = Utf16Map::build(src);
assert_eq!(map.line_count(), 4); assert_eq!(map.line_byte_start(0), 0);
assert_eq!(map.line_byte_start(1), 6);
assert_eq!(map.line_byte_start(2), 12);
assert_eq!(map.line_at_byte(7), 1);
}
#[test]
fn roundtrip() {
let src = "hello 🌍 world\nañ\n".as_bytes();
let map = Utf16Map::build(src);
for byte_off in 0..src.len() as u32 {
if byte_off == 0
|| (byte_off > 0 && src[byte_off as usize] < 0x80)
|| (byte_off > 0 && src[byte_off as usize] >= 0xC0)
{
let utf16 = map.byte_to_utf16(byte_off, src);
let back = map.utf16_to_byte(utf16, src);
assert_eq!(back, byte_off, "roundtrip failed for byte_off={byte_off}");
}
}
}
}