Skip to content

Commit d9d4446

Browse files
authored
fs: add windowsHandle option to file streams
Fixes: #57288 Signed-off-by: PickBas <[email protected]> PR-URL: #63851 Reviewed-By: Stefan Stojanovic <[email protected]>
1 parent 3d80990 commit d9d4446

7 files changed

Lines changed: 245 additions & 2 deletions

File tree

‎doc/api/fs.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2944,6 +2944,9 @@ behavior is similar to `cp dir1/ dir2/`.
29442944
<!-- YAML
29452945
added: v0.1.31
29462946
changes:
2947+
- version: REPLACEME
2948+
pr-url: https://github.com/nodejs/node/pull/63851
2949+
description: Add the `windowsHandle` option.
29472950
- version: v16.10.0
29482951
pr-url: https://github.com/nodejs/node/pull/40013
29492952
description: The `fs` option does not need `open` method if an `fd` was provided.
@@ -3000,6 +3003,8 @@ changes:
30003003
* `highWaterMark` {integer} **Default:** `64 * 1024`
30013004
* `fs` {Object|null} **Default:** `null`
30023005
* `signal` {AbortSignal|null} **Default:** `null`
3006+
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place
3007+
of `fd`. Windows only. **Default:** `null`
30033008
* Returns: {fs.ReadStream}
30043009
30053010
`options` can include `start` and `end` values to read a range of bytes from
@@ -3020,6 +3025,12 @@ If `fd` points to a character device that only supports blocking reads
30203025
available. This can prevent the process from exiting and the stream from
30213026
closing naturally.
30223027
3028+
On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
3029+
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
3030+
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
3031+
in a file descriptor that the stream owns and closes. The `windowsHandle` option
3032+
throws on non-Windows platforms and cannot be combined with the `fs` option.
3033+
30233034
By default, the stream will emit a `'close'` event after it has been
30243035
destroyed. Set the `emitClose` option to `false` to change this behavior.
30253036
@@ -3070,6 +3081,9 @@ If `options` is a string, then it specifies the encoding.
30703081
<!-- YAML
30713082
added: v0.1.31
30723083
changes:
3084+
- version: REPLACEME
3085+
pr-url: https://github.com/nodejs/node/pull/63851
3086+
description: Add the `windowsHandle` option.
30733087
- version: v22.0.0
30743088
pr-url: https://github.com/nodejs/node/pull/52037
30753089
description: bump default highWaterMark.
@@ -3134,6 +3148,8 @@ changes:
31343148
[`stream.getDefaultHighWaterMark()`][].
31353149
* `flush` {boolean} If `true`, the underlying file descriptor is flushed
31363150
prior to closing it. **Default:** `false`.
3151+
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place
3152+
of `fd`. Windows only. **Default:** `null`
31373153
* Returns: {fs.WriteStream}
31383154
31393155
`options` may also include a `start` option to allow writing data at some
@@ -3148,6 +3164,12 @@ then the file descriptor won't be closed, even if there's an error.
31483164
It is the application's responsibility to close it and make sure there's no
31493165
file descriptor leak.
31503166
3167+
On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
3168+
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
3169+
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
3170+
in a file descriptor that the stream owns and closes. The `windowsHandle` option
3171+
throws on non-Windows platforms and cannot be combined with the `fs` option.
3172+
31513173
By default, the stream will emit a `'close'` event after it has been
31523174
destroyed. Set the `emitClose` option to `false` to change this behavior.
31533175

‎lib/internal/fs/streams.js‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@ const {
1313
} = primordials;
1414

