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.
This commit is contained in:
dotCooCoo 2026-10-09 13:33:48 -07:00 committed by bflatastic
parent 5709e636a5
commit 7b7e10e82e
4 changed files with 36 additions and 7 deletions

View File

@ -811,11 +811,15 @@ fn file_face_names(path: &Path) -> Vec<FaceStyle> {
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<FaceStyle> {
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<usize> {
#[cfg(not(target_arch = "wasm32"))]
{

View File

@ -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<Vec<u8>> {
}
#[cfg(not(target_os = "macos"))]
fn fork_len(_: &Path) -> u64 {
pub(crate) fn fork_len(_: &Path) -> u64 {
0
}

View File

@ -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"] {

View File

@ -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