pub mod character_definition;
pub mod connection_cost_matrix;
pub mod context_id_map;
pub mod metadata;
pub mod prefix_dictionary;
pub mod schema;
pub mod unknown_dictionary;
use std::fs;
use std::path::Path;
use std::str;
use std::sync::Arc;
use byteorder::{ByteOrder, LittleEndian};
use once_cell::sync::Lazy;
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
use serde::{Deserialize, Serialize};
use crate::LinderaResult;
use crate::dictionary::character_definition::CharacterDefinition;
use crate::dictionary::connection_cost_matrix::ConnectionCostMatrix;
use crate::dictionary::context_id_map::ContextIdMap;
use crate::dictionary::metadata::Metadata;
use crate::dictionary::prefix_dictionary::{PrefixDictionary, UserPrefixDictionary};
use crate::dictionary::unknown_dictionary::UnknownDictionary;
use crate::error::LinderaErrorKind;
use crate::loader::character_definition::CharacterDefinitionLoader;
use crate::loader::connection_cost_matrix::ConnectionCostMatrixLoader;
use crate::loader::metadata::MetadataLoader;
use crate::loader::prefix_dictionary::PrefixDictionaryLoader;
use crate::loader::unknown_dictionary::UnknownDictionaryLoader;
use crate::util::{Data, detail_field_count, joined_details_at, words_idx_offset};
use crate::viterbi::WordEntry;
const UNK_FIELD: &str = "UNK";
pub static UNK: Lazy<Vec<&str>> = Lazy::new(|| vec![UNK_FIELD]);
pub struct DetailFields<'a> {
inner: str::Split<'a, char>,
remaining: usize,
}
impl<'a> DetailFields<'a> {
#[inline]
fn from_joined(joined: &'a str) -> Self {
Self {
inner: joined.split('\0'),
remaining: detail_field_count(joined.as_bytes()),
}
}
#[inline]
pub fn unk() -> Self {
Self::from_joined(UNK_FIELD)
}
#[inline]
pub fn empty() -> Self {
Self {
inner: "".split('\0'),
remaining: 0,
}
}
}
impl<'a> Iterator for DetailFields<'a> {
type Item = &'a str;
#[inline]
fn next(&mut self) -> Option<&'a str> {
if self.remaining == 0 {
return None;
}
self.remaining -= 1;
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
(self.remaining, Some(self.remaining))
}
}
impl ExactSizeIterator for DetailFields<'_> {}
#[inline]
fn packed_details_iter<'a>(
words_idx_data: &'a [u8],
words_data: &'a [u8],
word_id: usize,
missing: fn() -> DetailFields<'a>,
) -> DetailFields<'a> {
let Some(offset) = words_idx_offset(words_idx_data, word_id) else {
return missing();
};
match joined_details_at(words_data, offset) {
Some(joined) => DetailFields::from_joined(joined),
None => DetailFields::unk(),
}
}
#[derive(Clone)]
pub struct Dictionary {
pub prefix_dictionary: Arc<PrefixDictionary>,
pub connection_cost_matrix: Arc<ConnectionCostMatrix>,
pub character_definition: Arc<CharacterDefinition>,
pub unknown_dictionary: Arc<UnknownDictionary>,
pub metadata: Arc<Metadata>,
}
impl Dictionary {
pub fn unknown_word_details(&self, word_id: usize) -> Vec<&str> {
self.unknown_word_details_iter(word_id).collect()
}
#[inline]
pub fn unknown_word_details_iter<'a>(&'a self, word_id: usize) -> DetailFields<'a> {
self.unknown_dictionary
.word_details_iter(word_id as u32)
.unwrap_or_else(DetailFields::unk)
}
pub fn word_details(&self, word_id: usize) -> Vec<&str> {
self.word_details_iter(word_id).collect()
}
#[inline]
pub fn word_details_iter<'a>(&'a self, word_id: usize) -> DetailFields<'a> {
packed_details_iter(
&self.prefix_dictionary.words_idx_data,
&self.prefix_dictionary.words_data,
word_id,
DetailFields::empty,
)
}
pub fn load_from_path(dict_path: &Path) -> LinderaResult<Self> {
Self::load_from_path_with_options(dict_path, cfg!(feature = "mmap"))
}
pub fn load_from_path_with_options(dict_path: &Path, use_mmap: bool) -> LinderaResult<Self> {
if !dict_path.exists() {
return Err(LinderaErrorKind::Io.with_error(anyhow::anyhow!(
"Dictionary path does not exist: {}",
dict_path.display()
)));
}
if !dict_path.is_dir() {
return Err(LinderaErrorKind::Io.with_error(anyhow::anyhow!(
"Dictionary path is not a directory: {}",
dict_path.display()
)));
}
let metadata = MetadataLoader::load(dict_path)?;
metadata.validate_format_version()?;
let character_definition = CharacterDefinitionLoader::load(dict_path)?;
let connection_cost_matrix = {
#[cfg(feature = "mmap")]
if use_mmap {
ConnectionCostMatrixLoader::load_mmap(dict_path)?
} else {
ConnectionCostMatrixLoader::load(dict_path)?
}
#[cfg(not(feature = "mmap"))]
ConnectionCostMatrixLoader::load(dict_path)?
};
let prefix_dictionary = {
#[cfg(feature = "mmap")]
if use_mmap {
PrefixDictionaryLoader::load_mmap(dict_path)?
} else {
PrefixDictionaryLoader::load(dict_path)?
}
#[cfg(not(feature = "mmap"))]
PrefixDictionaryLoader::load(dict_path)?
};
let unknown_dictionary = UnknownDictionaryLoader::load(dict_path)?;
Ok(Dictionary {
prefix_dictionary: Arc::new(prefix_dictionary),
connection_cost_matrix: Arc::new(connection_cost_matrix),
character_definition: Arc::new(character_definition),
unknown_dictionary: Arc::new(unknown_dictionary),
metadata: Arc::new(metadata),
})
}
pub fn save_to_path(&self, dict_path: &Path) -> LinderaResult<()> {
fs::create_dir_all(dict_path)
.map_err(|err| LinderaErrorKind::Io.with_error(anyhow::anyhow!(err)))?;
todo!("Dictionary saving will be implemented when needed")
}
}
#[derive(Clone, Serialize, Deserialize, Archive, RkyvSerialize, RkyvDeserialize)]
pub struct UserDictionary {
pub dict: UserPrefixDictionary,
}
impl UserDictionary {
pub fn remap_context_ids(&mut self, map: &ContextIdMap) {
const LEFT_ID_OFFSET: usize = 6;
const RIGHT_ID_OFFSET: usize = 8;
let mut vals = self.dict.vals_data.to_vec();
for entry in vals.as_chunks_mut::<{ WordEntry::SERIALIZED_LEN }>().0 {
let left = LittleEndian::read_u16(&entry[LEFT_ID_OFFSET..][..2]);
let right = LittleEndian::read_u16(&entry[RIGHT_ID_OFFSET..][..2]);
LittleEndian::write_u16(&mut entry[LEFT_ID_OFFSET..][..2], map.map_left(left));
LittleEndian::write_u16(&mut entry[RIGHT_ID_OFFSET..][..2], map.map_right(right));
}
self.dict.vals_data = Data::Vec(vals);
}
pub fn load(user_dict_data: &[u8]) -> LinderaResult<UserDictionary> {
let mut aligned = rkyv::util::AlignedVec::<16>::new();
aligned.extend_from_slice(user_dict_data);
rkyv::from_bytes::<UserDictionary, rkyv::rancor::Error>(&aligned).map_err(|err| {
LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!(err.to_string()))
})
}
pub fn word_details(&self, word_id: usize) -> Vec<&str> {
self.word_details_iter(word_id).collect()
}
#[inline]
pub fn word_details_iter<'a>(&'a self, word_id: usize) -> DetailFields<'a> {
packed_details_iter(
&self.dict.words_idx_data,
&self.dict.words_data,
word_id,
DetailFields::unk,
)
}
}
#[cfg(test)]
mod tests {
use daachorse::DoubleArrayAhoCorasickBuilder;
use super::{DetailFields, UNK, UserDictionary};
use crate::dictionary::prefix_dictionary::UserPrefixDictionary;
fn user_dictionary(entries: &[&[&str]]) -> UserDictionary {
let mut words_idx_data = Vec::new();
let mut words_data = Vec::new();
for fields in entries {
words_idx_data.extend_from_slice(&(words_data.len() as u32).to_le_bytes());
let joined = fields.join("\0");
words_data.extend_from_slice(&(joined.len() as u32).to_le_bytes());
words_data.extend_from_slice(joined.as_bytes());
}
let da = match DoubleArrayAhoCorasickBuilder::new().build_with_values([("x", 0u32)]) {
Ok(da) => da,
Err(err) => panic!("failed to build test automaton: {err}"),
};
UserDictionary {
dict: UserPrefixDictionary {
da,
vals_data: Vec::new().into(),
words_idx_data: words_idx_data.into(),
words_data: words_data.into(),
is_system: false,
},
}
}
#[test]
fn user_word_details_returns_fields_in_order() {
let dict = user_dictionary(&[&["カスタム名詞", "*", "リンデラ"], &["動詞", "自立", "*"]]);
assert_eq!(dict.word_details(0), vec!["カスタム名詞", "*", "リンデラ"]);
assert_eq!(dict.word_details(1), vec!["動詞", "自立", "*"]);
}
#[test]
fn user_word_details_handles_single_field() {
let dict = user_dictionary(&[&["ONLY"]]);
assert_eq!(dict.word_details(0), vec!["ONLY"]);
}
#[test]
fn user_word_details_preserves_empty_fields() {
let dict = user_dictionary(&[&["", "a", "", "b", ""]]);
assert_eq!(dict.word_details(0), vec!["", "a", "", "b", ""]);
}
#[test]
fn user_word_details_out_of_range_returns_unk() {
let dict = user_dictionary(&[&["名詞", "一般"]]);
assert_eq!(dict.word_details(1), UNK.to_vec());
assert_eq!(dict.word_details(usize::MAX / 4), UNK.to_vec());
}
#[test]
fn user_word_details_invalid_utf8_returns_unk() {
let mut dict = user_dictionary(&[&["ok", "fields"]]);
let words_data: &[u8] = &dict.dict.words_data;
let mut bytes = words_data.to_vec();
let last = bytes.len() - 1;
bytes[last] = 0xff;
dict.dict.words_data = bytes.into();
assert_eq!(dict.word_details(0), UNK.to_vec());
}
#[test]
fn user_word_details_iter_matches_word_details() {
let dict = user_dictionary(&[
&["カスタム名詞", "*", "リンデラ"],
&["動詞", "自立", "*"],
&["ONLY"],
]);
for word_id in 0..3 {
let via_iter: Vec<&str> = dict.word_details_iter(word_id).collect();
assert_eq!(via_iter, dict.word_details(word_id), "word_id {word_id}");
}
}
#[test]
fn user_word_details_iter_reports_an_exact_length() {
let dict = user_dictionary(&[&["a", "b", "c"], &["only"]]);
let mut fields = dict.word_details_iter(0);
assert_eq!(fields.len(), 3);
assert_eq!(fields.next(), Some("a"));
assert_eq!(fields.len(), 2, "len must track consumption");
assert_eq!(fields.count(), 2);
assert_eq!(dict.word_details_iter(1).len(), 1);
assert_eq!(dict.word_details_iter(99).len(), 1);
}
#[test]
fn user_word_details_corrupt_offset_falls_back_instead_of_panicking() {
let mut dict = user_dictionary(&[&["名詞", "一般"]]);
let idx: &[u8] = &dict.dict.words_idx_data;
let mut idx_bytes = idx.to_vec();
let words_len: &[u8] = &dict.dict.words_data;
let past_end = (words_len.len() as u32) + 1_000;
idx_bytes[0..4].copy_from_slice(&past_end.to_le_bytes());
dict.dict.words_idx_data = idx_bytes.into();
assert_eq!(dict.word_details(0), UNK.to_vec());
assert_eq!(dict.word_details_iter(0).collect::<Vec<_>>(), vec!["UNK"]);
}
#[test]
fn detail_fields_constructors() {
assert_eq!(
DetailFields::empty().collect::<Vec<_>>(),
Vec::<&str>::new()
);
assert_eq!(DetailFields::empty().len(), 0);
assert_eq!(DetailFields::unk().collect::<Vec<_>>(), UNK.to_vec());
assert_eq!(DetailFields::unk().len(), 1);
}
}