From 80e439f6dc6d47c8eb9b646d93c0bc11f5ae5a12 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:45:25 +0000 Subject: [PATCH] Reject signed hex digits in #rrggbb colour parsing Found while testing crates/slideshow/src/settings.rs:296-300 (parse_color); same pattern in crates/engine/src/slideshow.rs:117-122 (fadeColor) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/color/src/lib.rs | 13 +++++++++++-- crates/engine/src/slideshow.rs | 4 +++- crates/slideshow/src/settings.rs | 5 ++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/color/src/lib.rs b/crates/color/src/lib.rs index 855c3d049..89fe93c9f 100644 --- a/crates/color/src/lib.rs +++ b/crates/color/src/lib.rs @@ -180,8 +180,8 @@ pub struct Rgb8(pub u8, pub u8, pub u8); impl Rgb8 { pub fn from_hex(s: &str) -> Option { - let s = s.trim_start_matches('#'); - if s.len() != 6 { + let s = s.strip_prefix('#').unwrap_or(s); + if s.len() != 6 || !s.bytes().all(|b| b.is_ascii_hexdigit()) { return None; } let v = u32::from_str_radix(s, 16).ok()?; @@ -196,6 +196,15 @@ impl Rgb8 { mod tests { use super::*; + #[test] + fn rgb8_from_hex_rejects_signs_and_extra_hashes() { + assert_eq!(Rgb8::from_hex("#0f0f0f"), Some(Rgb8(15, 15, 15))); + assert_eq!(Rgb8::from_hex("ff8000"), Some(Rgb8(255, 128, 0))); + for bad in ["+12345", "#+12345", "#+f+f+f", "-12345", "##123456", "#12345", ""] { + assert_eq!(Rgb8::from_hex(bad), None, "{bad}"); + } + } + #[test] fn srgb_matrix_matches_standard() { let m = SRGB.to_xyz(); diff --git a/crates/engine/src/slideshow.rs b/crates/engine/src/slideshow.rs index 8f615bf0c..dbee505af 100644 --- a/crates/engine/src/slideshow.rs +++ b/crates/engine/src/slideshow.rs @@ -115,7 +115,7 @@ fn color(v: &Value) -> std::result::Result, String> { Value::String(s) if s.is_empty() || s.eq_ignore_ascii_case("none") => Ok(None), Value::String(s) => { let h = s.strip_prefix('#').unwrap_or(s); - if h.len() != 6 || !h.is_ascii() { + if h.len() != 6 || !h.bytes().all(|b| b.is_ascii_hexdigit()) { return Err(bad()); } let byte = |i: usize| h.get(i..i + 2).and_then(|x| u8::from_str_radix(x, 16).ok()).ok_or_else(bad); @@ -747,6 +747,8 @@ mod tests { json!({"repeat": 1}), json!({"fadeColor": "#12345"}), json!({"fadeColor": "#ééé"}), + json!({"fadeColor": "#+f+f+f"}), + json!({"fadeColor": "-12345"}), json!({"fadeColor": [300, 0, 0]}), json!({"fadeColor": [1, 2]}), json!({"speed": 2}), diff --git a/crates/slideshow/src/settings.rs b/crates/slideshow/src/settings.rs index 6fb004bf6..fd3b327ef 100644 --- a/crates/slideshow/src/settings.rs +++ b/crates/slideshow/src/settings.rs @@ -293,7 +293,7 @@ pub fn parse_color(v: &Value) -> Option<[u8; 3]> { match v { Value::String(s) => { let h = s.trim().strip_prefix('#').unwrap_or(s.trim()); - if h.len() != 6 || !h.is_ascii() { + if h.len() != 6 || !h.bytes().all(|b| b.is_ascii_hexdigit()) { return None; } let byte = |i: usize| h.get(i..i + 2).and_then(|x| u8::from_str_radix(x, 16).ok()); @@ -356,6 +356,9 @@ mod tests { json!({"zoomToFill": 1}), json!({"strokeColor": "#12345"}), json!({"strokeColor": "#ééééé"}), + json!({"strokeColor": "#+f+f+f"}), + json!({"backdropColor": "+12345"}), + json!({"washColor": "#-00000"}), json!({"washColor": [256, 0, 0]}), json!({"washColor": [-1, 0, 0]}), json!({"backdropColor": [1, 2]}),