Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/api/fs.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2944,6 +2944,9 @@ behavior is similar to `cp dir1/ dir2/`.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v16.10.0
pr-url: https://github.com/nodejs/node/pull/40013
description: The `fs` option does not need `open` method if an `fd` was provided.
Expand DownExpand Up@@ -3000,6 +3003,8 @@ changes:
* `highWaterMark` {integer} **Default:** `64 * 1024`
* `fs` {Object|null} **Default:** `null`
* `signal` {AbortSignal|null} **Default:** `null`
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.ReadStream}

`options` can include `start` and `end` values to read a range of bytes from
Expand All@@ -3020,6 +3025,12 @@ If `fd` points to a character device that only supports blocking reads
available. This can prevent the process from exiting and the stream from
closing naturally.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand DownExpand Up@@ -3070,6 +3081,9 @@ If `options` is a string, then it specifies the encoding.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v22.0.0
pr-url: https://github.com/nodejs/node/pull/52037
description: bump default highWaterMark.
Expand DownExpand Up@@ -3134,6 +3148,8 @@ changes:
[`stream.getDefaultHighWaterMark()`][].
* `flush` {boolean} If `true`, the underlying file descriptor is flushed
prior to closing it. **Default:** `false`.
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.WriteStream}

`options` may also include a `start` option to allow writing data at some
Expand All@@ -3148,6 +3164,12 @@ then the file descriptor won't be closed, even if there's an error.
It is the application's responsibility to close it and make sure there's no
file descriptor leak.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand Down
38 changes: 36 additions & 2 deletions lib/internal/fs/streams.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,13 +13,17 @@ const {
} = primordials;

const {
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
ERR_INCOMPATIBLE_OPTION_PAIR,
ERR_INVALID_ARG_TYPE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_MISSING_OPTION,
ERR_OUT_OF_RANGE,
ERR_STREAM_DESTROYED,
ERR_SYSTEM_ERROR,
} = require('internal/errors').codes;
const {
isWindows,
kEmptyObject,
} = require('internal/util');
const {
Expand All@@ -40,6 +44,8 @@ const {
} = require('internal/fs/utils');
const { Readable, Writable, finished } = require('stream');
const { toPathIfFileURL } = require('internal/url');
const binding = internalBinding('fs');
const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs;
const kIoDone = Symbol('kIoDone');
const kIsPerformingIO = Symbol('kIsPerformingIO');

Expand DownExpand Up@@ -160,6 +166,26 @@ function importFd(stream, options) {
['number', 'FileHandle'], options.fd);
}

function importWindowsHandle(stream, options, flags) {
if (options.windowsHandle == null) {
throw new ERR_MISSING_OPTION('options.windowsHandle');
}
if (!isWindows) {
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('windowsHandle');
}
if (options.fs) {
// The HANDLE is wrapped using the real filesystem, so a custom fs
// implementation cannot be combined with it.
throw new ERR_METHOD_NOT_IMPLEMENTED('windowsHandle with fs');
}
if (typeof options.windowsHandle !== 'bigint') {
throw new ERR_INVALID_ARG_TYPE('options.windowsHandle', 'bigint',
options.windowsHandle);
}
stream[kFs] = fs;
return binding.handleToFd(options.windowsHandle, flags);
}

function ReadStream(path, options) {
if (!(this instanceof ReadStream))
return new ReadStream(path, options);
Expand All@@ -173,7 +199,11 @@ function ReadStream(path, options) {
options.autoDestroy = false;
}

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand DownExpand Up@@ -325,7 +355,11 @@ function WriteStream(path, options) {
// Only buffers are supported.
options.decodeStrings = true;

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand Down
34 changes: 34 additions & 0 deletions src/node_file.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -4150,6 +4150,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) {
return info;
}

#ifdef _WIN32
static void HandleToFd(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 1);
CHECK(args[0]->IsBigInt());

int flags = 0;
if (args[1]->IsNumber()) {
flags = args[1].As<Int32>()->Value();
}

bool lossless;
int64_t handle = args[0].As<BigInt>()->Int64Value(&lossless);
if (!lossless) {
return THROW_ERR_OUT_OF_RANGE(env,
"windowsHandle does not fit into 64 bits");
}
intptr_t value = static_cast<intptr_t>(handle);

int fd = _open_osfhandle(value, flags);
if (fd == -1) {
return env->ThrowErrnoException(errno, "_open_osfhandle");
}
args.GetReturnValue().Set(fd);
}
#endif // _WIN32

void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Expand DownExpand Up@@ -4216,6 +4243,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,

SetMethod(isolate, target, "mkdtemp", Mkdtemp);

#ifdef _WIN32
SetMethod(isolate, target, "handleToFd", HandleToFd);
#endif

SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);
SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);
SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);
Expand DownExpand Up@@ -4343,6 +4374,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(LUTimes);

registry->Register(Mkdtemp);
#ifdef _WIN32
registry->Register(HandleToFd);
#endif
registry->Register(NewFSReqCallback);

registry->Register(FileHandle::New);
Expand Down
62 changes: 62 additions & 0 deletions test/addons/fs-windows-handle/binding.cc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
#include <node.h>
#include <v8.h>

#ifdef _WIN32
#include <windows.h>
#endif

namespace {

using v8::BigInt;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

// Creates an anonymous pipe and returns its raw Win32 read/write HANDLE values
// as JS bigints. These are NOT CRT file descriptors, so passing them as the
// `windowsHandle` stream option exercises the HANDLE -> fd conversion path on
// Windows. Returns undefined on other platforms.
void CreatePipeHandles(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
#ifdef _WIN32
Local<Context> context = isolate->GetCurrentContext();

HANDLE read_handle = nullptr;
HANDLE write_handle = nullptr;
if (!CreatePipe(&read_handle, &write_handle, nullptr, 0)) {
isolate->ThrowException(v8::Exception::Error(
String::NewFromUtf8(isolate, "CreatePipe failed").ToLocalChecked()));
return;
}

Local<Object> result = Object::New(isolate);
result
->Set(context,
String::NewFromUtf8(isolate, "readHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(read_handle))))
.Check();
result
->Set(context,
String::NewFromUtf8(isolate, "writeHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(write_handle))))
.Check();
args.GetReturnValue().Set(result);
#else
args.GetReturnValue().SetUndefined();
#endif
}

} // anonymous namespace

extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(
Local<Object> exports, Local<Value> module, Local<Context> context) {
NODE_SET_METHOD(exports, "createPipeHandles", CreatePipeHandles);
}
9 changes: 9 additions & 0 deletions test/addons/fs-windows-handle/binding.gyp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
},
]
}
35 changes: 35 additions & 0 deletions test/addons/fs-windows-handle/test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
'use strict';
// Verifies that fs.createReadStream()/createWriteStream() accept a raw Win32
// HANDLE through the `windowsHandle` option, as happens when a parent process
// passes an inherited anonymous pipe handle. The addon produces such handles
// via CreatePipe(); Node must wrap them in CRT file descriptors instead of
// failing with EBADF.

const common = require('../../common');

if (!common.isWindows) {
common.skip('windowsHandle is Windows-only');
}

const assert = require('assert');
const fs = require('fs');

const binding = require(`./build/${common.buildType}/binding`);

const { readHandle, writeHandle } = binding.createPipeHandles();
assert.strictEqual(typeof readHandle, 'bigint');
assert.strictEqual(typeof writeHandle, 'bigint');

const payload = 'payload';

const chunks = [];
const rs = fs.createReadStream(null, { windowsHandle: readHandle });
rs.on('error', (err) => assert.fail(err));
rs.on('data', (chunk) => chunks.push(chunk));
rs.on('end', common.mustCall(() => {
assert.strictEqual(Buffer.concat(chunks).toString(), payload);
}));

const ws = fs.createWriteStream(null, { windowsHandle: writeHandle });
ws.on('error', (err) => assert.fail(err));
ws.end(payload);
47 changes: 47 additions & 0 deletions test/parallel/test-fs-stream-windows-handle.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
'use strict';

// Tests option validation for the `windowsHandle` option of
// fs.createReadStream()/createWriteStream(). The functional round-trip on
// Windows (where a real Win32 HANDLE is wrapped in a file descriptor) is
// covered by test/addons/fs-windows-handle.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');

const handle = 1n;

for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle, fd: 2 }), {
code: 'ERR_INCOMPATIBLE_OPTION_PAIR',
});
}

if (!common.isWindows) {
for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle }), {
code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
});
}
return;
}

