diff --git a/lib/fs.js b/lib/fs.js index 02cc109535e936..4a03fada49ea8a 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -132,14 +132,6 @@ const { const { CHAR_FORWARD_SLASH, CHAR_BACKWARD_SLASH, - CHAR_COLON, - CHAR_QUESTION_MARK, - CHAR_UPPERCASE_A, - CHAR_UPPERCASE_C, - CHAR_UPPERCASE_Z, - CHAR_LOWERCASE_A, - CHAR_LOWERCASE_N, - CHAR_LOWERCASE_Z, } = require('internal/constants'); const { isInt32, @@ -2636,43 +2628,6 @@ function unwatchFile(filename, listener) { } -// Strips the Windows extended-length path prefix (\\?\) from a resolved path. -// Extended-length paths (\\?\C:\... or \\?\UNC\...) are a Win32 API mechanism -// to bypass MAX_PATH limits. Node.js should handle them transparently by -// converting to standard paths for internal processing. The \\?\ prefix is -// re-added when needed via path.toNamespacedPath() before system calls. -// See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file -function stripExtendedPathPrefix(p) { - // Check for \\?\ prefix - if (p.length >= 4 && - StringPrototypeCharCodeAt(p, 0) === CHAR_BACKWARD_SLASH && - StringPrototypeCharCodeAt(p, 1) === CHAR_BACKWARD_SLASH && - StringPrototypeCharCodeAt(p, 2) === CHAR_QUESTION_MARK && - StringPrototypeCharCodeAt(p, 3) === CHAR_BACKWARD_SLASH) { - // \\?\C:\ -> C:\ (extended drive path) - if (p.length >= 6) { - const drive = StringPrototypeCharCodeAt(p, 4); - if (((drive >= CHAR_UPPERCASE_A && drive <= CHAR_UPPERCASE_Z) || - (drive >= CHAR_LOWERCASE_A && drive <= CHAR_LOWERCASE_Z)) && - StringPrototypeCharCodeAt(p, 5) === CHAR_COLON) { - return StringPrototypeSlice(p, 4); - } - } - // \\?\UNC\server\share -> \\server\share (extended UNC path) - if (p.length >= 8 && - (StringPrototypeCharCodeAt(p, 4) === 85 /* U */ || - StringPrototypeCharCodeAt(p, 4) === 117 /* u */) && - (StringPrototypeCharCodeAt(p, 5) === 78 /* N */ || - StringPrototypeCharCodeAt(p, 5) === CHAR_LOWERCASE_N) && - (StringPrototypeCharCodeAt(p, 6) === CHAR_UPPERCASE_C || - StringPrototypeCharCodeAt(p, 6) === 99 /* c */) && - StringPrototypeCharCodeAt(p, 7) === CHAR_BACKWARD_SLASH) { - return '\\\\' + StringPrototypeSlice(p, 8); - } - } - return p; -} - let splitRoot; if (isWindows) { // Regex to find the device root on Windows (e.g. 'c:\\'), including trailing @@ -2735,12 +2690,6 @@ function realpathSync(p, options) { validatePath(p); p = pathModule.resolve(p); - // On Windows, strip the extended-length path prefix (\\?\) so that the - // path walking logic below works with standard drive-letter or UNC roots. - if (isWindows) { - p = stripExtendedPathPrefix(p); - } - const cache = options[realpathCacheKey]; const maybeCachedResult = cache?.get(p); if (maybeCachedResult) { @@ -2844,11 +2793,6 @@ function realpathSync(p, options) { // Resolve the link, then start over p = pathModule.resolve(resolvedLink, StringPrototypeSlice(p, pos)); - // Strip extended path prefix again in case pathModule.resolve re-added it - if (isWindows) { - p = stripExtendedPathPrefix(p); - } - // Skip over roots current = base = splitRoot(p); pos = current.length; @@ -2907,12 +2851,6 @@ function realpath(p, options, callback) { validatePath(p); p = pathModule.resolve(p); - // On Windows, strip the extended-length path prefix (\\?\) so that the - // path walking logic below works with standard drive-letter or UNC roots. - if (isWindows) { - p = stripExtendedPathPrefix(p); - } - const seenLinks = new SafeMap(); const knownHard = new SafeSet(); @@ -3013,12 +2951,6 @@ function realpath(p, options, callback) { function gotResolvedLink(resolvedLink) { // Resolve the link, then start over p = pathModule.resolve(resolvedLink, StringPrototypeSlice(p, pos)); - - // Strip extended path prefix again in case pathModule.resolve re-added it - if (isWindows) { - p = stripExtendedPathPrefix(p); - } - current = base = splitRoot(p); pos = current.length; diff --git a/lib/path.js b/lib/path.js index 63b037cddfb986..7e26ba7a22d528 100644 --- a/lib/path.js +++ b/lib/path.js @@ -230,10 +230,36 @@ const win32 = { } } - const len = path.length; let rootEnd = 0; let device = ''; let isAbsolute = false; + + // Strip extended-length path prefix (\\?\C:\... -> C:\..., + // \\?\UNC\... -> \\...) before processing. + if (path.length >= 6 && + StringPrototypeCharCodeAt(path, 0) === CHAR_BACKWARD_SLASH && + StringPrototypeCharCodeAt(path, 1) === CHAR_BACKWARD_SLASH && + StringPrototypeCharCodeAt(path, 2) === CHAR_QUESTION_MARK && + StringPrototypeCharCodeAt(path, 3) === CHAR_BACKWARD_SLASH) { + const drive = StringPrototypeCharCodeAt(path, 4); + if (isWindowsDeviceRoot(drive) && + StringPrototypeCharCodeAt(path, 5) === CHAR_COLON) { + // \\?\C:\ -> C:\ + path = StringPrototypeSlice(path, 4); + } else if (path.length >= 8 && + (drive === 85 || drive === 117) && // U/u + (StringPrototypeCharCodeAt(path, 5) === 78 || + StringPrototypeCharCodeAt(path, 5) === 110) && // N/n + (StringPrototypeCharCodeAt(path, 6) === 67 || + StringPrototypeCharCodeAt(path, 6) === 99) && // C/c + StringPrototypeCharCodeAt(path, 7) === + CHAR_BACKWARD_SLASH) { + // \\?\UNC\server\share -> \\server\share + path = `\\\\${StringPrototypeSlice(path, 8)}`; + } + } + + const len = path.length; const code = StringPrototypeCharCodeAt(path, 0); // Try to match a root diff --git a/src/path.cc b/src/path.cc index f4b8d4577bd1e6..fd0d9202670550 100644 --- a/src/path.cc +++ b/src/path.cc @@ -98,6 +98,28 @@ constexpr bool IsWindowsDeviceRoot(const char c) noexcept { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } +// Strip Windows extended-length path prefix (\\?\) only when it wraps a +// drive letter path (\\?\C:\...) or a UNC path (\\?\UNC\...). +// Device paths like \\?\PHYSICALDRIVE0 are left unchanged. +// See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file +static void StripExtendedPathPrefix(std::string& path) { + if (path.size() >= 4 && path[0] == '\\' && path[1] == '\\' && + path[2] == '?' && path[3] == '\\') { + // \\?\C:\ -> C:\ (extended drive path) + if (path.size() >= 6 && IsWindowsDeviceRoot(path[4]) && path[5] == ':') { + path = path.substr(4); + return; + } + // \\?\UNC\server\share -> \\server\share (extended UNC path) + if (path.size() >= 8 && ToLower(path[4]) == 'u' && + ToLower(path[5]) == 'n' && ToLower(path[6]) == 'c' && + path[7] == '\\') { + path = "\\\\" + path.substr(8); + return; + } + } +} + std::string PathResolve(Environment* env, const std::vector& paths) { std::string resolvedDevice = ""; @@ -132,6 +154,10 @@ std::string PathResolve(Environment* env, } } + // Strip extended-length path prefix (\\?\C:\... -> C:\..., + // \\?\UNC\... -> \\...) before processing. + StripExtendedPathPrefix(path); + const size_t len = path.length(); int rootEnd = 0; std::string device = ""; diff --git a/test/cctest/test_path.cc b/test/cctest/test_path.cc index 9e860d02cf77bd..6abb679bfd0410 100644 --- a/test/cctest/test_path.cc +++ b/test/cctest/test_path.cc @@ -43,6 +43,14 @@ TEST_F(PathTest, PathResolve) { "\\\\.\\PHYSICALDRIVE0"); EXPECT_EQ(PathResolve(*env, {"\\\\?\\PHYSICALDRIVE0"}), "\\\\?\\PHYSICALDRIVE0"); + // Extended-length path prefix (\\?\) should be stripped for drive paths + EXPECT_EQ(PathResolve(*env, {"\\\\?\\C:\\foo"}), "C:\\foo"); + EXPECT_EQ(PathResolve(*env, {"\\\\?\\C:\\"}), "C:\\"); + // Extended-length UNC path prefix (\\?\UNC\) should be stripped + EXPECT_EQ(PathResolve(*env, {"\\\\?\\UNC\\server\\share"}), + "\\\\server\\share\\"); + EXPECT_EQ(PathResolve(*env, {"\\\\?\\UNC\\server\\share\\dir"}), + "\\\\server\\share\\dir"); #else EXPECT_EQ(PathResolve(*env, {"/var/lib", "../", "file/"}), "/var/file"); EXPECT_EQ(PathResolve(*env, {"/var/lib", "/../", "file/"}), "/file"); diff --git a/test/parallel/test-fs-realpath-extended-windows-path.js b/test/parallel/test-fs-realpath-extended-windows-path.js deleted file mode 100644 index acbbb8f8522d81..00000000000000 --- a/test/parallel/test-fs-realpath-extended-windows-path.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; -const common = require('../common'); - -// This test verifies that fs.realpathSync and fs.realpath correctly handle -// Windows extended-length path prefixes (\\?\C:\... and \\?\UNC\...). -// See: https://github.com/nodejs/node/issues/62446 - -if (!common.isWindows) - common.skip('Windows-specific test.'); - -const assert = require('assert'); -const fs = require('fs'); -const path = require('path'); -const tmpdir = require('../common/tmpdir'); - -tmpdir.refresh(); - -const testFile = tmpdir.resolve('extended-path-test.js'); -fs.writeFileSync(testFile, 'module.exports = 42;'); - -// Construct the extended-length path for the test file. -// The \\?\ prefix is a Win32 API mechanism to bypass MAX_PATH limits. -const extendedPath = `\\\\?\\${testFile}`; - -// fs.realpathSync should handle the \\?\ prefix and return a standard path. -{ - const result = fs.realpathSync(extendedPath); - // The result should be the resolved path without the \\?\ prefix. - assert.strictEqual(result.toLowerCase(), testFile.toLowerCase()); -} - -// fs.realpath (async) should also handle the \\?\ prefix. -fs.realpath(extendedPath, common.mustSucceed((result) => { - assert.strictEqual(result.toLowerCase(), testFile.toLowerCase()); -})); - -// Also test that the extended path for the drive root works. -{ - const driveRoot = path.parse(testFile).root; // e.g., 'C:\' - const extendedRoot = `\\\\?\\${driveRoot}`; - const result = fs.realpathSync(extendedRoot); - assert.strictEqual(result.toLowerCase(), driveRoot.toLowerCase()); -} - -// Test extended-length path with subdirectory. -const subDir = tmpdir.resolve('sub', 'dir'); -fs.mkdirSync(subDir, { recursive: true }); -const subFile = path.join(subDir, 'file.txt'); -fs.writeFileSync(subFile, 'hello'); - -{ - const extendedSubFile = `\\\\?\\${subFile}`; - const result = fs.realpathSync(extendedSubFile); - assert.strictEqual(result.toLowerCase(), subFile.toLowerCase()); -} diff --git a/test/parallel/test-path-resolve.js b/test/parallel/test-path-resolve.js index 088ed4b3ff183f..ca2a656b974cfb 100644 --- a/test/parallel/test-path-resolve.js +++ b/test/parallel/test-path-resolve.js @@ -37,6 +37,12 @@ const resolveTests = [ 'C:\\foo\\tmp.3\\cycles\\root.js'], [['\\\\.\\PHYSICALDRIVE0'], '\\\\.\\PHYSICALDRIVE0'], [['\\\\?\\PHYSICALDRIVE0'], '\\\\?\\PHYSICALDRIVE0'], + // Extended-length path prefix (\\?\) should be stripped for drive paths + [['\\\\?\\C:\\foo'], 'C:\\foo'], + [['\\\\?\\C:\\'], 'C:\\'], + // Extended-length UNC path prefix (\\?\UNC\) should be stripped + [['\\\\?\\UNC\\server\\share'], '\\\\server\\share\\'], + [['\\\\?\\UNC\\server\\share\\dir'], '\\\\server\\share\\dir'], ], ], [ path.posix.resolve,