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: 0 additions & 68 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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;

Expand Down
28 changes: 27 additions & 1 deletion lib/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions src/path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string_view>& paths) {
std::string resolvedDevice = "";
Expand Down Expand Up @@ -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 = "";
Expand Down
8 changes: 8 additions & 0 deletions test/cctest/test_path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
55 changes: 0 additions & 55 deletions test/parallel/test-fs-realpath-extended-windows-path.js

This file was deleted.

6 changes: 6 additions & 0 deletions test/parallel/test-path-resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading