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.
This commit is contained in:
Don Omondi
2026-11-09 02:24:54 +07:00
committed by bflatastic
parent e61f0940d0
commit 70353f003e
4 changed files with 47 additions and 6 deletions
+1 -1
View File
@@ -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)
}
+23
View File
@@ -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"
));
}
+9 -5
View File
@@ -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);
+14
View File
@@ -31,6 +31,20 @@ pub(super) fn font_data(sfnts: &[Obj]) -> Option<Vec<u8>> {
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)> {