1515
const {
16+
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
17+
ERR_INCOMPATIBLE_OPTION_PAIR,
1618
ERR_INVALID_ARG_TYPE,
1719
ERR_METHOD_NOT_IMPLEMENTED,
20+
ERR_MISSING_OPTION,
1821
ERR_OUT_OF_RANGE,
1922
ERR_STREAM_DESTROYED,
2023
ERR_SYSTEM_ERROR,
2124
} = require('internal/errors').codes;
2225
const {
26+
isWindows,
2327
kEmptyObject,
2428
} = require('internal/util');
2529
const {
@@ -40,6 +44,8 @@ const {
4044
} = require('internal/fs/utils');
4145
const { Readable, Writable, finished } = require('stream');
4246
const { toPathIfFileURL } = require('internal/url');
47+
const binding = internalBinding('fs');
48+
const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs;
4349
const kIoDone = Symbol('kIoDone');
4450
const kIsPerformingIO = Symbol('kIsPerformingIO');
4551

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

169+
function importWindowsHandle(stream, options, flags) {
170+
if (options.windowsHandle == null) {
171+
throw new ERR_MISSING_OPTION('options.windowsHandle');
172+
}
173+
if (!isWindows) {
174+
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('windowsHandle');
175+
}
176+
if (options.fs) {
177+
// The HANDLE is wrapped using the real filesystem, so a custom fs
178+
// implementation cannot be combined with it.
179+
throw new ERR_METHOD_NOT_IMPLEMENTED('windowsHandle with fs');
180+
}
181+
if (typeof options.windowsHandle !== 'bigint') {
182+
throw new ERR_INVALID_ARG_TYPE('options.windowsHandle', 'bigint',
183+
options.windowsHandle);
184+
}
185+
stream[kFs] = fs;
186+
return binding.handleToFd(options.windowsHandle, flags);
187+
}
188+
163189
function ReadStream(path, options) {
164190
if (!(this instanceof ReadStream))
165191
return new ReadStream(path, options);
@@ -173,7 +199,11 @@ function ReadStream(path, options) {
173199
options.autoDestroy = false;
174200
}
175201

176-
if (options.fd == null) {
202+
if (options.fd != null && options.windowsHandle != null) {
203+
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
204+
} else if (options.windowsHandle != null) {
205+
this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY));
206+
} else if (options.fd == null) {
177207
this.fd = null;
178208
this[kFs] = options.fs || fs;
179209
validateFunction(this[kFs].open, 'options.fs.open');
@@ -325,7 +355,11 @@ function WriteStream(path, options) {
325355
// Only buffers are supported.
326356
options.decodeStrings = true;
327357

328-
if (options.fd == null) {
358+
if (options.fd != null && options.windowsHandle != null) {
359+
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
360+
} else if (options.windowsHandle != null) {
361+
this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY));
362+
} else if (options.fd == null) {
329363
this.fd = null;
330364
this[kFs] = options.fs || fs;
331365
validateFunction(this[kFs].open, 'options.fs.open');

‎src/node_file.cc‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4150,6 +4150,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) {
41504150
return info;
41514151
}
41524152

4153+
#ifdef _WIN32
4154+
static void HandleToFd(const FunctionCallbackInfo<Value>& args) {
4155+
Environment* env = Environment::GetCurrent(args);
4156+
CHECK_GE(args.Length(), 1);
4157+
CHECK(args[0]->IsBigInt());
4158+
4159+
int flags = 0;
4160+
if (args[1]->IsNumber()) {
4161+
flags = args[1].As<Int32>()->Value();
4162+
}
4163+
4164+
bool lossless;
4165+
int64_t handle = args[0].As<BigInt>()->Int64Value(&lossless);
4166+
if (!lossless) {
4167+
return THROW_ERR_OUT_OF_RANGE(env,
4168+
"windowsHandle does not fit into 64 bits");
4169+
}
4170+
intptr_t value = static_cast<intptr_t>(handle);
4171+
4172+
int fd = _open_osfhandle(value, flags);
4173+
if (fd == -1) {
4174+
return env->ThrowErrnoException(errno, "_open_osfhandle");
4175+
}
4176+
args.GetReturnValue().Set(fd);
4177+
}
4178+
#endif // _WIN32
4179+
41534180
void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
41544181
Local<ObjectTemplate> target) {
41554182
Isolate* isolate = isolate_data->isolate();
@@ -4216,6 +4243,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
42164243

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

4246+
#ifdef _WIN32
4247+
SetMethod(isolate, target, "handleToFd", HandleToFd);
4248+
#endif
4249+
42194250
SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);
42204251
SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);
42214252
SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);
@@ -4343,6 +4374,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
43434374
registry->Register(LUTimes);
43444375

43454376
registry->Register(Mkdtemp);
4377+
#ifdef _WIN32
4378+
registry->Register(HandleToFd);
4379+
#endif
43464380
registry->Register(NewFSReqCallback);
43474381

