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