From 9ae43a4c4ee3636ad5bcf818da448ea70c6bde06 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 31 Oct 2026 03:02:39 +0100 Subject: [PATCH] Reject empty arguments in parse_css color functions Found while testing crates/color/src/lib.rs:247 (css_args, used by the rgb/rgba and hsl/hsla arms of parse_css at :159 and :178) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/color/src/lib.rs | 20 +++++++++++++++----- crates/color/src/tests.rs | 9 +++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/color/src/lib.rs b/crates/color/src/lib.rs index c2c0d06e..190fdbb9 100644 --- a/crates/color/src/lib.rs +++ b/crates/color/src/lib.rs @@ -157,7 +157,7 @@ impl Color { return Self::from_hex(&t); } if let Some(inner) = t.strip_prefix("rgba(").or_else(|| t.strip_prefix("rgb(")).and_then(|r| r.strip_suffix(')')) { - let parts = css_args(inner); + let parts = css_args(inner)?; let num = |i: usize| -> Option { let p = parts.get(i)?; if let Some(pc) = p.strip_suffix('%') { @@ -176,7 +176,7 @@ impl Color { return Some(Self::new(num(0)?, num(1)?, num(2)?, alpha)); } if let Some(inner) = t.strip_prefix("hsla(").or_else(|| t.strip_prefix("hsl(")).and_then(|r| r.strip_suffix(')')) { - let parts = css_args(inner); + let parts = css_args(inner)?; let h = parts.first()?.trim_end_matches("deg").parse::().ok()?; let s = parts.get(1)?.trim_end_matches('%').parse::().ok()? / 100.0; let l = parts.get(2)?.trim_end_matches('%').parse::().ok()? / 100.0; @@ -244,14 +244,24 @@ impl Color { } } -fn css_args(inner: &str) -> Vec { - inner.replace('/', ",").split([',', ' ']).map(str::trim).filter(|p| !p.is_empty()).map(str::to_string).collect() +/// Split CSS function arguments on `,` and `/` (each may be surrounded by spaces) and on runs of +/// whitespace. An empty argument between two separators makes the whole list invalid. +fn css_args(inner: &str) -> Option> { + let mut out = Vec::new(); + for seg in inner.replace('/', ",").split(',') { + let before = out.len(); + out.extend(seg.split_whitespace().map(str::to_string)); + if out.len() == before { + return None; + } + } + Some(out) } /// The arguments of `lch()` / `oklch()`: lightness, chroma, hue and alpha. Percentages are of /// `l_full` / `c_full`; `none` is 0. fn css_polar(inner: &str, l_full: f32, c_full: f32) -> Option<([f32; 3], f32)> { - let parts = css_args(inner); + let parts = css_args(inner)?; if !(3..=4).contains(&parts.len()) { return None; } diff --git a/crates/color/src/tests.rs b/crates/color/src/tests.rs index 03e4d167..a41d0dd2 100644 --- a/crates/color/src/tests.rs +++ b/crates/color/src/tests.rs @@ -148,3 +148,12 @@ fn css_hsl_rejects_non_finite() { assert_eq!(Color::parse_css(s), None, "{s}"); } } + +#[test] +fn css_rejects_empty_arguments() { + for s in ["rgb(255,,0,0)", "rgb(,1,2,3)", "rgb(1,2,3,)", "hsl(10,,50%,50%)", "rgb(1,2,,)", "oklch(0.5,,0.1,20)"] { + assert_eq!(Color::parse_css(s), None, "{s}"); + } + assert!(Color::parse_css("rgb(255 0 0 / 50%)").is_some()); + assert!(Color::parse_css("hsl(120deg 50% 50%)").is_some()); +}