SIGN IN SIGN UP
oven-sh / bun UNCLAIMED

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one

0 0 150 Rust

css: escape custom pseudo-class/element names when printing (#31404)

### What

Fuzzer-found round-trip violation (`invariant:css:minified CSS does not
reparse`): a 6-byte CSS input whose selector is a pseudo-class named
with an escaped space (`:\ `) minifies to a bare `: `, and the
minifier's own output no longer parses.

```sh
BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING=1 bun -e 'require("bun:internal-for-testing").cssInternals.minifyTest(":\\ {w:", "")'
```

Before: the input `:\ {w:` prints as `: {w:}`, and re-parsing that
output fails with `Invalid selector. Expected identifier in
pseudo-element, found: `.

### Cause

Unknown (custom) pseudo-class and pseudo-element names are stored
unescaped by the tokenizer, but `serialize_pseudo_class` /
`serialize_pseudo_element` in `src/css/selectors/selector.rs` wrote them
back raw:

```rust
PseudoClass::Custom { name } => {
    dest.write_char(b':')?;
    return dest.write_str(name);
}
```

So any name containing characters that require escaping (a space, a
colon, etc.) loses its escapes on output, producing CSS that no longer
parses. Other identifier-bearing selector parts (classes, ids, `:lang()`
arguments) already re-escape via `serialize_identifier`; the four custom
pseudo arms (`Custom` / `CustomFunction` for both pseudo-classes and
pseudo-elements) did not.

### Fix

Serialize custom pseudo-class/element names with
`Printer::serialize_identifier` instead of `write_str`, so escapes are
regenerated on output. Ordinary names (letters, digits, hyphens, vendor
prefixes) are unaffected — `serialize_identifier` only escapes when
needed.

Now `:\ {w:` prints as `:\ {w:}`, which reparses to the same stylesheet.

### Verification

- New test `test/js/bun/css/custom-pseudo-ident-escape.test.ts` covers
the fuzzer-minimized input, escaped names in custom
pseudo-classes/elements/functions, round-trip stability of the minified
output, and that ordinary unknown pseudo names are unchanged. 3 of 4
tests fail without the fix.
- `test/js/bun/css/css.test.ts` (1087 pass), the other CSS test files,
and `test/bundler/css/` all pass with the change.
R
robobun committed
984bc1e18063165aa5358c46a04e90f0550c5aa8
Parent: 2cd36bd
Committed by GitHub <noreply@github.com> on 5/25/2026, 11:52:49 PM