Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/css/values/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2459,6 +2459,13 @@ pub fn parse_color_mix(input: &mut css::Parser) -> CssResult<CssColor> {
};

// https://drafts.csswg.org/css-color-5/#color-mix-percent-norm
// The grammar is <percentage [0,100]>; values outside that range are invalid.
if first_percent.is_some_and(|p| !(0.0..=1.0).contains(&p))
|| second_percent.is_some_and(|p| !(0.0..=1.0).contains(&p))
{
return Err(input.new_custom_error(css::ParserError::invalid_value));
}

let (p1, p2): (f32, f32) = if first_percent.is_none() && second_percent.is_none() {
(0.5, 0.5)
} else {
Expand Down
46 changes: 46 additions & 0 deletions test/js/bun/css/color.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,3 +536,49 @@
expect(color("#f00", "[rgb]")).toEqual([255, 0, 0]);
});
});

// https://drafts.csswg.org/css-color-5/#color-mix — the grammar is
// <percentage [0,100]>, so a value outside that range is a parse error.
describe("color-mix() percentage range", () => {
// fuzz repro: -9% drove HSL saturation negative and tripped a debug assertion
// in hsl_to_rgb; release builds produced out-of-gamut garbage.
test("does not crash on a negative mix percentage", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`process.stdout.write(String(Bun.color("color-mix(in hsl,red -9%,color(display-p3 0 0 0)", "lab")))`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toEqual({ stdout: "null", stderr: "", exitCode: 0 });

Check warning on line 556 in test/js/bun/css/color.test.ts

View check run for this annotation

Claude / Claude Code Review

Test asserts stderr is exactly empty

nit: this asserts `stderr: ""`, which the repo guidance says to avoid (ASAN/debug builds can emit benign warnings). This file's own `autoAnsi` helper (~line 260) already models the local convention — `stderr: exitCode === 0 ? undefined : stderr` — so on success stderr is captured for the failure diff but not required to be empty. Consider matching that here.
Comment thread
robobun marked this conversation as resolved.
Resolved
});

test.each([
"color-mix(in hsl, red -9%, blue)",
"color-mix(in hsl, red 150%, blue)",
"color-mix(in hsl, -10% red, blue)",
"color-mix(in hsl, red, blue -9%)",
"color-mix(in hsl, red, blue 150%)",
"color-mix(in hsl, red, 150% blue)",
"color-mix(in srgb, red -1%, blue)",
"color-mix(in srgb, red 100.001%, blue)",
"color-mix(in lab, red -9%, blue)",
"color-mix(in hwb, red -9%, blue)",
"color-mix(in oklch, red 150%, blue)",
])("rejects %s", input => {
expect(color(input, "css")).toBeNull();
});

test.each([
["color-mix(in hsl, red 0%, blue)", "#00f"],
["color-mix(in hsl, red 100%, blue)", "red"],
["color-mix(in hsl, red 50%, blue 50%)", "#f0f"],
["color-mix(in hsl, red, blue 0%)", "red"],
["color-mix(in hsl, red, blue 100%)", "#00f"],
])("accepts %s", (input, expected) => {
expect(color(input, "css")).toBe(expected);
});
});
Loading