diff --git a/crates/pdf/src/cff.rs b/crates/pdf/src/cff.rs index 03493d81d..ce39a732a 100644 --- a/crates/pdf/src/cff.rs +++ b/crates/pdf/src/cff.rs @@ -646,12 +646,21 @@ impl T2<'_> { self.path.move_to((self.x, self.y)); self.open = true; } + /// A segment before the first moveto starts at the current point. + fn ensure_open(&mut self) { + if !self.open { + self.path.move_to((self.x, self.y)); + self.open = true; + } + } fn line_to(&mut self, dx: f64, dy: f64) { + self.ensure_open(); self.x += dx; self.y += dy; self.path.line_to((self.x, self.y)); } fn curve(&mut self, d: [f64; 6]) { + self.ensure_open(); let (x1, y1) = (self.x + d[0], self.y + d[1]); let (x2, y2) = (x1 + d[2], y1 + d[3]); self.x = x2 + d[4]; diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 3db854fc8..39d150749 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -7,6 +7,8 @@ //! children are the document's **layers**: one group per top-level optional-content group //! (Illustrator layers), with content outside them gathered into groups of its own. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + mod build; mod ccitt; mod cff; diff --git a/crates/pdf/src/page.rs b/crates/pdf/src/page.rs index c754c1dd8..94f629335 100644 --- a/crates/pdf/src/page.rs +++ b/crates/pdf/src/page.rs @@ -332,7 +332,7 @@ impl<'a> Interp<'a> { // Painting. "S" => self.paint(false, None, true, depth), "s" => { - self.path.close_path(); + self.close_subpath(); self.paint(false, None, true, depth) } "f" | "F" => self.paint(true, Some(FillRule::NonZero), false, depth), @@ -340,11 +340,11 @@ impl<'a> Interp<'a> { "B" => self.paint(true, Some(FillRule::NonZero), true, depth), "B*" => self.paint(true, Some(FillRule::EvenOdd), true, depth), "b" => { - self.path.close_path(); + self.close_subpath(); self.paint(true, Some(FillRule::NonZero), true, depth) } "b*" => { - self.path.close_path(); + self.close_subpath(); self.paint(true, Some(FillRule::EvenOdd), true, depth) } "n" => self.paint(false, None, false, depth), @@ -549,6 +549,13 @@ impl<'a> Interp<'a> { } } + /// Close the current subpath; nothing to close without a current point. + fn close_subpath(&mut self) { + if self.cur.is_some() { + self.path.close_path(); + } + } + fn ensure_start(&mut self) { if self.cur.is_none() { self.path.move_to((0.0, 0.0)); diff --git a/crates/pdf/src/ps.rs b/crates/pdf/src/ps.rs index 22d2e2314..f0adaab2b 100644 --- a/crates/pdf/src/ps.rs +++ b/crates/pdf/src/ps.rs @@ -278,7 +278,7 @@ impl Src<'_> { None => { // Radix numbers (16#FF). let s = String::from_utf8_lossy(w).into_owned(); - match s.split_once('#').and_then(|(r, v)| i64::from_str_radix(v, r.parse().ok()?).ok()) { + match s.split_once('#').and_then(|(r, v)| i64::from_str_radix(v, r.parse::().ok().filter(|r| (2..=36).contains(r))?).ok()) { Some(n) => V::Num(n as f64), None => V::Name(s.into()), } diff --git a/crates/pdf/src/tests_import.rs b/crates/pdf/src/tests_import.rs index b6b96b80e..8314c7d13 100644 --- a/crates/pdf/src/tests_import.rs +++ b/crates/pdf/src/tests_import.rs @@ -295,3 +295,21 @@ fn truncated_and_corrupt_eps_never_panic() { assert!(r.is_ok(), "{}", String::from_utf8_lossy(prog)); } } + +#[test] +fn closing_an_empty_path_is_a_no_op() { + // "s", "b" and "b*" closed the path without a current point (kurbo asserts on that). + for ops in ["s", "b", "b*", "0 0 m 50 0 l 50 50 l b s b*", "f s 10 10 m 20 20 l s"] { + let doc = parse(&page_pdf(ops, "", vec![])).unwrap(); + let _ = render(&doc); + } +} + +#[test] +fn postscript_radix_numbers_outside_2_to_36_are_not_numbers() { + // `0#7` asked `i64::from_str_radix` for radix 0, which panics (found by fuzzing). + for n in ["0#7", "1#1", "37#1", "99999999999#1", "16#FF"] { + let src = format!("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n{n} pop 0 0 moveto 10 10 lineto stroke\n"); + let _ = parse(src.as_bytes()); + } +}