1use std::borrow::Cow;
2use std::collections::{btree_map::Entry, BTreeSet, HashMap};
3use std::error::Error;
4use std::fmt;
5use std::fmt::Write as _;
6use std::fs::File;
7use std::io::{Read, Write};
8use std::path::Path;
9use std::string::FromUtf8Error;
10use std::sync::Arc;
11
12use fst::{Map, MapBuilder, Streamer};
13use memmap2::Mmap;
14use moine_core::Lattice;
15use serde::{Deserialize, Serialize};
16use sha2::{Digest, Sha256};
17
18use crate::romaji::{
19 can_build_direct_romaji_path, romaji_paths_from_segmented_readings,
20 romaji_symbol_paths_from_segmented_readings, JaLatticeError, RomajiSegmentMode,
21};
22
23const SURFACE_COLUMN: usize = 0;
24const POS1_COLUMN: usize = 4;
25const LFORM_COLUMN: usize = 10;
26const PRON_COLUMN: usize = 13;
27const ARTIFACT_PAYLOAD_SCHEMA_VERSION: u32 = 1;
28const ARTIFACT_PAYLOAD_TYPE: &str = "moine.unidic.reading-index.surface-readings";
29const BINARY_ARTIFACT_MAGIC: &[u8; 8] = b"MOINEU01";
30const BINARY_ARTIFACT_VERSION: u32 = 1;
31const INDEXED_ARTIFACT_MAGIC: &[u8; 8] = b"MOINEI01";
32const INDEXED_ARTIFACT_VERSION: u32 = 1;
33const INDEXED_ARTIFACT_HEADER_LEN: usize = 40;
34const MAX_ARTIFACT_PAYLOAD_BYTES: u64 = 512 * 1024 * 1024;
35const MAX_ARTIFACT_ENTRIES: usize = 3_000_000;
36const MAX_ARTIFACT_READINGS_PER_ENTRY: usize = 256;
37const MAX_ARTIFACT_STRING_BYTES: usize = 16 * 1024;
38const DEFAULT_QUERY_SPAN_CHARS: usize = 8;
39const DEFAULT_QUERY_PATHS: usize = 1024;
40const MAX_QUERY_SPAN_CHARS: usize = DEFAULT_QUERY_SPAN_CHARS * 32;
43const MAX_QUERY_PATHS: usize = DEFAULT_QUERY_PATHS * 16;
44const MAX_QUERY_READINGS_PER_SEGMENT: usize = MAX_ARTIFACT_READINGS_PER_ENTRY;
45pub const ARTIFACT_PAYLOAD_CHECKSUM_ALGORITHM: &str = "sha256-canonical-v1";
47pub const LEGACY_ARTIFACT_PAYLOAD_CHECKSUM_ALGORITHM: &str = "fnv1a64-canonical-v1";
49pub const ARTIFACT_PAYLOAD_FILE_DIGEST_ALGORITHM: &str = "sha256-file-v1";
51
52#[derive(Clone, Debug)]
54pub struct UnidicReadingIndex {
55 storage: UnidicReadingStorage,
56}
57
58#[derive(Clone, Debug)]
59enum UnidicReadingStorage {
60 Eager(HashMap<String, Vec<String>>),
61 Indexed(IndexedUnidicPayload),
62}
63
64impl Default for UnidicReadingIndex {
65 fn default() -> Self {
66 Self {
67 storage: UnidicReadingStorage::Eager(HashMap::new()),
68 }
69 }
70}
71
72impl PartialEq for UnidicReadingIndex {
73 fn eq(&self, other: &Self) -> bool {
74 self.artifact_payload() == other.artifact_payload()
75 }
76}
77
78impl Eq for UnidicReadingIndex {}
79
80#[derive(Clone, Debug)]
81struct IndexedUnidicPayload {
82 mmap: Arc<Mmap>,
83 map: Map<Vec<u8>>,
84 readings_start: usize,
85 entries: usize,
86}
87
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub struct UnidicIndexedArtifactPayloadHeader {
91 pub version: u32,
93 pub entries: usize,
95 pub fst_len: usize,
97 pub readings_len: usize,
99}
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub struct UnidicBinaryArtifactPayloadHeader {
104 pub version: u32,
106 pub entries: usize,
108}
109
110#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub struct DictionaryReadingOptions {
113 pub max_span_chars: usize,
115 pub max_paths: usize,
117 pub longest_match_only: bool,
119 pub max_readings_per_segment: Option<usize>,
121}
122
123#[derive(Clone, Debug, Eq, PartialEq)]
125pub struct DictionaryReadingSegment {
126 pub surface: String,
128 pub reading: String,
130 pub source: DictionaryReadingSegmentSource,
132}
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136pub enum DictionaryReadingSegmentSource {
137 Dictionary,
139 Direct,
141}
142
143#[derive(Clone, Debug, Eq, PartialEq)]
145pub struct DictionaryReadingPath {
146 pub segments: Vec<DictionaryReadingSegment>,
148 pub joined_reading: String,
150}
151
152#[derive(Clone, Debug, Default, Eq, PartialEq)]
154pub struct DictionaryReadingExpansion {
155 pub paths: Vec<DictionaryReadingPath>,
157 pub stats: DictionaryReadingStats,
159}
160
161#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
163pub struct DictionaryReadingStats {
164 pub matched_spans: usize,
166 pub direct_fallback_spans: usize,
168 pub longest_match_pruned_spans: usize,
170 pub raw_segment_readings: usize,
172 pub used_segment_readings: usize,
174 pub pruned_segment_readings: usize,
176 pub candidate_combinations: usize,
178 pub unique_paths: usize,
180 pub duplicate_joined_readings: usize,
182 pub max_paths_hit_count: usize,
184}
185
186pub fn romaji_lattice_from_reading_paths(
188 paths: &[DictionaryReadingPath],
189) -> Result<Lattice, JaLatticeError> {
190 if paths.is_empty() {
191 return Err(JaLatticeError::EmptyReadings);
192 }
193
194 let paths = romaji_symbol_paths_from_segmented_readings(
195 paths
196 .iter()
197 .map(|path| path.segments.iter().map(romaji_segment_input)),
198 )?;
199 Ok(Lattice::from_symbol_paths_compact(paths))
200}
201
202pub fn romaji_paths_from_reading_paths(
204 paths: &[DictionaryReadingPath],
205) -> Result<Vec<String>, JaLatticeError> {
206 if paths.is_empty() {
207 return Err(JaLatticeError::EmptyReadings);
208 }
209
210 romaji_paths_from_segmented_readings(
211 paths
212 .iter()
213 .map(|path| path.segments.iter().map(romaji_segment_input)),
214 )
215}
216
217fn romaji_segment_input(segment: &DictionaryReadingSegment) -> (&str, RomajiSegmentMode) {
218 let mode = match segment.source {
219 DictionaryReadingSegmentSource::Dictionary => RomajiSegmentMode::Reading,
220 DictionaryReadingSegmentSource::Direct => RomajiSegmentMode::Surface,
221 };
222 (segment.reading.as_str(), mode)
223}
224
225#[derive(Clone, Copy, Debug, Eq, PartialEq)]
227pub enum UnidicReadingField {
228 LForm,
230 Pron,
232}
233
234#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
236pub struct UnidicArtifactMetadata {
237 pub schema_version: u32,
239 pub artifact_type: String,
241 pub artifact_name: String,
243 pub generator: String,
245 pub payload: UnidicArtifactPayload,
247 pub source: UnidicArtifactSource,
249 pub build: UnidicArtifactBuild,
251 pub query_defaults: UnidicArtifactQueryDefaults,
253 pub license: UnidicArtifactLicense,
255}
256
257#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
259pub struct UnidicArtifactPayload {
260 pub path: String,
262 pub format: String,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub file_digest_algorithm: Option<String>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub file_digest: Option<String>,
270 pub checksum_algorithm: String,
272 pub checksum: String,
274}
275
276#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
278pub struct UnidicArtifactSource {
279 pub name: String,
281 pub version: String,
283 pub lex_csv: String,
285}
286
287#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
289pub struct UnidicArtifactBuild {
290 pub reading_field: String,
292 pub max_readings_per_surface: Option<usize>,
294 pub exclude_ascii_surfaces: bool,
296 pub exclude_symbol_pos: bool,
298 #[serde(default, skip_serializing_if = "is_false")]
300 pub include_normalized_surfaces: bool,
301 #[serde(default, skip_serializing_if = "is_false")]
303 pub exclude_unsupported_readings: bool,
304 pub entries: usize,
306}
307
308#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
310pub struct UnidicArtifactQueryDefaults {
311 pub max_span_chars: usize,
313 pub max_paths: usize,
315 pub longest_match_only: bool,
317 pub max_readings_per_segment: Option<usize>,
319}
320
321#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
323pub struct UnidicArtifactLicense {
324 pub selected_license: String,
326 pub references: Vec<UnidicArtifactLicenseReference>,
328}
329
330#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
332pub struct UnidicArtifactLicenseReference {
333 pub label: String,
335 pub path: String,
337}
338
339#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
341pub struct UnidicReadingIndexPayload {
342 pub schema_version: u32,
344 pub payload_type: String,
346 pub entries: Vec<UnidicReadingIndexPayloadEntry>,
348}
349
350#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
352pub struct UnidicReadingIndexPayloadEntry {
353 pub surface: String,
355 pub readings: Vec<String>,
357}
358
359#[derive(Clone, Debug, Eq, PartialEq)]
361pub struct UnidicArtifactMetadataOptions {
362 pub artifact_name: String,
364 pub generator: String,
366 pub payload_file_name: String,
368 pub payload_format: String,
370 pub source_name: String,
372 pub source_version: String,
374 pub source_lex_csv: String,
376 pub index_options: UnidicIndexOptions,
378 pub query_defaults: DictionaryReadingOptions,
380 pub license: UnidicArtifactLicense,
382}
383
384#[derive(Clone, Copy, Debug, Eq, PartialEq)]
386pub struct UnidicIndexOptions {
387 pub reading_field: UnidicReadingField,
389 pub max_readings_per_surface: Option<usize>,
391 pub exclude_ascii_surfaces: bool,
393 pub exclude_symbol_pos: bool,
395}
396
397impl UnidicReadingField {
398 fn column(self) -> usize {
399 match self {
400 Self::LForm => LFORM_COLUMN,
401 Self::Pron => PRON_COLUMN,
402 }
403 }
404
405 pub fn as_str(self) -> &'static str {
407 match self {
408 Self::LForm => "lform",
409 Self::Pron => "pron",
410 }
411 }
412}
413
414impl Default for UnidicIndexOptions {
415 fn default() -> Self {
416 Self {
417 reading_field: UnidicReadingField::Pron,
418 max_readings_per_surface: None,
419 exclude_ascii_surfaces: true,
420 exclude_symbol_pos: true,
421 }
422 }
423}
424
425impl Default for DictionaryReadingOptions {
426 fn default() -> Self {
427 Self {
428 max_span_chars: DEFAULT_QUERY_SPAN_CHARS,
429 max_paths: DEFAULT_QUERY_PATHS,
430 longest_match_only: false,
431 max_readings_per_segment: None,
432 }
433 }
434}
435
436impl DictionaryReadingOptions {
437 pub fn validate(self) -> Result<Self, UnidicArtifactPayloadError> {
440 check_limit("max_span_chars", self.max_span_chars, MAX_QUERY_SPAN_CHARS)?;
441 check_limit("max_paths", self.max_paths, MAX_QUERY_PATHS)?;
442 if let Some(max_readings) = self.max_readings_per_segment {
443 check_limit(
444 "max_readings_per_segment",
445 max_readings,
446 MAX_QUERY_READINGS_PER_SEGMENT,
447 )?;
448 }
449 Ok(self)
450 }
451}
452
453impl Default for UnidicArtifactLicense {
454 fn default() -> Self {
455 Self {
456 selected_license: "BSD-3-Clause".to_string(),
457 references: vec![
458 UnidicArtifactLicenseReference {
459 label: "BSD".to_string(),
460 path: "license/BSD".to_string(),
461 },
462 UnidicArtifactLicenseReference {
463 label: "COPYING".to_string(),
464 path: "license/COPYING".to_string(),
465 },
466 ],
467 }
468 }
469}
470
471#[derive(Debug)]
473pub enum UnidicCsvError {
474 Csv(csv::Error),
476 Io(std::io::Error),
478 MissingColumn {
480 record_index: u64,
482 column: usize,
484 len: usize,
486 },
487}
488
489#[derive(Debug)]
491pub enum UnidicArtifactPayloadError {
492 Io(std::io::Error),
494 Yaml(serde_yaml::Error),
496 InvalidBinaryMagic {
498 magic: [u8; 8],
500 },
501 UnsupportedBinaryVersion {
503 version: u32,
505 },
506 NonZeroBinaryReserved {
508 value: u32,
510 },
511 TruncatedBinary {
513 field: &'static str,
515 },
516 InvalidBinaryUtf8 {
518 field: &'static str,
520 source: FromUtf8Error,
522 },
523 BinaryValueTooLarge {
525 field: &'static str,
527 len: usize,
529 },
530 BinaryEntryCountTooLarge {
532 entries: u64,
534 },
535 ArtifactLimitExceeded {
537 field: &'static str,
539 len: u64,
541 max: u64,
543 },
544 InvalidIndexedMagic {
546 magic: [u8; 8],
548 },
549 UnsupportedIndexedVersion {
551 version: u32,
553 },
554 NonZeroIndexedReserved {
556 value: u32,
558 },
559 TruncatedIndexed {
561 field: &'static str,
563 },
564 InvalidIndexedFst {
566 message: String,
568 },
569 IndexedSectionTooLarge {
571 field: &'static str,
573 len: u64,
575 },
576 InvalidIndexedOffset {
578 offset: u64,
580 },
581 InvalidIndexedUtf8 {
583 field: &'static str,
585 source: std::str::Utf8Error,
587 },
588 IndexedEntryCountMismatch {
590 header_entries: usize,
592 fst_entries: usize,
594 },
595 UnsupportedSchemaVersion {
597 version: u32,
599 },
600 UnsupportedPayloadType {
602 payload_type: String,
604 },
605 EmptySurface {
607 entry_index: usize,
609 },
610 DuplicateSurface {
612 surface: String,
614 },
615 EmptyReadings {
617 surface: String,
619 },
620 EmptyReading {
622 surface: String,
624 reading_index: usize,
626 },
627 DuplicateReading {
629 surface: String,
631 reading: String,
633 },
634}
635
636impl fmt::Display for UnidicCsvError {
637 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
638 match self {
639 Self::Csv(err) => write!(f, "invalid UniDic CSV: {err}"),
640 Self::Io(err) => write!(f, "failed to read UniDic CSV: {err}"),
641 Self::MissingColumn {
642 record_index,
643 column,
644 len,
645 } => write!(
646 f,
647 "UniDic CSV record {record_index} has no column {column}; record has {len} columns"
648 ),
649 }
650 }
651}
652
653impl Error for UnidicCsvError {}
654
655impl fmt::Display for UnidicArtifactPayloadError {
656 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657 match self {
658 Self::Io(err) => write!(f, "failed to read UniDic artifact payload: {err}"),
659 Self::Yaml(err) => write!(f, "invalid UniDic artifact payload YAML: {err}"),
660 Self::InvalidBinaryMagic { magic } => {
661 write!(f, "invalid UniDic binary artifact magic {magic:?}")
662 }
663 Self::UnsupportedBinaryVersion { version } => {
664 write!(f, "unsupported UniDic binary artifact version {version}")
665 }
666 Self::NonZeroBinaryReserved { value } => {
667 write!(f, "UniDic binary artifact reserved header field is {value}")
668 }
669 Self::TruncatedBinary { field } => {
670 write!(f, "truncated UniDic binary artifact while reading {field}")
671 }
672 Self::InvalidBinaryUtf8 { field, source } => {
673 write!(f, "invalid UTF-8 in UniDic binary artifact {field}: {source}")
674 }
675 Self::BinaryValueTooLarge { field, len } => write!(
676 f,
677 "UniDic binary artifact {field} length {len} exceeds u32::MAX"
678 ),
679 Self::BinaryEntryCountTooLarge { entries } => write!(
680 f,
681 "UniDic binary artifact entry count {entries} exceeds usize::MAX"
682 ),
683 Self::ArtifactLimitExceeded { field, len, max } => write!(
684 f,
685 "UniDic artifact {field} length/count {len} exceeds limit {max}"
686 ),
687 Self::InvalidIndexedMagic { magic } => {
688 write!(f, "invalid UniDic indexed artifact magic {magic:?}")
689 }
690 Self::UnsupportedIndexedVersion { version } => {
691 write!(f, "unsupported UniDic indexed artifact version {version}")
692 }
693 Self::NonZeroIndexedReserved { value } => {
694 write!(f, "UniDic indexed artifact reserved header field is {value}")
695 }
696 Self::TruncatedIndexed { field } => {
697 write!(f, "truncated UniDic indexed artifact while reading {field}")
698 }
699 Self::InvalidIndexedFst { message } => {
700 write!(f, "invalid UniDic indexed artifact FST: {message}")
701 }
702 Self::IndexedSectionTooLarge { field, len } => write!(
703 f,
704 "UniDic indexed artifact {field} length {len} exceeds usize::MAX"
705 ),
706 Self::InvalidIndexedOffset { offset } => {
707 write!(f, "invalid UniDic indexed artifact readings offset {offset}")
708 }
709 Self::InvalidIndexedUtf8 { field, source } => {
710 write!(f, "invalid UTF-8 in UniDic indexed artifact {field}: {source}")
711 }
712 Self::IndexedEntryCountMismatch {
713 header_entries,
714 fst_entries,
715 } => write!(
716 f,
717 "UniDic indexed artifact header entry count {header_entries} does not match FST entry count {fst_entries}"
718 ),
719 Self::UnsupportedSchemaVersion { version } => write!(
720 f,
721 "unsupported UniDic artifact payload schema version {version}"
722 ),
723 Self::UnsupportedPayloadType { payload_type } => {
724 write!(f, "unsupported UniDic artifact payload type {payload_type:?}")
725 }
726 Self::EmptySurface { entry_index } => write!(
727 f,
728 "UniDic artifact payload entry {entry_index} has an empty surface"
729 ),
730 Self::DuplicateSurface { surface } => {
731 write!(f, "UniDic artifact payload has duplicate surface {surface:?}")
732 }
733 Self::EmptyReadings { surface } => write!(
734 f,
735 "UniDic artifact payload surface {surface:?} has no readings"
736 ),
737 Self::EmptyReading {
738 surface,
739 reading_index,
740 } => write!(
741 f,
742 "UniDic artifact payload surface {surface:?} has an empty reading at index {reading_index}"
743 ),
744 Self::DuplicateReading { surface, reading } => write!(
745 f,
746 "UniDic artifact payload surface {surface:?} has duplicate reading {reading:?}"
747 ),
748 }
749 }
750}
751
752impl Error for UnidicArtifactPayloadError {
753 fn source(&self) -> Option<&(dyn Error + 'static)> {
754 match self {
755 Self::Io(err) => Some(err),
756 Self::Yaml(err) => Some(err),
757 Self::InvalidBinaryUtf8 { source, .. } => Some(source),
758 Self::InvalidIndexedUtf8 { source, .. } => Some(source),
759 _ => None,
760 }
761 }
762}
763
764impl From<csv::Error> for UnidicCsvError {
765 fn from(err: csv::Error) -> Self {
766 Self::Csv(err)
767 }
768}
769
770impl From<std::io::Error> for UnidicCsvError {
771 fn from(err: std::io::Error) -> Self {
772 Self::Io(err)
773 }
774}
775
776impl From<std::io::Error> for UnidicArtifactPayloadError {
777 fn from(err: std::io::Error) -> Self {
778 Self::Io(err)
779 }
780}
781
782impl From<serde_yaml::Error> for UnidicArtifactPayloadError {
783 fn from(err: serde_yaml::Error) -> Self {
784 Self::Yaml(err)
785 }
786}
787
788impl UnidicReadingIndex {
789 pub fn from_lex_csv_path(path: impl AsRef<Path>) -> Result<Self, UnidicCsvError> {
791 Self::from_lex_csv_path_with_options(path, UnidicIndexOptions::default())
792 }
793
794 pub fn from_lex_csv_path_with_field(
796 path: impl AsRef<Path>,
797 field: UnidicReadingField,
798 ) -> Result<Self, UnidicCsvError> {
799 Self::from_lex_csv_path_with_options(
800 path,
801 UnidicIndexOptions {
802 reading_field: field,
803 ..UnidicIndexOptions::default()
804 },
805 )
806 }
807
808 pub fn from_lex_csv_path_with_options(
810 path: impl AsRef<Path>,
811 options: UnidicIndexOptions,
812 ) -> Result<Self, UnidicCsvError> {
813 let file = File::open(path)?;
814 Self::from_lex_csv_reader_with_options(file, options)
815 }
816
817 pub fn from_lex_csv_reader(reader: impl Read) -> Result<Self, UnidicCsvError> {
819 Self::from_lex_csv_reader_with_options(reader, UnidicIndexOptions::default())
820 }
821
822 pub fn from_lex_csv_reader_with_field(
824 reader: impl Read,
825 reading_field: UnidicReadingField,
826 ) -> Result<Self, UnidicCsvError> {
827 Self::from_lex_csv_reader_with_options(
828 reader,
829 UnidicIndexOptions {
830 reading_field,
831 ..UnidicIndexOptions::default()
832 },
833 )
834 }
835
836 pub fn from_lex_csv_reader_with_options(
838 reader: impl Read,
839 options: UnidicIndexOptions,
840 ) -> Result<Self, UnidicCsvError> {
841 let mut by_surface = HashMap::<String, BTreeSet<String>>::new();
842 for record in lex_csv_reader(reader).records() {
843 let record = record?;
844 let surface = field(&record, SURFACE_COLUMN)?;
845 let reading = field(&record, options.reading_field.column())?;
846
847 if surface == "*" || reading == "*" {
848 continue;
849 }
850 if options.exclude_ascii_surfaces && surface.is_ascii() {
851 continue;
852 }
853 if options.exclude_symbol_pos && is_symbol_pos(field(&record, POS1_COLUMN)?) {
854 continue;
855 }
856
857 insert_surface_reading(&mut by_surface, surface, reading);
858 if let Some(normalized_surface) = normalize_ascii_width(surface) {
859 insert_surface_reading(&mut by_surface, &normalized_surface, reading);
860 }
861 }
862
863 let readings_by_surface = by_surface
864 .into_iter()
865 .map(|(surface, readings)| {
866 let mut readings = readings.into_iter().collect::<Vec<_>>();
867 if let Some(max_readings) = options.max_readings_per_surface {
868 readings.truncate(max_readings);
869 }
870 (surface, readings)
871 })
872 .filter(|(_, readings)| !readings.is_empty())
873 .collect();
874
875 Ok(Self::from_readings_by_surface(readings_by_surface))
876 }
877
878 pub fn from_artifact_payload_path(
880 path: impl AsRef<Path>,
881 ) -> Result<Self, UnidicArtifactPayloadError> {
882 let path = path.as_ref();
883 check_payload_file_size(path)?;
884 let file = File::open(path)?;
885 Self::from_artifact_payload_reader(file)
886 }
887
888 pub fn from_artifact_payload_reader(
890 reader: impl Read,
891 ) -> Result<Self, UnidicArtifactPayloadError> {
892 let payload = serde_yaml::from_reader(reader)?;
893 Self::from_artifact_payload(payload)
894 }
895
896 pub fn from_artifact_payload(
898 payload: UnidicReadingIndexPayload,
899 ) -> Result<Self, UnidicArtifactPayloadError> {
900 validate_artifact_payload_header(&payload)?;
901 check_limit("entry_count", payload.entries.len(), MAX_ARTIFACT_ENTRIES)?;
902
903 let mut readings_by_surface = HashMap::new();
904 for (entry_index, entry) in payload.entries.into_iter().enumerate() {
905 check_limit(
906 "surface_bytes",
907 entry.surface.len(),
908 MAX_ARTIFACT_STRING_BYTES,
909 )?;
910 check_limit(
911 "reading_count",
912 entry.readings.len(),
913 MAX_ARTIFACT_READINGS_PER_ENTRY,
914 )?;
915 if entry.surface.is_empty() {
916 return Err(UnidicArtifactPayloadError::EmptySurface { entry_index });
917 }
918 if entry.readings.is_empty() {
919 return Err(UnidicArtifactPayloadError::EmptyReadings {
920 surface: entry.surface,
921 });
922 }
923
924 let mut seen_readings = BTreeSet::new();
925 for (reading_index, reading) in entry.readings.iter().enumerate() {
926 check_limit("reading_bytes", reading.len(), MAX_ARTIFACT_STRING_BYTES)?;
927 if reading.is_empty() {
928 return Err(UnidicArtifactPayloadError::EmptyReading {
929 surface: entry.surface,
930 reading_index,
931 });
932 }
933 if !seen_readings.insert(reading) {
934 return Err(UnidicArtifactPayloadError::DuplicateReading {
935 surface: entry.surface,
936 reading: reading.clone(),
937 });
938 }
939 }
940
941 if readings_by_surface
942 .insert(entry.surface.clone(), entry.readings)
943 .is_some()
944 {
945 return Err(UnidicArtifactPayloadError::DuplicateSurface {
946 surface: entry.surface,
947 });
948 }
949 }
950
951 Ok(Self::from_readings_by_surface(readings_by_surface))
952 }
953
954 pub fn from_binary_artifact_payload_path(
956 path: impl AsRef<Path>,
957 ) -> Result<Self, UnidicArtifactPayloadError> {
958 let path = path.as_ref();
959 check_payload_file_size(path)?;
960 let file = File::open(path)?;
961 Self::from_binary_artifact_payload_reader(file)
962 }
963
964 pub fn from_binary_artifact_payload_reader(
966 mut reader: impl Read,
967 ) -> Result<Self, UnidicArtifactPayloadError> {
968 let header = read_binary_artifact_payload_header(&mut reader)?;
969 check_limit("entry_count", header.entries, MAX_ARTIFACT_ENTRIES)?;
970 let mut entries = Vec::with_capacity(header.entries);
971 for _ in 0..header.entries {
972 let surface = read_binary_string(&mut reader, "surface")?;
973 let reading_count = read_u32_le(&mut reader, "reading_count")?;
974 let reading_count = usize::try_from(reading_count).expect("u32 fits usize");
975 check_limit(
976 "reading_count",
977 reading_count,
978 MAX_ARTIFACT_READINGS_PER_ENTRY,
979 )?;
980 let mut readings = Vec::with_capacity(reading_count);
981 for _ in 0..reading_count {
982 readings.push(read_binary_string(&mut reader, "reading")?);
983 }
984 entries.push(UnidicReadingIndexPayloadEntry { surface, readings });
985 }
986
987 Self::from_artifact_payload(UnidicReadingIndexPayload {
988 schema_version: ARTIFACT_PAYLOAD_SCHEMA_VERSION,
989 payload_type: ARTIFACT_PAYLOAD_TYPE.to_string(),
990 entries,
991 })
992 }
993
994 pub fn from_indexed_artifact_payload_path(
996 path: impl AsRef<Path>,
997 ) -> Result<Self, UnidicArtifactPayloadError> {
998 let path = path.as_ref();
999 check_payload_file_size(path)?;
1000 let file = File::open(path)?;
1001 let mmap = unsafe { Mmap::map(&file)? };
1004 Self::from_indexed_mmap(mmap)
1005 }
1006
1007 pub fn from_indexed_artifact_payload_bytes(
1018 bytes: &[u8],
1019 ) -> Result<Self, UnidicArtifactPayloadError> {
1020 if bytes.len() as u64 > MAX_ARTIFACT_PAYLOAD_BYTES {
1021 return Err(UnidicArtifactPayloadError::ArtifactLimitExceeded {
1022 field: "payload_bytes",
1023 len: bytes.len() as u64,
1024 max: MAX_ARTIFACT_PAYLOAD_BYTES,
1025 });
1026 }
1027 let header = read_indexed_artifact_payload_header_bytes(bytes)?;
1028 let fst_start = INDEXED_ARTIFACT_HEADER_LEN;
1029 let fst_end = fst_start.checked_add(header.fst_len).ok_or(
1030 UnidicArtifactPayloadError::TruncatedIndexed {
1031 field: "fst_section",
1032 },
1033 )?;
1034 let readings_end = fst_end.checked_add(header.readings_len).ok_or(
1035 UnidicArtifactPayloadError::TruncatedIndexed {
1036 field: "readings_section",
1037 },
1038 )?;
1039 if bytes.len() < readings_end {
1040 return Err(UnidicArtifactPayloadError::TruncatedIndexed {
1041 field: "indexed_payload",
1042 });
1043 }
1044
1045 let map = Map::new(bytes[fst_start..fst_end].to_vec()).map_err(|err| {
1046 UnidicArtifactPayloadError::InvalidIndexedFst {
1047 message: err.to_string(),
1048 }
1049 })?;
1050 let fst_entries = map.len();
1051 if fst_entries != header.entries {
1052 return Err(UnidicArtifactPayloadError::IndexedEntryCountMismatch {
1053 header_entries: header.entries,
1054 fst_entries,
1055 });
1056 }
1057
1058 let mut entries = Vec::with_capacity(header.entries);
1059 let mut stream = map.stream();
1060 while let Some((surface, offset)) = stream.next() {
1061 let surface = std::str::from_utf8(surface)
1062 .map_err(|source| UnidicArtifactPayloadError::InvalidIndexedUtf8 {
1063 field: "surface",
1064 source,
1065 })?
1066 .to_string();
1067 let readings = read_indexed_readings_at_bytes(bytes, fst_end, offset)?;
1068 entries.push(UnidicReadingIndexPayloadEntry { surface, readings });
1069 }
1070
1071 Self::from_artifact_payload(UnidicReadingIndexPayload {
1072 schema_version: ARTIFACT_PAYLOAD_SCHEMA_VERSION,
1073 payload_type: ARTIFACT_PAYLOAD_TYPE.to_string(),
1074 entries,
1075 })
1076 }
1077
1078 fn from_indexed_mmap(mmap: Mmap) -> Result<Self, UnidicArtifactPayloadError> {
1079 if mmap.len() as u64 > MAX_ARTIFACT_PAYLOAD_BYTES {
1080 return Err(UnidicArtifactPayloadError::ArtifactLimitExceeded {
1081 field: "payload_bytes",
1082 len: mmap.len() as u64,
1083 max: MAX_ARTIFACT_PAYLOAD_BYTES,
1084 });
1085 }
1086 let header = read_indexed_artifact_payload_header_bytes(&mmap)?;
1087 let fst_start = INDEXED_ARTIFACT_HEADER_LEN;
1088 let fst_end = fst_start.checked_add(header.fst_len).ok_or(
1089 UnidicArtifactPayloadError::TruncatedIndexed {
1090 field: "fst_section",
1091 },
1092 )?;
1093 let readings_end = fst_end.checked_add(header.readings_len).ok_or(
1094 UnidicArtifactPayloadError::TruncatedIndexed {
1095 field: "readings_section",
1096 },
1097 )?;
1098 if mmap.len() < readings_end {
1099 return Err(UnidicArtifactPayloadError::TruncatedIndexed {
1100 field: "indexed_payload",
1101 });
1102 }
1103
1104 let map = Map::new(mmap[fst_start..fst_end].to_vec()).map_err(|err| {
1105 UnidicArtifactPayloadError::InvalidIndexedFst {
1106 message: err.to_string(),
1107 }
1108 })?;
1109 let fst_entries = map.len();
1110 if fst_entries != header.entries {
1111 return Err(UnidicArtifactPayloadError::IndexedEntryCountMismatch {
1112 header_entries: header.entries,
1113 fst_entries,
1114 });
1115 }
1116
1117 let indexed = IndexedUnidicPayload {
1118 mmap: Arc::new(mmap),
1119 map,
1120 readings_start: fst_end,
1121 entries: header.entries,
1122 };
1123 indexed.validate()?;
1124 Ok(Self {
1125 storage: UnidicReadingStorage::Indexed(indexed),
1126 })
1127 }
1128
1129 pub fn binary_artifact_payload_header_path(
1131 path: impl AsRef<Path>,
1132 ) -> Result<UnidicBinaryArtifactPayloadHeader, UnidicArtifactPayloadError> {
1133 let file = File::open(path)?;
1134 Self::binary_artifact_payload_header_reader(file)
1135 }
1136
1137 pub fn binary_artifact_payload_header_reader(
1139 mut reader: impl Read,
1140 ) -> Result<UnidicBinaryArtifactPayloadHeader, UnidicArtifactPayloadError> {
1141 read_binary_artifact_payload_header(&mut reader)
1142 }
1143
1144 pub(crate) fn from_readings_by_surface(
1145 readings_by_surface: HashMap<String, Vec<String>>,
1146 ) -> Self {
1147 Self {
1148 storage: UnidicReadingStorage::Eager(readings_by_surface),
1149 }
1150 }
1151
1152 pub fn readings(&self, surface: &str) -> Option<Cow<'_, [String]>> {
1158 self.try_readings(surface).ok().flatten()
1159 }
1160
1161 pub fn try_readings(
1164 &self,
1165 surface: &str,
1166 ) -> Result<Option<Cow<'_, [String]>>, UnidicArtifactPayloadError> {
1167 if let Some(readings) = self.try_readings_exact(surface)? {
1168 return Ok(Some(readings));
1169 }
1170
1171 let Some(normalized_surface) = normalize_ascii_width(surface) else {
1172 return Ok(None);
1173 };
1174 if normalized_surface == surface {
1175 return Ok(None);
1176 }
1177
1178 self.try_readings_exact(&normalized_surface)
1179 }
1180
1181 fn try_readings_exact(
1182 &self,
1183 surface: &str,
1184 ) -> Result<Option<Cow<'_, [String]>>, UnidicArtifactPayloadError> {
1185 match &self.storage {
1186 UnidicReadingStorage::Eager(readings_by_surface) => Ok(readings_by_surface
1187 .get(surface)
1188 .map(|readings| Cow::Borrowed(readings.as_slice()))),
1189 UnidicReadingStorage::Indexed(indexed) => indexed
1190 .readings(surface)
1191 .map(|readings| readings.map(Cow::Owned)),
1192 }
1193 }
1194
1195 pub fn len(&self) -> usize {
1197 match &self.storage {
1198 UnidicReadingStorage::Eager(readings_by_surface) => readings_by_surface.len(),
1199 UnidicReadingStorage::Indexed(indexed) => indexed.entries,
1200 }
1201 }
1202
1203 pub fn is_empty(&self) -> bool {
1205 self.len() == 0
1206 }
1207
1208 pub fn artifact_metadata(
1214 &self,
1215 options: UnidicArtifactMetadataOptions,
1216 ) -> UnidicArtifactMetadata {
1217 let build = UnidicArtifactBuild {
1218 reading_field: options.index_options.reading_field.as_str().to_string(),
1219 max_readings_per_surface: options.index_options.max_readings_per_surface,
1220 exclude_ascii_surfaces: options.index_options.exclude_ascii_surfaces,
1221 exclude_symbol_pos: options.index_options.exclude_symbol_pos,
1222 include_normalized_surfaces: false,
1223 exclude_unsupported_readings: false,
1224 entries: self.len(),
1225 };
1226 self.artifact_metadata_with_build(options, build)
1227 }
1228
1229 pub fn artifact_metadata_with_build(
1234 &self,
1235 options: UnidicArtifactMetadataOptions,
1236 mut build: UnidicArtifactBuild,
1237 ) -> UnidicArtifactMetadata {
1238 build.entries = self.len();
1239 UnidicArtifactMetadata {
1240 schema_version: 1,
1241 artifact_type: "moine.unidic.reading-index".to_string(),
1242 artifact_name: options.artifact_name,
1243 generator: options.generator,
1244 payload: UnidicArtifactPayload {
1245 path: options.payload_file_name,
1246 format: options.payload_format,
1247 file_digest_algorithm: None,
1248 file_digest: None,
1249 checksum_algorithm: ARTIFACT_PAYLOAD_CHECKSUM_ALGORITHM.to_string(),
1250 checksum: self.artifact_payload_checksum(),
1251 },
1252 source: UnidicArtifactSource {
1253 name: options.source_name,
1254 version: options.source_version,
1255 lex_csv: options.source_lex_csv,
1256 },
1257 build,
1258 query_defaults: UnidicArtifactQueryDefaults {
1259 max_span_chars: options.query_defaults.max_span_chars,
1260 max_paths: options.query_defaults.max_paths,
1261 longest_match_only: options.query_defaults.longest_match_only,
1262 max_readings_per_segment: options.query_defaults.max_readings_per_segment,
1263 },
1264 license: options.license,
1265 }
1266 }
1267
1268 pub fn artifact_payload(&self) -> UnidicReadingIndexPayload {
1273 let entries = match &self.storage {
1274 UnidicReadingStorage::Eager(readings_by_surface) => {
1275 let mut entries = readings_by_surface
1276 .iter()
1277 .map(|(surface, readings)| UnidicReadingIndexPayloadEntry {
1278 surface: surface.clone(),
1279 readings: readings.clone(),
1280 })
1281 .collect::<Vec<_>>();
1282 entries.sort_by(|left, right| left.surface.cmp(&right.surface));
1283 entries
1284 }
1285 UnidicReadingStorage::Indexed(indexed) => indexed
1286 .entries()
1287 .expect("validated indexed artifact should decode"),
1288 };
1289
1290 UnidicReadingIndexPayload {
1291 schema_version: ARTIFACT_PAYLOAD_SCHEMA_VERSION,
1292 payload_type: ARTIFACT_PAYLOAD_TYPE.to_string(),
1293 entries,
1294 }
1295 }
1296
1297 pub fn artifact_payload_checksum(&self) -> String {
1299 self.artifact_payload_checksum_for_algorithm(ARTIFACT_PAYLOAD_CHECKSUM_ALGORITHM)
1300 .expect("default artifact checksum algorithm should be supported")
1301 }
1302
1303 pub fn artifact_payload_checksum_for_algorithm(&self, algorithm: &str) -> Option<String> {
1309 let payload = self.artifact_payload();
1310 let bytes = canonical_payload_bytes(&payload);
1311 match algorithm {
1312 ARTIFACT_PAYLOAD_CHECKSUM_ALGORITHM => Some(sha256_hex(&bytes)),
1313 LEGACY_ARTIFACT_PAYLOAD_CHECKSUM_ALGORITHM => Some(format!("{:016x}", fnv1a64(&bytes))),
1314 _ => None,
1315 }
1316 }
1317
1318 pub fn write_artifact_binary_payload(
1323 &self,
1324 mut writer: impl Write,
1325 ) -> Result<(), UnidicArtifactPayloadError> {
1326 let payload = self.artifact_payload();
1327 writer.write_all(BINARY_ARTIFACT_MAGIC)?;
1328 writer.write_all(&BINARY_ARTIFACT_VERSION.to_le_bytes())?;
1329 writer.write_all(&0_u32.to_le_bytes())?;
1330 writer.write_all(&(payload.entries.len() as u64).to_le_bytes())?;
1331
1332 for entry in &payload.entries {
1333 write_binary_string(&mut writer, "surface", &entry.surface)?;
1334 write_u32_len(&mut writer, "reading_count", entry.readings.len())?;
1335 for reading in &entry.readings {
1336 write_binary_string(&mut writer, "reading", reading)?;
1337 }
1338 }
1339
1340 Ok(())
1341 }
1342
1343 pub fn write_indexed_artifact_payload(
1349 &self,
1350 mut writer: impl Write,
1351 ) -> Result<(), UnidicArtifactPayloadError> {
1352 let payload = self.artifact_payload();
1353 let mut fst_bytes = Vec::new();
1354 let mut readings_bytes = Vec::new();
1355 {
1356 let mut builder = MapBuilder::new(&mut fst_bytes).map_err(|err| {
1357 UnidicArtifactPayloadError::InvalidIndexedFst {
1358 message: err.to_string(),
1359 }
1360 })?;
1361 for entry in &payload.entries {
1362 let offset = readings_bytes.len() as u64;
1363 builder.insert(&entry.surface, offset).map_err(|err| {
1364 UnidicArtifactPayloadError::InvalidIndexedFst {
1365 message: err.to_string(),
1366 }
1367 })?;
1368 write_indexed_reading_block(&mut readings_bytes, &entry.readings)?;
1369 }
1370 builder
1371 .finish()
1372 .map_err(|err| UnidicArtifactPayloadError::InvalidIndexedFst {
1373 message: err.to_string(),
1374 })?;
1375 }
1376
1377 writer.write_all(INDEXED_ARTIFACT_MAGIC)?;
1378 writer.write_all(&INDEXED_ARTIFACT_VERSION.to_le_bytes())?;
1379 writer.write_all(&0_u32.to_le_bytes())?;
1380 writer.write_all(&(payload.entries.len() as u64).to_le_bytes())?;
1381 writer.write_all(&(fst_bytes.len() as u64).to_le_bytes())?;
1382 writer.write_all(&(readings_bytes.len() as u64).to_le_bytes())?;
1383 writer.write_all(&fst_bytes)?;
1384 writer.write_all(&readings_bytes)?;
1385 Ok(())
1386 }
1387
1388 pub fn reading_sequences(&self, text: &str, options: DictionaryReadingOptions) -> Vec<String> {
1394 self.reading_sequences_with_stats_inner(text, options, false)
1395 .unwrap_or_default()
1396 .paths
1397 }
1398
1399 pub fn reading_paths(
1406 &self,
1407 text: &str,
1408 options: DictionaryReadingOptions,
1409 ) -> Vec<DictionaryReadingPath> {
1410 self.reading_paths_with_stats(text, options).paths
1411 }
1412
1413 pub fn reading_paths_with_stats(
1420 &self,
1421 text: &str,
1422 options: DictionaryReadingOptions,
1423 ) -> DictionaryReadingExpansion {
1424 self.try_reading_paths_with_stats(text, options)
1425 .unwrap_or_default()
1426 }
1427
1428 pub fn try_reading_paths_with_stats(
1431 &self,
1432 text: &str,
1433 options: DictionaryReadingOptions,
1434 ) -> Result<DictionaryReadingExpansion, UnidicArtifactPayloadError> {
1435 self.reading_paths_with_stats_inner(text, options, false)
1436 }
1437
1438 pub fn hybrid_reading_paths(
1444 &self,
1445 text: &str,
1446 options: DictionaryReadingOptions,
1447 ) -> Vec<DictionaryReadingPath> {
1448 self.hybrid_reading_paths_with_stats(text, options).paths
1449 }
1450
1451 pub fn hybrid_reading_paths_with_stats(
1458 &self,
1459 text: &str,
1460 options: DictionaryReadingOptions,
1461 ) -> DictionaryReadingExpansion {
1462 self.try_hybrid_reading_paths_with_stats(text, options)
1463 .unwrap_or_default()
1464 }
1465
1466 pub fn try_hybrid_reading_paths_with_stats(
1469 &self,
1470 text: &str,
1471 options: DictionaryReadingOptions,
1472 ) -> Result<DictionaryReadingExpansion, UnidicArtifactPayloadError> {
1473 self.reading_paths_with_stats_inner(text, options, true)
1474 }
1475
1476 fn reading_paths_with_stats_inner(
1477 &self,
1478 text: &str,
1479 options: DictionaryReadingOptions,
1480 allow_direct_fallback: bool,
1481 ) -> Result<DictionaryReadingExpansion, UnidicArtifactPayloadError> {
1482 let options = options.validate()?;
1483 if text.is_empty() || options.max_span_chars == 0 || options.max_paths == 0 {
1484 return Ok(DictionaryReadingExpansion::default());
1485 }
1486
1487 let mut stats = DictionaryReadingStats::default();
1488 let boundaries = char_boundaries(text);
1489 let char_len = boundaries.len() - 1;
1490 let mut suffix_paths = vec![Vec::<DictionaryReadingPath>::new(); char_len + 1];
1491 suffix_paths[char_len].push(DictionaryReadingPath {
1492 segments: Vec::new(),
1493 joined_reading: String::new(),
1494 });
1495
1496 for start in (0..char_len).rev() {
1497 let mut paths_by_reading = std::collections::BTreeMap::new();
1498 let end_limit = char_len.min(start.saturating_add(options.max_span_chars));
1499 let mut matching_ends = Vec::new();
1500
1501 for end in start + 1..=end_limit {
1502 let surface = &text[boundaries[start]..boundaries[end]];
1503 if !suffix_paths[end].is_empty() {
1504 if let Some(surface_readings) = self.try_readings(surface)? {
1505 matching_ends.push((end, surface_readings));
1506 }
1507 }
1508 }
1509 stats.matched_spans += matching_ends.len();
1510
1511 if options.longest_match_only && !allow_direct_fallback {
1512 let pruned_spans = matching_ends.len().saturating_sub(1);
1513 if let Some(longest_match) = matching_ends.pop() {
1514 stats.longest_match_pruned_spans += pruned_spans;
1515 matching_ends.clear();
1516 matching_ends.push(longest_match);
1517 }
1518 }
1519
1520 for (end, surface_readings) in matching_ends {
1521 let surface = &text[boundaries[start]..boundaries[end]];
1522
1523 stats.raw_segment_readings += surface_readings.len();
1524 let raw_surface_reading_count = surface_readings.len();
1525 let surface_readings = limited_surface_readings(surface_readings.as_ref(), options);
1526 stats.used_segment_readings += surface_readings.len();
1527 stats.pruned_segment_readings += raw_surface_reading_count - surface_readings.len();
1528 for surface_reading in surface_readings {
1529 for suffix in &suffix_paths[end] {
1530 stats.candidate_combinations += 1;
1531 let mut reading = String::with_capacity(
1532 surface_reading.len() + suffix.joined_reading.len(),
1533 );
1534 reading.push_str(surface_reading);
1535 reading.push_str(&suffix.joined_reading);
1536
1537 let mut segments = Vec::with_capacity(suffix.segments.len() + 1);
1538 segments.push(DictionaryReadingSegment {
1539 surface: surface.to_string(),
1540 reading: surface_reading.to_string(),
1541 source: DictionaryReadingSegmentSource::Dictionary,
1542 });
1543 segments.extend(suffix.segments.iter().cloned());
1544
1545 match paths_by_reading.entry(reading.clone()) {
1546 Entry::Vacant(entry) => {
1547 entry.insert(DictionaryReadingPath {
1548 segments,
1549 joined_reading: reading,
1550 });
1551 stats.unique_paths += 1;
1552 }
1553 Entry::Occupied(_) => {
1554 stats.duplicate_joined_readings += 1;
1555 }
1556 }
1557
1558 if paths_by_reading.len() >= options.max_paths {
1559 stats.max_paths_hit_count += 1;
1560 break;
1561 }
1562 }
1563
1564 if paths_by_reading.len() >= options.max_paths {
1565 break;
1566 }
1567 }
1568
1569 if paths_by_reading.len() >= options.max_paths {
1570 break;
1571 }
1572 }
1573
1574 if allow_direct_fallback && paths_by_reading.len() < options.max_paths {
1575 if let Some(end) = direct_fallback_end(text, &boundaries, start, char_len) {
1576 if !suffix_paths[end].is_empty() {
1577 stats.direct_fallback_spans += 1;
1578 let surface = &text[boundaries[start]..boundaries[end]];
1579 for suffix in &suffix_paths[end] {
1580 stats.candidate_combinations += 1;
1581 let mut reading =
1582 String::with_capacity(surface.len() + suffix.joined_reading.len());
1583 reading.push_str(surface);
1584 reading.push_str(&suffix.joined_reading);
1585
1586 let mut segments = Vec::with_capacity(suffix.segments.len() + 1);
1587 segments.push(DictionaryReadingSegment {
1588 surface: surface.to_string(),
1589 reading: surface.to_string(),
1590 source: DictionaryReadingSegmentSource::Direct,
1591 });
1592 segments.extend(suffix.segments.iter().cloned());
1593
1594 match paths_by_reading.entry(reading.clone()) {
1595 Entry::Vacant(entry) => {
1596 entry.insert(DictionaryReadingPath {
1597 segments,
1598 joined_reading: reading,
1599 });
1600 stats.unique_paths += 1;
1601 }
1602 Entry::Occupied(_) => {
1603 stats.duplicate_joined_readings += 1;
1604 }
1605 }
1606
1607 if paths_by_reading.len() >= options.max_paths {
1608 stats.max_paths_hit_count += 1;
1609 break;
1610 }
1611 }
1612 }
1613 }
1614 }
1615
1616 suffix_paths[start] = paths_by_reading.into_values().collect();
1617 }
1618
1619 Ok(DictionaryReadingExpansion {
1620 paths: suffix_paths.remove(0),
1621 stats,
1622 })
1623 }
1624
1625 fn reading_sequences_with_stats_inner(
1626 &self,
1627 text: &str,
1628 options: DictionaryReadingOptions,
1629 allow_direct_fallback: bool,
1630 ) -> Result<DictionaryReadingSequenceExpansion, UnidicArtifactPayloadError> {
1631 let options = options.validate()?;
1632 if text.is_empty() || options.max_span_chars == 0 || options.max_paths == 0 {
1633 return Ok(DictionaryReadingSequenceExpansion::default());
1634 }
1635
1636 let mut stats = DictionaryReadingStats::default();
1637 let boundaries = char_boundaries(text);
1638 let char_len = boundaries.len() - 1;
1639 let mut suffix_paths = vec![Vec::<String>::new(); char_len + 1];
1640 suffix_paths[char_len].push(String::new());
1641
1642 for start in (0..char_len).rev() {
1643 let mut paths_by_reading = BTreeSet::new();
1644 let end_limit = char_len.min(start.saturating_add(options.max_span_chars));
1645 let mut matching_ends = Vec::new();
1646
1647 for end in start + 1..=end_limit {
1648 let surface = &text[boundaries[start]..boundaries[end]];
1649 if !suffix_paths[end].is_empty() {
1650 if let Some(surface_readings) = self.try_readings(surface)? {
1651 matching_ends.push((end, surface_readings));
1652 }
1653 }
1654 }
1655 stats.matched_spans += matching_ends.len();
1656
1657 if options.longest_match_only && !allow_direct_fallback {
1658 let pruned_spans = matching_ends.len().saturating_sub(1);
1659 if let Some(longest_match) = matching_ends.pop() {
1660 stats.longest_match_pruned_spans += pruned_spans;
1661 matching_ends.clear();
1662 matching_ends.push(longest_match);
1663 }
1664 }
1665
1666 for (end, surface_readings) in matching_ends {
1667 stats.raw_segment_readings += surface_readings.len();
1668 let raw_surface_reading_count = surface_readings.len();
1669 let surface_readings = limited_surface_readings(surface_readings.as_ref(), options);
1670 stats.used_segment_readings += surface_readings.len();
1671 stats.pruned_segment_readings += raw_surface_reading_count - surface_readings.len();
1672 for surface_reading in surface_readings {
1673 for suffix in &suffix_paths[end] {
1674 stats.candidate_combinations += 1;
1675 let mut reading =
1676 String::with_capacity(surface_reading.len() + suffix.len());
1677 reading.push_str(surface_reading);
1678 reading.push_str(suffix);
1679
1680 if paths_by_reading.insert(reading) {
1681 stats.unique_paths += 1;
1682 } else {
1683 stats.duplicate_joined_readings += 1;
1684 }
1685
1686 if paths_by_reading.len() >= options.max_paths {
1687 stats.max_paths_hit_count += 1;
1688 break;
1689 }
1690 }
1691
1692 if paths_by_reading.len() >= options.max_paths {
1693 break;
1694 }
1695 }
1696
1697 if paths_by_reading.len() >= options.max_paths {
1698 break;
1699 }
1700 }
1701
1702 if allow_direct_fallback && paths_by_reading.len() < options.max_paths {
1703 if let Some(end) = direct_fallback_end(text, &boundaries, start, char_len) {
1704 if !suffix_paths[end].is_empty() {
1705 stats.direct_fallback_spans += 1;
1706 let surface = &text[boundaries[start]..boundaries[end]];
1707 for suffix in &suffix_paths[end] {
1708 stats.candidate_combinations += 1;
1709 let mut reading = String::with_capacity(surface.len() + suffix.len());
1710 reading.push_str(surface);
1711 reading.push_str(suffix);
1712
1713 if paths_by_reading.insert(reading) {
1714 stats.unique_paths += 1;
1715 } else {
1716 stats.duplicate_joined_readings += 1;
1717 }
1718
1719 if paths_by_reading.len() >= options.max_paths {
1720 stats.max_paths_hit_count += 1;
1721 break;
1722 }
1723 }
1724 }
1725 }
1726 }
1727
1728 suffix_paths[start] = paths_by_reading.into_iter().collect();
1729 }
1730
1731 Ok(DictionaryReadingSequenceExpansion {
1732 paths: suffix_paths.remove(0),
1733 stats,
1734 })
1735 }
1736
1737 pub fn romaji_lattice(
1743 &self,
1744 text: &str,
1745 options: DictionaryReadingOptions,
1746 ) -> Result<Option<Lattice>, JaLatticeError> {
1747 let readings = if options.longest_match_only {
1748 self.reading_sequences_longest_only(text, options)
1749 .map_err(|err| JaLatticeError::ArtifactPayload(err.to_string()))?
1750 } else {
1751 self.reading_sequences_with_stats_inner(text, options, false)
1752 .map_err(|err| JaLatticeError::ArtifactPayload(err.to_string()))?
1753 .paths
1754 };
1755 if readings.is_empty() {
1756 return Ok(None);
1757 }
1758
1759 crate::romaji::romaji_lattice_from_supported_readings(readings)
1760 }
1761
1762 pub fn hybrid_romaji_lattice(
1767 &self,
1768 text: &str,
1769 options: DictionaryReadingOptions,
1770 ) -> Result<Option<Lattice>, JaLatticeError> {
1771 let readings = self
1772 .reading_sequences_with_stats_inner(text, options, true)
1773 .map_err(|err| JaLatticeError::ArtifactPayload(err.to_string()))?;
1774 if readings.paths.is_empty() {
1775 return Ok(None);
1776 }
1777
1778 crate::romaji::romaji_lattice_from_supported_readings(readings.paths)
1779 }
1780
1781 fn reading_sequences_longest_only(
1782 &self,
1783 text: &str,
1784 options: DictionaryReadingOptions,
1785 ) -> Result<Vec<String>, UnidicArtifactPayloadError> {
1786 let options = options.validate()?;
1787 if text.is_empty() || options.max_span_chars == 0 || options.max_paths == 0 {
1788 return Ok(Vec::new());
1789 }
1790
1791 let boundaries = char_boundaries(text);
1792 let char_len = boundaries.len() - 1;
1793 let mut suffix_paths = vec![Vec::<String>::new(); char_len + 1];
1794 suffix_paths[char_len].push(String::new());
1795
1796 for start in (0..char_len).rev() {
1797 let mut paths_by_reading = BTreeSet::new();
1798 let end_limit = char_len.min(start.saturating_add(options.max_span_chars));
1799
1800 for end in (start + 1..=end_limit).rev() {
1801 if suffix_paths[end].is_empty() {
1802 continue;
1803 }
1804 let surface = &text[boundaries[start]..boundaries[end]];
1805 let Some(surface_readings) = self.try_readings(surface)? else {
1806 continue;
1807 };
1808 let surface_readings = limited_surface_readings(surface_readings.as_ref(), options);
1809 for surface_reading in surface_readings {
1810 for suffix in &suffix_paths[end] {
1811 let mut reading =
1812 String::with_capacity(surface_reading.len() + suffix.len());
1813 reading.push_str(surface_reading);
1814 reading.push_str(suffix);
1815 paths_by_reading.insert(reading);
1816 if paths_by_reading.len() >= options.max_paths {
1817 break;
1818 }
1819 }
1820
1821 if paths_by_reading.len() >= options.max_paths {
1822 break;
1823 }
1824 }
1825 break;
1826 }
1827
1828 suffix_paths[start] = paths_by_reading.into_iter().collect();
1829 }
1830
1831 Ok(suffix_paths.remove(0))
1832 }
1833}
1834
1835#[derive(Clone, Debug, Default, Eq, PartialEq)]
1836struct DictionaryReadingSequenceExpansion {
1837 paths: Vec<String>,
1838 stats: DictionaryReadingStats,
1839}
1840
1841fn char_boundaries(text: &str) -> Vec<usize> {
1842 text.char_indices()
1843 .map(|(index, _)| index)
1844 .chain(std::iter::once(text.len()))
1845 .collect()
1846}
1847
1848pub(crate) fn insert_surface_reading(
1849 by_surface: &mut HashMap<String, BTreeSet<String>>,
1850 surface: &str,
1851 reading: &str,
1852) {
1853 by_surface
1854 .entry(surface.to_string())
1855 .or_default()
1856 .insert(reading.to_string());
1857}
1858
1859pub(crate) fn normalize_ascii_width(input: &str) -> Option<String> {
1860 let mut normalized = String::with_capacity(input.len());
1861 let mut changed = false;
1862
1863 for ch in input.chars() {
1864 let normalized_ch = normalize_ascii_width_char(ch);
1865 changed |= normalized_ch != ch;
1866 normalized.push(normalized_ch);
1867 }
1868
1869 changed.then_some(normalized)
1870}
1871
1872fn normalize_ascii_width_char(ch: char) -> char {
1873 match ch {
1874 '\u{3000}' => ' ',
1875 '\u{ff01}'..='\u{ff5e}' => {
1876 char::from_u32(ch as u32 - 0xfee0).expect("fullwidth ASCII maps to ASCII")
1877 }
1878 _ => ch,
1879 }
1880}
1881
1882pub(crate) fn lex_csv_reader(reader: impl Read) -> csv::Reader<impl Read> {
1883 csv::ReaderBuilder::new()
1884 .has_headers(false)
1885 .flexible(true)
1886 .from_reader(reader)
1887}
1888
1889pub(crate) fn field(record: &csv::StringRecord, column: usize) -> Result<&str, UnidicCsvError> {
1890 record
1891 .get(column)
1892 .ok_or_else(|| UnidicCsvError::MissingColumn {
1893 record_index: record
1894 .position()
1895 .map(|position| position.record())
1896 .unwrap_or(0),
1897 column,
1898 len: record.len(),
1899 })
1900}
1901
1902pub(crate) fn is_symbol_pos(pos1: &str) -> bool {
1903 pos1.contains("記号")
1904}
1905
1906fn is_false(value: &bool) -> bool {
1907 !*value
1908}
1909
1910fn limited_surface_readings(readings: &[String], options: DictionaryReadingOptions) -> &[String] {
1911 if let Some(max_readings) = options.max_readings_per_segment {
1912 &readings[..readings.len().min(max_readings)]
1913 } else {
1914 readings
1915 }
1916}
1917
1918fn direct_fallback_end(
1919 text: &str,
1920 boundaries: &[usize],
1921 start: usize,
1922 char_len: usize,
1923) -> Option<usize> {
1924 let mut end = start;
1925 while end < char_len {
1926 let surface = &text[boundaries[start]..boundaries[end + 1]];
1927 if !can_build_direct_romaji_path(surface) {
1928 break;
1929 }
1930 end += 1;
1931 }
1932
1933 (end > start).then_some(end)
1934}
1935
1936fn write_binary_string(
1937 writer: &mut impl Write,
1938 field: &'static str,
1939 value: &str,
1940) -> Result<(), UnidicArtifactPayloadError> {
1941 write_u32_len(writer, field, value.len())?;
1942 writer.write_all(value.as_bytes())?;
1943 Ok(())
1944}
1945
1946fn write_u32_len(
1947 writer: &mut impl Write,
1948 field: &'static str,
1949 len: usize,
1950) -> Result<(), UnidicArtifactPayloadError> {
1951 let len = u32::try_from(len)
1952 .map_err(|_| UnidicArtifactPayloadError::BinaryValueTooLarge { field, len })?;
1953 writer.write_all(&len.to_le_bytes())?;
1954 Ok(())
1955}
1956
1957fn read_binary_string(
1958 reader: &mut impl Read,
1959 field: &'static str,
1960) -> Result<String, UnidicArtifactPayloadError> {
1961 let len = read_u32_le(reader, field)? as usize;
1962 check_limit(field, len, MAX_ARTIFACT_STRING_BYTES)?;
1963 let mut bytes = vec![0_u8; len];
1964 read_exact_binary(reader, &mut bytes, field)?;
1965 String::from_utf8(bytes)
1966 .map_err(|source| UnidicArtifactPayloadError::InvalidBinaryUtf8 { field, source })
1967}
1968
1969fn read_u32_le(
1970 reader: &mut impl Read,
1971 field: &'static str,
1972) -> Result<u32, UnidicArtifactPayloadError> {
1973 let mut bytes = [0_u8; 4];
1974 read_exact_binary(reader, &mut bytes, field)?;
1975 Ok(u32::from_le_bytes(bytes))
1976}
1977
1978fn read_u64_le(
1979 reader: &mut impl Read,
1980 field: &'static str,
1981) -> Result<u64, UnidicArtifactPayloadError> {
1982 let mut bytes = [0_u8; 8];
1983 read_exact_binary(reader, &mut bytes, field)?;
1984 Ok(u64::from_le_bytes(bytes))
1985}
1986
1987fn read_exact_binary(
1988 reader: &mut impl Read,
1989 bytes: &mut [u8],
1990 field: &'static str,
1991) -> Result<(), UnidicArtifactPayloadError> {
1992 match reader.read_exact(bytes) {
1993 Ok(()) => Ok(()),
1994 Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
1995 Err(UnidicArtifactPayloadError::TruncatedBinary { field })
1996 }
1997 Err(err) => Err(UnidicArtifactPayloadError::Io(err)),
1998 }
1999}
2000
2001fn read_binary_artifact_payload_header(
2002 reader: &mut impl Read,
2003) -> Result<UnidicBinaryArtifactPayloadHeader, UnidicArtifactPayloadError> {
2004 let mut magic = [0_u8; 8];
2005 read_exact_binary(reader, &mut magic, "magic")?;
2006 if &magic != BINARY_ARTIFACT_MAGIC {
2007 return Err(UnidicArtifactPayloadError::InvalidBinaryMagic { magic });
2008 }
2009
2010 let version = read_u32_le(reader, "version")?;
2011 if version != BINARY_ARTIFACT_VERSION {
2012 return Err(UnidicArtifactPayloadError::UnsupportedBinaryVersion { version });
2013 }
2014
2015 let reserved = read_u32_le(reader, "reserved")?;
2016 if reserved != 0 {
2017 return Err(UnidicArtifactPayloadError::NonZeroBinaryReserved { value: reserved });
2018 }
2019
2020 let entry_count = read_u64_le(reader, "entry_count")?;
2021 let entries = usize::try_from(entry_count).map_err(|_| {
2022 UnidicArtifactPayloadError::BinaryEntryCountTooLarge {
2023 entries: entry_count,
2024 }
2025 })?;
2026 check_limit("entry_count", entries, MAX_ARTIFACT_ENTRIES)?;
2027
2028 Ok(UnidicBinaryArtifactPayloadHeader { version, entries })
2029}
2030
2031fn read_indexed_artifact_payload_header_bytes(
2032 bytes: &[u8],
2033) -> Result<UnidicIndexedArtifactPayloadHeader, UnidicArtifactPayloadError> {
2034 if bytes.len() < INDEXED_ARTIFACT_HEADER_LEN {
2035 return Err(UnidicArtifactPayloadError::TruncatedIndexed { field: "header" });
2036 }
2037 let mut magic = [0_u8; 8];
2038 magic.copy_from_slice(&bytes[..8]);
2039 if &magic != INDEXED_ARTIFACT_MAGIC {
2040 return Err(UnidicArtifactPayloadError::InvalidIndexedMagic { magic });
2041 }
2042
2043 let version = read_u32_le_bytes(bytes, 8, "version")?;
2044 if version != INDEXED_ARTIFACT_VERSION {
2045 return Err(UnidicArtifactPayloadError::UnsupportedIndexedVersion { version });
2046 }
2047 let reserved = read_u32_le_bytes(bytes, 12, "reserved")?;
2048 if reserved != 0 {
2049 return Err(UnidicArtifactPayloadError::NonZeroIndexedReserved { value: reserved });
2050 }
2051 let entry_count = read_u64_le_bytes(bytes, 16, "entry_count")?;
2052 let fst_len = read_u64_le_bytes(bytes, 24, "fst_len")?;
2053 let readings_len = read_u64_le_bytes(bytes, 32, "readings_len")?;
2054 let entries = checked_indexed_usize("entry_count", entry_count)?;
2055 check_limit("entry_count", entries, MAX_ARTIFACT_ENTRIES)?;
2056 Ok(UnidicIndexedArtifactPayloadHeader {
2057 version,
2058 entries,
2059 fst_len: checked_indexed_usize("fst_len", fst_len)?,
2060 readings_len: checked_indexed_usize("readings_len", readings_len)?,
2061 })
2062}
2063
2064fn read_u32_le_bytes(
2065 bytes: &[u8],
2066 offset: usize,
2067 field: &'static str,
2068) -> Result<u32, UnidicArtifactPayloadError> {
2069 let end = offset
2070 .checked_add(4)
2071 .ok_or(UnidicArtifactPayloadError::TruncatedIndexed { field })?;
2072 let chunk = bytes
2073 .get(offset..end)
2074 .ok_or(UnidicArtifactPayloadError::TruncatedIndexed { field })?;
2075 Ok(u32::from_le_bytes(
2076 chunk.try_into().expect("slice length is 4"),
2077 ))
2078}
2079
2080fn read_u64_le_bytes(
2081 bytes: &[u8],
2082 offset: usize,
2083 field: &'static str,
2084) -> Result<u64, UnidicArtifactPayloadError> {
2085 let end = offset
2086 .checked_add(8)
2087 .ok_or(UnidicArtifactPayloadError::TruncatedIndexed { field })?;
2088 let chunk = bytes
2089 .get(offset..end)
2090 .ok_or(UnidicArtifactPayloadError::TruncatedIndexed { field })?;
2091 Ok(u64::from_le_bytes(
2092 chunk.try_into().expect("slice length is 8"),
2093 ))
2094}
2095
2096fn checked_indexed_usize(
2097 field: &'static str,
2098 len: u64,
2099) -> Result<usize, UnidicArtifactPayloadError> {
2100 usize::try_from(len)
2101 .map_err(|_| UnidicArtifactPayloadError::IndexedSectionTooLarge { field, len })
2102}
2103
2104fn check_payload_file_size(path: &Path) -> Result<(), UnidicArtifactPayloadError> {
2105 let len = std::fs::metadata(path)?.len();
2106 if len > MAX_ARTIFACT_PAYLOAD_BYTES {
2107 return Err(UnidicArtifactPayloadError::ArtifactLimitExceeded {
2108 field: "payload_bytes",
2109 len,
2110 max: MAX_ARTIFACT_PAYLOAD_BYTES,
2111 });
2112 }
2113 Ok(())
2114}
2115
2116fn check_limit(
2117 field: &'static str,
2118 len: usize,
2119 max: usize,
2120) -> Result<(), UnidicArtifactPayloadError> {
2121 if len > max {
2122 return Err(UnidicArtifactPayloadError::ArtifactLimitExceeded {
2123 field,
2124 len: len as u64,
2125 max: max as u64,
2126 });
2127 }
2128 Ok(())
2129}
2130
2131fn write_indexed_reading_block(
2132 writer: &mut Vec<u8>,
2133 readings: &[String],
2134) -> Result<(), UnidicArtifactPayloadError> {
2135 write_u32_len(writer, "reading_count", readings.len())?;
2136 for reading in readings {
2137 write_binary_string(writer, "reading", reading)?;
2138 }
2139 Ok(())
2140}
2141
2142impl IndexedUnidicPayload {
2143 fn validate(&self) -> Result<(), UnidicArtifactPayloadError> {
2144 let mut stream = self.map.stream();
2145 while let Some((surface, offset)) = stream.next() {
2146 let surface = std::str::from_utf8(surface).map_err(|source| {
2147 UnidicArtifactPayloadError::InvalidIndexedUtf8 {
2148 field: "surface",
2149 source,
2150 }
2151 })?;
2152 if surface.is_empty() {
2153 return Err(UnidicArtifactPayloadError::EmptySurface { entry_index: 0 });
2154 }
2155 let readings = self.readings_at(offset)?;
2156 if readings.is_empty() {
2157 return Err(UnidicArtifactPayloadError::EmptyReadings {
2158 surface: surface.to_string(),
2159 });
2160 }
2161 let mut seen = BTreeSet::new();
2162 for (reading_index, reading) in readings.iter().enumerate() {
2163 if reading.is_empty() {
2164 return Err(UnidicArtifactPayloadError::EmptyReading {
2165 surface: surface.to_string(),
2166 reading_index,
2167 });
2168 }
2169 if !seen.insert(reading) {
2170 return Err(UnidicArtifactPayloadError::DuplicateReading {
2171 surface: surface.to_string(),
2172 reading: reading.clone(),
2173 });
2174 }
2175 }
2176 }
2177 Ok(())
2178 }
2179
2180 fn readings(&self, surface: &str) -> Result<Option<Vec<String>>, UnidicArtifactPayloadError> {
2181 self.map
2182 .get(surface)
2183 .map(|offset| self.readings_at(offset))
2184 .transpose()
2185 }
2186
2187 fn entries(&self) -> Result<Vec<UnidicReadingIndexPayloadEntry>, UnidicArtifactPayloadError> {
2188 let mut entries = Vec::with_capacity(self.entries);
2189 let mut stream = self.map.stream();
2190 while let Some((surface, offset)) = stream.next() {
2191 let surface = std::str::from_utf8(surface)
2192 .map_err(|source| UnidicArtifactPayloadError::InvalidIndexedUtf8 {
2193 field: "surface",
2194 source,
2195 })?
2196 .to_string();
2197 let readings = self.readings_at(offset)?;
2198 entries.push(UnidicReadingIndexPayloadEntry { surface, readings });
2199 }
2200 Ok(entries)
2201 }
2202
2203 fn readings_at(&self, offset: u64) -> Result<Vec<String>, UnidicArtifactPayloadError> {
2204 read_indexed_readings_at_bytes(&self.mmap, self.readings_start, offset)
2205 }
2206}
2207
2208fn read_indexed_readings_at_bytes(
2209 bytes: &[u8],
2210 readings_start: usize,
2211 offset: u64,
2212) -> Result<Vec<String>, UnidicArtifactPayloadError> {
2213 let offset = usize::try_from(offset)
2214 .map_err(|_| UnidicArtifactPayloadError::InvalidIndexedOffset { offset })?;
2215 let start = readings_start.checked_add(offset).ok_or(
2216 UnidicArtifactPayloadError::InvalidIndexedOffset {
2217 offset: offset as u64,
2218 },
2219 )?;
2220 if start >= bytes.len() {
2221 return Err(UnidicArtifactPayloadError::InvalidIndexedOffset {
2222 offset: offset as u64,
2223 });
2224 }
2225 let mut cursor = start;
2226 let reading_count = read_u32_le_bytes(bytes, cursor, "reading_count")? as usize;
2227 check_limit(
2228 "reading_count",
2229 reading_count,
2230 MAX_ARTIFACT_READINGS_PER_ENTRY,
2231 )?;
2232 cursor += 4;
2233 let mut readings = Vec::with_capacity(reading_count);
2234 for _ in 0..reading_count {
2235 let len = read_u32_le_bytes(bytes, cursor, "reading_len")? as usize;
2236 check_limit("reading_bytes", len, MAX_ARTIFACT_STRING_BYTES)?;
2237 cursor += 4;
2238 let end = cursor
2239 .checked_add(len)
2240 .ok_or(UnidicArtifactPayloadError::TruncatedIndexed { field: "reading" })?;
2241 let reading_bytes = bytes
2242 .get(cursor..end)
2243 .ok_or(UnidicArtifactPayloadError::TruncatedIndexed { field: "reading" })?;
2244 let reading = std::str::from_utf8(reading_bytes)
2245 .map_err(|source| UnidicArtifactPayloadError::InvalidIndexedUtf8 {
2246 field: "reading",
2247 source,
2248 })?
2249 .to_string();
2250 readings.push(reading);
2251 cursor = end;
2252 }
2253 Ok(readings)
2254}
2255
2256pub fn artifact_file_digest_path(path: impl AsRef<Path>) -> Result<String, std::io::Error> {
2258 let file = File::open(path)?;
2259 artifact_file_digest_reader(file)
2260}
2261
2262pub fn artifact_file_digest_reader(mut reader: impl Read) -> Result<String, std::io::Error> {
2264 let mut hasher = Sha256::new();
2265 let mut buffer = [0_u8; 64 * 1024];
2266 loop {
2267 let read = reader.read(&mut buffer)?;
2268 if read == 0 {
2269 break;
2270 }
2271 hasher.update(&buffer[..read]);
2272 }
2273 Ok(sha256_digest_hex(hasher.finalize()))
2274}
2275
2276fn validate_artifact_payload_header(
2277 payload: &UnidicReadingIndexPayload,
2278) -> Result<(), UnidicArtifactPayloadError> {
2279 if payload.schema_version != ARTIFACT_PAYLOAD_SCHEMA_VERSION {
2280 return Err(UnidicArtifactPayloadError::UnsupportedSchemaVersion {
2281 version: payload.schema_version,
2282 });
2283 }
2284 if payload.payload_type != ARTIFACT_PAYLOAD_TYPE {
2285 return Err(UnidicArtifactPayloadError::UnsupportedPayloadType {
2286 payload_type: payload.payload_type.clone(),
2287 });
2288 }
2289 Ok(())
2290}
2291
2292fn canonical_payload_bytes(payload: &UnidicReadingIndexPayload) -> Vec<u8> {
2293 let mut bytes = Vec::new();
2294 bytes.extend_from_slice(b"moine.unidic.reading-index.surface-readings/v1\n");
2295 for entry in &payload.entries {
2296 push_len_prefixed(&mut bytes, b"S", &entry.surface);
2297 bytes.extend_from_slice(format!("R{}\n", entry.readings.len()).as_bytes());
2298 for reading in &entry.readings {
2299 push_len_prefixed(&mut bytes, b"r", reading);
2300 }
2301 }
2302 bytes
2303}
2304
2305fn push_len_prefixed(bytes: &mut Vec<u8>, tag: &[u8], value: &str) {
2306 bytes.extend_from_slice(tag);
2307 bytes.extend_from_slice(value.len().to_string().as_bytes());
2308 bytes.push(b'\n');
2309 bytes.extend_from_slice(value.as_bytes());
2310 bytes.push(b'\n');
2311}
2312
2313fn fnv1a64(bytes: &[u8]) -> u64 {
2314 let mut hash = 0xcbf29ce484222325_u64;
2315 for byte in bytes {
2316 hash ^= u64::from(*byte);
2317 hash = hash.wrapping_mul(0x100000001b3);
2318 }
2319 hash
2320}
2321
2322fn sha256_hex(bytes: &[u8]) -> String {
2323 sha256_digest_hex(Sha256::digest(bytes))
2324}
2325
2326fn sha256_digest_hex(digest: impl IntoIterator<Item = u8>) -> String {
2327 let mut output = String::with_capacity(64);
2328 for byte in digest {
2329 write!(&mut output, "{byte:02x}").expect("writing to String should not fail");
2330 }
2331 output
2332}
2333
2334#[cfg(test)]
2335mod tests {
2336 use super::*;
2337
2338 #[test]
2339 fn builds_surface_to_readings_index() {
2340 let csv = "\
2341印刷,18331,19434,9138,名詞,普通名詞,サ変可能,*,*,*,インサツ,印刷,印刷,インサツ,印刷,インサツ,漢,*,*,*,*,*,*,体,インサツ,インサツ,インサツ,インサツ,0,C2,*,752349454934528,2737
2342刃,18521,20041,11551,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,ハ,刃,ハ,和,ハ濁,基本形,*,*,*,*,体,ハ,ハ,ハ,ハ,1,C3,*,8060803244761600,29325
2343刃,18419,19578,12664,名詞,普通名詞,一般,*,*,*,ヤイバ,刃,刃,ヤイバ,刃,ヤイバ,和,*,*,*,*,*,*,体,ヤイバ,ヤイバ,ヤイバ,ヤイバ,\"1,0\",C1,*,18677687522566656,67949
2344";
2345 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
2346
2347 assert_eq!(
2348 index.readings("印刷").as_deref(),
2349 Some(&["インサツ".to_string()][..])
2350 );
2351 assert_eq!(
2352 index.readings("刃").as_deref(),
2353 Some(&["ハ".to_string(), "ヤイバ".to_string()][..])
2354 );
2355 }
2356
2357 #[test]
2358 fn skips_star_readings() {
2359 let csv = "記号,1,2,3,補助記号,一般,*,*,*,*,*,記号,記号,*,記号,*,記号\n";
2360 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
2361
2362 assert!(index.is_empty());
2363 }
2364
2365 #[test]
2366 fn excludes_ascii_and_symbol_surfaces_by_default() {
2367 let csv = "\
2368a,1,2,3,記号,文字,*,*,*,*,エー,a,a,エー,a,エー,外
2369!,1,2,3,補助記号,一般,*,*,*,*,!,!,!,!,!,!,記号
2370印刷,1,2,3,名詞,普通名詞,サ変可能,*,*,*,インサツ,印刷,印刷,インサツ,印刷,インサツ,漢
2371";
2372 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
2373
2374 assert_eq!(index.readings("a"), None);
2375 assert_eq!(index.readings("!"), None);
2376 assert_eq!(
2377 index.readings("印刷").as_deref(),
2378 Some(&["インサツ".to_string()][..])
2379 );
2380 }
2381
2382 #[test]
2383 fn can_keep_ascii_surfaces_when_requested() {
2384 let csv = "a,1,2,3,名詞,普通名詞,一般,*,*,*,エー,a,a,エー,a,エー,外\n";
2385 let index = UnidicReadingIndex::from_lex_csv_reader_with_options(
2386 csv.as_bytes(),
2387 UnidicIndexOptions {
2388 exclude_ascii_surfaces: false,
2389 ..UnidicIndexOptions::default()
2390 },
2391 )
2392 .unwrap();
2393
2394 assert_eq!(
2395 index.readings("a").as_deref(),
2396 Some(&["エー".to_string()][..])
2397 );
2398 }
2399
2400 #[test]
2401 fn limits_readings_per_surface_when_requested() {
2402 let csv = "\
2403刃,1,2,3,名詞,普通名詞,一般,*,*,*,ジン,刃,刃,ジン,刃,ジン,漢
2404刃,1,2,3,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,ハ,刃,ハ,和
2405刃,1,2,3,名詞,普通名詞,一般,*,*,*,ヤイバ,刃,刃,ヤイバ,刃,ヤイバ,和
2406";
2407 let index = UnidicReadingIndex::from_lex_csv_reader_with_options(
2408 csv.as_bytes(),
2409 UnidicIndexOptions {
2410 max_readings_per_surface: Some(2),
2411 ..UnidicIndexOptions::default()
2412 },
2413 )
2414 .unwrap();
2415
2416 assert_eq!(
2417 index.readings("刃").as_deref(),
2418 Some(&["ジン".to_string(), "ハ".to_string()][..])
2419 );
2420 }
2421
2422 #[test]
2423 fn can_limit_readings_per_segment_at_query_time() {
2424 let csv = "\
2425刃,1,2,3,名詞,普通名詞,一般,*,*,*,ジン,刃,刃,ジン,刃,ジン,漢
2426刃,1,2,3,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,ハ,刃,ハ,和
2427刃,1,2,3,名詞,普通名詞,一般,*,*,*,ヤイバ,刃,刃,ヤイバ,刃,ヤイバ,和
2428";
2429 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
2430 let readings = index.reading_sequences(
2431 "刃",
2432 DictionaryReadingOptions {
2433 max_readings_per_segment: Some(2),
2434 ..DictionaryReadingOptions::default()
2435 },
2436 );
2437
2438 assert_eq!(readings, vec!["ジン".to_string(), "ハ".to_string()]);
2439 }
2440
2441 #[test]
2442 fn builds_artifact_metadata_from_index_and_options() {
2443 let csv = "\
2444刃,1,2,3,名詞,普通名詞,一般,*,*,*,ジン,刃,刃,ジン,刃,ジン,漢
2445刃,1,2,3,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,ハ,刃,ハ,和
2446";
2447 let index_options = UnidicIndexOptions {
2448 reading_field: UnidicReadingField::Pron,
2449 max_readings_per_surface: Some(1),
2450 exclude_ascii_surfaces: true,
2451 exclude_symbol_pos: true,
2452 };
2453 let index =
2454 UnidicReadingIndex::from_lex_csv_reader_with_options(csv.as_bytes(), index_options)
2455 .unwrap();
2456
2457 let metadata = index.artifact_metadata(UnidicArtifactMetadataOptions {
2458 artifact_name: "moine-unidic-cwj-202512".to_string(),
2459 generator: "moine-cli".to_string(),
2460 payload_file_name: "moine-unidic-cwj-202512.readings.yaml".to_string(),
2461 payload_format: "yaml.surface-readings.v1".to_string(),
2462 source_name: "UniDic-CWJ".to_string(),
2463 source_version: "2025.12".to_string(),
2464 source_lex_csv: "unidic-cwj-202512_full/lex.csv".to_string(),
2465 index_options,
2466 query_defaults: DictionaryReadingOptions {
2467 longest_match_only: true,
2468 max_readings_per_segment: Some(16),
2469 ..DictionaryReadingOptions::default()
2470 },
2471 license: UnidicArtifactLicense::default(),
2472 });
2473
2474 assert_eq!(metadata.schema_version, 1);
2475 assert_eq!(metadata.artifact_type, "moine.unidic.reading-index");
2476 assert_eq!(
2477 metadata.payload.path,
2478 "moine-unidic-cwj-202512.readings.yaml"
2479 );
2480 assert_eq!(metadata.payload.format, "yaml.surface-readings.v1");
2481 assert_eq!(
2482 metadata.payload.checksum_algorithm,
2483 ARTIFACT_PAYLOAD_CHECKSUM_ALGORITHM
2484 );
2485 assert_eq!(metadata.payload.checksum.len(), 64);
2486 assert_eq!(metadata.source.version, "2025.12");
2487 assert_eq!(metadata.build.reading_field, "pron");
2488 assert_eq!(metadata.build.entries, 1);
2489 assert_eq!(metadata.build.max_readings_per_surface, Some(1));
2490 assert!(metadata.query_defaults.longest_match_only);
2491 assert_eq!(metadata.query_defaults.max_readings_per_segment, Some(16));
2492 assert_eq!(metadata.license.selected_license, "BSD-3-Clause");
2493 }
2494
2495 #[test]
2496 fn builds_deterministic_payload_entries() {
2497 let csv = "\
2498刃,1,2,3,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,ハ,刃,ハ,和
2499印刷,1,2,3,名詞,普通名詞,サ変可能,*,*,*,インサツ,印刷,印刷,インサツ,印刷,インサツ,漢
2500刃,1,2,3,名詞,普通名詞,一般,*,*,*,ヤイバ,刃,刃,ヤイバ,刃,ヤイバ,和
2501";
2502 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
2503 let payload = index.artifact_payload();
2504
2505 assert_eq!(payload.schema_version, 1);
2506 assert_eq!(
2507 payload.payload_type,
2508 "moine.unidic.reading-index.surface-readings"
2509 );
2510 assert_eq!(
2511 payload.entries,
2512 vec![
2513 UnidicReadingIndexPayloadEntry {
2514 surface: "刃".to_string(),
2515 readings: vec!["ハ".to_string(), "ヤイバ".to_string()],
2516 },
2517 UnidicReadingIndexPayloadEntry {
2518 surface: "印刷".to_string(),
2519 readings: vec!["インサツ".to_string()],
2520 },
2521 ]
2522 );
2523 }
2524
2525 #[test]
2526 fn payload_checksum_changes_with_payload_content() {
2527 let first = UnidicReadingIndex::from_lex_csv_reader(
2528 "刃,1,2,3,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,ハ,刃,ハ,和\n".as_bytes(),
2529 )
2530 .unwrap();
2531 let second = UnidicReadingIndex::from_lex_csv_reader(
2532 "刃,1,2,3,名詞,普通名詞,一般,*,*,*,ヤイバ,刃,刃,ヤイバ,刃,ヤイバ,和\n".as_bytes(),
2533 )
2534 .unwrap();
2535
2536 assert_eq!(first.artifact_payload_checksum().len(), 64);
2537 assert_eq!(
2538 first.artifact_payload_checksum(),
2539 first
2540 .artifact_payload_checksum_for_algorithm(ARTIFACT_PAYLOAD_CHECKSUM_ALGORITHM)
2541 .unwrap()
2542 );
2543 assert_eq!(
2544 first
2545 .artifact_payload_checksum_for_algorithm(LEGACY_ARTIFACT_PAYLOAD_CHECKSUM_ALGORITHM)
2546 .unwrap()
2547 .len(),
2548 16
2549 );
2550 assert_ne!(
2551 first.artifact_payload_checksum(),
2552 second.artifact_payload_checksum()
2553 );
2554 }
2555
2556 #[test]
2557 fn loads_artifact_payload_back_into_index() {
2558 let payload = UnidicReadingIndexPayload {
2559 schema_version: 1,
2560 payload_type: "moine.unidic.reading-index.surface-readings".to_string(),
2561 entries: vec![UnidicReadingIndexPayloadEntry {
2562 surface: "印刷".to_string(),
2563 readings: vec!["インサツ".to_string()],
2564 }],
2565 };
2566
2567 let index = UnidicReadingIndex::from_artifact_payload(payload).unwrap();
2568
2569 assert_eq!(index.len(), 1);
2570 assert_eq!(
2571 index.readings("印刷").as_deref(),
2572 Some(&["インサツ".to_string()][..])
2573 );
2574 }
2575
2576 #[test]
2577 fn loads_artifact_payload_reader() {
2578 let yaml = "\
2579schema_version: 1
2580payload_type: moine.unidic.reading-index.surface-readings
2581entries:
2582- surface: 刃
2583 readings:
2584 - ハ
2585 - ヤイバ
2586";
2587
2588 let index = UnidicReadingIndex::from_artifact_payload_reader(yaml.as_bytes()).unwrap();
2589
2590 assert_eq!(
2591 index.readings("刃").as_deref(),
2592 Some(&["ハ".to_string(), "ヤイバ".to_string()][..])
2593 );
2594 }
2595
2596 #[test]
2597 fn binary_artifact_payload_round_trips_to_equivalent_index() {
2598 let csv = "\
2599刃,1,2,3,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,ハ,刃,ハ,和
2600刃,1,2,3,名詞,普通名詞,一般,*,*,*,ヤイバ,刃,刃,ヤイバ,刃,ヤイバ,和
2601印刷,1,2,3,名詞,普通名詞,サ変可能,*,*,*,インサツ,印刷,印刷,インサツ,印刷,インサツ,漢
2602";
2603 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
2604 let mut bytes = Vec::new();
2605
2606 index.write_artifact_binary_payload(&mut bytes).unwrap();
2607 let loaded = UnidicReadingIndex::from_binary_artifact_payload_reader(bytes.as_slice())
2608 .expect("binary payload should load");
2609 let header = UnidicReadingIndex::binary_artifact_payload_header_reader(bytes.as_slice())
2610 .expect("binary payload header should load");
2611
2612 assert_eq!(
2613 header,
2614 UnidicBinaryArtifactPayloadHeader {
2615 version: 1,
2616 entries: 2,
2617 }
2618 );
2619 assert_eq!(loaded.artifact_payload(), index.artifact_payload());
2620 assert_eq!(
2621 loaded.artifact_payload_checksum(),
2622 index.artifact_payload_checksum()
2623 );
2624 }
2625
2626 #[test]
2627 fn indexed_artifact_payload_round_trips_and_supports_lookup() {
2628 let csv = "\
2629刃,1,2,3,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,ハ,刃,ハ,和
2630刃,1,2,3,名詞,普通名詞,一般,*,*,*,ヤイバ,刃,刃,ヤイバ,刃,ヤイバ,和
2631印刷,1,2,3,名詞,普通名詞,サ変可能,*,*,*,インサツ,印刷,印刷,インサツ,印刷,インサツ,漢
2632";
2633 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
2634 let mut bytes = Vec::new();
2635 index.write_indexed_artifact_payload(&mut bytes).unwrap();
2636
2637 let unique = std::time::SystemTime::now()
2638 .duration_since(std::time::UNIX_EPOCH)
2639 .unwrap()
2640 .as_nanos();
2641 let path = std::env::temp_dir().join(format!(
2642 "moine-indexed-test-{}-{}.moineidx",
2643 std::process::id(),
2644 unique
2645 ));
2646 std::fs::write(&path, &bytes).unwrap();
2647 let loaded = UnidicReadingIndex::from_indexed_artifact_payload_path(&path)
2648 .expect("indexed payload should load");
2649 let _ = std::fs::remove_file(&path);
2650 let loaded_from_bytes = UnidicReadingIndex::from_indexed_artifact_payload_bytes(&bytes)
2651 .expect("indexed payload bytes should load");
2652
2653 assert_eq!(loaded.len(), 2);
2654 assert_eq!(
2655 loaded.readings("刃").as_deref(),
2656 Some(&["ハ".to_string(), "ヤイバ".to_string()][..])
2657 );
2658 assert_eq!(
2659 loaded_from_bytes.artifact_payload(),
2660 index.artifact_payload()
2661 );
2662 assert_eq!(loaded.artifact_payload(), index.artifact_payload());
2663 assert_eq!(
2664 loaded.artifact_payload_checksum(),
2665 index.artifact_payload_checksum()
2666 );
2667 assert_eq!(
2668 loaded.reading_sequences("印刷", DictionaryReadingOptions::default()),
2669 vec!["インサツ".to_string()]
2670 );
2671 }
2672
2673 #[test]
2674 fn binary_artifact_payload_uses_stable_little_endian_layout() {
2675 let csv = "\
2676刃,1,2,3,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,ハ,刃,ハ,和
2677刃,1,2,3,名詞,普通名詞,一般,*,*,*,ヤイバ,刃,刃,ヤイバ,刃,ヤイバ,和
2678印刷,1,2,3,名詞,普通名詞,サ変可能,*,*,*,インサツ,印刷,印刷,インサツ,印刷,インサツ,漢
2679";
2680 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
2681 let mut bytes = Vec::new();
2682
2683 index.write_artifact_binary_payload(&mut bytes).unwrap();
2684
2685 #[rustfmt::skip]
2686 let expected = vec![
2687 b'M', b'O', b'I', b'N', b'E', b'U', b'0', b'1',
2688 1, 0, 0, 0,
2689 0, 0, 0, 0,
2690 2, 0, 0, 0, 0, 0, 0, 0,
2691 3, 0, 0, 0, 0xe5, 0x88, 0x83,
2692 2, 0, 0, 0,
2693 3, 0, 0, 0, 0xe3, 0x83, 0x8f,
2694 9, 0, 0, 0, 0xe3, 0x83, 0xa4, 0xe3, 0x82, 0xa4, 0xe3, 0x83, 0x90,
2695 6, 0, 0, 0, 0xe5, 0x8d, 0xb0, 0xe5, 0x88, 0xb7,
2696 1, 0, 0, 0,
2697 12, 0, 0, 0, 0xe3, 0x82, 0xa4, 0xe3, 0x83, 0xb3, 0xe3, 0x82, 0xb5, 0xe3, 0x83, 0x84,
2698 ];
2699 assert_eq!(bytes, expected);
2700 }
2701
2702 #[test]
2703 fn rejects_binary_artifact_bad_magic() {
2704 let bytes = *b"NOTMOINE";
2705 let err =
2706 UnidicReadingIndex::from_binary_artifact_payload_reader(bytes.as_slice()).unwrap_err();
2707
2708 assert!(matches!(
2709 err,
2710 UnidicArtifactPayloadError::InvalidBinaryMagic { .. }
2711 ));
2712 }
2713
2714 #[test]
2715 fn rejects_binary_artifact_unsupported_version() {
2716 let mut bytes = Vec::new();
2717 bytes.extend_from_slice(b"MOINEU01");
2718 bytes.extend_from_slice(&2_u32.to_le_bytes());
2719 bytes.extend_from_slice(&0_u32.to_le_bytes());
2720 bytes.extend_from_slice(&0_u64.to_le_bytes());
2721
2722 let err =
2723 UnidicReadingIndex::from_binary_artifact_payload_reader(bytes.as_slice()).unwrap_err();
2724
2725 assert!(matches!(
2726 err,
2727 UnidicArtifactPayloadError::UnsupportedBinaryVersion { version: 2 }
2728 ));
2729 }
2730
2731 #[test]
2732 fn rejects_binary_artifact_truncated_string() {
2733 let mut bytes = Vec::new();
2734 bytes.extend_from_slice(b"MOINEU01");
2735 bytes.extend_from_slice(&1_u32.to_le_bytes());
2736 bytes.extend_from_slice(&0_u32.to_le_bytes());
2737 bytes.extend_from_slice(&1_u64.to_le_bytes());
2738 bytes.extend_from_slice(&4_u32.to_le_bytes());
2739 bytes.extend_from_slice("刃".as_bytes());
2740
2741 let err =
2742 UnidicReadingIndex::from_binary_artifact_payload_reader(bytes.as_slice()).unwrap_err();
2743
2744 assert!(matches!(
2745 err,
2746 UnidicArtifactPayloadError::TruncatedBinary { field: "surface" }
2747 ));
2748 }
2749
2750 #[test]
2751 fn rejects_binary_artifact_invalid_utf8() {
2752 let mut bytes = Vec::new();
2753 bytes.extend_from_slice(b"MOINEU01");
2754 bytes.extend_from_slice(&1_u32.to_le_bytes());
2755 bytes.extend_from_slice(&0_u32.to_le_bytes());
2756 bytes.extend_from_slice(&1_u64.to_le_bytes());
2757 bytes.extend_from_slice(&1_u32.to_le_bytes());
2758 bytes.push(0xff);
2759 bytes.extend_from_slice(&0_u32.to_le_bytes());
2760
2761 let err =
2762 UnidicReadingIndex::from_binary_artifact_payload_reader(bytes.as_slice()).unwrap_err();
2763
2764 assert!(matches!(
2765 err,
2766 UnidicArtifactPayloadError::InvalidBinaryUtf8 {
2767 field: "surface",
2768 ..
2769 }
2770 ));
2771 }
2772
2773 #[test]
2774 fn rejects_binary_artifact_excessive_entry_count() {
2775 let mut bytes = Vec::new();
2776 bytes.extend_from_slice(b"MOINEU01");
2777 bytes.extend_from_slice(&1_u32.to_le_bytes());
2778 bytes.extend_from_slice(&0_u32.to_le_bytes());
2779 bytes.extend_from_slice(&((MAX_ARTIFACT_ENTRIES as u64) + 1).to_le_bytes());
2780
2781 let err =
2782 UnidicReadingIndex::from_binary_artifact_payload_reader(bytes.as_slice()).unwrap_err();
2783
2784 assert!(matches!(
2785 err,
2786 UnidicArtifactPayloadError::ArtifactLimitExceeded {
2787 field: "entry_count",
2788 ..
2789 }
2790 ));
2791 }
2792
2793 #[test]
2794 fn rejects_excessive_dictionary_reading_options() {
2795 let index = UnidicReadingIndex::default();
2796 let err = index
2797 .try_reading_paths_with_stats(
2798 "印刷",
2799 DictionaryReadingOptions {
2800 max_span_chars: MAX_QUERY_SPAN_CHARS + 1,
2801 ..DictionaryReadingOptions::default()
2802 },
2803 )
2804 .unwrap_err();
2805
2806 assert!(matches!(
2807 err,
2808 UnidicArtifactPayloadError::ArtifactLimitExceeded {
2809 field: "max_span_chars",
2810 ..
2811 }
2812 ));
2813
2814 let err = index
2815 .try_reading_paths_with_stats(
2816 "印刷",
2817 DictionaryReadingOptions {
2818 max_paths: MAX_QUERY_PATHS + 1,
2819 ..DictionaryReadingOptions::default()
2820 },
2821 )
2822 .unwrap_err();
2823
2824 assert!(matches!(
2825 err,
2826 UnidicArtifactPayloadError::ArtifactLimitExceeded {
2827 field: "max_paths",
2828 ..
2829 }
2830 ));
2831
2832 let err = index
2833 .try_reading_paths_with_stats(
2834 "印刷",
2835 DictionaryReadingOptions {
2836 max_readings_per_segment: Some(MAX_QUERY_READINGS_PER_SEGMENT + 1),
2837 ..DictionaryReadingOptions::default()
2838 },
2839 )
2840 .unwrap_err();
2841
2842 assert!(matches!(
2843 err,
2844 UnidicArtifactPayloadError::ArtifactLimitExceeded {
2845 field: "max_readings_per_segment",
2846 ..
2847 }
2848 ));
2849 }
2850
2851 #[test]
2852 fn rejects_artifact_payload_duplicate_surfaces() {
2853 let payload = UnidicReadingIndexPayload {
2854 schema_version: 1,
2855 payload_type: "moine.unidic.reading-index.surface-readings".to_string(),
2856 entries: vec![
2857 UnidicReadingIndexPayloadEntry {
2858 surface: "刃".to_string(),
2859 readings: vec!["ハ".to_string()],
2860 },
2861 UnidicReadingIndexPayloadEntry {
2862 surface: "刃".to_string(),
2863 readings: vec!["ヤイバ".to_string()],
2864 },
2865 ],
2866 };
2867
2868 let err = UnidicReadingIndex::from_artifact_payload(payload).unwrap_err();
2869
2870 assert!(matches!(
2871 err,
2872 UnidicArtifactPayloadError::DuplicateSurface { surface } if surface == "刃"
2873 ));
2874 }
2875
2876 #[test]
2877 fn rejects_artifact_payload_duplicate_readings() {
2878 let payload = UnidicReadingIndexPayload {
2879 schema_version: 1,
2880 payload_type: "moine.unidic.reading-index.surface-readings".to_string(),
2881 entries: vec![UnidicReadingIndexPayloadEntry {
2882 surface: "刃".to_string(),
2883 readings: vec!["ハ".to_string(), "ハ".to_string()],
2884 }],
2885 };
2886
2887 let err = UnidicReadingIndex::from_artifact_payload(payload).unwrap_err();
2888
2889 assert!(matches!(
2890 err,
2891 UnidicArtifactPayloadError::DuplicateReading { surface, reading }
2892 if surface == "刃" && reading == "ハ"
2893 ));
2894 }
2895
2896 #[test]
2897 fn rejects_artifact_payload_excessive_reading_count() {
2898 let payload = UnidicReadingIndexPayload {
2899 schema_version: 1,
2900 payload_type: "moine.unidic.reading-index.surface-readings".to_string(),
2901 entries: vec![UnidicReadingIndexPayloadEntry {
2902 surface: "刃".to_string(),
2903 readings: vec!["ハ".to_string(); MAX_ARTIFACT_READINGS_PER_ENTRY + 1],
2904 }],
2905 };
2906
2907 let err = UnidicReadingIndex::from_artifact_payload(payload).unwrap_err();
2908
2909 assert!(matches!(
2910 err,
2911 UnidicArtifactPayloadError::ArtifactLimitExceeded {
2912 field: "reading_count",
2913 ..
2914 }
2915 ));
2916 }
2917
2918 #[test]
2919 fn rejects_artifact_payload_schema_mismatch() {
2920 let payload = UnidicReadingIndexPayload {
2921 schema_version: 2,
2922 payload_type: "moine.unidic.reading-index.surface-readings".to_string(),
2923 entries: Vec::new(),
2924 };
2925
2926 let err = UnidicReadingIndex::from_artifact_payload(payload).unwrap_err();
2927
2928 assert!(matches!(
2929 err,
2930 UnidicArtifactPayloadError::UnsupportedSchemaVersion { version: 2 }
2931 ));
2932 }
2933
2934 #[test]
2935 fn reports_reading_expansion_stats() {
2936 let csv = "\
2937刃,1,2,3,名詞,普通名詞,一般,*,*,*,ジン,刃,刃,ジン,刃,ジン,漢
2938刃,1,2,3,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,ハ,刃,ハ,和
2939刃,1,2,3,名詞,普通名詞,一般,*,*,*,ヤイバ,刃,刃,ヤイバ,刃,ヤイバ,和
2940";
2941 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
2942 let expansion = index.reading_paths_with_stats(
2943 "刃",
2944 DictionaryReadingOptions {
2945 max_readings_per_segment: Some(2),
2946 ..DictionaryReadingOptions::default()
2947 },
2948 );
2949
2950 assert_eq!(expansion.paths.len(), 2);
2951 assert_eq!(
2952 expansion.stats,
2953 DictionaryReadingStats {
2954 matched_spans: 1,
2955 direct_fallback_spans: 0,
2956 longest_match_pruned_spans: 0,
2957 raw_segment_readings: 3,
2958 used_segment_readings: 2,
2959 pruned_segment_readings: 1,
2960 candidate_combinations: 2,
2961 unique_paths: 2,
2962 duplicate_joined_readings: 0,
2963 max_paths_hit_count: 0,
2964 }
2965 );
2966 }
2967
2968 #[test]
2969 fn reports_longest_match_and_path_limit_stats() {
2970 let csv = "\
2971茶,1,2,3,名詞,普通名詞,一般,*,*,*,チャ,茶,茶,チャ,茶,チャ,和
2972道,1,2,3,名詞,普通名詞,一般,*,*,*,ミチ,道,道,ミチ,道,ミチ,和
2973道具,1,2,3,名詞,普通名詞,一般,*,*,*,ドウグ,道具,道具,ドーグ,道具,ドーグ,和
2974具,1,2,3,名詞,普通名詞,一般,*,*,*,グ,具,具,グ,具,グ,和
2975";
2976 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
2977 let expansion = index.reading_paths_with_stats(
2978 "茶道具",
2979 DictionaryReadingOptions {
2980 longest_match_only: true,
2981 max_paths: 1,
2982 ..DictionaryReadingOptions::default()
2983 },
2984 );
2985
2986 assert_eq!(expansion.paths.len(), 1);
2987 assert!(expansion.stats.longest_match_pruned_spans > 0);
2988 assert!(expansion.stats.max_paths_hit_count > 0);
2989 }
2990
2991 #[test]
2992 fn hybrid_reading_paths_use_direct_fallback_for_kana_ascii_spans() {
2993 let csv = "\
2994印,1,2,3,名詞,普通名詞,一般,*,*,*,イン,印,印,イン,印,イン,漢
2995";
2996 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
2997 let expansion =
2998 index.hybrid_reading_paths_with_stats("印さt", DictionaryReadingOptions::default());
2999
3000 assert_eq!(
3001 expansion.paths,
3002 vec![DictionaryReadingPath {
3003 joined_reading: "インさt".to_string(),
3004 segments: vec![
3005 DictionaryReadingSegment {
3006 surface: "印".to_string(),
3007 reading: "イン".to_string(),
3008 source: DictionaryReadingSegmentSource::Dictionary,
3009 },
3010 DictionaryReadingSegment {
3011 surface: "さt".to_string(),
3012 reading: "さt".to_string(),
3013 source: DictionaryReadingSegmentSource::Direct,
3014 },
3015 ],
3016 }]
3017 );
3018 assert_eq!(expansion.stats.direct_fallback_spans, 2);
3019 }
3020
3021 #[test]
3022 fn hybrid_reading_paths_keep_shorter_dictionary_spans_for_direct_tail() {
3023 let csv = "\
3024印,1,2,3,名詞,普通名詞,一般,*,*,*,イン,印,印,イン,印,イン,漢
3025印さ,1,2,3,動詞,一般,*,*,*,*,シルス,印す,印す,シルス,印す,シルス,和
3026";
3027 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
3028 let expansion = index.hybrid_reading_paths_with_stats(
3029 "印さt",
3030 DictionaryReadingOptions {
3031 longest_match_only: true,
3032 ..DictionaryReadingOptions::default()
3033 },
3034 );
3035
3036 assert!(expansion
3037 .paths
3038 .iter()
3039 .any(|path| path.joined_reading == "インさt"));
3040 assert_eq!(expansion.stats.longest_match_pruned_spans, 0);
3041 }
3042
3043 #[test]
3044 fn hybrid_reading_paths_still_reject_uncovered_kanji() {
3045 let index = UnidicReadingIndex::default();
3046 let expansion =
3047 index.hybrid_reading_paths_with_stats("未知z", DictionaryReadingOptions::default());
3048
3049 assert!(expansion.paths.is_empty());
3050 assert_eq!(expansion.stats.direct_fallback_spans, 1);
3051 }
3052
3053 #[test]
3054 fn can_use_pron_instead_of_lform() {
3055 let csv = "\
3056刃,18521,20041,11551,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,ハ,刃,ハ,和,ハ濁,基本形,*,*,*,*,体,ハ,ハ,ハ,ハ,1,C3,*,8060803244761600,29325
3057刃,18521,20055,14836,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,バ,刃,バ,和,ハ濁,濁音形,*,*,*,*,体,バ,バ,バ,ハ,1,C3,*,8060803244769792,29325
3058";
3059 let index = UnidicReadingIndex::from_lex_csv_reader_with_field(
3060 csv.as_bytes(),
3061 UnidicReadingField::Pron,
3062 )
3063 .unwrap();
3064
3065 assert_eq!(
3066 index.readings("刃").as_deref(),
3067 Some(&["ハ".to_string(), "バ".to_string()][..])
3068 );
3069 }
3070
3071 #[test]
3072 fn fullwidth_ascii_surfaces_are_indexed_under_halfwidth_aliases() {
3073 let csv = "\
3074WHISKY,1,2,3,名詞,普通名詞,一般,*,*,*,ウイスキー,WHISKY,WHISKY,ウイスキー,WHISKY,ウイスキー,外
3075WHISKEY,1,2,3,名詞,普通名詞,一般,*,*,*,ウイスキー,WHISKEY,WHISKEY,ウイスキー,WHISKEY,ウイスキー,外
3076MALT,1,2,3,名詞,普通名詞,一般,*,*,*,モルト,MALT,MALT,モルト,MALT,モルト,外
3077abc,1,2,3,名詞,固有名詞,一般,*,*,*,エービーシー,abc,abc,エービーシー,abc,エービーシー,外
3078";
3079 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
3080
3081 assert_eq!(
3082 index.readings("WHISKY").as_deref(),
3083 Some(&["ウイスキー".to_string()][..])
3084 );
3085 assert_eq!(
3086 index.readings("WHISKY").as_deref(),
3087 Some(&["ウイスキー".to_string()][..])
3088 );
3089 assert_eq!(
3090 index.readings("WHISKEY").as_deref(),
3091 Some(&["ウイスキー".to_string()][..])
3092 );
3093 assert_eq!(
3094 index.readings("WHISKEY").as_deref(),
3095 Some(&["ウイスキー".to_string()][..])
3096 );
3097 assert_eq!(
3098 index.readings("MALT").as_deref(),
3099 Some(&["モルト".to_string()][..])
3100 );
3101 assert_eq!(index.readings("abc"), None);
3102 }
3103
3104 #[test]
3105 fn builds_reading_sequences_from_dictionary_segments() {
3106 let csv = "\
3107鬼滅,1,2,3,名詞,普通名詞,一般,*,*,*,キメツ,鬼滅,鬼滅,キメツ,鬼滅,キメツ,固
3108の,1,2,3,助詞,格助詞,*,*,*,*,ノ,の,の,ノ,の,ノ,和
3109刃,1,2,3,名詞,普通名詞,一般,*,*,*,ハ,刃,刃,ハ,刃,ハ,和
3110刃,1,2,3,名詞,普通名詞,一般,*,*,*,ヤイバ,刃,刃,ヤイバ,刃,ヤイバ,和
3111";
3112 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
3113 let readings = index.reading_sequences("鬼滅の刃", DictionaryReadingOptions::default());
3114
3115 assert_eq!(
3116 readings,
3117 vec!["キメツノハ".to_string(), "キメツノヤイバ".to_string()]
3118 );
3119 }
3120
3121 #[test]
3122 fn reading_paths_keep_segmentation_and_segment_readings() {
3123 let csv = "\
3124茶,1,2,3,名詞,普通名詞,一般,*,*,*,チャ,茶,茶,チャ,茶,チャ,和
3125道具,1,2,3,名詞,普通名詞,一般,*,*,*,ドウグ,道具,道具,ドーグ,道具,ドーグ,漢
3126";
3127 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
3128 let paths = index.reading_paths(
3129 "茶道具",
3130 DictionaryReadingOptions {
3131 longest_match_only: true,
3132 ..DictionaryReadingOptions::default()
3133 },
3134 );
3135
3136 assert_eq!(
3137 paths,
3138 vec![DictionaryReadingPath {
3139 joined_reading: "チャドーグ".to_string(),
3140 segments: vec![
3141 DictionaryReadingSegment {
3142 surface: "茶".to_string(),
3143 reading: "チャ".to_string(),
3144 source: DictionaryReadingSegmentSource::Dictionary,
3145 },
3146 DictionaryReadingSegment {
3147 surface: "道具".to_string(),
3148 reading: "ドーグ".to_string(),
3149 source: DictionaryReadingSegmentSource::Dictionary,
3150 },
3151 ],
3152 }]
3153 );
3154 }
3155
3156 #[test]
3157 fn builds_romaji_lattice_from_dictionary_segments() {
3158 let csv = "\
3159茶,1,2,3,名詞,普通名詞,一般,*,*,*,チャ,茶,茶,チャ,茶,チャ,和
3160道具,1,2,3,名詞,普通名詞,一般,*,*,*,ドウグ,道具,道具,ドーグ,道具,ドーグ,和
3161";
3162 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
3163 let lattice = index
3164 .romaji_lattice("茶道具", DictionaryReadingOptions::default())
3165 .unwrap()
3166 .unwrap();
3167
3168 assert_eq!(
3169 moine_core::distance(&lattice, &Lattice::from_paths(["chadougu"])),
3170 0
3171 );
3172 }
3173
3174 #[test]
3175 fn builds_romaji_lattice_directly_from_reading_paths() {
3176 let paths = vec![
3177 DictionaryReadingPath {
3178 joined_reading: "チャドウグ".to_string(),
3179 segments: vec![
3180 DictionaryReadingSegment {
3181 surface: "茶".to_string(),
3182 reading: "チャ".to_string(),
3183 source: DictionaryReadingSegmentSource::Dictionary,
3184 },
3185 DictionaryReadingSegment {
3186 surface: "道具".to_string(),
3187 reading: "ドウグ".to_string(),
3188 source: DictionaryReadingSegmentSource::Dictionary,
3189 },
3190 ],
3191 },
3192 DictionaryReadingPath {
3193 joined_reading: "チャドーグ".to_string(),
3194 segments: vec![
3195 DictionaryReadingSegment {
3196 surface: "茶".to_string(),
3197 reading: "チャ".to_string(),
3198 source: DictionaryReadingSegmentSource::Dictionary,
3199 },
3200 DictionaryReadingSegment {
3201 surface: "道具".to_string(),
3202 reading: "ドーグ".to_string(),
3203 source: DictionaryReadingSegmentSource::Dictionary,
3204 },
3205 ],
3206 },
3207 ];
3208 let lattice = romaji_lattice_from_reading_paths(&paths).unwrap();
3209
3210 assert_eq!(
3211 moine_core::distance(&lattice, &Lattice::from_paths(["chadougu"])),
3212 0
3213 );
3214 assert_eq!(
3215 moine_core::distance(&lattice, &Lattice::from_paths(["chadoogu"])),
3216 0
3217 );
3218 }
3219
3220 #[test]
3221 fn structured_reading_paths_keep_cross_segment_context() {
3222 let paths = vec![DictionaryReadingPath {
3223 joined_reading: "マッチャ".to_string(),
3224 segments: vec![
3225 DictionaryReadingSegment {
3226 surface: "抹".to_string(),
3227 reading: "マッ".to_string(),
3228 source: DictionaryReadingSegmentSource::Dictionary,
3229 },
3230 DictionaryReadingSegment {
3231 surface: "茶".to_string(),
3232 reading: "チャ".to_string(),
3233 source: DictionaryReadingSegmentSource::Dictionary,
3234 },
3235 ],
3236 }];
3237 let lattice = romaji_lattice_from_reading_paths(&paths).unwrap();
3238
3239 assert_eq!(
3240 moine_core::distance(&lattice, &Lattice::from_paths(["maccha"])),
3241 0
3242 );
3243 assert_eq!(
3244 moine_core::distance(&lattice, &Lattice::from_paths(["mattya"])),
3245 0
3246 );
3247 }
3248
3249 #[test]
3250 fn can_restrict_reading_sequences_to_longest_matches() {
3251 let csv = "\
3252茶,1,2,3,名詞,普通名詞,一般,*,*,*,チャ,茶,茶,チャ,茶,チャ,和
3253道,1,2,3,名詞,普通名詞,一般,*,*,*,ミチ,道,道,ミチ,道,ミチ,和
3254道具,1,2,3,名詞,普通名詞,一般,*,*,*,ドウグ,道具,道具,ドーグ,道具,ドーグ,和
3255具,1,2,3,名詞,普通名詞,一般,*,*,*,グ,具,具,グ,具,グ,和
3256";
3257 let index = UnidicReadingIndex::from_lex_csv_reader(csv.as_bytes()).unwrap();
3258 let readings = index.reading_sequences(
3259 "茶道具",
3260 DictionaryReadingOptions {
3261 longest_match_only: true,
3262 ..DictionaryReadingOptions::default()
3263 },
3264 );
3265
3266 assert_eq!(readings, vec!["チャドーグ".to_string()]);
3267 }
3268}