Skip to content

Commit 560f446

Browse files
committed
path: handle extended-length windows entry paths
Normalize `\\?\` drive and UNC prefixes before resolution so namespaced entry points follow standard Windows path semantics. Fixes: #62446 Signed-off-by: jazelly <xzha4350@gmail.com>
1 parent ff08094 commit 560f446

File tree

4 files changed

+78
-0
lines changed

4 files changed

+78
-0
lines changed

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: 13 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");
@@ -85,6 +93,11 @@ TEST_F(PathTest, ToNamespacedPath) {
8593
.ToLocalChecked());
8694
ToNamespacedPath(*env, &data_4);
8795
EXPECT_EQ(data_4.ToStringView(), "\\\\?\\c:\\Windows\\System");
96+
BufferValue data_5(
97+
isolate_,
98+
v8::String::NewFromUtf8(isolate_, "\\\\?\\C:\\").ToLocalChecked());
99+
ToNamespacedPath(*env, &data_5);
100+
EXPECT_EQ(data_5.ToStringView(), "\\\\?\\C:\\");
88101
#else
89102
BufferValue data(
90103
isolate_,

test/es-module/test-esm-long-path-win.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,28 @@ describe('long path on Windows', () => {
4747
tmpdir.refresh();
4848
});
4949

50+
it('check extended-length path in executeUserEntryPoint', async () => {
51+
const packageDirPath = tmpdir.resolve('issue-62446');
52+
const mainJsFilePath = path.resolve(packageDirPath, 'main.js');
53+
const namespacedMainJsPath = path.toNamespacedPath(mainJsFilePath);
54+
55+
tmpdir.refresh();
56+
57+
fs.mkdirSync(packageDirPath);
58+
fs.writeFileSync(mainJsFilePath, 'console.log("hello world");');
59+
60+
const { code, signal, stderr, stdout } = await spawnPromisified(
61+
execPath,
62+
[namespacedMainJsPath],
63+
);
64+
assert.strictEqual(stderr.trim(), '');
65+
assert.strictEqual(stdout.trim(), 'hello world');
66+
assert.strictEqual(code, 0);
67+
assert.strictEqual(signal, null);
68+
69+
tmpdir.refresh();
70+
});
71+
5072
it('check long path in LegacyMainResolve - 1', () => {
5173
// Module layout will be the following:
5274
// package.json

test/parallel/test-fs-realpath.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,22 @@ function test_root_with_null_options(realpath, realpathSync, cb) {
558558
}));
559559
}
560560

561+
function test_windows_namespaced_path(realpath, realpathSync, cb) {
562+
if (!common.isWindows) {
563+
cb();
564+
return;
565+
}
566+
567+
const entry = tmp('issue-62446-entry.js');
568+
fs.writeFileSync(entry, 'console.log("ok");');
569+
const namespacedEntry = path.toNamespacedPath(entry);
570+
571+
assertEqualPath(realpathSync(namespacedEntry), path.resolve(entry));
572+
asynctest(realpath, [namespacedEntry], cb, function(err, result) {
573+
assertEqualPath(result, path.resolve(entry));
574+
});
575+
}
576+
561577
// ----------------------------------------------------------------------------
562578

563579
const tests = [
@@ -579,6 +595,7 @@ const tests = [
579595
test_up_multiple_with_null_options,
580596
test_root,
581597
test_root_with_null_options,
598+
test_windows_namespaced_path,
582599
];
583600
const numtests = tests.length;
584601
let testsRun = 0;

0 commit comments

Comments
 (0)