[njs] QuickJS: added fs module.

noreply at nginx.com noreply at nginx.com
Thu Dec 19 20:19:04 UTC 2024


details:   https://github.com/nginx/njs/commit/1f8f9992d03e2865f354da3415f8a49931cf2fe8
branches:  master
commit:    1f8f9992d03e2865f354da3415f8a49931cf2fe8
user:      Dmitry Volyntsev <xeioex at nginx.com>
date:      Wed, 4 Dec 2024 17:31:23 -0800
description:
QuickJS: added fs module.


---
 auto/qjs_modules         |    6 +
 external/qjs_fs_module.c | 3068 ++++++++++++++++++++++++++++++++++++++++++++++
 src/qjs.h                |    9 +-
 test/fs/methods.t.mjs    |    6 +-
 4 files changed, 3083 insertions(+), 6 deletions(-)

diff --git a/auto/qjs_modules b/auto/qjs_modules
index 0216d376..e74679d3 100644
--- a/auto/qjs_modules
+++ b/auto/qjs_modules
@@ -7,6 +7,12 @@ njs_module_srcs=src/qjs_buffer.c
 
 . auto/qjs_module
 
+njs_module_name=qjs_fs_module
+njs_module_incs=
+njs_module_srcs=external/qjs_fs_module.c
+
+. auto/qjs_module
+
 if [ $NJS_ZLIB = YES -a $NJS_HAVE_ZLIB = YES ]; then
     njs_module_name=qjs_zlib_module
     njs_module_incs=
