Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e0e4139

Browse files
committedApr 4, 2022
fs: make promises/write and writeSync params optional
This change allows passing objects as "named parameters" Fixes: #41666
1 parent 0c9273d commit e0e4139

File tree

5 files changed

+231
-4
lines changed

5 files changed

+231
-4
lines changed
 

‎doc/api/fs.md

+36
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,25 @@ On Linux, positional writes do not work when the file is opened in append mode.
619619
The kernel ignores the position argument and always appends the data to
620620
the end of the file.
621621
622+
#### `filehandle.write(buffer, options)`
623+
624+
<!-- YAML
625+
added: REPLACEME
626+
-->
627+
628+
* `buffer` {Buffer|TypedArray|DataView}
629+
* `options` {Object}
630+
* `offset` {integer} **Default:** `0`
631+
* `length` {integer} **Default:** `buffer.byteLength - offset`
632+
* `position` {integer} **Default:** `null`
633+
* Returns: {Promise}
634+
635+
Write `buffer` to the file.
636+
637+
Similar to the above `filehandle.write` function, this version takes an
638+
optional `options` object. If no `options` object is specified, it will
639+
default with the above values.
640+
622641
#### `filehandle.write(string[, position[, encoding]])`
623642
624643
<!-- YAML
@@ -5763,6 +5782,23 @@ changes:
57635782
For detailed information, see the documentation of the asynchronous version of
57645783
this API: [`fs.write(fd, buffer...)`][].
57655784
5785+
### `fs.writeSync(fd, buffer, options)`
5786+
5787+
<!-- YAML
5788+
added: REPLACEME
5789+
-->
5790+
5791+
* `fd` {integer}
5792+
* `buffer` {Buffer|TypedArray|DataView}
5793+
* `options` {Object}
5794+
* `offset` {integer} **Default:** `0`
5795+
* `length` {integer} **Default:** `buffer.byteLength - offset`
5796+
* `position` {integer} **Default:** `null`
5797+
* Returns: {number} The number of bytes written.
5798+
5799+
For detailed information, see the documentation of the asynchronous version of
5800+
this API: [`fs.write(fd, buffer...)`][].
5801+
57665802
### `fs.writeSync(fd, string[, position[, encoding]])`
57675803
57685804
<!-- YAML

