diff --git a/lib/fs.js b/lib/fs.js index 81c4d2b9c884..8df941d1731e 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -3178,13 +3178,22 @@ function unwatchFile(filename, listener) { let splitRoot; +let getRealpathRootLstatPath; if (isWindows) { // Regex to find the device root on Windows (e.g. 'c:\\'), including trailing // slash. const splitRootRe = /^(?:[a-zA-Z]:|[\\/]{2}[^\\/]+[\\/][^\\/]+)?[\\/]*/; + const namespacedDriveRootRe = /^\\\\\?\\([a-zA-Z]:\\)$/; splitRoot = function splitRoot(str) { return SideEffectFreeRegExpPrototypeExec(splitRootRe, str)[0]; }; + + // The root probe is the only use of this path. Passing a namespaced drive + // root to the binding would lose its trailing separator during resolution. + getRealpathRootLstatPath = function getRealpathRootLstatPath(path) { + const match = SideEffectFreeRegExpPrototypeExec(namespacedDriveRootRe, path); + return match === null ? path : match[1]; + }; } else { splitRoot = function splitRoot(str) { for (let i = 0; i < str.length; ++i) { @@ -3193,6 +3202,7 @@ if (isWindows) { } return str; }; + } function encodeRealpathResult(result, options) { @@ -3274,7 +3284,8 @@ function realpathSync(p, options) { // On windows, check that the root exists. On unix there is no need. if (isWindows) { - const out = binding.lstat(base, false, undefined, true /* throwIfNoEntry */); + const out = binding.lstat( + getRealpathRootLstatPath(base), false, undefined, true /* throwIfNoEntry */); if (out === undefined) { return; } @@ -3359,7 +3370,8 @@ function realpathSync(p, options) { // On windows, check that the root exists. On unix there is no need. if (isWindows && !knownHard.has(base)) { - const out = binding.lstat(base, false, undefined, true /* throwIfNoEntry */); + const out = binding.lstat( + getRealpathRootLstatPath(base), false, undefined, true /* throwIfNoEntry */); if (out === undefined) { return; } @@ -3443,7 +3455,7 @@ function realpath(p, options, callback) { // On windows, check that the root exists. On unix there is no need. if (isWindows && !knownHard.has(base)) { - fs.lstat(base, (err) => { + fs.lstat(getRealpathRootLstatPath(base), (err) => { if (err) return callback(err); knownHard.add(base); LOOP(); @@ -3532,7 +3544,7 @@ function realpath(p, options, callback) { // On windows, check that the root exists. On unix there is no need. if (isWindows && !knownHard.has(base)) { - fs.lstat(base, (err) => { + fs.lstat(getRealpathRootLstatPath(base), (err) => { if (err) return callback(err); knownHard.add(base); LOOP(); diff --git a/test/es-module/test-esm-long-path-win.js b/test/es-module/test-esm-long-path-win.js index d125d341f092..d8aaabcca857 100644 --- a/test/es-module/test-esm-long-path-win.js +++ b/test/es-module/test-esm-long-path-win.js @@ -47,6 +47,24 @@ describe('long path on Windows', () => { tmpdir.refresh(); }); + it('runs an extended-length path as the entry point', async () => { + // The module loader resolves argv[1] through the JavaScript realpath + // implementation before executing it. + tmpdir.refresh(); + const entry = tmpdir.resolve('extended-entry.js'); + fs.writeFileSync(entry, 'console.log("hello world");'); + + const { code, signal, stderr, stdout } = await spawnPromisified( + execPath, + [path.toNamespacedPath(entry)], + ); + + assert.strictEqual(stderr, ''); + assert.strictEqual(stdout.trim(), 'hello world'); + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + }); + it('check long path in LegacyMainResolve - 1', () => { // Module layout will be the following: // package.json diff --git a/test/parallel/test-fs-realpath-namespaced-drive-win.js b/test/parallel/test-fs-realpath-namespaced-drive-win.js new file mode 100644 index 000000000000..eac72eb8dfa2 --- /dev/null +++ b/test/parallel/test-fs-realpath-namespaced-drive-win.js @@ -0,0 +1,63 @@ +'use strict'; + +const common = require('../common'); +if (!common.isWindows) { + common.skip('This test is Windows-specific.'); +} + +// Verify that the JavaScript realpath implementation accepts namespaced drive +// paths, including when a junction switches the walk back to a regular drive +// path, and reports a missing entry instead of treating the drive as a file. + +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); +const { test } = require('node:test'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const entry = tmpdir.resolve('entry.js'); +const namespacedEntry = path.toNamespacedPath(entry); +const namespacedMissing = path.toNamespacedPath(tmpdir.resolve('missing.js')); +const targetDir = tmpdir.resolve('target'); +const targetEntry = path.join(targetDir, 'entry.js'); +const junctionDir = tmpdir.resolve('junction'); +const namespacedJunctionEntry = path.toNamespacedPath( + path.join(junctionDir, 'entry.js'), +); + +fs.writeFileSync(entry, ''); +fs.mkdirSync(targetDir); +fs.writeFileSync(targetEntry, ''); +fs.symlinkSync(targetDir, junctionDir, 'junction'); + +function assertNamespacedRealpath(result) { + assert.strictEqual(path.toNamespacedPath(result), namespacedEntry); +} + +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); +}); + +test('fs.realpath resolves a namespaced drive path', (t, done) => { + fs.realpath(namespacedEntry, common.mustSucceed((result) => { + assertNamespacedRealpath(result); + done(); + })); +}); + +test('fs.realpath resolves a namespaced path through a junction', (t, done) => { + fs.realpath(namespacedJunctionEntry, common.mustSucceed((result) => { + assert.strictEqual(result, targetEntry); + done(); + })); +});