Skip to content

Commit 50d050d

Browse files
committed
fs: support caller-supplied readFile() buffers
1 parent 8d0a3b8 commit 50d050d

7 files changed

Lines changed: 627 additions & 18 deletions

File tree

doc/api/fs.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,9 @@ added: v10.0.0
701701
* `options` {Object|string}
702702
* `encoding` {string|null} **Default:** `null`
703703
* `signal` {AbortSignal} allows aborting an in-progress readFile
704+
* `buffer` {Buffer|TypedArray|DataView} A buffer to read into.
705+
* `getBuffer` {Function} A synchronous function called with the file size and
706+
returning the buffer to read into.
704707
* Returns: {Promise} Fulfills upon a successful read with the contents of the
705708
file. If no encoding is specified (using `options.encoding`), the data is
706709
returned as a {Buffer} object. Otherwise, the data will be a string.
@@ -709,6 +712,11 @@ Asynchronously reads the entire contents of a file.
709712
710713
If `options` is a string, then it specifies the `encoding`.
711714
715+
The `buffer` and `getBuffer` options cannot be used together. If one of them is
716+
provided and no encoding is specified, the returned {Buffer} is a view over the
717+
supplied buffer containing only the bytes read. If the supplied buffer is too
718+
small to contain the entire file, the operation will fail.
719+
712720
The {FileHandle} has to support reading.
713721
714722
If one or more `filehandle.read()` calls are made on a file handle and then a
@@ -1778,6 +1786,9 @@ changes:
17781786
* `encoding` {string|null} **Default:** `null`
17791787
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
17801788
* `signal` {AbortSignal} allows aborting an in-progress readFile
1789+
* `buffer` {Buffer|TypedArray|DataView} A buffer to read into.
1790+
* `getBuffer` {Function} A synchronous function called with the file size and
1791+
returning the buffer to read into.
17811792
* Returns: {Promise} Fulfills with the contents of the file.
17821793
17831794
Asynchronously reads the entire contents of a file.
@@ -1787,6 +1798,11 @@ as a {Buffer} object. Otherwise, the data will be a string.
17871798
17881799
If `options` is a string, then it specifies the encoding.
17891800
1801+
The `buffer` and `getBuffer` options cannot be used together. If one of them is
1802+
provided and no encoding is specified, the returned {Buffer} is a view over the
1803+
supplied buffer containing only the bytes read. If the supplied buffer is too
1804+
small to contain the entire file, the promise will be rejected.
1805+
17901806
When the `path` is a directory, the behavior of `fsPromises.readFile()` is
17911807
platform-specific. On macOS, Linux, and Windows, the promise will be rejected
17921808
with an error. On FreeBSD, a representation of the directory's contents will be
@@ -4266,6 +4282,9 @@ changes:
42664282
* `encoding` {string|null} **Default:** `null`
42674283
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
42684284
* `signal` {AbortSignal} allows aborting an in-progress readFile
4285+
* `buffer` {Buffer|TypedArray|DataView} A buffer to read into.
4286+
* `getBuffer` {Function} A synchronous function called with the file size and
4287+
returning the buffer to read into.
42694288
* `callback` {Function}
42704289
* `err` {Error|AggregateError}
42714290
* `data` {string|Buffer}
@@ -4286,6 +4305,11 @@ contents of the file.
42864305
42874306
If no encoding is specified, then the raw buffer is returned.
42884307
4308+
The `buffer` and `getBuffer` options cannot be used together. If one of them is
4309+
provided and no encoding is specified, the returned {Buffer} is a view over the
4310+
supplied buffer containing only the bytes read. If the supplied buffer is too
4311+
small to contain the entire file, the callback is called with an error.
4312+
42894313
If `options` is a string, then it specifies the encoding:
42904314
42914315
```mjs
@@ -6441,6 +6465,9 @@ changes:
64416465
* `options` {Object|string}
64426466
* `encoding` {string|null} **Default:** `null`
64436467
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
6468+
* `buffer` {Buffer|TypedArray|DataView} A buffer to read into.
6469+
* `getBuffer` {Function} A synchronous function called with the file size and
6470+
returning the buffer to read into.
64446471
* Returns: {string|Buffer}
64456472
64466473
Returns the contents of the `path`.
@@ -6451,6 +6478,11 @@ this API: [`fs.readFile()`][].
64516478
If the `encoding` option is specified then this function returns a
64526479
string. Otherwise it returns a buffer.
64536480
6481+
The `buffer` and `getBuffer` options cannot be used together. If one of them is
6482+
provided and no encoding is specified, the returned {Buffer} is a view over the
6483+
supplied buffer containing only the bytes read. If the supplied buffer is too
6484+
small to contain the entire file, an error will be thrown.
6485+
64546486
Similar to [`fs.readFile()`][], when the path is a directory, the behavior of
64556487
`fs.readFileSync()` is platform-specific.
64566488

lib/fs.js

Lines changed: 74 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,12 @@ 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 =
473+
options.buffer !== undefined || options.getBuffer !== undefined;
431474

432-
if (options.encoding === 'utf8' || options.encoding === 'utf-8') {
475+
if ((options.encoding === 'utf8' || options.encoding === 'utf-8') &&
476+
!hasUserBuffer) {
433477
if (!isInt32(path)) {
434478
path = getValidatedPath(path);
435479
}
@@ -445,15 +489,31 @@ function readFileSync(path, options) {
445489
let buffer; // Single buffer with file data
446490
let buffers; // List for when size is unknown
447491

448-
if (size === 0) {
492+
if (hasUserBuffer) {
493+
buffer = tryGetReadFileBuffer(options, size, fd, isUserFd);
494+
} else if (size === 0) {
449495
buffers = [];
450496
} else {
451497
buffer = tryCreateBuffer(size, fd, isUserFd);
452498
}
453499

454500
let bytesRead;
455501

456-
if (size !== 0) {
502+
if (hasUserBuffer) {
503+
if (size !== 0) {
504+
do {
505+
bytesRead = tryReadSync(fd, isUserFd, buffer, pos, size - pos);
506+
pos += bytesRead;
507+
} while (bytesRead !== 0 && pos < size);
508+
} else {
509+
pos = tryReadSyncWithUserBuffer(
510+
fd,
511+
isUserFd,
512+
buffer,
513+
getReadFileBufferByteLengthName(options),
514+
);
515+
}
516+
} else if (size !== 0) {
457517
do {
458518
bytesRead = tryReadSync(fd, isUserFd, buffer, pos, size - pos);
459519
pos += bytesRead;
@@ -474,7 +534,9 @@ function readFileSync(path, options) {
474534
if (!isUserFd)
475535
fs.closeSync(fd);
476536

477-
if (size === 0) {
537+
if (hasUserBuffer) {
538+
buffer = buffer.subarray(0, pos);
539+
} else if (size === 0) {
478540
// Data was collected into the buffers list.
479541
buffer = Buffer.concat(buffers, pos);
480542
} 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 || options.getBuffer !== 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)