diff --git a/external/qjs_fs_module.c b/external/qjs_fs_module.c
new file mode 100644
index 00000000..fc2222ff
--- /dev/null
+++ b/external/qjs_fs_module.c
@@ -0,0 +1,3068 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) F5, Inc.
+ */
+
+#include <qjs.h>
+#include <njs_utils.h>
+
+#include <dirent.h>
+#include <njs_unix.h>
+
+
+#if (NJS_SOLARIS)
+
+#define DT_DIR         0
+#define DT_REG         1
+#define DT_CHR         2
+#define DT_LNK         3
+#define DT_BLK         4
+#define DT_FIFO        5
+#define DT_SOCK        6
+#define QJS_DT_INVALID -1
+
+#define qjs_dentry_type(_dentry)                                             \
+    (QJS_DT_INVALID)
+
+#else
+
+#define QJS_DT_INVALID -1
+
+#define qjs_dentry_type(_dentry)                                             \
+    ((_dentry)->d_type)
+
+#endif
+
+
+#define qjs_fs_magic(calltype, mode)                                         \
+    (((mode) << 2) | calltype)
+
+#define qjs_fs_magic2(field, type)                                           \
+    (((type) << 4) | field)
+
+
+typedef enum {
+    QJS_FS_DIRECT,
+    QJS_FS_PROMISE,
+    QJS_FS_CALLBACK,
+} qjs_fs_calltype_t;
+
+
+typedef enum {
+    QJS_FTW_PHYS = 1,
+    QJS_FTW_MOUNT = 2,
+    QJS_FTW_DEPTH = 8,
+} qjs_ftw_flags_t;
+
+
+typedef enum {
+    QJS_FTW_F,
+    QJS_FTW_D,
+    QJS_FTW_DNR,
+    QJS_FTW_NS,
+    QJS_FTW_SL,
+    QJS_FTW_DP,
+    QJS_FTW_SLN,
+} qjs_ftw_type_t;
+
+
+typedef enum {
+    QJS_FS_TRUNC,
+    QJS_FS_APPEND,
+} qjs_fs_writemode_t;
+
+
+typedef enum {
+    QJS_FS_STAT,
+    QJS_FS_LSTAT,
+    QJS_FS_FSTAT,
+} njs_fs_statmode_t;
+
+
+typedef struct {
+    long tv_sec;
+    long tv_nsec;
+} qjs_timespec_t;
+
+
+typedef struct {
+    uint64_t        st_dev;
+    uint64_t        st_mode;
+    uint64_t        st_nlink;
+    uint64_t        st_uid;
+    uint64_t        st_gid;
+    uint64_t        st_rdev;
+    uint64_t        st_ino;
+    uint64_t        st_size;
+    uint64_t        st_blksize;
+    uint64_t        st_blocks;
+    qjs_timespec_t  st_atim;
+    qjs_timespec_t  st_mtim;
+    qjs_timespec_t  st_ctim;
+    qjs_timespec_t  st_birthtim;
+} qjs_stat_t;
+
+
+typedef enum {
+    QJS_FS_STAT_DEV,
+    QJS_FS_STAT_INO,
+    QJS_FS_STAT_MODE,
+    QJS_FS_STAT_NLINK,
+    QJS_FS_STAT_UID,
+    QJS_FS_STAT_GID,
+    QJS_FS_STAT_RDEV,
+    QJS_FS_STAT_SIZE,
+    QJS_FS_STAT_BLKSIZE,
+    QJS_FS_STAT_BLOCKS,
+    QJS_FS_STAT_ATIME,
+    QJS_FS_STAT_BIRTHTIME,
+    QJS_FS_STAT_CTIME,
+    QJS_FS_STAT_MTIME,
+} qjs_stat_prop_t;
+
+
+typedef struct {
+    njs_str_t       name;
+    int             value;
+} qjs_fs_entry_t;
+
+
+typedef int (*qjs_file_tree_walk_cb_t)(const char *, const struct stat *,
+     qjs_ftw_type_t);
+
+
+static JSValue qjs_fs_access(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int calltype);
+static JSValue qjs_fs_exists_sync(JSContext *cx, JSValueConst this_val,
+    int argc,JSValueConst *argv);
+static JSValue qjs_fs_close(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype);
+static JSValue qjs_fs_mkdir(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype);
+static JSValue qjs_fs_open(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype);
+static JSValue qjs_fs_read(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype);
+static JSValue qjs_fs_read_file(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype);
+static JSValue qjs_fs_readlink(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype);
+static JSValue qjs_fs_readdir(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype);
+static JSValue qjs_fs_realpath(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype);
+static JSValue qjs_fs_rename(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int calltype);
+static JSValue qjs_fs_rmdir(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype);
+static JSValue qjs_fs_stat(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int magic);
+static JSValue qjs_fs_symlink(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype);
+static JSValue qjs_fs_write(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype);
+static JSValue qjs_fs_write_file(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int magic);
+static JSValue qjs_fs_unlink(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int calltype);
+
+static JSValue qjs_fs_stats_to_string_tag(JSContext *cx, JSValueConst this_val);
+static JSValue qjs_fs_stats_test(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int testtype);
+static int qjs_fs_stats_get_own_property(JSContext *cx,
+    JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop);
+static int qjs_fs_stats_get_own_property_names(JSContext *cx,
+    JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj);
+static void qjs_fs_stats_finalizer(JSRuntime *rt, JSValue val);
+
+static JSValue qjs_fs_dirent_to_string_tag(JSContext *cx,
+    JSValueConst this_val);
+static JSValue qjs_fs_dirent_ctor(JSContext *cx, JSValueConst new_target,
+    int argc, JSValueConst *argv);
+static JSValue qjs_fs_dirent_test(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int testtype);
+
+static JSValue qjs_fs_filehandle_to_string_tag(JSContext *cx,
+    JSValueConst this_val);
+static JSValue qjs_fs_filehandle_fd(JSContext *cx, JSValueConst this_val);
+static JSValue qjs_fs_filehandle_value_of(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static void qjs_fs_filehandle_finalizer(JSRuntime *rt, JSValue val);
+
+static char *qjs_fs_path(JSContext *cx, char storage[NJS_MAX_PATH + 1],
+    JSValue src, const char *prop_name);
+static JSValue qjs_fs_result(JSContext *cx, JSValue result, int calltype,
+    JSValue callback);
+static JSValue qjs_fs_error(JSContext *cx, const char *syscall,
+    const char *description, const char *path, int errn);
+static JSValue qjs_fs_encode(JSContext *cx,
+    const qjs_buffer_encoding_t *encoding, njs_str_t *str);
+static int qjs_fs_flags(JSContext *cx, JSValue value, int default_flags);
+static mode_t qjs_fs_mode(JSContext *cx, JSValue value, mode_t default_mode);
+static JSModuleDef *qjs_fs_init(JSContext *cx, const char *name);
+
+
+static qjs_fs_entry_t qjs_flags_table[] = {
+    { njs_str("a"),   O_APPEND | O_CREAT | O_WRONLY },
+    { njs_str("a+"),  O_APPEND | O_CREAT | O_RDWR },
+    { njs_str("as"),  O_APPEND | O_CREAT | O_SYNC | O_WRONLY },
+    { njs_str("as+"), O_APPEND | O_CREAT | O_RDWR | O_SYNC },
+    { njs_str("ax"),  O_APPEND | O_CREAT | O_EXCL | O_WRONLY },
+    { njs_str("ax+"), O_APPEND | O_CREAT | O_EXCL | O_RDWR },
+    { njs_str("r"),   O_RDONLY },
+    { njs_str("r+"),  O_RDWR },
+    { njs_str("rs+"), O_RDWR   | O_SYNC },
+    { njs_str("w"),   O_CREAT  | O_TRUNC | O_WRONLY },
+    { njs_str("w+"),  O_CREAT  | O_TRUNC | O_RDWR },
+    { njs_str("wx"),  O_CREAT  | O_TRUNC | O_EXCL | O_WRONLY },
+    { njs_str("wx+"), O_CREAT  | O_TRUNC | O_EXCL | O_RDWR },
+    { njs_null_str, 0 }
+};
+
+
+static const JSCFunctionListEntry qjs_fs_stats_proto[] = {
+    JS_CGETSET_DEF("[Symbol.toStringTag]", qjs_fs_stats_to_string_tag, NULL),
+    JS_CFUNC_MAGIC_DEF("isBlockDevice", 0, qjs_fs_stats_test, DT_BLK),
+    JS_CFUNC_MAGIC_DEF("isCharacterDevice", 0, qjs_fs_stats_test, DT_CHR),
+    JS_CFUNC_MAGIC_DEF("isDirectory", 0, qjs_fs_stats_test, DT_DIR),
+    JS_CFUNC_MAGIC_DEF("isFIFO", 0, qjs_fs_stats_test, DT_FIFO),
+    JS_CFUNC_MAGIC_DEF("isFile", 0, qjs_fs_stats_test, DT_REG),
+    JS_CFUNC_MAGIC_DEF("isSocket", 0, qjs_fs_stats_test, DT_SOCK),
+    JS_CFUNC_MAGIC_DEF("isSymbolicLink", 0, qjs_fs_stats_test, DT_LNK),
+};
+
+
+static const JSCFunctionListEntry qjs_fs_dirent_proto[] = {
+    JS_CGETSET_DEF("[Symbol.toStringTag]", qjs_fs_dirent_to_string_tag, NULL),
+    JS_CFUNC_MAGIC_DEF("isBlockDevice", 0, qjs_fs_dirent_test, DT_BLK),
+    JS_CFUNC_MAGIC_DEF("isCharacterDevice", 0, qjs_fs_dirent_test, DT_CHR),
+    JS_CFUNC_MAGIC_DEF("isDirectory", 0, qjs_fs_dirent_test, DT_DIR),
+    JS_CFUNC_MAGIC_DEF("isFIFO", 0, qjs_fs_dirent_test, DT_FIFO),
+    JS_CFUNC_MAGIC_DEF("isFile", 0, qjs_fs_dirent_test, DT_REG),
+    JS_CFUNC_MAGIC_DEF("isSocket", 0, qjs_fs_dirent_test, DT_SOCK),
+    JS_CFUNC_MAGIC_DEF("isSymbolicLink", 0, qjs_fs_dirent_test, DT_LNK),
+    JS_CFUNC_SPECIAL_DEF("constructor", 1, constructor, qjs_fs_dirent_ctor),
+};
+
+
+static const JSCFunctionListEntry qjs_fs_filehandle_proto[] = {
+    JS_CGETSET_DEF("[Symbol.toStringTag]", qjs_fs_filehandle_to_string_tag,
+                   NULL),
+    JS_CFUNC_MAGIC_DEF("close", 0, qjs_fs_close, QJS_FS_PROMISE),
+    JS_CGETSET_DEF("fd", qjs_fs_filehandle_fd, NULL),
+    JS_CFUNC_MAGIC_DEF("stat", 4, qjs_fs_stat,
+                       qjs_fs_magic(QJS_FS_PROMISE, QJS_FS_FSTAT)),
+    JS_CFUNC_MAGIC_DEF("read", 4, qjs_fs_read, QJS_FS_PROMISE),
+    JS_CFUNC_DEF("valueOf", 0, qjs_fs_filehandle_value_of),
+    JS_CFUNC_MAGIC_DEF("write", 4, qjs_fs_write, QJS_FS_PROMISE),
+};
+
+
+static const JSCFunctionListEntry qjs_fs_constants[] = {
+    JS_PROP_INT32_DEF("F_OK", F_OK, JS_PROP_ENUMERABLE),
+    JS_PROP_INT32_DEF("R_OK", R_OK, JS_PROP_ENUMERABLE),
+    JS_PROP_INT32_DEF("W_OK", W_OK, JS_PROP_ENUMERABLE),
+    JS_PROP_INT32_DEF("X_OK", X_OK, JS_PROP_ENUMERABLE),
+};
+
+
+static const JSCFunctionListEntry qjs_fs_promises[] = {
+    JS_CFUNC_MAGIC_DEF("access", 2, qjs_fs_access, QJS_FS_PROMISE),
+    JS_CFUNC_MAGIC_DEF("appendFile", 3, qjs_fs_write_file,
+                       qjs_fs_magic(QJS_FS_PROMISE, QJS_FS_APPEND)),
+    JS_CFUNC_MAGIC_DEF("fstat", 2, qjs_fs_stat,
+                       qjs_fs_magic(QJS_FS_PROMISE, QJS_FS_FSTAT)),
+    JS_CFUNC_MAGIC_DEF("lstat", 2, qjs_fs_stat,
+                       qjs_fs_magic(QJS_FS_PROMISE, QJS_FS_LSTAT)),
+    JS_CFUNC_MAGIC_DEF("mkdir", 2, qjs_fs_mkdir, QJS_FS_PROMISE),
+    JS_CFUNC_MAGIC_DEF("open", 3, qjs_fs_open, QJS_FS_PROMISE),
+    JS_CFUNC_MAGIC_DEF("readFile", 2, qjs_fs_read_file, QJS_FS_PROMISE),
+    JS_CFUNC_MAGIC_DEF("realpath", 2, qjs_fs_realpath, QJS_FS_PROMISE),
+    JS_CFUNC_MAGIC_DEF("readdir", 2, qjs_fs_readdir, QJS_FS_PROMISE),
+    JS_CFUNC_MAGIC_DEF("readlink", 2, qjs_fs_readlink, QJS_FS_PROMISE),
+    JS_CFUNC_MAGIC_DEF("rename", 2, qjs_fs_rename, QJS_FS_PROMISE),
+    JS_CFUNC_MAGIC_DEF("rmdir", 2, qjs_fs_rmdir, QJS_FS_PROMISE),
+    JS_CFUNC_MAGIC_DEF("stat", 2, qjs_fs_stat,
+                       qjs_fs_magic(QJS_FS_PROMISE, QJS_FS_STAT)),
+    JS_CFUNC_MAGIC_DEF("symlink", 3, qjs_fs_symlink, QJS_FS_PROMISE),
+    JS_CFUNC_MAGIC_DEF("writeFile", 3, qjs_fs_write_file,
+                       qjs_fs_magic(QJS_FS_PROMISE, QJS_FS_TRUNC)),
+    JS_CFUNC_MAGIC_DEF("unlink", 1, qjs_fs_unlink, QJS_FS_PROMISE),
+};
+
+
+static const JSCFunctionListEntry qjs_fs_export[] = {
+    JS_OBJECT_DEF("constants",
+                  qjs_fs_constants,
+                  njs_nitems(qjs_fs_constants),
+                  JS_PROP_CONFIGURABLE),
+    JS_OBJECT_DEF("promises",
+                  qjs_fs_promises,
+                  njs_nitems(qjs_fs_promises),
+                  JS_PROP_CONFIGURABLE),
+    JS_CFUNC_MAGIC_DEF("access", 3, qjs_fs_access, QJS_FS_CALLBACK),
+    JS_CFUNC_MAGIC_DEF("accessSync", 2, qjs_fs_access, QJS_FS_DIRECT),
+    JS_CFUNC_MAGIC_DEF("appendFile", 4, qjs_fs_write_file,
+                       qjs_fs_magic(QJS_FS_CALLBACK, QJS_FS_APPEND)),
+    JS_CFUNC_MAGIC_DEF("appendFileSync", 3, qjs_fs_write_file,
+                       qjs_fs_magic(QJS_FS_DIRECT, QJS_FS_APPEND)),
+    JS_CFUNC_MAGIC_DEF("closeSync", 1, qjs_fs_close, QJS_FS_DIRECT),
+    JS_CFUNC_SPECIAL_DEF("Dirent", 1, constructor, qjs_fs_dirent_ctor),
+    JS_CFUNC_DEF("existsSync", 1, qjs_fs_exists_sync),
+    JS_CFUNC_MAGIC_DEF("fstatSync", 2, qjs_fs_stat,
+                       qjs_fs_magic(QJS_FS_DIRECT, QJS_FS_FSTAT)),
+    JS_CFUNC_MAGIC_DEF("lstat", 3, qjs_fs_stat,
+                       qjs_fs_magic(QJS_FS_CALLBACK, QJS_FS_LSTAT)),
+    JS_CFUNC_MAGIC_DEF("lstatSync", 2, qjs_fs_stat,
+                       qjs_fs_magic(QJS_FS_DIRECT, QJS_FS_LSTAT)),
+    JS_CFUNC_MAGIC_DEF("mkdir", 3, qjs_fs_mkdir, QJS_FS_CALLBACK),
+    JS_CFUNC_MAGIC_DEF("mkdirSync", 2, qjs_fs_mkdir, QJS_FS_DIRECT),
+    JS_CFUNC_MAGIC_DEF("openSync", 3, qjs_fs_open, QJS_FS_DIRECT),
+    JS_CFUNC_MAGIC_DEF("readSync", 5, qjs_fs_read, QJS_FS_DIRECT),
+    JS_CFUNC_MAGIC_DEF("readFile", 3, qjs_fs_read_file, QJS_FS_CALLBACK),
+    JS_CFUNC_MAGIC_DEF("readFileSync", 2, qjs_fs_read_file, QJS_FS_DIRECT),
+    JS_CFUNC_MAGIC_DEF("realpath", 3, qjs_fs_realpath, QJS_FS_CALLBACK),
+    JS_CFUNC_MAGIC_DEF("realpathSync", 2, qjs_fs_realpath, QJS_FS_DIRECT),
+    JS_CFUNC_MAGIC_DEF("readdir", 3, qjs_fs_readdir, QJS_FS_CALLBACK),
+    JS_CFUNC_MAGIC_DEF("readdirSync", 2, qjs_fs_readdir, QJS_FS_DIRECT),
+    JS_CFUNC_MAGIC_DEF("readlink", 3, qjs_fs_readlink, QJS_FS_CALLBACK),
+    JS_CFUNC_MAGIC_DEF("readlinkSync", 2, qjs_fs_readlink, QJS_FS_DIRECT),
+    JS_CFUNC_MAGIC_DEF("rename", 3, qjs_fs_rename, QJS_FS_CALLBACK),
+    JS_CFUNC_MAGIC_DEF("renameSync", 2, qjs_fs_rename, QJS_FS_DIRECT),
+    JS_CFUNC_MAGIC_DEF("rmdir", 3, qjs_fs_rmdir, QJS_FS_CALLBACK),
+    JS_CFUNC_MAGIC_DEF("rmdirSync", 2, qjs_fs_rmdir, QJS_FS_DIRECT),
+    JS_CFUNC_MAGIC_DEF("stat", 3, qjs_fs_stat,
+                       qjs_fs_magic(QJS_FS_CALLBACK, QJS_FS_STAT)),
+    JS_CFUNC_MAGIC_DEF("statSync", 2, qjs_fs_stat,
+                       qjs_fs_magic(QJS_FS_DIRECT, QJS_FS_STAT)),
+    JS_CFUNC_MAGIC_DEF("symlink", 4, qjs_fs_symlink, QJS_FS_CALLBACK),
+    JS_CFUNC_MAGIC_DEF("symlinkSync", 3, qjs_fs_symlink, QJS_FS_DIRECT),
+    JS_CFUNC_MAGIC_DEF("writeSync", 5, qjs_fs_write, QJS_FS_DIRECT),
+    JS_CFUNC_MAGIC_DEF("writeFile", 4, qjs_fs_write_file,
+                       qjs_fs_magic(QJS_FS_CALLBACK, QJS_FS_TRUNC)),
+    JS_CFUNC_MAGIC_DEF("writeFileSync", 3, qjs_fs_write_file,
+                       qjs_fs_magic(QJS_FS_DIRECT, QJS_FS_TRUNC)),
+    JS_CFUNC_MAGIC_DEF("unlink", 2, qjs_fs_unlink, QJS_FS_CALLBACK),
+    JS_CFUNC_MAGIC_DEF("unlinkSync", 1, qjs_fs_unlink, QJS_FS_DIRECT),
+};
+
+
+static JSClassDef qjs_fs_stats_class = {
+    "Stats",
+    .finalizer = qjs_fs_stats_finalizer,
+    .exotic = & (JSClassExoticMethods) {
+        .get_own_property = qjs_fs_stats_get_own_property,
+        .get_own_property_names = qjs_fs_stats_get_own_property_names,
+    },
+};
+
+
+static JSClassDef qjs_fs_filehandle_class = {
+    "FileHandle",
+    .finalizer = qjs_fs_filehandle_finalizer,
+};
+
+
+qjs_module_t  qjs_fs_module = {
+    .name = "fs",
+    .init = qjs_fs_init,
+};
+
+
+static JSValue
+qjs_fs_access(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype)
+{
+    int         md, ret;
+    JSValue     callback, mode, result;
+    const char  *path;
+    char        path_buf[NJS_MAX_PATH + 1];
+
+    path = qjs_fs_path(cx, path_buf, argv[0], "path");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    mode = argv[1];
+    callback = JS_UNDEFINED;
+
+    if (calltype == QJS_FS_CALLBACK) {
+        if (argc > 0) {
+            callback = argv[njs_min(argc - 1, 2)];
+        }
+
+        if (!JS_IsFunction(cx, callback)) {
+            JS_ThrowTypeError(cx, "\"callback\" must be a function");
+            return JS_EXCEPTION;
+        }
+
+        if (JS_SameValue(cx, mode, callback)) {
+            mode = JS_UNDEFINED;
+        }
+    }
+
+    if (JS_IsNumber(mode)) {
+        md = JS_VALUE_GET_INT(mode);
+
+    } else if (JS_IsUndefined(mode)) {
+        md = F_OK;
+
+    } else {
+        JS_ThrowTypeError(cx, "\"mode\" must be a number");
+        return JS_EXCEPTION;
+    }
+
+    result = JS_UNDEFINED;
+
+    ret = access(path, md);
+    if (ret != 0) {
+        result = qjs_fs_error(cx, "access", strerror(errno), path, errno);
+    }
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, callback);
+}
+
+
+static JSValue
+qjs_fs_exists_sync(JSContext *cx, JSValueConst this_val, int nargs,
+    JSValueConst *args)
+{
+    const char  *path;
+    char        path_buf[NJS_MAX_PATH + 1];
+
+    path = qjs_fs_path(cx, path_buf, args[0], "path");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    return (access(path, F_OK) == 0) ? JS_TRUE : JS_FALSE;
+}
+
+
+static JSValue
+qjs_fs_close(JSContext *cx, JSValueConst this_val, int nargs,
+    JSValueConst *args, int calltype)
+{
+    int      fd;
+    JSValue  result;
+
+    if (calltype == QJS_FS_DIRECT) {
+        if (JS_ToInt32(cx, &fd, args[0]) < 0) {
+            return JS_EXCEPTION;
+        }
+
+    } else {
+        fd = (intptr_t) JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_FS_FILEHANDLE);
+        if (fd == -1) {
+            JS_ThrowTypeError(cx, "file was already closed");
+            return JS_EXCEPTION;
+        }
+
+        JS_SetOpaque(this_val, (void *) -1);
+    }
+
+    result = JS_UNDEFINED;
+
+    if (close(fd) != 0) {
+        result = qjs_fs_error(cx, "close", strerror(errno), NULL, errno);
+        goto done;
+    }
+
+done:
+
+    return qjs_fs_result(cx, result, calltype, JS_UNDEFINED);
+}
+
+
+static JSValue
+qjs_fs_make_path(JSContext *cx, char *path, mode_t md, int recursive)
+{
+    int          err;
+    njs_int_t    ret;
+    const char   *p, *prev, *end;
+    struct stat  sb;
+
+    end = path + strlen(path);
+
+    if (!recursive) {
+        ret = mkdir(path, md);
+        if (ret != 0) {
+            err = errno;
+            goto failed;
+        }
+
+        return JS_UNDEFINED;
+    }
+
+    p = path;
+    prev = p;
+
+    for ( ;; ) {
+        p = strchr(prev + 1, '/');
+        if (p == NULL) {
+            p = end;
+        }
+
+        if ((p - path) > NJS_MAX_PATH) {
+            JS_ThrowInternalError(cx, "too large path");
+            return JS_EXCEPTION;
+        }
+
+        path[p - path] = '\0';
+
+        ret = mkdir(path, md);
+        err = errno;
+
+        switch (ret) {
+        case 0:
+            break;
+
+        case EACCES:
+        case ENOTDIR:
+        case EPERM:
+            goto failed;
+
+        case EEXIST:
+        default:
+            ret = stat(path, &sb);
+            if (ret == 0) {
+                if (!S_ISDIR(sb.st_mode)) {
+                    err = ENOTDIR;
+                    goto failed;
+                }
+
+                break;
+            }
+
+            goto failed;
+        }
+
+        if (p == end) {
+            break;
+        }
+
+        path[p - path] = '/';
+        prev = p;
+    }
+
+    return JS_UNDEFINED;
+
+failed:
+
+    return qjs_fs_error(cx, "mkdir", strerror(err), path, err);
+}
+
+
+static JSValue
+qjs_fs_mkdir(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype)
+{
+    int      recursive;
+    char     *path;
+    mode_t   md;
+    JSValue  callback, v, options, result;
+    char     path_buf[NJS_MAX_PATH + 1];
+
+    path = qjs_fs_path(cx, path_buf, argv[0], "path");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    callback = JS_UNDEFINED;
+    options = argv[1];
+
+    if (calltype == QJS_FS_CALLBACK) {
+        if (argc > 0) {
+            callback = argv[njs_min(argc - 1, 2)];
+        }
+
+        if (!JS_IsFunction(cx, callback)) {
+            JS_ThrowTypeError(cx, "\"callback\" must be a function");
+            return JS_EXCEPTION;
+        }
+
+        if (JS_SameValue(cx, options, callback)) {
+            options = JS_UNDEFINED;
+        }
+    }
+
+    md = 0777;
+    recursive = 0;
+
+    if (JS_IsNumber(options)) {
+        md = JS_VALUE_GET_INT(options);
+
+    } else if (!JS_IsUndefined(options)) {
+        if (!JS_IsObject(options)) {
+            JS_ThrowTypeError(cx, "Unknown options type (a number or object "
+                              "required)");
+            return JS_EXCEPTION;
+        }
+
+        v = JS_GetPropertyStr(cx, options, "mode");
+        if (!JS_IsUndefined(v) && !JS_IsException(v)) {
+            md = qjs_fs_mode(cx, v, 0777);
+            if (md == (mode_t) -1) {
+                JS_FreeValue(cx, v);
+                return JS_EXCEPTION;
+            }
+        }
+
+        v = JS_GetPropertyStr(cx, options, "recursive");
+        if (!JS_IsUndefined(v) && !JS_IsException(v)) {
+            recursive = JS_ToBool(cx, v);
+        }
+    }
+
+    result = qjs_fs_make_path(cx, path, md, recursive);
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, callback);
+}
+
+
+static JSValue
+qjs_fs_open(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv,
+    int calltype)
+{
+    int         fd, flags;
+    mode_t      md;
+    JSValue     result;
+    const char  *path;
+    char        path_buf[NJS_MAX_PATH + 1];
+
+    path = qjs_fs_path(cx, path_buf, argv[0], "path");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    flags = qjs_fs_flags(cx, argv[1], O_RDONLY);
+    if (flags == -1) {
+        return JS_EXCEPTION;
+    }
+
+    md = qjs_fs_mode(cx, argv[2], 0666);
+    if (md == (mode_t) -1) {
+        return JS_EXCEPTION;
+    }
+
+    fd = open(path, flags, md);
+    if (fd < 0) {
+        result = qjs_fs_error(cx, "open", strerror(errno), path, errno);
+        goto done;
+    }
+
+    if (calltype == QJS_FS_DIRECT) {
+        /* Leaks fd if user does not close it. */
+        result = JS_NewInt32(cx, fd);
+
+    } else {
+        result = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_FS_FILEHANDLE);
+        if (JS_IsException(result)) {
+            (void) close(fd);
+            goto done;
+        }
+
+        JS_SetOpaque(result, (void *) (intptr_t) fd);
+    }
+
+done:
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, JS_UNDEFINED);
+}
+
+
+static JSValue
+qjs_fs_fd_read(JSContext *cx, int fd, njs_str_t *data)
+{
+    u_char   *p, *end, *start;
+    size_t   size;
+    ssize_t  n;
+
+    size = data->length;
+
+    if (size == 0) {
+        size = 4096;
+    }
+
+    data->start = js_malloc(cx, size);
+    if (data->start == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        return JS_EXCEPTION;
+    }
+
+    p = data->start;
+    end = p + size;
+
+    for ( ;; ) {
+        n = read(fd, p, end - p);
+
+        if (n < 0) {
+            js_free(cx, data->start);
+            return JS_FALSE;
+        }
+
+        p += n;
+
+        if (n == 0) {
+            break;
+        }
+
+        if (end - p < 2048) {
+            size *= 2;
+
+            start = js_realloc(cx, data->start, size);
+            if (start == NULL) {
+                js_free(cx, data->start);
+                JS_ThrowOutOfMemory(cx);
+                return JS_EXCEPTION;
+            }
+
+            p = start + (p - data->start);
+            end = start + size;
+            data->start = start;
+        }
+    }
+
+    data->length = p - data->start;
+
+    return JS_TRUE;
+}
+
+
+static JSValue
+qjs_fs_read(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype)
+{
+    int        fd, fd_offset;
+    int64_t    length, pos, offset;
+    ssize_t    n;
+    JSValue    ret, result;
+    njs_str_t  buffer;
+
+    /*
+     * fh.read(buffer, offset[, length[, position]])
+     * fs.readSync(fd, buffer, offset[, length[, position]])
+     */
+
+    if (calltype == QJS_FS_DIRECT) {
+        fd_offset = 0;
+        if (JS_ToInt32(cx, &fd, argv[0]) < 0) {
+            return JS_EXCEPTION;
+        }
+
+    } else {
+        fd_offset = -1;
+        if (JS_ToInt32(cx, &fd, this_val) < 0) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    ret = qjs_typed_array_data(cx, argv[fd_offset + 1], &buffer);
+    if (JS_IsException(ret)) {
+        return ret;
+    }
+
+    if (JS_ToInt64(cx, &offset, argv[fd_offset + 2]) < 0) {
+        return JS_EXCEPTION;
+    }
+
+    if (offset < 0 || (size_t) offset > buffer.length) {
+        JS_ThrowRangeError(cx, "offset is out of range (must be <= %zu)",
+                           buffer.length);
+        return JS_EXCEPTION;
+    }
+
+    buffer.length -= offset;
+    buffer.start += offset;
+
+    if (!JS_IsUndefined(argv[fd_offset + 3])) {
+        if (JS_ToInt64(cx, &length, argv[fd_offset + 3]) < 0) {
+            return JS_EXCEPTION;
+        }
+
+        if (length < 0 || (size_t) length > buffer.length) {
+            JS_ThrowRangeError(cx, "length is out of range (must be <= %zu)",
+                               buffer.length);
+            return JS_EXCEPTION;
+        }
+
+        buffer.length = length;
+    }
+
+    pos = -1;
+
+    if (!JS_IsNullOrUndefined(argv[fd_offset + 4])) {
+        if (JS_ToInt64(cx, &pos, argv[fd_offset + 4]) < 0) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    if (pos == -1) {
+        n = read(fd, buffer.start, buffer.length);
+
+    } else {
+        n = pread(fd, buffer.start, buffer.length, pos);
+    }
+
+    if (n == -1) {
+        result = qjs_fs_error(cx, "read", strerror(errno), NULL, errno);
+        goto done;
+    }
+
+    if (calltype == QJS_FS_PROMISE) {
+        result = JS_NewObject(cx);
+        if (JS_IsException(result)) {
+            goto done;
+        }
+
+        if (JS_DefinePropertyValueStr(cx, result, "bytesRead",
+                                      JS_NewInt32(cx, n), JS_PROP_ENUMERABLE)
+            < 0)
+        {
+            JS_FreeValue(cx, result);
+            result = JS_EXCEPTION;
+            goto done;
+        }
+
+        if (JS_DefinePropertyValueStr(cx, result, "buffer",
+                                      JS_DupValue(cx, argv[fd_offset + 1]),
+                                      JS_PROP_ENUMERABLE)
+            < 0)
+        {
+            JS_FreeValue(cx, result);
+            result = JS_EXCEPTION;
+            goto done;
+        }
+
+    } else {
+        result = JS_NewInt32(cx, n);
+    }
+
+done:
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, JS_UNDEFINED);
+}
+
+
+static JSValue
+qjs_fs_read_file(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype)
+{
+    int                          fd, flags;
+    njs_str_t                    str;
+    JSValue                      callback, v, encode, options, result;
+    const char                   *path;
+    struct stat                  sb;
+    const qjs_buffer_encoding_t  *encoding;
+    char                         path_buf[NJS_MAX_PATH + 1];
+
+    path = qjs_fs_path(cx, path_buf, argv[0], "path");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    flags = O_RDONLY;
+
+    options = argv[1];
+    encode = JS_UNDEFINED;
+    callback = JS_UNDEFINED;
+
+    if (calltype == QJS_FS_CALLBACK) {
+        if (argc > 0) {
+            callback = argv[njs_min(argc - 1, 2)];
+        }
+
+        if (!JS_IsFunction(cx, callback)) {
+            JS_ThrowTypeError(cx, "\"callback\" must be a function");
+            return JS_EXCEPTION;
+        }
+
+        if (JS_SameValue(cx, options, callback)) {
+            options = JS_UNDEFINED;
+        }
+    }
+
+    if (JS_IsString(options)) {
+        encode = JS_DupValue(cx, options);
+
+    } else if (!JS_IsUndefined(options)) {
+        if (!JS_IsObject(options)) {
+            JS_ThrowTypeError(cx, "Unknown options type (a string or object "
+                              "required)");
+            return JS_EXCEPTION;
+        }
+
+        v = JS_GetPropertyStr(cx, options, "flag");
+        if (!JS_IsUndefined(v) && !JS_IsException(v)) {
+            flags = qjs_fs_flags(cx, v, O_RDONLY);
+            if (flags == -1) {
+                JS_FreeValue(cx, v);
+                return JS_EXCEPTION;
+            }
+        }
+
+        v = JS_GetPropertyStr(cx, options, "encoding");
+        if (!JS_IsUndefined(v) && !JS_IsException(v)) {
+            encode = v;
+        }
+    }
+
+    encoding = NULL;
+    if (!JS_IsUndefined(encode)) {
+        encoding = qjs_buffer_encoding(cx, encode, 1);
+        if (encoding == NULL) {
+            JS_FreeValue(cx, encode);
+            return JS_EXCEPTION;
+        }
+    }
+
+    JS_FreeValue(cx, encode);
+
+    fd = open(path, flags);
+    if (fd < 0) {
+        result = qjs_fs_error(cx, "open", strerror(errno), path, errno);
+        goto done;
+    }
+
+    if (fstat(fd, &sb) == -1) {
+        result = qjs_fs_error(cx, "stat", strerror(errno), path, errno);
+        goto done;
+    }
+
+    if (!S_ISREG(sb.st_mode)) {
+        result = qjs_fs_error(cx, "stat", "File is not regular", path, 0);
+        goto done;
+    }
+
+    str.start = NULL;
+    str.length = sb.st_size;
+
+    v = qjs_fs_fd_read(cx, fd, &str);
+    if (!JS_SameValue(cx, v, JS_TRUE)) {
+        if (JS_IsException(v)) {
+            result = JS_EXCEPTION;
+
+        } else {
+            result = qjs_fs_error(cx, "read", strerror(errno), path, errno);
+        }
+
+        goto done;
+    }
+
+    result = qjs_fs_encode(cx, encoding, &str);
+    js_free(cx, str.start);
+
+done:
+
+    if (fd != -1) {
+        (void) close(fd);
+    }
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, callback);
+}
+
+
+static JSValue
+qjs_fs_readlink(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype)
+{
+    ssize_t                      n;
+    JSValue                      callback, v, result, encode, options;
+    njs_str_t                    str;
+    const char                   *path, *enc;
+    const qjs_buffer_encoding_t  *encoding;
+    char                         path_buf[NJS_MAX_PATH + 1],
+                                 dst_buf[NJS_MAX_PATH + 1];
+
+    path = qjs_fs_path(cx, path_buf, argv[0], "path");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    options = argv[1];
+    encode = JS_UNDEFINED;
+    callback = JS_UNDEFINED;
+
+    if (calltype == QJS_FS_CALLBACK) {
+        callback = argv[njs_min(argc - 1, 2)];
+
+        if (!JS_IsFunction(cx, callback)) {
+            JS_ThrowTypeError(cx, "\"callback\" must be a function");
+            return JS_EXCEPTION;
+        }
+
+        if (JS_SameValue(cx, options, callback)) {
+            options = JS_UNDEFINED;
+        }
+    }
+
+    if (JS_IsString(options)) {
+        encode = JS_DupValue(cx, options);
+
+    } else if (!JS_IsUndefined(options)) {
+        if (!JS_IsObject(options)) {
+            JS_ThrowTypeError(cx, "Unknown options type (a string or object "
+                              "required)");
+            return JS_EXCEPTION;
+        }
+
+        v = JS_GetPropertyStr(cx, options, "encoding");
+        if (!JS_IsUndefined(v) && !JS_IsException(v)) {
+            encode = v;
+        }
+    }
+
+    encoding = NULL;
+    enc = JS_ToCString(cx, encode);
+    if (enc == NULL) {
+        JS_FreeValue(cx, encode);
+        return JS_EXCEPTION;
+    }
+
+    if (strncmp(enc, "buffer", 6) != 0) {
+        encoding = qjs_buffer_encoding(cx, encode, 1);
+        if (encoding == NULL) {
+            JS_FreeCString(cx, enc);
+            JS_FreeValue(cx, encode);
+            return JS_EXCEPTION;
+        }
+    }
+
+    JS_FreeCString(cx, enc);
+    JS_FreeValue(cx, encode);
+
+    str.start = (u_char *) dst_buf;
+    n = readlink(path, dst_buf, sizeof(dst_buf) - 1);
+    if (n < 0) {
+        result = qjs_fs_error(cx, "readlink", strerror(errno), path, errno);
+        goto done;
+    }
+
+    str.length = n;
+
+    result = qjs_fs_encode(cx, encoding, &str);
+
+done:
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, callback);
+}
+
+
+static JSValue
+qjs_fs_dirent_create(JSContext *cx, JSValueConst name, struct dirent *entry)
+{
+    JSValue  obj;
+
+    obj = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_FS_DIRENT);
+    if (JS_IsException(obj)) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_DefinePropertyValueStr(cx, obj, "name", name,
+                                  JS_PROP_ENUMERABLE) < 0)
+    {
+        JS_FreeValue(cx, obj);
+        return JS_EXCEPTION;
+    }
+
+    if (entry != NULL) {
+        if (JS_DefinePropertyValueStr(cx, obj, "type",
+                                      JS_NewInt32(cx, qjs_dentry_type(entry)),
+                                      0) < 0)
+        {
+            JS_FreeValue(cx, obj);
+            return JS_EXCEPTION;
+        }
+    }
+
+    return obj;
+}
+
+
+static JSValue
+qjs_fs_dirent_ctor(JSContext *cx, JSValueConst new_target, int argc,
+    JSValueConst *argv)
+{
+    if (argc < 1) {
+        JS_ThrowTypeError(cx, "name is required");
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_dirent_create(cx, JS_DupValue(cx, argv[0]), NULL);
+}
+
+
+static JSValue
+qjs_fs_readdir(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype)
+{
+    DIR                          *dir;
+    int                          types, idx;
+    njs_str_t                    str;
+    JSValue                      callback, v, result, encode, options, ename;
+    const char                   *path, *enc;
+    struct dirent                *entry;
+    const qjs_buffer_encoding_t  *encoding;
+    char                         path_buf[NJS_MAX_PATH + 1];
+
+    path = qjs_fs_path(cx, path_buf, argv[0], "path");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    types = 0;
+    options = argv[1];
+    encode = JS_UNDEFINED;
+    callback = JS_UNDEFINED;
+
+    if (calltype == QJS_FS_CALLBACK) {
+        if (argc > 0) {
+            callback = argv[njs_min(argc - 1, 2)];
+        }
+
+        if (!JS_IsFunction(cx, callback)) {
+            JS_ThrowTypeError(cx, "\"callback\" must be a function");
+            return JS_EXCEPTION;
+        }
+
+        if (JS_SameValue(cx, options, callback)) {
+            options = JS_UNDEFINED;
+        }
+    }
+
+    if (JS_IsString(options)) {
+        encode = JS_DupValue(cx, options);
+
+    } else if (!JS_IsUndefined(options)) {
+        if (!JS_IsObject(options)) {
+            JS_ThrowTypeError(cx, "Unknown options type (a string or object "
+                              "required)");
+            return JS_EXCEPTION;
+        }
+
+        v = JS_GetPropertyStr(cx, options, "encoding");
+        if (!JS_IsUndefined(v) && !JS_IsException(v)) {
+            encode = v;
+        }
+
+        v = JS_GetPropertyStr(cx, options, "withFileTypes");
+        if (!JS_IsUndefined(v) && !JS_IsException(v)) {
+            types = JS_ToBool(cx, v);
+        }
+    }
+
+    encoding = NULL;
+
+    enc = JS_ToCString(cx, encode);
+    if (enc == NULL) {
+        JS_FreeValue(cx, encode);
+        return JS_EXCEPTION;
+    }
+
+    if (strncmp(enc, "buffer", 6) != 0) {
+        encoding = qjs_buffer_encoding(cx, encode, 1);
+        if (encoding == NULL) {
+            JS_FreeCString(cx, enc);
+            JS_FreeValue(cx, encode);
+            return JS_EXCEPTION;
+        }
+    }
+
+    JS_FreeCString(cx, enc);
+    JS_FreeValue(cx, encode);
+
+    idx = 0;
+    dir = opendir(path);
+
+    if (dir == NULL) {
+        result = qjs_fs_error(cx, "opendir", strerror(errno), path, errno);
+        goto done;
+    }
+
+    result = JS_NewArray(cx);
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    for ( ;; ) {
+        errno = 0;
+        entry = readdir(dir);
+        if (entry == NULL) {
+            if (errno != 0) {
+                JS_FreeValue(cx, result);
+                result = qjs_fs_error(cx, "readdir", strerror(errno), path,
+                                      errno);
+            }
+
+            break;
+        }
+
+        if (entry->d_name[0] == '.' && (entry->d_name[1] == '\0'
+            || (entry->d_name[1] == '.' && entry->d_name[2] == '\0')))
+        {
+            continue;
+        }
+
+        str.start = (u_char *) entry->d_name;
+        str.length = strlen((const char *) str.start);
+
+        if (str.length == 0) {
+            continue;
+        }
+
+        ename = qjs_fs_encode(cx, encoding, &str);
+        if (JS_IsException(ename)) {
+            JS_FreeValue(cx, result);
+            goto done;
+        }
+
+        if (!types) {
+            if (JS_DefinePropertyValueUint32(cx, result, idx++, ename, 0) < 0) {
+                JS_FreeValue(cx, ename);
+                JS_FreeValue(cx, result);
+                goto done;
+            }
+
+        } else {
+            v = qjs_fs_dirent_create(cx, ename, entry);
+            if (JS_IsException(v)) {
+                JS_FreeValue(cx, ename);
+                JS_FreeValue(cx, result);
+                goto done;
+            }
+
+            if (JS_DefinePropertyValueUint32(cx, result, idx++, v, 0) < 0) {
+                JS_FreeValue(cx, ename);
+                JS_FreeValue(cx, result);
+                goto done;
+            }
+        }
+    }
+
+done:
+
+    if (dir != NULL) {
+        (void) closedir(dir);
+    }
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, callback);
+}
+
+
+
+static JSValue
+qjs_fs_realpath(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype)
+{
+    JSValue                      callback, v, encode, options, result;
+    njs_str_t                    str;
+    const char                   *path, *enc;
+    const qjs_buffer_encoding_t  *encoding;
+    char                         path_buf[NJS_MAX_PATH + 1],
+                                 dst_buf[NJS_MAX_PATH + 1];
+
+    path = qjs_fs_path(cx, path_buf, argv[0], "path");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    options = argv[1];
+    encode = JS_UNDEFINED;
+    callback = JS_UNDEFINED;
+
+    if (calltype == QJS_FS_CALLBACK) {
+        if (argc > 0) {
+            callback = argv[njs_min(argc - 1, 2)];
+        }
+
+        if (!JS_IsFunction(cx, callback)) {
+            JS_ThrowTypeError(cx, "\"callback\" must be a function");
+            return JS_EXCEPTION;
+        }
+
+        if (JS_SameValue(cx, options, callback)) {
+            options = JS_UNDEFINED;
+        }
+    }
+
+    if (JS_IsString(options)) {
+        encode = JS_DupValue(cx, options);
+
+    } else if (!JS_IsUndefined(options)) {
+        if (!JS_IsObject(options)) {
+            JS_ThrowTypeError(cx, "Unknown options type (a string or object "
+                              "required)");
+            return JS_EXCEPTION;
+        }
+
+        v = JS_GetPropertyStr(cx, options, "encoding");
+        if (!JS_IsUndefined(v) && !JS_IsException(v)) {
+            encode = v;
+        }
+    }
+
+    enc = JS_ToCString(cx, encode);
+    if (enc == NULL) {
+        JS_FreeValue(cx, encode);
+        return JS_EXCEPTION;
+    }
+
+    encoding = NULL;
+    if (strncmp(enc, "buffer", 6) != 0) {
+        encoding = qjs_buffer_encoding(cx, encode, 1);
+        if (encoding == NULL) {
+            JS_FreeCString(cx, enc);
+            JS_FreeValue(cx, encode);
+            return JS_EXCEPTION;
+        }
+    }
+
+    JS_FreeCString(cx, enc);
+    JS_FreeValue(cx, encode);
+
+    str.start = (u_char *) realpath(path, dst_buf);
+    if (str.start == NULL) {
+        result = qjs_fs_error(cx, "realpath", strerror(errno), path, errno);
+        goto done;
+    }
+
+    str.length = strlen((const char *) str.start);
+
+    result = qjs_fs_encode(cx, encoding, &str);
+
+done:
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, callback);
+}
+
+
+static JSValue
+qjs_fs_rename(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype)
+{
+    int         ret;
+    JSValue     callback, result;
+    const char  *path, *newpath;
+    char        path_buf[NJS_MAX_PATH + 1],
+                newpath_buf[NJS_MAX_PATH + 1];
+
+    path = qjs_fs_path(cx, path_buf, argv[0], "oldPath");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    newpath = qjs_fs_path(cx, newpath_buf, argv[1], "newPath");
+    if (newpath == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    callback = JS_UNDEFINED;
+
+    if (calltype == QJS_FS_CALLBACK) {
+        callback = argv[2];
+
+        if (!JS_IsFunction(cx, callback)) {
+            JS_ThrowTypeError(cx, "\"callback\" must be a function");
+            return JS_EXCEPTION;
+        }
+    }
+
+    result = JS_UNDEFINED;
+
+    ret = rename(path, newpath);
+    if (ret != 0) {
+        result = qjs_fs_error(cx, "rename", strerror(errno), NULL, errno);
+    }
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, callback);
+}
+
+
+typedef struct qjs_ftw_trace_s  qjs_ftw_trace_t;
+
+struct qjs_ftw_trace_s {
+    struct qjs_ftw_trace_s  *chain;
+    dev_t                   dev;
+    ino_t                   ino;
+};
+
+
+static int
+qjs_ftw(char *path, qjs_file_tree_walk_cb_t cb, int fd_limit,
+    qjs_ftw_flags_t flags, qjs_ftw_trace_t *parent)
+{
+    int              type, ret, dfd;
+    DIR              *d;
+    size_t           base, len, length;
+    const char       *d_name;
+    struct stat      st;
+    struct dirent    *entry;
+    qjs_ftw_trace_t  trace, *h;
+
+    ret = (flags & QJS_FTW_PHYS) ? lstat(path, &st) : stat(path, &st);
+
+    if (ret < 0) {
+        if (!(flags & QJS_FTW_PHYS) && errno == ENOENT && !lstat(path, &st)) {
+            type = QJS_FTW_SLN;
+
+        } else if (errno != EACCES) {
+            return -1;
+
+        } else {
+            type = QJS_FTW_NS;
+        }
+
+    } else if (S_ISDIR(st.st_mode)) {
+        type = (flags & QJS_FTW_DEPTH) ? QJS_FTW_DP : QJS_FTW_D;
+
+    } else if (S_ISLNK(st.st_mode)) {
+        type = (flags & QJS_FTW_PHYS) ? QJS_FTW_SL : QJS_FTW_SLN;
+
+    } else {
+        type = QJS_FTW_F;
+    }
+
+    if ((flags & QJS_FTW_MOUNT) && parent != NULL && st.st_dev != parent->dev) {
+        return 0;
+    }
+
+    for (h = parent; h != NULL; h = h->chain) {
+        if (h->dev == st.st_dev && h->ino == st.st_ino) {
+            return 0;
+        }
+    }
+
+    len = strlen(path);
+    base = len && (path[len - 1] == '/') ? len - 1 : len;
+
+    trace.chain = parent;
+    trace.dev = st.st_dev;
+    trace.ino = st.st_ino;
+
+    d = NULL;
+    dfd = -1;
+
+    if (type == QJS_FTW_D || type == QJS_FTW_DP) {
+        dfd = open(path, O_RDONLY);
+        if (dfd < 0) {
+            if (errno != EACCES) {
+                return -1;
+            }
+
+            type = QJS_FTW_DNR;
+        }
+    }
+
+    if (!(flags & QJS_FTW_DEPTH)) {
+        ret = cb(path, &st, type);
+        if (ret != 0) {
+            goto done;
+        }
+    }
+
+    if (type == QJS_FTW_D || type == QJS_FTW_DP) {
+        d = fdopendir(dfd);
+        if (d == NULL) {
+            ret = -1;
+            goto done;
+        }
+
+        for ( ;; ) {
+            entry = readdir(d);
+
+            if (entry == NULL) {
+                break;
+            }
+
+            d_name = entry->d_name;
+            length = strlen(d_name);
+
+            if ((length == 1 && d_name[0] == '.')
+                || (length == 2 && (d_name[0] == '.' && d_name[1] == '.')))
+            {
+                continue;
+            }
+
+            if (length >= (NJS_MAX_PATH - len)) {
+                errno = ENAMETOOLONG;
+                ret = -1;
+                goto done;
+            }
+
+            path[base] = '/';
+            memcpy(&path[base + 1], d_name, length + njs_length("\0"));
+
+            if (fd_limit != 0) {
+                ret = qjs_ftw(path, cb, fd_limit - 1, flags, &trace);
+                if (ret != 0) {
+                    goto done;
+                }
+            }
+        }
+
+        (void) closedir(d);
+        d = NULL;
+        dfd = -1;
+    }
+
+    path[len] = '\0';
+
+    if (flags & QJS_FTW_DEPTH) {
+        ret = cb(path, &st, type);
+        if (ret != 0) {
+            return ret;
+        }
+    }
+
+    ret = 0;
+
+done:
+
+    if (d != NULL) {
+        /* closedir() also closes underlying dfd. */
+        (void) closedir(d);
+
+    } else if (dfd >= 0) {
+        (void) close(dfd);
+    }
+
+    return ret;
+}
+
+
+static int
+qjs_file_tree_walk(const char *path, qjs_file_tree_walk_cb_t cb, int fd_limit,
+    qjs_ftw_flags_t flags)
+{
+    size_t  len;
+    char    pathbuf[NJS_MAX_PATH + 1];
+
+    len = strlen(path);
+    if (len > NJS_MAX_PATH) {
+        errno = ENAMETOOLONG;
+        return -1;
+    }
+
+    memcpy(pathbuf, path, len + 1);
+
+    return qjs_ftw(pathbuf, cb, fd_limit, flags, NULL);
+}
+
+
+static int
+qjs_fs_rmtree_cb(const char *path, const struct stat *sb, qjs_ftw_type_t type)
+{
+    njs_int_t  ret;
+
+    ret = remove(path);
+    if (ret != 0) {
+        return NJS_ERROR;
+    }
+
+    return NJS_OK;
+}
+
+
+static JSValue
+qjs_fs_rmtree(JSContext *cx, const char *path, int recursive)
+{
+    njs_int_t   ret;
+    const char  *description;
+
+    ret = rmdir(path);
+    if (ret == 0) {
+        return JS_UNDEFINED;
+    }
+
+    description = strerror(errno);
+
+    if (recursive && (errno == ENOTEMPTY || errno == EEXIST)) {
+        ret = qjs_file_tree_walk(path, qjs_fs_rmtree_cb, 16,
+                                 QJS_FTW_PHYS | QJS_FTW_MOUNT | QJS_FTW_DEPTH);
+
+        if (ret == 0) {
+            return JS_UNDEFINED;
+        }
+
+        description = strerror(errno);
+    }
+
+    return qjs_fs_error(cx, "rmdir", description, path, errno);
+}
+
+
+static JSValue
+qjs_fs_rmdir(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype)
+{
+    int         recursive;
+    JSValue     callback, v, options, result;
+    const char  *path;
+    char        path_buf[NJS_MAX_PATH + 1];
+
+    path = qjs_fs_path(cx, path_buf, argv[0], "path");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    recursive = 0;
+
+    options = argv[1];
+    callback = JS_UNDEFINED;
+
+    if (calltype == QJS_FS_CALLBACK) {
+        callback = argv[njs_min(argc - 1, 2)];
+
+        if (!JS_IsFunction(cx, callback)) {
+            JS_ThrowTypeError(cx, "\"callback\" must be a function");
+            return JS_EXCEPTION;
+        }
+
+        if (JS_SameValue(cx, options, callback)) {
+            options = JS_UNDEFINED;
+        }
+    }
+
+    if (!JS_IsUndefined(options)) {
+        if (!JS_IsObject(options)) {
+            JS_ThrowTypeError(cx, "Unknown options type (an object required)");
+            return JS_EXCEPTION;
+        }
+
+        v = JS_GetPropertyStr(cx, options, "recursive");
+        if (!JS_IsUndefined(v) && !JS_IsException(v)) {
+            recursive = JS_ToBool(cx, v);
+        }
+    }
+
+    result = qjs_fs_rmtree(cx, path, recursive);
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, callback);
+}
+
+
+static void
+qjs_fs_to_stat(qjs_stat_t *dst, struct stat *st)
+{
+    dst->st_dev = st->st_dev;
+    dst->st_mode = st->st_mode;
+    dst->st_nlink = st->st_nlink;
+    dst->st_uid = st->st_uid;
+    dst->st_gid = st->st_gid;
+    dst->st_rdev = st->st_rdev;
+    dst->st_ino = st->st_ino;
+    dst->st_size = st->st_size;
+    dst->st_blksize = st->st_blksize;
+    dst->st_blocks = st->st_blocks;
+
+#if (NJS_HAVE_STAT_ATIMESPEC)
+
+    dst->st_atim.tv_sec = st->st_atimespec.tv_sec;
+    dst->st_atim.tv_nsec = st->st_atimespec.tv_nsec;
+    dst->st_mtim.tv_sec = st->st_mtimespec.tv_sec;
+    dst->st_mtim.tv_nsec = st->st_mtimespec.tv_nsec;
+    dst->st_ctim.tv_sec = st->st_ctimespec.tv_sec;
+    dst->st_ctim.tv_nsec = st->st_ctimespec.tv_nsec;
+
+#elif (NJS_HAVE_STAT_ATIM)
+
+    dst->st_atim.tv_sec = st->st_atim.tv_sec;
+    dst->st_atim.tv_nsec = st->st_atim.tv_nsec;
+    dst->st_mtim.tv_sec = st->st_mtim.tv_sec;
+    dst->st_mtim.tv_nsec = st->st_mtim.tv_nsec;
+    dst->st_ctim.tv_sec = st->st_ctim.tv_sec;
+    dst->st_ctim.tv_nsec = st->st_ctim.tv_nsec;
+
+#if (NJS_HAVE_STAT_BIRTHTIM)
+    dst->st_birthtim.tv_sec = st->st_birthtim.tv_sec;
+    dst->st_birthtim.tv_nsec = st->st_birthtim.tv_nsec;
+#elif (NJS_HAVE__STAT_BIRTHTIM)
+    dst->st_birthtim.tv_sec = st->__st_birthtim.tv_sec;
+    dst->st_birthtim.tv_nsec = st->__st_birthtim.tv_nsec;
+#else
+    dst->st_birthtim.tv_sec = st->st_ctim.tv_sec;
+    dst->st_birthtim.tv_nsec = st->st_ctim.tv_nsec;
+#endif
+
+#else
+
+  dst->st_atim.tv_sec = st->st_atime;
+  dst->st_atim.tv_nsec = 0;
+  dst->st_mtim.tv_sec = st->st_mtime;
+  dst->st_mtim.tv_nsec = 0;
+  dst->st_ctim.tv_sec = st->st_ctime;
+  dst->st_ctim.tv_nsec = 0;
+  dst->st_birthtim.tv_sec = st->st_ctime;
+  dst->st_birthtim.tv_nsec = 0;
+
+#endif
+}
+
+
+static JSValue
+qjs_fs_stats_create(JSContext *cx, struct stat *st)
+{
+    JSValue     obj;
+    qjs_stat_t  *stat;
+
+    stat = js_malloc(cx, sizeof(qjs_stat_t));
+    if (stat == NULL) {
+        JS_ThrowOutOfMemory(cx);
+        return JS_EXCEPTION;
+    }
+
+    qjs_fs_to_stat(stat, st);
+
+    obj = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_FS_STATS);
+    if (JS_IsException(obj)) {
+        js_free(cx, stat);
+        return JS_EXCEPTION;
+    }
+
+    JS_SetOpaque(obj, stat);
+
+    return obj;
+}
+
+
+static JSValue
+qjs_fs_stat(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv,
+    int magic)
+{
+    int          ret, fd, fd_offset, throw, calltype;
+    JSValue      callback, result, options, value;
+    const char   *path;
+    struct stat  sb;
+    char         path_buf[NJS_MAX_PATH + 1];
+
+    fd = -1;
+    path = NULL;
+    calltype = magic & 3;
+
+    if ((magic >> 2) != QJS_FS_FSTAT) {
+        path = qjs_fs_path(cx, path_buf, argv[0], "path");
+        if (path == NULL) {
+            return JS_EXCEPTION;
+        }
+
+        options = argv[1];
+
+    } else {
+        if (calltype == QJS_FS_DIRECT) {
+            fd_offset = 0;
+            if (JS_ToInt32(cx, &fd, argv[fd_offset]) < 0) {
+                return JS_EXCEPTION;
+            }
+
+        } else {
+            fd_offset = -1;
+            if (JS_ToInt32(cx, &fd, this_val) < 0) {
+                return JS_EXCEPTION;
+            }
+        }
+
+        options = argv[fd_offset + 1];
+    }
+
+    callback = JS_UNDEFINED;
+
+    if (calltype == QJS_FS_CALLBACK) {
+        if (argc > 0) {
+            callback = argv[njs_min(argc - 1, 2)];
+        }
+
+        if (!JS_IsFunction(cx, callback)) {
+            JS_ThrowTypeError(cx, "\"callback\" must be a function");
+            return JS_EXCEPTION;
+        }
+
+        if (JS_SameValue(cx, options, callback)) {
+            options = JS_UNDEFINED;
+        }
+    }
+
+    throw = 1;
+
+    if (!JS_IsUndefined(options)) {
+        if (!JS_IsObject(options)) {
+            JS_ThrowTypeError(cx, "Unknown options type (an object required)");
+            return JS_EXCEPTION;
+        }
+
+        value = JS_GetPropertyStr(cx, options, "bigint");
+        if (!JS_IsUndefined(value)) {
+            JS_ThrowTypeError(cx, "\"bigint\" is not supported");
+            return JS_EXCEPTION;
+        }
+
+        if (calltype == QJS_FS_DIRECT) {
+            value = JS_GetPropertyStr(cx, options, "throwIfNoEntry");
+            if (!JS_IsUndefined(value)) {
+                throw = JS_ToBool(cx, value);
+            }
+        }
+    }
+
+    switch (magic >> 2) {
+    case QJS_FS_STAT:
+        ret = stat(path, &sb);
+        break;
+
+    case QJS_FS_LSTAT:
+        ret = lstat(path, &sb);
+        break;
+
+    case QJS_FS_FSTAT:
+    default:
+        ret = fstat(fd, &sb);
+        break;
+    }
+
+    if (ret != 0) {
+        if (errno != ENOENT || throw) {
+            result = qjs_fs_error(cx, ((magic >> 2) == QJS_FS_STAT)
+                                      ? "stat" : "lstat",
+                                  strerror(errno), path, errno);
+            if (JS_IsException(result)) {
+                return JS_EXCEPTION;
+            }
+
+        } else {
+            result = JS_UNDEFINED;
+        }
+
+        return qjs_fs_result(cx, result, calltype, callback);
+    }
+
+    result = qjs_fs_stats_create(cx, &sb);
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, callback);
+}
+
+
+static JSValue
+qjs_fs_symlink(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype)
+{
+    int         ret;
+    JSValue     callback, result, type;
+    const char  *target, *path;
+    char        target_buf[NJS_MAX_PATH + 1],
+                path_buf[NJS_MAX_PATH + 1];
+
+    target = qjs_fs_path(cx, target_buf, argv[0], "target");
+    if (target == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    path = qjs_fs_path(cx, path_buf, argv[1], "path");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    callback = JS_UNDEFINED;
+    type = argv[2];
+
+    if (calltype == QJS_FS_CALLBACK) {
+        callback = argv[njs_min(argc - 1, 3)];
+
+        if (!JS_IsFunction(cx, callback)) {
+            JS_ThrowTypeError(cx, "\"callback\" must be a function");
+            return JS_EXCEPTION;
+        }
+
+        if (JS_SameValue(cx, type, callback)) {
+            type = JS_UNDEFINED;
+        }
+    }
+
+    if (!JS_IsUndefined(type) && !JS_IsString(type)) {
+        JS_ThrowTypeError(cx, "\"type\" must be a string");
+        return JS_EXCEPTION;
+    }
+
+    result = JS_UNDEFINED;
+
+    ret = symlink(target, path);
+    if (ret != 0) {
+        result = qjs_fs_error(cx, "symlink", strerror(errno), path, errno);
+    }
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, callback);
+}
+
+
+static JSValue
+qjs_fs_write(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype)
+{
+    int                          fd, fd_offset;
+    u_char                       *to_free_content;
+    int64_t                      length, pos, offset;
+    ssize_t                      n;
+    JSValue                      buffer, v, result;
+    njs_str_t                    str, content;
+    const qjs_buffer_encoding_t  *encoding;
+
+    if (calltype == QJS_FS_DIRECT) {
+        fd_offset = 0;
+        if (JS_ToInt32(cx, &fd, argv[0]) < 0) {
+            return JS_EXCEPTION;
+        }
+
+    } else {
+        fd_offset = -1;
+        if (JS_ToInt32(cx, &fd, this_val) < 0) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    pos = -1;
+    encoding = NULL;
+    str.start = NULL;
+    to_free_content = NULL;
+    buffer = argv[fd_offset + 1];
+
+    /*
+     * fs.writeSync(fd, string[, position[, encoding]])
+     * fh.write(string[, position[, encoding]])
+     */
+
+    if (JS_IsString(buffer)) {
+        v = argv[fd_offset + 2];
+
+        if (!JS_IsNullOrUndefined(v)) {
+            if (JS_ToInt64(cx, &pos, v) < 0) {
+                return JS_EXCEPTION;
+            }
+        }
+
+        encoding = qjs_buffer_encoding(cx, argv[fd_offset + 3], 1);
+        if (encoding == NULL) {
+            return JS_EXCEPTION;
+        }
+
+        str.start = (u_char *) JS_ToCStringLen(cx, &str.length, buffer);
+        if (str.start == NULL) {
+            return JS_EXCEPTION;
+        }
+
+        if (encoding->decode_length != NULL) {
+            content.length = encoding->decode_length(cx, &str);
+            content.start = js_malloc(cx, content.length);
+            if (content.start == NULL) {
+                JS_FreeCString(cx, (const char *) str.start);
+                JS_ThrowOutOfMemory(cx);
+                return JS_EXCEPTION;
+            }
+
+            to_free_content = content.start;
+
+            if (encoding->decode(cx, &str, &content) != 0) {
+                JS_FreeCString(cx, (const char *) str.start);
+                return JS_EXCEPTION;
+            }
+
+        } else {
+            content.start = (u_char *) str.start;
+            content.length = str.length;
+        }
+
+        goto process;
+    }
+
+    /*
+     * fh.write(buffer, offset[, length[, position]])
+     * fs.writeSync(fd, buffer, offset[, length[, position]])
+     */
+
+    v = qjs_typed_array_data(cx, buffer, &content);
+    if (JS_IsException(v)) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_ToInt64(cx, &offset, argv[fd_offset + 2]) < 0) {
+        return JS_EXCEPTION;
+    }
+
+    if (offset < 0 || (size_t) offset > content.length) {
+        JS_ThrowRangeError(cx, "offset is out of range (must be <= %zu)",
+                           content.length);
+        return JS_EXCEPTION;
+    }
+
+    content.length -= offset;
+    content.start += offset;
+
+    v = argv[fd_offset + 3];
+    if (!JS_IsNullOrUndefined(v)) {
+        if (JS_ToInt64(cx, &length, v) < 0) {
+            return JS_EXCEPTION;
+        }
+
+        if (length < 0 || (size_t) length > content.length) {
+            JS_ThrowRangeError(cx, "length is out of range (must be <= %zu)",
+                               content.length);
+            return JS_EXCEPTION;
+        }
+
+        content.length = length;
+    }
+
+    v = argv[fd_offset + 4];
+    if (!JS_IsNullOrUndefined(v)) {
+        if (JS_ToInt64(cx, &pos, v) < 0) {
+            return JS_EXCEPTION;
+        }
+    }
+
+process:
+
+    if (pos == -1) {
+        n = write(fd, content.start, content.length);
+
+    } else {
+        n = pwrite(fd, content.start, content.length, pos);
+    }
+
+    if (n == -1) {
+        result = qjs_fs_error(cx, "write", strerror(errno), NULL, errno);
+        goto done;
+    }
+
+    if ((size_t) n != content.length) {
+        result = qjs_fs_error(cx, "write", "failed to write all the data",
+                              NULL, 0);
+        goto done;
+    }
+
+    if (calltype == QJS_FS_PROMISE) {
+        result = JS_NewObject(cx);
+        if (JS_IsException(result)) {
+            goto done;
+        }
+
+        if (JS_DefinePropertyValueStr(cx, result, "bytesWritten",
+                                      JS_NewInt32(cx, n),
+                                      JS_PROP_C_W_E) < 0)
+        {
+            JS_FreeValue(cx, result);
+            result = JS_EXCEPTION;
+            goto done;
+        }
+
+        buffer = JS_DupValue(cx, buffer);
+
+        if (JS_DefinePropertyValueStr(cx, result, "buffer", buffer,
+                                      JS_PROP_C_W_E) < 0)
+        {
+            JS_FreeValue(cx, result);
+            JS_FreeValue(cx, buffer);
+            result = JS_EXCEPTION;
+            goto done;
+        }
+
+    } else {
+        result = JS_NewInt32(cx, n);
+    }
+
+done:
+
+    if (str.start != NULL) {
+        JS_FreeCString(cx, (const char *) str.start);
+    }
+
+    if (to_free_content != NULL) {
+        js_free(cx, to_free_content);
+    }
+
+    return qjs_fs_result(cx, result, calltype, JS_UNDEFINED);
+}
+
+
+static JSValue
+qjs_fs_write_file(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int magic)
+{
+    int                          fd, flags, want_to_free_content;
+    u_char                       *p, *end;
+    mode_t                       md;
+    ssize_t                      n;
+    JSValue                      callback, data, v, encode, options, result;
+    njs_str_t                    str, content;
+    const char                   *path;
+    qjs_fs_calltype_t            calltype;
+    const qjs_buffer_encoding_t  *encoding;
+    char                         path_buf[NJS_MAX_PATH + 1];
+
+    path = qjs_fs_path(cx, path_buf, argv[0], "path");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    md = 0666;
+    flags = O_CREAT | O_WRONLY;
+    flags |= ((magic >> 2) == QJS_FS_APPEND) ? O_APPEND : O_TRUNC;
+    calltype = magic & 3;
+
+    encode = JS_UNDEFINED;
+    callback = JS_UNDEFINED;
+    options = argv[2];
+
+    if (calltype == QJS_FS_CALLBACK) {
+        if (argc > 0) {
+            callback = argv[njs_min(argc - 1, 3)];
+        }
+
+        if (!JS_IsFunction(cx, callback)) {
+            JS_ThrowTypeError(cx, "\"callback\" must be a function");
+            return JS_EXCEPTION;
+        }
+
+        if (JS_SameValue(cx, options, callback)) {
+            options = JS_UNDEFINED;
+        }
+    }
+
+    if (JS_IsString(options)) {
+        encode = JS_DupValue(cx, options);
+
+    } else if (!JS_IsUndefined(options)) {
+        if (!JS_IsObject(options)) {
+            JS_ThrowTypeError(cx, "Unknown options type (a string or object "
+                              "required)");
+            return JS_EXCEPTION;
+        }
+
+        v = JS_GetPropertyStr(cx, options, "flag");
+        if (!JS_IsUndefined(v) && !JS_IsException(v)) {
+            flags = qjs_fs_flags(cx, v, O_CREAT | O_WRONLY);
+            if (flags == -1) {
+                JS_FreeValue(cx, v);
+                return JS_EXCEPTION;
+            }
+        }
+
+        v = JS_GetPropertyStr(cx, options, "mode");
+        if (!JS_IsUndefined(v) && !JS_IsException(v)) {
+            md = qjs_fs_mode(cx, v, 0666);
+            if (md == (mode_t) -1) {
+                JS_FreeValue(cx, v);
+                return JS_EXCEPTION;
+            }
+        }
+
+        v = JS_GetPropertyStr(cx, options, "encoding");
+        if (!JS_IsUndefined(v) && !JS_IsException(v)) {
+            encode = v;
+        }
+    }
+
+    encoding = qjs_buffer_encoding(cx, encode, 1);
+    if (encoding == NULL) {
+        JS_FreeValue(cx, encode);
+        return JS_EXCEPTION;
+    }
+
+    JS_FreeValue(cx, encode);
+
+    data = argv[1];
+    str.start = NULL;
+    want_to_free_content = 0;
+
+    if (JS_IsString(data)) {
+        goto decode;
+    }
+
+    v = qjs_typed_array_data(cx, data, &content);
+    if (JS_IsException(v)) {
+decode:
+
+        str.start = (u_char *) JS_ToCStringLen(cx, &str.length, data);
+        if (str.start == NULL) {
+            return JS_EXCEPTION;
+        }
+
+        if (encoding->decode_length != NULL) {
+            content.length = encoding->decode_length(cx, &str);
+            content.start = js_malloc(cx, content.length);
+            if (content.start == NULL) {
+                JS_FreeCString(cx, (const char *) str.start);
+                JS_ThrowOutOfMemory(cx);
+                return JS_EXCEPTION;
+            }
+
+            want_to_free_content = 1;
+
+            if (encoding->decode(cx, &str, &content) != 0) {
+                JS_FreeCString(cx, (const char *) str.start);
+                return JS_EXCEPTION;
+            }
+
+        } else {
+            content.start = (u_char *) str.start;
+            content.length = str.length;
+        }
+    }
+
+    fd = open(path, flags, md);
+    if (fd < 0) {
+        result = qjs_fs_error(cx, "open", strerror(errno), path, errno);
+        goto done;
+    }
+
+    p = content.start;
+    end = p + content.length;
+
+    while (p < end) {
+        n = write(fd, p, end - p);
+        if (n == -1) {
+            if (errno == EINTR) {
+                continue;
+            }
+
+            result = qjs_fs_error(cx, "write", strerror(errno), path, errno);
+            goto done;
+        }
+
+        p += n;
+    }
+
+    result = JS_UNDEFINED;
+
+done:
+
+    if (fd != -1) {
+        (void) close(fd);
+    }
+
+    if (str.start != NULL) {
+        JS_FreeCString(cx, (const char *) str.start);
+    }
+
+    if (want_to_free_content) {
+        js_free(cx, content.start);
+    }
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, callback);
+}
+
+
+static JSValue
+qjs_fs_unlink(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int calltype)
+{
+    int         ret;
+    JSValue     callback, result;
+    const char  *path;
+    char        path_buf[NJS_MAX_PATH + 1];
+
+    path = qjs_fs_path(cx, path_buf, argv[0], "path");
+    if (path == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    callback = JS_UNDEFINED;
+
+    if (calltype == QJS_FS_CALLBACK) {
+        callback = argv[1];
+
+        if (!JS_IsFunction(cx, callback)) {
+            JS_ThrowTypeError(cx, "\"callback\" must be a function");
+            return JS_EXCEPTION;
+        }
+    }
+
+    result = JS_UNDEFINED;
+
+    ret = unlink(path);
+    if (ret != 0) {
+        result = qjs_fs_error(cx, "unlink", strerror(errno), path, errno);
+    }
+
+    if (JS_IsException(result)) {
+        return JS_EXCEPTION;
+    }
+
+    return qjs_fs_result(cx, result, calltype, callback);
+}
+
+
+static JSValue
+qjs_fs_stats_to_string_tag(JSContext *cx, JSValueConst this_val)
+{
+    return JS_NewString(cx, "Stats");
+}
+
+
+static JSValue
+qjs_fs_stats_test(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int testtype)
+{
+    unsigned    mask;
+    qjs_stat_t  *st;
+
+    st = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_FS_STATS);
+    if (st == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    switch (testtype) {
+    case DT_DIR:
+        mask = S_IFDIR;
+        break;
+
+    case DT_REG:
+        mask = S_IFREG;
+        break;
+
+    case DT_CHR:
+        mask = S_IFCHR;
+        break;
+
+    case DT_LNK:
+        mask = S_IFLNK;
+        break;
+
+    case DT_BLK:
+        mask = S_IFBLK;
+        break;
+
+    case DT_FIFO:
+        mask = S_IFIFO;
+        break;
+
+    case DT_SOCK:
+    default:
+        mask = S_IFSOCK;
+    }
+
+    return JS_NewBool(cx, (st->st_mode & S_IFMT) == mask);
+}
+
+
+static int
+qjs_fs_stats_get_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
+    JSValueConst obj, JSAtom prop)
+{
+    JSValue     value;
+    njs_str_t   name;
+    qjs_stat_t  *st;
+
+#define qjs_fs_time_ms(ts) ((ts)->tv_sec * 1000.0 + (ts)->tv_nsec / 1000000.0)
+
+    st = JS_GetOpaque2(cx, obj, QJS_CORE_CLASS_ID_FS_STATS);
+    if (st == NULL) {
+        (void) JS_ThrowInternalError(cx, "\"this\" is not a Stats object");
+        return -1;
+    }
+
+    name.start = (u_char *) JS_AtomToCString(cx, prop);
+    if (name.start == NULL) {
+        return -1;
+    }
+
+    name.length = strlen((const char *) name.start);
+
+    if (name.length < 3) {
+        JS_FreeCString(cx, (const char *) name.start);
+        return 0;
+    }
+
+    switch (name.start[0]) {
+    case 'a':
+        if (name.length == 5 && memcmp(name.start, "atime", 5) == 0) {
+            value = JS_NewDate(cx, qjs_fs_time_ms(&st->st_atim));
+            goto done;
+        }
+
+        if (name.length == 7 && memcmp(name.start, "atimeMs", 7) == 0) {
+            value = JS_NewFloat64(cx, qjs_fs_time_ms(&st->st_atim));
+            goto done;
+        }
+
+        break;
+
+    case 'b':
+        if (name.length == 6 && memcmp(name.start, "blocks", 6) == 0) {
+            value = JS_NewFloat64(cx, st->st_blocks);
+            goto done;
+        }
+
+        if (name.length == 7 && memcmp(name.start, "blksize", 7) == 0) {
+            value = JS_NewFloat64(cx, st->st_blksize);
+            goto done;
+        }
+
+        if (name.length == 9 && memcmp(name.start, "birthtime", 9) == 0) {
+            value = JS_NewDate(cx, qjs_fs_time_ms(&st->st_birthtim));
+            goto done;
+        }
+
+        if (name.length == 11 && memcmp(name.start, "birthtimeMs", 11) == 0) {
+            value = JS_NewFloat64(cx, qjs_fs_time_ms(&st->st_birthtim));
+            goto done;
+        }
+
+        break;
+
+    case 'c':
+        if (name.length == 5 && memcmp(name.start, "ctime", 5) == 0) {
+            value = JS_NewDate(cx, qjs_fs_time_ms(&st->st_ctim));
+            goto done;
+        }
+
+        if (name.length == 7 && memcmp(name.start, "ctimeMs", 7) == 0) {
+            value = JS_NewFloat64(cx, qjs_fs_time_ms(&st->st_ctim));
+            goto done;
+        }
+
+        break;
+
+    case 'd':
+        if (name.length == 3 && memcmp(name.start, "dev", 3) == 0) {
+            value = JS_NewFloat64(cx, st->st_dev);
+            goto done;
+        }
+
+        break;
+
+    case 'g':
+        if (name.length == 3 && memcmp(name.start, "gid", 3) == 0) {
+            value = JS_NewFloat64(cx, st->st_gid);
+            goto done;
+        }
+
+        break;
+
+    case 'i':
+        if (name.length == 3 && memcmp(name.start, "ino", 3) == 0) {
+            value = JS_NewFloat64(cx, st->st_ino);
+            goto done;
+        }
+
+        break;
+
+    case 'm':
+        if (name.length == 4 && memcmp(name.start, "mode", 4) == 0) {
+            value = JS_NewFloat64(cx, st->st_mode);
+            goto done;
+        }
+
+        if (name.length == 5 && memcmp(name.start, "mtime", 5) == 0) {
+            value = JS_NewDate(cx, qjs_fs_time_ms(&st->st_mtim));
+            goto done;
+        }
+
+        if (name.length == 7 && memcmp(name.start, "mtimeMs", 7) == 0) {
+            value = JS_NewFloat64(cx, qjs_fs_time_ms(&st->st_mtim));
+            goto done;
+        }
+
+        break;
+
+    case 'n':
+        if (name.length == 5 && memcmp(name.start, "nlink", 5) == 0) {
+            value = JS_NewFloat64(cx, st->st_nlink);
+            goto done;
+        }
+
+        break;
+
+    case 'r':
+        if (name.length == 4 && memcmp(name.start, "rdev", 4) == 0) {
+            value = JS_NewFloat64(cx, st->st_rdev);
+            goto done;
+        }
+
+        break;
+
+    case 's':
+        if (name.length == 4 && memcmp(name.start, "size", 4) == 0) {
+            value = JS_NewFloat64(cx, st->st_size);
+            goto done;
+        }
+
+        break;
+
+    case 'u':
+        if (name.length == 3 && memcmp(name.start, "uid", 3) == 0) {
+            value = JS_NewFloat64(cx, st->st_uid);
+            goto done;
+        }
+
+        break;
+    }
+
+    JS_FreeCString(cx, (const char *) name.start);
+
+    return 0;
+
+done:
+
+    JS_FreeCString(cx, (const char *) name.start);
+
+    if (pdesc != NULL) {
+        pdesc->flags = JS_PROP_ENUMERABLE | JS_PROP_CONFIGURABLE;
+        pdesc->getter = JS_UNDEFINED;
+        pdesc->setter = JS_UNDEFINED;
+        pdesc->value = value;
+    }
+
+    return 1;
+}
+
+
+static int
+qjs_fs_stats_get_own_property_names(JSContext *cx, JSPropertyEnum **ptab,
+    uint32_t *plen, JSValueConst obj)
+{
+    int       ret;
+    JSValue   keys;
+    unsigned  i;
+
+    static const char *stat_props[] = {
+        "atime",
+        "atimeMs",
+        "birthtime",
+        "birthtimeMs",
+        "blksize",
+        "blocks",
+        "ctime",
+        "ctimeMs",
+        "dev",
+        "gid",
+        "ino",
+        "mode",
+        "mtime",
+        "mtimeMs",
+        "nlink",
+        "size",
+        "rdev",
+        "uid",
+    };
+
+    keys = JS_NewObject(cx);
+    if (JS_IsException(keys)) {
+        return -1;
+    }
+
+    for (i = 0; i < njs_nitems(stat_props); i++) {
+        if (JS_DefinePropertyValueStr(cx, keys, stat_props[i],
+                                      JS_UNDEFINED, JS_PROP_C_W_E) < 0)
+        {
+            JS_FreeValue(cx, keys);
+            return -1;
+        }
+    }
+
+    ret = JS_GetOwnPropertyNames(cx, ptab, plen, keys, JS_GPN_STRING_MASK);
+
+    JS_FreeValue(cx, keys);
+
+    return ret;
+}
+
+
+static void
+qjs_fs_stats_finalizer(JSRuntime *rt, JSValue val)
+{
+    qjs_stat_t  *stat;
+
+    stat = JS_GetOpaque(val, QJS_CORE_CLASS_ID_FS_STATS);
+    if (stat != NULL) {
+        js_free_rt(rt, stat);
+    }
+}
+
+
+static JSValue
+qjs_fs_dirent_to_string_tag(JSContext *cx, JSValueConst this_val)
+{
+    return JS_NewString(cx, "Dirent");
+}
+
+
+static JSValue
+qjs_fs_dirent_test(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv, int testtype)
+{
+    int       value;
+    JSValue   type;
+
+    type = JS_GetPropertyStr(cx, this_val, "type");
+    if (JS_IsException(type)) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_VALUE_GET_TAG(type) != JS_TAG_INT) {
+        JS_FreeValue(cx, type);
+        return JS_FALSE;
+    }
+
+    value = JS_VALUE_GET_INT(type);
+    JS_FreeValue(cx, type);
+
+    if (value == QJS_DT_INVALID) {
+        JS_ThrowInternalError(cx, "dentry type is not supported on this "
+                              "platform");
+        return JS_EXCEPTION;
+    }
+
+    return JS_NewBool(cx, testtype == value);
+}
+
+
+static JSValue
+qjs_fs_filehandle_to_string_tag(JSContext *cx, JSValueConst this_val)
+{
+    return JS_NewString(cx, "FileHandle");
+}
+
+
+static JSValue
+qjs_fs_filehandle_fd(JSContext *cx, JSValueConst thisval)
+{
+    int  fd;
+
+    fd = (intptr_t) JS_GetOpaque2(cx, thisval, QJS_CORE_CLASS_ID_FS_FILEHANDLE);
+
+    if (fd == -1) {
+        return JS_ThrowTypeError(cx, "file was already closed");
+    }
+
+    return JS_NewInt32(cx, fd);
+}
+
+
+static JSValue
+qjs_fs_filehandle_value_of(JSContext *cx, JSValueConst thisval, int argc,
+    JSValueConst *argv)
+{
+    int  fd;
+
+    fd = (intptr_t) JS_GetOpaque2(cx, thisval, QJS_CORE_CLASS_ID_FS_FILEHANDLE);
+
+    if (fd == -1) {
+        return JS_ThrowTypeError(cx, "file was already closed");
+    }
+
+    return JS_NewInt32(cx, fd);
+}
+
+
+static void
+qjs_fs_filehandle_finalizer(JSRuntime *rt, JSValue val)
+{
+    int   fd;
+
+    fd = (intptr_t) JS_GetOpaque(val, QJS_CORE_CLASS_ID_FS_FILEHANDLE);
+
+    (void) close(fd);
+}
+
+
+static JSValue
+qjs_fs_promise_trampoline(JSContext *cx, int argc, JSValueConst *argv)
+{
+    return JS_Call(cx, argv[0], JS_UNDEFINED, 1, &argv[1]);
+}
+
+
+static JSValue
+qjs_fs_result(JSContext *cx, JSValue result, int calltype, JSValue callback)
+{
+    JS_BOOL  is_error;
+    JSValue  promise, callbacks[2], arguments[2];
+
+    switch (calltype) {
+    case QJS_FS_DIRECT:
+        if (JS_IsError(cx, result)) {
+            JS_Throw(cx, result);
+            return JS_EXCEPTION;
+        }
+
+        return result;
+
+    case QJS_FS_PROMISE:
+        promise = JS_NewPromiseCapability(cx, callbacks);
+        if (JS_IsException(promise)) {
+            JS_FreeValue(cx, result);
+            return JS_EXCEPTION;
+        }
+
+        is_error = !!JS_IsError(cx, result);
+
+        arguments[0] = callbacks[is_error];
+        arguments[1] = result;
+        JS_FreeValue(cx, callbacks[!is_error]);
+
+        if (JS_EnqueueJob(cx, qjs_fs_promise_trampoline, 2, arguments) < 0) {
+            JS_FreeValue(cx, promise);
+            JS_FreeValue(cx, callbacks[is_error]);
+            JS_FreeValue(cx, result);
+            return JS_EXCEPTION;
+        }
+
+        JS_FreeValue(cx, arguments[0]);
+        JS_FreeValue(cx, arguments[1]);
+
+        return promise;
+
+    case QJS_FS_CALLBACK:
+        if (JS_IsError(cx, result)) {
+            arguments[0] = result;
+            arguments[1] = JS_UNDEFINED;
+
+        } else {
+            arguments[0] = JS_UNDEFINED;
+            arguments[1] = result;
+        }
+
+        promise = JS_Call(cx, callback, JS_UNDEFINED, 2, arguments);
+        JS_FreeValue(cx, arguments[0]);
+        JS_FreeValue(cx, arguments[1]);
+
+        if (JS_IsException(promise)) {
+            return JS_EXCEPTION;
+        }
+
+        return JS_UNDEFINED;
+
+    default:
+        return JS_ThrowInternalError(cx, "unexpected calltype %d", calltype);
+    }
+}
+
+
+static int
+qjs_fs_flags(JSContext *cx, JSValue value, int default_flags)
+{
+    JSValue          ret;
+    njs_str_t       flags;
+    qjs_fs_entry_t  *fl;
+
+    if (JS_IsUndefined(value)) {
+        return default_flags;
+    }
+
+    ret = JS_ToString(cx, value);
+    if (JS_IsException(ret)) {
+        return -1;
+    }
+
+    flags.start = (u_char *) JS_ToCStringLen(cx, &flags.length, ret);
+    JS_FreeValue(cx, ret);
+    if (flags.start == NULL) {
+        return -1;
+    }
+
+    for (fl = &qjs_flags_table[0]; fl->name.length != 0; fl++) {
+        if (njs_strstr_eq(&flags, &fl->name)) {
+            JS_FreeCString(cx, (const char *) flags.start);
+            return fl->value;
+        }
+    }
+
+    JS_ThrowTypeError(cx, "Unknown file open flags: \"%s\"", flags.start);
+
+    JS_FreeCString(cx, (const char *) flags.start);
+
+    return -1;
+}
+
+
+static mode_t
+qjs_fs_mode(JSContext *cx, JSValue value, mode_t default_mode)
+{
+    int64_t  i64;
+
+    /* GCC complains about uninitialized i64. */
+    i64 = 0;
+
+    if (JS_IsUndefined(value)) {
+        return default_mode;
+    }
+
+    if (JS_ToInt64(cx, &i64, value) < 0) {
+        return (mode_t) -1;
+    }
+
+    return (mode_t) i64;
+}
+
+
+static JSValue
+qjs_fs_error(JSContext *cx, const char *syscall, const char *description,
+    const char *path, int errn)
+{
+    JSValue  value;
+
+    value = JS_NewError(cx);
+    if (JS_IsException(value)) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_SetPropertyStr(cx, value, "message",
+                          JS_NewString(cx, description)) < 0)
+    {
+        JS_FreeValue(cx, value);
+        return JS_EXCEPTION;
+    }
+
+    if (errn != 0) {
+        if (JS_SetPropertyStr(cx, value, "errno", JS_NewInt32(cx, errn)) < 0) {
+            JS_FreeValue(cx, value);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_SetPropertyStr(cx, value, "code",
+                              JS_NewString(cx, njs_errno_string(errn))) < 0)
+        {
+            JS_FreeValue(cx, value);
+            return JS_EXCEPTION;
+        }
+    }
+
+    if (path != NULL) {
+        if (JS_SetPropertyStr(cx, value, "path",
+                              JS_NewString(cx, path)) < 0)
+        {
+            JS_FreeValue(cx, value);
+            return JS_EXCEPTION;
+        }
+    }
+
+    if (syscall != NULL) {
+        if (JS_SetPropertyStr(cx, value, "syscall",
+                              JS_NewString(cx, syscall)) < 0)
+        {
+            JS_FreeValue(cx, value);
+            return JS_EXCEPTION;
+        }
+    }
+
+    return value;
+}
+
+
+static JSValue
+qjs_fs_encode(JSContext *cx, const qjs_buffer_encoding_t *encoding,
+    njs_str_t *str)
+{
+    JSValue    ret;
+    njs_str_t  data;
+
+    if (encoding == NULL) {
+        return qjs_buffer_create(cx, str->start, str->length);
+
+    } else if (encoding->encode_length != NULL) {
+        data.length = encoding->encode_length(cx, str);
+
+        data.start = js_malloc(cx, data.length);
+        if (data.start == NULL) {
+            JS_ThrowOutOfMemory(cx);
+            return JS_EXCEPTION;
+        }
+
+        if (encoding->encode(cx, str, &data) != 0) {
+            js_free(cx, data.start);
+            return JS_EXCEPTION;
+        }
+
+        ret = JS_NewStringLen(cx, (const char *) data.start, data.length);
+        js_free(cx, data.start);
+
+        return ret;
+    }
+
+    return JS_NewStringLen(cx, (const char *) str->start, str->length);
+}
+
+
+static char *
+qjs_fs_path(JSContext *cx, char storage[NJS_MAX_PATH + 1], JSValue src,
+    const char *prop_name)
+{
+    u_char       *p;
+    JSValue      val;
+    qjs_bytes_t  bytes;
+
+    if (!JS_IsString(src)) {
+        val = JS_GetTypedArrayBuffer(cx, src, NULL, NULL, NULL);
+        if (JS_IsException(val)) {
+            JS_ThrowTypeError(cx, "\"%s\" must be a string or Buffer",
+                              prop_name);
+            return NULL;
+        }
+
+        JS_FreeValue(cx, val);
+    }
+
+    if (qjs_to_bytes(cx, &bytes, src) != 0) {
+        return NULL;
+    }
+
+    if (bytes.length > NJS_MAX_PATH - 1) {
+        qjs_bytes_free(cx, &bytes);
+        JS_ThrowRangeError(cx, "\"%s\" is too long >= %d", prop_name,
+                                  NJS_MAX_PATH);
+        return NULL;
+    }
+
+    if (memchr(bytes.start, '\0', bytes.length) != 0) {
+        qjs_bytes_free(cx, &bytes);
+        JS_ThrowTypeError(cx, "\"%s\" must be a Buffer without null bytes",
+                          prop_name);
+        return NULL;
+    }
+
+    p = njs_cpymem(storage, bytes.start, bytes.length);
+    *p++ = '\0';
+
+    qjs_bytes_free(cx, &bytes);
+
+    return storage;
+}
+
+
+static int
+qjs_fs_module_init(JSContext *cx, JSModuleDef *m)
+{
+    int      rc;
+    JSValue  proto;
+
+    proto = JS_NewObject(cx);
+    JS_SetPropertyFunctionList(cx, proto, qjs_fs_export,
+                               njs_nitems(qjs_fs_export));
+
+    rc = JS_SetModuleExport(cx, m, "default", proto);
+    if (rc != 0) {
+        return -1;
+    }
+
+    return JS_SetModuleExportList(cx, m, qjs_fs_export,
+                                  njs_nitems(qjs_fs_export));
+}
+
+
+static JSModuleDef *
+qjs_fs_init(JSContext *cx, const char *name)
+{
+    int          rc;
+    JSValue      proto;
+    JSModuleDef  *m;
+
+    if (!JS_IsRegisteredClass(JS_GetRuntime(cx),
+                              QJS_CORE_CLASS_ID_FS_STATS))
+    {
+        if (JS_NewClass(JS_GetRuntime(cx), QJS_CORE_CLASS_ID_FS_STATS,
+                        &qjs_fs_stats_class) < 0)
+        {
+            return NULL;
+        }
+
+        proto = JS_NewObject(cx);
+        if (JS_IsException(proto)) {
+            return NULL;
+        }
+
+        JS_SetPropertyFunctionList(cx, proto, qjs_fs_stats_proto,
+                                   njs_nitems(qjs_fs_stats_proto));
+
+        JS_SetClassProto(cx, QJS_CORE_CLASS_ID_FS_STATS, proto);
+
+        proto = JS_NewObject(cx);
+        if (JS_IsException(proto)) {
+            return NULL;
+        }
+
+        JS_SetPropertyFunctionList(cx, proto, qjs_fs_dirent_proto,
+                                   njs_nitems(qjs_fs_dirent_proto));
+
+        JS_SetClassProto(cx, QJS_CORE_CLASS_ID_FS_DIRENT, proto);
+
+        if (JS_NewClass(JS_GetRuntime(cx), QJS_CORE_CLASS_ID_FS_FILEHANDLE,
+                        &qjs_fs_filehandle_class) < 0)
+        {
+            return NULL;
+        }
+
+        proto = JS_NewObject(cx);
+        if (JS_IsException(proto)) {
+            return NULL;
+        }
+
+        JS_SetPropertyFunctionList(cx, proto, qjs_fs_filehandle_proto,
+                                   njs_nitems(qjs_fs_filehandle_proto));
+
+        JS_SetClassProto(cx, QJS_CORE_CLASS_ID_FS_FILEHANDLE, proto);
+    }
+
+    m = JS_NewCModule(cx, name, qjs_fs_module_init);
+    if (m == NULL) {
+        return NULL;
+    }
+
+    JS_AddModuleExport(cx, m, "default");
+    rc = JS_AddModuleExportList(cx, m, qjs_fs_export,
+                                njs_nitems(qjs_fs_export));
+    if (rc != 0) {
+        return NULL;
+    }
+
+    return m;
+}
diff --git a/src/qjs.h b/src/qjs.h
index 81769abb..dec6419d 100644
--- a/src/qjs.h
+++ b/src/qjs.h
@@ -33,10 +33,13 @@
 #include <pthread.h>
 
 
