1#![cfg_attr(docsrs, feature(doc_cfg))]
74
75#[cfg(feature = "static-dictionaries")]
76#[macro_use]
77extern crate lazy_static_include;
78
79#[cfg(feature = "static-dictionaries")]
80use std::fs::{self, File};
81#[cfg(feature = "static-dictionaries")]
82use std::io::Write;
83#[cfg(feature = "static-dictionaries")]
84use std::sync::LazyLock;
85use std::{
86 ffi::{CStr, CString},
87 fmt,
88 path::Path,
89 sync::Mutex,
90};
91
92use enum_ordinalize::Ordinalize;
93use libc::{c_char, c_int, c_void, size_t};
94
95#[link(name = "opencc")]
96unsafe extern "C" {
97 pub fn opencc_open(config_file_path: *const c_char) -> *mut c_void;
98 pub fn opencc_close(opencc: *mut c_void) -> c_int;
99 pub fn opencc_convert_utf8(
100 opencc: *mut c_void,
101 input: *const c_char,
102 length: size_t,
103 ) -> *mut c_char;
104 pub fn opencc_convert_utf8_to_buffer(
105 opencc: *mut c_void,
106 input: *const c_char,
107 length: size_t,
108 output: *mut c_char,
109 ) -> size_t;
110 pub fn opencc_convert_utf8_free(str: *mut c_char);
111 pub fn opencc_error() -> *const c_char;
112}
113
114#[cfg(feature = "static-dictionaries")]
115struct SD(&'static str, &'static [u8]);
116
117#[cfg(feature = "static-dictionaries")]
118macro_rules! new_sd_instance {
119 ($name:ident, $file_name:expr) => {
120 static $name: LazyLock<SD> = LazyLock::new(|| {
121 lazy_static_include_bytes! {
122 RES => ("opencc", $file_name)
123 }
124
125 SD($file_name, &RES)
126 });
127 };
128}
129
130#[cfg(feature = "static-dictionaries")]
131new_sd_instance!(CJK_COMPATIBILITY_IDEOGRAPHS_OCD, "CJK_Compatibility_Ideographs.ocd2");
132#[cfg(feature = "static-dictionaries")]
133new_sd_instance!(HK2S_JSON, "hk2s.json");
134#[cfg(feature = "static-dictionaries")]
135new_sd_instance!(HK2SP_JSON, "hk2sp.json");
136#[cfg(feature = "static-dictionaries")]
137new_sd_instance!(HK2T_JSON, "hk2t.json");
138#[cfg(feature = "static-dictionaries")]
139new_sd_instance!(HKPHRASES_OCD, "HKPhrases.ocd2");
140#[cfg(feature = "static-dictionaries")]
141new_sd_instance!(HKPHRASES_REV_OCD, "HKPhrasesRev.ocd2");
142#[cfg(feature = "static-dictionaries")]
143new_sd_instance!(HKVARIANTS_OCD, "HKVariants.ocd2");
144#[cfg(feature = "static-dictionaries")]
145new_sd_instance!(HKVARIANTS_PHRASES_OCD, "HKVariantsPhrases.ocd2");
146#[cfg(feature = "static-dictionaries")]
147new_sd_instance!(HKVARIANTS_REV_OCD, "HKVariantsRev.ocd2");
148#[cfg(feature = "static-dictionaries")]
149new_sd_instance!(HKVARIANTS_REV_PHRASES_OCD, "HKVariantsRevPhrases.ocd2");
150#[cfg(feature = "static-dictionaries")]
151new_sd_instance!(JP2T_JSON, "jp2t.json");
152#[cfg(feature = "static-dictionaries")]
153new_sd_instance!(JPSHINJITAI_CHARACTERS_OCD, "JPShinjitaiCharacters.ocd2");
154#[cfg(feature = "static-dictionaries")]
155new_sd_instance!(JPSHINJITAI_CHARACTERS_REV_OCD, "JPShinjitaiCharactersRev.ocd2");
156#[cfg(feature = "static-dictionaries")]
157new_sd_instance!(JPSHINJITAI_PHRASES_OCD, "JPShinjitaiPhrases.ocd2");
158#[cfg(feature = "static-dictionaries")]
159new_sd_instance!(S2HK_JSON, "s2hk.json");
160#[cfg(feature = "static-dictionaries")]
161new_sd_instance!(S2HKP_JSON, "s2hkp.json");
162#[cfg(feature = "static-dictionaries")]
163new_sd_instance!(S2T_JSON, "s2t.json");
164#[cfg(feature = "static-dictionaries")]
165new_sd_instance!(S2TW_JSON, "s2tw.json");
166#[cfg(feature = "static-dictionaries")]
167new_sd_instance!(S2TWP_JSON, "s2twp.json");
168#[cfg(feature = "static-dictionaries")]
169new_sd_instance!(STCHARACTERS_OCD, "STCharacters.ocd2");
170#[cfg(feature = "static-dictionaries")]
171new_sd_instance!(
172 STPHRASES_GENERATED_FROM_REGIONAL_PHRASES_OCD,
173 "STPhrases_GeneratedFromRegionalPhrases.ocd2"
174);
175#[cfg(feature = "static-dictionaries")]
176new_sd_instance!(STPHRASES_OCD, "STPhrases.ocd2");
177#[cfg(feature = "static-dictionaries")]
178new_sd_instance!(T2HK_JSON, "t2hk.json");
179#[cfg(feature = "static-dictionaries")]
180new_sd_instance!(T2JP_JSON, "t2jp.json");
181#[cfg(feature = "static-dictionaries")]
182new_sd_instance!(T2S_JSON, "t2s.json");
183#[cfg(feature = "static-dictionaries")]
184new_sd_instance!(T2TW_JSON, "t2tw.json");
185#[cfg(feature = "static-dictionaries")]
186new_sd_instance!(TSCHARACTERS_OCD, "TSCharacters.ocd2");
187#[cfg(feature = "static-dictionaries")]
188new_sd_instance!(TSCHARACTERS_EXT_OCD, "TSCharactersExt.ocd2");
189#[cfg(feature = "static-dictionaries")]
190new_sd_instance!(TSPHRASES_OCD, "TSPhrases.ocd2");
191#[cfg(feature = "static-dictionaries")]
192new_sd_instance!(TW2S_JSON, "tw2s.json");
193#[cfg(feature = "static-dictionaries")]
194new_sd_instance!(TW2SP_JSON, "tw2sp.json");
195#[cfg(feature = "static-dictionaries")]
196new_sd_instance!(TW2T_JSON, "tw2t.json");
197#[cfg(feature = "static-dictionaries")]
198new_sd_instance!(TWPHRASES_OCD, "TWPhrases.ocd2");
199#[cfg(feature = "static-dictionaries")]
200new_sd_instance!(TWPHRASES_REV_OCD, "TWPhrasesRev.ocd2");
201#[cfg(feature = "static-dictionaries")]
202new_sd_instance!(TWVARIANTS_OCD, "TWVariants.ocd2");
203#[cfg(feature = "static-dictionaries")]
204new_sd_instance!(TWVARIANTS_PHRASES_OCD, "TWVariantsPhrases.ocd2");
205#[cfg(feature = "static-dictionaries")]
206new_sd_instance!(TWVARIANTS_REV_OCD, "TWVariantsRev.ocd2");
207#[cfg(feature = "static-dictionaries")]
208new_sd_instance!(TWVARIANTS_REV_PHRASES_OCD, "TWVariantsRevPhrases.ocd2");
209
210#[derive(Debug, Copy, Clone, Ordinalize)]
212pub enum DefaultConfig {
213 HK2S,
215 HK2SP,
217 HK2T,
219 JP2T,
221 S2HK,
223 S2HKP,
225 S2T,
227 S2TW,
229 S2TWP,
231 T2HK,
233 T2JP,
235 T2S,
237 T2TW,
239 TW2S,
241 TW2SP,
243 TW2T,
245}
246
247impl DefaultConfig {
248 pub fn get_file_name(self) -> &'static str {
250 match self {
251 DefaultConfig::HK2S => "hk2s.json",
252 DefaultConfig::HK2SP => "hk2sp.json",
253 DefaultConfig::HK2T => "hk2t.json",
254 DefaultConfig::JP2T => "jp2t.json",
255 DefaultConfig::S2HK => "s2hk.json",
256 DefaultConfig::S2HKP => "s2hkp.json",
257 DefaultConfig::S2T => "s2t.json",
258 DefaultConfig::S2TW => "s2tw.json",
259 DefaultConfig::S2TWP => "s2twp.json",
260 DefaultConfig::T2HK => "t2hk.json",
261 DefaultConfig::T2JP => "t2jp.json",
262 DefaultConfig::T2S => "t2s.json",
263 DefaultConfig::T2TW => "t2tw.json",
264 DefaultConfig::TW2S => "tw2s.json",
265 DefaultConfig::TW2SP => "tw2sp.json",
266 DefaultConfig::TW2T => "tw2t.json",
267 }
268 }
269}
270
271impl AsRef<Path> for DefaultConfig {
272 fn as_ref(&self) -> &Path {
273 Path::new(self.get_file_name())
274 }
275}
276
277impl AsRef<str> for DefaultConfig {
278 fn as_ref(&self) -> &str {
279 self.get_file_name()
280 }
281}
282
283pub struct OpenCC {
285 opencc: *mut c_void,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
290pub enum OpenCCError {
291 InvalidConfigPath,
293 ConfigPathContainsNul,
295 InputContainsNul,
297 OpenCC(String),
299 InvalidOutput,
301}
302
303impl fmt::Display for OpenCCError {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 match self {
306 OpenCCError::InvalidConfigPath => {
307 f.write_str("The config file path is not valid UTF-8.")
308 },
309 OpenCCError::ConfigPathContainsNul => {
310 f.write_str("The config file path contains a null byte.")
311 },
312 OpenCCError::InputContainsNul => f.write_str("The input string contains a null byte."),
313 OpenCCError::OpenCC(message) => f.write_str(message),
314 OpenCCError::InvalidOutput => {
315 f.write_str("OpenCC returned bytes that are not valid UTF-8.")
316 },
317 }
318 }
319}
320
321impl std::error::Error for OpenCCError {}
322
323static OPENCC_ERROR_LOCK: Mutex<()> = Mutex::new(());
324
325fn opencc_error_lock() -> std::sync::MutexGuard<'static, ()> {
326 OPENCC_ERROR_LOCK.lock().unwrap_or_else(|error| error.into_inner())
327}
328
329fn last_opencc_error() -> OpenCCError {
330 let error = unsafe { opencc_error() };
331
332 if error.is_null() {
333 return OpenCCError::OpenCC("OpenCC returned an unknown error.".to_string());
334 }
335
336 match unsafe { CStr::from_ptr(error) }.to_str() {
337 Ok(message) if !message.is_empty() => OpenCCError::OpenCC(message.to_string()),
338 _ => OpenCCError::OpenCC("OpenCC returned an unreadable error message.".to_string()),
339 }
340}
341
342unsafe impl Send for OpenCC {}
344
345unsafe impl Sync for OpenCC {}
347
348impl OpenCC {
349 pub fn new<P: AsRef<Path>>(config_file_path: P) -> Result<OpenCC, OpenCCError> {
351 let config_file_path =
352 config_file_path.as_ref().as_os_str().to_str().ok_or(OpenCCError::InvalidConfigPath)?;
353 let config_file_path =
354 CString::new(config_file_path).map_err(|_| OpenCCError::ConfigPathContainsNul)?;
355
356 let opencc = {
358 let _lock = opencc_error_lock();
359 let opencc = unsafe { opencc_open(config_file_path.as_ptr()) };
360
361 if opencc.is_null() || opencc as usize == usize::MAX {
362 return Err(last_opencc_error());
363 }
364
365 opencc
366 };
367
368 Ok(OpenCC {
369 opencc,
370 })
371 }
372
373 pub fn convert<S: AsRef<str>>(&self, input: S) -> Result<String, OpenCCError> {
375 let input = input.as_ref();
376
377 let length = input.len();
378 let input = CString::new(input).map_err(|_| OpenCCError::InputContainsNul)?;
379
380 let result_ptr = {
381 let _lock = opencc_error_lock();
382 let result_ptr = unsafe { opencc_convert_utf8(self.opencc, input.as_ptr(), length) };
383
384 if result_ptr.is_null() {
385 return Err(last_opencc_error());
386 }
387
388 result_ptr
389 };
390
391 let result_cstr = unsafe { CStr::from_ptr(result_ptr) };
392 let result =
393 result_cstr.to_str().map(str::to_string).map_err(|_| OpenCCError::InvalidOutput);
394
395 unsafe {
396 opencc_convert_utf8_free(result_ptr);
397 }
398
399 result
400 }
401
402 pub fn convert_to_buffer<S: AsRef<str>>(
404 &self,
405 input: S,
406 mut output: String,
407 ) -> Result<String, OpenCCError> {
408 output.push_str(&self.convert(input)?);
409
410 Ok(output)
411 }
412}
413
414impl Drop for OpenCC {
415 fn drop(&mut self) {
416 if !self.opencc.is_null() {
417 unsafe {
418 opencc_close(self.opencc);
419 }
420 }
421 }
422}
423
424#[cfg(feature = "static-dictionaries")]
425fn generate_static_dictionary_inner<P: AsRef<Path>>(
426 path: P,
427 config: DefaultConfig,
428) -> Result<(), &'static str> {
429 let path = path.as_ref();
430
431 let mut output_data: Vec<&SD> = Vec::new();
432
433 match config {
435 DefaultConfig::HK2S => {
436 output_data.push(&HK2S_JSON);
437 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
438 output_data.push(&TSPHRASES_OCD);
439 output_data.push(&HKVARIANTS_REV_PHRASES_OCD);
440 output_data.push(&HKVARIANTS_REV_OCD);
441 output_data.push(&TSPHRASES_OCD);
442 output_data.push(&TSCHARACTERS_EXT_OCD);
443 output_data.push(&TSCHARACTERS_OCD);
444 },
445 DefaultConfig::HK2SP => {
446 output_data.push(&HK2SP_JSON);
447 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
448 output_data.push(&TSPHRASES_OCD);
449 output_data.push(&HKPHRASES_REV_OCD);
450 output_data.push(&HKVARIANTS_REV_PHRASES_OCD);
451 output_data.push(&HKVARIANTS_REV_OCD);
452 output_data.push(&TSPHRASES_OCD);
453 output_data.push(&TSCHARACTERS_EXT_OCD);
454 output_data.push(&TSCHARACTERS_OCD);
455 },
456 DefaultConfig::HK2T => {
457 output_data.push(&HK2T_JSON);
458 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
459 output_data.push(&HKVARIANTS_REV_PHRASES_OCD);
460 output_data.push(&HKVARIANTS_REV_OCD);
461 },
462 DefaultConfig::JP2T => {
463 output_data.push(&JP2T_JSON);
464 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
465 output_data.push(&JPSHINJITAI_PHRASES_OCD);
466 output_data.push(&JPSHINJITAI_CHARACTERS_OCD);
467 },
468 DefaultConfig::S2HK => {
469 output_data.push(&S2HK_JSON);
470 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
471 output_data.push(&STPHRASES_OCD);
472 output_data.push(&STPHRASES_GENERATED_FROM_REGIONAL_PHRASES_OCD);
473 output_data.push(&STCHARACTERS_OCD);
474 output_data.push(&HKVARIANTS_PHRASES_OCD);
475 output_data.push(&HKVARIANTS_OCD);
476 },
477 DefaultConfig::S2HKP => {
478 output_data.push(&S2HKP_JSON);
479 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
480 output_data.push(&STPHRASES_OCD);
481 output_data.push(&STPHRASES_GENERATED_FROM_REGIONAL_PHRASES_OCD);
482 output_data.push(&STCHARACTERS_OCD);
483 output_data.push(&HKPHRASES_OCD);
484 output_data.push(&HKVARIANTS_PHRASES_OCD);
485 output_data.push(&HKVARIANTS_OCD);
486 },
487 DefaultConfig::S2T => {
488 output_data.push(&S2T_JSON);
489 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
490 output_data.push(&STPHRASES_OCD);
491 output_data.push(&STPHRASES_GENERATED_FROM_REGIONAL_PHRASES_OCD);
492 output_data.push(&STCHARACTERS_OCD);
493 },
494 DefaultConfig::S2TW => {
495 output_data.push(&S2TW_JSON);
496 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
497 output_data.push(&STPHRASES_OCD);
498 output_data.push(&STPHRASES_GENERATED_FROM_REGIONAL_PHRASES_OCD);
499 output_data.push(&STCHARACTERS_OCD);
500 output_data.push(&TWVARIANTS_PHRASES_OCD);
501 output_data.push(&TWVARIANTS_OCD);
502 },
503 DefaultConfig::S2TWP => {
504 output_data.push(&S2TWP_JSON);
505 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
506 output_data.push(&STPHRASES_OCD);
507 output_data.push(&STPHRASES_GENERATED_FROM_REGIONAL_PHRASES_OCD);
508 output_data.push(&STCHARACTERS_OCD);
509 output_data.push(&TWPHRASES_OCD);
510 output_data.push(&TWVARIANTS_PHRASES_OCD);
511 output_data.push(&TWVARIANTS_OCD);
512 },
513 DefaultConfig::T2HK => {
514 output_data.push(&T2HK_JSON);
515 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
516 output_data.push(&HKVARIANTS_PHRASES_OCD);
517 output_data.push(&HKVARIANTS_OCD);
518 },
519 DefaultConfig::T2JP => {
520 output_data.push(&T2JP_JSON);
521 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
522 output_data.push(&JPSHINJITAI_CHARACTERS_REV_OCD);
523 },
524 DefaultConfig::T2S => {
525 output_data.push(&T2S_JSON);
526 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
527 output_data.push(&TSPHRASES_OCD);
528 output_data.push(&TSCHARACTERS_EXT_OCD);
529 output_data.push(&TSCHARACTERS_OCD);
530 },
531 DefaultConfig::T2TW => {
532 output_data.push(&T2TW_JSON);
533 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
534 output_data.push(&TWVARIANTS_PHRASES_OCD);
535 output_data.push(&TWVARIANTS_OCD);
536 },
537 DefaultConfig::TW2S => {
538 output_data.push(&TW2S_JSON);
539 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
540 output_data.push(&TSPHRASES_OCD);
541 output_data.push(&TWVARIANTS_REV_PHRASES_OCD);
542 output_data.push(&TWVARIANTS_REV_OCD);
543 output_data.push(&TSPHRASES_OCD);
544 output_data.push(&TSCHARACTERS_EXT_OCD);
545 output_data.push(&TSCHARACTERS_OCD);
546 },
547 DefaultConfig::TW2SP => {
548 output_data.push(&TW2SP_JSON);
549 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
550 output_data.push(&TSPHRASES_OCD);
551 output_data.push(&TWPHRASES_REV_OCD);
552 output_data.push(&TWVARIANTS_REV_PHRASES_OCD);
553 output_data.push(&TWVARIANTS_REV_OCD);
554 output_data.push(&TSPHRASES_OCD);
555 output_data.push(&TSCHARACTERS_EXT_OCD);
556 output_data.push(&TSCHARACTERS_OCD);
557 },
558 DefaultConfig::TW2T => {
559 output_data.push(&TW2T_JSON);
560 output_data.push(&CJK_COMPATIBILITY_IDEOGRAPHS_OCD);
561 output_data.push(&TWVARIANTS_REV_PHRASES_OCD);
562 output_data.push(&TWVARIANTS_REV_OCD);
563 },
564 }
565
566 for data in output_data {
567 let output_path = path.join(data.0);
568
569 if output_path.exists() {
570 if output_path.is_file() {
571 continue;
572 } else {
573 return Err("The dictionary is not correct.");
574 }
575 }
576
577 let mut file = File::create(output_path).map_err(|_| "Cannot create a new file.")?;
578
579 file.write(data.1).map_err(|_| "Cannot write data to a file.")?;
580
581 file.flush().map_err(|_| "Cannot flush file.")?;
582 }
583
584 Ok(())
585}
586
587#[cfg(feature = "static-dictionaries")]
588pub fn generate_static_dictionary<P: AsRef<Path>>(
590 path: P,
591 config: DefaultConfig,
592) -> Result<(), &'static str> {
593 let path = path.as_ref();
594
595 if path.exists() {
596 if !path.is_dir() {
597 return Err("The path of static dictionaries needs to be a directory.");
598 }
599 } else {
600 match fs::create_dir_all(path) {
601 Ok(_) => (),
602 Err(_) => return Err("Cannot create new directories."),
603 }
604 }
605
606 generate_static_dictionary_inner(path, config)
607}
608
609#[cfg(feature = "static-dictionaries")]
610pub fn generate_static_dictionaries<P: AsRef<Path>>(
612 path: P,
613 configs: &[DefaultConfig],
614) -> Result<(), &'static str> {
615 let path = path.as_ref();
616
617 if path.exists() {
618 if !path.is_dir() {
619 return Err("The path of static dictionaries needs to be a directory.");
620 }
621 } else {
622 match fs::create_dir_all(path) {
623 Ok(_) => (),
624 Err(_) => return Err("Cannot create new directories."),
625 }
626 }
627
628 for config in configs.iter().copied() {
629 generate_static_dictionary_inner(path, config)?
630 }
631
632 Ok(())
633}