From 6708ff55b467cb4ae6e9e454d8ae425b5ed9029d Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 4 Oct 2026 12:14:58 -0400 Subject: [PATCH] Never crash: SVG path data no longer loops forever on a number after Z Path data like "M0 0 L10 0 Z9" repeated Z implicitly for the stray number; Z consumes no numbers, so the parser looped forever appending close-paths until memory ran out (a 1-byte change to a valid SVG used 24 GB and was killed). A number after Z is a syntax error, so parsing now stops there and keeps what was read, as the spec asks. Also denies the panic lints in the crate (it had no panic sites). Co-Authored-By: Claude Opus 5.5 --- clippy.toml | 4 ++++ crates/svg/src/lib.rs | 2 ++ crates/svg/src/pathdata.rs | 14 ++++++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 clippy.toml diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..4b6c79f5 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,4 @@ +allow-unwrap-in-tests = true +allow-expect-in-tests = true +allow-panic-in-tests = true +allow-indexing-slicing-in-tests = true diff --git a/crates/svg/src/lib.rs b/crates/svg/src/lib.rs index 2825cdfa..06ae671a 100644 --- a/crates/svg/src/lib.rs +++ b/crates/svg/src/lib.rs @@ -4,6 +4,8 @@ //! ([`Doc`]): groups with transforms and opacity, and shapes with fills and strokes in their own //! user space. [`rasterize`] draws it at any scale; the engine converts it to shape layers. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + mod color; mod pathdata; mod raster; diff --git a/crates/svg/src/pathdata.rs b/crates/svg/src/pathdata.rs index 842efb45..c8fa7f67 100644 --- a/crates/svg/src/pathdata.rs +++ b/crates/svg/src/pathdata.rs @@ -109,8 +109,9 @@ pub fn parse_path_data(d: &str) -> BezPath { let c = match l.command() { Some(c) => c, None => match cmd { - // Implicit repeat (a moveto repeats as lineto). - Some(c) if l.at_number() => match c { + // Implicit repeat (a moveto repeats as lineto). Z takes no numbers, so a number + // after it is an error: repeating Z would loop forever without consuming input. + Some(c) if l.at_number() && !c.eq_ignore_ascii_case(&b'z') => match c { b'M' => b'L', b'm' => b'l', other => other, @@ -284,6 +285,15 @@ mod tests { assert!(parse_path_data("").elements().is_empty()); } + #[test] + fn number_after_close_stops_instead_of_looping() { + // "Z9": Z was repeated implicitly without consuming the number, appending close-paths + // until memory ran out. + let p = parse_path_data("M0 0 L10 0 L10 10 Z9 C1 2 3 4 5 6"); + assert_eq!(p.elements().len(), 4); + assert!(parse_path_data("M0 0 L10 0 z 9 9").elements().len() <= 4); + } + #[test] fn number_lists() { assert_eq!(numbers("0 0,100 50"), vec![0.0, 0.0, 100.0, 50.0]);