Skip to content

Commit 2dbf9d7

Browse files
authored
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 5b316e5 commit 2dbf9d7

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
@@ -3272,13 +3272,22 @@ function unwatchFile(filename, listener) {
32723272

32733273

32743274
let splitRoot;
3275+
let getRealpathRootLstatPath;
32753276
if (isWindows) {
32763277
// Regex to find the device root on Windows (e.g. 'c:\\'), including trailing
32773278
// slash.
32783279
const splitRootRe = /^(?:[a-zA-Z]:|[\\/]{2}[^\\/]+[\\/][^\\/]+)?[\\/]*/;
3280+
const namespacedDriveRootRe = /^\\\\\?\\([a-zA-Z]:\\)$/;
32793281
splitRoot = function splitRoot(str) {
32803282
return SideEffectFreeRegExpPrototypeExec(splitRootRe, str)[0];
32813283
};
3284+
3285+
// The root probe is the only use of this path. Passing a namespaced drive
3286+
// root to the binding would lose its trailing separator during resolution.
3287+
getRealpathRootLstatPath = function getRealpathRootLstatPath(path) {
3288+
const match = SideEffectFreeRegExpPrototypeExec(namespacedDriveRootRe, path);
3289+
return match === null ? path : match[1];
3290+
};
32823291
} else {
32833292
splitRoot = function splitRoot(str) {
32843293
for (let i = 0; i < str.length; ++i) {
@@ -3287,6 +3296,7 @@ if (isWindows) {
32873296
}
32883297
return str;
32893298
};
3299+
32903300
}
32913301

32923302
function encodeRealpathResult(result, options) {
@@ -3368,7 +3378,8 @@ function realpathSync(p, options) {
33683378

33693379
// On windows, check that the root exists. On unix there is no need.
33703380
if (isWindows) {
3371-
const out = binding.lstat(base, false, undefined, true /* throwIfNoEntry */);
3381+
const out = binding.lstat(
3382+
getRealpathRootLstatPath(base), false, undefined, true /* throwIfNoEntry */);
33723383
if (out === undefined) {
33733384
return;
33743385
}
@@ -3453,7 +3464,8 @@ function realpathSync(p, options) {
34533464

34543465
// On windows, check that the root exists. On unix there is no need.
34553466
if (isWindows && !knownHard.has(base)) {
3456-
const out = binding.lstat(base, false, undefined, true /* throwIfNoEntry */);
3467+
const out = binding.lstat(
3468+
getRealpathRootLstatPath(base), false, undefined, true /* throwIfNoEntry */);
34573469
if (out === undefined) {
34583470
return;
34593471
}
@@ -3537,7 +3549,7 @@ function realpath(p, options, callback) {
35373549

35383550
// On windows, check that the root exists. On unix there is no need.
35393551
if (isWindows && !knownHard.has(base)) {
3540-
fs.lstat(base, (err) => {
3552+
fs.lstat(getRealpathRootLstatPath(base), (err) => {
35413553
if (err) return callback(err);
35423554
knownHard.add(base);
35433555
LOOP();
@@ -3626,7 +3638,7 @@ function realpath(p, options, callback) {
36263638

36273639
// On windows, check that the root exists. On unix there is no need.
36283640
if (isWindows && !knownHard.has(base)) {
3629-
fs.lstat(base, (err) => {
3641+
fs.lstat(getRealpathRootLstatPath(base), (err) => {
36303642
if (err) return callback(err);
36313643
knownHard.add(base);
36323644
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)