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)> {