Skip to content

Commit f45e199

Browse files
jazellyaduh95
authored andcommitted
fs: fix realpath of namespaced drive paths
The JavaScript realpath implementation probes a namespaced drive root through the fs binding. Windows path resolution drops the trailing separator from that probe, so lstat receives C: and reports EISDIR. Use the regular drive-root spelling only for the probe. Preserve the namespaced spelling for traversal and returned paths. Signed-off-by: Jason Zhang <xzha4350@gmail.com> PR-URL: #65378 Fixes: #62446 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stefan Stojanovic <stefan.stojanovic@janeasystems.com>
1 parent f3db8db commit f45e199

3 files changed

Lines changed: 97 additions & 4 deletions

File tree

lib/fs.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2716,13 +2716,22 @@ function unwatchFile(filename, listener) {
27162716

27172717

27182718
let splitRoot;
2719+
let getRealpathRootLstatPath;
27192720
if (isWindows) {
27202721
// Regex to find the device root on Windows (e.g. 'c:\\'), including trailing
27212722
// slash.
27222723
const splitRootRe = /^(?:[a-zA-Z]:|[\\/]{2}[^\\/]+[\\/][^\\/]+)?[\\/]*/;
2724+
const namespacedDriveRootRe = /^\\\\\?\\([a-zA-Z]:\\)$/;
27232725
splitRoot = function splitRoot(str) {
27242726
return SideEffectFreeRegExpPrototypeExec(splitRootRe, str)[0];
27252727
};
2728+
2729+
// The root probe is the only use of this path. Passing a namespaced drive
2730+
// root to the binding would lose its trailing separator during resolution.
2731+
getRealpathRootLstatPath = function getRealpathRootLstatPath(path) {
2732+
const match = SideEffectFreeRegExpPrototypeExec(namespacedDriveRootRe, path);
2733+
return match === null ? path : match[1];
2734+
};
27262735
} else {
27272736
splitRoot = function splitRoot(str) {
27282737
for (let i = 0; i < str.length; ++i) {
@@ -2731,6 +2740,7 @@ if (isWindows) {
27312740
}
27322741
return str;
27332742
};
2743+
27342744
}
27352745

27362746
function encodeRealpathResult(result, options) {
@@ -2807,7 +2817,8 @@ function realpathSync(p, options) {
28072817

28082818
// On windows, check that the root exists. On unix there is no need.
28092819
if (isWindows) {
2810-
const out = binding.lstat(base, false, undefined, true /* throwIfNoEntry */);
2820+
const out = binding.lstat(
2821+
getRealpathRootLstatPath(base), false, undefined, true /* throwIfNoEntry */);
28112822
if (out === undefined) {
28122823
return;
28132824
}
@@ -2892,7 +2903,8 @@ function realpathSync(p, options) {
28922903

28932904
// On windows, check that the root exists. On unix there is no need.
28942905
if (isWindows && !knownHard.has(base)) {
2895-
const out = binding.lstat(base, false, undefined, true /* throwIfNoEntry */);
2906+
const out = binding.lstat(
2907+
getRealpathRootLstatPath(base), false, undefined, true /* throwIfNoEntry */);
28962908
if (out === undefined) {
28972909
return;
28982910
}
@@ -2966,7 +2978,7 @@ function realpath(p, options, callback) {
29662978

29672979
// On windows, check that the root exists. On unix there is no need.
29682980
if (isWindows && !knownHard.has(base)) {
2969-
fs.lstat(base, (err) => {
2981+
fs.lstat(getRealpathRootLstatPath(base), (err) => {
29702982
if (err) return callback(err);
29712983
knownHard.add(base);
29722984
LOOP();
@@ -3055,7 +3067,7 @@ function realpath(p, options, callback) {
30553067

30563068
// On windows, check that the root exists. On unix there is no need.
30573069
if (isWindows && !knownHard.has(base)) {
3058-
fs.lstat(base, (err) => {
3070+
fs.lstat(getRealpathRootLstatPath(base), (err) => {
30593071
if (err) return callback(err);
30603072
knownHard.add(base);
30613073
LOOP();

test/es-module/test-esm-long-path-win.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ describe('long path on Windows', () => {
4747
tmpdir.refresh();
4848
});
4949

50+
it('runs an extended-length path as the entry point', async () => {
51+
// The module loader resolves argv[1] through the JavaScript realpath
52+
// implementation before executing it.
53+
tmpdir.refresh();
54+
const entry = tmpdir.resolve('extended-entry.js');
55+
fs.writeFileSync(entry, 'console.log("hello world");');
56+
57+
const { code, signal, stderr, stdout } = await spawnPromisified(
58+
execPath,
59+
[path.toNamespacedPath(entry)],
60+
);
61+
62+
assert.strictEqual(stderr, '');
63+
assert.strictEqual(stdout.trim(), 'hello world');
64+
assert.strictEqual(code, 0);
65+
assert.strictEqual(signal, null);
66+
});
67+
5068
it('check long path in LegacyMainResolve - 1', () => {
5169
// Module layout will be the following:
5270
// package.json
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.isWindows) {
5+
common.skip('This test is Windows-specific.');
6+
}
7+
8+
// Verify that the JavaScript realpath implementation accepts namespaced drive
9+
// paths, including when a junction switches the walk back to a regular drive
10+
// path, and reports a missing entry instead of treating the drive as a file.
11+
12+
const assert = require('node:assert');
13+
const fs = require('node:fs');
14+
const path = require('node:path');
15+
const { test } = require('node:test');
16+
const tmpdir = require('../common/tmpdir');
17+
18+
tmpdir.refresh();
19+
20+
const entry = tmpdir.resolve('entry.js');
21+
const namespacedEntry = path.toNamespacedPath(entry);
22+
const namespacedMissing = path.toNamespacedPath(tmpdir.resolve('missing.js'));
23+
const targetDir = tmpdir.resolve('target');
24+
const targetEntry = path.join(targetDir, 'entry.js');
25+
const junctionDir = tmpdir.resolve('junction');
26+
const namespacedJunctionEntry = path.toNamespacedPath(
27+
path.join(junctionDir, 'entry.js'),
28+
);
29+
30+
fs.writeFileSync(entry, '');
31+
fs.mkdirSync(targetDir);
32+
fs.writeFileSync(targetEntry, '');
33+
fs.symlinkSync(targetDir, junctionDir, 'junction');
34+
35+
function assertNamespacedRealpath(result) {
36+
assert.strictEqual(path.toNamespacedPath(result), namespacedEntry);
37+
}
38+
39+
test('fs.realpathSync resolves a namespaced drive path', () => {
40+
assertNamespacedRealpath(fs.realpathSync(namespacedEntry));
41+
});
42+
43+
test('fs.realpathSync reports ENOENT for a missing namespaced drive path', () => {
44+
assert.throws(() => fs.realpathSync(namespacedMissing), { code: 'ENOENT' });
45+
});
46+
47+
test('fs.realpathSync resolves a namespaced path through a junction', () => {
48+
assert.strictEqual(fs.realpathSync(namespacedJunctionEntry), targetEntry);
49+
});
50+
51+
test('fs.realpath resolves a namespaced drive path', (t, done) => {
52+
fs.realpath(namespacedEntry, common.mustSucceed((result) => {
53+
assertNamespacedRealpath(result);
54+
done();
55+
}));
56+
});
57+
58+
test('fs.realpath resolves a namespaced path through a junction', (t, done) => {
59+
fs.realpath(namespacedJunctionEntry, common.mustSucceed((result) => {
60+
assert.strictEqual(result, targetEntry);
61+
done();
62+
}));
63+
});

0 commit comments

Comments
 (0)