Skip to content

Commit e04d346

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

File tree

6 files changed

+180
-8
lines changed

6 files changed

+180
-8
lines changed

doc/api/path.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,12 +619,16 @@ added: v9.0.0
619619
-->
620620

621621
* `path` {string}
622+
* `convertToDevicePath`: {boolean}
622623
* Returns: {string}
623624

624625
On Windows systems only, returns an equivalent [namespace-prefixed path][] for
625626
the given `path`. If `path` is not a string, `path` will be returned without
626627
modifications.
627628

629+
To convert the `path` to a device namespacedPath,
630+
set `convertToDevicePath` to true; by default, this option is set to false.
631+
628632
This method is meaningful only on Windows systems. On POSIX systems, the
629633
method is non-operational and always returns `path` without modifications.
630634

lib/path.js

Lines changed: 39 additions & 1 deletion
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 windowsDevicePatterns = [
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)
@@ -680,13 +703,28 @@ const win32 = {
680703

681704
/**
682705
* @param {string} path
706+
* @param {boolean} convertToDevicePath
683707
* @returns {string}
684708
*/
685-
toNamespacedPath(path) {
709+
toNamespacedPath(path, convertToDevicePath = false) {
686710
// Note: this will *probably* throw somewhere.
687711
if (typeof path !== 'string' || path.length === 0)
688712
return path;
689713

714+
// Only check for Windows device path patterns if conversion is needed.
715+
// This avoids conflicts with file creation (e.g., mkfile).
716+
if (convertToDevicePath && windowsDevicePatterns.some((pattern) => pattern.test(path))) {
717+
let deviceName;
718+
if (/^(PIPE\\.+)$/i.test(path) || /^(MAILSLOT\\.+)$/i.test(path)) {
719+
// If the path starts with PIPE\ or MAILSLOT\, keep it as is
720+
deviceName = path;
721+
} else {
722+
// Extract the last component after the last slash or backslash
723+
deviceName = path.split(/[\\/]/).pop();
724+
}
725+
return `\\\\.\\${deviceName}`;
726+
}
727+
690728
const resolvedPath = win32.resolve(path);
691729

692730
if (resolvedPath.length <= 2)

src/path.cc

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,52 @@ std::string PathResolve(Environment* env,
266266
}
267267
#endif // _WIN32
268268

269-
void ToNamespacedPath(Environment* env, BufferValue* path) {
269+
void ToNamespacedPath(Environment* env,
270+
BufferValue* path,
271+
bool convertToDevicePath) {
270272
#ifdef _WIN32
271273
if (path->length() == 0) return;
274+
275+
static const std::vector<std::regex> windowsDevicePatterns = {
276+
std::regex(R"((.*[\\/])?COM\d+$)", std::regex_constants::icase),
277+
std::regex(R"((.*[\\/])?LPT\d+$)", std::regex_constants::icase),
278+
std::regex(R"((.*[\\/])?NUL$)", std::regex_constants::icase),
279+
std::regex(R"((.*[\\/])?CON$)", std::regex_constants::icase),
280+
std::regex(R"((.*[\\/])?PRN$)", std::regex_constants::icase),
281+
std::regex(R"((.*[\\/])?AUX$)", std::regex_constants::icase),
282+
std::regex(R"((.*[\\/])?CONIN\$$)", std::regex_constants::icase),
283+
std::regex(R"((.*[\\/])?CONOUT\$$)", std::regex_constants::icase),
284+
std::regex(R"(^PHYSICALDRIVE\d+$)", std::regex_constants::icase),
285+
std::regex(R"(^(PIPE\\.+)$)", std::regex_constants::icase),
286+
std::regex(R"(^(MAILSLOT\\.+)$)", std::regex_constants::icase),
287+
std::regex(R"(^TAPE\d+$)", std::regex_constants::icase),
288+
std::regex(R"(^CHANGER\d+$)", std::regex_constants::icase)};
289+
290+
// Only check for Windows device path patterns if conversion is needed.
291+
// This avoids conflicts with file creation (e.g., mkfile).
292+
if (convertToDevicePath) {
293+
std::string path_str(path->ToStringView());
294+
for (const std::regex& pattern : windowsDevicePatterns) {
295+
if (std::regex_match(path_str, pattern)) {
296+
std::string deviceName;
297+
if (std::regex_match(path_str,
298+
std::regex(R"(^(PIPE\\.+|MAILSLOT\\.+)$)",
299+
std::regex_constants::icase))) {
300+
deviceName = path_str;
301+
} else {
302+
size_t pos = path_str.find_last_of("\\/");
303+
deviceName =
304+
(pos != std::string::npos) ? path_str.substr(pos + 1) : path_str;
305+
}
306+
std::string new_path = "\\\\.\\" + deviceName;
307+
path->AllocateSufficientStorage(new_path.size() + 1);
308+
path->SetLength(new_path.size());
309+
memcpy(path->out(), new_path.c_str(), new_path.size() + 1);
310+
return;
311+
}
312+
}
313+
}
314+
272315
std::string resolved_path = node::PathResolve(env, {path->ToStringView()});
273316
if (resolved_path.size() <= 2) {
274317
return;

src/path.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ std::string PathResolve(Environment* env,
2424
constexpr bool IsWindowsDeviceRoot(const char c) noexcept;
2525
#endif // _WIN32
2626

27-
void ToNamespacedPath(Environment* env, BufferValue* path);
27+
void ToNamespacedPath(Environment* env,
28+
BufferValue* path,
29+
bool convertToDevicePath = false);
2830
void FromNamespacedPath(std::string* path);
2931

3032
} // namespace node

test/cctest/test_path.cc

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ TEST_F(PathTest, ToNamespacedPath) {
5757
#ifdef _WIN32
5858
BufferValue data(isolate_,
5959
v8::String::NewFromUtf8(isolate_, "").ToLocalChecked());
60-
ToNamespacedPath(*env, &data);
60+
ToNamespacedPath(*env, &data, true);
6161
EXPECT_EQ(data.ToStringView(), ""); // Empty string should not be mutated
6262
BufferValue data_2(
6363
isolate_, v8::String::NewFromUtf8(isolate_, "C://").ToLocalChecked());
64-
ToNamespacedPath(*env, &data_2);
64+
ToNamespacedPath(*env, &data_2, true);
6565
EXPECT_EQ(data_2.ToStringView(), "\\\\?\\C:\\");
6666
BufferValue data_3(
6767
isolate_,
@@ -70,7 +70,7 @@ TEST_F(PathTest, ToNamespacedPath) {
7070
"C:\\workspace\\node-test-binary-windows-js-"
7171
"suites\\node\\test\\fixtures\\permission\\deny\\protected-file.md")
7272
.ToLocalChecked());
73-
ToNamespacedPath(*env, &data_3);
73+
ToNamespacedPath(*env, &data_3, true);
7474
EXPECT_EQ(
7575
data_3.ToStringView(),
7676
"\\\\?\\C:\\workspace\\node-test-binary-windows-js-"
@@ -79,8 +79,67 @@ TEST_F(PathTest, ToNamespacedPath) {
7979
isolate_,
8080
v8::String::NewFromUtf8(isolate_, "\\\\?\\c:\\Windows/System")
8181
.ToLocalChecked());
82-
ToNamespacedPath(*env, &data_4);
82+
ToNamespacedPath(*env, &data_4, true);
8383
EXPECT_EQ(data_4.ToStringView(), "\\\\?\\c:\\Windows\\System");
84+
BufferValue data5(
85+
isolate_,
86+
v8::String::NewFromUtf8(isolate_, "C:\\path\\COM1").ToLocalChecked());
87+
ToNamespacedPath(*env, &data5, true);
88+
EXPECT_EQ(data5.ToStringView(), "\\\\.\\COM1");
89+
BufferValue data6(isolate_,
90+
v8::String::NewFromUtf8(isolate_, "COM1").ToLocalChecked());
91+
ToNamespacedPath(*env, &data6, true);
92+
EXPECT_EQ(data6.ToStringView(), "\\\\.\\COM1");
93+
BufferValue data7(isolate_,
94+
v8::String::NewFromUtf8(isolate_, "LPT1").ToLocalChecked());
95+
ToNamespacedPath(*env, &data7, true);
96+
EXPECT_EQ(data7.ToStringView(), "\\\\.\\LPT1");
97+
BufferValue data8(
98+
isolate_, v8::String::NewFromUtf8(isolate_, "C:\\LPT1").ToLocalChecked());
99+
ToNamespacedPath(*env, &data8, true);
100+
EXPECT_EQ(data8.ToStringView(), "\\\\.\\LPT1");
101+
BufferValue data9(
102+
isolate_,
103+
v8::String::NewFromUtf8(isolate_, "PhysicalDrive0").ToLocalChecked());
104+
ToNamespacedPath(*env, &data9, true);
105+
EXPECT_EQ(data9.ToStringView(), "\\\\.\\PhysicalDrive0");
106+
BufferValue data10(
107+
isolate_,
108+
v8::String::NewFromUtf8(isolate_, "pipe\\mypipe").ToLocalChecked());
109+
ToNamespacedPath(*env, &data10, true);
110+
EXPECT_EQ(data10.ToStringView(), "\\\\.\\pipe\\mypipe");
111+
BufferValue data11(
112+
isolate_,
113+
v8::String::NewFromUtf8(isolate_, "MAILSLOT\\mySlot").ToLocalChecked());
114+
ToNamespacedPath(*env, &data11, true);
115+
EXPECT_EQ(data11.ToStringView(), "\\\\.\\MAILSLOT\\mySlot");
116+
BufferValue data12(isolate_,
117+
v8::String::NewFromUtf8(isolate_, "NUL").ToLocalChecked());
118+
ToNamespacedPath(*env, &data12, true);
119+
EXPECT_EQ(data12.ToStringView(), "\\\\.\\NUL");
120+
BufferValue data13(
121+
isolate_, v8::String::NewFromUtf8(isolate_, "Tape0").ToLocalChecked());
122+
ToNamespacedPath(*env, &data13, true);
123+
EXPECT_EQ(data13.ToStringView(), "\\\\.\\Tape0");
124+
BufferValue data14(
125+
isolate_, v8::String::NewFromUtf8(isolate_, "Changer0").ToLocalChecked());
126+
ToNamespacedPath(*env, &data14, true);
127+
EXPECT_EQ(data14.ToStringView(), "\\\\.\\Changer0");
128+
BufferValue data15(isolate_,
129+
v8::String::NewFromUtf8(isolate_, "\\\\.\\pipe\\somepipe")
130+
.ToLocalChecked());
131+
ToNamespacedPath(*env, &data15, true);
132+
EXPECT_EQ(data15.ToStringView(), "\\\\.\\pipe\\somepipe");
133+
BufferValue data16(
134+
isolate_,
135+
v8::String::NewFromUtf8(isolate_, "\\\\.\\COM1").ToLocalChecked());
136+
ToNamespacedPath(*env, &data16, true);
137+
EXPECT_EQ(data16.ToStringView(), "\\\\.\\COM1");
138+
BufferValue data17(
139+
isolate_,
140+
v8::String::NewFromUtf8(isolate_, "\\\\.\\LPT1").ToLocalChecked());
141+
ToNamespacedPath(*env, &data17, true);
142+
EXPECT_EQ(data17.ToStringView(), "\\\\.\\LPT1");
84143
#else
85144
BufferValue data(
86145
isolate_,

test/parallel/test-path-makelong.js

Lines changed: 27 additions & 1 deletion
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-
assert.strictEqual(path.toNamespacedPath('\\\\.\\pipe\\somepipe'),
42+
// Device name tests
43+
assert.strictEqual(path.toNamespacedPath('C:\\path\\COM1', true),
44+
'\\\\.\\COM1');
45+
assert.strictEqual(path.toNamespacedPath('COM1', true),
46+
'\\\\.\\COM1');
47+
assert.strictEqual(path.toNamespacedPath('LPT1', true),
48+
'\\\\.\\LPT1');
49+
assert.strictEqual(path.toNamespacedPath('C:\\LPT1', true),
50+
'\\\\.\\LPT1');
51+
assert.strictEqual(path.toNamespacedPath('PhysicalDrive0', true),
52+
'\\\\.\\PhysicalDrive0');
53+
assert.strictEqual(path.toNamespacedPath('pipe\\mypipe', true),
54+
'\\\\.\\pipe\\mypipe');
55+
assert.strictEqual(path.toNamespacedPath('MAILSLOT\\mySlot', true),
56+
'\\\\.\\MAILSLOT\\mySlot');
57+
assert.strictEqual(path.toNamespacedPath('NUL', true),
58+
'\\\\.\\NUL');
59+
assert.strictEqual(path.toNamespacedPath('Tape0', true),
60+
'\\\\.\\Tape0');
61+
assert.strictEqual(path.toNamespacedPath('Changer0', true),
62+
'\\\\.\\Changer0');
63+
// Test cases for inputs with "\\.\" prefix
64+
assert.strictEqual(path.toNamespacedPath('\\\\.\\pipe\\somepipe', true),
4365
'\\\\.\\pipe\\somepipe');
66+
assert.strictEqual(path.toNamespacedPath('\\\\.\\COM1', true),
67+
'\\\\.\\COM1');
68+
assert.strictEqual(path.toNamespacedPath('\\\\.\\LPT1', true),
69+
'\\\\.\\LPT1');
4470
}
4571

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

0 commit comments

Comments
 (0)