From 70353f003ea0630f39668d4fcb8dc4b2beb7ce2c Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Sat, 7 Nov 2026 12:23:38 -0500 Subject: [PATCH] EPS import: reading sfnts data is charged before the copy, whatever its result Joining a Type 42 font's strings is charged per byte (as other bulk copies are) before it runs, so data that turns out unreadable, read again after leaving the cache, no longer copies for free. --- crates/eps/src/import/interp.rs | 2 +- crates/eps/src/import/tests_composite.rs | 23 +++++++++++++++++++++++ crates/eps/src/import/text.rs | 14 +++++++++----- crates/eps/src/import/type42.rs | 14 ++++++++++++++ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index 5814bf4b2..d3d9c2bdd 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -373,7 +373,7 @@ impl<'a> Interp<'a> { /// Charge for work on `n` bytes or objects in one operator: one operation per 256, as `search` /// charges per 64 bytes, so a loop of big copies runs out of the budget. - fn spend_bytes(&mut self, n: usize) -> Res { + pub fn spend_bytes(&mut self, n: usize) -> Res { self.spend((n / 256) as u64) } diff --git a/crates/eps/src/import/tests_composite.rs b/crates/eps/src/import/tests_composite.rs index f68102e73..7df7cd861 100644 --- a/crates/eps/src/import/tests_composite.rs +++ b/crates/eps/src/import/tests_composite.rs @@ -187,3 +187,26 @@ fn type42_data_read_again_counts_once() { // 17 fonts of 431 kB, each read 200 times: 1.4 GB if each read counted. super::tests::check(&format!("{fonts} /Comp findfont 50 scalefont setfont 200 {{ ({s}) stringwidth pop pop }} repeat true")); } + +/// Reading `sfnts` data that turns out unreadable is charged too: a composite font cycling +/// through more such descendants than are kept read copies their strings on every glyph, and a +/// `loop` of it runs out of the budget. +#[test] +fn unreadable_type42_data_read_again_runs_out_of_the_budget() { + let mut fonts = String::from("/S [ 0 1 7 { pop 4000000 string } for ] def\n"); + for k in 0..17 { + fonts.push_str(&format!( + "10 dict begin /FontName /Bad{k} def /FontType 42 def /FontMatrix [1 0 0 1 0 0] def /FontBBox [0 0 1 1] def /PaintType 0 def\n\ + /Encoding 256 array def 0 1 255 {{ Encoding exch /.notdef put }} for\n\ + /CharStrings 1 dict def CharStrings /.notdef 0 put /sfnts [ S aload pop 0 ] def FontName currentdict end definefont pop\n" + )); + } + let deps: String = (0..17).map(|k| format!(" /Bad{k} findfont")).collect(); + let picks: String = (0..17).map(|k| format!(" {k}")).collect(); + let s: String = (0..17).map(|k| format!("\\{k:03o}\\001")).collect(); + super::tests::runs_out_of_budget(&format!( + "{fonts}16 dict begin /FontName /Comp def /FontType 0 def /FontMatrix [1 0 0 1 0 0] def /FMapType 2 def\n\ + /Encoding [{picks}] def /FDepVector [{deps}] def FontName currentdict end definefont pop\n\ + /Comp findfont 50 scalefont setfont {{ ({s}) stringwidth pop pop }} loop" + )); +} diff --git a/crates/eps/src/import/text.rs b/crates/eps/src/import/text.rs index 4061e2e3d..ef7000e10 100644 --- a/crates/eps/src/import/text.rs +++ b/crates/eps/src/import/text.rs @@ -690,13 +690,17 @@ impl Interp<'_> { data } None => { + // Joining the strings visits each element and copies its bytes, whether or not + // the font can then be read: charged first, as other bulk copies are. + let (elements, bytes) = super::type42::font_size(&sfnts.borrow()); + self.spend_bytes(bytes)?; + self.spend((elements / 16) as u64)?; // Fonts whose data can't be read are kept too, so they are copied only once. let data = super::type42::font_data(&sfnts.borrow()).map(Rc::new); - if let Some(d) = &data { - if self.type42_counted.insert(sfnts.clone()) { - self.alloc(d.len())?; - } - self.spend((d.len() / 4096) as u64)?; + if let Some(d) = &data + && self.type42_counted.insert(sfnts.clone()) + { + self.alloc(d.len())?; } if self.type42.len() >= MAX_TYPE42 { self.type42.remove(0); diff --git a/crates/eps/src/import/type42.rs b/crates/eps/src/import/type42.rs index 41c234cc2..1aac00b00 100644 --- a/crates/eps/src/import/type42.rs +++ b/crates/eps/src/import/type42.rs @@ -31,6 +31,20 @@ pub(super) fn font_data(sfnts: &[Obj]) -> Option> { Some(data) } +/// What [`font_data`] visits for `sfnts`: the elements up to the first that isn't a string, and +/// the bytes of those strings (at most [`MAX_FONT`] and one string more). +pub(super) fn font_size(sfnts: &[Obj]) -> (usize, usize) { + let mut bytes = 0usize; + for (i, s) in sfnts.iter().enumerate() { + let Obj::Str(s) = s else { return (i + 1, bytes) }; + bytes = bytes.saturating_add(s.borrow().len()); + if bytes > MAX_FONT { + return (i + 1, bytes.min(2 * MAX_FONT)); + } + } + (sfnts.len(), bytes) +} + /// Glyph `gid` of TrueType font `data`: its outline and advance width, in ems (y up), or `None` /// when the font can't be read. pub(super) fn glyph(data: &[u8], gid: u32) -> Option<(BezPath, f64)> {