Skip to content

Commit

Permalink
fs: make promises/write and writeSync params optional
Browse files Browse the repository at this point in the history
This change allows passing objects as "named parameters"

Fixes: #41666
  • Loading branch information
LiviaMedeiros committed Apr 4, 2022
1 parent 0c9273d commit 334eabc
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 5 deletions.
36 changes: 36 additions & 0 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,25 @@ On Linux, positional writes do not work when the file is opened in append mode.
The kernel ignores the position argument and always appends the data to
the end of the file.
#### `filehandle.write(buffer, options)`
<!-- YAML
added: REPLACEME
-->
* `buffer` {Buffer|TypedArray|DataView}
* `options` {Object}
* `offset` {integer} **Default:** `0`
* `length` {integer} **Default:** `buffer.byteLength - offset`
* `position` {integer} **Default:** `null`
* Returns: {Promise}
Write `buffer` to the file.
Similar to the above `filehandle.write` function, this version takes an
optional `options` object. If no `options` object is specified, it will
default with the above values.
#### `filehandle.write(string[, position[, encoding]])`
<!-- YAML
Expand Down Expand Up @@ -5763,6 +5782,23 @@ changes:
For detailed information, see the documentation of the asynchronous version of
this API: [`fs.write(fd, buffer...)`][].
### `fs.writeSync(fd, buffer, options)`
<!-- YAML
added: REPLACEME
-->
* `fd` {integer}
* `buffer` {Buffer|TypedArray|DataView}
* `options` {Object}
* `offset` {integer} **Default:** `0`
* `length` {integer} **Default:** `buffer.byteLength - offset`
* `position` {integer} **Default:** `null`
* Returns: {number} The number of bytes written.
For detailed information, see the documentation of the asynchronous version of
this API: [`fs.write(fd, buffer...)`][].
### `fs.writeSync(fd, string[, position[, encoding]])`
<!-- YAML
Expand Down
17 changes: 14 additions & 3 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -854,15 +854,26 @@ ObjectDefineProperty(write, internalUtil.customPromisifyArgs,
* specified `fd` (file descriptor).
* @param {number} fd
* @param {Buffer | TypedArray | DataView | string} buffer
* @param {number} [offset]
* @param {number} [length]
* @param {number} [position]
* @param {{
* offset?: number;
* length?: number;
* position?: number;
* }} [offset]
* @returns {number}
*/
function writeSync(fd, buffer, offset, length, position) {
fd = getValidatedFd(fd);
const ctx = {};
let result;

if (typeof offset === 'object' && offset !== null) {
({
offset = 0,
length = buffer.byteLength - offset,
position = null
} = offset);
}

if (isArrayBufferView(buffer)) {
if (position === undefined)
position = null;
Expand Down
15 changes: 13 additions & 2 deletions lib/internal/fs/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -560,8 +560,19 @@ async function readv(handle, buffers, position) {
return { bytesRead, buffers };
}

async function write(handle, buffer, offset, length, position) {
if (buffer?.byteLength === 0)
async function write(handle, bufferOrOptions, offset, length, position) {
let buffer = bufferOrOptions;
if (!isArrayBufferView(buffer) && typeof buffer !== 'string') {
validateBuffer(bufferOrOptions?.buffer);
({
buffer,
offset = 0,
length = buffer.byteLength - offset,
position = null
} = bufferOrOptions);
}

if (buffer.byteLength === 0)
return { bytesWritten: 0, buffer };

if (isArrayBufferView(buffer)) {
Expand Down
87 changes: 87 additions & 0 deletions test/parallel/test-fs-promises-write-optional-params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict';

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

// This test ensures that filehandle.write accepts "named parameters" object
// and doesn't interpret objects as strings

const assert = require('assert');
const fsPromises = require('fs').promises;
const path = require('path');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

const dest = path.resolve(tmpdir.path, 'tmp.txt');
const buffer = Buffer.from('zyx');

async function testInvalid(dest, expectedCode, params) {
let fh;
try {
fh = await fsPromises.open(dest, 'w+');
await assert.rejects(
async () => fh.write(params),
{ code: expectedCode });
} finally {
await fh?.close();
}
}

async function testValid(dest, params) {
let fh;
try {
fh = await fsPromises.open(dest, 'w+');
const writeResult = await fh.write(params);
const writeBufCopy = Uint8Array.prototype.slice.call(writeResult.buffer);
const readResult = await fh.read(params);
const readBufCopy = Uint8Array.prototype.slice.call(readResult.buffer);

assert.ok(writeResult.bytesWritten >= readResult.bytesRead);
if (params.length !== undefined && params.length !== null) {
assert.strictEqual(writeResult.bytesWritten, params.length);
}
if (params.offset === undefined || params.offset === 0) {
assert.deepStrictEqual(writeBufCopy, readBufCopy);
}
assert.deepStrictEqual(writeResult.buffer, readResult.buffer);
} finally {
await fh?.close();
}
}

(async () => {
// Test if first argument is not wrongly interpreted as ArrayBufferView|string
for (const badParams of [
undefined, null, true, 42, 42n, Symbol('42'), NaN, [],
{},
{ buffer: 'amNotParam' },
{ string: 'amNotParam' },
{ buffer: new Uint8Array(1).buffer },
new Date(),
new String('notPrimitive'),
{ toString() { return 'amObject'; } },
{ [Symbol.toPrimitive]: (hint) => 'amObject' },
]) {
await testInvalid(dest, 'ERR_INVALID_ARG_TYPE', badParams);
}

// Various invalid params
await testInvalid(dest, 'ERR_OUT_OF_RANGE', { buffer, length: 5 });
await testInvalid(dest, 'ERR_OUT_OF_RANGE', { buffer, offset: 5 });
await testInvalid(dest, 'ERR_OUT_OF_RANGE', { buffer, length: 1, offset: 3 });
await testInvalid(dest, 'ERR_OUT_OF_RANGE', { buffer, length: -1 });
await testInvalid(dest, 'ERR_OUT_OF_RANGE', { buffer, offset: -1 });

// Test compatibility with filehandle.read counterpart with reused params
for (const params of [
{ buffer },
{ buffer, length: 1 },
{ buffer, position: 5 },
{ buffer, length: 1, position: 5 },
{ buffer, length: 1, position: -1, offset: 2 },
{ buffer, length: null },
{ buffer, offset: 1 },
]) {
await testValid(dest, params);
}
})().then(common.mustCall());
82 changes: 82 additions & 0 deletions test/parallel/test-fs-write-sync-optional-params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use strict';

require('../common');

// This test ensures that fs.writeSync accepts "named parameters" object
// and doesn't interpret objects as strings

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

tmpdir.refresh();

const dest = path.resolve(tmpdir.path, 'tmp.txt');
const buffer = Buffer.from('zyx');

function testInvalid(dest, expectedCode, ...bufferAndParams) {
let fd;
try {
fd = fs.openSync(dest, 'w+');
assert.throws(
() => fs.writeSync(fd, ...bufferAndParams),
{ code: expectedCode });
} finally {
if (fd != null) fs.closeSync(fd);
}
}

function testValid(dest, buffer, params) {
let fd;
try {
fd = fs.openSync(dest, 'w+');
const bytesWritten = fs.writeSync(fd, buffer, params);
const bytesRead = fs.readSync(fd, buffer, params);

assert.ok(bytesWritten >= bytesRead);
if (params.length !== undefined && params.length !== null) {
assert.strictEqual(bytesWritten, params.length);
}
} finally {
if (fd != null) fs.closeSync(fd);
}
}

{
// Test if second argument is not wrongly interpreted as string or params
for (const badBuffer of [
undefined, null, true, 42, 42n, Symbol('42'), NaN, [],
{},
{ buffer: 'amNotParam' },
{ string: 'amNotParam' },
{ buffer: new Uint8Array(1) },
{ buffer: new Uint8Array(1).buffer },
new Date(),
new String('notPrimitive'),
{ toString() { return 'amObject'; } },
{ [Symbol.toPrimitive]: (hint) => 'amObject' },
]) {
testInvalid(dest, 'ERR_INVALID_ARG_TYPE', badBuffer);
}

// Various invalid params
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: 5 });
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { offset: 5 });
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: 1, offset: 3 });
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: -1 });
testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { offset: -1 });

// Test compatibility with fs.readSync counterpart with reused params
for (const params of [
{},
{ length: 1 },
{ position: 5 },
{ length: 1, position: 5 },
{ length: 1, position: -1, offset: 2 },
{ length: null },
{ offset: 1 },
]) {
testValid(dest, buffer, params);
}
}

0 comments on commit 334eabc

Please sign in to comment.