From 7b7e10e82ebbea4607ff5898e672d550d48922a9 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 9 Oct 2026 13:33:48 -0700 Subject: [PATCH] Fonts: a folder search skips suitcase fonts whose resource fork is over 8 MB A folder search reads at most 8 MB of each font file. The suitcase reader reads a suitcase font's resource fork whole, up to 256 MB. Before this change, the search applied its limit to each font only after that read. One search thread could read 256 MB from a single suitcase and hold a copy of its fonts as well. The search now reads a suitcase font only when its resource fork is 8 MB or less. The font scan still reads each suitcase font's resource fork whole, up to 256 MB. Docs: docs/mcp.md. --- crates/text/src/fontdb.rs | 12 ++++++++---- crates/text/src/suitcase.rs | 4 ++-- crates/text/src/tests_sysfonts.rs | 25 +++++++++++++++++++++++++ docs/mcp.md | 2 +- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index 17fc5d58..8c9695b5 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -811,11 +811,15 @@ fn file_face_names(path: &Path) -> Vec { file_face_names_within(path, MAX_FACES, u64::MAX) } -/// [`file_face_names`], reading at most `max_faces` faces and `max_read` bytes of the file in all -/// (of each font in all, for a suitcase font). +/// [`file_face_names`], reading at most `max_faces` faces and `max_read` bytes of the file in all. +/// A suitcase font's resource fork is read whole when it is at most `max_read` bytes long, and the +/// limits then apply to each of its fonts; a longer fork gives none. #[cfg(not(target_arch = "wasm32"))] pub(crate) fn file_face_names_within(path: &Path, max_faces: u32, max_read: u64) -> Vec { if crate::suitcase::is_suitcase(path) { + if crate::suitcase::fork_len(path) > max_read { + return vec![]; + } return crate::suitcase::read_fonts(path) .into_iter() .flat_map(|font| reader_face_names_within(std::io::Cursor::new(font), max_faces, max_read)) @@ -961,8 +965,8 @@ impl WantedFonts { /// font scan reads, the file makes [`FontDb::resolve`] find each of them exactly, in one of its /// faces or a variable font's named instances. The file is read as the scan reads it (its /// table directories and its `name`, `fvar` and `OS/2` tables), at most 64 faces and 8 MB of - /// it, when it is a font file by its extension ([`is_font_file`]) or a suitcase font. Sorted; - /// none on the web. + /// it, when it is a font file by its extension ([`is_font_file`]) or a suitcase font whose + /// resource fork is at most 8 MB long. Sorted; none on the web. pub fn provided_by(&self, path: &Path) -> Vec { #[cfg(not(target_arch = "wasm32"))] { diff --git a/crates/text/src/suitcase.rs b/crates/text/src/suitcase.rs index 0de80f2a..ff06a666 100644 --- a/crates/text/src/suitcase.rs +++ b/crates/text/src/suitcase.rs @@ -78,7 +78,7 @@ fn fork_path(path: &Path) -> std::path::PathBuf { } #[cfg(target_os = "macos")] -fn fork_len(path: &Path) -> u64 { +pub(crate) fn fork_len(path: &Path) -> u64 { std::fs::metadata(fork_path(path)).map_or(0, |m| m.len()) } @@ -91,7 +91,7 @@ fn read_fork(path: &Path) -> Option> { } #[cfg(not(target_os = "macos"))] -fn fork_len(_: &Path) -> u64 { +pub(crate) fn fork_len(_: &Path) -> u64 { 0 } diff --git a/crates/text/src/tests_sysfonts.rs b/crates/text/src/tests_sysfonts.rs index 700cc826..faebd104 100644 --- a/crates/text/src/tests_sysfonts.rs +++ b/crates/text/src/tests_sysfonts.rs @@ -500,6 +500,31 @@ fn a_search_reads_at_most_its_share_of_a_font_file() { assert!(wanted.provided_by(&dir.join("No Such.ttf")).is_empty()); } +/// A search reads a suitcase font only when its whole resource fork fits in the 8 MB a search reads +/// of one file. +#[cfg(target_os = "macos")] +#[test] +fn a_search_reads_a_suitcase_font_only_within_its_share() { + use crate::fontdb::file_face_names_within; + let dir = std::env::temp_dir().join(format!("vc-sysfonts-{}-big-suitcase", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let suitcase = dir.join("Sysfont Sans3"); + std::fs::write(&suitcase, b"").unwrap(); + let fork = crate::suitcase::fork_of(&[renamed("SourceSans3-Regular.ttf")]); + let len = fork.len() as u64; + std::fs::write(suitcase.join("..namedfork/rsrc"), &fork).unwrap(); + assert_eq!(file_face_names_within(&suitcase, 64, len).len(), 1); + assert!(file_face_names_within(&suitcase, 64, len - 1).is_empty()); + let wanted = WantedFonts::new(&[WantedFont { family: FAMILY.into(), style: "Regular".into(), installed: None }]); + assert_eq!(wanted.provided_by(&suitcase), [0]); + // Past the 8 MB a search reads of one file. + let mut big = fork; + big.resize((8 << 20) + 1, 0); + std::fs::write(suitcase.join("..namedfork/rsrc"), big).unwrap(); + assert!(wanted.provided_by(&suitcase).is_empty()); + let _ = std::fs::remove_dir_all(&dir); +} + #[test] fn font_files_are_told_by_their_extension() { for name in ["a/B.TTF", "c.otf", "d.Ttc", "e.otc"] { diff --git a/docs/mcp.md b/docs/mcp.md index bbe1f998..14b0e1e6 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2597,7 +2597,7 @@ and the `name`, `fvar` and `OS/2` tables of `.ttf`, `.otf`, `.ttc` and `.otc` fi the file makes the font resolve exactly as `text.fonts` resolves it. It ends after `maxSeconds` (60 by default, 1 to 600), at its limits on the entries listed (10,000,000), the folders waiting to be listed (1,000,000), the font files read (10,000) and the files kept (1,000), or once every font has a file. Folders more than 64 levels below the picked -one are skipped and counted in `skipped`. +one are skipped and counted in `skipped`. A search does not read a suitcase font whose resource fork is larger than 8 MB. A search does not enter app and media library packages (`.app`, `.bundle`, `.framework`, `.photoslibrary` and the like), folders named `Program Files` (also `Program Files (x86)` and `Program Files (Arm)`), `ProgramData`, `$Recycle.Bin` or