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