diff --git a/crates/fonts/src/fallback.rs b/crates/fonts/src/fallback.rs index 8d54453f8..168fa443d 100644 --- a/crates/fonts/src/fallback.rs +++ b/crates/fonts/src/fallback.rs @@ -169,8 +169,14 @@ impl FontDb { if bundled.contains(fam) { continue; } - if self.is_loaded(fam) || self.has_family(fam) { - let f = self.face(fam, style); + if self.is_loaded(fam) || (self.system_fallback_enabled() && self.has_family(fam)) { + // With system fallback off, only a face that is already loaded: `face` could load more catalog files. + let f = if self.system_fallback_enabled() { + self.face(fam, style) + } else { + let Some(f) = self.find(fam, style) else { continue }; + f + }; if f.family.eq_ignore_ascii_case(fam) && covers_cluster(&f, cluster) { return f; } @@ -311,4 +317,21 @@ mod tests { assert!(c.windows(2).all(|w| w[0] <= w[1])); } } + + /// With system fallback off, the automatic cascades (script families and symbol faces) load no + /// installed family: the result comes from what is already loaded. + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn cascades_load_no_installed_family_when_system_fallback_is_off() { + let db = FontDb::with_font_dirs(Vec::new()); + db.set_system_fallback(false); + let base = db.face(crate::DEFAULT_FAMILY, "Regular"); + let loaded = |db: &FontDb| [b"Arab", b"Hebr", b"Thai", b"Deva"].into_iter().flat_map(|s| os_families(*s)).filter(|f| db.is_loaded(f)).count(); + let before = loaded(&db); + for c in ['\u{0628}', '\u{05D0}', '\u{0E01}', '\u{0905}', '\u{2211}', '\u{2603}'] { + let f = db.cascade(&[c], std::slice::from_ref(&base), "Regular"); + assert!(!f.family.is_empty()); + } + assert_eq!(loaded(&db), before, "no installed family was loaded"); + } } diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 19f190445..e542c8d18 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -976,6 +976,17 @@ impl FontDb { let _ = on; } + /// Whether automatic system-font lookups (fallback by character, symbol and script families) + /// may touch installed fonts. Lookups of a family the document or host names are not gated. + pub(crate) fn system_fallback_enabled(&self) -> bool { + #[cfg(not(target_arch = "wasm32"))] + { + self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).enabled + } + #[cfg(target_arch = "wasm32")] + false + } + /// Process-wide database: the bundled fonts, then the installed system fonts, cataloged the /// first time a lookup by family name needs them (or ahead of time by /// [`FontDb::scan_in_background`]). @@ -1287,7 +1298,7 @@ impl FontDb { Some(f) } - fn find(&self, family: &str, style: &str) -> Option> { + pub(crate) fn find(&self, family: &str, style: &str) -> Option> { if style.contains('{') { { let faces = self.read_faces(); @@ -1473,7 +1484,8 @@ impl FontDb { }; #[cfg(not(target_arch = "wasm32"))] { - let cataloged: Vec = self.read_catalog().iter().map(|e| e.family.clone()).collect(); + let cataloged: Vec = + if self.system_fallback_enabled() { self.read_catalog().iter().map(|e| e.family.clone()).collect() } else { Vec::new() }; for fam in SYSTEM_FALLBACKS.iter().filter(|f| f.contains("Symbol")) { if !self.is_loaded(fam) && cataloged.iter().any(|f| f.eq_ignore_ascii_case(fam)) { self.load_cataloged(fam); diff --git a/crates/fonts/src/tests.rs b/crates/fonts/src/tests.rs index 596415d51..e7a3f064d 100644 --- a/crates/fonts/src/tests.rs +++ b/crates/fonts/src/tests.rs @@ -425,3 +425,11 @@ fn the_font_scan_skips_fifos_folders_and_link_loops() { assert!(fifo || n >= 1); let _ = std::fs::remove_dir_all(&dir); } + +#[test] +fn disabling_system_fallback_gates_automatic_family_loads() { + let db = FontDb::with_font_dirs(Vec::new()); + assert!(cfg!(target_arch = "wasm32") || db.system_fallback_enabled()); + db.set_system_fallback(false); + assert!(!db.system_fallback_enabled()); +}