for (const create of [fs.createReadStream, fs.createWriteStream]) {
// Cannot be combined with a custom `fs` implementation.
assert.throws(() => create(null, { windowsHandle: handle, fs: {} }), {
code: 'ERR_METHOD_NOT_IMPLEMENTED',
});

// Must be a bigint.
assert.throws(() => create(null, { windowsHandle: 'nope' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => create(null, { windowsHandle: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});

// Must fit into 64 bits.
assert.throws(() => create(null, { windowsHandle: 2n ** 64n }), {
code: 'ERR_OUT_OF_RANGE',
});
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/api/fs.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2944,6 +2944,9 @@ behavior is similar to `cp dir1/ dir2/`.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v16.10.0
pr-url: https://github.com/nodejs/node/pull/40013
description: The `fs` option does not need `open` method if an `fd` was provided.
Expand DownExpand Up@@ -3000,6 +3003,8 @@ changes:
* `highWaterMark` {integer} **Default:** `64 * 1024`
* `fs` {Object|null} **Default:** `null`
* `signal` {AbortSignal|null} **Default:** `null`
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.ReadStream}

`options` can include `start` and `end` values to read a range of bytes from
Expand All@@ -3020,6 +3025,12 @@ If `fd` points to a character device that only supports blocking reads
available. This can prevent the process from exiting and the stream from
closing naturally.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand DownExpand Up@@ -3070,6 +3081,9 @@ If `options` is a string, then it specifies the encoding.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v22.0.0
pr-url: https://github.com/nodejs/node/pull/52037
description: bump default highWaterMark.
Expand DownExpand Up@@ -3134,6 +3148,8 @@ changes:
[`stream.getDefaultHighWaterMark()`][].
* `flush` {boolean} If `true`, the underlying file descriptor is flushed
prior to closing it. **Default:** `false`.
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.WriteStream}

`options` may also include a `start` option to allow writing data at some
Expand All@@ -3148,6 +3164,12 @@ then the file descriptor won't be closed, even if there's an error.
It is the application's responsibility to close it and make sure there's no
file descriptor leak.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand Down
38 changes: 36 additions & 2 deletions lib/internal/fs/streams.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,13 +13,17 @@ const {
} = primordials;

const {
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
ERR_INCOMPATIBLE_OPTION_PAIR,
ERR_INVALID_ARG_TYPE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_MISSING_OPTION,
ERR_OUT_OF_RANGE,
ERR_STREAM_DESTROYED,
ERR_SYSTEM_ERROR,
} = require('internal/errors').codes;
const {
isWindows,
kEmptyObject,
} = require('internal/util');
const {
Expand All@@ -40,6 +44,8 @@ const {
} = require('internal/fs/utils');
const { Readable, Writable, finished } = require('stream');
const { toPathIfFileURL } = require('internal/url');
const binding = internalBinding('fs');
const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs;
const kIoDone = Symbol('kIoDone');
const kIsPerformingIO = Symbol('kIsPerformingIO');

Expand DownExpand Up@@ -160,6 +166,26 @@ function importFd(stream, options) {
['number', 'FileHandle'], options.fd);
}

function importWindowsHandle(stream, options, flags) {
if (options.windowsHandle == null) {
throw new ERR_MISSING_OPTION('options.windowsHandle');
}
if (!isWindows) {
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('windowsHandle');
}
if (options.fs) {
// The HANDLE is wrapped using the real filesystem, so a custom fs
// implementation cannot be combined with it.
throw new ERR_METHOD_NOT_IMPLEMENTED('windowsHandle with fs');
}
if (typeof options.windowsHandle !== 'bigint') {
throw new ERR_INVALID_ARG_TYPE('options.windowsHandle', 'bigint',
options.windowsHandle);
}
stream[kFs] = fs;
return binding.handleToFd(options.windowsHandle, flags);
}

function ReadStream(path, options) {
if (!(this instanceof ReadStream))
return new ReadStream(path, options);
Expand All@@ -173,7 +199,11 @@ function ReadStream(path, options) {
options.autoDestroy = false;
}

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand DownExpand Up@@ -325,7 +355,11 @@ function WriteStream(path, options) {
// Only buffers are supported.
options.decodeStrings = true;

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand Down
34 changes: 34 additions & 0 deletions src/node_file.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -4150,6 +4150,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) {
return info;
}

#ifdef _WIN32
static void HandleToFd(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 1);
CHECK(args[0]->IsBigInt());

int flags = 0;
if (args[1]->IsNumber()) {
flags = args[1].As<Int32>()->Value();
}

bool lossless;
int64_t handle = args[0].As<BigInt>()->Int64Value(&lossless);
if (!lossless) {
return THROW_ERR_OUT_OF_RANGE(env,
"windowsHandle does not fit into 64 bits");
}
intptr_t value = static_cast<intptr_t>(handle);

int fd = _open_osfhandle(value, flags);
if (fd == -1) {
return env->ThrowErrnoException(errno, "_open_osfhandle");
}
args.GetReturnValue().Set(fd);
}
#endif // _WIN32

void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Expand DownExpand Up@@ -4216,6 +4243,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,

SetMethod(isolate, target, "mkdtemp", Mkdtemp);

#ifdef _WIN32
SetMethod(isolate, target, "handleToFd", HandleToFd);
#endif

SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);
SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);
SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);
Expand DownExpand Up@@ -4343,6 +4374,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(LUTimes);

registry->Register(Mkdtemp);
#ifdef _WIN32
registry->Register(HandleToFd);
#endif
registry->Register(NewFSReqCallback);

registry->Register(FileHandle::New);
Expand Down
62 changes: 62 additions & 0 deletions test/addons/fs-windows-handle/binding.cc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
#include <node.h>
#include <v8.h>

#ifdef _WIN32
#include <windows.h>
#endif

namespace {

using v8::BigInt;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

// Creates an anonymous pipe and returns its raw Win32 read/write HANDLE values
// as JS bigints. These are NOT CRT file descriptors, so passing them as the
// `windowsHandle` stream option exercises the HANDLE -> fd conversion path on
// Windows. Returns undefined on other platforms.
void CreatePipeHandles(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
#ifdef _WIN32
Local<Context> context = isolate->GetCurrentContext();

HANDLE read_handle = nullptr;
HANDLE write_handle = nullptr;
if (!CreatePipe(&read_handle, &write_handle, nullptr, 0)) {
isolate->ThrowException(v8::Exception::Error(
String::NewFromUtf8(isolate, "CreatePipe failed").ToLocalChecked()));
return;
}

Local<Object> result = Object::New(isolate);
result
->Set(context,
String::NewFromUtf8(isolate, "readHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(read_handle))))
.Check();
result
->Set(context,
String::NewFromUtf8(isolate, "writeHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(write_handle))))
.Check();
args.GetReturnValue().Set(result);
#else
args.GetReturnValue().SetUndefined();
#endif
}

} // anonymous namespace

extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(
Local<Object> exports, Local<Value> module, Local<Context> context) {
NODE_SET_METHOD(exports, "createPipeHandles", CreatePipeHandles);
}
9 changes: 9 additions & 0 deletions test/addons/fs-windows-handle/binding.gyp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
},
]
}
35 changes: 35 additions & 0 deletions test/addons/fs-windows-handle/test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
'use strict';
// Verifies that fs.createReadStream()/createWriteStream() accept a raw Win32
// HANDLE through the `windowsHandle` option, as happens when a parent process
// passes an inherited anonymous pipe handle. The addon produces such handles
// via CreatePipe(); Node must wrap them in CRT file descriptors instead of
// failing with EBADF.

const common = require('../../common');

if (!common.isWindows) {
common.skip('windowsHandle is Windows-only');
}

const assert = require('assert');
const fs = require('fs');

const binding = require(`./build/${common.buildType}/binding`);

const { readHandle, writeHandle } = binding.createPipeHandles();
assert.strictEqual(typeof readHandle, 'bigint');
assert.strictEqual(typeof writeHandle, 'bigint');

const payload = 'payload';

const chunks = [];
const rs = fs.createReadStream(null, { windowsHandle: readHandle });
rs.on('error', (err) => assert.fail(err));
rs.on('data', (chunk) => chunks.push(chunk));
rs.on('end', common.mustCall(() => {
assert.strictEqual(Buffer.concat(chunks).toString(), payload);
}));

const ws = fs.createWriteStream(null, { windowsHandle: writeHandle });
ws.on('error', (err) => assert.fail(err));
ws.end(payload);
47 changes: 47 additions & 0 deletions test/parallel/test-fs-stream-windows-handle.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
'use strict';

// Tests option validation for the `windowsHandle` option of
// fs.createReadStream()/createWriteStream(). The functional round-trip on
// Windows (where a real Win32 HANDLE is wrapped in a file descriptor) is
// covered by test/addons/fs-windows-handle.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');

const handle = 1n;

for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle, fd: 2 }), {
code: 'ERR_INCOMPATIBLE_OPTION_PAIR',
});
}

if (!common.isWindows) {
for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle }), {
code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
});
}
return;
}

