use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CharsmapError {
Truncated,
InvalidUtf8,
}
impl std::fmt::Display for CharsmapError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Truncated => write!(f, "precompiled_charsmap blob truncated"),
Self::InvalidUtf8 => write!(f, "precompiled_charsmap replacements not UTF-8"),
}
}
}
impl std::error::Error for CharsmapError {}
#[inline]
fn has_leaf(unit: usize) -> bool {
(unit >> 8) & 1 == 1
}
#[inline]
fn value(unit: usize) -> usize {
unit & ((1usize << 31) - 1)
}
#[inline]
fn label(unit: usize) -> usize {
unit & ((1usize << 31) | 0xFF)
}
#[inline]
fn offset(unit: usize) -> usize {
(unit >> 10) << ((unit & (1usize << 9)) >> 6)
}
pub struct PrecompiledCharsmap {
blob: Vec<u8>,
trie: Vec<u32>,
replacements: String,
ascii: [Option<Box<str>>; 128],
crlf: Option<Box<str>>,
}
impl PrecompiledCharsmap {
pub fn from_blob(blob: &[u8]) -> Result<Self, CharsmapError> {
if blob.len() < 4 {
return Err(CharsmapError::Truncated);
}
let trie_bytes = u32::from_le_bytes(blob[0..4].try_into().unwrap()) as usize;
let trie_end = 4usize.checked_add(trie_bytes).ok_or(CharsmapError::Truncated)?;
if trie_end > blob.len() || trie_bytes % 4 != 0 {
return Err(CharsmapError::Truncated);
}
let trie: Vec<u32> = blob[4..trie_end]
.chunks_exact(4)
.map(|c| u32::from_le_bytes(c.try_into().unwrap()))
.collect();
let replacements = std::str::from_utf8(&blob[trie_end..])
.map_err(|_| CharsmapError::InvalidUtf8)?
.to_string();
let mut this = Self {
blob: blob.to_vec(),
trie,
replacements,
ascii: std::array::from_fn(|_| None),
crlf: None,
};
let mut buf = [0u8; 1];
this.ascii = std::array::from_fn(|b| {
this.transform((b as u8 as char).encode_utf8(&mut buf))
.map(Into::into)
});
this.crlf = this.transform("\r\n").map(Into::into);
Ok(this)
}
pub fn blob(&self) -> &[u8] {
&self.blob
}
#[inline]
fn first_prefix_match(&self, key: &[u8]) -> Option<usize> {
let mut node_pos = 0usize;
let mut unit = *self.trie.first()? as usize;
node_pos ^= offset(unit);
for &c in key {
if c == 0 {
break;
}
node_pos ^= c as usize;
unit = *self.trie.get(node_pos)? as usize;
if label(unit) != c as usize {
return None;
}
node_pos ^= offset(unit);
if has_leaf(unit) {
return Some(value(*self.trie.get(node_pos)? as usize));
}
}
None
}
#[inline]
fn transform(&self, chunk: &str) -> Option<&str> {
let start = self.first_prefix_match(chunk.as_bytes())?;
let bytes = self.replacements.as_bytes();
let end = memchr::memchr(0, bytes.get(start..)?).map_or(bytes.len(), |p| start + p);
self.replacements.get(start..end)
}
pub fn normalize_into(&self, text: &str, out: &mut String) {
let bytes = text.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
let b = bytes[i];
if b < 0x80 && (i + 1 >= len || bytes[i + 1] < 0x80) {
if b == b'\r' && i + 1 < len && bytes[i + 1] == b'\n' {
match &self.crlf {
Some(r) => out.push_str(r),
None => out.push_str("\r\n"),
}
i += 2;
continue;
}
match &self.ascii[b as usize] {
Some(r) => out.push_str(r),
None => out.push(b as char),
}
i += 1;
} else {
let mut j = i + 1;
while j < len {
if bytes[j] < 0x80
&& bytes[j - 1] < 0x80
&& !(bytes[j - 1] == b'\r' && bytes[j] == b'\n')
{
break;
}
j += 1;
}
self.normalize_graphemes(&text[i..j], out);
i = j;
}
}
}
fn normalize_graphemes(&self, region: &str, out: &mut String) {
for grapheme in region.graphemes(true) {
if grapheme.len() < 6 {
if let Some(norm) = self.transform(grapheme) {
out.push_str(norm);
continue;
}
}
for (ci, c) in grapheme.char_indices() {
let part = &grapheme[ci..ci + c.len_utf8()];
match self.transform(part) {
Some(norm) => out.push_str(norm),
None => out.push(c),
}
}
}
}
}
impl PartialEq for PrecompiledCharsmap {
fn eq(&self, other: &Self) -> bool {
self.blob == other.blob
}
}
impl Eq for PrecompiledCharsmap {}
impl std::fmt::Debug for PrecompiledCharsmap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PrecompiledCharsmap")
.field("blob_len", &self.blob.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_truncated_blob() {
assert_eq!(PrecompiledCharsmap::from_blob(&[]), Err(CharsmapError::Truncated));
assert_eq!(
PrecompiledCharsmap::from_blob(&[16, 0, 0, 0, 1, 2]),
Err(CharsmapError::Truncated)
);
}
#[test]
fn empty_trie_is_identity() {
let cm = PrecompiledCharsmap::from_blob(&[0, 0, 0, 0]).unwrap();
let mut out = String::new();
cm.normalize_into("Hello, wörld!\r\nnext", &mut out);
assert_eq!(out, "Hello, wörld!\r\nnext");
}
}