From 352086304ead5973c0055f7aa1d2ea904068a614 Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Wed, 28 Oct 2026 09:55:23 -0500 Subject: [PATCH 1/3] F-09: cap font collection face enumeration --- crates/fonts/src/fontdb.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 751f36ca..373f4c32 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -604,12 +604,19 @@ fn fontconfig_dirs(text: &str, data_home: Option<&str>, home: Option<&std::path: /// axis settings. type Found = (u32, String, String, Vec<([u8; 4], f32)>); +/// Maximum number of faces we enumerate from a single font collection. +const MAX_COLLECTION_FACES: u32 = 256; + +fn capped_face_count(count: u32) -> u32 { + count.min(MAX_COLLECTION_FACES) +} + /// Parse every face in `data` (a font file or collection); a variable font yields one face per /// named instance (InDesign lists them as styles). fn enumerate_faces(data: &[u8]) -> Vec { let count = match FileRef::new(data) { Ok(FileRef::Font(_)) => 1, - Ok(FileRef::Collection(c)) => c.len(), + Ok(FileRef::Collection(c)) => capped_face_count(c.len()), Err(_) => 0, }; let mut out = Vec::new(); @@ -1435,6 +1442,12 @@ mod font_dir_tests { use super::*; + #[test] + fn font_collection_face_count_is_capped() { + assert_eq!(capped_face_count(MAX_COLLECTION_FACES), MAX_COLLECTION_FACES); + assert_eq!(capped_face_count(MAX_COLLECTION_FACES + 1), MAX_COLLECTION_FACES); + } + #[test] fn xdg_data_dirs_contribute_their_fonts_folders() { let home = Path::new("/home/ann"); From 6d699b484fd844ca3f33d6d33a23d8b17decea78 Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:37:24 -0500 Subject: [PATCH 2/3] M1.2: bound collection face enumeration --- crates/fonts/src/fontdb.rs | 59 +++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 373f4c32..1b04e0f3 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -606,26 +606,51 @@ type Found = (u32, String, String, Vec<([u8; 4], f32)>); /// Maximum number of faces we enumerate from a single font collection. const MAX_COLLECTION_FACES: u32 = 256; +/// Maximum collection entries inspected while looking for distinct table directories. +const MAX_COLLECTION_ENTRIES_SCANNED: u32 = 16_384; +/// Maximum named instances read from a single face. +const MAX_NAMED_INSTANCES_PER_FACE: usize = 1024; -fn capped_face_count(count: u32) -> u32 { - count.min(MAX_COLLECTION_FACES) +fn distinct_face_indices(entries: impl IntoIterator) -> Vec { + let mut directories = std::collections::HashSet::new(); + let mut indices = Vec::with_capacity(MAX_COLLECTION_FACES as usize); + for (index, directory) in entries.into_iter().take(MAX_COLLECTION_ENTRIES_SCANNED as usize) { + if directories.insert(directory) { + indices.push(index); + if indices.len() == MAX_COLLECTION_FACES as usize { + break; + } + } + } + indices +} + +fn capped_named_instance_count(count: usize) -> usize { + count.min(MAX_NAMED_INSTANCES_PER_FACE) } /// Parse every face in `data` (a font file or collection); a variable font yields one face per /// named instance (InDesign lists them as styles). fn enumerate_faces(data: &[u8]) -> Vec { - let count = match FileRef::new(data) { - Ok(FileRef::Font(_)) => 1, - Ok(FileRef::Collection(c)) => capped_face_count(c.len()), - Err(_) => 0, + let indices = match FileRef::new(data) { + Ok(FileRef::Font(_)) => vec![0], + Ok(FileRef::Collection(c)) => { + let entries = (0..c.len().min(MAX_COLLECTION_ENTRIES_SCANNED)).filter_map(|i| { + let f = skrifa::FontRef::from_index(data, i).ok()?; + Some((i, f.table_directory().table_records().as_ptr() as usize)) + }); + distinct_face_indices(entries) + } + Err(_) => Vec::new(), }; let mut out = Vec::new(); - for i in 0..count { + for i in indices { let Ok(f) = skrifa::FontRef::from_index(data, i) else { continue }; let Some((family, style)) = face_names(&f) else { continue }; let axes = f.axes(); let mut named = Vec::new(); - for ni in f.named_instances().iter() { + let instances = f.named_instances(); + for ni in instances.iter().take(capped_named_instance_count(instances.len())) { let Some(style) = name(&f, &[ni.subfamily_name_id()]) else { continue }; if named.iter().any(|(_, s, _): &(u32, String, _)| s.eq_ignore_ascii_case(&style)) { continue; @@ -1443,9 +1468,21 @@ mod font_dir_tests { use super::*; #[test] - fn font_collection_face_count_is_capped() { - assert_eq!(capped_face_count(MAX_COLLECTION_FACES), MAX_COLLECTION_FACES); - assert_eq!(capped_face_count(MAX_COLLECTION_FACES + 1), MAX_COLLECTION_FACES); + fn duplicate_collection_offsets_do_not_consume_face_cap() { + let mut entries = vec![(0, 7); MAX_COLLECTION_FACES as usize]; + entries.extend((0..MAX_COLLECTION_FACES).map(|i| (i + MAX_COLLECTION_FACES, i as usize + 8))); + let faces = distinct_face_indices(entries); + assert_eq!(faces.len(), MAX_COLLECTION_FACES as usize); + assert_eq!(faces.first(), Some(&0)); + assert_eq!(faces.get(1), Some(&MAX_COLLECTION_FACES)); + assert_eq!(faces.last(), Some(&(MAX_COLLECTION_FACES * 2 - 2))); + } + + #[test] + fn named_instance_limit_is_bounded_per_face() { + assert_eq!(capped_named_instance_count(0), 0); + assert_eq!(capped_named_instance_count(MAX_NAMED_INSTANCES_PER_FACE), MAX_NAMED_INSTANCES_PER_FACE); + assert_eq!(capped_named_instance_count(MAX_NAMED_INSTANCES_PER_FACE + 1), MAX_NAMED_INSTANCES_PER_FACE); } #[test] From cfd30ce4b20d89f09830b7856cbf058163b3280a Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:49:33 -0500 Subject: [PATCH 3/3] M1.2: deduplicate font collection offsets early --- crates/fonts/src/fontdb.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 1b04e0f3..f355e762 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -611,11 +611,11 @@ const MAX_COLLECTION_ENTRIES_SCANNED: u32 = 16_384; /// Maximum named instances read from a single face. const MAX_NAMED_INSTANCES_PER_FACE: usize = 1024; -fn distinct_face_indices(entries: impl IntoIterator) -> Vec { - let mut directories = std::collections::HashSet::new(); +fn distinct_face_indices(entries: impl IntoIterator) -> Vec { + let mut offsets = std::collections::HashSet::new(); let mut indices = Vec::with_capacity(MAX_COLLECTION_FACES as usize); - for (index, directory) in entries.into_iter().take(MAX_COLLECTION_ENTRIES_SCANNED as usize) { - if directories.insert(directory) { + for (index, offset) in entries.into_iter().take(MAX_COLLECTION_ENTRIES_SCANNED as usize) { + if offsets.insert(offset) { indices.push(index); if indices.len() == MAX_COLLECTION_FACES as usize { break; @@ -636,8 +636,10 @@ fn enumerate_faces(data: &[u8]) -> Vec { Ok(FileRef::Font(_)) => vec![0], Ok(FileRef::Collection(c)) => { let entries = (0..c.len().min(MAX_COLLECTION_ENTRIES_SCANNED)).filter_map(|i| { - let f = skrifa::FontRef::from_index(data, i).ok()?; - Some((i, f.table_directory().table_records().as_ptr() as usize)) + let start = 12usize.checked_add((i as usize).checked_mul(4)?)?; + let end = start.checked_add(4)?; + let offset = u32::from_be_bytes(data.get(start..end)?.try_into().ok()?); + Some((i, offset)) }); distinct_face_indices(entries) } @@ -1470,7 +1472,7 @@ mod font_dir_tests { #[test] fn duplicate_collection_offsets_do_not_consume_face_cap() { let mut entries = vec![(0, 7); MAX_COLLECTION_FACES as usize]; - entries.extend((0..MAX_COLLECTION_FACES).map(|i| (i + MAX_COLLECTION_FACES, i as usize + 8))); + entries.extend((0..MAX_COLLECTION_FACES).map(|i| (i + MAX_COLLECTION_FACES, i + 8))); let faces = distinct_face_indices(entries); assert_eq!(faces.len(), MAX_COLLECTION_FACES as usize); assert_eq!(faces.first(), Some(&0));