Skip to content

Commit 118ce75

Browse files
authored
path: strip extended-length path prefix in C++ PathResolve and JS win32.resolve
2 parents a04f0ec + e7caeec commit 118ce75

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)