From 98ef01a6d397afddf5f401b3c203058aeea2f3cc Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 3 Nov 2026 15:02:20 -0800 Subject: [PATCH] Type: bound the glyphs a contour-point caret lookup reads A composite glyph whose components expand into glyphs with no points could make the contour-point caret lookup read billions of glyphs. The whole lookup now shares a budget of 65,536 glyph reads and gives no caret when it runs out, so the caret falls back to dividing the advance. --- crates/text/src/fontdb/face.rs | 15 +++-- crates/text/src/tests/ligature_carets.rs | 73 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/crates/text/src/fontdb/face.rs b/crates/text/src/fontdb/face.rs index ba11c4ae9..3ec57c9ef 100644 --- a/crates/text/src/fontdb/face.rs +++ b/crates/text/src/fontdb/face.rs @@ -3,9 +3,14 @@ use super::names::{localized, name}; use super::traits::{FaceKeys, FontTraits}; use super::{Arc, GlyphId, HashMap, Location, LocationRef, MetadataProvider, Size, StringId}; +/// The most glyphs one [`outline_points`] lookup reads, composite components included. +const MAX_OUTLINE_VISITS: usize = 65_536; + /// Unhinted TrueType contour points in glyph order, including transformed composite components. -/// A cap and depth bound keep damaged fonts from making caret lookup unbounded. -fn outline_points(font: &skrifa::FontRef<'_>, gid: GlyphId, depth: usize) -> Option> { +/// A point cap, a depth bound and `visits`, the number of glyphs the whole lookup may still read, +/// keep damaged fonts from making caret lookup unbounded. Callers start `visits` at +/// [`MAX_OUTLINE_VISITS`]. +fn outline_points(font: &skrifa::FontRef<'_>, gid: GlyphId, depth: usize, visits: &mut usize) -> Option> { use skrifa::raw::TableProvider; use skrifa::raw::tables::glyf::{Anchor, CompositeGlyphFlags, Glyph}; @@ -14,6 +19,7 @@ fn outline_points(font: &skrifa::FontRef<'_>, gid: GlyphId, depth: usize) -> Opt if depth >= MAX_DEPTH { return None; } + *visits = visits.checked_sub(1)?; let glyph = font.loca(None).ok()?.get(gid, &font.glyf().ok()?)?.into_glyph()?; match glyph { Glyph::Simple(simple) => { @@ -26,7 +32,7 @@ fn outline_points(font: &skrifa::FontRef<'_>, gid: GlyphId, depth: usize) -> Opt Glyph::Composite(composite) => { let mut points = Vec::new(); for component in composite.components() { - let child = outline_points(font, GlyphId::new(component.glyph.to_u32()), depth + 1)?; + let child = outline_points(font, GlyphId::new(component.glyph.to_u32()), depth + 1, visits)?; let t = component.transform; let xx = f64::from(t.xx.to_f32()); let xy = f64::from(t.xy.to_f32()); @@ -341,7 +347,8 @@ impl FontFace { if self.location.coords().iter().any(|c| c.to_f32() != 0.0) { return None; } - let point = outline_points(&font, GlyphId::new(gid), 0)?.get(usize::from(v.caret_value_point_index())).copied()?; + let mut visits = MAX_OUTLINE_VISITS; + let point = outline_points(&font, GlyphId::new(gid), 0, &mut visits)?.get(usize::from(v.caret_value_point_index())).copied()?; Some(if vertical { point.1 } else { point.0 }) } }) diff --git a/crates/text/src/tests/ligature_carets.rs b/crates/text/src/tests/ligature_carets.rs index 801713a35..508bfae1c 100644 --- a/crates/text/src/tests/ligature_carets.rs +++ b/crates/text/src/tests/ligature_carets.rs @@ -62,6 +62,79 @@ fn a_contour_point_caret_uses_its_unhinted_x_and_y() { assert_eq!(bad.face(LIGATURE_CARET_FAMILY, "Regular").unwrap().ligature_carets(gid, 1), None); } +/// [`contour_caret_font`] with its `glyf` and `loca` replaced: the ligature is a composite of +/// eight copies of a glyph that is itself eight copies of the next, 15 levels down to a glyph with +/// no contours. Every other glyph is empty. +fn composite_fan_caret_font() -> Vec { + use skrifa::raw::TableProvider; + use write_fonts::FontBuilder; + use write_fonts::types::Tag; + + const FAN: usize = 8; + const LEVELS: usize = 15; + let base = contour_caret_font(0); + let font = skrifa::FontRef::new(&base).expect("test font parses"); + let ligature = source_sans_gid(LIGATURE_CARET_GLYPH).expect("Source Sans 3 has the glyph"); + let num_glyphs = font.maxp().expect("maxp reads").num_glyphs(); + let short_loca = font.head().expect("head reads").index_to_loc_format() == 0; + let levels: Vec = (1..num_glyphs).filter(|g| *g != ligature).take(LEVELS).collect(); + let composite = |child: u16| { + let mut data = vec![0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0]; + for i in 0..FAN { + // ARGS_ARE_XY_VALUES, plus MORE_COMPONENTS on all but the last; byte offsets of 0. + let flags: u16 = if i + 1 < FAN { 0x0022 } else { 0x0002 }; + data.extend(flags.to_be_bytes()); + data.extend(child.to_be_bytes()); + data.extend([0, 0]); + } + data + }; + let mut glyf = Vec::new(); + let mut offsets = vec![0usize]; + for gid in 0..num_glyphs { + if gid == ligature { + glyf.extend(composite(levels[0])); + } else if let Some(level) = levels.iter().position(|g| *g == gid) { + match levels.get(level + 1) { + Some(child) => glyf.extend(composite(*child)), + // No contours, a zero bounding box and no instructions. + None => glyf.extend([0u8; 12]), + } + } + offsets.push(glyf.len()); + } + let loca: Vec = if short_loca { + offsets.iter().flat_map(|o| u16::try_from(o / 2).expect("short offset").to_be_bytes()).collect() + } else { + offsets.iter().flat_map(|o| u32::try_from(*o).expect("long offset").to_be_bytes()).collect() + }; + let mut builder = FontBuilder::new(); + builder.add_raw(Tag::new(b"glyf"), glyf); + builder.add_raw(Tag::new(b"loca"), loca); + for record in font.table_directory.table_records() { + let tag = Tag::new(&record.tag().to_be_bytes()); + if !builder.contains(tag) { + builder.add_raw(tag, font.table_data(record.tag()).expect("base table reads").as_bytes()); + } + } + builder.build() +} + +/// A contour-point caret on a composite that expands to billions of glyphs with no points gives +/// no caret, and quickly, rather than reading every one of them. +#[test] +fn a_contour_point_caret_on_a_huge_empty_composite_gives_up() { + let gid = u32::from(source_sans_gid(LIGATURE_CARET_GLYPH).expect("Source Sans 3 has the glyph")); + let (send, receive) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let db = db_with(Some(composite_fan_caret_font())); + let carets = db.face(LIGATURE_CARET_FAMILY, "Regular").map(|face| face.ligature_carets(gid, 1)); + let _ = send.send(carets); + }); + let carets = receive.recv_timeout(std::time::Duration::from_secs(30)).expect("the caret lookup finishes"); + assert_eq!(carets, Some(None)); +} + fn point(text: &str, family: &str) -> TextObject { TextObject::point(Point::ZERO, text, CharStyle { font_family: family.into(), size: 100.0, ..CharStyle::default() }) }