mod matcher;
mod run;
#[cfg(test)]
mod tests;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
pub(crate) use matcher::next_word;
pub(crate) use run::{Run, RunScript, find_next_cj_word, find_next_word, is_katakana};
const MAGIC: [u8; 4] = *b"WKDF";
const VERSION: u16 = 1;
const HEADER: usize = 24;
const SHORT_COUNT: usize = 31;
const TERMINAL: u8 = 0x80;
const SUPPLEMENTARY: u16 = 0xD800;
const MOST_SUPPLEMENTARY: usize = 0x800;
#[derive(Clone, Debug)]
pub enum Bytes {
Static(&'static [u8]),
Shared(Arc<[u8]>),
}
impl Bytes {
#[inline]
pub fn as_slice(&self) -> &[u8] {
match self {
Self::Static(bytes) => bytes,
Self::Shared(bytes) => bytes,
}
}
}
impl From<&'static [u8]> for Bytes {
fn from(bytes: &'static [u8]) -> Self {
Self::Static(bytes)
}
}
impl From<Arc<[u8]>> for Bytes {
fn from(bytes: Arc<[u8]>) -> Self {
Self::Shared(bytes)
}
}
impl From<Vec<u8>> for Bytes {
fn from(bytes: Vec<u8>) -> Self {
Self::Shared(bytes.into())
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum DictionaryScript {
Thai,
Lao,
Khmer,
Myanmar,
ChineseJapanese,
}
impl DictionaryScript {
const fn from_byte(byte: u8) -> Option<Self> {
Some(match byte {
1 => Self::Thai,
2 => Self::Lao,
3 => Self::Khmer,
4 => Self::Myanmar,
5 => Self::ChineseJapanese,
_ => return None,
})
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum DictionaryError {
Magic,
Version(u16),
Script(u8),
Header,
Alphabet,
State(usize),
Edge(usize),
WrongScript {
expected: DictionaryScript,
found: DictionaryScript,
},
}
impl fmt::Display for DictionaryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Magic => f.write_str("not a dictionary: no magic number"),
Self::Version(version) => write!(f, "dictionary format version {version} is unknown"),
Self::Script(script) => write!(f, "dictionary script {script} is unknown"),
Self::Header => f.write_str("the dictionary header does not fit its bytes"),
Self::Alphabet => f.write_str("the dictionary source or alphabet is malformed"),
Self::State(at) => write!(f, "the dictionary state at byte {at} is malformed"),
Self::Edge(at) => write!(f, "an edge of the dictionary state at byte {at} is bad"),
Self::WrongScript { expected, found } => {
write!(f, "a {found:?} dictionary where a {expected:?} one belongs")
}
}
}
}
impl core::error::Error for DictionaryError {}
#[derive(Clone, Debug)]
pub struct Dictionary {
bytes: Bytes,
script: DictionaryScript,
wide: bool,
alphabet: usize,
records: usize,
window_start: u32,
window: [u8; 256],
}
impl Dictionary {
pub fn new(bytes: impl Into<Bytes>) -> Result<Self, DictionaryError> {
let dictionary = Self::from_header(bytes.into())?;
dictionary.check_records()?;
Ok(dictionary)
}
#[inline]
pub fn script(&self) -> DictionaryScript {
self.script
}
pub fn source(&self) -> &str {
let bytes = self.bytes.as_slice().get(HEADER..self.alphabet);
bytes
.and_then(|bytes| core::str::from_utf8(bytes).ok())
.unwrap_or_default()
}
#[inline]
pub fn as_bytes(&self) -> &[u8] {
self.bytes.as_slice()
}
pub fn contains(&self, word: &str) -> bool {
let automaton = self.automaton();
let mut state = 0;
for ch in word.chars() {
match self
.symbol(ch)
.and_then(|symbol| automaton.step(state, symbol))
{
Some(next) => state = next,
None => return false,
}
}
automaton.is_terminal(state)
}
#[cfg(feature = "dictionaries")]
fn from_compiled(bytes: &'static [u8]) -> Option<Self> {
Self::from_header(Bytes::Static(bytes)).ok()
}
fn from_header(bytes: Bytes) -> Result<Self, DictionaryError> {
let data = bytes.as_slice();
if data.len() < HEADER || data.get(..4) != Some(&MAGIC[..]) {
return Err(DictionaryError::Magic);
}
let u16_at = |at: usize| u16::from_le_bytes([data[at], data[at + 1]]);
let u32_at =
|at: usize| u32::from_le_bytes([data[at], data[at + 1], data[at + 2], data[at + 3]]);
let version = u16_at(4);
if version != VERSION {
return Err(DictionaryError::Version(version));
}
let script =
DictionaryScript::from_byte(data[6]).ok_or(DictionaryError::Script(data[6]))?;
let wide = match data[7] {
1 => false,
2 => true,
_ => return Err(DictionaryError::Header),
};
let length = u32_at(8) as usize;
let (flags, source_length, alphabet_length, reserved) =
(u16_at(12), u16_at(14), u16_at(16), u16_at(18));
let records_length = u32_at(20) as usize;
let alphabet = HEADER + usize::from(source_length);
let records = alphabet + 4 * usize::from(alphabet_length);
if flags != 0
|| reserved != 0
|| length != data.len()
|| records.checked_add(records_length) != Some(length)
|| records_length == 0
{
return Err(DictionaryError::Header);
}
let most = if wide { MOST_SUPPLEMENTARY } else { 256 };
if usize::from(alphabet_length) > most
|| !data.get(HEADER..alphabet).is_some_and(<[u8]>::is_ascii)
{
return Err(DictionaryError::Alphabet);
}
let mut window_start = 0;
let mut window = [0; 256];
let mut previous = None;
for (index, chunk) in data
.get(alphabet..records)
.unwrap_or_default()
.as_chunks::<4>()
.0
.iter()
.enumerate()
{
let cp = u32::from_le_bytes(*chunk);
let valid = char::from_u32(cp).is_some()
&& previous.is_none_or(|previous| cp > previous)
&& (!wide || cp >= 0x1_0000);
if !valid {
return Err(DictionaryError::Alphabet);
}
if index == 0 {
window_start = cp;
}
if !wide && let Some(slot) = window.get_mut(cp.wrapping_sub(window_start) as usize) {
*slot = index as u8 + 1;
}
previous = Some(cp);
}
Ok(Self {
bytes,
script,
wide,
alphabet,
records,
window_start,
window,
})
}
fn check_records(&self) -> Result<(), DictionaryError> {
let records = self.records_slice();
let symbol_width = if self.wide { 2 } else { 1 };
let alphabet_length = (self.records - self.alphabet) / 4;
let mut starts = vec![0u64; records.len().div_ceil(64)];
let mut at = 0;
while at < records.len() {
let Some(layout) = record_layout(records, at, symbol_width) else {
return Err(DictionaryError::State(at));
};
let mut previous = None;
for edge in 0..layout.count {
let symbol = read_symbol(records, layout.symbols + edge * symbol_width, self.wide);
let named = if self.wide {
!(SUPPLEMENTARY..0xE000).contains(&symbol)
|| usize::from(symbol - SUPPLEMENTARY) < alphabet_length
} else {
usize::from(symbol) < alphabet_length
};
if !named || previous.is_some_and(|previous| symbol <= previous) {
return Err(DictionaryError::State(at));
}
previous = Some(symbol);
}
starts[at / 64] |= 1 << (at % 64);
at = layout.end;
}
let is_start = |at: usize| {
starts
.get(at / 64)
.is_some_and(|bits| bits & (1 << (at % 64)) != 0)
};
let mut at = 0;
while at < records.len() {
let Some(layout) = record_layout(records, at, symbol_width) else {
return Err(DictionaryError::State(at));
};
for edge in 0..layout.count {
let target = read_target(records, &layout, edge);
if !is_start(target) {
return Err(DictionaryError::Edge(at));
}
}
at = layout.end;
}
Ok(())
}
#[inline]
fn records_slice(&self) -> &[u8] {
self.bytes
.as_slice()
.get(self.records..)
.unwrap_or_default()
}
#[inline]
pub(crate) fn automaton(&self) -> Automaton<'_> {
Automaton {
records: self.records_slice(),
wide: self.wide,
}
}
#[inline]
pub(crate) fn symbol(&self, ch: char) -> Option<u16> {
let cp = u32::from(ch);
if self.wide {
if cp < 0x1_0000 {
return Some(cp as u16);
}
let index = self.alphabet_index(cp)?;
return Some(SUPPLEMENTARY + index as u16);
}
if let Some(&slot) = self.window.get(cp.wrapping_sub(self.window_start) as usize) {
return slot.checked_sub(1).map(u16::from);
}
self.alphabet_index(cp).map(|index| index as u16)
}
#[cold]
fn alphabet_index(&self, cp: u32) -> Option<usize> {
let alphabet = self
.bytes
.as_slice()
.get(self.alphabet..self.records)
.unwrap_or_default();
let count = alphabet.len() / 4;
let (mut low, mut high) = (0, count);
while low < high {
let middle = (low + high) / 2;
let at = middle * 4;
let found = u32::from_le_bytes([
alphabet[at],
alphabet[at + 1],
alphabet[at + 2],
alphabet[at + 3],
]);
match found.cmp(&cp) {
core::cmp::Ordering::Less => low = middle + 1,
core::cmp::Ordering::Greater => high = middle,
core::cmp::Ordering::Equal => return Some(middle),
}
}
None
}
}
struct RecordLayout {
count: usize,
target_width: usize,
symbols: usize,
targets: usize,
end: usize,
}
fn record_layout(records: &[u8], at: usize, symbol_width: usize) -> Option<RecordLayout> {
let first = *records.get(at)?;
let mut count = usize::from(first & 0x1F);
let mut symbols = at + 1;
if count == SHORT_COUNT {
let extra = records.get(symbols..symbols + 2)?;
count += usize::from(u16::from_le_bytes([extra[0], extra[1]]));
symbols += 2;
}
let target_width = usize::from((first >> 5) & 3);
let targets = symbols + count * symbol_width;
let end = targets + count * target_width;
(end <= records.len()).then_some(RecordLayout {
count,
target_width,
symbols,
targets,
end,
})
}
#[inline(always)]
fn read_symbol(records: &[u8], at: usize, wide: bool) -> u16 {
if wide {
u16::from_le_bytes([records[at], records[at + 1]])
} else {
u16::from(records[at])
}
}
#[inline(always)]
fn read_target(records: &[u8], layout: &RecordLayout, edge: usize) -> usize {
let at = layout.targets + edge * layout.target_width;
let mut distance = 0;
for byte in 0..layout.target_width {
distance |= usize::from(records[at + byte]) << (8 * byte);
}
layout.end + distance
}
#[derive(Copy, Clone)]
pub(crate) struct Automaton<'a> {
records: &'a [u8],
wide: bool,
}
impl Automaton<'_> {
#[inline]
pub(crate) fn step(self, state: usize, symbol: u16) -> Option<usize> {
let symbol_width = if self.wide { 2 } else { 1 };
let layout = self.layout(state, symbol_width);
let (mut low, mut high) = (0, layout.count);
while low < high {
let middle = (low + high) / 2;
let found = read_symbol(
self.records,
layout.symbols + middle * symbol_width,
self.wide,
);
match found.cmp(&symbol) {
core::cmp::Ordering::Less => low = middle + 1,
core::cmp::Ordering::Greater => high = middle,
core::cmp::Ordering::Equal => {
return Some(read_target(self.records, &layout, middle));
}
}
}
None
}
#[inline]
pub(crate) fn is_terminal(self, state: usize) -> bool {
self.records[state] & TERMINAL != 0
}
#[cfg(test)]
pub(crate) fn is_leaf(self, state: usize) -> bool {
self.records[state] & 0x1F == 0
}
#[inline(always)]
fn layout(self, state: usize, symbol_width: usize) -> RecordLayout {
let first = self.records[state];
let mut count = usize::from(first & 0x1F);
let mut symbols = state + 1;
if count == SHORT_COUNT {
count += usize::from(u16::from_le_bytes([
self.records[symbols],
self.records[symbols + 1],
]));
symbols += 2;
}
let target_width = usize::from((first >> 5) & 3);
let targets = symbols + count * symbol_width;
RecordLayout {
count,
target_width,
symbols,
targets,
end: targets + count * target_width,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Dictionaries {
thai: Option<Dictionary>,
lao: Option<Dictionary>,
khmer: Option<Dictionary>,
myanmar: Option<Dictionary>,
cj: Option<Dictionary>,
}
impl Dictionaries {
pub const fn new() -> Self {
Self {
thai: None,
lao: None,
khmer: None,
myanmar: None,
cj: None,
}
}
#[cfg(feature = "dictionaries")]
pub fn with_compiled() -> Self {
Self {
thai: Dictionary::from_compiled(include_bytes!("data/thai.dafsa")),
lao: Dictionary::from_compiled(include_bytes!("data/lao.dafsa")),
khmer: Dictionary::from_compiled(include_bytes!("data/khmer.dafsa")),
myanmar: Dictionary::from_compiled(include_bytes!("data/myanmar.dafsa")),
cj: Dictionary::from_compiled(include_bytes!("data/cj.dafsa")),
}
}
pub fn with_thai(mut self, dictionary: Dictionary) -> Result<Self, DictionaryError> {
self.thai = Some(expect(dictionary, DictionaryScript::Thai)?);
Ok(self)
}
pub fn with_lao(mut self, dictionary: Dictionary) -> Result<Self, DictionaryError> {
self.lao = Some(expect(dictionary, DictionaryScript::Lao)?);
Ok(self)
}
pub fn with_khmer(mut self, dictionary: Dictionary) -> Result<Self, DictionaryError> {
self.khmer = Some(expect(dictionary, DictionaryScript::Khmer)?);
Ok(self)
}
pub fn with_myanmar(mut self, dictionary: Dictionary) -> Result<Self, DictionaryError> {
self.myanmar = Some(expect(dictionary, DictionaryScript::Myanmar)?);
Ok(self)
}
pub fn with_chinese_japanese(
mut self,
dictionary: Dictionary,
) -> Result<Self, DictionaryError> {
self.cj = Some(expect(dictionary, DictionaryScript::ChineseJapanese)?);
Ok(self)
}
pub fn get(&self, script: DictionaryScript) -> Option<&Dictionary> {
match script {
DictionaryScript::Thai => self.thai.as_ref(),
DictionaryScript::Lao => self.lao.as_ref(),
DictionaryScript::Khmer => self.khmer.as_ref(),
DictionaryScript::Myanmar => self.myanmar.as_ref(),
DictionaryScript::ChineseJapanese => self.cj.as_ref(),
}
}
pub fn segments(&self, ch: char) -> bool {
crate::word::run_script(ch)
.and_then(|script| script.find(Some(self)))
.is_some()
}
pub fn is_empty(&self) -> bool {
self.thai.is_none()
&& self.lao.is_none()
&& self.khmer.is_none()
&& self.myanmar.is_none()
&& self.cj.is_none()
}
}
fn expect(dictionary: Dictionary, script: DictionaryScript) -> Result<Dictionary, DictionaryError> {
if dictionary.script == script {
Ok(dictionary)
} else {
Err(DictionaryError::WrongScript {
expected: script,
found: dictionary.script,
})
}
}