Skip to content

Commit ee89dc7

Browse files
committed
path: update win32 toNamespacedPath to support device namespace paths
1 parent 880c446 commit ee89dc7

File tree

4 files changed

+177
-0
lines changed

4 files changed

+177
-0
lines changed

lib/path.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,29 @@ function glob(path, pattern, windows) {
180180
});
181181
}
182182

183+
// Regular expressions to identify special device names in Windows.
184+
// COM to AUX (e.g., COM1, LPT1, NUL, CON, CONIN$, PRN, AUX) are reserved OS device names.
185+
// therefore, Paths like C:\path\to\COM1 map to \\.\COM1, referencing hardware or system streams.
186+
// Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
187+
//
188+
// PhysicalDrive to Changer (e.g., PhysicalDrive1, TAPE0, Changer0) are not reserved OS device names.
189+
// Ref: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea
190+
const windowDevicePatterns = [
191+
/([\\/])?(COM\d+)$/i,
192+
/([\\/])?(LPT\d+)$/i,
193+
/([\\/])?(NUL)$/i,
194+
/([\\/])?(CON)$/i,
195+
/([\\/])?(PRN)$/i,
196+
/([\\/])?(AUX)$/i,
197+
/([\\/])?(CONIN\$)$/i,
198+
/([\\/])?(CONOUT\$)$/i,
199+
/^(PHYSICALDRIVE\d+)$/i,
200+
/^(PIPE\\.+)$/i,
201+
/^(MAILSLOT\\.+)$/i,
202+
/^(TAPE\d+)$/i,
203+
/^(CHANGER\d+)$/i,
204+
];
205+
183206
const win32 = {
184207
/**
185208
* path.resolve([from ...], to)
@@ -687,6 +710,19 @@ const win32 = {
687710
if (typeof path !== 'string' || path.length === 0)
688711
return path;
689712

713+
// Check if the path matches any device pattern
714+
if (windowDevicePatterns.some((pattern) => pattern.test(path))) {
715+
let deviceName;
716+
if (/^(PIPE\\.+)$/i.test(path) || /^(MAILSLOT\\.+)$/i.test(path)) {
717+
// If the path starts with PIPE\ or MAILSLOT\, keep it as is
718+
deviceName = path;
719+
} else {
720+
// Extract the last component after the last slash or backslash
721+
deviceName = path.split(/[\\/]/).pop();
722+
}
723+
return `\\\\.\\${deviceName}`;
724+
}
725+
690726
const resolvedPath = win32.resolve(path);
691727

692728
if (resolvedPath.length <= 2)

src/path.cc

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,43 @@ std::string PathResolve(Environment* env,
269269
void ToNamespacedPath(Environment* env, BufferValue* path) {
270270
#ifdef _WIN32
271271
if (path->length() == 0) return;
272+
273+
const std::vector<std::regex> windowDevicePatterns = {
274+
std::regex(R"((.*[\\/])?COM\d+$)", std::regex_constants::icase),
275+
std::regex(R"((.*[\\/])?LPT\d+$)", std::regex_constants::icase),
276+
std::regex(R"((.*[\\/])?NUL$)", std::regex_constants::icase),
277+
std::regex(R"((.*[\\/])?CON$)", std::regex_constants::icase),
278+
std::regex(R"((.*[\\/])?PRN$)", std::regex_constants::icase),
279+
std::regex(R"((.*[\\/])?AUX$)", std::regex_constants::icase),
280+
std::regex(R"((.*[\\/])?CONIN\$$)", std::regex_constants::icase),
281+
std::regex(R"((.*[\\/])?CONOUT\$$)", std::regex_constants::icase),
282+
std::regex(R"(^PHYSICALDRIVE\d+$)", std::regex_constants::icase),
283+
std::regex(R"(^(PIPE\\.+)$)", std::regex_constants::icase),
284+
std::regex(R"(^(MAILSLOT\\.+)$)", std::regex_constants::icase),
285+
std::regex(R"(^TAPE\d+$)", std::regex_constants::icase),
286+
std::regex(R"(^CHANGER\d+$)", std::regex_constants::icase)
287+
};
288+
289+
std::string path_str(path->ToStringView());
290+
for (const std::regex& pattern : windowDevicePatterns) {
291+
if (std::regex_match(path_str, pattern)) {
292+
std::string deviceName;
293+
if (std::regex_match(path_str, std::regex(R"(^(PIPE\\.+|MAILSLOT\\.+)$)",
294+
std::regex_constants::icase))) {
295+
deviceName = path_str;
296+
} else {
297+
size_t pos = path_str.find_last_of("\\/");
298+
deviceName = (pos != std::string::npos) ?
299+
path_str.substr(pos + 1) : path_str;
300+
}
301+
std::string new_path = "\\\\.\\" + deviceName;
302+
path->AllocateSufficientStorage(new_path.size() + 1);
303+
path->SetLength(new_path.size());
304+
memcpy(path->out(), new_path.c_str(), new_path.size() + 1);
305+
return;
306+
}
307+
}
308+
272309
std::string resolved_path = node::PathResolve(env, {path->ToStringView()});
273310
if (resolved_path.size() <= 2) {
274311
return;

test/cctest/test_path.cc

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,84 @@ TEST_F(PathTest, ToNamespacedPath) {
8181
.ToLocalChecked());
8282
ToNamespacedPath(*env, &data_4);
8383
EXPECT_EQ(data_4.ToStringView(), "\\\\?\\c:\\Windows\\System");
84+
BufferValue data5(
85+
isolate_,
86+
v8::String::NewFromUtf8(isolate_, "C:\\path\\COM1")
87+
.ToLocalChecked());
88+
ToNamespacedPath(*env, &data5);
89+
EXPECT_EQ(data5.ToStringView(), "\\\\.\\COM1");
90+
BufferValue data6(
91+
isolate_,
92+
v8::String::NewFromUtf8(isolate_, "COM1")
93+
.ToLocalChecked());
94+
ToNamespacedPath(*env, &data6);
95+
EXPECT_EQ(data6.ToStringView(), "\\\\.\\COM1");
96+
BufferValue data7(
97+
isolate_,
98+
v8::String::NewFromUtf8(isolate_, "LPT1")
99+
.ToLocalChecked());
100+
ToNamespacedPath(*env, &data7);
101+
EXPECT_EQ(data7.ToStringView(), "\\\\.\\LPT1");
102+
BufferValue data8(
103+
isolate_,
104+
v8::String::NewFromUtf8(isolate_, "C:\\LPT1")
105+
.ToLocalChecked());
106+
ToNamespacedPath(*env, &data8);
107+
EXPECT_EQ(data8.ToStringView(), "\\\\.\\LPT1");
108+
BufferValue data9(
109+
isolate_,
110+
v8::String::NewFromUtf8(isolate_, "PhysicalDrive0")
111+
.ToLocalChecked());
112+
ToNamespacedPath(*env, &data9);
113+
EXPECT_EQ(data9.ToStringView(), "\\\\.\\PhysicalDrive0");
114+
BufferValue data10(
115+
isolate_,
116+
v8::String::NewFromUtf8(isolate_, "pipe\\mypipe")
117+
.ToLocalChecked());
118+
ToNamespacedPath(*env, &data10);
119+
EXPECT_EQ(data10.ToStringView(), "\\\\.\\pipe\\mypipe");
120+
BufferValue data11(
121+
isolate_,
122+
v8::String::NewFromUtf8(isolate_, "MAILSLOT\\mySlot")
123+
.ToLocalChecked());
124+
ToNamespacedPath(*env, &data11);
125+
EXPECT_EQ(data11.ToStringView(), "\\\\.\\MAILSLOT\\mySlot");
126+
BufferValue data12(
127+
isolate_,
128+
v8::String::NewFromUtf8(isolate_, "NUL")
129+
.ToLocalChecked());
130+
ToNamespacedPath(*env, &data12);
131+
EXPECT_EQ(data12.ToStringView(), "\\\\.\\NUL");
132+
BufferValue data13(
133+
isolate_,
134+
v8::String::NewFromUtf8(isolate_, "Tape0")
135+
.ToLocalChecked());
136+
ToNamespacedPath(*env, &data13);
137+
EXPECT_EQ(data13.ToStringView(), "\\\\.\\Tape0");
138+
BufferValue data14(
139+
isolate_,
140+
v8::String::NewFromUtf8(isolate_, "Changer0")
141+
.ToLocalChecked());
142+
ToNamespacedPath(*env, &data14);
143+
EXPECT_EQ(data14.ToStringView(), "\\\\.\\Changer0");
144+
BufferValue data15(
145+
isolate_,
146+
v8::String::NewFromUtf8(isolate_, "\\\\.\\pipe\\somepipe")
147+
.ToLocalChecked());
148+
ToNamespacedPath(*env, &data15);
149+
EXPECT_EQ(data15.ToStringView(), "\\\\.\\pipe\\somepipe");
150+
BufferValue data16(
151+
isolate_,
152+
v8::String::NewFromUtf8(isolate_, "\\\\.\\COM1")
153+
.ToLocalChecked());
154+
ToNamespacedPath(*env, &data16);
155+
EXPECT_EQ(data16.ToStringView(), "\\\\.\\COM1");
156+
BufferValue data17(
157+
isolate_,
158+
v8::String::NewFromUtf8(isolate_, "\\\\.\\LPT1")
159+
.ToLocalChecked());
160+
ToNamespacedPath(*env, &data17);
161+
EXPECT_EQ(data17.ToStringView(), "\\\\.\\LPT1");
84162
#else
85163
BufferValue data(
86164
isolate_,

test/parallel/test-path-makelong.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,34 @@ if (common.isWindows) {
3939
assert.strictEqual(path.toNamespacedPath(
4040
'\\\\?\\UNC\\someserver\\someshare\\somefile'),
4141
'\\\\?\\UNC\\someserver\\someshare\\somefile');
42+
// Device name tests
43+
assert.strictEqual(path.toNamespacedPath('C:\\path\\COM1'),
44+
'\\\\.\\COM1');
45+
assert.strictEqual(path.toNamespacedPath('COM1'),
46+
'\\\\.\\COM1');
47+
assert.strictEqual(path.toNamespacedPath('LPT1'),
48+
'\\\\.\\LPT1');
49+
assert.strictEqual(path.toNamespacedPath('C:\\LPT1'),
50+
'\\\\.\\LPT1');
51+
assert.strictEqual(path.toNamespacedPath('PhysicalDrive0'),
52+
'\\\\.\\PhysicalDrive0');
53+
assert.strictEqual(path.toNamespacedPath('pipe\\mypipe'),
54+
'\\\\.\\pipe\\mypipe');
55+
assert.strictEqual(path.toNamespacedPath('MAILSLOT\\mySlot'),
56+
'\\\\.\\MAILSLOT\\mySlot');
57+
assert.strictEqual(path.toNamespacedPath('NUL'),
58+
'\\\\.\\NUL');
59+
assert.strictEqual(path.toNamespacedPath('Tape0'),
60+
'\\\\.\\Tape0');
61+
assert.strictEqual(path.toNamespacedPath('Changer0'),
62+
'\\\\.\\Changer0');
63+
// Test cases for inputs with "\\.\" prefix
4264
assert.strictEqual(path.toNamespacedPath('\\\\.\\pipe\\somepipe'),
4365
'\\\\.\\pipe\\somepipe');
66+
assert.strictEqual(path.toNamespacedPath('\\\\.\\COM1'),
67+
'\\\\.\\COM1');
68+
assert.strictEqual(path.toNamespacedPath('\\\\.\\LPT1'),
69+
'\\\\.\\LPT1');
4470
}
4571

4672
assert.strictEqual(path.toNamespacedPath(''), '');

0 commit comments

Comments
 (0)