for (const create of [fs.createReadStream, fs.createWriteStream]) {
// Cannot be combined with a custom `fs` implementation.
assert.throws(() => create(null, { windowsHandle: handle, fs: {} }), {
code: 'ERR_METHOD_NOT_IMPLEMENTED',
});

// Must be a bigint.
assert.throws(() => create(null, { windowsHandle: 'nope' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => create(null, { windowsHandle: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});

// Must fit into 64 bits.
assert.throws(() => create(null, { windowsHandle: 2n ** 64n }), {
code: 'ERR_OUT_OF_RANGE',
});
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/api/fs.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2944,6 +2944,9 @@ behavior is similar to `cp dir1/ dir2/`.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v16.10.0
pr-url: https://github.com/nodejs/node/pull/40013
description: The `fs` option does not need `open` method if an `fd` was provided.
Expand DownExpand Up@@ -3000,6 +3003,8 @@ changes:
* `highWaterMark` {integer} **Default:** `64 * 1024`
* `fs` {Object|null} **Default:** `null`
* `signal` {AbortSignal|null} **Default:** `null`
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.ReadStream}

`options` can include `start` and `end` values to read a range of bytes from
Expand All@@ -3020,6 +3025,12 @@ If `fd` points to a character device that only supports blocking reads
available. This can prevent the process from exiting and the stream from
closing naturally.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand DownExpand Up@@ -3070,6 +3081,9 @@ If `options` is a string, then it specifies the encoding.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v22.0.0
pr-url: https://github.com/nodejs/node/pull/52037
description: bump default highWaterMark.
Expand DownExpand Up@@ -3134,6 +3148,8 @@ changes:
[`stream.getDefaultHighWaterMark()`][].
* `flush` {boolean} If `true`, the underlying file descriptor is flushed
prior to closing it. **Default:** `false`.
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.WriteStream}

`options` may also include a `start` option to allow writing data at some
Expand All@@ -3148,6 +3164,12 @@ then the file descriptor won't be closed, even if there's an error.
It is the application's responsibility to close it and make sure there's no
file descriptor leak.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand Down
38 changes: 36 additions & 2 deletions lib/internal/fs/streams.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,13 +13,17 @@ const {
} = primordials;

const {
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
ERR_INCOMPATIBLE_OPTION_PAIR,
ERR_INVALID_ARG_TYPE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_MISSING_OPTION,
ERR_OUT_OF_RANGE,
ERR_STREAM_DESTROYED,
ERR_SYSTEM_ERROR,
} = require('internal/errors').codes;
const {
isWindows,
kEmptyObject,
} = require('internal/util');
const {
Expand All@@ -40,6 +44,8 @@ const {
} = require('internal/fs/utils');
const { Readable, Writable, finished } = require('stream');
const { toPathIfFileURL } = require('internal/url');
const binding = internalBinding('fs');
const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs;
const kIoDone = Symbol('kIoDone');
const kIsPerformingIO = Symbol('kIsPerformingIO');

Expand DownExpand Up@@ -160,6 +166,26 @@ function importFd(stream, options) {
['number', 'FileHandle'], options.fd);
}

function importWindowsHandle(stream, options, flags) {
if (options.windowsHandle == null) {
throw new ERR_MISSING_OPTION('options.windowsHandle');
}
if (!isWindows) {
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('windowsHandle');
}
if (options.fs) {
// The HANDLE is wrapped using the real filesystem, so a custom fs
// implementation cannot be combined with it.
throw new ERR_METHOD_NOT_IMPLEMENTED('windowsHandle with fs');
}
if (typeof options.windowsHandle !== 'bigint') {
throw new ERR_INVALID_ARG_TYPE('options.windowsHandle', 'bigint',
options.windowsHandle);
}
stream[kFs] = fs;
return binding.handleToFd(options.windowsHandle, flags);
}

function ReadStream(path, options) {
if (!(this instanceof ReadStream))
return new ReadStream(path, options);
Expand All@@ -173,7 +199,11 @@ function ReadStream(path, options) {
options.autoDestroy = false;
}

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand DownExpand Up@@ -325,7 +355,11 @@ function WriteStream(path, options) {
// Only buffers are supported.
options.decodeStrings = true;

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand Down
34 changes: 34 additions & 0 deletions src/node_file.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -4150,6 +4150,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) {
return info;
}

#ifdef _WIN32
static void HandleToFd(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 1);
CHECK(args[0]->IsBigInt());

int flags = 0;
if (args[1]->IsNumber()) {
flags = args[1].As<Int32>()->Value();
}

bool lossless;
int64_t handle = args[0].As<BigInt>()->Int64Value(&lossless);
if (!lossless) {
return THROW_ERR_OUT_OF_RANGE(env,
"windowsHandle does not fit into 64 bits");
}
intptr_t value = static_cast<intptr_t>(handle);

int fd = _open_osfhandle(value, flags);
if (fd == -1) {
return env->ThrowErrnoException(errno, "_open_osfhandle");
}
args.GetReturnValue().Set(fd);
}
#endif // _WIN32

void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Expand DownExpand Up@@ -4216,6 +4243,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,

SetMethod(isolate, target, "mkdtemp", Mkdtemp);

#ifdef _WIN32
SetMethod(isolate, target, "handleToFd", HandleToFd);
#endif

SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);
SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);
SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);
Expand DownExpand Up@@ -4343,6 +4374,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(LUTimes);

registry->Register(Mkdtemp);
#ifdef _WIN32
registry->Register(HandleToFd);
#endif
registry->Register(NewFSReqCallback);

registry->Register(FileHandle::New);
Expand Down
62 changes: 62 additions & 0 deletions test/addons/fs-windows-handle/binding.cc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
#include <node.h>
#include <v8.h>

#ifdef _WIN32
#include <windows.h>
#endif

namespace {

using v8::BigInt;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

// Creates an anonymous pipe and returns its raw Win32 read/write HANDLE values
// as JS bigints. These are NOT CRT file descriptors, so passing them as the
// `windowsHandle` stream option exercises the HANDLE -> fd conversion path on
// Windows. Returns undefined on other platforms.
void CreatePipeHandles(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
#ifdef _WIN32
Local<Context> context = isolate->GetCurrentContext();

HANDLE read_handle = nullptr;
HANDLE write_handle = nullptr;
if (!CreatePipe(&read_handle, &write_handle, nullptr, 0)) {
isolate->ThrowException(v8::Exception::Error(
String::NewFromUtf8(isolate, "CreatePipe failed").ToLocalChecked()));
return;
}

Local<Object> result = Object::New(isolate);
result
->Set(context,
String::NewFromUtf8(isolate, "readHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(read_handle))))
.Check();
result
->Set(context,
String::NewFromUtf8(isolate, "writeHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(write_handle))))
.Check();
args.GetReturnValue().Set(result);
#else
args.GetReturnValue().SetUndefined();
#endif
}

} // anonymous namespace

extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(
Local<Object> exports, Local<Value> module, Local<Context> context) {
NODE_SET_METHOD(exports, "createPipeHandles", CreatePipeHandles);
}
9 changes: 9 additions & 0 deletions test/addons/fs-windows-handle/binding.gyp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
},
]
}
35 changes: 35 additions & 0 deletions test/addons/fs-windows-handle/test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
'use strict';
// Verifies that fs.createReadStream()/createWriteStream() accept a raw Win32
// HANDLE through the `windowsHandle` option, as happens when a parent process
// passes an inherited anonymous pipe handle. The addon produces such handles
// via CreatePipe(); Node must wrap them in CRT file descriptors instead of
// failing with EBADF.

const common = require('../../common');

if (!common.isWindows) {
common.skip('windowsHandle is Windows-only');
}

const assert = require('assert');
const fs = require('fs');

const binding = require(`./build/${common.buildType}/binding`);

const { readHandle, writeHandle } = binding.createPipeHandles();
assert.strictEqual(typeof readHandle, 'bigint');
assert.strictEqual(typeof writeHandle, 'bigint');

const payload = 'payload';

const chunks = [];
const rs = fs.createReadStream(null, { windowsHandle: readHandle });
rs.on('error', (err) => assert.fail(err));
rs.on('data', (chunk) => chunks.push(chunk));
rs.on('end', common.mustCall(() => {
assert.strictEqual(Buffer.concat(chunks).toString(), payload);
}));

const ws = fs.createWriteStream(null, { windowsHandle: writeHandle });
ws.on('error', (err) => assert.fail(err));
ws.end(payload);
47 changes: 47 additions & 0 deletions test/parallel/test-fs-stream-windows-handle.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
'use strict';

// Tests option validation for the `windowsHandle` option of
// fs.createReadStream()/createWriteStream(). The functional round-trip on
// Windows (where a real Win32 HANDLE is wrapped in a file descriptor) is
// covered by test/addons/fs-windows-handle.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');

const handle = 1n;

for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle, fd: 2 }), {
code: 'ERR_INCOMPATIBLE_OPTION_PAIR',
});
}

if (!common.isWindows) {
for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle }), {
code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
});
}
return;
}

