Skip to content

Commit 4b331df

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. Signed-off-by: jazelly <xzha4350@gmail.com>
1 parent ff08094 commit 4b331df

File tree

5 files changed

+208
-5
lines changed

5 files changed

+208
-5
lines changed

src/path.cc

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

101+
enum class WindowsNamespacedPathType {
102+
kNotNamespaced,
103+
kDriveAbsolutePath,
104+
kUNCPath,
105+
kOtherNamespacedPath,
106+
};
107+
108+
static WindowsNamespacedPathType ClassifyWindowsNamespacedPath(
109+
std::string_view path) {
110+
if (!(path.size() >= 4 && path[0] == '\\' && path[1] == '\\' &&
111+
path[2] == '?' && path[3] == '\\')) {
112+
return WindowsNamespacedPathType::kNotNamespaced;
113+
}
114+
115+
if (path.size() >= 7 && IsWindowsDeviceRoot(path[4]) && path[5] == ':' &&
116+
IsPathSeparator(path[6])) {
117+
return WindowsNamespacedPathType::kDriveAbsolutePath;
118+
}
119+
120+
if (path.size() >= 8 && ToLower(path[4]) == 'u' &&
121+
ToLower(path[5]) == 'n' && ToLower(path[6]) == 'c' &&
122+
path[7] == '\\') {
123+
size_t i = 8;
124+
const size_t server_start = i;
125+
while (i < path.size() && !IsPathSeparator(path[i])) {
126+
i++;
127+
}
128+
if (i == server_start || i == path.size()) {
129+
return WindowsNamespacedPathType::kOtherNamespacedPath;
130+
}
131+
132+
while (i < path.size() && IsPathSeparator(path[i])) {
133+
i++;
134+
}
135+
const size_t share_start = i;
136+
while (i < path.size() && !IsPathSeparator(path[i])) {
137+
i++;
138+
}
139+
if (i == share_start) {
140+
return WindowsNamespacedPathType::kOtherNamespacedPath;
141+
}
142+
143+
return WindowsNamespacedPathType::kUNCPath;
144+
}
145+
146+
return WindowsNamespacedPathType::kOtherNamespacedPath;
147+
}
148+
149+
static void StripExtendedPathPrefixForPathResolve(std::string& path) {
150+
switch (ClassifyWindowsNamespacedPath(path)) {
151+
case WindowsNamespacedPathType::kDriveAbsolutePath:
152+
path = path.substr(4);
153+
return;
154+
case WindowsNamespacedPathType::kUNCPath:
155+
path = "\\\\" + path.substr(8);
156+
return;
157+
case WindowsNamespacedPathType::kNotNamespaced:
158+
case WindowsNamespacedPathType::kOtherNamespacedPath:
159+
return;
160+
}
161+
}
162+
101163
std::string PathResolve(Environment* env,
102164
const std::vector<std::string_view>& paths) {
103165
std::string resolvedDevice = "";
@@ -132,6 +194,8 @@ std::string PathResolve(Environment* env,
132194
}
133195
}
134196

