diff --git a/crates/pdf/src/ccitt.rs b/crates/pdf/src/ccitt.rs index fa013161f..ca6a40d21 100644 --- a/crates/pdf/src/ccitt.rs +++ b/crates/pdf/src/ccitt.rs @@ -401,8 +401,10 @@ fn mode(r: &mut Reader) -> Option { /// (index parity: even indices start black runs), and the one after it. fn b1b2(refl: &[usize], a0: i64, white: bool, columns: usize) -> (usize, usize) { let want = if white { 0 } else { 1 }; - let mut i = 0; - while i < refl.len() && (refl[i] as i64 <= a0 || i % 2 != want) { + // The changing elements are in order, so the first past `a0` is found by bisection (a scan + // from the start made every row quadratic in its transitions). + let mut i = refl.partition_point(|&c| c as i64 <= a0); + if i % 2 != want { i += 1; } let b1 = refl.get(i).copied().unwrap_or(columns); @@ -804,4 +806,24 @@ pub(crate) mod tests { let _ = decode(&garbage, &Params { k, columns: 1, rows: 0, ..Params::default() }, 1 << 20); } } + + /// `b1b2` by bisection agrees with the scan it replaced, for every position and colour + /// over a dense reference row. + #[test] + fn b1b2_by_bisection_matches_the_scan() { + let scan = |refl: &[usize], a0: i64, white: bool, columns: usize| { + let want = if white { 0 } else { 1 }; + let mut i = 0; + while i < refl.len() && (refl[i] as i64 <= a0 || i % 2 != want) { + i += 1; + } + (refl.get(i).copied().unwrap_or(columns), refl.get(i + 1).copied().unwrap_or(columns)) + }; + let refl: Vec = (0..64).map(|i| i * 3 / 2).collect(); + for a0 in -1..100 { + for white in [true, false] { + assert_eq!(b1b2(&refl, a0, white, 100), scan(&refl, a0, white, 100), "a0 {a0} white {white}"); + } + } + } } diff --git a/crates/pdf/src/cff.rs b/crates/pdf/src/cff.rs index 61bb7f9eb..ef35c8c96 100644 --- a/crates/pdf/src/cff.rs +++ b/crates/pdf/src/cff.rs @@ -545,8 +545,21 @@ impl Cff { let r = self.charstrings.get(gid as usize)?.clone(); let fd = self.fdselect.get(gid as usize).copied().unwrap_or(0) as usize; let fd = fd.min(self.lsubrs.len() - 1); - let mut st = - T2 { cff: self, fd, path: BezPath::new(), x: 0.0, y: 0.0, stack: vec![], stems: 0, width: None, open: false, done: false, depth: 0, seac: None }; + let mut st = T2 { + cff: self, + fd, + path: BezPath::new(), + x: 0.0, + y: 0.0, + stack: vec![], + stems: 0, + width: None, + open: false, + done: false, + depth: 0, + ops: MAX_GLYPH_OPS, + seac: None, + }; st.run(&self.data[r]); if st.open { st.path.close_path(); @@ -646,9 +659,16 @@ struct T2<'a> { open: bool, done: bool, depth: usize, + /// Instructions this glyph may still run, across its subroutine calls. + ops: usize, seac: Option<(f64, f64, u8, u8)>, } +/// The most charstring instructions one glyph runs: subroutines that branch (two calls each, +/// ten levels deep) would otherwise multiply a small font into unbounded work and geometry. +/// A real glyph runs a few hundred. +pub(crate) const MAX_GLYPH_OPS: usize = 100_000; + impl T2<'_> { fn move_to(&mut self, dx: f64, dy: f64) { if self.open { @@ -698,6 +718,11 @@ impl T2<'_> { } let mut i = 0; while i < cs.len() && !self.done { + let Some(left) = self.ops.checked_sub(1) else { + self.done = true; + return; + }; + self.ops = left; let b0 = cs[i]; i += 1; match b0 { @@ -958,6 +983,12 @@ impl T2<'_> { /// named by custom strings. #[cfg(test)] pub(crate) fn write_test_cff(glyphs: &[(&str, Vec)]) -> Vec { + write_test_cff_with_gsubrs(glyphs, &[]) +} + +/// [`write_test_cff`] with global subroutines. +#[cfg(test)] +pub(crate) fn write_test_cff_with_gsubrs(glyphs: &[(&str, Vec)], gsubrs: &[Vec]) -> Vec { fn idx(items: &[Vec]) -> Vec { let mut out = (items.len() as u16).to_be_bytes().to_vec(); if items.is_empty() { @@ -983,7 +1014,7 @@ pub(crate) fn write_test_cff(glyphs: &[(&str, Vec)]) -> Vec { let names = idx(&[b"Test".to_vec()]); let strings: Vec> = glyphs.iter().skip(1).map(|(n, _)| n.as_bytes().to_vec()).collect(); let strings_idx = idx(&strings); - let gsubrs = idx(&[]); + let gsubrs = idx(gsubrs); let cs = idx(&glyphs.iter().map(|(_, c)| c.clone()).collect::>()); // charset format 0: SIDs of glyphs 1.. (custom strings from 391). let mut charset = vec![0u8]; @@ -1090,6 +1121,22 @@ mod tests { assert!(p.elements().is_empty()); } + /// A global subroutine that calls itself ten times: 10^10 calls within the depth limit of + /// ten. The glyph stops at its instruction budget instead. + #[test] + fn branching_subroutines_stop_at_the_instruction_budget() { + // `-107 callgsubr` (bias 107) is subroutine 0. + let call = [32u8, 29]; + let mut subr = call.repeat(10); + subr.push(11); + let mut cs = call.to_vec(); + cs.push(14); + let font = write_test_cff_with_gsubrs(&[(".notdef", vec![14]), ("A", cs)], &[subr]); + let cff = Cff::parse(&font).unwrap(); + let (p, _) = cff.outline(cff.gid_by_name("A").unwrap()).unwrap(); + assert!(p.elements().is_empty()); + } + /// A CFF whose top DICT points its Private DICT at offsets near `usize::MAX`: `size` and `off` /// come from the file, and reading the Private DICT must not overflow `off + size`. #[test] diff --git a/crates/pdf/src/font.rs b/crates/pdf/src/font.rs index 64b0a1c3a..db973f505 100644 --- a/crates/pdf/src/font.rs +++ b/crates/pdf/src/font.rs @@ -211,6 +211,12 @@ fn utf16(b: &[u8]) -> String { String::from_utf16_lossy(&u) } +/// The most code → text mappings a CMap keeps (more than Unicode has code points), and the +/// longest destination string a `bfrange` is expanded with: a range of 65,536 codes would +/// otherwise multiply a long destination into gigabytes of text. +const MAX_UNICODE_MAPPINGS: usize = 1 << 20; +const MAX_RANGE_DST: usize = 256; + fn parse_cmap(data: &[u8]) -> CMap { let mut m = CMap::default(); let mut lx = Lexer::new(data, 0); @@ -252,9 +258,12 @@ fn parse_cmap(data: &[u8]) -> CMap { continue; } match &p[2] { - Obj::Str(dst) => { + Obj::Str(dst) if dst.len() <= MAX_RANGE_DST => { let mut d = dst.clone(); for c in lo..=hi { + if m.unicode.len() >= MAX_UNICODE_MAPPINGS { + break; + } m.unicode.insert(c, utf16(&d)); if let Some(last) = d.last_mut() { *last = last.wrapping_add(1); diff --git a/crates/pdf/src/type1.rs b/crates/pdf/src/type1.rs index 4b3e05756..edb42e6a2 100644 --- a/crates/pdf/src/type1.rs +++ b/crates/pdf/src/type1.rs @@ -148,8 +148,9 @@ impl Type1 { }; if let Some(p) = find(&priv_, b"/Subrs", 0) { let mut q = p + 6; + // The section ends at `/CharStrings`, found once rather than for every entry. + let lim = find(&priv_, b"/CharStrings", q).unwrap_or(priv_.len()); while let Some(d) = find(&priv_, b"dup", q) { - let lim = find(&priv_, b"/CharStrings", q).unwrap_or(priv_.len()); if d > lim { break; } @@ -223,6 +224,7 @@ impl Type1 { flex: None, done: false, depth: 0, + ops: crate::cff::MAX_GLYPH_OPS, seac: None, }; st.run(cs); @@ -270,6 +272,8 @@ struct T1<'a> { flex: Option>, done: bool, depth: usize, + /// Instructions this glyph may still run, across its subroutine calls. + ops: usize, seac: Option<(f64, f64, f64, u8, u8)>, } @@ -314,6 +318,12 @@ impl T1<'_> { } let mut i = 0; while i < cs.len() && !self.done { + // Subroutines that branch would multiply the work: the budget is shared. + let Some(left) = self.ops.checked_sub(1) else { + self.done = true; + return; + }; + self.ops = left; let b0 = cs[i]; i += 1; let s = |st: &Self, k: usize| st.stack.get(k).copied().unwrap_or(0.0); @@ -533,6 +543,25 @@ mod tests { assert_eq!(p.bounding_box(), kurbo::Rect::new(50.0, 0.0, 550.0, 500.0)); } + /// A subroutine that calls itself ten times: 10^10 calls within the depth limit of ten. The + /// glyph stops at its instruction budget instead. + #[test] + fn branching_subroutines_stop_at_the_instruction_budget() { + let mut out = b"%!PS-AdobeFont-1.0: Test 001\n/FontMatrix [0.001 0 0 0.001 0 0] readonly def\ncurrentfile eexec\n".to_vec(); + // `0 callsubr` is `139 10`; `return` is 11, `endchar` 14 (lenIV -1: charstrings in clear). + let mut subr = [139u8, 10].repeat(10); + subr.push(11); + let mut priv_ = b"dup /Private 8 dict dup begin /lenIV -1 def\n/Subrs 1 array\n".to_vec(); + priv_.extend_from_slice(format!("dup 0 {} RD ", subr.len()).as_bytes()); + priv_.extend_from_slice(&subr); + priv_.extend_from_slice(b" NP\nend\n/CharStrings 1 dict dup begin\n/A 3 RD \x8b\x0a\x0e ND\nend\nend\nmark currentfile closefile\n"); + out.extend(encrypt(&priv_, EEXEC_R, 4)); + out.extend_from_slice(b"\n0000000000000000\ncleartomark\n"); + let t = Type1::parse(&out).unwrap(); + let (p, _) = t.outline("A").unwrap(); + assert!(p.elements().is_empty()); + } + /// A `/Subrs` entry whose `RD` length is `usize::MAX`: the bytes are not in the file, and /// `q + n` must not overflow. #[test]