Skip to content

Commit eae6b26

Browse files
committed
fs: make params in writing methods optional
This change allows passing objects as "named parameters" Fixes: nodejs#41666
1 parent 51fd5db commit eae6b26

File tree

5 files changed

+234
-6
lines changed

5 files changed

+234
-6
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
@@ -5769,6 +5788,23 @@ changes:
57695788
For detailed information, see the documentation of the asynchronous version of
57705789
this API: [`fs.write(fd, buffer...)`][].
57715790
5791+
### `fs.writeSync(fd, buffer, options)`
5792+
5793+
<!-- YAML
5794+
added: REPLACEME
5795+
-->
5796+
5797+
* `fd` {integer}
5798+
* `buffer` {Buffer|TypedArray|DataView}
5799+
* `options` {Object}
5800+
* `offset` {integer} **Default:** `0`
5801+
* `length` {integer} **Default:** `buffer.byteLength - offset`
5802+
* `position` {integer} **Default:** `null`
5803+
* Returns: {number} The number of bytes written.
5804+
5805+
For detailed information, see the documentation of the asynchronous version of
5806+
this API: [`fs.write(fd, buffer...)`][].
5807+
57725808
### `fs.writeSync(fd, string[, position[, encoding]])`
57735809
57745810
<!-- YAML

lib/fs.js

+16-4
Original file line numberDiff line numberDiff line change
@@ -862,15 +862,27 @@ ObjectDefineProperty(write, internalUtil.customPromisifyArgs,
862862
* specified `fd` (file descriptor).
863863
* @param {number} fd
864864
* @param {Buffer | TypedArray | DataView | string} buffer
865-
* @param {number} [offset]
866-
* @param {number} [length]
867-
* @param {number} [position]
865+
* @param {{
866+
* offset?: number;
867+
* length?: number;
868+
* position?: number;
869+
* }} [offsetOrOptions]
868870
* @returns {number}
869871
*/
870-
function writeSync(fd, buffer, offset, length, position) {
872+
function writeSync(fd, buffer, offsetOrOptions, length, position) {
871873
fd = getValidatedFd(fd);
872874
const ctx = {};
873875
let result;
876+
877+
let offset = offsetOrOptions;
878+
if (typeof offset === 'object' && offset !== null) {
879+
({
880+
offset = 0,
881+
length = buffer.byteLength - offset,
882+
position = null
883+
} = offsetOrOptions);
884+
}
885+
874886
if (isArrayBufferView(buffer)) {
875887
if (position === undefined)
876888
position = null;

lib/internal/fs/promises.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -560,8 +560,19 @@ async function readv(handle, buffers, position) {
560560
return { bytesRead, buffers };
561561
}
562562

563-
async function write(handle, buffer, offset, length, position) {
564-
if (buffer?.byteLength === 0)
563+
async function write(handle, bufferOrOptions, offset, length, position) {
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)