197+
StripExtendedPathPrefixForPathResolve(path);
198+
135199
const size_t len = path.length();
136200
int rootEnd = 0;
137201
std::string device = "";
@@ -330,11 +394,16 @@ void ToNamespacedPath(Environment* env, BufferValue* path) {
330394
// namespace-prefixed path.
331395
void FromNamespacedPath(std::string* path) {
332396
#ifdef _WIN32
333-
if (path->starts_with("\\\\?\\UNC\\")) {
334-
*path = path->substr(8);
335-
path->insert(0, "\\\\");
336-
} else if (path->starts_with("\\\\?\\")) {
337-
*path = path->substr(4);
397+
switch (ClassifyWindowsNamespacedPath(*path)) {
398+
case WindowsNamespacedPathType::kUNCPath:
399+
*path = "\\\\" + path->substr(8);
400+
return;
401+
case WindowsNamespacedPathType::kDriveAbsolutePath:
402+
*path = path->substr(4);
403+
return;
404+
case WindowsNamespacedPathType::kNotNamespaced:
405+
case WindowsNamespacedPathType::kOtherNamespacedPath:
406+
return;
338407
}
339408
#endif
340409
}

test/cctest/test_path.cc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "v8.h"
99

1010
using node::BufferValue;
11+
using node::FromNamespacedPath;
1112
using node::PathResolve;
1213
using node::ToNamespacedPath;
1314

@@ -43,6 +44,13 @@ TEST_F(PathTest, PathResolve) {
4344
"\\\\.\\PHYSICALDRIVE0");
4445
EXPECT_EQ(PathResolve(*env, {"\\\\?\\PHYSICALDRIVE0"}),
4546
"\\\\?\\PHYSICALDRIVE0");
47+
EXPECT_EQ(PathResolve(*env, {"\\\\?\\C:\\foo"}), "C:\\foo");
48+
EXPECT_EQ(PathResolve(*env, {"\\\\?\\C:\\"}), "C:\\");
49+
EXPECT_EQ(PathResolve(*env, {"\\\\?\\UNC\\server\\share"}),
50+
"\\\\server\\share\\");
51+
EXPECT_EQ(PathResolve(*env, {"\\\\?\\UNC\\server\\share\\dir"}),
52+
"\\\\server\\share\\dir");
53+
EXPECT_EQ(PathResolve(*env, {"\\\\?\\C:foo"}), "\\\\?\\C:foo");
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_,
@@ -93,3 +106,23 @@ TEST_F(PathTest, ToNamespacedPath) {
93106
EXPECT_EQ(data.ToStringView(), "hello world"); // Input should not be mutated
94107
#endif
95108
}
109+
110+
TEST_F(PathTest, FromNamespacedPath) {
111+
#ifdef _WIN32
112+
std::string drive_absolute = "\\\\?\\C:\\foo";
113+
FromNamespacedPath(&drive_absolute);
114+
EXPECT_EQ(drive_absolute, "C:\\foo");
115+
116+
std::string unc_absolute = "\\\\?\\UNC\\server\\share\\dir";
117+
FromNamespacedPath(&unc_absolute);
118+
EXPECT_EQ(unc_absolute, "\\\\server\\share\\dir");
119+
120+
std::string device_path = "\\\\?\\PHYSICALDRIVE0";
121+
FromNamespacedPath(&device_path);
122+
EXPECT_EQ(device_path, "\\\\?\\PHYSICALDRIVE0");
123+
124+
std::string drive_relative = "\\\\?\\C:foo";
125+
FromNamespacedPath(&drive_relative);
126+
EXPECT_EQ(drive_relative, "\\\\?\\C:foo");
127+
#endif
128+
}

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
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict';
2+
3+
// This tests NODE_COMPILE_CACHE works with a Windows namespaced path.
4+
5+
const common = require('../common');
6+
if (!common.isWindows) {
7+
common.skip('this test is Windows-specific.');
8+
}
9+
10+
const { spawnSyncAndAssert } = require('../common/child_process');
11+
const assert = require('assert');
12+
const fixtures = require('../common/fixtures');
13+
const tmpdir = require('../common/tmpdir');
14+
const fs = require('fs');
15+
const path = require('path');
16+
17+
{
18+
tmpdir.refresh();
19+
const cacheDir = tmpdir.resolve('.compile_cache_dir');
20+
const namespacedCacheDir = path.toNamespacedPath(cacheDir);
21+
22+
spawnSyncAndAssert(
23+
process.execPath,
24+
[fixtures.path('empty.js')],
25+
{
26+
env: {
27+
...process.env,
28+
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
29+
NODE_COMPILE_CACHE: namespacedCacheDir,
30+
},
31+
cwd: tmpdir.path,
32+
},
33+
{
34+
stderr(output) {
35+
assert.match(output, /writing cache for .*empty\.js.*success/);
36+
return true;
37+
},
38+
});
39+
40+
const topEntries = fs.readdirSync(cacheDir);
41+
assert.strictEqual(topEntries.length, 1);
42+
const cacheEntries = fs.readdirSync(path.join(cacheDir, topEntries[0]));
43+
assert.strictEqual(cacheEntries.length, 1);
44+
45+
spawnSyncAndAssert(
46+
process.execPath,
47+
[fixtures.path('empty.js')],
48+
{
49+
env: {
50+
...process.env,
51+
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
52+
NODE_COMPILE_CACHE: namespacedCacheDir,
53+
},
54+
cwd: tmpdir.path,
55+
},
56+
{
57+
stderr(output) {
58+
assert.match(output, /cache for .*empty\.js was accepted/);
59+
return true;
60+
},
61+
});
62+
}

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)