mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
EPS: huge arcs, fine dashes and rectangle arrays in a loop no longer hang the import
The operation budget counted each operator once, however long a path it made or read. arc and arct with a huge radius make up to about a million curve segments, strokepath makes a piece for each dash, and rectfill makes five path elements for each rectangle in an array. pathbbox, clip, clippath, shfill, pathforall, gstate, currentgstate and setgstate read or copy the whole current path or clip path, and in an encoded user path a byte of the operator string can repeat the next operator up to 223 times. In a loop, each pass counted a few operations. vectorcraft-cli info was still reading each of 15 files that run one of these in "loop" when "timeout 300" stopped it, after 28 to 128 seconds of user CPU time. With a radius of 50 instead of 2.9e30, the loop of arc ran out of the budget after 6.9 seconds of user CPU time. Each of these operators now counts the path elements it makes, reads or copies as one operation each. Making or reading an element took up to about as long as an operation in a loop of pop, and copying one took less. strokepath also counts each entry of the dash pattern in each period of the path. An encoded user path counts each entry of its data array, each byte of its operator string and each operator it runs. The 15 loops now end with "the program runs too long" after 0.06 to 2.35 seconds of user CPU time, and the loop of arc with a radius of 50 ends after 1.9 seconds of user CPU time. Tests: a loop of each of these operators runs out of the operation budget. The loops cover arc and arct with a huge radius, strokepath with a fine dash and with dashes of length zero, rectfill with 2,000 rectangles, the eight operators that read or copy a path of 10,001 elements, and uappend on three encoded user paths. The tests read each program on a thread of its own and fail when no result arrives within 20 seconds.
This commit is contained in:
1 parent
4a27d897ed
commit
d0bb09c16f
2 files changed
+116
-10
No files matched your search
@@ -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<Item = PathEl>) -> 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<f64> = 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<Vec<f64>> = 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<f64> = 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<Rect> {
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user