-#define QJS_CORE_CLASS_ID_OFFSET  64
-#define QJS_CORE_CLASS_ID_BUFFER  (QJS_CORE_CLASS_ID_OFFSET)
+#define QJS_CORE_CLASS_ID_OFFSET    64
+#define QJS_CORE_CLASS_ID_BUFFER    (QJS_CORE_CLASS_ID_OFFSET)
 #define QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR (QJS_CORE_CLASS_ID_OFFSET + 1)
-#define QJS_CORE_CLASS_ID_LAST    (QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR)
+#define QJS_CORE_CLASS_ID_FS_STATS  (QJS_CORE_CLASS_ID_OFFSET + 2)
+#define QJS_CORE_CLASS_ID_FS_DIRENT (QJS_CORE_CLASS_ID_OFFSET + 3)
+#define QJS_CORE_CLASS_ID_FS_FILEHANDLE (QJS_CORE_CLASS_ID_OFFSET + 4)
+#define QJS_CORE_CLASS_ID_LAST      (QJS_CORE_CLASS_ID_OFFSET + 5)
 
 
 typedef JSModuleDef *(*qjs_addon_init_pt)(JSContext *ctx, const char *name);
diff --git a/test/fs/methods.t.mjs b/test/fs/methods.t.mjs
index 9b695296..928d1682 100644
--- a/test/fs/methods.t.mjs
+++ b/test/fs/methods.t.mjs
@@ -116,7 +116,7 @@ let readfile_tests = () => [
           return true;
       } },
 
