Merge pull request #16 from storytold/pr/never-crash-pdf

Never crash: PDF/EPS close-path, CFF outlines and PostScript radix numbers
This commit is contained in:
Brandon Thomas
2026-10-04 15:29:03 -04:00
committed by GitHub
5 changed files with 40 additions and 4 deletions
+9
View File
@@ -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];
+2
View File
@@ -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;
+10 -3
View File
@@ -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));
+1 -1
View File
@@ -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::<u32>().ok().filter(|r| (2..=36).contains(r))?).ok()) {
Some(n) => V::Num(n as f64),
None => V::Name(s.into()),
}
+18
View File
@@ -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());
}
}