Repository navigation
Conversation
fileURLToPath handed the URL pathname to WTF::URL::fileSystemPath(), whose percent-decoder is lenient: malformed escapes pass through verbatim and escapes that decode to invalid UTF-8 produce a null string, which surfaced as an empty path. fileSystemPath() is also compiled for the host platform only, so the `windows` option was ignored. Implement node's getPathFromURLWin32/getPathFromURLPosix instead: decode the pathname with ECMA-262 Decode semantics (throwing URIError on malformed escapes), read `options.windows`, and apply the matching platform rules (UNC host, drive letter, encoded separators). Fixes #29174
|
Updated 11:09 AM PT - Jul 5th, 2026
❌ @robobun, your commit 07c0929 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 33373That installs a local version of the PR into your bun-33373 --bun |
WalkthroughThis PR updates ChangesfileURLToPath windows option and strict decoding
domainToUnicode helper extraction
Related Issues: Sequence Diagram(s)sequenceDiagram
participant JS as JavaScript caller
participant functionFileURLToPath
participant decodeFileURLPath
JS->>functionFileURLToPath: fileURLToPath(url, options)
functionFileURLToPath->>functionFileURLToPath: determine windows/platformName from options or build target
alt windows mode
functionFileURLToPath->>functionFileURLToPath: reject encoded / or \
functionFileURLToPath->>functionFileURLToPath: replace / with \
else posix mode
functionFileURLToPath->>functionFileURLToPath: validate host against localhost
functionFileURLToPath->>functionFileURLToPath: reject encoded /
end
functionFileURLToPath->>decodeFileURLPath: decode path
decodeFileURLPath-->>functionFileURLToPath: decoded string or throw URI malformed
functionFileURLToPath->>functionFileURLToPath: enforce drive-letter/UNC shape or return path
functionFileURLToPath-->>JS: return decoded path or throw
Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
Node's getPathFromURLWin32 runs the hostname through domainToUnicode before building the UNC path, so file://xn--mnchen-3ya/foo becomes \\münchen\foo. Extract the IDNA-to-Unicode conversion out of the node:url binding and apply it in the UNC branch. Bun's URL parser accepts hosts node rejects outright (file://xn--/foo), and uidna_nameToUnicode cannot decode those, so fall back to the raw host instead of emitting a UNC path with an empty server name. Also drops the commented-out fileURLToPath shim in node/url.ts, which existed to patch in the windows path checking the binding now does itself.
There was a problem hiding this comment.
Thanks for the punycode fix — the domainToUnicode refactor and new UNC test cover exactly what I flagged. I'm deferring on approval since this is a full C++ rewrite of fileURLToPath's decoding/validation across all platforms and worth a human pass.
Extended reasoning...
Overview
Rewrites functionFileURLToPath in BunObject.cpp to implement Node's getPathFromURLWin32/getPathFromURLPosix directly instead of delegating to WTF::URL::fileSystemPath(). Adds a hand-rolled percent-decoder (decodeFileURLPath) with ECMA-262 Decode semantics, makes options.windows select platform rules at runtime, and factors domainToUnicode out of NodeURL.cpp into a reusable helper (used for UNC server names — the fix for my earlier comment). Also touches type declarations, deletes dead commented code from src/js/node/url.ts, and adds ~100 lines of tests.
Prior feedback
My previous review flagged that the Windows UNC branch used the punycode-encoded host directly. Commit 8439e38 addresses this exactly as suggested: Bun::domainToUnicode() is now a standalone helper, the UNC branch calls it with a fallback to the raw host on failure, and there's a dedicated decodes punycode UNC server names test block covering both Unicode and pre-encoded xn-- inputs plus an IPv4 host. That concern is resolved.
Security risks
fileURLToPath converts untrusted URL input into filesystem paths, so the encoded-separator checks and drive-letter validation are security-adjacent. The change actually tightens behavior (throwing where the old code silently returned "" or passed malformed escapes through), and the %2f/%5c rejection matches Node's guarantee that the result cannot contain smuggled separators. I don't see a new attack surface, but the hand-written UTF-8 validation via String::fromUTF8 and the ASSERT(input.containsOnlyASCII()) (debug-only) after /→\ replacement are the kind of details a maintainer should sanity-check.
Level of scrutiny
Medium-high. This is ~120 lines of new C++ in JSC bindings replacing a WebKit builtin, changing user-visible error messages and error classes on every platform, and the options.get() path can invoke user getters. Node/Web compat changes in this repo are explicitly called out as needing careful review. It's well-tested (including a claimed 21k-case differential against Node), but it's not a mechanical change.
Other factors
Test coverage is thorough — malformed escapes, overlong/surrogate/out-of-range UTF-8, both platform modes, encoded-separator rejection, drive-letter validation, punycode UNC hosts, and options-defaulting including primitive/null second args. The jsDomainToUnicode refactor preserves the pre-existing forbidden-host-code-point filter and 8-bit early-return, so node:url's domainToUnicode export should behave identically.
fileURLToPath now rejects "file:///\\\\server\\share\\db" the way node does: the URL has no host, so the windows drive-letter check fails. That newly activates parseDefinitelySqliteUrl's fallback, which sliced off "file://" and left "/\\\\server\\share\\db" with a leading slash no windows path wants. Slice the matched prefix and drop that slash on windows, which is what fileURLToPath does when it accepts the URL, and what this returned before.
|
Thanks — and Windows CI just earned its keep on exactly one of the two things you flagged, so here are answers to both plus what it caught.
The What CI caught.
|
There was a problem hiding this comment.
Thanks for the punycode fix — that thread is resolved and the differential now covers IDN hosts. No further issues found, but since this is a full native rewrite of fileURLToPath with cross-platform path-shape changes and a knock-on SQL fallback fix, it's worth a human pass before merge.
Extended reasoning...
Overview
This PR rewrites functionFileURLToPath in src/jsc/bindings/BunObject.cpp, replacing the WTF::URL::fileSystemPath() call with a hand-rolled implementation of Node's getPathFromURLWin32/getPathFromURLPosix. It adds a new decodeFileURLPath percent-decoder with ECMA-262 Decode semantics, factors Bun::domainToUnicode out of NodeURL.cpp for shared use, wires up the previously-ignored options.windows flag, updates the bun.d.ts signature, deletes dead shim code from src/js/node/url.ts, and patches a newly-exercised fallback in src/js/internal/sql/shared.ts. Test coverage is thorough (~100 new lines across two files) and backed by a 30k-case differential against Node v26.
Prior feedback
My earlier inline comment about the Windows UNC branch not running the hostname through domainToUnicode was addressed in 8439e38: the ICU call is now shared via Bun::domainToUnicode, with a fallback to the raw host when decoding fails (a deliberate, documented deviation for hosts Bun's parser accepts but Node's rejects). A dedicated test and an expanded IDN-host fuzz grammar cover it. The thread is resolved.
Security risks
fileURLToPath sits on the boundary between URLs and filesystem paths, so changes here are security-adjacent (path traversal, encoded-separator smuggling). The direction of the change is strictly tightening: malformed escapes now throw instead of silently returning "" or passing bytes through, and encoded / / \ are rejected per platform exactly as Node does. The ASSERT(input.containsOnlyASCII()) in decodeFileURLPath holds because the WHATWG parser percent-encodes the pathname; if it didn't, the static_cast<Latin1Character> would truncate, but that path is guarded by the contains('%') fast-path returning early for unescaped input anyway. I don't see a new attack surface, but path-shape logic (drive-letter check, UNC prefixing, leading-slash stripping in the SQL fallback) is exactly the kind of code where a second pair of eyes is cheap insurance.
Level of scrutiny
Medium-high. This is ~120 lines of new native C++ in a function that backs import.meta, module resolution, and node:url, with per-platform branches and a behavioral change on Windows (some inputs that previously returned a path now throw ERR_INVALID_FILE_URL_PATH, matching Node). The domainToUnicode refactor is behavior-preserving for node:url callers but moves a 2KB stack buffer into a helper now called from a second site. The SQL fallback change is small but was never previously exercised.
Other factors
The PR is exceptionally well-documented and well-tested; the author responded to review feedback promptly and re-ran the differential. The bug-hunting system found nothing on the current revision. I'm deferring rather than approving solely because a native rewrite of cross-platform path handling with downstream fallout is beyond the "simple/mechanical" bar for bot approval, not because of any specific concern.
The previous commit dropped it for any windows fallback path, which turned "file:///tmp/test.db" into "tmp/test.db". Those posix-shaped URLs also reach the fallback on windows, since they have no drive letter, and they keep the slash they start with. Narrow it to a leading "/\", which is the UNC shape the URL's third slash precedes, and the only path shape whose fileURLToPath result moved from a value to a throw.
|
Agreed on deferring to a human, and your instinct about the SQL fallback being "never previously exercised" was the right thing to be nervous about: my first attempt at it was wrong and Windows CI caught it.
The only path shape whose Rather than guess a third time, I simulated every
The narrowed rule also reproduces bun's pre-change Windows output exactly: On your two other notes: the |
The UNC shape is not the only one whose fileURLToPath result moved from a value to a throw: malformed percent escapes now raise URIError, so "file:///C:/50%off.db" reaches the fallback too and came back as "/C:/50%off.db", which windows cannot open. It used to return "C:\50%off.db", since the old decoder passed the bad escape through verbatim. Strip the slash before either windows root, UNC or drive letter, which is what fileURLToPath does for the same paths when it accepts them.
CI status: the diff is green, the two red lanes are unrelatedBuild #68589 is the first build to run this branch to completion: 284 passed, 2 failed. Both failures are independent of this change, and I am not pushing a 1. The upstream 2. Pre-existing, verified rather than assumed. I checked out What did passAll three Windows test lanes are green ( A maintainer can retry those two jobs in a couple of seconds, which is a better use of the fleet than rebuilding ~290 jobs on a roughly one-in-three chance of dodging the same darwin timeout. |
|
Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-05, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. The linked issue (#29174) stays open. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main. |
Fixes #29174. Supersedes #29176, which fixed the percent-decoding half only (and gave the
URIErroracodethat Node does not set).Reproduction
Returning
""instead of throwing turns a malformed URL into "the current directory" for whateverfscall receives it, and the unvalidated pass-throughs produce paths containing characters Node's contract guarantees cannot appear.Cause
functionFileURLToPathhanded the URL pathname toWTF::URL::fileSystemPath():%not followed by two hex digits is copied through verbatim, and escapes that decode to invalid UTF-8 produce a nullWTF::String(WebKit's ownFIXME: This returns a null string when we encounter an invalid UTF-8 sequence. Is that OK?), which surfaces in JS as"". Node decodes withdecodeURIComponent, which throwsURIError: URI malformedfor both.windowsoption was ignored: the%5Ccheck, the drive-letter check, and the/->\conversion never ran on POSIX, and the POSIX host check never ran on Windows.Fix
Implement Node's
getPathFromURLWin32/getPathFromURLPosixdirectly in the binding and drop thefileSystemPath()call:decodeFileURLPathdecodes the pathname with ECMA-262Decodesemantics (the algorithm behinddecodeURIComponent), throwingURIError: URI malformedon a truncated or non-hex escape, and on bytes that are not well-formed UTF-8 (lone continuation bytes, truncated sequences, overlong encodings, encoded surrogates, code points above U+10FFFF).options.windowsselects the platform rules at runtime, defaulting to the host platform, matchingfileURLToPath(path, { windows }).Bun::domainToUnicodeis factored out ofnode:url'sdomainToUnicodebinding so the two share one implementation.Error messages and the
ERR_INVALID_FILE_URL_PATH/ERR_INVALID_FILE_URL_HOSTcodes now match Node exactly, includingFile URL path must be absolute(previouslymust be an absolute path).Deletes the commented-out
fileURLToPathshim insrc/js/node/url.ts, which existed to patch in the Windows path checking the binding now does itself, along with theChar.PERCENTconstant it was the last reference to.One deliberate deviation
Bun's URL parser accepts IDN hosts Node's rejects outright (
new URL("file://xn--/foo")throwsERR_INVALID_URLin Node, parses in Bun), anduidna_nameToUnicodecannot decode those. A literal port of Node's`\\\\${domainToUnicode(hostname)}${pathname}`would emit\\\foofor such a host, a UNC path with an empty server name, because Node'sdomainToUnicodereturns''on failure. The raw host is kept instead. For every host Node can parse, the result is identical to Node.pathToFileURLstill ignoresoptions.windows. That is a pre-existing gap onmain, not something this change introduces, and it is left for a separate fix.Fallout: a latent sqlite bug this surfaced
fileURLToPath("file:///\\\\server\\share\\db")now throws on Windows, because the URL carries no host and the drive-letter check rejects///server/share/db. Node throws the sameERR_INVALID_FILE_URL_PATH. That throw newly activates a fallback inparseDefinitelySqliteUrl(src/js/internal/sql/shared.ts) which had never been exercised on any platform, and which sliced offfile://and returned/\\server\share\dbwith a leading slash no Windows path wants. It now slices the matched prefix and, on Windows only, drops that slash when a windows-rooted path follows it, either a UNC share or a drive letter. Two shapes reach the fallback because this PR made them throw:file:///\\\\server\\share\\db(no host, so the drive-letter check rejects it) andfile:///C:/50%off.db(a literal%is now aURIError, where the old lenient decoder passed it through and returnedC:\\50%off.db). Posix-shaped URLs likefile:///tmp/dbreach the same fallback on Windows too, since they carry no drive letter, and they keep the slash they start with. The strip mirrors whatfileURLToPathitself removes for those paths when it accepts them.Every
file://case insqlite-url-parsing.test.tswas simulated under Windows semantics before the change landed, which this PR makes possible:fileURLToPath(url, { windows: true })exercises the real win32 branch from any host, and the fallback is pure string arithmetic on top of it.The fallback is best effort and stays that way. It slices a fixed prefix and so has always ignored the URL's authority:
new SQL("file://localhost/tmp/db")returnedlocalhost/tmp/dbon Windows long before this change, because the path is not drive-rooted andfileURLToPathrejected it then too. MakingfileURLToPathstricter widens the set of URLs that reach the fallback, so that pre-existing blind spot is now also reachable for URLs that carry an authority and a malformed escape (file://server/share/50%off.db). Fixing that properly means giving the sqlite layer its own lenient URL-to-path conversion rather than piggybacking onfileURLToPath, which is a separate change; reusing the parsedpathnamehere would break thefile://.hidden.dbcases the suite already asserts. What this PR does fix is the class that is actually unopenable on Windows: a leading slash in front of a drive letter or UNC root.Verification
bun bd test test/js/node/url/url-fileurltopath.test.js test/js/bun/util/fileUrl.test.js— 50 pass, 0 fail. The new cases all fail onmain.Beyond the unit tests, a differential against Node v26 over a generated grammar of file URLs (hosts x path segments x escape sequences x query/fragment x
{windows: true|false}x string/URLinput) compared 30,824 cases. Every result is byte-identical to Node except 362 that hinge onC|:The
C|cases are a pre-existing URL parser divergence, notfileURLToPathWTF's URL parser only applies the WHATWG "normalized windows drive letter" rule in some positions. That reproduces on released Bun and is untouched by this change, so it is left for a separate fix.