for (const create of [fs.createReadStream, fs.createWriteStream]) {
// Cannot be combined with a custom `fs` implementation.
assert.throws(() => create(null, { windowsHandle: handle, fs: {} }), {
code: 'ERR_METHOD_NOT_IMPLEMENTED',
});

// Must be a bigint.
assert.throws(() => create(null, { windowsHandle: 'nope' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => create(null, { windowsHandle: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});

// Must fit into 64 bits.
assert.throws(() => create(null, { windowsHandle: 2n ** 64n }), {
code: 'ERR_OUT_OF_RANGE',
});
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/api/fs.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2944,6 +2944,9 @@ behavior is similar to `cp dir1/ dir2/`.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v16.10.0
pr-url: https://github.com/nodejs/node/pull/40013
description: The `fs` option does not need `open` method if an `fd` was provided.
Expand DownExpand Up@@ -3000,6 +3003,8 @@ changes:
* `highWaterMark` {integer} **Default:** `64 * 1024`
* `fs` {Object|null} **Default:** `null`
* `signal` {AbortSignal|null} **Default:** `null`
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.ReadStream}

`options` can include `start` and `end` values to read a range of bytes from
Expand All@@ -3020,6 +3025,12 @@ If `fd` points to a character device that only supports blocking reads
available. This can prevent the process from exiting and the stream from
closing naturally.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand DownExpand Up@@ -3070,6 +3081,9 @@ If `options` is a string, then it specifies the encoding.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v22.0.0
pr-url: https://github.com/nodejs/node/pull/52037
description: bump default highWaterMark.
Expand DownExpand Up@@ -3134,6 +3148,8 @@ changes:
[`stream.getDefaultHighWaterMark()`][].
* `flush` {boolean} If `true`, the underlying file descriptor is flushed
prior to closing it. **Default:** `false`.
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.WriteStream}

`options` may also include a `start` option to allow writing data at some
Expand All@@ -3148,6 +3164,12 @@ then the file descriptor won't be closed, even if there's an error.
It is the application's responsibility to close it and make sure there's no
file descriptor leak.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand Down
38 changes: 36 additions & 2 deletions lib/internal/fs/streams.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,13 +13,17 @@ const {
} = primordials;

const {
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
ERR_INCOMPATIBLE_OPTION_PAIR,
ERR_INVALID_ARG_TYPE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_MISSING_OPTION,
ERR_OUT_OF_RANGE,
ERR_STREAM_DESTROYED,
ERR_SYSTEM_ERROR,
} = require('internal/errors').codes;
const {
isWindows,
kEmptyObject,
} = require('internal/util');
const {
Expand All@@ -40,6 +44,8 @@ const {
} = require('internal/fs/utils');
const { Readable, Writable, finished } = require('stream');
const { toPathIfFileURL } = require('internal/url');
const binding = internalBinding('fs');
const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs;
const kIoDone = Symbol('kIoDone');
const kIsPerformingIO = Symbol('kIsPerformingIO');

Expand DownExpand Up@@ -160,6 +166,26 @@ function importFd(stream, options) {
['number', 'FileHandle'], options.fd);
}

function importWindowsHandle(stream, options, flags) {
if (options.windowsHandle == null) {
throw new ERR_MISSING_OPTION('options.windowsHandle');
}
if (!isWindows) {
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('windowsHandle');
}
if (options.fs) {
// The HANDLE is wrapped using the real filesystem, so a custom fs
// implementation cannot be combined with it.
throw new ERR_METHOD_NOT_IMPLEMENTED('windowsHandle with fs');
}
if (typeof options.windowsHandle !== 'bigint') {
throw new ERR_INVALID_ARG_TYPE('options.windowsHandle', 'bigint',
options.windowsHandle);
}
stream[kFs] = fs;
return binding.handleToFd(options.windowsHandle, flags);
}

function ReadStream(path, options) {
if (!(this instanceof ReadStream))
return new ReadStream(path, options);
Expand All@@ -173,7 +199,11 @@ function ReadStream(path, options) {
options.autoDestroy = false;
}

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand DownExpand Up@@ -325,7 +355,11 @@ function WriteStream(path, options) {
// Only buffers are supported.
options.decodeStrings = true;

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand Down
34 changes: 34 additions & 0 deletions src/node_file.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -4150,6 +4150,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) {
return info;
}

#ifdef _WIN32
static void HandleToFd(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 1);
CHECK(args[0]->IsBigInt());

int flags = 0;
if (args[1]->IsNumber()) {
flags = args[1].As<Int32>()->Value();
}

bool lossless;
int64_t handle = args[0].As<BigInt>()->Int64Value(&lossless);
if (!lossless) {
return THROW_ERR_OUT_OF_RANGE(env,
"windowsHandle does not fit into 64 bits");
}
intptr_t value = static_cast<intptr_t>(handle);

int fd = _open_osfhandle(value, flags);
if (fd == -1) {
return env->ThrowErrnoException(errno, "_open_osfhandle");
}
args.GetReturnValue().Set(fd);
}
#endif // _WIN32

void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Expand DownExpand Up@@ -4216,6 +4243,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,

SetMethod(isolate, target, "mkdtemp", Mkdtemp);

#ifdef _WIN32
SetMethod(isolate, target, "handleToFd", HandleToFd);
#endif

SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);
SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);
SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);
Expand DownExpand Up@@ -4343,6 +4374,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(LUTimes);

registry->Register(Mkdtemp);
#ifdef _WIN32
registry->Register(HandleToFd);
#endif
registry->Register(NewFSReqCallback);

registry->Register(FileHandle::New);
Expand Down
62 changes: 62 additions & 0 deletions test/addons/fs-windows-handle/binding.cc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
#include <node.h>
#include <v8.h>

#ifdef _WIN32
#include <windows.h>
#endif

namespace {

using v8::BigInt;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

// Creates an anonymous pipe and returns its raw Win32 read/write HANDLE values
// as JS bigints. These are NOT CRT file descriptors, so passing them as the
// `windowsHandle` stream option exercises the HANDLE -> fd conversion path on
// Windows. Returns undefined on other platforms.
void CreatePipeHandles(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
#ifdef _WIN32
Local<Context> context = isolate->GetCurrentContext();

HANDLE read_handle = nullptr;
HANDLE write_handle = nullptr;
if (!CreatePipe(&read_handle, &write_handle, nullptr, 0)) {
isolate->ThrowException(v8::Exception::Error(
String::NewFromUtf8(isolate, "CreatePipe failed").ToLocalChecked()));
return;
}

Local<Object> result = Object::New(isolate);
result
->Set(context,
String::NewFromUtf8(isolate, "readHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(read_handle))))
.Check();
result
->Set(context,
String::NewFromUtf8(isolate, "writeHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(write_handle))))
.Check();
args.GetReturnValue().Set(result);
#else
args.GetReturnValue().SetUndefined();
#endif
}

} // anonymous namespace

extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(
Local<Object> exports, Local<Value> module, Local<Context> context) {
NODE_SET_METHOD(exports, "createPipeHandles", CreatePipeHandles);
}
9 changes: 9 additions & 0 deletions test/addons/fs-windows-handle/binding.gyp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
},
]
}
35 changes: 35 additions & 0 deletions test/addons/fs-windows-handle/test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
'use strict';
// Verifies that fs.createReadStream()/createWriteStream() accept a raw Win32
// HANDLE through the `windowsHandle` option, as happens when a parent process
// passes an inherited anonymous pipe handle. The addon produces such handles
// via CreatePipe(); Node must wrap them in CRT file descriptors instead of
// failing with EBADF.

const common = require('../../common');

if (!common.isWindows) {
common.skip('windowsHandle is Windows-only');
}

const assert = require('assert');
const fs = require('fs');

const binding = require(`./build/${common.buildType}/binding`);

const { readHandle, writeHandle } = binding.createPipeHandles();
assert.strictEqual(typeof readHandle, 'bigint');
assert.strictEqual(typeof writeHandle, 'bigint');

const payload = 'payload';

const chunks = [];
const rs = fs.createReadStream(null, { windowsHandle: readHandle });
rs.on('error', (err) => assert.fail(err));
rs.on('data', (chunk) => chunks.push(chunk));
rs.on('end', common.mustCall(() => {
assert.strictEqual(Buffer.concat(chunks).toString(), payload);
}));

const ws = fs.createWriteStream(null, { windowsHandle: writeHandle });
ws.on('error', (err) => assert.fail(err));
ws.end(payload);
47 changes: 47 additions & 0 deletions test/parallel/test-fs-stream-windows-handle.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
'use strict';

// Tests option validation for the `windowsHandle` option of
// fs.createReadStream()/createWriteStream(). The functional round-trip on
// Windows (where a real Win32 HANDLE is wrapped in a file descriptor) is
// covered by test/addons/fs-windows-handle.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');

const handle = 1n;

for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle, fd: 2 }), {
code: 'ERR_INCOMPATIBLE_OPTION_PAIR',
});
}

if (!common.isWindows) {
for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle }), {
code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
});
}
return;
}

