diff --git a/crates/eps/src/import/graphics.rs b/crates/eps/src/import/graphics.rs index 79143dd9..cc2c2db8 100644 --- a/crates/eps/src/import/graphics.rs +++ b/crates/eps/src/import/graphics.rs @@ -402,6 +402,13 @@ impl Interp<'_> { Ok(()) } + /// Count `n` path elements an operator makes, reads or copies against the operation budget, + /// an operation each: making or reading an element takes up to about as long as an operation, + /// and copying one takes less (measured). + fn spend_elements(&mut self, n: usize) -> Res { + self.spend(n as u64) + } + fn move_to(&mut self, p: Point) -> Res { self.grow()?; // Consecutive movetos: the last one counts. @@ -424,10 +431,12 @@ impl Interp<'_> { Ok(()) } - /// Path elements in user space appended (the first move a line when there is a current point). + /// Path elements in user space appended (the first move a line when there is a current point), + /// each counted against the operation budget. fn append_user(&mut self, els: impl Iterator) -> Res { let xf = self.xf(); for el in els { + self.spend_elements(1)?; match el { PathEl::MoveTo(p) => { let p = xf * p; @@ -516,6 +525,8 @@ impl Interp<'_> { let nums: Vec = match self.stack.last() { Some(Obj::Array { .. }) => { let items = self.pop_array()?; + // Five elements a rectangle, counted before the array is read. + self.spend_elements(items.len() / 4 * 5)?; let v: Option> = items.borrow().iter().map(Obj::as_num).collect(); v.ok_or(PsError::Ps("typecheck", String::new()))? } @@ -606,6 +617,7 @@ impl Interp<'_> { let curve = self.pop_proc()?; let line = self.pop_proc()?; let mv = self.pop_proc()?; + self.spend_elements(self.g.path.elements().len())?; for (op, pts) in self.user_elements()? { for p in pts { self.push_num(p.x)?; @@ -649,7 +661,12 @@ impl Interp<'_> { (Op::UCache, 0), ]; let nums: Vec = match nums { - Obj::Array { items, .. } => items.borrow().iter().filter_map(Obj::as_num).collect(), + Obj::Array { items, .. } => { + // Each entry of the data array and each byte of the operator string counts as an + // element read. + self.spend_elements(items.len().saturating_add(ops.len()))?; + items.borrow().iter().filter_map(Obj::as_num).collect() + } _ => return ps_err("typecheck", "an encoded user path"), }; let mut next = nums.into_iter(); @@ -665,6 +682,8 @@ impl Interp<'_> { let v = next.next().ok_or(PsError::Ps("rangecheck", "a user path".into()))?; self.push_num(v)?; } + // Each operator run counts as an operation. + self.spend(1)?; self.op(op)?; } } @@ -752,9 +771,13 @@ impl Interp<'_> { } } - /// The area the clip leaves, in document space (the page without a clip). - fn clip_bounds(&self) -> Rect { - self.g.clips.iter().rev().find_map(|c| c.region.as_ref()).map_or(self.out.frame, |(p, _)| p.bounding_box()) + /// The area the clip leaves, in document space (the page without a clip). The clip path read + /// counts against the operation budget. + fn clip_bounds(&mut self) -> Res { + let (bounds, len) = + self.g.clips.iter().rev().find_map(|c| c.region.as_ref()).map_or((self.out.frame, 0), |(p, _)| (p.bounding_box(), p.elements().len())); + self.spend_elements(len)?; + Ok(bounds) } // ---------- colour ---------- @@ -1181,11 +1204,16 @@ impl Interp<'_> { 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()); - if st.dash.is_some() && !(period > 0.0 && bp.perimeter(1e-3) / period < MAX_DASHES) { + let periods = if st.dash.is_some() && period > 0.0 { bp.perimeter(1e-3) / period } else { 0.0 }; + if st.dash.is_some() && !(period > 0.0 && periods < MAX_DASHES) { st.dash = None; } let tol = (0.01 / self.scale().max(1e-9)).clamp(1e-4, 1.0); let outline = vectorcraft_effects::stroke::line_outline(&bp, &st, st.width, tol); + // The path, each entry of the dash pattern in each period, and the outline count + // as elements. + let dashes = st.dash.as_ref().map_or(0.0, |d| periods * d.pattern.len() as f64) as usize; + self.spend_elements(bp.elements().len().saturating_add(dashes).saturating_add(outline.elements().len()))?; if outline.elements().len() >= MAX_PATH { return Err(PsError::Limit("a path has too many points")); } @@ -1245,13 +1273,20 @@ impl Interp<'_> { self.push(Obj::Int(0))?; self.push(Obj::Int(0))?; } - GStateNew => self.push(Obj::GState(Rc::new(self.g.clone())))?, + GStateNew => { + self.spend_elements(self.g.path.elements().len())?; + self.push(Obj::GState(Rc::new(self.g.clone())))?; + } CurrentGState => { self.pop()?; + self.spend_elements(self.g.path.elements().len())?; self.push(Obj::GState(Rc::new(self.g.clone())))?; } SetGState => match self.pop()? { - Obj::GState(g) => self.g = (*g).clone(), + Obj::GState(g) => { + self.spend_elements(g.path.elements().len())?; + self.g = (*g).clone(); + } _ => return ps_err("typecheck", ""), }, SetLineWidth => self.g.width = self.pop_num()?.abs(), @@ -1573,13 +1608,14 @@ impl Interp<'_> { if xf.determinant().abs() < 1e-12 { return ps_err("undefinedresult", ""); } + self.spend_elements(self.g.path.elements().len())?; let b = xf.inverse().transform_rect_bbox(self.g.path.bounding_box()); for v in [b.x0, b.y0, b.x1, b.y1] { self.push_num(v)?; } } ClipPath => { - let b = self.clip_bounds(); + let b = self.clip_bounds()?; self.take_path(); self.g.path = b.to_path(0.1); self.g.cur = Some(b.origin()); @@ -1600,6 +1636,7 @@ impl Interp<'_> { } } Clip | EoClip => { + self.spend_elements(self.g.path.elements().len())?; let bp = self.g.path.clone(); self.clip_with(bp, if op == EoClip { FillRule::EvenOdd } else { FillRule::NonZero }); } @@ -1643,7 +1680,7 @@ impl Interp<'_> { } let m = self.xf(); if let Some(g) = self.gradient(&d, m)? { - let area = self.clip_bounds(); + let area = self.clip_bounds()?; let paint = Paint::Gradient(Box::new(g)); let clips = self.g.clips.clone(); self.out.fill(area.to_path(0.1), FillRule::NonZero, paint, self.g.overprint, &clips); diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 68d99db1..5a3d705d 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -669,6 +669,75 @@ fn read_soon(body: &str) -> crate::Imported { rx.recv_timeout(std::time::Duration::from_secs(20)).unwrap_or_else(|_| panic!("reading {body} did not finish")).unwrap() } +/// Reads `body` after a square through [`read_soon`]: the square stays, and the program runs out +/// of the operation budget. +fn runs_out_of_budget(body: &str) { + let r = read_soon(&format!("0 0 10 10 rectfill {body}")); + assert_eq!(objects(&r.document).len(), 1, "{body}: {:?}", r.warnings); + assert!(r.warnings.iter().any(|w| w.contains("the program runs too long")), "{body}: {:?}", r.warnings); +} + +/// A loop of operators whose work grows with the path they make runs out of the operation budget +/// (PLRM 3rd ed., chapter 8): `arc` and `arct` with a huge radius, which make thousands of curve +/// segments, `strokepath` with a dash far finer than the line, dashes of length zero included, and +/// `rectfill` with an array of 2,000 rectangles. +#[test] +fn paths_made_in_a_loop_run_out_of_the_budget() { + for body in [ + "{ newpath 0 0 2.2e18 0 1440 arc } loop", + "{ newpath 0 0 moveto 1 0 1 1 1e30 arct } loop", + "[1] 0 setdash { newpath 0 0 moveto 9900 0 lineto strokepath } loop", + "[0 1] 0 setdash { newpath 0 0 moveto 9900 0 lineto strokepath } loop", + "/a [ 0 1 1999 { pop 0 0 1 1 } for ] def nulldevice { a rectfill } loop", + ] { + runs_out_of_budget(body); + } +} + +/// A loop of operators that read or copy the whole current path or clip path runs out of the +/// operation budget (PLRM 3rd ed., chapter 8): `pathbbox`, `clip`, `clippath`, `shfill` (which +/// reads the clip path for the area it paints), `pathforall` (which copies the path before it runs +/// a procedure), `gstate`, `currentgstate` and `setgstate`. +#[test] +fn paths_read_or_copied_in_a_loop_run_out_of_the_budget() { + // 10,001 elements reaching 10,000,000 points to the right: what `shfill` paints in a clip of + // them lies far outside the page and is left out. + let path = "newpath 0 0 moveto 0 1 9999 { pop 1000 0 rlineto } for"; + let sh = "/sh << /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 10 0] \ + /Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> >> def"; + for body in [ + "{ pathbbox pop pop pop pop } loop", + "{ clip } loop", + "clip newpath { clippath } loop", + "clip newpath { sh shfill } loop", + "{ { pop pop exit } { } { } { } pathforall } loop", + "{ gstate pop } loop", + "/g gstate def { g currentgstate pop } loop", + "/g gstate def { g setgstate } loop", + ] { + runs_out_of_budget(&format!("{sh} {path} {body}")); + } +} + +/// A loop of `uappend` on an encoded user path runs out of the operation budget (PLRM 3rd ed., +/// section 4.6.2 and Table 4.3: each byte of the operator string is an operator, or a count of up +/// to 223 repeats of the next one): one that runs `ucache` millions of times, one of repeat counts +/// alone, and one with a long data array. +#[test] +fn encoded_user_paths_in_a_loop_run_out_of_the_budget() { + for body in [ + // 100,000 pairs of a count of 223 repeats (byte 255) and `ucache` (byte 11): each + // `uappend` runs `ucache` 22,300,000 times. + "/s 200000 string def 0 2 199998 { dup s exch 255 put 1 add s exch 11 put } for /up [ [ ] s ] def", + // 200,000 repeat counts and no operator. + "/s 200000 string def 0 1 199999 { s exch 255 put } for /up [ [ ] s ] def", + // A data array of 400,000 nulls and an empty operator string. + "/up [ 400000 array () ] def", + ] { + runs_out_of_budget(&format!("{body} {{ up uappend }} loop")); + } +} + /// `arc` raises its second angle by multiples of 360 until it is at least the first, and `arcn` /// lowers it until it is at most the first (PLRM 3rd ed., `arc` and `arcn` in chapter 8), however /// large the angles: the arc runs between the angles' remainders after whole turns and ends at