diff --git a/crates/eps/src/import/graphics.rs b/crates/eps/src/import/graphics.rs index cc2c2db8..6799c56b 100644 --- a/crates/eps/src/import/graphics.rs +++ b/crates/eps/src/import/graphics.rs @@ -26,6 +26,9 @@ const HAIRLINE: f64 = 0.25; const MAX_INKS: usize = 32; /// Most dashes `strokepath` makes of a path (more and it outlines the path undashed). const MAX_DASHES: f64 = 1e5; +/// Most entries a dash pattern has: the most elements an array has in a typical PostScript +/// implementation (PLRM 3rd ed., Table B.1). +const MAX_DASH_ENTRIES: usize = 65_535; /// Most entries an indexed colour space's table has. const MAX_HIVAL: usize = 4095; /// Most elements the current path may have. @@ -121,7 +124,8 @@ pub(crate) struct GState { pub cap: LineCap, pub join: LineJoin, pub miter: f64, - pub dash: Vec, + /// The dash pattern, shared by the states saved or copied from this one. + pub dash: Rc<[f64]>, pub dash_offset: f64, /// The current path, in document space. pub path: BezPath, @@ -151,7 +155,7 @@ impl Default for GState { cap: LineCap::Butt, join: LineJoin::Miter, miter: 10.0, - dash: vec![], + dash: Rc::from([]), dash_offset: 0.0, path: BezPath::new(), cur: None, @@ -564,16 +568,19 @@ impl Interp<'_> { Ok(()) } - fn stroke_path(&mut self, bp: BezPath) { + fn stroke_path(&mut self, bp: BezPath) -> Res { if bp.elements().is_empty() || self.g.paint.is_none() || self.g.null { - return; + return Ok(()); } - let st = self.stroke_layer(); + let st = self.stroke_layer()?; self.out.stroke(bp, st, &self.g.clips); + Ok(()) } - /// The stroke the graphics state draws, in document space. - fn stroke_layer(&self) -> StrokeLayer { + /// The stroke the graphics state draws, in document space. Each entry of the dash pattern it + /// copies counts as an operation. + fn stroke_layer(&mut self) -> Res { + self.spend(self.g.dash.len() as u64)?; let k = self.scale(); let w = self.g.width * k; let mut st = StrokeLayer::new(self.g.paint.clone(), if w > 0.0 && w.is_finite() { w } else { HAIRLINE }); @@ -584,7 +591,7 @@ impl Interp<'_> { let pattern: Vec = self.g.dash.iter().map(|d| d * k).collect(); let dash = Dash { pattern, offset: self.g.dash_offset * k, align_corners: false }; st.dash = dash.is_dashed().then_some(dash); - st + Ok(st) } /// The current path in user space, each element as its operator and points (quadratic @@ -707,10 +714,7 @@ impl Interp<'_> { } let bp = self.take_path(); match op { - Op::UStroke => { - self.stroke_path(bp); - Ok(()) - } + Op::UStroke => self.stroke_path(bp), _ => self.fill_path(bp, if op == Op::UEoFill { FillRule::EvenOdd } else { FillRule::NonZero }), } }); @@ -1201,7 +1205,7 @@ impl Interp<'_> { ExecForm => self.exec_form()?, StrokePath => { let bp = self.take_path(); - let mut st = self.stroke_layer(); + let mut st = self.stroke_layer()?; // A dash pattern far finer than the path would make countless pieces. let period: f64 = st.dash.as_ref().map_or(0.0, |d| d.pattern.iter().sum()); let periods = if st.dash.is_some() && period > 0.0 { bp.perimeter(1e-3) / period } else { 0.0 }; @@ -1312,11 +1316,18 @@ impl Interp<'_> { SetDash => { let offset = self.pop_num()?; let items = self.pop_array()?; + if items.len() > MAX_DASH_ENTRIES { + return ps_err("limitcheck", ""); + } + // Each entry read counts as an operation. + self.spend(items.len() as u64)?; let v: Option> = items.borrow().iter().map(|o| o.as_num().filter(|v| v.is_finite() && *v >= 0.0)).collect(); - self.g.dash = v.ok_or(PsError::Ps("typecheck", String::new()))?; + self.g.dash = v.ok_or(PsError::Ps("typecheck", String::new()))?.into(); self.g.dash_offset = offset; } CurrentDash => { + // The array made counts against the memory budget. + self.alloc(self.g.dash.len() * std::mem::size_of::())?; let d = self.g.dash.iter().map(|v| Obj::Real(*v)).collect(); self.push(Obj::array(d))?; self.push_num(self.g.dash_offset)?; @@ -1651,7 +1662,7 @@ impl Interp<'_> { } Stroke => { let bp = self.take_path(); - self.stroke_path(bp); + self.stroke_path(bp)?; } RectFill => { let bp = self.rects()?; @@ -1667,7 +1678,7 @@ impl Interp<'_> { let xf = self.xf(); bp.apply_affine(xf * m * xf.inverse()); } - self.stroke_path(bp); + self.stroke_path(bp)?; } ShFill => { let d = self.pop_dict()?; diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 5a3d705d..8408d030 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -171,6 +171,41 @@ fn line_state_reaches_the_stroke() { assert!(stroke(&objects(&read("0 setlinewidth 0 0 moveto 9 9 lineto stroke").document)[0]).unwrap().width > 0.0); } +/// `setdash` takes a dash pattern of up to 65,535 entries, the most elements an array has in a +/// typical implementation (PLRM 3rd ed., Table B.1), and raises `limitcheck` for a longer one. +#[test] +fn dash_patterns_hold_at_most_65535_entries() { + let dash = |n: usize| format!("/a {n} array def 0 1 {} {{ a exch 1 put }} for a 0 setdash", n - 1); + check(&format!("{} currentdash pop length 65535 eq", dash(65535))); + let r = read(&format!("0 0 10 10 rectfill {}", dash(65536))); + assert_eq!(objects(&r.document).len(), 1, "{:?}", r.warnings); + assert!(r.warnings.iter().any(|w| w.contains("limitcheck in `setdash`")), "{:?}", r.warnings); +} + +/// Saved and copied graphics states share the dash pattern, and `setdash`, `currentdash` and +/// each stroke count the entries they read or copy against the budgets. Under a pattern of +/// 65,535 entries, 4,000,000 passes of `gsave grestore` and 2,000,000 passes of +/// `gstate currentgstate setgstate` finish, and loops of `setdash`, `currentdash` and `strokepath` +/// run out of a budget. +#[test] +fn work_on_the_longest_dash_pattern_ends_soon() { + let dash = "0 0 10 10 rectfill /a 65535 array def 0 1 65534 { a exch 1 put } for a 0 setdash"; + for body in ["4000000 { gsave grestore } repeat", "2000000 { gstate currentgstate setgstate } repeat"] { + let r = read_soon(&format!("{dash} {body} 20 20 10 10 rectfill")); + assert_eq!(objects(&r.document).len(), 2, "{body}: {:?}", r.warnings); + assert!(r.warnings.is_empty(), "{body}: {:?}", r.warnings); + } + for (body, why) in [ + ("{ a 0 setdash } loop", "runs too long"), + ("{ currentdash pop pop } loop", "uses too much memory"), + ("{ newpath 0 0 moveto 100 0 lineto strokepath } loop", "runs too long"), + ] { + let r = read_soon(&format!("{dash} {body}")); + assert_eq!(objects(&r.document).len(), 1, "{body}: {:?}", r.warnings); + assert!(r.warnings.iter().any(|w| w.contains(why)), "{body}: {:?}", r.warnings); + } +} + #[test] fn procedures_bind_def_loops_and_dictionaries() { let r = read(