43484382
registry->Register(FileHandle::New);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#include <node.h>
2+
#include <v8.h>
3+
4+
#ifdef _WIN32
5+
#include <windows.h>
6+
#endif
7+
8+
namespace {
9+
10+
using v8::BigInt;
11+
using v8::Context;
12+
using v8::FunctionCallbackInfo;
13+
using v8::Isolate;
14+
using v8::Local;
15+
using v8::Object;
16+
using v8::String;
17+
using v8::Value;
18+
19+
// Creates an anonymous pipe and returns its raw Win32 read/write HANDLE values
20+
// as JS bigints. These are NOT CRT file descriptors, so passing them as the
21+
// `windowsHandle` stream option exercises the HANDLE -> fd conversion path on
22+
// Windows. Returns undefined on other platforms.
23+
void CreatePipeHandles(const FunctionCallbackInfo<Value>& args) {
24+
Isolate* isolate = args.GetIsolate();
25+
#ifdef _WIN32
26+
Local<Context> context = isolate->GetCurrentContext();
27+
28+
HANDLE read_handle = nullptr;
29+
HANDLE write_handle = nullptr;
30+
if (!CreatePipe(&read_handle, &write_handle, nullptr, 0)) {
31+
isolate->ThrowException(v8::Exception::Error(
32+
String::NewFromUtf8(isolate, "CreatePipe failed").ToLocalChecked()));
33+
return;
34+
}
35+
36+
Local<Object> result = Object::New(isolate);
37+
result
38+
->Set(context,
39+
String::NewFromUtf8(isolate, "readHandle").ToLocalChecked(),
40+
BigInt::New(
41+
isolate,
42+
static_cast<int64_t>(reinterpret_cast<intptr_t>(read_handle))))
43+
.Check();
44+
result
45+
->Set(context,
46+
String::NewFromUtf8(isolate, "writeHandle").ToLocalChecked(),
47+
BigInt::New(
48+
isolate,
49+
static_cast<int64_t>(reinterpret_cast<intptr_t>(write_handle))))
50+
.Check();
51+
args.GetReturnValue().Set(result);
52+
#else
53+
args.GetReturnValue().SetUndefined();
54+
#endif
55+
}
56+
57+
} // anonymous namespace
58+
59+
extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(
60+
Local<Object> exports, Local<Value> module, Local<Context> context) {
61+
NODE_SET_METHOD(exports, "createPipeHandles", CreatePipeHandles);
62+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'binding',
5+
'sources': [ 'binding.cc' ],
6+
'includes': ['../common.gypi'],
7+
},
8+
]
9+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
// Verifies that fs.createReadStream()/createWriteStream() accept a raw Win32
3+
// HANDLE through the `windowsHandle` option, as happens when a parent process
4+
// passes an inherited anonymous pipe handle. The addon produces such handles
5+
// via CreatePipe(); Node must wrap them in CRT file descriptors instead of
6+
// failing with EBADF.
7+
8+
const common = require('../../common');
9+
10+
if (!common.isWindows) {
11+
common.skip('windowsHandle is Windows-only');
12+
}
13+
14+
const assert = require('assert');
15+
const fs = require('fs');
16+
17+
const binding = require(`./build/${common.buildType}/binding`);
18+
19+
const { readHandle, writeHandle } = binding.createPipeHandles();
20+
assert.strictEqual(typeof readHandle, 'bigint');
21+
assert.strictEqual(typeof writeHandle, 'bigint');
22+
23+
const payload = 'payload';
24+
25+
const chunks = [];
26+
const rs = fs.createReadStream(null, { windowsHandle: readHandle });
27+
rs.on('error', (err) => assert.fail(err));
28+
rs.on('data', (chunk) => chunks.push(chunk));
29+
rs.on('end', common.mustCall(() => {
30+
assert.strictEqual(Buffer.concat(chunks).toString(), payload);
31+
}));
32+
33+
const ws = fs.createWriteStream(null, { windowsHandle: writeHandle });
34+
ws.on('error', (err) => assert.fail(err));
35+
ws.end(payload);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
// Tests option validation for the `windowsHandle` option of
4+
// fs.createReadStream()/createWriteStream(). The functional round-trip on
5+
// Windows (where a real Win32 HANDLE is wrapped in a file descriptor) is
6+
// covered by test/addons/fs-windows-handle.
7+
8+
const common = require('../common');
9+
const assert = require('assert');
10+
const fs = require('fs');
11+
12+
const handle = 1n;
13+
14+
for (const create of [fs.createReadStream, fs.createWriteStream]) {
15+
assert.throws(() => create(null, { windowsHandle: handle, fd: 2 }), {
16+
code: 'ERR_INCOMPATIBLE_OPTION_PAIR',
17+
});
18+
}
19+
20+
if (!common.isWindows) {
21+
for (const create of [fs.createReadStream, fs.createWriteStream]) {
22+
assert.throws(() => create(null, { windowsHandle: handle }), {
23+
code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
24+
});
25+
}
26+
return;
27+
}
28+
29+
for (const create of [fs.createReadStream, fs.createWriteStream]) {
30+
// Cannot be combined with a custom `fs` implementation.
31+
assert.throws(() => create(null, { windowsHandle: handle, fs: {} }), {
32+
code: 'ERR_METHOD_NOT_IMPLEMENTED',
33+
});
34+
35+
// Must be a bigint.
36+
assert.throws(() => create(null, { windowsHandle: 'nope' }), {
37+
code: 'ERR_INVALID_ARG_TYPE',
38+
});
39+
assert.throws(() => create(null, { windowsHandle: 1 }), {
40+
code: 'ERR_INVALID_ARG_TYPE',
41+
});
42+
43+
// Must fit into 64 bits.
44+
assert.throws(() => create(null, { windowsHandle: 2n ** 64n }), {
45+
code: 'ERR_OUT_OF_RANGE',
46+
});
47+
}

0 commit comments

Comments
 (0)