-    { args: ["test/fs/non_utf8", "utf8"], expected: "��" },
+    { args: ["test/fs/non_utf8", "utf8"], expected: "��", skip() { return njs && njs.engine == 'QuickJS'; } },
     { args: ["test/fs/non_utf8", {encoding: "hex"}], expected: "8080" },
     { args: ["test/fs/non_utf8", "base64"], expected: "gIA=" },
     { args: ["test/fs/ascii", "utf8"], expected: "x".repeat(600) },
@@ -219,7 +219,7 @@ let writefile_tests = () => [
     { args: ["@", Buffer.from("XYZ"),  {encoding: "utf8", mode: 0o666}],
       expected: Buffer.from("XYZ") },
     { args: ["@", new DataView(Buffer.alloc(3).fill(66).buffer)],
-      expected: Buffer.from("BBB") },
+      expected: Buffer.from("BBB"), skip() { return njs && njs.engine == 'QuickJS'; } },
     { args: ["@", new Uint8Array(Buffer.from("ABCD"))],
       expected: Buffer.from("ABCD")},
     { args: ["@", "XYZ"], expected: Buffer.from("XYZ")},
@@ -309,7 +309,7 @@ let append_tests = () => [
     { args: ["@", Buffer.from("XYZ"),  {encoding: "utf8", mode: 0o666}],
       expected: Buffer.from("XYZXYZ") },
     { args: ["@", new DataView(Buffer.alloc(3).fill(66).buffer)],
-      expected: Buffer.from("BBBBBB") },
+      expected: Buffer.from("BBBBBB"), skip() { return njs && njs.engine == 'QuickJS'; } },
     { args: ["@", new Uint8Array(Buffer.from("ABCD"))],
       expected: Buffer.from("ABCDABCD")},
     { args: ["@", "XYZ"], expected: Buffer.from("XYZXYZ")},


More information about the nginx-devel mailing list