Skip to main content

sudachi/dic/
description.rs

1/*
2 * Copyright (c) 2025-2026 Works Applications Co., Ltd.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use nom::number::complete::le_u64;
18use std::fmt::Display;
19use std::time::Duration;
20use thiserror::Error;
21
22use super::header::HeaderVersion;
23use super::read::{
24    error::SudachiNomResult,
25    utf8_string::utf8_string,
26    varint::{varint32, varint64},
27};
28use crate::error::SudachiResult;
29
30static MAGIC_BYTES: &[u8] = b"SudachiBinaryDic";
31
32/// Sudachi error
33#[derive(Error, Debug, Eq, PartialEq)]
34#[non_exhaustive]
35pub enum DescriptionError {
36    #[error("Unable to parse")]
37    CannotParse,
38
39    #[error("Invalid magic bytes")]
40    InvalidMagicBytes,
41
42    #[error("V0 version")]
43    V0Version,
44
45    #[error("Invalid header version {0}")]
46    InvalidVersion(u64),
47
48    #[error("Dictionary part not found: {0}")]
49    DictionaryPartNotFound(String),
50
51    #[error("Dictionary part out of range: {0}..{1}")]
52    DictionaryPartOutOfRange(usize, usize),
53}
54
55/// Parts of the binary dictionary
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum Block {
58    // description always takes the first 1 block
59    // grammar parts:
60    // word connection cost matrix
61    ConnectionMatrix,
62    // list of part of speechs
63    POSTable,
64    // lexicon parts:
65    // TRIE
66    TRIEIndex,
67    // mapping from a index-form to entries
68    WordPointers,
69    // word entries
70    Entries,
71    // storage of strings in the lexicon
72    Strings,
73    // build-time reference_id table
74    ReferenceIdTable,
75}
76
77impl Block {
78    /// return the string representation of the block
79    /// This must be same as the name defined in the Java version.
80    fn as_string_representation(&self) -> &str {
81        match self {
82            Block::ConnectionMatrix => "ConnMatrix",
83            Block::POSTable => "POS",
84            Block::TRIEIndex => "TrieIndex",
85            Block::WordPointers => "WordPointers",
86            Block::Entries => "Entries",
87            Block::Strings => "Strings",
88            Block::ReferenceIdTable => "ReferenceIdTable",
89        }
90    }
91}
92
93impl Display for Block {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(f, "{}", self.as_string_representation())
96    }
97}
98
99/// Information of each blocks in the binary dictionary
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct BlockInfo {
102    name: String,
103    start: u64,
104    size: u64,
105}
106
107impl BlockInfo {
108    /// Parse BlockInfo from bytes.
109    pub fn parse(buf: &[u8]) -> SudachiNomResult<&[u8], Self> {
110        let (rest, (name, start, size)) =
111            nom::sequence::tuple((utf8_string, varint64, varint64))(buf)?;
112        Ok((rest, Self { name, start, size }))
113    }
114
115    pub fn name(&self) -> &str {
116        &self.name
117    }
118
119    pub fn start(&self) -> u64 {
120        self.start
121    }
122
123    pub fn size(&self) -> u64 {
124        self.size
125    }
126
127    pub fn end(&self) -> u64 {
128        self.start + self.size
129    }
130}
131
132/// The description part of the binary dictionary
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct Description {
135    creation_time: Duration,
136    comment: String,
137    signature: String,
138    reference: String,
139    blocks: Vec<BlockInfo>,
140    flags: u64,
141    num_total_entries: u32,
142    num_indexed_entries: u32,
143}
144
145impl Description {
146    pub fn load(buf: &[u8]) -> SudachiResult<Self> {
147        Self::check_v0_format(buf)?;
148
149        let rest = Self::check_magic(buf)?;
150        let (rest, version) = le_u64(rest)?;
151        if version == 1 {
152            Self::load_v1(rest)
153        } else {
154            Err(DescriptionError::InvalidVersion(version).into())
155        }
156    }
157
158    /// Check if the dictionary is in V0 format.
159    ///
160    /// In V0 format, the version is stored as a u64 (long) at the beginning of the binary.
161    fn check_v0_format(buf: &[u8]) -> SudachiResult<()> {
162        let (_rest, version) = le_u64(buf)?;
163        let v0_version = HeaderVersion::from_u64(version);
164        match v0_version {
165            Some(_) => Err(DescriptionError::V0Version.into()),
166            None => Ok(()),
167        }
168    }
169
170    /// Check if the first bytes are magic bytes.
171    ///
172    /// If so, return the rest of the buffer.
173    fn check_magic(buf: &[u8]) -> SudachiResult<&[u8]> {
174        let (rest, first_bytes) = nom::bytes::complete::take(MAGIC_BYTES.len())(buf)?;
175        match MAGIC_BYTES
176            .iter()
177            .zip(first_bytes.iter())
178            .position(|(a, b)| a != b)
179        {
180            Some(_) => Err(DescriptionError::InvalidMagicBytes.into()),
181            None => Ok(rest),
182        }
183    }
184
185    /// Load Vi format description.
186    ///
187    /// assume that followings are already loaded:
188    /// - magic bytes (16 bytes)
189    /// - version (long/u64)
190    fn load_v1(buf: &[u8]) -> SudachiResult<Self> {
191        let (
192            _rest,
193            (
194                creation_time_secs,
195                flags,
196                comment,
197                signature,
198                reference,
199                num_indexed_entries,
200                num_total_entries,
201                blocks,
202            ),
203        ) = nom::sequence::tuple((
204            le_u64,
205            le_u64,
206            utf8_string,
207            utf8_string,
208            utf8_string,
209            varint32,
210            varint32,
211            nom::multi::length_count(varint32, BlockInfo::parse),
212        ))(buf)?;
213
214        Ok(Self {
215            creation_time: Duration::from_secs(creation_time_secs),
216            comment,
217            signature,
218            reference,
219            blocks,
220            flags,
221            num_total_entries,
222            num_indexed_entries,
223        })
224    }
225
226    pub fn creation_time(&self) -> Duration {
227        self.creation_time
228    }
229
230    pub fn comment(&self) -> &str {
231        &self.comment
232    }
233
234    pub fn signature(&self) -> &str {
235        &self.signature
236    }
237
238    pub fn reference(&self) -> &str {
239        &self.reference
240    }
241
242    pub fn is_system_dictionary(&self) -> bool {
243        self.reference.is_empty()
244    }
245
246    pub fn is_user_dictionary(&self) -> bool {
247        !self.reference.is_empty()
248    }
249
250    pub fn blocks(&self) -> &[BlockInfo] {
251        &self.blocks
252    }
253
254    pub fn slice_or_none<'a>(
255        &self,
256        buf: &'a [u8],
257        block: Block,
258    ) -> SudachiResult<Option<&'a [u8]>> {
259        let block_name = block.as_string_representation();
260        match self.blocks.iter().find(|block| block.name() == block_name) {
261            Some(block) => {
262                let start = block.start() as usize;
263                let end = block.end() as usize;
264                if buf.len() < end {
265                    return Err(DescriptionError::DictionaryPartOutOfRange(start, end).into());
266                }
267                Ok(Some(&buf[start..end]))
268            }
269            None => Ok(None),
270        }
271    }
272
273    pub fn slice<'a>(&self, buf: &'a [u8], block: Block) -> SudachiResult<&'a [u8]> {
274        self.slice_or_none(buf, block)?
275            .ok_or_else(|| DescriptionError::DictionaryPartNotFound(block.to_string()).into())
276    }
277
278    pub fn is_runtime_costs(&self) -> bool {
279        self.flags & 0x1 != 0
280    }
281
282    pub fn num_total_entries(&self) -> u32 {
283        self.num_total_entries
284    }
285
286    pub fn num_indexed_entries(&self) -> u32 {
287        self.num_indexed_entries
288    }
289}