Skip to content

Commit e7caeec

Browse files
Copilotjazelly
andcommitted
path: strip extended-length path prefix in PathResolve (C++) and win32.resolve (JS)
Move the fix for Windows extended-length path prefix handling from lib/fs.js to the C++ PathResolve function (src/path.cc) and the JS path.resolve function (lib/path.js). The \\?\ prefix is a Win32 API mechanism for bypassing MAX_PATH limits. When path.resolve encounters paths like \\?\C:\foo or \\?\UNC\server\share, it should strip the prefix and resolve the underlying standard path. Device paths like \\?\PHYSICALDRIVE0 are left unchanged. This fix addresses the root cause in the path resolution layer rather than working around it in fs.realpathSync/realpath. Agent-Logs-Url: https://github.com/jazelly/node/sessions/b73cebf2-9b87-42a3-a50e-c5da1b454971 Co-authored-by: jazelly <28685065+jazelly@users.noreply.github.com>
1 parent a04f0ec commit e7caeec

6 files changed

Lines changed: 67 additions & 124 deletions

File tree

lib/fs.js

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,6 @@ const {
132132
const {
133133
CHAR_FORWARD_SLASH,
134134
CHAR_BACKWARD_SLASH,
135-
CHAR_COLON,
136-
CHAR_QUESTION_MARK,
137-
CHAR_UPPERCASE_A,
138-
CHAR_UPPERCASE_C,
139-
CHAR_UPPERCASE_Z,
140-
CHAR_LOWERCASE_A,
141-
CHAR_LOWERCASE_N,
142-
CHAR_LOWERCASE_Z,
143135
} = require('internal/constants');
144136
const {
145137
isInt32,
@@ -2636,43 +2628,6 @@ function unwatchFile(filename, listener) {
26362628
}
26372629

26382630

2639-
// Strips the Windows extended-length path prefix (\\?\) from a resolved path.
2640-
// Extended-length paths (\\?\C:\... or \\?\UNC\...) are a Win32 API mechanism
2641-
// to bypass MAX_PATH limits. Node.js should handle them transparently by
2642-
// converting to standard paths for internal processing. The \\?\ prefix is
2643-
// re-added when needed via path.toNamespacedPath() before system calls.
2644-
// See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
2645-
function stripExtendedPathPrefix(p) {
2646-
// Check for \\?\ prefix
2647-
if (p.length >= 4 &&
2648-
StringPrototypeCharCodeAt(p, 0) === CHAR_BACKWARD_SLASH &&
2649-
StringPrototypeCharCodeAt(p, 1) === CHAR_BACKWARD_SLASH &&
2650-
StringPrototypeCharCodeAt(p, 2) === CHAR_QUESTION_MARK &&
2651-
StringPrototypeCharCodeAt(p, 3) === CHAR_BACKWARD_SLASH) {
2652-
// \\?\C:\ -> C:\ (extended drive path)
2653-
if (p.length >= 6) {
2654-
const drive = StringPrototypeCharCodeAt(p, 4);
2655-
if (((drive >= CHAR_UPPERCASE_A && drive <= CHAR_UPPERCASE_Z) ||
2656-
(drive >= CHAR_LOWERCASE_A && drive <= CHAR_LOWERCASE_Z)) &&
2657-
StringPrototypeCharCodeAt(p, 5) === CHAR_COLON) {
2658-
return StringPrototypeSlice(p, 4);
2659-
}
2660-
}
2661-
// \\?\UNC\server\share -> \\server\share (extended UNC path)
2662-
if (p.length >= 8 &&
2663-
(StringPrototypeCharCodeAt(p, 4) === 85 /* U */ ||
2664-
StringPrototypeCharCodeAt(p, 4) === 117 /* u */) &&
2665-
(StringPrototypeCharCodeAt(p, 5) === 78 /* N */ ||
2666-
StringPrototypeCharCodeAt(p, 5) === CHAR_LOWERCASE_N) &&
2667-
(StringPrototypeCharCodeAt(p, 6) === CHAR_UPPERCASE_C ||
2668-
StringPrototypeCharCodeAt(p, 6) === 99 /* c */) &&
2669-
StringPrototypeCharCodeAt(p, 7) === CHAR_BACKWARD_SLASH) {
2670-
return '\\\\' + StringPrototypeSlice(p, 8);
2671-
}
2672-
}
2673-
return p;
2674-
}
2675-
26762631
let splitRoot;
26772632
if (isWindows) {
26782633
// Regex to find the device root on Windows (e.g. 'c:\\'), including trailing
@@ -2735,12 +2690,6 @@ function realpathSync(p, options) {
27352690
validatePath(p);
27362691
p = pathModule.resolve(p);
27372692

2738-
// On Windows, strip the extended-length path prefix (\\?\) so that the
2739-
// path walking logic below works with standard drive-letter or UNC roots.
2740-
if (isWindows) {
2741-
p = stripExtendedPathPrefix(p);
2742-
}
2743-
27442693
const cache = options[realpathCacheKey];
27452694
const maybeCachedResult = cache?.get(p);
27462695
if (maybeCachedResult) {
@@ -2844,11 +2793,6 @@ function realpathSync(p, options) {
28442793
// Resolve the link, then start over
28452794
p = pathModule.resolve(resolvedLink, StringPrototypeSlice(p, pos));
28462795

2847-
// Strip extended path prefix again in case pathModule.resolve re-added it
2848-
if (isWindows) {
2849-
p = stripExtendedPathPrefix(p);
2850-
}
2851-
28522796
// Skip over roots
28532797
current = base = splitRoot(p);
28542798
pos = current.length;
@@ -2907,12 +2851,6 @@ function realpath(p, options, callback) {
29072851
validatePath(p);
29082852
p = pathModule.resolve(p);
29092853

2910-
// On Windows, strip the extended-length path prefix (\\?\) so that the
2911-
// path walking logic below works with standard drive-letter or UNC roots.
2912-
if (isWindows) {
2913-
p = stripExtendedPathPrefix(p);
2914-
}
2915-
29162854
const seenLinks = new SafeMap();
29172855
const knownHard = new SafeSet();
29182856

@@ -3013,12 +2951,6 @@ function realpath(p, options, callback) {
30132951
function gotResolvedLink(resolvedLink) {
30142952
// Resolve the link, then start over
30152953
p = pathModule.resolve(resolvedLink, StringPrototypeSlice(p, pos));
3016-
3017-
// Strip extended path prefix again in case pathModule.resolve re-added it
3018-
if (isWindows) {
3019-
p = stripExtendedPathPrefix(p);
3020-
}
3021-
30222954
current = base = splitRoot(p);
30232955
pos = current.length;
30242956

lib/path.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,36 @@ const win32 = {
230230
}
231231
}
232232

233-
const len = path.length;
234233
let rootEnd = 0;
235234
let device = '';
236235
let isAbsolute = false;
236+
237+
// Strip extended-length path prefix (\\?\C:\... -> C:\...,
238+
// \\?\UNC\... -> \\...) before processing.
239+
if (path.length >= 6 &&
240+
StringPrototypeCharCodeAt(path, 0) === CHAR_BACKWARD_SLASH &&
241+
StringPrototypeCharCodeAt(path, 1) === CHAR_BACKWARD_SLASH &&
242+
StringPrototypeCharCodeAt(path, 2) === CHAR_QUESTION_MARK &&
243+
StringPrototypeCharCodeAt(path, 3) === CHAR_BACKWARD_SLASH) {
244+
const drive = StringPrototypeCharCodeAt(path, 4);
245+
if (isWindowsDeviceRoot(drive) &&
246+
StringPrototypeCharCodeAt(path, 5) === CHAR_COLON) {
247+
// \\?\C:\ -> C:\
248+
path = StringPrototypeSlice(path, 4);
249+
} else if (path.length >= 8 &&
250+
(drive === 85 || drive === 117) && // U/u
251+
(StringPrototypeCharCodeAt(path, 5) === 78 ||
252+
StringPrototypeCharCodeAt(path, 5) === 110) && // N/n
253+
(StringPrototypeCharCodeAt(path, 6) === 67 ||
254+
StringPrototypeCharCodeAt(path, 6) === 99) && // C/c
255+
StringPrototypeCharCodeAt(path, 7) ===
256+
CHAR_BACKWARD_SLASH) {
257+
// \\?\UNC\server\share -> \\server\share
258+
path = `\\\\${StringPrototypeSlice(path, 8)}`;
259+
}
260+
}
261+
262+
const len = path.length;
237263
const code = StringPrototypeCharCodeAt(path, 0);
238264

239265
// Try to match a root

src/path.cc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,28 @@ constexpr bool IsWindowsDeviceRoot(const char c) noexcept {
9898
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
9999
}
100100

101+
// Strip Windows extended-length path prefix (\\?\) only when it wraps a
102+
// drive letter path (\\?\C:\...) or a UNC path (\\?\UNC\...).
103+
// Device paths like \\?\PHYSICALDRIVE0 are left unchanged.
104+
// See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
105+
static void StripExtendedPathPrefix(std::string& path) {
106+
if (path.size() >= 4 && path[0] == '\\' && path[1] == '\\' &&
107+
path[2] == '?' && path[3] == '\\') {
108+
// \\?\C:\ -> C:\ (extended drive path)
109+
if (path.size() >= 6 && IsWindowsDeviceRoot(path[4]) && path[5] == ':') {
110+
path = path.substr(4);
111+
return;
112+
}
113+
// \\?\UNC\server\share -> \\server\share (extended UNC path)
114+
if (path.size() >= 8 && ToLower(path[4]) == 'u' &&
115+
ToLower(path[5]) == 'n' && ToLower(path[6]) == 'c' &&
116+
path[7] == '\\') {
117+
path = "\\\\" + path.substr(8);
118+
return;
119+
}
120+
}
121+
}
122+
101123
std::string PathResolve(Environment* env,
102124
const std::vector<std::string_view>& paths) {
103125
std::string resolvedDevice = "";
@@ -132,6 +154,10 @@ std::string PathResolve(Environment* env,
132154
}
133155
}
134156

157+
// Strip extended-length path prefix (\\?\C:\... -> C:\...,
158+
// \\?\UNC\... -> \\...) before processing.
159+
StripExtendedPathPrefix(path);
160+
135161
const size_t len = path.length();
136162
int rootEnd = 0;
137163
std::string device = "";

test/cctest/test_path.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ TEST_F(PathTest, PathResolve) {
4343
"\\\\.\\PHYSICALDRIVE0");
4444
EXPECT_EQ(PathResolve(*env, {"\\\\?\\PHYSICALDRIVE0"}),
4545
"\\\\?\\PHYSICALDRIVE0");
46+
// Extended-length path prefix (\\?\) should be stripped for drive paths
47+
EXPECT_EQ(PathResolve(*env, {"\\\\?\\C:\\foo"}), "C:\\foo");
48+
EXPECT_EQ(PathResolve(*env, {"\\\\?\\C:\\"}), "C:\\");
49+
// Extended-length UNC path prefix (\\?\UNC\) should be stripped
50+
EXPECT_EQ(PathResolve(*env, {"\\\\?\\UNC\\server\\share"}),
51+
"\\\\server\\share\\");
52+
EXPECT_EQ(PathResolve(*env, {"\\\\?\\UNC\\server\\share\\dir"}),
53+
"\\\\server\\share\\dir");
4654
#else
4755
EXPECT_EQ(PathResolve(*env, {"/var/lib", "../", "file/"}), "/var/file");
4856
EXPECT_EQ(PathResolve(*env, {"/var/lib", "/../", "file/"}), "/file");

test/parallel/test-fs-realpath-extended-windows-path.js

Lines changed: 0 additions & 55 deletions
This file was deleted.

test/parallel/test-path-resolve.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ const resolveTests = [
3737
'C:\\foo\\tmp.3\\cycles\\root.js'],
3838
[['\\\\.\\PHYSICALDRIVE0'], '\\\\.\\PHYSICALDRIVE0'],
3939
[['\\\\?\\PHYSICALDRIVE0'], '\\\\?\\PHYSICALDRIVE0'],
40+
// Extended-length path prefix (\\?\) should be stripped for drive paths
41+
[['\\\\?\\C:\\foo'], 'C:\\foo'],
42+
[['\\\\?\\C:\\'], 'C:\\'],
43+
// Extended-length UNC path prefix (\\?\UNC\) should be stripped
44+
[['\\\\?\\UNC\\server\\share'], '\\\\server\\share\\'],
45+
[['\\\\?\\UNC\\server\\share\\dir'], '\\\\server\\share\\dir'],
4046
],
4147
],
4248
[ path.posix.resolve,

0 commit comments

Comments
 (0)