fs: fix realpath of namespaced drive paths
Project: nodejs/node | Language: JavaScript | Concepts: error-handling, refactoring, testing-strategy | Difficulty: mid
Link: https://github.com/nodejs/node/pull/65378
The problem
On Windows, the JavaScript implementation of fs.realpath did not correctly handle "namespaced" drive paths (e.g., \\?\C:\). When probing such a root, the path handling logic would drop the trailing separator, causing lstat to receive a path like C: instead of C:\. This led to incorrect errors (e.g., EISDIR) or failures to resolve the real path as expected.
Why it's non-trivial
A naive fix might simply pass the path through as-is, but Windows path resolution rules are subtle: namespaced paths need to be preserved for traversal and returned values, but the probe to check if the root exists must use the non-namespaced spelling to avoid losing the trailing separator. Failing to distinguish these cases can break path resolution or introduce subtle bugs.
The approach
This PR introduces a targeted fix by:
-
Detecting namespaced drive roots and normalizing them for probing only
Adds a helper to strip the namespace for lstat probes, but leaves the original path untouched for traversal and return.+let getRealpathRootLstatPath; if (isWindows) { ... + const namespacedDriveRootRe = /^\\\\\?\\([a-zA-Z]:\\)$/; ... + getRealpathRootLstatPath = function getRealpathRootLstatPath(path) { + const match = SideEffectFreeRegExpPrototypeExec(namespacedDriveRootRe, path); + return match === null ? path : match[1]; + }; }This snippet defines a regex to match namespaced drive roots and a function to extract the regular drive root (
C:\) from a namespaced path (\\?\C:\). This function is only used for probing the root withlstat. -
Using the helper when probing the root in realpath logic
All places that check the existence of a root on Windows now use the helper to ensure the correct path is passed tolstat.- const out = binding.lstat(base, false, undefined, true /* throwIfNoEntry */); + const out = binding.lstat( + getRealpathRootLstatPath(base), false, undefined, true /* throwIfNoEntry */);This ensures that the probe for the drive root works even if the input is a namespaced path.
Similarly, in the async version:
- fs.lstat(base, (err) => { + fs.lstat(getRealpathRootLstatPath(base), (err) => { -
Comprehensive Windows-specific tests
Adds a new test file to check all relevant behaviors, including resolving namespaced paths, error cases, and traversing junctions.+test('fs.realpathSync resolves a namespaced drive path', () => { + assertNamespacedRealpath(fs.realpathSync(namespacedEntry)); +}); + +test('fs.realpathSync reports ENOENT for a missing namespaced drive path', () => { + assert.throws(() => fs.realpathSync(namespacedMissing), { code: 'ENOENT' }); +}); + +test('fs.realpathSync resolves a namespaced path through a junction', () => { + assert.strictEqual(fs.realpathSync(namespacedJunctionEntry), targetEntry); +});These tests verify that namespaced drive paths are handled correctly in all edge cases.
From the review thread
No notable review discussion on this PR.
Takeaways
- Handle platform-specific path quirks explicitly: When working with filesystem paths, especially on Windows, always account for special cases like namespaced paths and drive roots.
- Isolate platform-specific logic in helpers: Encapsulate tricky normalization or transformation logic in dedicated functions to avoid scattering subtle path handling throughout the codebase.
- Comprehensive testing for edge cases: When fixing bugs involving platform-specific behavior, add targeted tests that cover all relevant scenarios, including error conditions and path traversals.