summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/api/errors.md6
-rw-r--r--doc/api/fs.md216
-rw-r--r--lib/fs.js27
-rw-r--r--lib/internal/errors.js1
-rw-r--r--lib/internal/fs/dir.js201
-rw-r--r--lib/internal/fs/promises.js2
-rw-r--r--lib/internal/fs/utils.js52
-rw-r--r--node.gyp3
-rw-r--r--src/async_wrap.h1
-rw-r--r--src/env.h1
-rw-r--r--src/node_binding.cc1
-rw-r--r--src/node_dir.cc350
-rw-r--r--src/node_dir.h60
-rw-r--r--src/node_file.cc90
-rw-r--r--src/node_file.h89
-rw-r--r--test/parallel/test-bootstrap-modules.js2
-rw-r--r--test/parallel/test-fs-opendir.js174
-rw-r--r--test/sequential/test-async-wrap-getasyncid.js7
-rw-r--r--tools/doc/type-parser.js1
19 files changed, 1165 insertions, 119 deletions
diff --git a/doc/api/errors.md b/doc/api/errors.md
index e13e25827e..8c86012630 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -826,6 +826,11 @@ A signing `key` was not provided to the [`sign.sign()`][] method.
[`crypto.timingSafeEqual()`][] was called with `Buffer`, `TypedArray`, or
`DataView` arguments of different lengths.
+<a id="ERR_DIR_CLOSED"></a>
+### ERR_DIR_CLOSED
+
+The [`fs.Dir`][] was previously closed.
+
<a id="ERR_DNS_SET_SERVERS_FAILED"></a>
### ERR_DNS_SET_SERVERS_FAILED
@@ -2388,6 +2393,7 @@ such as `process.stdout.on('data')`.
[`dgram.disconnect()`]: dgram.html#dgram_socket_disconnect
[`dgram.remoteAddress()`]: dgram.html#dgram_socket_remoteaddress
[`errno`(3) man page]: http://man7.org/linux/man-pages/man3/errno.3.html
+[`fs.Dir`]: fs.html#fs_class_fs_dir
[`fs.readFileSync`]: fs.html#fs_fs_readfilesync_path_options
[`fs.readdir`]: fs.html#fs_fs_readdir_path_options_callback
[`fs.symlink()`]: fs.html#fs_fs_symlink_target_path_type_callback
diff --git a/doc/api/fs.md b/doc/api/fs.md
index c22e169f1c..a6736d127e 100644
--- a/doc/api/fs.md
+++ b/doc/api/fs.md
@@ -284,13 +284,148 @@ synchronous use libuv's threadpool, which can have surprising and negative
performance implications for some applications. See the
[`UV_THREADPOOL_SIZE`][] documentation for more information.
+## Class fs.Dir
+<!-- YAML
+added: REPLACEME
+-->
+
+A class representing a directory stream.
+
+Created by [`fs.opendir()`][], [`fs.opendirSync()`][], or [`fsPromises.opendir()`][].
+
+Example using async interation:
+
+```js
+const fs = require('fs');
+
+async function print(path) {
+ const dir = await fs.promises.opendir(path);
+ for await (const dirent of dir) {
+ console.log(dirent.name);
+ }
+}
+print('./').catch(console.error);
+```
+
+### dir.path
+<!-- YAML
+added: REPLACEME
+-->
+
+* {string}
+
+The read-only path of this directory as was provided to [`fs.opendir()`][],
+[`fs.opendirSync()`][], or [`fsPromises.opendir()`][].
+
+### dir.close()
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {Promise}
+
+Asynchronously close the directory's underlying resource handle.
+Subsequent reads will result in errors.
+
+A `Promise` is returned that will be resolved after the resource has been
+closed.
+
+### dir.close(callback)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `callback` {Function}
+ * `err` {Error}
+
+Asynchronously close the directory's underlying resource handle.
+Subsequent reads will result in errors.
+
+The `callback` will be called after the resource handle has been closed.
+
+### dir.closeSync()
+<!-- YAML
+added: REPLACEME
+-->
+
+Synchronously close the directory's underlying resource handle.
+Subsequent reads will result in errors.
+
+### dir.read([options])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `options` {Object}
+ * `encoding` {string|null} **Default:** `'utf8'`
+* Returns: {Promise} containing {fs.Dirent}
+
+Asynchronously read the next directory entry via readdir(3) as an
+[`fs.Dirent`][].
+
+A `Promise` is returned that will be resolved with a [Dirent][] after the read
+is completed.
+
+_Directory entries returned by this function are in no particular order as
+provided by the operating system's underlying directory mechanisms._
+
+### dir.read([options, ]callback)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `options` {Object}
+ * `encoding` {string|null} **Default:** `'utf8'`
+* `callback` {Function}
+ * `err` {Error}
+ * `dirent` {fs.Dirent}
+
+Asynchronously read the next directory entry via readdir(3) as an
+[`fs.Dirent`][].
+
+The `callback` will be called with a [Dirent][] after the read is completed.
+
+The `encoding` option sets the encoding of the `name` in the `dirent`.
+
+_Directory entries returned by this function are in no particular order as
+provided by the operating system's underlying directory mechanisms._
+
+### dir.readSync([options])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `options` {Object}
+ * `encoding` {string|null} **Default:** `'utf8'`
+* Returns: {fs.Dirent}
+
+Synchronously read the next directory entry via readdir(3) as an
+[`fs.Dirent`][].
+
+The `encoding` option sets the encoding of the `name` in the `dirent`.
+
+_Directory entries returned by this function are in no particular order as
+provided by the operating system's underlying directory mechanisms._
+
+### dir\[Symbol.asyncIterator\]()
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {AsyncIterator} to fully iterate over all entries in the directory.
+
+_Directory entries returned by this iterator are in no particular order as
+provided by the operating system's underlying directory mechanisms._
+
## Class: fs.Dirent
<!-- YAML
added: v10.10.0
-->
-When [`fs.readdir()`][] or [`fs.readdirSync()`][] is called with the
-`withFileTypes` option set to `true`, the resulting array is filled with
+A representation of a directory entry, as returned by reading from an [`fs.Dir`][].
+
+Additionally, when [`fs.readdir()`][] or [`fs.readdirSync()`][] is called with
+the `withFileTypes` option set to `true`, the resulting array is filled with
`fs.Dirent` objects, rather than strings or `Buffers`.
### dirent.isBlockDevice()
@@ -2505,6 +2640,46 @@ Returns an integer representing the file descriptor.
For detailed information, see the documentation of the asynchronous version of
this API: [`fs.open()`][].
+## fs.opendir(path[, options], callback)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `path` {string|Buffer|URL}
+* `options` {Object}
+ * `encoding` {string|null} **Default:** `'utf8'`
+* `callback` {Function}
+ * `err` {Error}
+ * `dir` {fs.Dir}
+
+Asynchronously open a directory. See opendir(3).
+
+Creates an [`fs.Dir`][], which contains all further functions for reading from
+and cleaning up the directory.
+
+The `encoding` option sets the encoding for the `path` while opening the
+directory and subsequent read operations (unless otherwise overriden during
+reads from the directory).
+
+## fs.opendirSync(path[, options])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `path` {string|Buffer|URL}
+* `options` {Object}
+ * `encoding` {string|null} **Default:** `'utf8'`
+* Returns: {fs.Dir}
+
+Synchronously open a directory. See opendir(3).
+
+Creates an [`fs.Dir`][], which contains all further functions for reading from
+and cleaning up the directory.
+
+The `encoding` option sets the encoding for the `path` while opening the
+directory and subsequent read operations (unless otherwise overriden during
+reads from the directory).
+
## fs.read(fd, buffer, offset, length, position, callback)
<!-- YAML
added: v0.0.2
@@ -4644,6 +4819,39 @@ by [Naming Files, Paths, and Namespaces][]. Under NTFS, if the filename contains
a colon, Node.js will open a file system stream, as described by
[this MSDN page][MSDN-Using-Streams].
+## fsPromises.opendir(path[, options])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `path` {string|Buffer|URL}
+* `options` {Object}
+ * `encoding` {string|null} **Default:** `'utf8'`
+* Returns: {Promise} containing {fs.Dir}
+
+Asynchronously open a directory. See opendir(3).
+
+Creates an [`fs.Dir`][], which contains all further functions for reading from
+and cleaning up the directory.
+
+The `encoding` option sets the encoding for the `path` while opening the
+directory and subsequent read operations (unless otherwise overriden during
+reads from the directory).
+
+Example using async interation:
+
+```js
+const fs = require('fs');
+
+async function print(path) {
+ const dir = await fs.promises.opendir(path);
+ for await (const dirent of dir) {
+ console.log(dirent.name);
+ }
+}
+print('./').catch(console.error);
+```
+
### fsPromises.readdir(path[, options])
<!-- YAML
added: v10.0.0
@@ -5253,6 +5461,7 @@ the file contents.
[`UV_THREADPOOL_SIZE`]: cli.html#cli_uv_threadpool_size_size
[`WriteStream`]: #fs_class_fs_writestream
[`event ports`]: https://illumos.org/man/port_create
+[`fs.Dir`]: #fs_class_fs_dir
[`fs.Dirent`]: #fs_class_fs_dirent
[`fs.FSWatcher`]: #fs_class_fs_fswatcher
[`fs.Stats`]: #fs_class_fs_stats
@@ -5269,6 +5478,8 @@ the file contents.
[`fs.mkdir()`]: #fs_fs_mkdir_path_options_callback
[`fs.mkdtemp()`]: #fs_fs_mkdtemp_prefix_options_callback
[`fs.open()`]: #fs_fs_open_path_flags_mode_callback
+[`fs.opendir()`]: #fs_fs_opendir_path_options_callback
+[`fs.opendirSync()`]: #fs_fs_opendirsync_path_options
[`fs.read()`]: #fs_fs_read_fd_buffer_offset_length_position_callback
[`fs.readFile()`]: #fs_fs_readfile_path_options_callback
[`fs.readFileSync()`]: #fs_fs_readfilesync_path_options
@@ -5284,6 +5495,7 @@ the file contents.
[`fs.write(fd, string...)`]: #fs_fs_write_fd_string_position_encoding_callback
[`fs.writeFile()`]: #fs_fs_writefile_file_data_options_callback
[`fs.writev()`]: #fs_fs_writev_fd_buffers_position_callback
+[`fsPromises.opendir()`]: #fs_fspromises_opendir_path_options
[`inotify(7)`]: http://man7.org/linux/man-pages/man7/inotify.7.html
[`kqueue(2)`]: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
[`net.Socket`]: net.html#net_class_net_socket
diff --git a/lib/fs.js b/lib/fs.js
index 5c7d907f5e..34517a17da 100644
--- a/lib/fs.js
+++ b/lib/fs.js
@@ -64,6 +64,7 @@ const {
getDirents,
getOptions,
getValidatedPath,
+ handleErrorFromBinding,
nullCheck,
preprocessSymlinkDestination,
Stats,
@@ -80,6 +81,11 @@ const {
warnOnNonPortableTemplate
} = require('internal/fs/utils');
const {
+ Dir,
+ opendir,
+ opendirSync
+} = require('internal/fs/dir');
+const {
CHAR_FORWARD_SLASH,
CHAR_BACKWARD_SLASH,
} = require('internal/constants');
@@ -122,23 +128,6 @@ function showTruncateDeprecation() {
}
}
-function handleErrorFromBinding(ctx) {
- if (ctx.errno !== undefined) { // libuv error numbers
- const err = uvException(ctx);
- // eslint-disable-next-line no-restricted-syntax
- Error.captureStackTrace(err, handleErrorFromBinding);
- throw err;
- }
- if (ctx.error !== undefined) { // Errors created in C++ land.
- // TODO(joyeecheung): currently, ctx.error are encoding errors
- // usually caused by memory problems. We need to figure out proper error
- // code(s) for this.
- // eslint-disable-next-line no-restricted-syntax
- Error.captureStackTrace(ctx.error, handleErrorFromBinding);
- throw ctx.error;
- }
-}
-
function maybeCallback(cb) {
if (typeof cb === 'function')
return cb;
@@ -1834,7 +1823,6 @@ function createWriteStream(path, options) {
return new WriteStream(path, options);
}
-
module.exports = fs = {
appendFile,
appendFileSync,
@@ -1880,6 +1868,8 @@ module.exports = fs = {
mkdtempSync,
open,
openSync,
+ opendir,
+ opendirSync,
readdir,
readdirSync,
read,
@@ -1913,6 +1903,7 @@ module.exports = fs = {
writeSync,
writev,
writevSync,
+ Dir,
Dirent,
Stats,
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 39e6b6deda..cd3c162183 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -764,6 +764,7 @@ E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error);
E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign', Error);
E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',
'Input buffers must have the same byte length', RangeError);
+E('ERR_DIR_CLOSED', 'Directory handle was closed', Error);
E('ERR_DNS_SET_SERVERS_FAILED', 'c-ares failed to set servers: "%s" [%s]',
Error);
E('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',
diff --git a/lib/internal/fs/dir.js b/lib/internal/fs/dir.js
new file mode 100644
index 0000000000..c1a25f0de3
--- /dev/null
+++ b/lib/internal/fs/dir.js
@@ -0,0 +1,201 @@
+'use strict';
+
+const { Object } = primordials;
+
+const pathModule = require('path');
+const binding = internalBinding('fs');
+const dirBinding = internalBinding('fs_dir');
+const {
+ codes: {
+ ERR_DIR_CLOSED,
+ ERR_INVALID_CALLBACK,
+ ERR_MISSING_ARGS
+ }
+} = require('internal/errors');
+
+const { FSReqCallback } = binding;
+const internalUtil = require('internal/util');
+const {
+ getDirent,
+ getOptions,
+ getValidatedPath,
+ handleErrorFromBinding
+} = require('internal/fs/utils');
+
+const kDirHandle = Symbol('kDirHandle');
+const kDirPath = Symbol('kDirPath');
+const kDirClosed = Symbol('kDirClosed');
+const kDirOptions = Symbol('kDirOptions');
+const kDirReadPromisified = Symbol('kDirReadPromisified');
+const kDirClosePromisified = Symbol('kDirClosePromisified');
+
+class Dir {
+ constructor(handle, path, options) {
+ if (handle == null) throw new ERR_MISSING_ARGS('handle');
+ this[kDirHandle] = handle;
+ this[kDirPath] = path;
+ this[kDirClosed] = false;
+
+ this[kDirOptions] = getOptions(options, {
+ encoding: 'utf8'
+ });
+
+ this[kDirReadPromisified] = internalUtil.promisify(this.read).bind(this);
+ this[kDirClosePromisified] = internalUtil.promisify(this.close).bind(this);
+ }
+
+ get path() {
+ return this[kDirPath];
+ }
+
+ read(options, callback) {
+ if (this[kDirClosed] === true) {
+ throw new ERR_DIR_CLOSED();
+ }
+
+ callback = typeof options === 'function' ? options : callback;
+ if (callback === undefined) {
+ return this[kDirReadPromisified](options);
+ } else if (typeof callback !== 'function') {
+ throw new ERR_INVALID_CALLBACK(callback);
+ }
+ options = getOptions(options, this[kDirOptions]);
+
+ const req = new FSReqCallback();
+ req.oncomplete = (err, result) => {
+ if (err || result === null) {
+ return callback(err, result);
+ }
+ getDirent(this[kDirPath], result[0], result[1], callback);
+ };
+
+ this[kDirHandle].read(
+ options.encoding,
+ req
+ );
+ }
+
+ readSync(options) {
+ if (this[kDirClosed] === true) {
+ throw new ERR_DIR_CLOSED();
+ }
+
+ options = getOptions(options, this[kDirOptions]);
+
+ const ctx = { path: this[kDirPath] };
+ const result = this[kDirHandle].read(
+ options.encoding,
+ undefined,
+ ctx
+ );
+ handleErrorFromBinding(ctx);
+
+ if (result === null) {
+ return result;
+ }
+
+ return getDirent(this[kDirPath], result[0], result[1]);
+ }
+
+ close(callback) {
+ if (this[kDirClosed] === true) {
+ throw new ERR_DIR_CLOSED();
+ }
+
+ if (callback === undefined) {
+ return this[kDirClosePromisified]();
+ } else if (typeof callback !== 'function') {
+ throw new ERR_INVALID_CALLBACK(callback);
+ }
+
+ this[kDirClosed] = true;
+ const req = new FSReqCallback();
+ req.oncomplete = callback;
+ this[kDirHandle].close(req);
+ }
+
+ closeSync() {
+ if (this[kDirClosed] === true) {
+ throw new ERR_DIR_CLOSED();
+ }
+
+ this[kDirClosed] = true;
+ const ctx = { path: this[kDirPath] };
+ const result = this[kDirHandle].close(undefined, ctx);
+ handleErrorFromBinding(ctx);
+ return result;
+ }
+
+ async* entries() {
+ try {
+ while (true) {
+ const result = await this[kDirReadPromisified]();
+ if (result === null) {
+ break;
+ }
+ yield result;
+ }
+ } finally {
+ await this[kDirClosePromisified]();
+ }
+ }
+}
+
+Object.defineProperty(Dir.prototype, Symbol.asyncIterator, {
+ value: Dir.prototype.entries,
+ enumerable: false,
+ writable: true,
+ configurable: true,
+});
+
+function opendir(path, options, callback) {
+ callback = typeof options === 'function' ? options : callback;
+ if (typeof callback !== 'function') {
+ throw new ERR_INVALID_CALLBACK(callback);
+ }
+ path = getValidatedPath(path);
+ options = getOptions(options, {
+ encoding: 'utf8'
+ });
+
+ function opendirCallback(error, handle) {
+ if (error) {
+ callback(error);
+ } else {
+ callback(null, new Dir(handle, path, options));
+ }
+ }
+
+ const req = new FSReqCallback();
+ req.oncomplete = opendirCallback;
+
+ dirBinding.opendir(
+ pathModule.toNamespacedPath(path),
+ options.encoding,
+ req
+ );
+}
+
+function opendirSync(path, options) {
+ path = getValidatedPath(path);
+ options = getOptions(options, {
+ encoding: 'utf8'
+ });
+
+ const ctx = { path };
+ const handle = dirBinding.opendir(
+ pathModule.toNamespacedPath(path),
+ options.encoding,
+ undefined,
+ ctx
+ );
+ handleErrorFromBinding(ctx);
+
+ return new Dir(handle, path, options);
+}
+
+module.exports = {
+ Dir,
+ opendir,
+ opendirSync
+};
diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js
index 5af2ffa763..7660ff66be 100644
--- a/lib/internal/fs/promises.js
+++ b/lib/internal/fs/promises.js
@@ -36,6 +36,7 @@ const {
validateRmdirOptions,
warnOnNonPortableTemplate
} = require('internal/fs/utils');
+const { opendir } = require('internal/fs/dir');
const {
parseMode,
validateBuffer,
@@ -509,6 +510,7 @@ module.exports = {
access,
copyFile,
open,
+ opendir: promisify(opendir),
rename,
truncate,
rmdir,
diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js
index f6050f53ee..3324ec5080 100644
--- a/lib/internal/fs/utils.js
+++ b/lib/internal/fs/utils.js
@@ -12,7 +12,8 @@ const {
ERR_INVALID_OPT_VALUE_ENCODING,
ERR_OUT_OF_RANGE
},
- hideStackFrames
+ hideStackFrames,
+ uvException
} = require('internal/errors');
const {
isArrayBufferView,
@@ -165,19 +166,33 @@ function getDirents(path, [names, types], callback) {
} else {
const len = names.length;
for (i = 0; i < len; i++) {
- const type = types[i];
- if (type === UV_DIRENT_UNKNOWN) {
- const name = names[i];
- const stats = lazyLoadFs().lstatSync(pathModule.join(path, name));
- names[i] = new DirentFromStats(name, stats);
- } else {
- names[i] = new Dirent(names[i], types[i]);
- }
+ names[i] = getDirent(path, names[i], types[i]);
}
return names;
}
}
+function getDirent(path, name, type, callback) {
+ if (typeof callback === 'function') {
+ if (type === UV_DIRENT_UNKNOWN) {
+ lazyLoadFs().lstat(pathModule.join(path, name), (err, stats) => {
+ if (err) {
+ callback(err);
+ return;
+ }
+ callback(null, new DirentFromStats(name, stats));
+ });
+ } else {
+ callback(null, new Dirent(name, type));
+ }
+ } else if (type === UV_DIRENT_UNKNOWN) {
+ const stats = lazyLoadFs().lstatSync(pathModule.join(path, name));
+ return new DirentFromStats(name, stats);
+ } else {
+ return new Dirent(name, type);
+ }
+}
+
function getOptions(options, defaultOptions) {
if (options === null || options === undefined ||
typeof options === 'function') {
@@ -197,6 +212,23 @@ function getOptions(options, defaultOptions) {
return options;
}
+function handleErrorFromBinding(ctx) {
+ if (ctx.errno !== undefined) { // libuv error numbers
+ const err = uvException(ctx);
+ // eslint-disable-next-line no-restricted-syntax
+ Error.captureStackTrace(err, handleErrorFromBinding);
+ throw err;
+ }
+ if (ctx.error !== undefined) { // Errors created in C++ land.
+ // TODO(joyeecheung): currently, ctx.error are encoding errors
+ // usually caused by memory problems. We need to figure out proper error
+ // code(s) for this.
+ // eslint-disable-next-line no-restricted-syntax
+ Error.captureStackTrace(ctx.error, handleErrorFromBinding);
+ throw ctx.error;
+ }
+}
+
// Check if the path contains null types if it is a string nor Uint8Array,
// otherwise return silently.
const nullCheck = hideStackFrames((path, propName, throwError = true) => {
@@ -558,9 +590,11 @@ module.exports = {
BigIntStats, // for testing
copyObject,
Dirent,
+ getDirent,
getDirents,
getOptions,
getValidatedPath,
+ handleErrorFromBinding,
nullCheck,
preprocessSymlinkDestination,
realpathCacheKey: Symbol('realpathCacheKey'),
diff --git a/node.gyp b/node.gyp
index 31d10b7444..ced6fbc954 100644
--- a/node.gyp
+++ b/node.gyp
@@ -121,6 +121,7 @@
'lib/internal/fixed_queue.js',
'lib/internal/freelist.js',
'lib/internal/freeze_intrinsics.js',
+ 'lib/internal/fs/dir.js',
'lib/internal/fs/promises.js',
'lib/internal/fs/read_file_context.js',
'lib/internal/fs/rimraf.js',
@@ -526,6 +527,7 @@
'src/node_constants.cc',
'src/node_contextify.cc',
'src/node_credentials.cc',
+ 'src/node_dir.cc',
'src/node_domain.cc',
'src/node_env_var.cc',
'src/node_errors.cc',
@@ -606,6 +608,7 @@
'src/node_constants.h',
'src/node_context_data.h',
'src/node_contextify.h',
+ 'src/node_dir.h',
'src/node_errors.h',
'src/node_file.h',
'src/node_http2.h',
diff --git a/src/async_wrap.h b/src/async_wrap.h
index 876bd0c341..2651b5a054 100644
--- a/src/async_wrap.h
+++ b/src/async_wrap.h
@@ -33,6 +33,7 @@ namespace node {
#define NODE_ASYNC_NON_CRYPTO_PROVIDER_TYPES(V) \
V(NONE) \
+ V(DIRHANDLE) \
V(DNSCHANNEL) \
V(ELDHISTOGRAM) \
V(FILEHANDLE) \
diff --git a/src/env.h b/src/env.h
index 534b888e2c..df0df0b99c 100644
--- a/src/env.h
+++ b/src/env.h
@@ -382,6 +382,7 @@ constexpr size_t kFsStatsBufferLength =
V(async_wrap_ctor_template, v8::FunctionTemplate) \
V(async_wrap_object_ctor_template, v8::FunctionTemplate) \
V(compiled_fn_entry_template, v8::ObjectTemplate) \
+ V(dir_instance_template, v8::ObjectTemplate) \
V(fd_constructor_template, v8::ObjectTemplate) \
V(fdclose_constructor_template, v8::ObjectTemplate) \
V(filehandlereadwrap_template, v8::ObjectTemplate) \
diff --git a/src/node_binding.cc b/src/node_binding.cc
index 2deefefb65..f0a148a495 100644
--- a/src/node_binding.cc
+++ b/src/node_binding.cc
@@ -51,6 +51,7 @@
V(domain) \
V(errors) \
V(fs) \
+ V(fs_dir) \
V(fs_event_wrap) \
V(heap_utils) \
V(http2) \
diff --git a/src/node_dir.cc b/src/node_dir.cc
new file mode 100644
index 0000000000..c9df7e67e8
--- /dev/null
+++ b/src/node_dir.cc
@@ -0,0 +1,350 @@
+#include "node_dir.h"
+#include "node_process.h"
+#include "util.h"
+
+#include "tracing/trace_event.h"
+
+#include "req_wrap-inl.h"
+#include "string_bytes.h"
+
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <cstring>
+#include <cerrno>
+#include <climits>
+
+#include <memory>
+
+namespace node {
+
+namespace fs_dir {
+
+using fs::FSReqAfterScope;
+using fs::FSReqBase;
+using fs::FSReqWrapSync;
+using fs::GetReqWrap;
+
+using v8::Array;
+using v8::Context;
+using v8::Function;
+using v8::FunctionCallbackInfo;
+using v8::FunctionTemplate;
+using v8::HandleScope;
+using v8::Integer;
+using v8::Isolate;
+using v8::Local;
+using v8::MaybeLocal;
+using v8::Null;
+using v8::Object;
+using v8::ObjectTemplate;
+using v8::String;
+using v8::Value;
+
+#define TRACE_NAME(name) "fs_dir.sync." #name
+#define GET_TRACE_ENABLED \
+ (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED \
+ (TRACING_CATEGORY_NODE2(fs_dir, sync)) != 0)
+#define FS_DIR_SYNC_TRACE_BEGIN(syscall, ...) \
+ if (GET_TRACE_ENABLED) \
+ TRACE_EVENT_BEGIN(TRACING_CATEGORY_NODE2(fs_dir, sync), TRACE_NAME(syscall), \
+ ##__VA_ARGS__);
+#define FS_DIR_SYNC_TRACE_END(syscall, ...) \
+ if (GET_TRACE_ENABLED) \
+ TRACE_EVENT_END(TRACING_CATEGORY_NODE2(fs_dir, sync), TRACE_NAME(syscall), \
+ ##__VA_ARGS__);
+
+DirHandle::DirHandle(Environment* env, Local<Object> obj, uv_dir_t* dir)
+ : AsyncWrap(env, obj, AsyncWrap::PROVIDER_DIRHANDLE),
+ dir_(dir) {
+ MakeWeak();
+
+ dir_->nentries = 1;
+ dir_->dirents = &dirent_;
+}
+
+DirHandle* DirHandle::New(Environment* env, uv_dir_t* dir) {
+ Local<Object> obj;
+ if (!env->dir_instance_template()
+ ->NewInstance(env->context())
+ .ToLocal(&obj)) {
+ return nullptr;
+ }
+
+ return new DirHandle(env, obj, dir);
+}
+
+void DirHandle::New(const FunctionCallbackInfo<Value>& args) {
+ CHECK(args.IsConstructCall());
+}
+
+DirHandle::~DirHandle() {
+ CHECK(!closing_); // We should not be deleting while explicitly closing!
+ GCClose(); // Close synchronously and emit warning
+ CHECK(closed_); // We have to be closed at the point
+}
+
+// Close the directory handle if it hasn't already been closed. A process
+// warning will be emitted using a SetImmediate to avoid calling back to
+// JS during GC. If closing the fd fails at this point, a fatal exception
+// will crash the process immediately.
+inline void DirHandle::GCClose() {
+ if (closed_) return;
+ uv_fs_t req;
+ int ret = uv_fs_closedir(nullptr, &req, dir_, nullptr);
+ uv_fs_req_cleanup(&req);
+ closing_ = false;
+ closed_ = true;
+
+ struct err_detail { int ret; };
+
+ err_detail detail { ret };
+
+ if (ret < 0) {
+ // Do not unref this
+ env()->SetImmediate([detail](Environment* env) {
+ char msg[70];
+ snprintf(msg, arraysize(msg),
+ "Closing directory handle on garbage collection failed");
+ // This exception will end up being fatal for the process because
+ // it is being thrown from within the SetImmediate handler and
+ // there is no JS stack to bubble it to. In other words, tearing
+ // down the process is the only reasonable thing we can do here.
+ HandleScope handle_scope(env->isolate());
+ env->ThrowUVException(detail.ret, "close", msg);
+ });
+ return;
+ }
+
+ // If the close was successful, we still want to emit a process warning
+ // to notify that the file descriptor was gc'd. We want to be noisy about
+ // this because not explicitly closing the DirHandle is a bug.
+
+ env()->SetUnrefImmediate([](Environment* env) {
+ ProcessEmitWarning(env,
+ "Closing directory handle on garbage collection");
+ });
+}
+
+void AfterClose(uv_fs_t* req) {
+ FSReqBase* req_wrap = FSReqBase::from_req(req);
+ FSReqAfterScope after(req_wrap, req);
+
+ if (after.Proceed())
+ req_wrap->Resolve(Undefined(req_wrap->env()->isolate()));
+}
+
+void DirHandle::Close(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+
+ const int argc = args.Length();
+ CHECK_GE(argc, 1);
+
+ DirHandle* dir;
+ ASSIGN_OR_RETURN_UNWRAP(&dir, args.Holder());
+
+ dir->closing_ = false;
+ dir->closed_ = true;
+
+ FSReqBase* req_wrap_async = GetReqWrap(env, args[0]);
+ if (req_wrap_async != nullptr) { // close(req)
+ AsyncCall(env, req_wrap_async, args, "closedir", UTF8, AfterClose,
+ uv_fs_closedir, dir->dir());
+ } else { // close(undefined, ctx)
+ CHECK_EQ(argc, 2);
+ FSReqWrapSync req_wrap_sync;
+ FS_DIR_SYNC_TRACE_BEGIN(closedir);
+ SyncCall(env, args[1], &req_wrap_sync, "closedir", uv_fs_closedir,
+ dir->dir());
+ FS_DIR_SYNC_TRACE_END(closedir);
+ }
+}
+
+void AfterDirReadSingle(uv_fs_t* req) {
+ FSReqBase* req_wrap = FSReqBase::from_req(req);
+ FSReqAfterScope after(req_wrap, req);
+
+ if (!after.Proceed()) {
+ return;
+ }
+
+ Environment* env = req_wrap->env();
+ Isolate* isolate = env->isolate();
+ Local<Value> error;
+
+ if (req->result == 0) {
+ // Done
+ Local<Value> done = Null(isolate);
+ req_wrap->Resolve(done);
+ return;
+ }
+
+ uv_dir_t* dir = static_cast<uv_dir_t*>(req->ptr);
+ req->ptr = nullptr;
+
+ // Single entries are returned without an array wrapper
+ const uv_dirent_t& ent = dir->dirents[0];
+
+ MaybeLocal<Value> filename =
+ StringBytes::Encode(isolate,
+ ent.name,
+ req_wrap->encoding(),
+ &error);
+ if (filename.IsEmpty())
+ return req_wrap->Reject(error);
+
+
+ Local<Array> result = Array::New(isolate, 2);
+ result->Set(env->context(),
+ 0,
+ filename.ToLocalChecked()).FromJust();
+ result->Set(env->context(),
+ 1,
+ Integer::New(isolate, ent.type)).FromJust();
+ req_wrap->Resolve(result);
+}
+
+
+void DirHandle::Read(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Isolate* isolate = env->isolate();
+
+ const int argc = args.Length();
+ CHECK_GE(argc, 2);
+
+ const enum encoding encoding = ParseEncoding(isolate, args[0], UTF8);
+
+ DirHandle* dir;
+ ASSIGN_OR_RETURN_UNWRAP(&dir, args.Holder());
+
+ FSReqBase* req_wrap_async = static_cast<FSReqBase*>(GetReqWrap(env, args[1]));
+ if (req_wrap_async != nullptr) { // dir.read(encoding, req)
+ AsyncCall(env, req_wrap_async, args, "readdir", encoding,
+ AfterDirReadSingle, uv_fs_readdir, dir->dir());
+ } else { // dir.read(encoding, undefined, ctx)
+ CHECK_EQ(argc, 3);
+ FSReqWrapSync req_wrap_sync;
+ FS_DIR_SYNC_TRACE_BEGIN(readdir);
+ int err = SyncCall(env, args[2], &req_wrap_sync, "readdir", uv_fs_readdir,
+ dir->dir());
+ FS_DIR_SYNC_TRACE_END(readdir);
+ if (err < 0) {
+ return; // syscall failed, no need to continue, error info is in ctx
+ }
+
+ if (req_wrap_sync.req.result == 0) {
+ // Done
+ Local<Value> done = Null(isolate);
+ args.GetReturnValue().Set(done);
+ return;
+ }
+
+ CHECK_GE(req_wrap_sync.req.result, 0);
+ const uv_dirent_t& ent = dir->dir()->dirents[0];
+
+ Local<Value> error;
+ MaybeLocal<Value> filename =
+ StringBytes::Encode(isolate,
+ ent.name,
+ encoding,
+ &error);
+ if (filename.IsEmpty()) {
+ Local<Object> ctx = args[2].As<Object>();
+ ctx->Set(env->context(), env->error_string(), error).FromJust();
+ return;
+ }
+
+ Local<Array> result = Array::New(isolate, 2);
+ result->Set(env->context(),
+ 0,
+ filename.ToLocalChecked()).FromJust();
+ result->Set(env->context(),
+ 1,
+ Integer::New(isolate, ent.type)).FromJust();
+ args.GetReturnValue().Set(result);
+ }
+}
+
+void AfterOpenDir(uv_fs_t* req) {
+ FSReqBase* req_wrap = FSReqBase::from_req(req);
+ FSReqAfterScope after(req_wrap, req);
+
+ if (!after.Proceed()) {
+ return;
+ }
+
+ Environment* env = req_wrap->env();
+ Local<Value> error;
+
+ uv_dir_t* dir = static_cast<uv_dir_t*>(req->ptr);
+ DirHandle* handle = DirHandle::New(env, dir);
+
+ req_wrap->Resolve(handle->object().As<Value>());
+}
+
+static void OpenDir(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Isolate* isolate = env->isolate();
+
+ const int argc = args.Length();
+ CHECK_GE(argc, 3);
+
+ BufferValue path(isolate, args[0]);
+ CHECK_NOT_NULL(*path);
+
+ const enum encoding encoding = ParseEncoding(isolate, args[1], UTF8);
+
+ FSReqBase* req_wrap_async = static_cast<FSReqBase*>(GetReqWrap(env, args[2]));
+ if (req_wrap_async != nullptr) { // openDir(path, encoding, req)
+ AsyncCall(env, req_wrap_async, args, "opendir", encoding, AfterOpenDir,
+ uv_fs_opendir, *path);
+ } else { // openDir(path, encoding, undefined, ctx)
+ CHECK_EQ(argc, 4);
+ FSReqWrapSync req_wrap_sync;
+ FS_DIR_SYNC_TRACE_BEGIN(opendir);
+ int result = SyncCall(env, args[3], &req_wrap_sync, "opendir",
+ uv_fs_opendir, *path);
+ FS_DIR_SYNC_TRACE_END(opendir);
+ if (result < 0) {
+ return; // syscall failed, no need to continue, error info is in ctx
+ }
+
+ uv_fs_t* req = &req_wrap_sync.req;
+ uv_dir_t* dir = static_cast<uv_dir_t*>(req->ptr);
+ DirHandle* handle = DirHandle::New(env, dir);
+
+ args.GetReturnValue().Set(handle->object().As<Value>());
+ }
+}
+
+void Initialize(Local<Object> target,
+ Local<Value> unused,
+ Local<Context> context,
+ void* priv) {
+ Environment* env = Environment::GetCurrent(context);
+ Isolate* isolate = env->isolate();
+
+ env->SetMethod(target, "opendir", OpenDir);
+
+ // Create FunctionTemplate for DirHandle
+ Local<FunctionTemplate> dir = env->NewFunctionTemplate(DirHandle::New);
+ dir->Inherit(AsyncWrap::GetConstructorTemplate(env));
+ env->SetProtoMethod(dir, "read", DirHandle::Read);
+ env->SetProtoMethod(dir, "close", DirHandle::Close);
+ Local<ObjectTemplate> dirt = dir->InstanceTemplate();
+ dirt->SetInternalFieldCount(DirHandle::kDirHandleFieldCount);
+ Local<String> handleString =
+ FIXED_ONE_BYTE_STRING(isolate, "DirHandle");
+ dir->SetClassName(handleString);
+ target
+ ->Set(context, handleString,
+ dir->GetFunction(env->context()).ToLocalChecked())
+ .FromJust();
+ env->set_dir_instance_template(dirt);
+}
+
+} // namespace fs_dir
+
+} // end namespace node
+
+NODE_MODULE_CONTEXT_AWARE_INTERNAL(fs_dir, node::fs_dir::Initialize)
diff --git a/src/node_dir.h b/src/node_dir.h
new file mode 100644
index 0000000000..e099fe5510
--- /dev/null
+++ b/src/node_dir.h
@@ -0,0 +1,60 @@
+#ifndef SRC_NODE_DIR_H_
+#define SRC_NODE_DIR_H_
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#include "node_file.h"
+#include "node.h"
+#include "req_wrap-inl.h"
+
+namespace node {
+
+namespace fs_dir {
+
+// Needed to propagate `uv_dir_t`.
+class DirHandle : public AsyncWrap {
+ public:
+ static constexpr int kDirHandleFieldCount = 1;
+
+ static DirHandle* New(Environment* env, uv_dir_t* dir);
+ ~DirHandle() override;
+
+ static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void Open(const v8::FunctionCallbackInfo<Value>& args);
+ static void Read(const v8::FunctionCallbackInfo<Value>& args);
+ static void Close(const v8::FunctionCallbackInfo<Value>& args);
+
+ inline uv_dir_t* dir() { return dir_; }
+ AsyncWrap* GetAsyncWrap() { return this; }
+
+ void MemoryInfo(MemoryTracker* tracker) const override {
+ tracker->TrackFieldWithSize("dir", sizeof(*dir_));
+ }
+
+ SET_MEMORY_INFO_NAME(DirHandle)
+ SET_SELF_SIZE(DirHandle)
+
+ DirHandle(const DirHandle&) = delete;
+ DirHandle& operator=(const DirHandle&) = delete;
+ DirHandle(const DirHandle&&) = delete;
+ DirHandle& operator=(const DirHandle&&) = delete;
+
+ private:
+ DirHandle(Environment* env, v8::Local<v8::Object> obj, uv_dir_t* dir);
+
+ // Synchronous close that emits a warning
+ void GCClose();
+
+ uv_dir_t* dir_;
+ uv_dirent_t dirent_;
+ bool closing_ = false;
+ bool closed_ = false;
+};
+
+} // namespace fs_dir
+
+} // namespace node
+
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#endif // SRC_NODE_DIR_H_
diff --git a/src/node_file.cc b/src/node_file.cc
index 195757f43f..8b6a90989f 100644
--- a/src/node_file.cc
+++ b/src/node_file.cc
@@ -52,11 +52,9 @@ namespace node {
namespace fs {
using v8::Array;
-using v8::BigUint64Array;
using v8::Context;
using v8::DontDelete;
using v8::EscapableHandleScope;
-using v8::Float64Array;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
@@ -678,94 +676,6 @@ void AfterScanDirWithTypes(uv_fs_t* req) {
req_wrap->Resolve(result);
}
-
-// This class is only used on sync fs calls.
-// For async calls FSReqCallback is used.
-class FSReqWrapSync {
- public:
- FSReqWrapSync() = default;
- ~FSReqWrapSync() { uv_fs_req_cleanup(&req); }
- uv_fs_t req;
-
- FSReqWrapSync(const FSReqWrapSync&) = delete;
- FSReqWrapSync& operator=(const FSReqWrapSync&) = delete;
-};
-
-// Returns nullptr if the operation fails from the start.
-template <typename Func, typename... Args>
-inline FSReqBase* AsyncDestCall(Environment* env,
- FSReqBase* req_wrap,
- const FunctionCallbackInfo<Value>& args,
- const char* syscall, const char* dest, size_t len,
- enum encoding enc, uv_fs_cb after, Func fn, Args... fn_args) {
- CHECK_NOT_NULL(req_wrap);
- req_wrap->Init(syscall, dest, len, enc);
- int err = req_wrap->Dispatch(fn, fn_args..., after);
- if (err < 0) {
- uv_fs_t* uv_req = req_wrap->req();
- uv_req->result = err;
- uv_req->path = nullptr;
- after(uv_req); // after may delete req_wrap if there is an error
- req_wrap = nullptr;
- } else {
- req_wrap->SetReturnValue(args);
- }
-
- return req_wrap;
-}
-
-// Returns nullptr if the operation fails from the start.
-template <typename Func, typename... Args>
-inline FSReqBase* AsyncCall(Environment* env,
- FSReqBase* req_wrap,
- const FunctionCallbackInfo<Value>& args,
- const char* syscall, enum encoding enc,
- uv_fs_cb after, Func fn, Args... fn_args) {
- return AsyncDestCall(env, req_wrap, args,
- syscall, nullptr, 0, enc,
- after, fn, fn_args...);
-}
-
-// Template counterpart of SYNC_CALL, except that it only puts
-// the error number and the syscall in the context instead of
-// creating an error in the C++ land.
-// ctx must be checked using value->IsObject() before being passed.
-template <typename Func, typename... Args>
-inline int SyncCall(Environment* env, Local<Value> ctx, FSReqWrapSync* req_wrap,
- const char* syscall, Func fn, Args... args) {
- env->PrintSyncTrace();
- int err = fn(env->event_loop(), &(req_wrap->req), args..., nullptr);
- if (err < 0) {
- Local<Context> context = env->context();
- Local<Object> ctx_obj = ctx.As<Object>();
- Isolate* isolate = env->isolate();
- ctx_obj->Set(context,
- env->errno_string(),
- Integer::New(isolate, err)).Check();
- ctx_obj->Set(context,
- env->syscall_string(),
- OneByteString(isolate, syscall)).Check();
- }
- return err;
-}
-
-// TODO(addaleax): Currently, callers check the return value and assume
-// that nullptr indicates a synchronous call, rather than a failure.
-// Failure conditions should be disambiguated and handled appropriately.
-inline FSReqBase* GetReqWrap(Environment* env, Local<Value> value,
- bool use_bigint = false) {
- if (value->IsObject()) {
- return Unwrap<FSReqBase>(value.As<Object>());
- } else if (value->StrictEquals(env->fs_use_promises_symbol())) {
- if (use_bigint) {
- return FSReqPromise<AliasedBigUint64Array>::New(env, use_bigint);
- } else {
- return FSReqPromise<AliasedFloat64Array>::New(env, use_bigint);
- }
- }
- return nullptr;
-}
-
void Access(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
diff --git a/src/node_file.h b/src/node_file.h
index 2ea5af025d..84f4032cc2 100644
--- a/src/node_file.h
+++ b/src/node_file.h
@@ -4,7 +4,9 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "node.h"
+#include "aliased_buffer.h"
#include "stream_base.h"
+#include "memory_tracker-inl.h"
#include "req_wrap-inl.h"
#include <iostream>
@@ -450,6 +452,93 @@ int MKDirpSync(uv_loop_t* loop,
const std::string& path,
int mode,
uv_fs_cb cb = nullptr);
+
+class FSReqWrapSync {
+ public:
+ FSReqWrapSync() = default;
+ ~FSReqWrapSync() { uv_fs_req_cleanup(&req); }
+ uv_fs_t req;
+
+ FSReqWrapSync(const FSReqWrapSync&) = delete;
+ FSReqWrapSync& operator=(const FSReqWrapSync&) = delete;
+};
+
+// TODO(addaleax): Currently, callers check the return value and assume
+// that nullptr indicates a synchronous call, rather than a failure.
+// Failure conditions should be disambiguated and handled appropriately.
+inline FSReqBase* GetReqWrap(Environment* env, v8::Local<v8::Value> value,
+ bool use_bigint = false) {
+ if (value->IsObject()) {
+ return Unwrap<FSReqBase>(value.As<Object>());
+ } else if (value->StrictEquals(env->fs_use_promises_symbol())) {
+ if (use_bigint) {
+ return FSReqPromise<AliasedBigUint64Array>::New(env, use_bigint);
+ } else {
+ return FSReqPromise<AliasedFloat64Array>::New(env, use_bigint);
+ }
+ }
+ return nullptr;
+}
+
+// Returns nullptr if the operation fails from the start.
+template <typename Func, typename... Args>
+inline FSReqBase* AsyncDestCall(Environment* env, FSReqBase* req_wrap,
+ const v8::FunctionCallbackInfo<Value>& args,
+ const char* syscall, const char* dest,
+ size_t len, enum encoding enc, uv_fs_cb after,
+ Func fn, Args... fn_args) {
+ CHECK_NOT_NULL(req_wrap);
+ req_wrap->Init(syscall, dest, len, enc);
+ int err = req_wrap->Dispatch(fn, fn_args..., after);
+ if (err < 0) {
+ uv_fs_t* uv_req = req_wrap->req();
+ uv_req->result = err;
+ uv_req->path = nullptr;
+ after(uv_req); // after may delete req_wrap if there is an error
+ req_wrap = nullptr;
+ } else {
+ req_wrap->SetReturnValue(args);
+ }
+
+ return req_wrap;
+}
+
+// Returns nullptr if the operation fails from the start.
+template <typename Func, typename... Args>
+inline FSReqBase* AsyncCall(Environment* env,
+ FSReqBase* req_wrap,
+ const v8::FunctionCallbackInfo<Value>& args,
+ const char* syscall, enum encoding enc,
+ uv_fs_cb after, Func fn, Args... fn_args) {
+ return AsyncDestCall(env, req_wrap, args,
+ syscall, nullptr, 0, enc,
+ after, fn, fn_args...);
+}
+
+// Template counterpart of SYNC_CALL, except that it only puts
+// the error number and the syscall in the context instead of
+// creating an error in the C++ land.
+// ctx must be checked using value->IsObject() before being passed.
+template <typename Func, typename... Args>
+inline int SyncCall(Environment* env, v8::Local<v8::Value> ctx,
+ FSReqWrapSync* req_wrap, const char* syscall,
+ Func fn, Args... args) {
+ env->PrintSyncTrace();
+ int err = fn(env->event_loop(), &(req_wrap->req), args..., nullptr);
+ if (err < 0) {
+ v8::Local<Context> context = env->context();
+ v8::Local<Object> ctx_obj = ctx.As<v8::Object>();
+ v8::Isolate* isolate = env->isolate();
+ ctx_obj->Set(context,
+ env->errno_string(),
+ v8::Integer::New(isolate, err)).Check();
+ ctx_obj->Set(context,
+ env->syscall_string(),
+ OneByteString(isolate, syscall)).Check();
+ }
+ return err;
+}
+
} // namespace fs
} // namespace node
diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js
index ae69f5aef0..7ca48c49cb 100644
--- a/test/parallel/test-bootstrap-modules.js
+++ b/test/parallel/test-bootstrap-modules.js
@@ -17,6 +17,7 @@ const expectedModules = new Set([
'Internal Binding contextify',
'Internal Binding credentials',
'Internal Binding fs',
+ 'Internal Binding fs_dir',
'Internal Binding inspector',
'Internal Binding module_wrap',
'Internal Binding native_module',
@@ -42,6 +43,7 @@ const expectedModules = new Set([
'NativeModule internal/encoding',
'NativeModule internal/errors',
'NativeModule internal/fixed_queue',
+ 'NativeModule internal/fs/dir',
'NativeModule internal/fs/utils',
'NativeModule internal/idna',
'NativeModule internal/linkedlist',
diff --git a/test/parallel/test-fs-opendir.js b/test/parallel/test-fs-opendir.js
new file mode 100644
index 0000000000..c9a6d657ed
--- /dev/null
+++ b/test/parallel/test-fs-opendir.js
@@ -0,0 +1,174 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const tmpdir = require('../common/tmpdir');
+
+const testDir = tmpdir.path;
+const files = ['empty', 'files', 'for', 'just', 'testing'];
+
+// Make sure tmp directory is clean
+tmpdir.refresh();
+
+// Create the necessary files
+files.forEach(function(filename) {
+ fs.closeSync(fs.openSync(path.join(testDir, filename), 'w'));
+});
+
+function assertDirent(dirent) {
+ assert(dirent instanceof fs.Dirent);
+ assert.strictEqual(dirent.isFile(), true);
+ assert.strictEqual(dirent.isDirectory(), false);
+ assert.strictEqual(dirent.isSocket(), false);
+ assert.strictEqual(dirent.isBlockDevice(), false);
+ assert.strictEqual(dirent.isCharacterDevice(), false);
+ assert.strictEqual(dirent.isFIFO(), false);
+ assert.strictEqual(dirent.isSymbolicLink(), false);
+}
+
+const dirclosedError = {
+ code: 'ERR_DIR_CLOSED'
+};
+
+// Check the opendir Sync version
+{
+ const dir = fs.opendirSync(testDir);
+ const entries = files.map(() => {
+ const dirent = dir.readSync();
+ assertDirent(dirent);
+ return dirent.name;
+ });
+ assert.deepStrictEqual(files, entries.sort());
+
+ // dir.read should return null when no more entries exist
+ assert.strictEqual(dir.readSync(), null);
+
+ // check .path
+ assert.strictEqual(dir.path, testDir);
+
+ dir.closeSync();
+
+ assert.throws(() => dir.readSync(), dirclosedError);
+ assert.throws(() => dir.closeSync(), dirclosedError);
+}
+
+// Check the opendir async version
+fs.opendir(testDir, common.mustCall(function(err, dir) {
+ assert.ifError(err);
+ dir.read(common.mustCall(function(err, dirent) {
+ assert.ifError(err);
+
+ // Order is operating / file system dependent
+ assert(files.includes(dirent.name), `'files' should include ${dirent}`);
+ assertDirent(dirent);
+
+ dir.close(common.mustCall(function(err) {
+ assert.ifError(err);
+ }));
+ }));
+}));
+
+// opendir() on file should throw ENOTDIR
+assert.throws(function() {
+ fs.opendirSync(__filename);
+}, /Error: ENOTDIR: not a directory/);
+
+fs.opendir(__filename, common.mustCall(function(e) {
+ assert.strictEqual(e.code, 'ENOTDIR');
+}));
+
+[false, 1, [], {}, null, undefined].forEach((i) => {
+ common.expectsError(
+ () => fs.opendir(i, common.mustNotCall()),
+ {
+ code: 'ERR_INVALID_ARG_TYPE',
+ type: TypeError
+ }
+ );
+ common.expectsError(
+ () => fs.opendirSync(i),
+ {
+ code: 'ERR_INVALID_ARG_TYPE',
+ type: TypeError
+ }
+ );
+});
+
+// Promise-based tests
+async function doPromiseTest() {
+ // Check the opendir Promise version
+ const dir = await fs.promises.opendir(testDir);
+ const entries = [];
+
+ let i = files.length;
+ while (i--) {
+ const dirent = await dir.read();
+ entries.push(dirent.name);
+ assertDirent(dirent);
+ }
+
+ assert.deepStrictEqual(files, entries.sort());
+
+ // dir.read should return null when no more entries exist
+ assert.strictEqual(await dir.read(), null);
+
+ await dir.close();
+}
+doPromiseTest().then(common.mustCall());
+
+// Async iterator
+async function doAsyncIterTest() {
+ const entries = [];
+ for await (const dirent of await fs.promises.opendir(testDir)) {
+ entries.push(dirent.name);
+ assertDirent(dirent);
+ }
+
+ assert.deepStrictEqual(files, entries.sort());
+
+ // Automatically closed during iterator
+}
+doAsyncIterTest().then(common.mustCall());
+
+// Async iterators should do automatic cleanup
+
+async function doAsyncIterBreakTest() {
+ const dir = await fs.promises.opendir(testDir);
+ for await (const dirent of dir) { // eslint-disable-line no-unused-vars
+ break;
+ }
+
+ await assert.rejects(async () => dir.read(), dirclosedError);
+}
+doAsyncIterBreakTest().then(common.mustCall());
+
+async function doAsyncIterReturnTest() {
+ const dir = await fs.promises.opendir(testDir);
+ await (async function() {
+ for await (const dirent of dir) { // eslint-disable-line no-unused-vars
+ return;
+ }
+ })();
+
+ await assert.rejects(async () => dir.read(), dirclosedError);
+}
+doAsyncIterReturnTest().then(common.mustCall());
+
+async function doAsyncIterThrowTest() {
+ const dir = await fs.promises.opendir(testDir);
+ try {
+ for await (const dirent of dir) { // eslint-disable-line no-unused-vars
+ throw new Error('oh no');
+ }
+ } catch (err) {
+ if (err.message !== 'oh no') {
+ throw err;
+ }
+ }
+
+ await assert.rejects(async () => dir.read(), dirclosedError);
+}
+doAsyncIterThrowTest().then(common.mustCall());
diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js
index bef6b050ff..cc823cc17f 100644
--- a/test/sequential/test-async-wrap-getasyncid.js
+++ b/test/sequential/test-async-wrap-getasyncid.js
@@ -306,3 +306,10 @@ if (process.features.inspector && common.isMainThread) {
{
v8.getHeapSnapshot().destroy();
}
+
+// DIRHANDLE
+{
+ const dirBinding = internalBinding('fs_dir');
+ const handle = dirBinding.opendir('./', 'utf8', undefined, {});
+ testInitialized(handle, 'DirHandle');
+}
diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js
index 4c3ec0503e..6cc60b0403 100644
--- a/tools/doc/type-parser.js
+++ b/tools/doc/type-parser.js
@@ -72,6 +72,7 @@ const customTypesMap = {
'EventEmitter': 'events.html#events_class_eventemitter',
'FileHandle': 'fs.html#fs_class_filehandle',
+ 'fs.Dir': 'fs.html#fs_class_fs_dir',
'fs.Dirent': 'fs.html#fs_class_fs_dirent',
'fs.FSWatcher': 'fs.html#fs_class_fs_fswatcher',
'fs.ReadStream': 'fs.html#fs_class_fs_readstream',