‎lib/fs.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -854,15 +854,26 @@ ObjectDefineProperty(write, internalUtil.customPromisifyArgs,
854854
* specified `fd` (file descriptor).
855855
* @param {number} fd
856856
* @param {Buffer | TypedArray | DataView | string} buffer
857-
* @param {number} [offset]
858-
* @param {number} [length]
859-
* @param {number} [position]
857+
* @param {{
858+
* offset?: number;
859+
* length?: number;
860+
* position?: number;
861+
* }} [offset]
860862
* @returns {number}
861863
*/
862864
function writeSync(fd, buffer, offset, length, position) {
863865
fd = getValidatedFd(fd);
864866
const ctx = {};
865867
let result;
868+
869+
if (typeof offset === 'object' && offset !== null) {
870+
({
871+
offset = 0,
872+
length = buffer.byteLength - offset,
873+
position = null
874+
} = offset);
875+
}
876+
866877
if (isArrayBufferView(buffer)) {
867878
if (position === undefined)
868879
position = null;

‎lib/internal/fs/promises.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,18 @@ async function readv(handle, buffers, position) {
561561
}
562562

563563
async function write(handle, buffer, offset, length, position) {
564-
if (buffer?.byteLength === 0)
564+
let buffer = bufferOrOptions;
565+
if (!isArrayBufferView(buffer) && typeof buffer !== 'string') {
566+
validateBuffer(bufferOrOptions?.buffer);
567+
({
568+
buffer,
569+
offset = 0,
570+
length = buffer.byteLength - offset,
571+
position = null
572+
} = bufferOrOptions);
573+
}
574+
575+
if (buffer.byteLength === 0)
565576
return { bytesWritten: 0, buffer };
566577

567578
if (isArrayBufferView(buffer)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
// This test ensures that filehandle.write accepts "named parameters" object
6+
// and doesn't interpret objects as strings
7+
8+
const assert = require('assert');
9+
const fsPromises = require('fs').promises;
10+
const path = require('path');
11+
const tmpdir = require('../common/tmpdir');
12+
13+
tmpdir.refresh();
14+
15+
const dest = path.resolve(tmpdir.path, 'tmp.txt');
16+
const buffer = Buffer.from('zyx');
17+
18+
async function testInvalid(dest, expectedCode, params) {
19+
let fh;
20+
try {
21+
fh = await fsPromises.open(dest, 'w+');
22+
await assert.rejects(
23+
async () => fh.write(params),
24+
{ code: expectedCode });
25+
} finally {
26+
await fh?.close();
27+
}
28+
}
29+
30+
async function testValid(dest, params) {
31+
let fh;
32+
try {
33+
fh = await fsPromises.open(dest, 'w+');
34+
const writeResult = await fh.write(params);
35+
const writeBufCopy = Uint8Array.prototype.slice.call(writeResult.buffer);
36+
const readResult = await fh.read(params);
37+
const readBufCopy = Uint8Array.prototype.slice.call(readResult.buffer);
38+
39+
assert.ok(writeResult.bytesWritten >= readResult.bytesRead);
40+
if (params.length !== undefined && params.length !== null) {
41+
assert.strictEqual(writeResult.bytesWritten, params.length);
42+
}
43+
if (params.offset === undefined || params.offset === 0) {
44+
assert.deepStrictEqual(writeBufCopy, readBufCopy);
45+
}
46+
assert.deepStrictEqual(writeResult.buffer, readResult.buffer);
47+
} finally {
48+
await fh?.close();
49+
}
50+
}
51+
52+
(async () => {
53+
// Test if first argument is not wrongly interpreted as ArrayBufferView|string
54+
for (const badParams of [
55+
undefined, null, true, 42, 42n, Symbol('42'), NaN, [],
56+
{},
57+
{ buffer: 'amNotParam' },
58+
{ string: 'amNotParam' },
59+
{ buffer: new Uint8Array(1).buffer },
60+
new Date(),
61+
new String('notPrimitive'),
62+
{ toString() { return 'amObject'; } },
63+
{ [Symbol.toPrimitive]: (hint) => 'amObject' },
64+
]) {
65+
await testInvalid(dest, 'ERR_INVALID_ARG_TYPE', badParams);
66+
}
67+
68+
// Various invalid params
69+
await testInvalid(dest, 'ERR_OUT_OF_RANGE', { buffer, length: 5 });
70+
await testInvalid(dest, 'ERR_OUT_OF_RANGE', { buffer, offset: 5 });
71+
await testInvalid(dest, 'ERR_OUT_OF_RANGE', { buffer, length: 1, offset: 3 });
72+
await testInvalid(dest, 'ERR_OUT_OF_RANGE', { buffer, length: -1 });
73+
await testInvalid(dest, 'ERR_OUT_OF_RANGE', { buffer, offset: -1 });
74+
75+
// Test compatibility with filehandle.read counterpart with reused params
76+
for (const params of [
77+
{ buffer },
78+
{ buffer, length: 1 },
79+
{ buffer, position: 5 },
80+
{ buffer, length: 1, position: 5 },
81+
{ buffer, length: 1, position: -1, offset: 2 },
82+
{ buffer, length: null },
83+
{ buffer, offset: 1 },
84+
]) {
85+
await testValid(dest, params);
86+
}
87+
})().then(common.mustCall());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
// This test ensures that fs.writeSync accepts "named parameters" object
6+
// and doesn't interpret objects as strings
7+
8+
const assert = require('assert');
9+
const fs = require('fs');
10+
const path = require('path');
11+
const tmpdir = require('../common/tmpdir');
12+
13+
tmpdir.refresh();
14+
15+
const dest = path.resolve(tmpdir.path, 'tmp.txt');
16+
const buffer = Buffer.from('zyx');
17+
18+
function testInvalid(dest, expectedCode, ...bufferAndParams) {
19+
let fd;
20+
try {
21+
fd = fs.openSync(dest, 'w+');
22+
assert.throws(
23+
() => fs.writeSync(fd, ...bufferAndParams),
24+
{ code: expectedCode });
25+
} finally {
26+
if (fd != null) fs.closeSync(fd);
27+
}
28+
}
29+
30+
function testValid(dest, buffer, params) {
31+
let fd;
32+
try {
33+
fd = fs.openSync(dest, 'w+');
34+
const bytesWritten = fs.writeSync(fd, buffer, params);
35+
const bytesRead = fs.readSync(fd, buffer, params);
36+
37+
assert.ok(bytesWritten >= bytesRead);
38+
if (params.length !== undefined && params.length !== null) {
39+
assert.strictEqual(bytesWritten, params.length);
40+
}
41+
} finally {
42+
if (fd != null) fs.closeSync(fd);
43+
}
44+
}
45+
46+
{
47+
// Test if second argument is not wrongly interpreted as string or params
48+
for (const badBuffer of [
49+
undefined, null, true, 42, 42n, Symbol('42'), NaN, [],
50+
{},
51+
{ buffer: 'amNotParam' },
52+
{ string: 'amNotParam' },
53+
{ buffer: new Uint8Array(1) },
54+
{ buffer: new Uint8Array(1).buffer },
55+
new Date(),
56+
new String('notPrimitive'),
57+
{ toString() { return 'amObject'; } },
58+
{ [Symbol.toPrimitive]: (hint) => 'amObject' },
59+
]) {
60+
testInvalid(dest, 'ERR_INVALID_ARG_TYPE', badBuffer);
61+
}
62+
63+
// Various invalid params
64+
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: 5 });
65+
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { offset: 5 });
66+
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: 1, offset: 3 });
67+
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: -1 });
68+
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { offset: -1 });
69+
70+
// Test compatibility with fs.readSync counterpart with reused params
71+
for (const params of [
72+
{},
73+
{ length: 1 },
74+
{ position: 5 },
75+
{ length: 1, position: 5 },
76+
{ length: 1, position: -1, offset: 2 },
77+
{ length: null },
78+
{ offset: 1 },
79+
]) {
80+
testValid(dest, buffer, params);
81+
}
82+
}

0 commit comments

Comments
 (0)
Please sign in to comment.