Proofing: say when a dictionary is installed but unreadable, with file and reason (storytold/wordcraft#2830) (storytold/wordcraft#2900)

* Proofing: say when a dictionary is installed but unreadable, with file and reason (storytold/wordcraft#2830)

* proof test: use SHIFT_JIS as the unsupported encoding (ISO 8859-1 is decoded since storytold/wordcraft#2904)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
MarekSpodBiedryandBrandon Thomas authored and GitHub committed 2026-10-18 11:51:13 -04:00
1 parent 0b12f06ea1
commit 401ea058d1
8 files changed
+94 -7

No files matched your search

+2 -1
View File
@@ -36,11 +36,12 @@ pub fn dictionary() -> Arc<AffixDict> {
fn load_installed() -> AffixDict {
let Some((aff, dic)) = crate::hunspell::find(NAMES) else {
crate::hunspell::clear_load_error(NAMES[0]);
log::info!("Croatian spelling: no Hunspell hr_HR dictionary installed");
return AffixDict::default();
};
crate::hunspell::load(&aff, &dic).unwrap_or_else(|e| {
crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| {
log::warn!("Croatian spelling: {e}");
AffixDict::default()
})
+44
View File
@@ -20,10 +20,36 @@ const MAX_FILE: u64 = 64 << 20;
/// An extra directory searched first (a setting), set by the front end.
static EXTRA_DIR: RwLock<Option<PathBuf>> = RwLock::new(None);
/// Why an installed dictionary failed to load, by `.dic` name (`pt_BR`): the file and the reason.
/// Without it a file that is there but unreadable looks the same as no file (#2830).
static LOAD_ERRORS: RwLock<Vec<(String, PathBuf, String)>> = RwLock::new(Vec::new());
/// Remember (or, for `Ok`, forget) why the dictionary `key` found at `dic` failed to load, and
/// hand the result back unchanged.
pub fn note_load<T>(key: &str, dic: &Path, result: Result<T, String>) -> Result<T, String> {
let mut errors = LOAD_ERRORS.write().unwrap_or_else(|e| e.into_inner());
errors.retain(|(k, _, _)| k != key);
if let Err(e) = &result {
errors.push((key.to_string(), dic.to_path_buf(), e.clone()));
}
result
}
/// Forget the load error of the dictionary `key` (none found, or it is about to be searched for again).
pub fn clear_load_error(key: &str) {
LOAD_ERRORS.write().unwrap_or_else(|e| e.into_inner()).retain(|(k, _, _)| k != key);
}
/// The `.dic` file and the reason of the last failed load of the dictionary `key`, if any.
pub fn load_error(key: &str) -> Option<(PathBuf, String)> {
LOAD_ERRORS.read().unwrap_or_else(|e| e.into_inner()).iter().find(|(k, _, _)| k == key).map(|(_, p, e)| (p.clone(), e.clone()))
}
/// Search `dir` before the system locations (a "dictionaries folder" setting); `None` clears it.
/// Takes effect for dictionaries not loaded yet.
pub fn set_dictionary_dir(dir: Option<PathBuf>) {
*EXTRA_DIR.write().unwrap_or_else(|e| e.into_inner()) = dir;
LOAD_ERRORS.write().unwrap_or_else(|e| e.into_inner()).clear(); // they belong to the old search path
}
/// The LibreOffice extension directories a Hunspell dictionary name may live in: LibreOffice's
@@ -191,6 +217,24 @@ mod tests {
assert!(decode(b"SET UTF-8\n", b"\xff\xfe").is_err());
}
/// #2830: a dictionary that is there but can't be read leaves its reason behind; a good
/// load or no file clears it.
#[test]
fn load_error_is_remembered_until_the_next_good_load() {
let key = "test_unreadable_2830";
assert_eq!(load_error(key), None);
let dic = Path::new("/dicts/test.dic");
assert!(note_load(key, dic, decode(b"SET SHIFT_JIS\n", b"")).is_err());
let (file, reason) = load_error(key).expect("error kept");
assert_eq!(file, dic);
assert_eq!(reason, "unsupported encoding SHIFT_JIS");
assert!(note_load(key, dic, decode(b"SET UTF-8\n", b"")).is_ok());
assert_eq!(load_error(key), None);
let _ = note_load::<()>(key, dic, Err("too large".into()));
clear_load_error(key);
assert_eq!(load_error(key), None);
}
#[test]
fn decodes_latin1() {
// `orçamento esperávamos` in ISO 8859-1: ç = 0xE7, á = 0xE1.
+2 -1
View File
@@ -41,10 +41,11 @@ pub fn dictionary() -> Arc<AffixDict> {
fn load_installed() -> AffixDict {
let Some((aff, dic)) = crate::hunspell::find(NAMES) else {
crate::hunspell::clear_load_error(NAMES[0]);
log::info!("Indonesian spelling: no Hunspell id_ID dictionary installed");
return AffixDict::default();
};
crate::hunspell::load(&aff, &dic).unwrap_or_else(|e| {
crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| {
log::warn!("Indonesian spelling: {e}");
AffixDict::default()
})
+2 -1
View File
@@ -45,10 +45,11 @@ pub fn dictionary() -> Arc<AffixDict> {
fn load_installed() -> AffixDict {
let Some((aff, dic)) = crate::hunspell::find(NAMES) else {
crate::hunspell::clear_load_error(NAMES[0]);
log::info!("Italian spelling: no Hunspell it_IT dictionary installed");
return AffixDict::default();
};
crate::hunspell::load(&aff, &dic).unwrap_or_else(|e| {
crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| {
log::warn!("Italian spelling: {e}");
AffixDict::default()
})
+33
View File
@@ -167,6 +167,22 @@ pub fn installed_dictionaries() -> Vec<(ProofLang, Option<std::path::PathBuf>)>
]
}
/// Why the dictionary found for `lang` failed to load (unsupported encoding, too large, a parse
/// error), if it was read and failed. Doesn't read anything itself: a dictionary nobody has
/// checked yet reports `None` (#2830).
pub fn dictionary_load_error(lang: ProofLang) -> Option<String> {
let key = match lang {
ProofLang::Pl => "pl_PL",
ProofLang::Pt => "pt_BR",
ProofLang::It => "it_IT",
ProofLang::Hr => "hr_HR",
ProofLang::Tr => "tr_TR",
ProofLang::Id => "id_ID",
_ => return None,
};
hunspell::load_error(key).map(|(_, reason)| reason)
}
/// A number that changes whenever a word is added to or removed from the user dictionary, or the
/// dictionaries folder changes.
pub fn dictionary_generation() -> u64 {
@@ -384,6 +400,23 @@ pub fn missing_dictionary(lang: Option<&str>) -> Option<&'static str> {
}
}
/// The Hunspell name, `.dic` file and reason of a dictionary that is installed for this language
/// but couldn't be read (unsupported `SET` encoding, too large, a parse error). `None` when it
/// loaded, or when no file exists (then [`missing_dictionary`] names it). Reads the dictionary
/// once (it is cached afterwards).
pub fn unreadable_dictionary(lang: Option<&str>) -> Option<(&'static str, std::path::PathBuf, String)> {
let (proof_lang, loaded) = match ProofLang::from_tag(lang) {
ProofLang::Pl => (ProofLang::Pl, pl::has_dictionary()),
ProofLang::Pt => (ProofLang::Pt, pt::has_dictionary()),
_ => return None,
};
let name = dictionary_needed(proof_lang, false)?;
if loaded {
return None;
}
hunspell::load_error(name).map(|(file, reason)| (name, file, reason))
}
/// The Hunspell name `lang` needs when `installed` is false; only Polish and Portuguese use one.
fn dictionary_needed(lang: ProofLang, installed: bool) -> Option<&'static str> {
match lang {
+2 -1
View File
@@ -44,10 +44,11 @@ pub fn dictionary() -> Arc<AffixDict> {
fn load_installed() -> AffixDict {
let Some((aff, dic)) = crate::hunspell::find(NAMES) else {
crate::hunspell::clear_load_error(NAMES[0]);
log::info!("Polish spelling: no Hunspell pl_PL dictionary installed");
return AffixDict::default();
};
crate::hunspell::load(&aff, &dic).unwrap_or_else(|e| {
crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| {
log::warn!("Polish spelling: {e}");
AffixDict::default()
})
+2 -1
View File
@@ -43,10 +43,11 @@ pub fn dictionary() -> Arc<AffixDict> {
fn load_installed() -> AffixDict {
let Some((aff, dic)) = crate::hunspell::find(NAMES) else {
crate::hunspell::clear_load_error(NAMES[0]);
log::info!("Portuguese spelling: no Hunspell pt_BR dictionary installed");
return AffixDict::default();
};
crate::hunspell::load(&aff, &dic).unwrap_or_else(|e| {
crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| {
log::warn!("Portuguese spelling: {e}");
AffixDict::default()
})
+7 -2
View File
@@ -40,16 +40,21 @@ pub fn dictionary() -> Arc<TrDict> {
fn load_installed() -> TrDict {
let Some((aff, dic)) = crate::hunspell::find(NAMES) else {
crate::hunspell::clear_load_error(NAMES[0]);
log::info!("Turkish spelling: no Hunspell tr_TR dictionary installed");
return TrDict::default();
};
let read = |p: &std::path::Path| std::fs::read_to_string(p);
match (read(&aff), read(&dic)) {
(Ok(aff_text), Ok(dic_text)) => TrDict::from_hunspell(&aff_text, &dic_text).unwrap_or_else(|e| {
(Ok(aff_text), Ok(dic_text)) => crate::hunspell::note_load(NAMES[0], &dic, TrDict::from_hunspell(&aff_text, &dic_text)).unwrap_or_else(|e| {
log::warn!("Turkish spelling: {e}");
TrDict::default()
}),
_ => TrDict::default(),
(a, d) => {
let reason = a.err().or(d.err()).map_or_else(String::new, |e| e.to_string());
let _ = crate::hunspell::note_load::<()>(NAMES[0], &dic, Err(reason));
TrDict::default()
}
}
}