stardict_lib/
lib.rs

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		// optional syn file
169		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}