From 37aaa76535fe7fe19ad3c16ae82ab1c4aa783e8c Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Mon, 2 Nov 2026 21:30:21 -0500 Subject: [PATCH] font-catalog: hand fontique only bounded font-store files fontique 0.11 imports a collection by registering the whole file once for each face in it, scanning every face again each time and rebuilding the family's font list, so a downloaded collection whose header declares many faces costs far more than its size. The store is now walked here (bounded depth and file count, no symbolic links) and collections that declare more than 64 faces are left to the catalog's own scan, which reads each face once. --- crates/font-catalog/src/system.rs | 79 ++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/crates/font-catalog/src/system.rs b/crates/font-catalog/src/system.rs index 8cac51310..9c72f3f10 100644 --- a/crates/font-catalog/src/system.rs +++ b/crates/font-catalog/src/system.rs @@ -9,7 +9,7 @@ #[cfg(all(feature = "system", not(target_arch = "wasm32")))] mod imp { - use std::path::PathBuf; + use std::path::{Path, PathBuf}; use std::sync::{Mutex, PoisonError}; use fontique::{Collection, CollectionOptions, SourceKind}; @@ -29,13 +29,62 @@ mod imp { std::panic::catch_unwind(|| { let mut collection = Collection::new(CollectionOptions { shared: false, system_fonts: true }); if let Some(store) = font_store_dir(&FontEnv::from_process()).filter(|d| d.is_dir()) { - collection.load_fonts_from_paths([store]); + let files = store_files(&store); + if !files.is_empty() { + collection.load_fonts_from_paths(files); + } } collection }) .ok() } + /// Most faces a font-store collection may have to be handed to fontique. fontique 0.11 + /// registers a collection file once for every face in it, each time scanning all its faces + /// again and rebuilding the family's font list, so the work grows with the fourth power of + /// the face count. Larger collections are left to the catalog's own scan, which reads each + /// face once. + const MAX_COLLECTION_FACES: u32 = 64; + /// Most font-store files handed to fontique, and how deep its folders are walked. + const MAX_STORE_FILES: usize = 20_000; + const MAX_STORE_DEPTH: u32 = 4; + + /// The regular files under the font store `dir` that fontique may import: at most + /// [`MAX_STORE_FILES`], no deeper than [`MAX_STORE_DEPTH`], without symbolic links, and + /// without collections of more than [`MAX_COLLECTION_FACES`] faces. + fn store_files(dir: &Path) -> Vec { + let mut out = Vec::new(); + let mut folders = vec![(dir.to_path_buf(), 0u32)]; + while let Some((folder, depth)) = folders.pop() { + let Ok(rd) = std::fs::read_dir(&folder) else { continue }; + for entry in rd.flatten() { + if out.len() >= MAX_STORE_FILES { + return out; + } + let Ok(kind) = entry.file_type() else { continue }; + let path = entry.path(); + if kind.is_dir() { + if depth < MAX_STORE_DEPTH { + folders.push((path, depth + 1)); + } + } else if kind.is_file() && collection_faces(&path) <= MAX_COLLECTION_FACES { + out.push(path); + } + } + } + out + } + + /// The face count a collection's header declares; 1 for any other file. + fn collection_faces(path: &Path) -> u32 { + use std::io::Read; + let mut head = [0u8; 12]; + match std::fs::File::open(path).and_then(|mut f| f.read_exact(&mut head)) { + Ok(()) if head.starts_with(b"ttcf") => u32::from_be_bytes([head[8], head[9], head[10], head[11]]), + _ => 1, + } + } + fn with(refresh: bool, f: impl FnOnce(&mut Shared) -> R) -> R { let mut guard = SHARED.lock().unwrap_or_else(PoisonError::into_inner); let generation = guard.as_ref().map_or(0, |s| s.generation); @@ -83,6 +132,32 @@ mod imp { files }) } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn store_files_leave_out_large_collections_links_and_deep_folders() { + let dir = std::env::temp_dir().join(format!("craft-font-catalog-store-files-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let family = dir.join("source/family"); + std::fs::create_dir_all(&family).unwrap(); + std::fs::create_dir_all(dir.join("a/b/c/d/e")).unwrap(); + let ttc = |faces: u32| [b"ttcf".as_slice(), &0x0001_0000u32.to_be_bytes(), &faces.to_be_bytes()].concat(); + std::fs::write(family.join("Face.ttf"), b"\0\x01\0\0 a font").unwrap(); + std::fs::write(family.join("Small.ttc"), ttc(4)).unwrap(); + std::fs::write(family.join("Huge.ttc"), ttc(100_000)).unwrap(); + std::fs::write(family.join("tiny"), b"x").unwrap(); + std::fs::write(dir.join("a/b/c/d/e/Deep.ttf"), b"font").unwrap(); + #[cfg(unix)] + std::os::unix::fs::symlink(family.join("Face.ttf"), family.join("Link.ttf")).unwrap(); + let mut files = store_files(&dir); + files.sort(); + assert_eq!(files, vec![family.join("Face.ttf"), family.join("Small.ttc"), family.join("tiny")]); + let _ = std::fs::remove_dir_all(&dir); + } + } } #[cfg(not(all(feature = "system", not(target_arch = "wasm32"))))]