for (const create of [fs.createReadStream, fs.createWriteStream]) {
// Cannot be combined with a custom `fs` implementation.
assert.throws(() => create(null, { windowsHandle: handle, fs: {} }), {
code: 'ERR_METHOD_NOT_IMPLEMENTED',
});

// Must be a bigint.
assert.throws(() => create(null, { windowsHandle: 'nope' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => create(null, { windowsHandle: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});

// Must fit into 64 bits.
assert.throws(() => create(null, { windowsHandle: 2n ** 64n }), {
code: 'ERR_OUT_OF_RANGE',
});
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/api/fs.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2944,6 +2944,9 @@ behavior is similar to `cp dir1/ dir2/`.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v16.10.0
pr-url: https://github.com/nodejs/node/pull/40013
description: The `fs` option does not need `open` method if an `fd` was provided.
Expand DownExpand Up@@ -3000,6 +3003,8 @@ changes:
* `highWaterMark` {integer} **Default:** `64 * 1024`
* `fs` {Object|null} **Default:** `null`
* `signal` {AbortSignal|null} **Default:** `null`
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.ReadStream}

`options` can include `start` and `end` values to read a range of bytes from
Expand All@@ -3020,6 +3025,12 @@ If `fd` points to a character device that only supports blocking reads
available. This can prevent the process from exiting and the stream from
closing naturally.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand DownExpand Up@@ -3070,6 +3081,9 @@ If `options` is a string, then it specifies the encoding.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v22.0.0
pr-url: https://github.com/nodejs/node/pull/52037
description: bump default highWaterMark.
Expand DownExpand Up@@ -3134,6 +3148,8 @@ changes:
[`stream.getDefaultHighWaterMark()`][].
* `flush` {boolean} If `true`, the underlying file descriptor is flushed
prior to closing it. **Default:** `false`.
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.WriteStream}

`options` may also include a `start` option to allow writing data at some
Expand All@@ -3148,6 +3164,12 @@ then the file descriptor won't be closed, even if there's an error.
It is the application's responsibility to close it and make sure there's no
file descriptor leak.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand Down
38 changes: 36 additions & 2 deletions lib/internal/fs/streams.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,13 +13,17 @@ const {
} = primordials;

const {
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
ERR_INCOMPATIBLE_OPTION_PAIR,
ERR_INVALID_ARG_TYPE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_MISSING_OPTION,
ERR_OUT_OF_RANGE,
ERR_STREAM_DESTROYED,
ERR_SYSTEM_ERROR,
} = require('internal/errors').codes;
const {
isWindows,
kEmptyObject,
} = require('internal/util');
const {
Expand All@@ -40,6 +44,8 @@ const {
} = require('internal/fs/utils');
const { Readable, Writable, finished } = require('stream');
const { toPathIfFileURL } = require('internal/url');
const binding = internalBinding('fs');
const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs;
const kIoDone = Symbol('kIoDone');
const kIsPerformingIO = Symbol('kIsPerformingIO');

Expand DownExpand Up@@ -160,6 +166,26 @@ function importFd(stream, options) {
['number', 'FileHandle'], options.fd);
}

function importWindowsHandle(stream, options, flags) {
if (options.windowsHandle == null) {
throw new ERR_MISSING_OPTION('options.windowsHandle');
}
if (!isWindows) {
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('windowsHandle');
}
if (options.fs) {
// The HANDLE is wrapped using the real filesystem, so a custom fs
// implementation cannot be combined with it.
throw new ERR_METHOD_NOT_IMPLEMENTED('windowsHandle with fs');
}
if (typeof options.windowsHandle !== 'bigint') {
throw new ERR_INVALID_ARG_TYPE('options.windowsHandle', 'bigint',
options.windowsHandle);
}
stream[kFs] = fs;
return binding.handleToFd(options.windowsHandle, flags);
}

function ReadStream(path, options) {
if (!(this instanceof ReadStream))
return new ReadStream(path, options);
Expand All@@ -173,7 +199,11 @@ function ReadStream(path, options) {
options.autoDestroy = false;
}

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand DownExpand Up@@ -325,7 +355,11 @@ function WriteStream(path, options) {
// Only buffers are supported.
options.decodeStrings = true;

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand Down
34 changes: 34 additions & 0 deletions src/node_file.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -4150,6 +4150,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) {
return info;
}

#ifdef _WIN32
static void HandleToFd(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 1);
CHECK(args[0]->IsBigInt());

int flags = 0;
if (args[1]->IsNumber()) {
flags = args[1].As<Int32>()->Value();
}

bool lossless;
int64_t handle = args[0].As<BigInt>()->Int64Value(&lossless);
if (!lossless) {
return THROW_ERR_OUT_OF_RANGE(env,
"windowsHandle does not fit into 64 bits");
}
intptr_t value = static_cast<intptr_t>(handle);

int fd = _open_osfhandle(value, flags);
if (fd == -1) {
return env->ThrowErrnoException(errno, "_open_osfhandle");
}
args.GetReturnValue().Set(fd);
}
#endif // _WIN32

void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Expand DownExpand Up@@ -4216,6 +4243,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,

SetMethod(isolate, target, "mkdtemp", Mkdtemp);

#ifdef _WIN32
SetMethod(isolate, target, "handleToFd", HandleToFd);
#endif

SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);
SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);
SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);
Expand DownExpand Up@@ -4343,6 +4374,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(LUTimes);

registry->Register(Mkdtemp);
#ifdef _WIN32
registry->Register(HandleToFd);
#endif
registry->Register(NewFSReqCallback);

registry->Register(FileHandle::New);
Expand Down
62 changes: 62 additions & 0 deletions test/addons/fs-windows-handle/binding.cc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
#include <node.h>
#include <v8.h>

#ifdef _WIN32
#include <windows.h>
#endif

namespace {

using v8::BigInt;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

// Creates an anonymous pipe and returns its raw Win32 read/write HANDLE values
// as JS bigints. These are NOT CRT file descriptors, so passing them as the
// `windowsHandle` stream option exercises the HANDLE -> fd conversion path on
// Windows. Returns undefined on other platforms.
void CreatePipeHandles(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
#ifdef _WIN32
Local<Context> context = isolate->GetCurrentContext();

HANDLE read_handle = nullptr;
HANDLE write_handle = nullptr;
if (!CreatePipe(&read_handle, &write_handle, nullptr, 0)) {
isolate->ThrowException(v8::Exception::Error(
String::NewFromUtf8(isolate, "CreatePipe failed").ToLocalChecked()));
return;
}

Local<Object> result = Object::New(isolate);
result
->Set(context,
String::NewFromUtf8(isolate, "readHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(read_handle))))
.Check();
result
->Set(context,
String::NewFromUtf8(isolate, "writeHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(write_handle))))
.Check();
args.GetReturnValue().Set(result);
#else
args.GetReturnValue().SetUndefined();
#endif
}

} // anonymous namespace

extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(
Local<Object> exports, Local<Value> module, Local<Context> context) {
NODE_SET_METHOD(exports, "createPipeHandles", CreatePipeHandles);
}
9 changes: 9 additions & 0 deletions test/addons/fs-windows-handle/binding.gyp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
},
]
}
35 changes: 35 additions & 0 deletions test/addons/fs-windows-handle/test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
'use strict';
// Verifies that fs.createReadStream()/createWriteStream() accept a raw Win32
// HANDLE through the `windowsHandle` option, as happens when a parent process
// passes an inherited anonymous pipe handle. The addon produces such handles
// via CreatePipe(); Node must wrap them in CRT file descriptors instead of
// failing with EBADF.

const common = require('../../common');

if (!common.isWindows) {
common.skip('windowsHandle is Windows-only');
}

const assert = require('assert');
const fs = require('fs');

const binding = require(`./build/${common.buildType}/binding`);

const { readHandle, writeHandle } = binding.createPipeHandles();
assert.strictEqual(typeof readHandle, 'bigint');
assert.strictEqual(typeof writeHandle, 'bigint');

const payload = 'payload';

const chunks = [];
const rs = fs.createReadStream(null, { windowsHandle: readHandle });
rs.on('error', (err) => assert.fail(err));
rs.on('data', (chunk) => chunks.push(chunk));
rs.on('end', common.mustCall(() => {
assert.strictEqual(Buffer.concat(chunks).toString(), payload);
}));

const ws = fs.createWriteStream(null, { windowsHandle: writeHandle });
ws.on('error', (err) => assert.fail(err));
ws.end(payload);
47 changes: 47 additions & 0 deletions test/parallel/test-fs-stream-windows-handle.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
'use strict';

// Tests option validation for the `windowsHandle` option of
// fs.createReadStream()/createWriteStream(). The functional round-trip on
// Windows (where a real Win32 HANDLE is wrapped in a file descriptor) is
// covered by test/addons/fs-windows-handle.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');

const handle = 1n;

for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle, fd: 2 }), {
code: 'ERR_INCOMPATIBLE_OPTION_PAIR',
});
}

if (!common.isWindows) {
for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle }), {
code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
});
}
return;
}

