Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -2628,6 +2636,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
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
Expand Down Expand Up @@ -2690,6 +2735,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) {
Expand Down Expand Up @@ -2793,6 +2844,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;
Expand Down Expand Up @@ -2851,6 +2907,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();

Expand Down Expand Up @@ -2951,6 +3013,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;

Expand Down
55 changes: 55 additions & 0 deletions test/parallel/test-fs-realpath-extended-windows-path.js
Original file line number Diff line number Diff line change
@@ -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());
}
Loading