Skip to content

Commit

Permalink
path: update win32 toNamespacedPath to support device namespace paths
Browse files Browse the repository at this point in the history
  • Loading branch information
EarlyRiser42 committed Sep 26, 2024
1 parent 880c446 commit e04d346
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 8 deletions.
4 changes: 4 additions & 0 deletions doc/api/path.md
Original file line number Diff line number Diff line change
Expand Up @@ -619,12 +619,16 @@ added: v9.0.0
-->

* `path` {string}
* `convertToDevicePath`: {boolean}
* Returns: {string}

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

To convert the `path` to a device namespacedPath,
set `convertToDevicePath` to true; by default, this option is set to false.

This method is meaningful only on Windows systems. On POSIX systems, the
method is non-operational and always returns `path` without modifications.

Expand Down
40 changes: 39 additions & 1 deletion lib/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 windowsDevicePatterns = [
/([\\/])?(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)
Expand Down Expand Up @@ -680,13 +703,28 @@ const win32 = {

/**
* @param {string} path
* @param {boolean} convertToDevicePath
* @returns {string}
*/
toNamespacedPath(path) {
toNamespacedPath(path, convertToDevicePath = false) {
// Note: this will *probably* throw somewhere.
if (typeof path !== 'string' || path.length === 0)
return path;

// Only check for Windows device path patterns if conversion is needed.
// This avoids conflicts with file creation (e.g., mkfile).
if (convertToDevicePath && windowsDevicePatterns.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)
Expand Down
45 changes: 44 additions & 1 deletion src/path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,52 @@ std::string PathResolve(Environment* env,
}
#endif // _WIN32

void ToNamespacedPath(Environment* env, BufferValue* path) {
void ToNamespacedPath(Environment* env,
BufferValue* path,
bool convertToDevicePath) {
#ifdef _WIN32
if (path->length() == 0) return;

static const std::vector<std::regex> windowsDevicePatterns = {
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)};

// Only check for Windows device path patterns if conversion is needed.
// This avoids conflicts with file creation (e.g., mkfile).
if (convertToDevicePath) {
std::string path_str(path->ToStringView());
for (const std::regex& pattern : windowsDevicePatterns) {
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;
Expand Down
4 changes: 3 additions & 1 deletion src/path.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ std::string PathResolve(Environment* env,
constexpr bool IsWindowsDeviceRoot(const char c) noexcept;
#endif // _WIN32

void ToNamespacedPath(Environment* env, BufferValue* path);
void ToNamespacedPath(Environment* env,
BufferValue* path,
bool convertToDevicePath = false);
void FromNamespacedPath(std::string* path);

} // namespace node
Expand Down
67 changes: 63 additions & 4 deletions test/cctest/test_path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ TEST_F(PathTest, ToNamespacedPath) {
#ifdef _WIN32
BufferValue data(isolate_,
v8::String::NewFromUtf8(isolate_, "").ToLocalChecked());
ToNamespacedPath(*env, &data);
ToNamespacedPath(*env, &data, true);
EXPECT_EQ(data.ToStringView(), ""); // Empty string should not be mutated
BufferValue data_2(
isolate_, v8::String::NewFromUtf8(isolate_, "C://").ToLocalChecked());
ToNamespacedPath(*env, &data_2);
ToNamespacedPath(*env, &data_2, true);
EXPECT_EQ(data_2.ToStringView(), "\\\\?\\C:\\");
BufferValue data_3(
isolate_,
Expand All @@ -70,7 +70,7 @@ TEST_F(PathTest, ToNamespacedPath) {
"C:\\workspace\\node-test-binary-windows-js-"
"suites\\node\\test\\fixtures\\permission\\deny\\protected-file.md")
.ToLocalChecked());
ToNamespacedPath(*env, &data_3);
ToNamespacedPath(*env, &data_3, true);
EXPECT_EQ(
data_3.ToStringView(),
"\\\\?\\C:\\workspace\\node-test-binary-windows-js-"
Expand All @@ -79,8 +79,67 @@ TEST_F(PathTest, ToNamespacedPath) {
isolate_,
v8::String::NewFromUtf8(isolate_, "\\\\?\\c:\\Windows/System")
.ToLocalChecked());
ToNamespacedPath(*env, &data_4);
ToNamespacedPath(*env, &data_4, true);
EXPECT_EQ(data_4.ToStringView(), "\\\\?\\c:\\Windows\\System");
BufferValue data5(
isolate_,
v8::String::NewFromUtf8(isolate_, "C:\\path\\COM1").ToLocalChecked());
ToNamespacedPath(*env, &data5, true);
EXPECT_EQ(data5.ToStringView(), "\\\\.\\COM1");
BufferValue data6(isolate_,
v8::String::NewFromUtf8(isolate_, "COM1").ToLocalChecked());
ToNamespacedPath(*env, &data6, true);
EXPECT_EQ(data6.ToStringView(), "\\\\.\\COM1");
BufferValue data7(isolate_,
v8::String::NewFromUtf8(isolate_, "LPT1").ToLocalChecked());
ToNamespacedPath(*env, &data7, true);
EXPECT_EQ(data7.ToStringView(), "\\\\.\\LPT1");
BufferValue data8(
isolate_, v8::String::NewFromUtf8(isolate_, "C:\\LPT1").ToLocalChecked());
ToNamespacedPath(*env, &data8, true);
EXPECT_EQ(data8.ToStringView(), "\\\\.\\LPT1");
BufferValue data9(
isolate_,
v8::String::NewFromUtf8(isolate_, "PhysicalDrive0").ToLocalChecked());
ToNamespacedPath(*env, &data9, true);
EXPECT_EQ(data9.ToStringView(), "\\\\.\\PhysicalDrive0");
BufferValue data10(
isolate_,
v8::String::NewFromUtf8(isolate_, "pipe\\mypipe").ToLocalChecked());
ToNamespacedPath(*env, &data10, true);
EXPECT_EQ(data10.ToStringView(), "\\\\.\\pipe\\mypipe");
BufferValue data11(
isolate_,
v8::String::NewFromUtf8(isolate_, "MAILSLOT\\mySlot").ToLocalChecked());
ToNamespacedPath(*env, &data11, true);
EXPECT_EQ(data11.ToStringView(), "\\\\.\\MAILSLOT\\mySlot");
BufferValue data12(isolate_,
v8::String::NewFromUtf8(isolate_, "NUL").ToLocalChecked());
ToNamespacedPath(*env, &data12, true);
EXPECT_EQ(data12.ToStringView(), "\\\\.\\NUL");
BufferValue data13(
isolate_, v8::String::NewFromUtf8(isolate_, "Tape0").ToLocalChecked());
ToNamespacedPath(*env, &data13, true);
EXPECT_EQ(data13.ToStringView(), "\\\\.\\Tape0");
BufferValue data14(
isolate_, v8::String::NewFromUtf8(isolate_, "Changer0").ToLocalChecked());
ToNamespacedPath(*env, &data14, true);
EXPECT_EQ(data14.ToStringView(), "\\\\.\\Changer0");
BufferValue data15(isolate_,
v8::String::NewFromUtf8(isolate_, "\\\\.\\pipe\\somepipe")
.ToLocalChecked());
ToNamespacedPath(*env, &data15, true);
EXPECT_EQ(data15.ToStringView(), "\\\\.\\pipe\\somepipe");
BufferValue data16(
isolate_,
v8::String::NewFromUtf8(isolate_, "\\\\.\\COM1").ToLocalChecked());
ToNamespacedPath(*env, &data16, true);
EXPECT_EQ(data16.ToStringView(), "\\\\.\\COM1");
BufferValue data17(
isolate_,
v8::String::NewFromUtf8(isolate_, "\\\\.\\LPT1").ToLocalChecked());
ToNamespacedPath(*env, &data17, true);
EXPECT_EQ(data17.ToStringView(), "\\\\.\\LPT1");
#else
BufferValue data(
isolate_,
Expand Down
28 changes: 27 additions & 1 deletion test/parallel/test-path-makelong.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,34 @@ if (common.isWindows) {
assert.strictEqual(path.toNamespacedPath(
'\\\\?\\UNC\\someserver\\someshare\\somefile'),
'\\\\?\\UNC\\someserver\\someshare\\somefile');
assert.strictEqual(path.toNamespacedPath('\\\\.\\pipe\\somepipe'),
// Device name tests
assert.strictEqual(path.toNamespacedPath('C:\\path\\COM1', true),
'\\\\.\\COM1');
assert.strictEqual(path.toNamespacedPath('COM1', true),
'\\\\.\\COM1');
assert.strictEqual(path.toNamespacedPath('LPT1', true),
'\\\\.\\LPT1');
assert.strictEqual(path.toNamespacedPath('C:\\LPT1', true),
'\\\\.\\LPT1');
assert.strictEqual(path.toNamespacedPath('PhysicalDrive0', true),
'\\\\.\\PhysicalDrive0');
assert.strictEqual(path.toNamespacedPath('pipe\\mypipe', true),
'\\\\.\\pipe\\mypipe');
assert.strictEqual(path.toNamespacedPath('MAILSLOT\\mySlot', true),
'\\\\.\\MAILSLOT\\mySlot');
assert.strictEqual(path.toNamespacedPath('NUL', true),
'\\\\.\\NUL');
assert.strictEqual(path.toNamespacedPath('Tape0', true),
'\\\\.\\Tape0');
assert.strictEqual(path.toNamespacedPath('Changer0', true),
'\\\\.\\Changer0');
// Test cases for inputs with "\\.\" prefix
assert.strictEqual(path.toNamespacedPath('\\\\.\\pipe\\somepipe', true),
'\\\\.\\pipe\\somepipe');
assert.strictEqual(path.toNamespacedPath('\\\\.\\COM1', true),
'\\\\.\\COM1');
assert.strictEqual(path.toNamespacedPath('\\\\.\\LPT1', true),
'\\\\.\\LPT1');
}

assert.strictEqual(path.toNamespacedPath(''), '');
Expand Down

0 comments on commit e04d346

Please sign in to comment.