for (const create of [fs.createReadStream, fs.createWriteStream]) {
// Cannot be combined with a custom `fs` implementation.
assert.throws(() => create(null, { windowsHandle: handle, fs: {} }), {
code: 'ERR_METHOD_NOT_IMPLEMENTED',
});

// Must be a bigint.
assert.throws(() => create(null, { windowsHandle: 'nope' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => create(null, { windowsHandle: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});

// Must fit into 64 bits.
assert.throws(() => create(null, { windowsHandle: 2n ** 64n }), {
code: 'ERR_OUT_OF_RANGE',
});
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/api/fs.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2944,6 +2944,9 @@ behavior is similar to `cp dir1/ dir2/`.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v16.10.0
pr-url: https://github.com/nodejs/node/pull/40013
description: The `fs` option does not need `open` method if an `fd` was provided.
Expand DownExpand Up@@ -3000,6 +3003,8 @@ changes:
* `highWaterMark` {integer} **Default:** `64 * 1024`
* `fs` {Object|null} **Default:** `null`
* `signal` {AbortSignal|null} **Default:** `null`
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.ReadStream}

`options` can include `start` and `end` values to read a range of bytes from
Expand All@@ -3020,6 +3025,12 @@ If `fd` points to a character device that only supports blocking reads
available. This can prevent the process from exiting and the stream from
closing naturally.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand DownExpand Up@@ -3070,6 +3081,9 @@ If `options` is a string, then it specifies the encoding.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v22.0.0
pr-url: https://github.com/nodejs/node/pull/52037
description: bump default highWaterMark.
Expand DownExpand Up@@ -3134,6 +3148,8 @@ changes:
[`stream.getDefaultHighWaterMark()`][].
* `flush` {boolean} If `true`, the underlying file descriptor is flushed
prior to closing it. **Default:** `false`.
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.WriteStream}

`options` may also include a `start` option to allow writing data at some
Expand All@@ -3148,6 +3164,12 @@ then the file descriptor won't be closed, even if there's an error.
It is the application's responsibility to close it and make sure there's no
file descriptor leak.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand Down
38 changes: 36 additions & 2 deletions lib/internal/fs/streams.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,13 +13,17 @@ const {
} = primordials;

const {
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
ERR_INCOMPATIBLE_OPTION_PAIR,
ERR_INVALID_ARG_TYPE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_MISSING_OPTION,
ERR_OUT_OF_RANGE,
ERR_STREAM_DESTROYED,
ERR_SYSTEM_ERROR,
} = require('internal/errors').codes;
const {
isWindows,
kEmptyObject,
} = require('internal/util');
const {
Expand All@@ -40,6 +44,8 @@ const {
} = require('internal/fs/utils');
const { Readable, Writable, finished } = require('stream');
const { toPathIfFileURL } = require('internal/url');
const binding = internalBinding('fs');
const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs;
const kIoDone = Symbol('kIoDone');
const kIsPerformingIO = Symbol('kIsPerformingIO');

Expand DownExpand Up@@ -160,6 +166,26 @@ function importFd(stream, options) {
['number', 'FileHandle'], options.fd);
}

function importWindowsHandle(stream, options, flags) {
if (options.windowsHandle == null) {
throw new ERR_MISSING_OPTION('options.windowsHandle');
}
if (!isWindows) {
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('windowsHandle');
}
if (options.fs) {
// The HANDLE is wrapped using the real filesystem, so a custom fs
// implementation cannot be combined with it.
throw new ERR_METHOD_NOT_IMPLEMENTED('windowsHandle with fs');
}
if (typeof options.windowsHandle !== 'bigint') {
throw new ERR_INVALID_ARG_TYPE('options.windowsHandle', 'bigint',
options.windowsHandle);
}
stream[kFs] = fs;
return binding.handleToFd(options.windowsHandle, flags);
}

function ReadStream(path, options) {
if (!(this instanceof ReadStream))
return new ReadStream(path, options);
Expand All@@ -173,7 +199,11 @@ function ReadStream(path, options) {
options.autoDestroy = false;
}

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand DownExpand Up@@ -325,7 +355,11 @@ function WriteStream(path, options) {
// Only buffers are supported.
options.decodeStrings = true;

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand Down
34 changes: 34 additions & 0 deletions src/node_file.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -4150,6 +4150,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) {
return info;
}

#ifdef _WIN32
static void HandleToFd(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 1);
CHECK(args[0]->IsBigInt());

int flags = 0;
if (args[1]->IsNumber()) {
flags = args[1].As<Int32>()->Value();
}

bool lossless;
int64_t handle = args[0].As<BigInt>()->Int64Value(&lossless);
if (!lossless) {
return THROW_ERR_OUT_OF_RANGE(env,
"windowsHandle does not fit into 64 bits");
}
intptr_t value = static_cast<intptr_t>(handle);

int fd = _open_osfhandle(value, flags);
if (fd == -1) {
return env->ThrowErrnoException(errno, "_open_osfhandle");
}
args.GetReturnValue().Set(fd);
}
#endif // _WIN32

void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Expand DownExpand Up@@ -4216,6 +4243,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,

SetMethod(isolate, target, "mkdtemp", Mkdtemp);

#ifdef _WIN32
SetMethod(isolate, target, "handleToFd", HandleToFd);
#endif

SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);
SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);
SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);
Expand DownExpand Up@@ -4343,6 +4374,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(LUTimes);

registry->Register(Mkdtemp);
#ifdef _WIN32
registry->Register(HandleToFd);
#endif
registry->Register(NewFSReqCallback);

registry->Register(FileHandle::New);
Expand Down
62 changes: 62 additions & 0 deletions test/addons/fs-windows-handle/binding.cc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
#include <node.h>
#include <v8.h>

#ifdef _WIN32
#include <windows.h>
#endif

namespace {

using v8::BigInt;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

// Creates an anonymous pipe and returns its raw Win32 read/write HANDLE values
// as JS bigints. These are NOT CRT file descriptors, so passing them as the
// `windowsHandle` stream option exercises the HANDLE -> fd conversion path on
// Windows. Returns undefined on other platforms.
void CreatePipeHandles(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
#ifdef _WIN32
Local<Context> context = isolate->GetCurrentContext();

HANDLE read_handle = nullptr;
HANDLE write_handle = nullptr;
if (!CreatePipe(&read_handle, &write_handle, nullptr, 0)) {
isolate->ThrowException(v8::Exception::Error(
String::NewFromUtf8(isolate, "CreatePipe failed").ToLocalChecked()));
return;
}

Local<Object> result = Object::New(isolate);
result
->Set(context,
String::NewFromUtf8(isolate, "readHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(read_handle))))
.Check();
result
->Set(context,
String::NewFromUtf8(isolate, "writeHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(write_handle))))
.Check();
args.GetReturnValue().Set(result);
#else
args.GetReturnValue().SetUndefined();
#endif
}

} // anonymous namespace

extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(
Local<Object> exports, Local<Value> module, Local<Context> context) {
NODE_SET_METHOD(exports, "createPipeHandles", CreatePipeHandles);
}
9 changes: 9 additions & 0 deletions test/addons/fs-windows-handle/binding.gyp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
},
]
}
35 changes: 35 additions & 0 deletions test/addons/fs-windows-handle/test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
'use strict';
// Verifies that fs.createReadStream()/createWriteStream() accept a raw Win32
// HANDLE through the `windowsHandle` option, as happens when a parent process
// passes an inherited anonymous pipe handle. The addon produces such handles
// via CreatePipe(); Node must wrap them in CRT file descriptors instead of
// failing with EBADF.

const common = require('../../common');

if (!common.isWindows) {
common.skip('windowsHandle is Windows-only');
}

const assert = require('assert');
const fs = require('fs');

const binding = require(`./build/${common.buildType}/binding`);

const { readHandle, writeHandle } = binding.createPipeHandles();
assert.strictEqual(typeof readHandle, 'bigint');
assert.strictEqual(typeof writeHandle, 'bigint');

const payload = 'payload';

const chunks = [];
const rs = fs.createReadStream(null, { windowsHandle: readHandle });
rs.on('error', (err) => assert.fail(err));
rs.on('data', (chunk) => chunks.push(chunk));
rs.on('end', common.mustCall(() => {
assert.strictEqual(Buffer.concat(chunks).toString(), payload);
}));

const ws = fs.createWriteStream(null, { windowsHandle: writeHandle });
ws.on('error', (err) => assert.fail(err));
ws.end(payload);
47 changes: 47 additions & 0 deletions test/parallel/test-fs-stream-windows-handle.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
'use strict';

// Tests option validation for the `windowsHandle` option of
// fs.createReadStream()/createWriteStream(). The functional round-trip on
// Windows (where a real Win32 HANDLE is wrapped in a file descriptor) is
// covered by test/addons/fs-windows-handle.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');

const handle = 1n;

for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle, fd: 2 }), {
code: 'ERR_INCOMPATIBLE_OPTION_PAIR',
});
}

if (!common.isWindows) {
for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle }), {
code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
});
}
return;
}

