lindera_dictionary/dictionary.rs
1pub mod character_definition;
2pub mod connection_cost_matrix;
3pub mod context_id_map;
4pub mod metadata;
5pub mod prefix_dictionary;
6pub mod schema;
7pub mod unknown_dictionary;
8
9use std::fs;
10use std::path::Path;
11use std::str;
12use std::sync::Arc;
13
14use byteorder::{ByteOrder, LittleEndian};
15use once_cell::sync::Lazy;
16use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
17use serde::{Deserialize, Serialize};
18
19use crate::LinderaResult;
20use crate::dictionary::character_definition::CharacterDefinition;
21use crate::dictionary::connection_cost_matrix::ConnectionCostMatrix;
22use crate::dictionary::context_id_map::ContextIdMap;
23use crate::dictionary::metadata::Metadata;
24use crate::dictionary::prefix_dictionary::{PrefixDictionary, UserPrefixDictionary};
25use crate::dictionary::unknown_dictionary::UnknownDictionary;
26use crate::error::LinderaErrorKind;
27use crate::loader::character_definition::CharacterDefinitionLoader;
28use crate::loader::connection_cost_matrix::ConnectionCostMatrixLoader;
29use crate::loader::metadata::MetadataLoader;
30use crate::loader::prefix_dictionary::PrefixDictionaryLoader;
31use crate::loader::unknown_dictionary::UnknownDictionaryLoader;
32use crate::util::{Data, detail_field_count, joined_details_at, words_idx_offset};
33use crate::viterbi::WordEntry;
34
35/// The single field the unknown-word sentinel consists of.
36///
37/// Kept as a constant so [`UNK`] and [`DetailFields::unk`] cannot drift apart.
38const UNK_FIELD: &str = "UNK";
39
40pub static UNK: Lazy<Vec<&str>> = Lazy::new(|| vec![UNK_FIELD]);
41
42/// The detail fields of one dictionary entry, borrowed from the dictionary's
43/// own bytes.
44///
45/// Yielding borrows rather than a `Vec` is what lets a caller materialize an
46/// entry's details in a single allocation: the fields are handed out straight
47/// from the packed record, with no intermediate collection (#966). The
48/// iterator is [`ExactSizeIterator`], so a caller can size its buffer exactly
49/// before consuming anything.
50///
51/// Obtained from [`Dictionary::word_details_iter`],
52/// [`Dictionary::unknown_word_details_iter`],
53/// [`UserDictionary::word_details_iter`], or
54/// [`UnknownDictionary::word_details_iter`].
55pub struct DetailFields<'a> {
56 /// The remaining fields, split from the entry's NUL-joined blob.
57 inner: str::Split<'a, char>,
58 /// How many fields `inner` has yet to yield, for `ExactSizeIterator`.
59 remaining: usize,
60}
61
62impl<'a> DetailFields<'a> {
63 /// Builds the field list of a validated NUL-joined blob.
64 ///
65 /// # 引数
66 ///
67 /// * `joined` - The entry's joined fields, already UTF-8 validated.
68 ///
69 /// # 戻り値
70 ///
71 /// The fields in schema order; always at least one, since an empty blob
72 /// splits into a single empty field.
73 #[inline]
74 fn from_joined(joined: &'a str) -> Self {
75 Self {
76 inner: joined.split('\0'),
77 remaining: detail_field_count(joined.as_bytes()),
78 }
79 }
80
81 /// The unknown-word sentinel: a single `"UNK"` field.
82 ///
83 /// This is what every accessor falls back to for a malformed entry. It is
84 /// built by splitting the sentinel itself, so the fallback runs through
85 /// the same machinery as a real entry instead of needing its own variant.
86 ///
87 /// # 戻り値
88 ///
89 /// A one-field iterator yielding `"UNK"`.
90 //
91 // Returns `Self`, not `DetailFields<'static>`: `str::Split` makes this
92 // type invariant over `'a`, so a `'static` value would not coerce to a
93 // shorter lifetime. The `&'static str` constant coerces to `&'a str`
94 // before construction instead, which needs no variance.
95 #[inline]
96 pub fn unk() -> Self {
97 Self::from_joined(UNK_FIELD)
98 }
99
100 /// An entry with no fields at all.
101 ///
102 /// Distinct from [`DetailFields::unk`]: this is what
103 /// [`Dictionary::word_details_iter`] yields for an out-of-range word id,
104 /// where the allocating accessor has always returned an empty vector.
105 ///
106 /// # 戻り値
107 ///
108 /// An iterator that yields nothing.
109 //
110 // Returns `Self` for the same invariance reason as `unk`.
111 #[inline]
112 pub fn empty() -> Self {
113 Self {
114 inner: "".split('\0'),
115 remaining: 0,
116 }
117 }
118}
119
120impl<'a> Iterator for DetailFields<'a> {
121 type Item = &'a str;
122
123 /// Yields the next detail field.
124 ///
125 /// # 戻り値
126 ///
127 /// The next field in schema order, or `None` once all are consumed.
128 #[inline]
129 fn next(&mut self) -> Option<&'a str> {
130 if self.remaining == 0 {
131 return None;
132 }
133 self.remaining -= 1;
134 self.inner.next()
135 }
136
137 /// Reports the exact number of fields left, so collecting into a `Vec`
138 /// reserves in one shot.
139 ///
140 /// # 戻り値
141 ///
142 /// `(remaining, Some(remaining))`.
143 #[inline]
144 fn size_hint(&self) -> (usize, Option<usize>) {
145 (self.remaining, Some(self.remaining))
146 }
147}
148
149impl ExactSizeIterator for DetailFields<'_> {}
150
151/// Looks up one entry in a packed dictionary's detail records.
152///
153/// Shared by [`Dictionary`] and [`UserDictionary`], whose storage layout is
154/// identical and whose only behavioural difference is what a word id with no
155/// slot in the index yields.
156///
157/// # 引数
158///
159/// * `words_idx_data` - The word-id index table.
160/// * `words_data` - The packed detail records.
161/// * `word_id` - The word id to look up.
162/// * `missing` - Builds the fallback for a word id with no slot in the index.
163///
164/// # 戻り値
165///
166/// The entry's fields; `missing()` when the id has no slot, and
167/// [`DetailFields::unk`] when the entry itself is malformed.
168#[inline]
169fn packed_details_iter<'a>(
170 words_idx_data: &'a [u8],
171 words_data: &'a [u8],
172 word_id: usize,
173 missing: fn() -> DetailFields<'a>,
174) -> DetailFields<'a> {
175 let Some(offset) = words_idx_offset(words_idx_data, word_id) else {
176 return missing();
177 };
178 // A malformed entry always falls back to the sentinel in both
179 // dictionaries; only a word id with no slot at all is subject to
180 // `missing`.
181 match joined_details_at(words_data, offset) {
182 Some(joined) => DetailFields::from_joined(joined),
183 None => DetailFields::unk(),
184 }
185}
186
187/// `prefix_dictionary` and `connection_cost_matrix` are `Arc`-wrapped so that
188/// `Dictionary::clone()` is O(1) regardless of load method (embedded, mmap,
189/// or plain filesystem read) -- these two components dominate a dictionary's
190/// memory footprint (tens to hundreds of MB), and nothing in this codebase
191/// mutates them after construction.
192#[derive(Clone)]
193pub struct Dictionary {
194 pub prefix_dictionary: Arc<PrefixDictionary>,
195 pub connection_cost_matrix: Arc<ConnectionCostMatrix>,
196 pub character_definition: Arc<CharacterDefinition>,
197 pub unknown_dictionary: Arc<UnknownDictionary>,
198 pub metadata: Arc<Metadata>,
199}
200
201impl Dictionary {
202 /// Retrieve the detail fields (POS, etc.) for an unknown word entry.
203 ///
204 /// # 引数
205 ///
206 /// * `word_id` - The unknown-word entry id.
207 ///
208 /// # 戻り値
209 ///
210 /// A freshly allocated vector of the entry's fields, or the [`UNK`]
211 /// sentinel when the id is out of range or the entry is malformed. Prefer
212 /// [`Dictionary::unknown_word_details_iter`] on per-token paths, which
213 /// yields the same fields without allocating.
214 pub fn unknown_word_details(&self, word_id: usize) -> Vec<&str> {
215 self.unknown_word_details_iter(word_id).collect()
216 }
217
218 /// Yields the detail fields of an unknown-word entry, borrowed from the
219 /// dictionary's own bytes.
220 ///
221 /// # 引数
222 ///
223 /// * `word_id` - The unknown-word entry id.
224 ///
225 /// # 戻り値
226 ///
227 /// The entry's fields, or the [`UNK`] sentinel when the id is out of
228 /// range or the entry is malformed -- the fallback
229 /// [`Dictionary::unknown_word_details`] has always applied.
230 #[inline]
231 pub fn unknown_word_details_iter<'a>(&'a self, word_id: usize) -> DetailFields<'a> {
232 self.unknown_dictionary
233 .word_details_iter(word_id as u32)
234 .unwrap_or_else(DetailFields::unk)
235 }
236
237 /// Retrieve the detail fields (POS, etc.) for a system dictionary entry.
238 ///
239 /// # 引数
240 ///
241 /// * `word_id` - The system word id.
242 ///
243 /// # 戻り値
244 ///
245 /// A freshly allocated vector of the entry's fields; empty when `word_id`
246 /// is out of range, and the [`UNK`] sentinel when the entry is malformed.
247 /// Prefer [`Dictionary::word_details_iter`] on per-token paths, which
248 /// yields the same fields without allocating.
249 pub fn word_details(&self, word_id: usize) -> Vec<&str> {
250 self.word_details_iter(word_id).collect()
251 }
252
253 /// Yields the detail fields of a system dictionary entry, borrowed from
254 /// the dictionary's own bytes.
255 ///
256 /// # 引数
257 ///
258 /// * `word_id` - The system word id.
259 ///
260 /// # 戻り値
261 ///
262 /// The entry's fields; [`DetailFields::empty`] when `word_id` is out of
263 /// range, and [`DetailFields::unk`] when the entry is malformed. Those
264 /// two fallbacks differ, and both are what
265 /// [`Dictionary::word_details`] has always returned.
266 #[inline]
267 pub fn word_details_iter<'a>(&'a self, word_id: usize) -> DetailFields<'a> {
268 packed_details_iter(
269 &self.prefix_dictionary.words_idx_data,
270 &self.prefix_dictionary.words_data,
271 word_id,
272 DetailFields::empty,
273 )
274 }
275
276 /// Load dictionary from a directory containing dictionary files.
277 ///
278 /// When the `mmap` feature is compiled in, the connection-cost matrix
279 /// and word list are routed through memory-mapped reads by default
280 /// (#879); use [`Dictionary::load_from_path_with_options`] with
281 /// `use_mmap = false` to force eager reads.
282 pub fn load_from_path(dict_path: &Path) -> LinderaResult<Self> {
283 Self::load_from_path_with_options(dict_path, cfg!(feature = "mmap"))
284 }
285
286 /// Load dictionary from a directory with options
287 ///
288 /// `use_mmap` (when the `mmap` feature is enabled) routes
289 /// `connection_cost_matrix` and `prefix_dictionary` through memory-mapped
290 /// reads instead of plain file reads. What that buys differs per
291 /// component:
292 ///
293 /// - `ConnectionCostMatrix` reads its costs **in place**, with no copy and
294 /// no anonymous memory: `matrix.mtx` already stores the values in the
295 /// in-memory layout, and an mmap base is page-aligned, so the payload
296 /// can be viewed as `[i16]` directly. Loading it is O(1) and the pages
297 /// are faulted in lazily during tokenization. (Costs are also borrowed
298 /// from a plain read's buffer whenever it happens to be `i16`-aligned;
299 /// the alignment is only *guaranteed* under mmap and for embedded data.)
300 /// - `PrefixDictionary`'s `vals_data`/`words_idx_data`/`words_data` are
301 /// likewise mmap-backed and read lazily at lookup time.
302 /// - `PrefixDictionary`'s double-array trie (`da`) is still eagerly
303 /// deserialized into owned daachorse structures, so for that component
304 /// `use_mmap` only avoids the initial file-read syscall/allocation.
305 ///
306 /// `metadata`, `character_definition` and `unknown_dictionary` are always
307 /// plain-read regardless of this flag. Separately, `Dictionary::clone()`
308 /// is O(1) regardless of `use_mmap`, since
309 /// `prefix_dictionary`/`connection_cost_matrix` are `Arc`-wrapped.
310 pub fn load_from_path_with_options(dict_path: &Path, use_mmap: bool) -> LinderaResult<Self> {
311 // Verify that the dictionary directory exists
312 if !dict_path.exists() {
313 return Err(LinderaErrorKind::Io.with_error(anyhow::anyhow!(
314 "Dictionary path does not exist: {}",
315 dict_path.display()
316 )));
317 }
318
319 if !dict_path.is_dir() {
320 return Err(LinderaErrorKind::Io.with_error(anyhow::anyhow!(
321 "Dictionary path is not a directory: {}",
322 dict_path.display()
323 )));
324 }
325
326 // Load each component from the dictionary directory. The format check
327 // comes first: the remaining artifacts are headerless raw arrays, so a
328 // stale dictionary decodes into garbage rather than failing, and the
329 // error would surface far from its cause.
330 let metadata = MetadataLoader::load(dict_path)?;
331 metadata.validate_format_version()?;
332
333 let character_definition = CharacterDefinitionLoader::load(dict_path)?;
334
335 let connection_cost_matrix = {
336 #[cfg(feature = "mmap")]
337 if use_mmap {
338 ConnectionCostMatrixLoader::load_mmap(dict_path)?
339 } else {
340 ConnectionCostMatrixLoader::load(dict_path)?
341 }
342 #[cfg(not(feature = "mmap"))]
343 ConnectionCostMatrixLoader::load(dict_path)?
344 };
345
346 let prefix_dictionary = {
347 #[cfg(feature = "mmap")]
348 if use_mmap {
349 PrefixDictionaryLoader::load_mmap(dict_path)?
350 } else {
351 PrefixDictionaryLoader::load(dict_path)?
352 }
353 #[cfg(not(feature = "mmap"))]
354 PrefixDictionaryLoader::load(dict_path)?
355 };
356
357 let unknown_dictionary = UnknownDictionaryLoader::load(dict_path)?;
358
359 Ok(Dictionary {
360 prefix_dictionary: Arc::new(prefix_dictionary),
361 connection_cost_matrix: Arc::new(connection_cost_matrix),
362 character_definition: Arc::new(character_definition),
363 unknown_dictionary: Arc::new(unknown_dictionary),
364 metadata: Arc::new(metadata),
365 })
366 }
367
368 /// Save dictionary to a directory
369 pub fn save_to_path(&self, dict_path: &Path) -> LinderaResult<()> {
370 // Create directory if it doesn't exist
371 fs::create_dir_all(dict_path)
372 .map_err(|err| LinderaErrorKind::Io.with_error(anyhow::anyhow!(err)))?;
373
374 // For now, we'll implement this as needed
375 // This would require implementing save methods for each component
376 todo!("Dictionary saving will be implemented when needed")
377 }
378}
379
380/// `dict` archives with the exact field sequence the pre-v6
381/// `PrefixDictionary` used, which is what keeps previously-built user
382/// dictionary `.bin` files loading across the v6 system-dictionary format
383/// break -- rkyv 0.8 archives structurally, without type names. See
384/// [`UserPrefixDictionary`]'s type-level comment before touching either type.
385#[derive(Clone, Serialize, Deserialize, Archive, RkyvSerialize, RkyvDeserialize)]
386
387pub struct UserDictionary {
388 pub dict: UserPrefixDictionary,
389}
390
391impl UserDictionary {
392 /// Relabel this dictionary's context IDs with a system dictionary's permutation.
393 ///
394 /// User dictionaries are always compiled in the *original* context-ID space, which
395 /// keeps a built `.bin` portable across remapped and un-remapped system
396 /// dictionaries. When one is attached to a system dictionary built with
397 /// `connection_id_mapping`, its `left_id`/`right_id` must be moved into the same
398 /// space, or every connection cost it participates in would address the wrong
399 /// matrix cell — silently, since the IDs stay in range.
400 ///
401 /// Entries live in `vals_data` as a flat [`WordEntry::SERIALIZED_LEN`]-byte stride
402 /// with `left_id` at offset 6 and `right_id` at offset 8 (little endian), so this
403 /// rewrites those two `u16`s in place. IDs outside the permutation are left
404 /// untouched, matching the builder's behaviour for malformed IDs.
405 ///
406 /// # Arguments
407 ///
408 /// * `map` - The permutation persisted in the system dictionary's metadata.
409 pub fn remap_context_ids(&mut self, map: &ContextIdMap) {
410 const LEFT_ID_OFFSET: usize = 6;
411 const RIGHT_ID_OFFSET: usize = 8;
412
413 let mut vals = self.dict.vals_data.to_vec();
414 for entry in vals.as_chunks_mut::<{ WordEntry::SERIALIZED_LEN }>().0 {
415 let left = LittleEndian::read_u16(&entry[LEFT_ID_OFFSET..][..2]);
416 let right = LittleEndian::read_u16(&entry[RIGHT_ID_OFFSET..][..2]);
417 LittleEndian::write_u16(&mut entry[LEFT_ID_OFFSET..][..2], map.map_left(left));
418 LittleEndian::write_u16(&mut entry[RIGHT_ID_OFFSET..][..2], map.map_right(right));
419 }
420 self.dict.vals_data = Data::Vec(vals);
421 }
422
423 pub fn load(user_dict_data: &[u8]) -> LinderaResult<UserDictionary> {
424 let mut aligned = rkyv::util::AlignedVec::<16>::new();
425 aligned.extend_from_slice(user_dict_data);
426 rkyv::from_bytes::<UserDictionary, rkyv::rancor::Error>(&aligned).map_err(|err| {
427 LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!(err.to_string()))
428 })
429 }
430
431 /// Retrieve the detail fields (POS, etc.) for a user dictionary entry.
432 ///
433 /// # 引数
434 ///
435 /// * `word_id` - The user-dictionary word id.
436 ///
437 /// # 戻り値
438 ///
439 /// A freshly allocated vector of the entry's fields, or the [`UNK`]
440 /// sentinel when the id is out of range or the entry is malformed --
441 /// note this differs from [`Dictionary::word_details`], which returns an
442 /// empty vector for an out-of-range id. Prefer
443 /// [`UserDictionary::word_details_iter`] on per-token paths, which yields
444 /// the same fields without allocating.
445 pub fn word_details(&self, word_id: usize) -> Vec<&str> {
446 self.word_details_iter(word_id).collect()
447 }
448
449 /// Yields the detail fields of a user dictionary entry, borrowed from the
450 /// dictionary's own bytes.
451 ///
452 /// # 引数
453 ///
454 /// * `word_id` - The user-dictionary word id.
455 ///
456 /// # 戻り値
457 ///
458 /// The entry's fields, or [`DetailFields::unk`] when the id is out of
459 /// range or the entry is malformed. Unlike
460 /// [`Dictionary::word_details_iter`], an out-of-range id yields the
461 /// sentinel rather than nothing; that divergence is pre-existing.
462 #[inline]
463 pub fn word_details_iter<'a>(&'a self, word_id: usize) -> DetailFields<'a> {
464 packed_details_iter(
465 &self.dict.words_idx_data,
466 &self.dict.words_data,
467 word_id,
468 DetailFields::unk,
469 )
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use daachorse::DoubleArrayAhoCorasickBuilder;
476
477 use super::{DetailFields, UNK, UserDictionary};
478 use crate::dictionary::prefix_dictionary::UserPrefixDictionary;
479
480 /// Builds a user dictionary whose words blob holds `entries`, each
481 /// encoded as a 4-byte LE length followed by its NUL-joined fields --
482 /// the layout `builder::user_dictionary` writes.
483 fn user_dictionary(entries: &[&[&str]]) -> UserDictionary {
484 let mut words_idx_data = Vec::new();
485 let mut words_data = Vec::new();
486 for fields in entries {
487 words_idx_data.extend_from_slice(&(words_data.len() as u32).to_le_bytes());
488 let joined = fields.join("\0");
489 words_data.extend_from_slice(&(joined.len() as u32).to_le_bytes());
490 words_data.extend_from_slice(joined.as_bytes());
491 }
492 // The automaton is irrelevant to `word_details`, but the type needs a
493 // real one; a single dummy key keeps the build valid.
494 let da = match DoubleArrayAhoCorasickBuilder::new().build_with_values([("x", 0u32)]) {
495 Ok(da) => da,
496 Err(err) => panic!("failed to build test automaton: {err}"),
497 };
498 UserDictionary {
499 dict: UserPrefixDictionary {
500 da,
501 vals_data: Vec::new().into(),
502 words_idx_data: words_idx_data.into(),
503 words_data: words_data.into(),
504 is_system: false,
505 },
506 }
507 }
508
509 /// Fields come back in order with the exact count that was written.
510 #[test]
511 fn user_word_details_returns_fields_in_order() {
512 let dict = user_dictionary(&[&["カスタム名詞", "*", "リンデラ"], &["動詞", "自立", "*"]]);
513
514 assert_eq!(dict.word_details(0), vec!["カスタム名詞", "*", "リンデラ"]);
515 assert_eq!(dict.word_details(1), vec!["動詞", "自立", "*"]);
516 }
517
518 /// A single-field entry has no separator, so the capacity must still be 1.
519 #[test]
520 fn user_word_details_handles_single_field() {
521 let dict = user_dictionary(&[&["ONLY"]]);
522 assert_eq!(dict.word_details(0), vec!["ONLY"]);
523 }
524
525 /// Empty fields are preserved rather than collapsed.
526 #[test]
527 fn user_word_details_preserves_empty_fields() {
528 let dict = user_dictionary(&[&["", "a", "", "b", ""]]);
529 assert_eq!(dict.word_details(0), vec!["", "a", "", "b", ""]);
530 }
531
532 /// Out-of-range ids fall back to the `UNK` sentinel -- note this differs
533 /// from `Dictionary::word_details`, which returns an empty vector.
534 #[test]
535 fn user_word_details_out_of_range_returns_unk() {
536 let dict = user_dictionary(&[&["名詞", "一般"]]);
537 assert_eq!(dict.word_details(1), UNK.to_vec());
538 assert_eq!(dict.word_details(usize::MAX / 4), UNK.to_vec());
539 }
540
541 /// Invalid UTF-8 falls back to the `UNK` sentinel rather than panicking.
542 #[test]
543 fn user_word_details_invalid_utf8_returns_unk() {
544 let mut dict = user_dictionary(&[&["ok", "fields"]]);
545 let words_data: &[u8] = &dict.dict.words_data;
546 let mut bytes = words_data.to_vec();
547 let last = bytes.len() - 1;
548 bytes[last] = 0xff;
549 dict.dict.words_data = bytes.into();
550 assert_eq!(dict.word_details(0), UNK.to_vec());
551 }
552
553 /// The iterator and the allocating accessor must agree field for field --
554 /// the latter is now implemented as `collect()` over the former, so this
555 /// pins that the delegation did not change what callers see.
556 #[test]
557 fn user_word_details_iter_matches_word_details() {
558 let dict = user_dictionary(&[
559 &["カスタム名詞", "*", "リンデラ"],
560 &["動詞", "自立", "*"],
561 &["ONLY"],
562 ]);
563
564 for word_id in 0..3 {
565 let via_iter: Vec<&str> = dict.word_details_iter(word_id).collect();
566 assert_eq!(via_iter, dict.word_details(word_id), "word_id {word_id}");
567 }
568 }
569
570 /// `DetailFields` reports its length before anything is consumed, which
571 /// is what lets a caller size its buffer in one shot. If this stopped
572 /// being exact, `Token::ensure_details` would silently start
573 /// reallocating.
574 #[test]
575 fn user_word_details_iter_reports_an_exact_length() {
576 let dict = user_dictionary(&[&["a", "b", "c"], &["only"]]);
577
578 let mut fields = dict.word_details_iter(0);
579 assert_eq!(fields.len(), 3);
580 assert_eq!(fields.next(), Some("a"));
581 assert_eq!(fields.len(), 2, "len must track consumption");
582 assert_eq!(fields.count(), 2);
583
584 assert_eq!(dict.word_details_iter(1).len(), 1);
585 // The out-of-range sentinel is one field, not zero.
586 assert_eq!(dict.word_details_iter(99).len(), 1);
587 }
588
589 /// A word id whose index entry points past `words_data` used to panic on
590 /// an unchecked slice. It now falls back to the sentinel, matching what
591 /// `UnknownDictionary` has always done and the repo's no-panic rule.
592 #[test]
593 fn user_word_details_corrupt_offset_falls_back_instead_of_panicking() {
594 let mut dict = user_dictionary(&[&["名詞", "一般"]]);
595
596 // Point word id 0's index entry far past the end of `words_data`.
597 let idx: &[u8] = &dict.dict.words_idx_data;
598 let mut idx_bytes = idx.to_vec();
599 let words_len: &[u8] = &dict.dict.words_data;
600 let past_end = (words_len.len() as u32) + 1_000;
601 idx_bytes[0..4].copy_from_slice(&past_end.to_le_bytes());
602 dict.dict.words_idx_data = idx_bytes.into();
603
604 assert_eq!(dict.word_details(0), UNK.to_vec());
605 assert_eq!(dict.word_details_iter(0).collect::<Vec<_>>(), vec!["UNK"]);
606 }
607
608 /// `DetailFields::empty` yields nothing and `DetailFields::unk` yields
609 /// exactly the sentinel, and the two are distinct -- `Dictionary` uses
610 /// the first for an out-of-range id where `UserDictionary` uses the
611 /// second.
612 #[test]
613 fn detail_fields_constructors() {
614 assert_eq!(
615 DetailFields::empty().collect::<Vec<_>>(),
616 Vec::<&str>::new()
617 );
618 assert_eq!(DetailFields::empty().len(), 0);
619 assert_eq!(DetailFields::unk().collect::<Vec<_>>(), UNK.to_vec());
620 assert_eq!(DetailFields::unk().len(), 1);
621 }
622}