From cb77c398b9e0ef2840ef8f44d7023e386f3c3e0b Mon Sep 17 00:00:00 2001 From: VoiceLessQ Date: Sat, 24 Oct 2026 20:57:37 -0100 Subject: [PATCH] Proofing: a dictionary load from before a folder change is not kept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dictionary is read outside the lock. If the dictionaries folder changed while it was being read (File › Options › Proofing), the reload emptied the slot first and the late load then filled it with the old folder's dictionary. It stayed until the next reload. Every language's copy of the loader had this window; the shared slot now closes it in one place. The slot counts reloads. A load that started before a reload still answers the check that asked for it, but it isn't kept: the next check reads the new folder. Found by CodeRabbit on storytold/wordcraft#4191. Tests: a_load_from_before_a_reload_is_not_kept; not counting reloads fails it. Proof: 288 tests. --- crates/proof/src/slot.rs | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/crates/proof/src/slot.rs b/crates/proof/src/slot.rs index 8b90a94a..6255ae5a 100644 --- a/crates/proof/src/slot.rs +++ b/crates/proof/src/slot.rs @@ -67,6 +67,8 @@ impl Slot for DictSlot { struct State { dict: Option>, in_memory: bool, + /// Counts reloads, so a load that started before one is not kept. + reloads: u64, } /// One language's dictionary. @@ -80,7 +82,7 @@ pub(crate) struct DictSlot { impl DictSlot { pub(crate) const fn new(names: &'static [&'static str], english: &'static str) -> Self { - DictSlot { names, english, state: RwLock::new(State { dict: None, in_memory: false }) } + DictSlot { names, english, state: RwLock::new(State { dict: None, in_memory: false, reloads: 0 }) } } fn read(&self) -> RwLockReadGuard<'_, State> { @@ -105,12 +107,25 @@ impl DictSlot { /// The dictionary: the installed one (read on first use), one given with [`Self::install`], or /// an empty one when none is available (the language's spelling is off then). pub(crate) fn get(&self) -> Arc { - if let Some(d) = self.read().dict.as_ref() { - return d.clone(); + let reloads = { + let s = self.read(); + if let Some(d) = s.dict.as_ref() { + return d.clone(); + } + s.reloads + }; + self.keep(reloads, Arc::new(self.load_installed())) + } + + /// Keep a dictionary loaded after `reloads` reloads. If the folder was reloaded since, the + /// load may have read the old folder: it serves this one check and the next one loads again. + fn keep(&self, reloads: u64, loaded: Arc) -> Arc { + let mut s = self.write(); + if s.reloads != reloads { + return s.dict.clone().unwrap_or(loaded); } - let loaded = Arc::new(self.load_installed()); // Another thread (or an install) may have filled it meanwhile: that one wins. - self.write().dict.get_or_insert(loaded).clone() + s.dict.get_or_insert(loaded).clone() } fn load_installed(&self) -> D { @@ -142,6 +157,7 @@ impl DictSlot { let mut slot = self.write(); slot.dict = None; slot.in_memory = false; + slot.reloads = slot.reloads.wrapping_add(1); } /// The `.dic` file of the installed dictionary, searching the dictionaries folder and the @@ -178,4 +194,17 @@ mod tests { assert!(!SLOT.has_dictionary(), "reloaded from disk, where there is none"); assert_eq!(SLOT.name(), "zz_SLOT_TEST"); } + + /// A load that started before a dictionaries-folder change is not kept after its reload. + #[test] + fn a_load_from_before_a_reload_is_not_kept() { + let slot: DictSlot = DictSlot::new(&["zz_SLOT_RELOAD_TEST"], "Test"); + let before = slot.read().reloads; + slot.reload(); + let _ = slot.keep(before, Arc::new(AffixDict::default())); + assert!(slot.read().dict.is_none(), "the next check loads from the new folder"); + let now = slot.read().reloads; + let _ = slot.keep(now, Arc::new(AffixDict::default())); + assert!(slot.read().dict.is_some(), "a load since the reload is kept"); + } }