From db8b9bb04ce92d606a8e14a1a811f4dd04ff465e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 11 Oct 2026 17:01:09 -0700 Subject: [PATCH] EPS: closepath no longer grows the path past its limit moveto, lineto and curveto check that the current path holds fewer than 1,048,576 elements before they add one, and they stop the reading with "a path has too many points" when it is full. closepath added its element without that check. In an encoded user path, a byte of 255 runs the next operator 223 times. vectorcraft-cli info on a 622-byte file with one uappend that runs closepath 11,150,000 times peaked at a memory footprint of 601.9 MiB and reported "the file draws nothing". The string operator makes strings of up to 4 MiB, and one uappend of an operator string that long would add about 468 million elements of 56 bytes, about 24 GiB. closepath now makes the same check before it adds its element, in the closepath operator and in the ClosePath arm of append_user. On the file above, the reading now stops with "a path has too many points", and vectorcraft-cli info peaks at 62.0 MiB. A path below the limit closes as before. Tests: a moveto followed by 1,100,000 closepaths, and a uappend that runs closepath 1,059,250 times, are each read between two squares. Each reading stops with "a path has too many points" and keeps only the first square. --- crates/eps/src/import/graphics.rs | 6 +++++- crates/eps/src/import/tests.rs | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/eps/src/import/graphics.rs b/crates/eps/src/import/graphics.rs index 10c462f6..2a6c5358 100644 --- a/crates/eps/src/import/graphics.rs +++ b/crates/eps/src/import/graphics.rs @@ -434,7 +434,10 @@ impl Interp<'_> { self.g.path.curve_to(xf * a, xf * b, xf * p); self.g.cur = Some(xf * p); } - PathEl::ClosePath => self.g.path.close_path(), + PathEl::ClosePath => { + self.grow()?; + self.g.path.close_path(); + } } } Ok(()) @@ -1528,6 +1531,7 @@ impl Interp<'_> { } ClosePath => { if self.g.cur.is_some() { + self.grow()?; self.g.path.close_path(); self.g.cur = self.g.start; } diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 3c44e1a6..5d6f4674 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -83,6 +83,26 @@ fn paths_fill_and_stroke_in_document_space() { assert!(stroke(&o[2]).is_some()); } +/// Each `closepath` adds an element to the path, so on a path that holds `MAX_PATH` (1,048,576) +/// elements it stops the reading with the same error as `lineto`, run from a procedure or from an +/// encoded user path. +#[test] +fn closepath_stops_at_the_path_limit() { + for body in [ + // A moveto and 1,100,000 closepaths. + "newpath 0 0 moveto 1100000 { closepath } repeat", + // An encoded user path: setbbox and moveto, then 4,750 pairs of a repeat count of 223 (the + // byte 255) and closepath (10), 1,059,250 closepaths. + "/s 9502 string def s 0 0 put s 1 1 put 2 2 9500 { dup s exch 255 put 1 add s exch 10 put } for \ + newpath [ [0 0 10 10 0 0] s ] uappend", + ] { + // The square drawn before the error stays, and the one after it is left out. + let r = read(&format!("0 0 10 10 rectfill {body} 20 20 10 10 rectfill")); + assert_eq!(objects(&r.document).len(), 1, "{body}: {:?}", r.warnings); + assert!(r.warnings[0].contains("a path has too many points"), "{body}: {:?}", r.warnings); + } +} + #[test] fn colours_in_every_model() { let r = read(