mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-07 16:52:50 +00:00
PDF fonts and fax: bound charstring work, ToUnicode ranges and reference search
Type 1 and CFF charstrings bounded subroutine depth but not total work: subroutines that call each other several times multiplied a small font into unbounded instructions and geometry. Each glyph now has a shared budget of 100,000 instructions. The Type 1 Subrs parser looked up /CharStrings again for every entry; it is found once. A ToUnicode bfrange expanded a long destination string for each of up to 65,536 codes; destinations past 256 bytes are skipped and a CMap keeps at most 2^20 mappings. CCITT 2-D decoding scanned the reference row from its start for every code; it bisects.
This commit is contained in:
+24
-2
@@ -401,8 +401,10 @@ fn mode(r: &mut Reader) -> Option<Mode> {
|
||||
/// (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<usize> = (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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+50
-3
@@ -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<u8>)]) -> Vec<u8> {
|
||||
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<u8>)], gsubrs: &[Vec<u8>]) -> Vec<u8> {
|
||||
fn idx(items: &[Vec<u8>]) -> Vec<u8> {
|
||||
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<u8>)]) -> Vec<u8> {
|
||||
let names = idx(&[b"Test".to_vec()]);
|
||||
let strings: Vec<Vec<u8>> = 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::<Vec<_>>());
|
||||
// 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]
|
||||
|
||||
+10
-1
@@ -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);
|
||||
|
||||
+30
-1
@@ -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<Vec<(f64, f64)>>,
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user