Skip to content

Commit 212fe77

Browse files
PickBasaduh95
authored andcommitted
fs: add windowsHandle option to file streams
Fixes: #57288 Signed-off-by: PickBas <sayed.kirill@gmail.com> PR-URL: #63851 Reviewed-By: Stefan Stojanovic <stefan.stojanovic@janeasystems.com>
1 parent 41c7062 commit 212fe77

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
@@ -2930,6 +2930,9 @@ behavior is similar to `cp dir1/ dir2/`.
29302930
<!-- YAML
29312931
added: v0.1.31
29322932
changes:
2933+
- version: REPLACEME
2934+
pr-url: https://github.com/nodejs/node/pull/63851
2935+
description: Add the `windowsHandle` option.
29332936
- version: v16.10.0
29342937
pr-url: https://github.com/nodejs/node/pull/40013
29352938
description: The `fs` option does not need `open` method if an `fd` was provided.
@@ -2986,6 +2989,8 @@ changes:
29862989
* `highWaterMark` {integer} **Default:** `64 * 1024`
29872990
* `fs` {Object|null} **Default:** `null`
29882991
* `signal` {AbortSignal|null} **Default:** `null`
2992+
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place
2993+
of `fd`. Windows only. **Default:** `null`
29892994
* Returns: {fs.ReadStream}
29902995
29912996
`options` can include `start` and `end` values to read a range of bytes from
@@ -3006,6 +3011,12 @@ If `fd` points to a character device that only supports blocking reads
30063011
available. This can prevent the process from exiting and the stream from
30073012
closing naturally.
30083013
3014+
On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
3015+
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
3016+
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
3017+
in a file descriptor that the stream owns and closes. The `windowsHandle` option
3018+
throws on non-Windows platforms and cannot be combined with the `fs` option.
3019+
30093020
By default, the stream will emit a `'close'` event after it has been
30103021
destroyed. Set the `emitClose` option to `false` to change this behavior.
30113022
@@ -3056,6 +3067,9 @@ If `options` is a string, then it specifies the encoding.
30563067
<!-- YAML
30573068
added: v0.1.31
30583069
changes:
3070+
- version: REPLACEME
3071+
pr-url: https://github.com/nodejs/node/pull/63851
3072+
description: Add the `windowsHandle` option.
30593073
- version: v22.0.0
30603074
pr-url: https://github.com/nodejs/node/pull/52037
30613075
description: bump default highWaterMark.
@@ -3120,6 +3134,8 @@ changes:
31203134
[`stream.getDefaultHighWaterMark()`][].
31213135
* `flush` {boolean} If `true`, the underlying file descriptor is flushed
31223136
prior to closing it. **Default:** `false`.
3137+
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place
3138+
of `fd`. Windows only. **Default:** `null`
31233139
* Returns: {fs.WriteStream}
31243140
31253141
`options` may also include a `start` option to allow writing data at some
@@ -3134,6 +3150,12 @@ then the file descriptor won't be closed, even if there's an error.
31343150
It is the application's responsibility to close it and make sure there's no
31353151
file descriptor leak.
31363152
3153+
On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
3154+
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
3155+
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
3156+
in a file descriptor that the stream owns and closes. The `windowsHandle` option
3157+
throws on non-Windows platforms and cannot be combined with the `fs` option.
3158+
31373159
By default, the stream will emit a `'close'` event after it has been
31383160
destroyed. Set the `emitClose` option to `false` to change this behavior.
31393161

lib/internal/fs/streams.js

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@ 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 {
2326
deprecate,
27+
isWindows,
2428
kEmptyObject,
2529
} = require('internal/util');
2630
const {
@@ -41,6 +45,8 @@ const {
4145
} = require('internal/fs/utils');
4246
const { Readable, Writable, finished } = require('stream');
4347
const { toPathIfFileURL } = require('internal/url');
48+
const binding = internalBinding('fs');
49+
const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs;
4450
const kIoDone = Symbol('kIoDone');
4551
const kIsPerformingIO = Symbol('kIsPerformingIO');
4652

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

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

177-
if (options.fd == null) {
203+
if (options.fd != null && options.windowsHandle != null) {
204+
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
205+
} else if (options.windowsHandle != null) {
206+
this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY));
207+
} else if (options.fd == null) {
178208
this.fd = null;
179209
this[kFs] = options.fs || fs;
180210
validateFunction(this[kFs].open, 'options.fs.open');
@@ -331,7 +361,11 @@ function WriteStream(path, options) {
331361
// Only buffers are supported.
332362
options.decodeStrings = true;
333363

334-
if (options.fd == null) {
364+
if (options.fd != null && options.windowsHandle != null) {
365+
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
366+
} else if (options.windowsHandle != null) {
367+
this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY));
368+
} else if (options.fd == null) {
335369
this.fd = null;
336370
this[kFs] = options.fs || fs;
337371
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
@@ -4144,6 +4144,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) {
41444144
return info;
41454145
}
41464146

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

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

4240+
#ifdef _WIN32
4241+
SetMethod(isolate, target, "handleToFd", HandleToFd);
4242+
#endif
4243+
42134244
SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);
42144245
SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);
42154246
SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);
@@ -4337,6 +4368,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
43374368
registry->Register(LUTimes);
43384369

43394370
registry->Register(Mkdtemp);
4371+
#ifdef _WIN32
4372+
registry->Register(HandleToFd);
4373+
#endif
43404374
registry->Register(NewFSReqCallback);
43414375

43424376
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)