for (const create of [fs.createReadStream, fs.createWriteStream]) {
// Cannot be combined with a custom `fs` implementation.
assert.throws(() => create(null, { windowsHandle: handle, fs: {} }), {
code: 'ERR_METHOD_NOT_IMPLEMENTED',
});

// Must be a bigint.
assert.throws(() => create(null, { windowsHandle: 'nope' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => create(null, { windowsHandle: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});

// Must fit into 64 bits.
assert.throws(() => create(null, { windowsHandle: 2n ** 64n }), {
code: 'ERR_OUT_OF_RANGE',
});
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/api/fs.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2944,6 +2944,9 @@ behavior is similar to `cp dir1/ dir2/`.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v16.10.0
pr-url: https://github.com/nodejs/node/pull/40013
description: The `fs` option does not need `open` method if an `fd` was provided.
Expand DownExpand Up@@ -3000,6 +3003,8 @@ changes:
* `highWaterMark` {integer} **Default:** `64 * 1024`
* `fs` {Object|null} **Default:** `null`
* `signal` {AbortSignal|null} **Default:** `null`
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.ReadStream}

`options` can include `start` and `end` values to read a range of bytes from
Expand All@@ -3020,6 +3025,12 @@ If `fd` points to a character device that only supports blocking reads
available. This can prevent the process from exiting and the stream from
closing naturally.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand DownExpand Up@@ -3070,6 +3081,9 @@ If `options` is a string, then it specifies the encoding.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v22.0.0
pr-url: https://github.com/nodejs/node/pull/52037
description: bump default highWaterMark.
Expand DownExpand Up@@ -3134,6 +3148,8 @@ changes:
[`stream.getDefaultHighWaterMark()`][].
* `flush` {boolean} If `true`, the underlying file descriptor is flushed
prior to closing it. **Default:** `false`.
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.WriteStream}

`options` may also include a `start` option to allow writing data at some
Expand All@@ -3148,6 +3164,12 @@ then the file descriptor won't be closed, even if there's an error.
It is the application's responsibility to close it and make sure there's no
file descriptor leak.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand Down
38 changes: 36 additions & 2 deletions lib/internal/fs/streams.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,13 +13,17 @@ const {
} = primordials;

const {
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
ERR_INCOMPATIBLE_OPTION_PAIR,
ERR_INVALID_ARG_TYPE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_MISSING_OPTION,
ERR_OUT_OF_RANGE,
ERR_STREAM_DESTROYED,
ERR_SYSTEM_ERROR,
} = require('internal/errors').codes;
const {
isWindows,
kEmptyObject,
} = require('internal/util');
const {
Expand All@@ -40,6 +44,8 @@ const {
} = require('internal/fs/utils');
const { Readable, Writable, finished } = require('stream');
const { toPathIfFileURL } = require('internal/url');
const binding = internalBinding('fs');
const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs;
const kIoDone = Symbol('kIoDone');
const kIsPerformingIO = Symbol('kIsPerformingIO');

Expand DownExpand Up@@ -160,6 +166,26 @@ function importFd(stream, options) {
['number', 'FileHandle'], options.fd);
}

function importWindowsHandle(stream, options, flags) {
if (options.windowsHandle == null) {
throw new ERR_MISSING_OPTION('options.windowsHandle');
}
if (!isWindows) {
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('windowsHandle');
}
if (options.fs) {
// The HANDLE is wrapped using the real filesystem, so a custom fs
// implementation cannot be combined with it.
throw new ERR_METHOD_NOT_IMPLEMENTED('windowsHandle with fs');
}
if (typeof options.windowsHandle !== 'bigint') {
throw new ERR_INVALID_ARG_TYPE('options.windowsHandle', 'bigint',
options.windowsHandle);
}
stream[kFs] = fs;
return binding.handleToFd(options.windowsHandle, flags);
}

function ReadStream(path, options) {
if (!(this instanceof ReadStream))
return new ReadStream(path, options);
Expand All@@ -173,7 +199,11 @@ function ReadStream(path, options) {
options.autoDestroy = false;
}

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand DownExpand Up@@ -325,7 +355,11 @@ function WriteStream(path, options) {
// Only buffers are supported.
options.decodeStrings = true;

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand Down
34 changes: 34 additions & 0 deletions src/node_file.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -4150,6 +4150,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) {
return info;
}

#ifdef _WIN32
static void HandleToFd(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 1);
CHECK(args[0]->IsBigInt());

int flags = 0;
if (args[1]->IsNumber()) {
flags = args[1].As<Int32>()->Value();
}

bool lossless;
int64_t handle = args[0].As<BigInt>()->Int64Value(&lossless);
if (!lossless) {
return THROW_ERR_OUT_OF_RANGE(env,
"windowsHandle does not fit into 64 bits");
}
intptr_t value = static_cast<intptr_t>(handle);

int fd = _open_osfhandle(value, flags);
if (fd == -1) {
return env->ThrowErrnoException(errno, "_open_osfhandle");
}
args.GetReturnValue().Set(fd);
}
#endif // _WIN32

void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Expand DownExpand Up@@ -4216,6 +4243,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,

SetMethod(isolate, target, "mkdtemp", Mkdtemp);

#ifdef _WIN32
SetMethod(isolate, target, "handleToFd", HandleToFd);
#endif

SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);
SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);
SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);
Expand DownExpand Up@@ -4343,6 +4374,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(LUTimes);

registry->Register(Mkdtemp);
#ifdef _WIN32
registry->Register(HandleToFd);
#endif
registry->Register(NewFSReqCallback);

registry->Register(FileHandle::New);
Expand Down
62 changes: 62 additions & 0 deletions test/addons/fs-windows-handle/binding.cc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
#include <node.h>
#include <v8.h>

#ifdef _WIN32
#include <windows.h>
#endif

namespace {

using v8::BigInt;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

// Creates an anonymous pipe and returns its raw Win32 read/write HANDLE values
// as JS bigints. These are NOT CRT file descriptors, so passing them as the
// `windowsHandle` stream option exercises the HANDLE -> fd conversion path on
// Windows. Returns undefined on other platforms.
void CreatePipeHandles(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
#ifdef _WIN32
Local<Context> context = isolate->GetCurrentContext();

HANDLE read_handle = nullptr;
HANDLE write_handle = nullptr;
if (!CreatePipe(&read_handle, &write_handle, nullptr, 0)) {
isolate->ThrowException(v8::Exception::Error(
String::NewFromUtf8(isolate, "CreatePipe failed").ToLocalChecked()));
return;
}

Local<Object> result = Object::New(isolate);
result
->Set(context,
String::NewFromUtf8(isolate, "readHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(read_handle))))
.Check();
result
->Set(context,
String::NewFromUtf8(isolate, "writeHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(write_handle))))
.Check();
args.GetReturnValue().Set(result);
#else
args.GetReturnValue().SetUndefined();
#endif
}

} // anonymous namespace

extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(
Local<Object> exports, Local<Value> module, Local<Context> context) {
NODE_SET_METHOD(exports, "createPipeHandles", CreatePipeHandles);
}
9 changes: 9 additions & 0 deletions test/addons/fs-windows-handle/binding.gyp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
},
]
}
35 changes: 35 additions & 0 deletions test/addons/fs-windows-handle/test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
'use strict';
// Verifies that fs.createReadStream()/createWriteStream() accept a raw Win32
// HANDLE through the `windowsHandle` option, as happens when a parent process
// passes an inherited anonymous pipe handle. The addon produces such handles
// via CreatePipe(); Node must wrap them in CRT file descriptors instead of
// failing with EBADF.

const common = require('../../common');

if (!common.isWindows) {
common.skip('windowsHandle is Windows-only');
}

const assert = require('assert');
const fs = require('fs');

const binding = require(`./build/${common.buildType}/binding`);

const { readHandle, writeHandle } = binding.createPipeHandles();
assert.strictEqual(typeof readHandle, 'bigint');
assert.strictEqual(typeof writeHandle, 'bigint');

const payload = 'payload';

const chunks = [];
const rs = fs.createReadStream(null, { windowsHandle: readHandle });
rs.on('error', (err) => assert.fail(err));
rs.on('data', (chunk) => chunks.push(chunk));
rs.on('end', common.mustCall(() => {
assert.strictEqual(Buffer.concat(chunks).toString(), payload);
}));

const ws = fs.createWriteStream(null, { windowsHandle: writeHandle });
ws.on('error', (err) => assert.fail(err));
ws.end(payload);
47 changes: 47 additions & 0 deletions test/parallel/test-fs-stream-windows-handle.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
'use strict';

// Tests option validation for the `windowsHandle` option of
// fs.createReadStream()/createWriteStream(). The functional round-trip on
// Windows (where a real Win32 HANDLE is wrapped in a file descriptor) is
// covered by test/addons/fs-windows-handle.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');

const handle = 1n;

for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle, fd: 2 }), {
code: 'ERR_INCOMPATIBLE_OPTION_PAIR',
});
}

if (!common.isWindows) {
for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle }), {
code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
});
}
return;
}

