Describe the bug
wrapId and unwrapId in packages/vite/src/shared/utils.ts encode and decode null bytes using String.prototype.replace with a string pattern:
// wrapId
VALID_ID_PREFIX + id.replace('\0', NULL_BYTE_PLACEHOLDER)
// unwrapId
id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, '\0')
String.prototype.replace with a string pattern replaces only the first occurrence. When an id contains more than one null byte, only the first is encoded, and only the first placeholder is decoded.
wrapId is meant to make an id URL-safe by encoding every \0 as __x00__ (see the NULL_BYTE_PLACEHOLDER doc comment in packages/vite/src/shared/constants.ts), but the wrapped id keeps a raw null byte after the first one, which is not valid in an import URL. unwrapId is asymmetric for the same reason.
This affects real ids. A virtual module wrapped by another plugin, for example a CommonJS proxy around a virtual module, produces an id such as \0commonjs-proxy:\0virtual that contains more than one null byte.
The same single-replace pattern is also used for the Lightning CSS filename in packages/vite/src/node/plugins/css.ts. The existing comment there notes that Lightning CSS treats a null byte as a string terminator, so a leftover null byte truncates the filename.
I have opened a PR that fixes all three call sites and adds tests: #22687.
Reproduction
https://gist.github.com/SujalXplores/7f6ff088efa07cd0699fbd1c81be9fde
Steps to reproduce
Save the gist as wrap-id-repro.mjs and run it:
Output on the current main (the \0 below is a raw null byte, not the __x00__ placeholder):
wrapped: "/@id/__x00__commonjs-proxy:\0virtual"
wrapped still contains a raw null byte: true
Expected output:
wrapped: "/@id/__x00__commonjs-proxy:__x00__virtual"
wrapped still contains a raw null byte: false
The same expectation is covered by the unit test added in #22687, which fails on main and passes with the fix.
System Info
Affected version: [email protected] (current main, commit d18e985) and earlier versions that use these helpers.
Platform-independent defect in pure string helpers: wrapId and unwrapId in src/shared/utils.ts, and the Lightning CSS filename encoder in src/node/plugins/css.ts. Not OS, Node, package manager, or browser specific.
Used Package Manager
pnpm
Logs
No response
Validations
Describe the bug
wrapIdandunwrapIdinpackages/vite/src/shared/utils.tsencode and decode null bytes usingString.prototype.replacewith a string pattern:String.prototype.replacewith a string pattern replaces only the first occurrence. When an id contains more than one null byte, only the first is encoded, and only the first placeholder is decoded.wrapIdis meant to make an id URL-safe by encoding every\0as__x00__(see theNULL_BYTE_PLACEHOLDERdoc comment inpackages/vite/src/shared/constants.ts), but the wrapped id keeps a raw null byte after the first one, which is not valid in an import URL.unwrapIdis asymmetric for the same reason.This affects real ids. A virtual module wrapped by another plugin, for example a CommonJS proxy around a virtual module, produces an id such as
\0commonjs-proxy:\0virtualthat contains more than one null byte.The same single-replace pattern is also used for the Lightning CSS filename in
packages/vite/src/node/plugins/css.ts. The existing comment there notes that Lightning CSS treats a null byte as a string terminator, so a leftover null byte truncates the filename.I have opened a PR that fixes all three call sites and adds tests: #22687.
Reproduction
https://gist.github.com/SujalXplores/7f6ff088efa07cd0699fbd1c81be9fde
Steps to reproduce
Save the gist as
wrap-id-repro.mjsand run it:Output on the current
main(the\0below is a raw null byte, not the__x00__placeholder):Expected output:
The same expectation is covered by the unit test added in #22687, which fails on
mainand passes with the fix.System Info
Used Package Manager
pnpm
Logs
No response
Validations