1pub mod error;
2mod stardict;
3mod idx;
4mod ifo;
5mod dict;
6mod dictzip;
7#[cfg(feature = "sled")]
8mod stardict_sled;
9#[cfg(feature = "sqlite")]
10mod stardict_sqlite;
11
12use std::fs;
13use std::fs::OpenOptions;
14use std::io::Read;
15use std::path::PathBuf;
16use dirs::cache_dir;
17#[cfg(feature = "sqlite")]
18use serde::{Serialize, Deserialize};
19
20use crate::error::{Error, Result};
21pub use crate::ifo::Ifo;
22pub use crate::stardict::StarDictStd;
23#[cfg(feature = "sled")]
24pub use crate::stardict_sled::StarDictCachedSled;
25#[cfg(feature = "sqlite")]
26pub use crate::stardict_sqlite::StarDictCachedSqlite;
27
28#[inline]
29fn buf_to_string(buf: &[u8]) -> String {
30 String::from_utf8_lossy(buf)
31 .chars()
32 .filter(|&c| c != '\u{fffd}')
33 .collect()
34}
35
36#[derive(Debug)]
37#[cfg_attr(feature = "sqlite", derive(Serialize, Deserialize))]
38pub struct WordDefinitionSegment {
39 pub types: String,
40 pub text: String,
41}
42
43#[derive(Debug)]
44#[cfg_attr(feature = "sqlite", derive(Serialize, Deserialize))]
45pub struct WordDefinition {
46 pub word: String,
47 pub segments: Vec<WordDefinitionSegment>,
48}
49
50pub trait StarDict {
51 fn path(&self) -> &PathBuf;
52 fn ifo(&self) -> &Ifo;
53 fn dict_name(&self) -> &str {
54 &self.ifo().bookname
55 }
56 fn lookup(&mut self, word: &str) -> Result<Option<Vec<WordDefinition>>>;
57 fn get_resource(&self, href: &str) -> Result<Option<Vec<u8>>> {
58 let mut path_str = href;
59 if let Some(ch) = path_str.chars().nth(0) {
60 if ch == '/' {
61 path_str = &path_str[1..];
62 }
63 if path_str.len() > 0 {
64 let mut path = self.path().join("res");
65 for sub in path_str.split("/") {
66 path = path.join(sub);
67 }
68 if path.exists() {
69 let mut file = OpenOptions::new()
70 .read(true)
71 .open(path)
72 .map_err(|e| Error::FailedLoadResource(href.to_owned(), e.to_string()))?;
73 let mut buf = vec![];
74 file.read_to_end(&mut buf)
75 .map_err(|e| Error::FailedLoadResource(href.to_owned(), e.to_string()))?;
76 return Ok(Some(buf));
77 }
78 }
79 }
80 Err(Error::NoResourceFound(href.to_owned()))
81 }
82}
83
84fn get_cache_dir<'a>(path: &'a PathBuf, cache_name: &str,
85 idx_cache_suffix: &str, syn_cache_suffix: Option<&str>)
86 -> Result<(PathBuf, Option<PathBuf>)>
87{
88 let dict_name = path.file_name().unwrap().to_str().unwrap();
89 let cache_dir = cache_dir().ok_or_else(|| Error::NoCacheDir)?;
90 let cache_dir = cache_dir.join(cache_name);
91 if !cache_dir.exists() {
92 fs::create_dir_all(&cache_dir)?;
93 }
94 let idx_cache_str = format!("{}.{}", dict_name, idx_cache_suffix);
95 let idx_cache = cache_dir.join(&idx_cache_str);
96 let syn_cache = if let Some(suffix) = syn_cache_suffix {
97 let syn_cache_str = format!("{}.{}", dict_name, suffix);
98 Some(cache_dir.join(&syn_cache_str))
99 } else {
100 None
101 };
102
103 Ok((idx_cache, syn_cache))
104}
105
106#[inline]
107#[cfg(feature = "sled")]
108pub fn with_sled(path: impl Into<PathBuf>, cache_name: &str)
109 -> Result<StarDictCachedSled> {
110 create(path, |path, ifo, idx, idx_gz, syn, dict, dict_bz|
111 StarDictCachedSled::new(path, ifo, idx, idx_gz, syn, dict, dict_bz, cache_name))
112}
113
114#[inline]
115#[cfg(feature = "sqlite")]
116pub fn with_sqlite(path: impl Into<PathBuf>, cache_name: &str)
117 -> Result<StarDictCachedSqlite> {
118 create(path, |path, ifo, idx, idx_gz, syn, dict, dict_bz|
119 StarDictCachedSqlite::new(path, ifo, idx, idx_gz, syn, dict, dict_bz, cache_name))
120}
121
122#[inline]
123pub fn no_cache(path: impl Into<PathBuf>) -> Result<StarDictStd> {
124 create(path, StarDictStd::new)
125}
126
127fn create<C, T>(path: impl Into<PathBuf>, creator: C) -> Result<T>
128 where C: FnOnce(PathBuf, Ifo, PathBuf, bool, Option<PathBuf>, PathBuf, bool) -> Result<T>
129{
130 fn get_sub_file(
131 prefix: &str,
132 name: &'static str,
133 compress_suffix: &'static str,
134 ) -> Result<(PathBuf, bool)> {
135 let mut path_str = format!("{}.{}", prefix, name);
136 let mut path = PathBuf::from(&path_str);
137 if path.exists() {
138 Ok((path, false))
139 } else {
140 path_str.push('.');
141 path_str.push_str(compress_suffix);
142 path = PathBuf::from(path_str);
143 if path.exists() {
144 Ok((path, true))
145 } else {
146 Err(Error::NoFileFound(name))
147 }
148 }
149 }
150
151 let mut ifo = None;
152 let path = path.into();
153 for p in path.read_dir().map_err(|e| Error::FailedOpenIfo(e))? {
154 let path = p.map_err(|e| Error::FailedOpenIfo(e))?.path();
155 if let Some(extension) = path.extension() {
156 if extension.to_str().unwrap() == "ifo" {
157 ifo = Some(path);
158 break;
159 }
160 }
161 }
162
163 if let Some(ifo) = ifo {
164 let ifo_path = ifo.to_str().unwrap();
165 let prefix = &ifo_path[0..ifo_path.len() - 4];
166 let (idx, idx_gz) = get_sub_file(prefix, "idx", "gz")?;
167 let (dict, dict_bz) = get_sub_file(prefix, "dict", "dz")?;
168 let syn_path = PathBuf::from(&format!("{}.syn", prefix));
170 let syn = if syn_path.exists() {
171 Some(syn_path)
172 } else {
173 None
174 };
175
176 let ifo = Ifo::new(ifo)?;
177 creator(path, ifo, idx, idx_gz, syn, dict, dict_bz)
178 } else {
179 Err(Error::NoFileFound("ifo"))
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use std::thread;
186 use std::time::Duration;
187 use crate::error::Error;
188 use crate::StarDict;
189 #[cfg(feature = "sled")]
190 use crate::with_sled;
191 #[cfg(feature = "sqlite")]
192 use crate::with_sqlite;
193 use crate::no_cache;
194
195 const CACHE_NAME: &str = "test";
196 const DICT: &str = "/home/zl/.stardict/dic/stardict-chibigenc-2.4.2/";
197 const WORD: &str = "汉";
198 const WORD_DEFINITION: &str = "漢";
199
200 #[test]
201 fn lookup() {
202 let mut dict = no_cache(DICT).unwrap();
203 let definitions = dict.lookup(WORD).unwrap().unwrap();
204 assert_eq!(definitions.len(), 1);
205 assert_eq!(definitions[0].word, WORD_DEFINITION);
206 assert_eq!(definitions[0].segments.len(), 1);
207 assert_eq!(definitions[0].segments[0].types, "g");
208 }
209
210 #[test]
211 #[cfg(feature = "sled")]
212 fn lookup_sled() {
213 let mut dict = with_sled(DICT, CACHE_NAME).unwrap();
214 let definitions = dict.lookup(WORD).unwrap().unwrap();
215 assert_eq!(definitions.len(), 1);
216 assert_eq!(definitions[0].word, WORD_DEFINITION);
217 assert_eq!(definitions[0].segments.len(), 1);
218 assert_eq!(definitions[0].segments[0].types, "g");
219
220 let mut dict = no_cache(DICT).unwrap();
221 let std_definitions = dict.lookup(WORD).unwrap().unwrap();
222 for i in 0..definitions.len() {
223 let cached = &definitions[i];
224 let std = &std_definitions[i];
225 assert_eq!(cached.word, std.word);
226 for j in 0..cached.segments.len() {
227 let c = &cached.segments[j];
228 let s = &std.segments[j];
229 assert_eq!(c.types, s.types);
230 assert_eq!(c.text, s.text);
231 }
232 }
233 }
234
235 #[test]
236 #[cfg(feature = "sqlite")]
237 fn lookup_sqlite() {
238 let mut dict = with_sqlite(DICT, CACHE_NAME).unwrap();
239 let definitions = loop {
240 match dict.lookup(WORD) {
241 Ok(definitions) => break definitions,
242 Err(Error::CacheInitiating) => thread::sleep(Duration::from_secs(1)),
243 Err(_) => assert!(false),
244 }
245 }.unwrap();
246 assert_eq!(definitions.len(), 1);
247 assert_eq!(definitions[0].word, WORD_DEFINITION);
248 assert_eq!(definitions[0].segments.len(), 1);
249 assert_eq!(definitions[0].segments[0].types, "g");
250
251 let mut dict = no_cache(DICT).unwrap();
252 let std_definitions = dict.lookup(WORD).unwrap().unwrap();
253 for i in 0..definitions.len() {
254 let cached = &definitions[i];
255 let std = &std_definitions[i];
256 assert_eq!(cached.word, std.word);
257 for j in 0..cached.segments.len() {
258 let c = &cached.segments[j];
259 let s = &std.segments[j];
260 assert_eq!(c.types, s.types);
261 assert_eq!(c.text, s.text);
262 }
263 }
264 }
265}