Skip to content

Commit 481800b

Browse files
committed
path: normalize windows namespaced paths and add regressions
1 parent 5a3570c commit 481800b

File tree

3 files changed

+150
-21
lines changed

3 files changed

+150
-21
lines changed

src/path.cc

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -98,23 +98,65 @@ 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-
if (path.size() >= 6 && IsWindowsDeviceRoot(path[4]) && path[5] == ':') {
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:
109152
path = path.substr(4);
110153
return;
111-
}
112-
if (path.size() >= 8 && ToLower(path[4]) == 'u' &&
113-
ToLower(path[5]) == 'n' && ToLower(path[6]) == 'c' &&
114-
path[7] == '\\') {
154+
case WindowsNamespacedPathType::kUNCPath:
115155
path = "\\\\" + path.substr(8);
116156
return;
117-
}
157+
case WindowsNamespacedPathType::kNotNamespaced:
158+
case WindowsNamespacedPathType::kOtherNamespacedPath:
159+
return;
118160
}
119161
}
120162

@@ -152,9 +194,7 @@ std::string PathResolve(Environment* env,
152194
}
153195
}
154196

155-
// Strip extended-length path prefix (\\?\C:\... -> C:\...,
156-
// \\?\UNC\... -> \\...) before processing.
157-
StripExtendedPathPrefix(path);
197+
StripExtendedPathPrefixForPathResolve(path);
158198

159199
const size_t len = path.length();
160200
int rootEnd = 0;
@@ -354,11 +394,16 @@ void ToNamespacedPath(Environment* env, BufferValue* path) {
354394
// namespace-prefixed path.
355395
void FromNamespacedPath(std::string* path) {
356396
#ifdef _WIN32
357-
if (path->starts_with("\\\\?\\UNC\\")) {
358-
*path = path->substr(8);
359-
path->insert(0, "\\\\");
360-
} else if (path->starts_with("\\\\?\\")) {
361-
*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;
362407
}
363408
#endif
364409
}

test/cctest/test_path.cc

Lines changed: 22 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

@@ -49,6 +50,7 @@ TEST_F(PathTest, PathResolve) {
4950
"\\\\server\\share\\");
5051
EXPECT_EQ(PathResolve(*env, {"\\\\?\\UNC\\server\\share\\dir"}),
5152
"\\\\server\\share\\dir");
53+
EXPECT_EQ(PathResolve(*env, {"\\\\?\\C:foo"}), "\\\\?\\C:foo");
5254
#else
5355
EXPECT_EQ(PathResolve(*env, {"/var/lib", "../", "file/"}), "/var/file");
5456
EXPECT_EQ(PathResolve(*env, {"/var/lib", "/../", "file/"}), "/file");
@@ -104,3 +106,23 @@ TEST_F(PathTest, ToNamespacedPath) {
104106
EXPECT_EQ(data.ToStringView(), "hello world"); // Input should not be mutated
105107
#endif
106108
}
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+
}
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+
}

0 commit comments

Comments
 (0)