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