diff --git a/lib/path.js b/lib/path.js index eba07f376ad0f9..ceca700065e43f 100644 --- a/lib/path.js +++ b/lib/path.js @@ -180,6 +180,29 @@ function glob(path, pattern, windows) { }); } +// Regular expressions to identify special device names in Windows. +// COM to AUX (e.g., COM1, LPT1, NUL, CON, CONIN$, PRN, AUX) are reserved OS device names. +// therefore, Paths like C:\path\to\COM1 map to \\.\COM1, referencing hardware or system streams. +// Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation +// +// PhysicalDrive to Changer (e.g., PhysicalDrive1, TAPE0, Changer0) are not reserved OS device names. +// Ref: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea +const windowDevicePatterns = [ + /([\\/])?(COM\d+)$/i, + /([\\/])?(LPT\d+)$/i, + /([\\/])?(NUL)$/i, + /([\\/])?(CON)$/i, + /([\\/])?(PRN)$/i, + /([\\/])?(AUX)$/i, + /([\\/])?(CONIN\$)$/i, + /([\\/])?(CONOUT\$)$/i, + /^(PHYSICALDRIVE\d+)$/i, + /^(PIPE\\.+)$/i, + /^(MAILSLOT\\.+)$/i, + /^(TAPE\d+)$/i, + /^(CHANGER\d+)$/i, +]; + const win32 = { /** * path.resolve([from ...], to) @@ -687,6 +710,19 @@ const win32 = { if (typeof path !== 'string' || path.length === 0) return path; + // Check if the path matches any device pattern + if (windowDevicePatterns.some((pattern) => pattern.test(path))) { + let deviceName; + if (/^(PIPE\\.+)$/i.test(path) || /^(MAILSLOT\\.+)$/i.test(path)) { + // If the path starts with PIPE\ or MAILSLOT\, keep it as is + deviceName = path; + } else { + // Extract the last component after the last slash or backslash + deviceName = path.split(/[\\/]/).pop(); + } + return `\\\\.\\${deviceName}`; + } + const resolvedPath = win32.resolve(path); if (resolvedPath.length <= 2) diff --git a/src/path.cc b/src/path.cc index fade21c8af9414..4ebcec0a7fb62d 100644 --- a/src/path.cc +++ b/src/path.cc @@ -269,6 +269,43 @@ std::string PathResolve(Environment* env, void ToNamespacedPath(Environment* env, BufferValue* path) { #ifdef _WIN32 if (path->length() == 0) return; + + const std::vector windowDevicePatterns = { + std::regex(R"((.*[\\/])?COM\d+$)", std::regex_constants::icase), + std::regex(R"((.*[\\/])?LPT\d+$)", std::regex_constants::icase), + std::regex(R"((.*[\\/])?NUL$)", std::regex_constants::icase), + std::regex(R"((.*[\\/])?CON$)", std::regex_constants::icase), + std::regex(R"((.*[\\/])?PRN$)", std::regex_constants::icase), + std::regex(R"((.*[\\/])?AUX$)", std::regex_constants::icase), + std::regex(R"((.*[\\/])?CONIN\$$)", std::regex_constants::icase), + std::regex(R"((.*[\\/])?CONOUT\$$)", std::regex_constants::icase), + std::regex(R"(^PHYSICALDRIVE\d+$)", std::regex_constants::icase), + std::regex(R"(^(PIPE\\.+)$)", std::regex_constants::icase), + std::regex(R"(^(MAILSLOT\\.+)$)", std::regex_constants::icase), + std::regex(R"(^TAPE\d+$)", std::regex_constants::icase), + std::regex(R"(^CHANGER\d+$)", std::regex_constants::icase)}; + + std::string path_str(path->ToStringView()); + for (const std::regex& pattern : windowDevicePatterns) { + if (std::regex_match(path_str, pattern)) { + std::string deviceName; + if (std::regex_match(path_str, + std::regex(R"(^(PIPE\\.+|MAILSLOT\\.+)$)", + std::regex_constants::icase))) { + deviceName = path_str; + } else { + size_t pos = path_str.find_last_of("\\/"); + deviceName = + (pos != std::string::npos) ? path_str.substr(pos + 1) : path_str; + } + std::string new_path = "\\\\.\\" + deviceName; + path->AllocateSufficientStorage(new_path.size() + 1); + path->SetLength(new_path.size()); + memcpy(path->out(), new_path.c_str(), new_path.size() + 1); + return; + } + } + std::string resolved_path = node::PathResolve(env, {path->ToStringView()}); if (resolved_path.size() <= 2) { return; diff --git a/test/cctest/test_path.cc b/test/cctest/test_path.cc index 16bd9872f3b035..d7c96b398a4ba3 100644 --- a/test/cctest/test_path.cc +++ b/test/cctest/test_path.cc @@ -81,6 +81,65 @@ TEST_F(PathTest, ToNamespacedPath) { .ToLocalChecked()); ToNamespacedPath(*env, &data_4); EXPECT_EQ(data_4.ToStringView(), "\\\\?\\c:\\Windows\\System"); + BufferValue data5( + isolate_, + v8::String::NewFromUtf8(isolate_, "C:\\path\\COM1").ToLocalChecked()); + ToNamespacedPath(*env, &data5); + EXPECT_EQ(data5.ToStringView(), "\\\\.\\COM1"); + BufferValue data6(isolate_, + v8::String::NewFromUtf8(isolate_, "COM1").ToLocalChecked()); + ToNamespacedPath(*env, &data6); + EXPECT_EQ(data6.ToStringView(), "\\\\.\\COM1"); + BufferValue data7(isolate_, + v8::String::NewFromUtf8(isolate_, "LPT1").ToLocalChecked()); + ToNamespacedPath(*env, &data7); + EXPECT_EQ(data7.ToStringView(), "\\\\.\\LPT1"); + BufferValue data8( + isolate_, v8::String::NewFromUtf8(isolate_, "C:\\LPT1").ToLocalChecked()); + ToNamespacedPath(*env, &data8); + EXPECT_EQ(data8.ToStringView(), "\\\\.\\LPT1"); + BufferValue data9( + isolate_, + v8::String::NewFromUtf8(isolate_, "PhysicalDrive0").ToLocalChecked()); + ToNamespacedPath(*env, &data9); + EXPECT_EQ(data9.ToStringView(), "\\\\.\\PhysicalDrive0"); + BufferValue data10( + isolate_, + v8::String::NewFromUtf8(isolate_, "pipe\\mypipe").ToLocalChecked()); + ToNamespacedPath(*env, &data10); + EXPECT_EQ(data10.ToStringView(), "\\\\.\\pipe\\mypipe"); + BufferValue data11( + isolate_, + v8::String::NewFromUtf8(isolate_, "MAILSLOT\\mySlot").ToLocalChecked()); + ToNamespacedPath(*env, &data11); + EXPECT_EQ(data11.ToStringView(), "\\\\.\\MAILSLOT\\mySlot"); + BufferValue data12(isolate_, + v8::String::NewFromUtf8(isolate_, "NUL").ToLocalChecked()); + ToNamespacedPath(*env, &data12); + EXPECT_EQ(data12.ToStringView(), "\\\\.\\NUL"); + BufferValue data13( + isolate_, v8::String::NewFromUtf8(isolate_, "Tape0").ToLocalChecked()); + ToNamespacedPath(*env, &data13); + EXPECT_EQ(data13.ToStringView(), "\\\\.\\Tape0"); + BufferValue data14( + isolate_, v8::String::NewFromUtf8(isolate_, "Changer0").ToLocalChecked()); + ToNamespacedPath(*env, &data14); + EXPECT_EQ(data14.ToStringView(), "\\\\.\\Changer0"); + BufferValue data15(isolate_, + v8::String::NewFromUtf8(isolate_, "\\\\.\\pipe\\somepipe") + .ToLocalChecked()); + ToNamespacedPath(*env, &data15); + EXPECT_EQ(data15.ToStringView(), "\\\\.\\pipe\\somepipe"); + BufferValue data16( + isolate_, + v8::String::NewFromUtf8(isolate_, "\\\\.\\COM1").ToLocalChecked()); + ToNamespacedPath(*env, &data16); + EXPECT_EQ(data16.ToStringView(), "\\\\.\\COM1"); + BufferValue data17( + isolate_, + v8::String::NewFromUtf8(isolate_, "\\\\.\\LPT1").ToLocalChecked()); + ToNamespacedPath(*env, &data17); + EXPECT_EQ(data17.ToStringView(), "\\\\.\\LPT1"); #else BufferValue data( isolate_, diff --git a/test/parallel/test-path-makelong.js b/test/parallel/test-path-makelong.js index 7a4783953c8fde..4486986417d32c 100644 --- a/test/parallel/test-path-makelong.js +++ b/test/parallel/test-path-makelong.js @@ -39,8 +39,34 @@ if (common.isWindows) { assert.strictEqual(path.toNamespacedPath( '\\\\?\\UNC\\someserver\\someshare\\somefile'), '\\\\?\\UNC\\someserver\\someshare\\somefile'); + // Device name tests + assert.strictEqual(path.toNamespacedPath('C:\\path\\COM1'), + '\\\\.\\COM1'); + assert.strictEqual(path.toNamespacedPath('COM1'), + '\\\\.\\COM1'); + assert.strictEqual(path.toNamespacedPath('LPT1'), + '\\\\.\\LPT1'); + assert.strictEqual(path.toNamespacedPath('C:\\LPT1'), + '\\\\.\\LPT1'); + assert.strictEqual(path.toNamespacedPath('PhysicalDrive0'), + '\\\\.\\PhysicalDrive0'); + assert.strictEqual(path.toNamespacedPath('pipe\\mypipe'), + '\\\\.\\pipe\\mypipe'); + assert.strictEqual(path.toNamespacedPath('MAILSLOT\\mySlot'), + '\\\\.\\MAILSLOT\\mySlot'); + assert.strictEqual(path.toNamespacedPath('NUL'), + '\\\\.\\NUL'); + assert.strictEqual(path.toNamespacedPath('Tape0'), + '\\\\.\\Tape0'); + assert.strictEqual(path.toNamespacedPath('Changer0'), + '\\\\.\\Changer0'); + // Test cases for inputs with "\\.\" prefix assert.strictEqual(path.toNamespacedPath('\\\\.\\pipe\\somepipe'), '\\\\.\\pipe\\somepipe'); + assert.strictEqual(path.toNamespacedPath('\\\\.\\COM1'), + '\\\\.\\COM1'); + assert.strictEqual(path.toNamespacedPath('\\\\.\\LPT1'), + '\\\\.\\LPT1'); } assert.strictEqual(path.toNamespacedPath(''), '');