Skip to content

Commit e8edd28

Browse files
committed
fs: support caller-supplied readFile() buffers
PR-URL: #63634 Signed-off-by: Matteo Collina <matteo.collina@gmail.com>
1 parent 8d0a3b8 commit e8edd28

7 files changed

Lines changed: 635 additions & 18 deletions

File tree

doc/api/fs.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,11 +696,18 @@ close the `FileHandle` automatically. User code must still call the
696696
697697
<!-- YAML
698698
added: v10.0.0
699+
changes:
700+
- version: REPLACEME
701+
pr-url: https://github.com/nodejs/node/pull/63634
702+
description: Added support for the `buffer` option.
699703
-->
700704
701705
* `options` {Object|string}
702706
* `encoding` {string|null} **Default:** `null`
703707
* `signal` {AbortSignal} allows aborting an in-progress readFile
708+
* `buffer` {Buffer|TypedArray|DataView|Function} A buffer to read into, or a
709+
synchronous function called with the file size and returning the buffer to
710+
read into.
704711
* Returns: {Promise} Fulfills upon a successful read with the contents of the
705712
file. If no encoding is specified (using `options.encoding`), the data is
706713
returned as a {Buffer} object. Otherwise, the data will be a string.
@@ -709,6 +716,11 @@ Asynchronously reads the entire contents of a file.
709716
710717
If `options` is a string, then it specifies the `encoding`.
711718
719+
If `buffer` is provided and no encoding is specified, the returned {Buffer} is
720+
a view over the supplied buffer containing only the bytes read. If the
721+
supplied buffer is too small to contain the entire file, the operation will
722+
fail.
723+
712724
The {FileHandle} has to support reading.
713725
714726
If one or more `filehandle.read()` calls are made on a file handle and then a
@@ -1765,6 +1777,9 @@ try {
17651777
<!-- YAML
17661778
added: v10.0.0
17671779
changes:
1780+
- version: REPLACEME
1781+
pr-url: https://github.com/nodejs/node/pull/63634
1782+
description: Added support for the `buffer` option.
17681783
- version:
17691784
- v15.2.0
17701785
- v14.17.0
@@ -1778,6 +1793,9 @@ changes:
17781793
* `encoding` {string|null} **Default:** `null`
17791794
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
17801795
* `signal` {AbortSignal} allows aborting an in-progress readFile
1796+
* `buffer` {Buffer|TypedArray|DataView|Function} A buffer to read into, or a
1797+
synchronous function called with the file size and returning the buffer to
1798+
read into.
17811799
* Returns: {Promise} Fulfills with the contents of the file.
17821800
17831801
Asynchronously reads the entire contents of a file.
@@ -1787,6 +1805,11 @@ as a {Buffer} object. Otherwise, the data will be a string.
17871805
17881806
If `options` is a string, then it specifies the encoding.
17891807
1808+
If `buffer` is provided and no encoding is specified, the returned {Buffer} is
1809+
a view over the supplied buffer containing only the bytes read. If the
1810+
supplied buffer is too small to contain the entire file, the promise will be
1811+
rejected.
1812+
17901813
When the `path` is a directory, the behavior of `fsPromises.readFile()` is
17911814
platform-specific. On macOS, Linux, and Windows, the promise will be rejected
17921815
with an error. On FreeBSD, a representation of the directory's contents will be
@@ -4225,6 +4248,9 @@ If `options.withFileTypes` is set to `true`, the `files` array will contain
42254248
<!-- YAML
42264249
added: v0.1.29
42274250
changes:
4251+
- version: REPLACEME
4252+
pr-url: https://github.com/nodejs/node/pull/63634
4253+
description: Added support for the `buffer` option.
42284254
- version: v18.0.0
42294255
pr-url: https://github.com/nodejs/node/pull/41678
42304256
description: Passing an invalid callback to the `callback` argument
@@ -4266,6 +4292,9 @@ changes:
42664292
* `encoding` {string|null} **Default:** `null`
42674293
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
42684294
* `signal` {AbortSignal} allows aborting an in-progress readFile
4295+
* `buffer` {Buffer|TypedArray|DataView|Function} A buffer to read into, or a
4296+
synchronous function called with the file size and returning the buffer to
4297+
read into.
42694298
* `callback` {Function}
42704299
* `err` {Error|AggregateError}
42714300
* `data` {string|Buffer}
@@ -4286,6 +4315,11 @@ contents of the file.
42864315
42874316
If no encoding is specified, then the raw buffer is returned.
42884317
4318+
If `buffer` is provided and no encoding is specified, the returned {Buffer} is
4319+
a view over the supplied buffer containing only the bytes read. If the
4320+
supplied buffer is too small to contain the entire file, the callback is
4321+
called with an error.
4322+
42894323
If `options` is a string, then it specifies the encoding:
42904324
42914325
```mjs
@@ -6428,6 +6462,9 @@ If `options.withFileTypes` is set to `true`, the result will contain
64286462
<!-- YAML
64296463
added: v0.1.8
64306464
changes:
6465+
- version: REPLACEME
6466+
pr-url: https://github.com/nodejs/node/pull/63634
6467+
description: Added support for the `buffer` option.
64316468
- version: v7.6.0
64326469
pr-url: https://github.com/nodejs/node/pull/10739
64336470
description: The `path` parameter can be a WHATWG `URL` object using `file:`
@@ -6441,6 +6478,9 @@ changes:
64416478
* `options` {Object|string}
64426479
* `encoding` {string|null} **Default:** `null`
64436480
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
6481+
* `buffer` {Buffer|TypedArray|DataView|Function} A buffer to read into, or a
6482+
synchronous function called with the file size and returning the buffer to
6483+
read into.
64446484
* Returns: {string|Buffer}
64456485
64466486
Returns the contents of the `path`.
@@ -6451,6 +6491,11 @@ this API: [`fs.readFile()`][].
64516491
If the `encoding` option is specified then this function returns a
64526492
string. Otherwise it returns a buffer.
64536493
6494+
If `buffer` is provided and no encoding is specified, the returned {Buffer} is
6495+
a view over the supplied buffer containing only the bytes read. If the
6496+
supplied buffer is too small to contain the entire file, an error will be
6497+
thrown.
6498+
64546499
Similar to [`fs.readFile()`][], when the path is a directory, the behavior of
64556500
`fs.readFileSync()` is platform-specific.
64566501

lib/fs.js

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ const {
111111
handleErrorFromBinding,
112112
preprocessSymlinkDestination,
113113
Stats,
114+
getReadFileBuffer,
115+
getReadFileBufferByteLengthName,
114116
getStatFsFromBinding,
115117
getStatsFromBinding,
116118
realpathCacheKey,
@@ -123,6 +125,7 @@ const {
123125
validateOffsetLengthWrite,
124126
validatePath,
125127
validatePosition,
128+
validateReadFileBufferOptions,
126129
validateRmOptions,
127130
validateRmOptionsSync,
128131
validateRmdirOptions,
@@ -319,13 +322,7 @@ function readFileAfterStat(err, stats) {
319322
}
320323

321324
try {
322-
if (size === 0) {
323-
// TODO(BridgeAR): If an encoding is set, use the StringDecoder to concat
324-
// the result and reuse the buffer instead of allocating a new one.
325-
context.buffers = [];
326-
} else {
327-
context.buffer = Buffer.allocUnsafeSlow(size);
328-
}
325+
context.prepare();
329326
} catch (err) {
330327
return context.close(err);
331328
}
@@ -358,8 +355,9 @@ function readFile(path, options, callback) {
358355
callback ||= options;
359356
validateFunction(callback, 'cb');
360357
options = getOptions(options, { flag: 'r' });
358+
validateReadFileBufferOptions(options);
361359
ReadFileContext ??= require('internal/fs/read/context');
362-
const context = new ReadFileContext(callback, options.encoding);
360+
const context = new ReadFileContext(callback, options);
363361
context.isUserFd = isFd(path); // File descriptor ownership
364362

365363
if (options.signal) {
@@ -405,6 +403,18 @@ function tryCreateBuffer(size, fd, isUserFd) {
405403
return buffer;
406404
}
407405

406+
function tryGetReadFileBuffer(options, size, fd, isUserFd) {
407+
let threw = true;
408+
let buffer;
409+
try {
410+
buffer = getReadFileBuffer(options, size);
411+
threw = false;
412+
} finally {
413+
if (threw && !isUserFd) fs.closeSync(fd);
414+
}
415+
return buffer;
416+
}
417+
408418
function tryReadSync(fd, isUserFd, buffer, pos, len) {
409419
let threw = true;
410420
let bytesRead;
@@ -417,6 +427,36 @@ function tryReadSync(fd, isUserFd, buffer, pos, len) {
417427
return bytesRead;
418428
}
419429

430+
function tryReadSyncWithUserBuffer(fd, isUserFd, buffer, byteLengthName) {
431+
let pos = 0;
432+
let bytesRead = 0;
433+
434+
while (pos < buffer.byteLength) {
435+
bytesRead = tryReadSync(fd, isUserFd, buffer, pos, buffer.byteLength - pos);
436+
pos += bytesRead;
437+
438+
if (bytesRead === 0) {
439+
return pos;
440+
}
441+
}
442+
443+
const extraBuffer = tryCreateBuffer(1, fd, isUserFd);
444+
bytesRead = tryReadSync(fd, isUserFd, extraBuffer, 0, 1);
445+
446+
if (bytesRead !== 0) {
447+
if (!isUserFd) {
448+
fs.closeSync(fd);
449+
}
450+
throw new ERR_INVALID_ARG_VALUE(
451+
byteLengthName,
452+
buffer.byteLength,
453+
'is too small to contain the entire file',
454+
);
455+
}
456+
457+
return pos;
458+
}
459+
420460
/**
421461
* Synchronously reads the entire contents of a file.
422462
* @param {string | Buffer | URL | number} path
@@ -428,8 +468,11 @@ function tryReadSync(fd, isUserFd, buffer, pos, len) {
428468
*/
429469
function readFileSync(path, options) {
430470
options = getOptions(options, { flag: 'r' });
471+
validateReadFileBufferOptions(options);
472+
const hasUserBuffer = options.buffer !== undefined;
431473

432-
if (options.encoding === 'utf8' || options.encoding === 'utf-8') {
474+
if ((options.encoding === 'utf8' || options.encoding === 'utf-8') &&
475+
!hasUserBuffer) {
433476
if (!isInt32(path)) {
434477
path = getValidatedPath(path);
435478
}
@@ -445,15 +488,31 @@ function readFileSync(path, options) {
445488
let buffer; // Single buffer with file data
446489
let buffers; // List for when size is unknown
447490

448-
if (size === 0) {
491+
if (hasUserBuffer) {
492+
buffer = tryGetReadFileBuffer(options, size, fd, isUserFd);
493+
} else if (size === 0) {
449494
buffers = [];
450495
} else {
451496
buffer = tryCreateBuffer(size, fd, isUserFd);
452497
}
453498

454499
let bytesRead;
455500

456-
if (size !== 0) {
501+
if (hasUserBuffer) {
502+
if (size !== 0) {
503+
do {
504+
bytesRead = tryReadSync(fd, isUserFd, buffer, pos, size - pos);
505+
pos += bytesRead;
506+
} while (bytesRead !== 0 && pos < size);
507+
} else {
508+
pos = tryReadSyncWithUserBuffer(
509+
fd,
510+
isUserFd,
511+
buffer,
512+
getReadFileBufferByteLengthName(options),
513+
);
514+
}
515+
} else if (size !== 0) {
457516
do {
458517
bytesRead = tryReadSync(fd, isUserFd, buffer, pos, size - pos);
459518
pos += bytesRead;
@@ -474,7 +533,9 @@ function readFileSync(path, options) {
474533
if (!isUserFd)
475534
fs.closeSync(fd);
476535

477-
if (size === 0) {
536+
if (hasUserBuffer) {
537+
buffer = buffer.subarray(0, pos);
538+
} else if (size === 0) {
478539
// Data was collected into the buffers list.
479540
buffer = Buffer.concat(buffers, pos);
480541
} else if (pos < size) {

lib/internal/fs/promises.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ const {
6666
getStatFsFromBinding,
6767
getStatsFromBinding,
6868
getValidatedPath,
69+
getReadFileBuffer,
70+
getReadFileBufferByteLengthName,
6971
preprocessSymlinkDestination,
7072
stringToFlags,
7173
stringToSymlinkType,
@@ -76,6 +78,7 @@ const {
7678
validateOffsetLengthRead,
7779
validateOffsetLengthWrite,
7880
validatePosition,
81+
validateReadFileBufferOptions,
7982
validateRmOptions,
8083
validateRmdirOptions,
8184
validateStringAfterArrayBufferView,
@@ -1157,6 +1160,56 @@ async function writeFileHandle(filehandle, data, signal, encoding) {
11571160
} while (remaining > 0);
11581161
}
11591162

1163+
async function readFileHandleWithUserBuffer(filehandle, options, size) {
1164+
const signal = options?.signal;
1165+
const encoding = options?.encoding;
1166+
const buffer = getReadFileBuffer(options, size);
1167+
const byteLengthName = getReadFileBufferByteLengthName(options);
1168+
let totalRead = 0;
1169+
1170+
while (totalRead < buffer.byteLength) {
1171+
checkAborted(signal);
1172+
1173+
const length = size === 0 ?
1174+
buffer.byteLength - totalRead :
1175+
MathMin(size - totalRead, kReadFileBufferLength);
1176+
1177+
const bytesRead = (await PromisePrototypeThen(
1178+
binding.read(filehandle.fd, buffer, totalRead, length, -1, kUsePromises),
1179+
undefined,
1180+
handleErrorFromBinding,
1181+
)) ?? 0;
1182+
1183+
totalRead += bytesRead;
1184+
1185+
if (bytesRead === 0 || totalRead === size) {
1186+
const result = buffer.subarray(0, totalRead);
1187+
return encoding ? result.toString(encoding) : result;
1188+
}
1189+
}
1190+
1191+
if (size === 0) {
1192+
checkAborted(signal);
1193+
1194+
const extraBuffer = Buffer.allocUnsafeSlow(1);
1195+
const bytesRead = (await PromisePrototypeThen(
1196+
binding.read(filehandle.fd, extraBuffer, 0, 1, -1, kUsePromises),
1197+
undefined,
1198+
handleErrorFromBinding,
1199+
)) ?? 0;
1200+
1201+
if (bytesRead !== 0) {
1202+
throw new ERR_INVALID_ARG_VALUE(
1203+
byteLengthName,
1204+
buffer.byteLength,
1205+
'is too small to contain the entire file',
1206+
);
1207+
}
1208+
}
1209+
1210+
return encoding ? buffer.toString(encoding) : buffer.subarray(0, totalRead);
1211+
}
1212+
11601213
async function readFileHandle(filehandle, options) {
11611214
const signal = options?.signal;
11621215
const encoding = options?.encoding;
@@ -1185,6 +1238,10 @@ async function readFileHandle(filehandle, options) {
11851238
if (size > kIoMaxLength)
11861239
throw new ERR_FS_FILE_TOO_LARGE(size);
11871240

1241+
if (options.buffer !== undefined) {
1242+
return readFileHandleWithUserBuffer(filehandle, options, size);
1243+
}
1244+
11881245
let totalRead = 0;
11891246
const noSize = size === 0;
11901247
let buffer = Buffer.allocUnsafeSlow(length);
@@ -1925,6 +1982,7 @@ async function appendFile(path, data, options) {
19251982

19261983
async function readFile(path, options) {
19271984
options = getOptions(options, { flag: 'r' });
1985+
validateReadFileBufferOptions(options);
19281986
const flag = options.flag || 'r';
19291987

19301988
if (path instanceof FileHandle)

0 commit comments

Comments
 (0)