From ce98a72e017aadccd6f11eeef22d2be9cb17fd43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 06:27:44 +0000 Subject: [PATCH 1/2] fs: handle extended Windows path prefix in realpathSync and realpath Strip the \\?\ extended-length path prefix from Windows paths in fs.realpathSync and fs.realpath before the path walking logic. The \\?\ prefix is a Win32 API mechanism to bypass MAX_PATH limits. When a user passes a path like \\?\C:\path\to\file.js, Node.js should handle it transparently by converting to the standard C:\path\to\file.js form for internal processing. The \\?\ prefix is re-added as needed via path.toNamespacedPath() before actual system calls. Handles two forms of extended paths: - \\?\C:\path -> C:\path (extended drive path) - \\?\UNC\server\share -> \\server\share (extended UNC path) Fixes: https://github.com/nodejs/node/issues/62446 Agent-Logs-Url: https://github.com/jazelly/node/sessions/77c338e9-ef46-46f1-8800-b6ad9a74ab36 Co-authored-by: jazelly <28685065+jazelly@users.noreply.github.com> --- lib/fs.js | 60 +++++++++++++++++++ .../test-fs-realpath-extended-windows-path.js | 55 +++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 test/parallel/test-fs-realpath-extended-windows-path.js diff --git a/lib/fs.js b/lib/fs.js index 4a03fada49ea8a..7f17c9f104ed01 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -2628,6 +2628,43 @@ 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 (charCode 92=\, 63=?) + if (p.length >= 4 && + StringPrototypeCharCodeAt(p, 0) === CHAR_BACKWARD_SLASH && + StringPrototypeCharCodeAt(p, 1) === CHAR_BACKWARD_SLASH && + StringPrototypeCharCodeAt(p, 2) === 63 /* ? */ && + StringPrototypeCharCodeAt(p, 3) === CHAR_BACKWARD_SLASH) { + // \\?\C:\ -> C:\ (extended drive path) + if (p.length >= 6) { + const drive = StringPrototypeCharCodeAt(p, 4); + if (((drive >= 65 /* A */ && drive <= 90 /* Z */) || + (drive >= 97 /* a */ && drive <= 122 /* z */)) && + StringPrototypeCharCodeAt(p, 5) === 58 /* : */) { + 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) === 110 /* n */) && + (StringPrototypeCharCodeAt(p, 6) === 67 /* 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 @@ -2690,6 +2727,12 @@ 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) { @@ -2793,6 +2836,11 @@ 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; @@ -2851,6 +2899,12 @@ 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(); @@ -2951,6 +3005,12 @@ 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/test/parallel/test-fs-realpath-extended-windows-path.js b/test/parallel/test-fs-realpath-extended-windows-path.js new file mode 100644 index 00000000000000..acbbb8f8522d81 --- /dev/null +++ b/test/parallel/test-fs-realpath-extended-windows-path.js @@ -0,0 +1,55 @@ +'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()); +} From 4a6670568c08e2a54a1a20e4a64a0c2cb843c648 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 06:31:31 +0000 Subject: [PATCH 2/2] fs: use named constants in stripExtendedPathPrefix Agent-Logs-Url: https://github.com/jazelly/node/sessions/77c338e9-ef46-46f1-8800-b6ad9a74ab36 Co-authored-by: jazelly <28685065+jazelly@users.noreply.github.com> --- lib/fs.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/fs.js b/lib/fs.js index 7f17c9f104ed01..02cc109535e936 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -132,6 +132,14 @@ 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, @@ -2635,18 +2643,18 @@ function unwatchFile(filename, listener) { // 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 (charCode 92=\, 63=?) + // Check for \\?\ prefix if (p.length >= 4 && StringPrototypeCharCodeAt(p, 0) === CHAR_BACKWARD_SLASH && StringPrototypeCharCodeAt(p, 1) === CHAR_BACKWARD_SLASH && - StringPrototypeCharCodeAt(p, 2) === 63 /* ? */ && + 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 >= 65 /* A */ && drive <= 90 /* Z */) || - (drive >= 97 /* a */ && drive <= 122 /* z */)) && - StringPrototypeCharCodeAt(p, 5) === 58 /* : */) { + 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); } } @@ -2655,8 +2663,8 @@ function stripExtendedPathPrefix(p) { (StringPrototypeCharCodeAt(p, 4) === 85 /* U */ || StringPrototypeCharCodeAt(p, 4) === 117 /* u */) && (StringPrototypeCharCodeAt(p, 5) === 78 /* N */ || - StringPrototypeCharCodeAt(p, 5) === 110 /* n */) && - (StringPrototypeCharCodeAt(p, 6) === 67 /* C */ || + 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);