From e027cd112a83af3f1d28589468a23fdf22dbb66d Mon Sep 17 00:00:00 2001 From: Bart Waardenburg Date: Thu, 22 Oct 2026 23:19:03 +0200 Subject: [PATCH] color: Normalize a non-finite hue to 0 degrees. A hue that overflows f32, such as hsl(1e40 50% 50%), made normalize_hue return NaN. The NaN hue failed the debug assertion in hsl_to_rgb and gave wrong channels in a release build. CSS Color 4 says that an infinite hue normalizes to 0 degrees, and CSS Values 4 censors NaN to 0. --- color/lib.rs | 7 +++++++ color/tests.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/color/lib.rs b/color/lib.rs index 4d5890e0..abc3e1a4 100644 --- a/color/lib.rs +++ b/color/lib.rs @@ -489,6 +489,13 @@ impl ToCss for ModernComponent<'_> { // Guaratees hue in [0..360) fn normalize_hue(hue: f32) -> f32 { + // An infinite hue normalizes to 0 degrees: + // + // NaN is censored to 0: + // + if !hue.is_finite() { + return 0.0; + } // // Subtract an integer before rounding, to avoid some rounding errors: hue - 360.0 * (hue / 360.0).floor() diff --git a/color/tests.rs b/color/tests.rs index fbbb553d..87a53ce0 100644 --- a/color/tests.rs +++ b/color/tests.rs @@ -394,3 +394,49 @@ fn serialize_modern_components() { "calc(NaN)".to_string() ); } + +fn parse_and_serialize(css: &str) -> String { + Color::parse(&mut Parser::new(css)).unwrap().to_css_string() +} + +#[test] +fn normalize_non_finite_hue() { + // : an infinite hue + // normalizes to 0 degrees. 1e40 does not fit in an f32, so it parses + // as an infinity. + for (input, expected) in [ + ("hsl(1e40 50% 50%)", "hsl(0 50% 50%)"), + ("hsl(-1e40 50% 50%)", "hsl(0 50% 50%)"), + ("hsl(1e40deg 50% 50%)", "hsl(0 50% 50%)"), + ("hsl(1e38turn 50% 50%)", "hsl(0 50% 50%)"), + ("hwb(1e40 0% 0%)", "hwb(0 0% 0%)"), + ("hwb(-1e40 0% 0%)", "hwb(0 0% 0%)"), + ("lch(50% 30 1e40)", "lch(50% 30 0)"), + ("oklch(50% 0.1 -1e40)", "oklch(50% 0.1 0)"), + ] { + assert_eq!( + parse_and_serialize(input), + parse_and_serialize(expected), + "{input}" + ); + } + assert_eq!(parse_and_serialize("hwb(1e40 0% 0%)"), "rgb(255, 0, 0)"); + assert_eq!(parse_and_serialize("lch(50% 30 1e40)"), "lch(50 30 0)"); +} + +#[test] +fn normalize_finite_hue() { + for (input, expected) in [ + ("hsl(480 50% 50%)", "hsl(120 50% 50%)"), + ("hsl(-240 50% 50%)", "hsl(120 50% 50%)"), + ("hsl(360 50% 50%)", "hsl(0 50% 50%)"), + ("hwb(-540 10% 10%)", "hwb(180 10% 10%)"), + ("lch(50% 30 400)", "lch(50% 30 40)"), + ] { + assert_eq!( + parse_and_serialize(input), + parse_and_serialize(expected), + "{input}" + ); + } +}