for (const create of [fs.createReadStream, fs.createWriteStream]) {
// Cannot be combined with a custom `fs` implementation.
assert.throws(() => create(null, { windowsHandle: handle, fs: {} }), {
code: 'ERR_METHOD_NOT_IMPLEMENTED',
});

// Must be a bigint.
assert.throws(() => create(null, { windowsHandle: 'nope' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => create(null, { windowsHandle: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});

// Must fit into 64 bits.
assert.throws(() => create(null, { windowsHandle: 2n ** 64n }), {
code: 'ERR_OUT_OF_RANGE',
});
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/api/fs.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2944,6 +2944,9 @@ behavior is similar to `cp dir1/ dir2/`.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v16.10.0
pr-url: https://github.com/nodejs/node/pull/40013
description: The `fs` option does not need `open` method if an `fd` was provided.
Expand DownExpand Up@@ -3000,6 +3003,8 @@ changes:
* `highWaterMark` {integer} **Default:** `64 * 1024`
* `fs` {Object|null} **Default:** `null`
* `signal` {AbortSignal|null} **Default:** `null`
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.ReadStream}

`options` can include `start` and `end` values to read a range of bytes from
Expand All@@ -3020,6 +3025,12 @@ If `fd` points to a character device that only supports blocking reads
available. This can prevent the process from exiting and the stream from
closing naturally.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand DownExpand Up@@ -3070,6 +3081,9 @@ If `options` is a string, then it specifies the encoding.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v22.0.0
pr-url: https://github.com/nodejs/node/pull/52037
description: bump default highWaterMark.
Expand DownExpand Up@@ -3134,6 +3148,8 @@ changes:
[`stream.getDefaultHighWaterMark()`][].
* `flush` {boolean} If `true`, the underlying file descriptor is flushed
prior to closing it. **Default:** `false`.
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.WriteStream}

`options` may also include a `start` option to allow writing data at some
Expand All@@ -3148,6 +3164,12 @@ then the file descriptor won't be closed, even if there's an error.
It is the application's responsibility to close it and make sure there's no
file descriptor leak.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand Down
38 changes: 36 additions & 2 deletions lib/internal/fs/streams.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,13 +13,17 @@ const {
} = primordials;

const {
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
ERR_INCOMPATIBLE_OPTION_PAIR,
ERR_INVALID_ARG_TYPE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_MISSING_OPTION,
ERR_OUT_OF_RANGE,
ERR_STREAM_DESTROYED,
ERR_SYSTEM_ERROR,
} = require('internal/errors').codes;
const {
isWindows,
kEmptyObject,
} = require('internal/util');
const {
Expand All@@ -40,6 +44,8 @@ const {
} = require('internal/fs/utils');
const { Readable, Writable, finished } = require('stream');
const { toPathIfFileURL } = require('internal/url');
const binding = internalBinding('fs');
const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs;
const kIoDone = Symbol('kIoDone');
const kIsPerformingIO = Symbol('kIsPerformingIO');

Expand DownExpand Up@@ -160,6 +166,26 @@ function importFd(stream, options) {
['number', 'FileHandle'], options.fd);
}

function importWindowsHandle(stream, options, flags) {
if (options.windowsHandle == null) {
throw new ERR_MISSING_OPTION('options.windowsHandle');
}
if (!isWindows) {
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('windowsHandle');
}
if (options.fs) {
// The HANDLE is wrapped using the real filesystem, so a custom fs
// implementation cannot be combined with it.
throw new ERR_METHOD_NOT_IMPLEMENTED('windowsHandle with fs');
}
if (typeof options.windowsHandle !== 'bigint') {
throw new ERR_INVALID_ARG_TYPE('options.windowsHandle', 'bigint',
options.windowsHandle);
}
stream[kFs] = fs;
return binding.handleToFd(options.windowsHandle, flags);
}

function ReadStream(path, options) {
if (!(this instanceof ReadStream))
return new ReadStream(path, options);
Expand All@@ -173,7 +199,11 @@ function ReadStream(path, options) {
options.autoDestroy = false;
}

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand DownExpand Up@@ -325,7 +355,11 @@ function WriteStream(path, options) {
// Only buffers are supported.
options.decodeStrings = true;

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand Down
34 changes: 34 additions & 0 deletions src/node_file.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -4150,6 +4150,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) {
return info;
}

#ifdef _WIN32
static void HandleToFd(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 1);
CHECK(args[0]->IsBigInt());

int flags = 0;
if (args[1]->IsNumber()) {
flags = args[1].As<Int32>()->Value();
}

bool lossless;
int64_t handle = args[0].As<BigInt>()->Int64Value(&lossless);
if (!lossless) {
return THROW_ERR_OUT_OF_RANGE(env,
"windowsHandle does not fit into 64 bits");
}
intptr_t value = static_cast<intptr_t>(handle);

int fd = _open_osfhandle(value, flags);
if (fd == -1) {
return env->ThrowErrnoException(errno, "_open_osfhandle");
}
args.GetReturnValue().Set(fd);
}
#endif // _WIN32

void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Expand DownExpand Up@@ -4216,6 +4243,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,

SetMethod(isolate, target, "mkdtemp", Mkdtemp);

#ifdef _WIN32
SetMethod(isolate, target, "handleToFd", HandleToFd);
#endif

SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);
SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);
SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);
Expand DownExpand Up@@ -4343,6 +4374,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(LUTimes);

registry->Register(Mkdtemp);
#ifdef _WIN32
registry->Register(HandleToFd);
#endif
registry->Register(NewFSReqCallback);

registry->Register(FileHandle::New);
Expand Down
62 changes: 62 additions & 0 deletions test/addons/fs-windows-handle/binding.cc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
#include <node.h>
#include <v8.h>

#ifdef _WIN32
#include <windows.h>
#endif

namespace {

using v8::BigInt;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

// Creates an anonymous pipe and returns its raw Win32 read/write HANDLE values
// as JS bigints. These are NOT CRT file descriptors, so passing them as the
// `windowsHandle` stream option exercises the HANDLE -> fd conversion path on
// Windows. Returns undefined on other platforms.
void CreatePipeHandles(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
#ifdef _WIN32
Local<Context> context = isolate->GetCurrentContext();

HANDLE read_handle = nullptr;
HANDLE write_handle = nullptr;
if (!CreatePipe(&read_handle, &write_handle, nullptr, 0)) {
isolate->ThrowException(v8::Exception::Error(
String::NewFromUtf8(isolate, "CreatePipe failed").ToLocalChecked()));
return;
}

Local<Object> result = Object::New(isolate);
result
->Set(context,
String::NewFromUtf8(isolate, "readHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(read_handle))))
.Check();
result
->Set(context,
String::NewFromUtf8(isolate, "writeHandle").ToLocalChecked(),
BigInt::New(
isolate,
static_cast<int64_t>(reinterpret_cast<intptr_t>(write_handle))))
.Check();
args.GetReturnValue().Set(result);
#else
args.GetReturnValue().SetUndefined();
#endif
}

} // anonymous namespace

extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(
Local<Object> exports, Local<Value> module, Local<Context> context) {
NODE_SET_METHOD(exports, "createPipeHandles", CreatePipeHandles);
}
9 changes: 9 additions & 0 deletions test/addons/fs-windows-handle/binding.gyp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
},
]
}
35 changes: 35 additions & 0 deletions test/addons/fs-windows-handle/test.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
'use strict';
// Verifies that fs.createReadStream()/createWriteStream() accept a raw Win32
// HANDLE through the `windowsHandle` option, as happens when a parent process
// passes an inherited anonymous pipe handle. The addon produces such handles
// via CreatePipe(); Node must wrap them in CRT file descriptors instead of
// failing with EBADF.

const common = require('../../common');

if (!common.isWindows) {
common.skip('windowsHandle is Windows-only');
}

const assert = require('assert');
const fs = require('fs');

const binding = require(`./build/${common.buildType}/binding`);

const { readHandle, writeHandle } = binding.createPipeHandles();
assert.strictEqual(typeof readHandle, 'bigint');
assert.strictEqual(typeof writeHandle, 'bigint');

const payload = 'payload';

const chunks = [];
const rs = fs.createReadStream(null, { windowsHandle: readHandle });
rs.on('error', (err) => assert.fail(err));
rs.on('data', (chunk) => chunks.push(chunk));
rs.on('end', common.mustCall(() => {
assert.strictEqual(Buffer.concat(chunks).toString(), payload);
}));

const ws = fs.createWriteStream(null, { windowsHandle: writeHandle });
ws.on('error', (err) => assert.fail(err));
ws.end(payload);
47 changes: 47 additions & 0 deletions test/parallel/test-fs-stream-windows-handle.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
'use strict';

// Tests option validation for the `windowsHandle` option of
// fs.createReadStream()/createWriteStream(). The functional round-trip on
// Windows (where a real Win32 HANDLE is wrapped in a file descriptor) is
// covered by test/addons/fs-windows-handle.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');

const handle = 1n;

for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle, fd: 2 }), {
code: 'ERR_INCOMPATIBLE_OPTION_PAIR',
});
}

if (!common.isWindows) {
for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle }), {
code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
});
}
return;
}

for (const create of [fs.createReadStream, fs.createWriteStream]) {
// Cannot be combined with a custom `fs` implementation.
assert.throws(() => create(null, { windowsHandle: handle, fs: {} }), {
code: 'ERR_METHOD_NOT_IMPLEMENTED',
});

// Must be a bigint.
assert.throws(() => create(null, { windowsHandle: 'nope' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => create(null, { windowsHandle: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});

// Must fit into 64 bits.
assert.throws(() => create(null, { windowsHandle: 2n ** 64n }), {
code: 'ERR_OUT_OF_RANGE',
});
}
Loading