diff --git a/lib/fs.js b/lib/fs.js index 1ea70ff192d6dd..3088cdb11dffa6 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -25,6 +25,7 @@ 'use strict'; const { + Array, ArrayFromAsync, ArrayPrototypePush, BigIntPrototypeToString, @@ -531,12 +532,18 @@ function readFileSync(path, options) { validateReadFileBufferOptions(options); const hasUserBuffer = options.buffer !== undefined; - if ((options.encoding === 'utf8' || options.encoding === 'utf-8') && - !hasUserBuffer) { + if (!hasUserBuffer) { if (!isInt32(path)) { path = getValidatedPath(path); } - return binding.readFileUtf8(path, stringToFlags(options.flag)); + const flags = stringToFlags(options.flag); + // C++ fast paths: single native call instead of open/stat/read/close. + if (options.encoding === 'utf8' || options.encoding === 'utf-8') { + return binding.readFileUtf8(path, flags); + } + if (options.encoding === undefined || options.encoding === null) { + return binding.readFileBuffer(path, flags); + } } const isUserFd = isFd(path); // File descriptor ownership @@ -1756,45 +1763,35 @@ function handleFilePaths({ result, currentPath, context }) { } /** - * An iterative algorithm for reading the entire contents of the `basePath` directory. - * This function does not validate `basePath` as a directory. It is passed directly to - * `binding.readdir`. + * Reads the entire contents of the `basePath` directory recursively. + * This function does not validate `basePath` as a directory. It is passed + * directly to `binding.readdirRecursiveSync`. * @param {string} basePath * @param {{ encoding: string, withFileTypes: boolean }} options * @returns {string[] | Dirent[]} */ function readdirSyncRecursive(basePath, options) { - const context = { - withFileTypes: Boolean(options.withFileTypes), - encoding: options.encoding, + const withFileTypes = Boolean(options.withFileTypes); + // C++ walk uses scandir dirent types and avoids per-entry stat() for the + // common case (known directory/file types). + const result = binding.readdirRecursiveSync( basePath, - readdirResults: [], - pathsQueue: [basePath], - }; - - function read(path) { - const readdirResult = binding.readdir( - path, - context.encoding, - context.withFileTypes, - ); - - if (readdirResult === undefined) { - return; - } - - processReaddirResult({ - result: readdirResult, - currentPath: path, - context, - }); + options.encoding, + withFileTypes, + ); + if (result === undefined) { + return undefined; } - - for (let i = 0; i < context.pathsQueue.length; i++) { - read(context.pathsQueue[i]); + if (!withFileTypes) { + return result; } - - return context.readdirResults; + // result = [names, types, parentPaths] + const { 0: names, 1: types, 2: parentPaths } = result; + const out = new Array(names.length); + for (let i = 0; i < names.length; i++) { + out[i] = new Dirent(names[i], types[i], parentPaths[i]); + } + return out; } /** @@ -2919,6 +2916,19 @@ function writeFileSync(path, data, options) { ); } + // C++ fast path for Buffer / TypedArray payloads (no flush option). + if (isArrayBufferView(data) && !flush) { + if (!isInt32(path)) { + path = getValidatedPath(path); + } + return binding.writeFileBuffer( + path, + data, + stringToFlags(flag), + parseFileMode(options.mode, 'mode', 0o666), + ); + } + if (!isArrayBufferView(data)) { validateStringAfterArrayBufferView(data, 'data'); data = Buffer.from(data, options.encoding || 'utf8'); diff --git a/src/node_errors.h b/src/node_errors.h index 62cfba88f00d43..3c67516ea13174 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -86,6 +86,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details); V(ERR_FS_CP_NON_DIR_TO_DIR, Error) \ V(ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY, Error) \ V(ERR_FS_EISDIR, Error) \ + V(ERR_FS_FILE_TOO_LARGE, RangeError) \ V(ERR_FS_CP_EEXIST, Error) \ V(ERR_FS_CP_SOCKET, Error) \ V(ERR_FS_CP_FIFO_PIPE, Error) \ diff --git a/src/node_file.cc b/src/node_file.cc index d93f213202ec43..bd359892382564 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -47,6 +47,8 @@ #include #include #include +#include +#include #if defined(__MINGW32__) || defined(_MSC_VER) #include @@ -91,6 +93,10 @@ using v8::Value; #define S_ISDIR(mode) (((mode)&S_IFMT) == S_IFDIR) #endif +#ifndef S_ISREG +#define S_ISREG(mode) (((mode)&S_IFMT) == S_IFREG) +#endif + #ifdef __POSIX__ constexpr char kPathSeparator = '/'; #else @@ -2946,6 +2952,376 @@ static void ReadFileUtf8(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(val); } +// Fast C++ path for fs.readFileSync(path) / fs.readFileSync(path, null) +// returning a Buffer — avoids open/fstat/read/close JS round-trips. +static void ReadFileBuffer(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + CHECK_GE(args.Length(), 2); + CHECK(args[1]->IsInt32()); + const int flags = args[1].As()->Value(); + + uv_file file; + uv_fs_t req; + + bool is_fd = args[0]->IsInt32(); + + if (is_fd) { + file = args[0].As()->Value(); + } else { + BufferValue path(isolate, args[0]); + CHECK_NOT_NULL(*path); + ToNamespacedPath(env, &path); + if (CheckOpenPermissions(env, path, flags).IsNothing()) return; + + FS_SYNC_TRACE_BEGIN(open); + file = uv_fs_open(nullptr, &req, *path, flags, 0666, nullptr); + FS_SYNC_TRACE_END(open); + if (req.result < 0) { + uv_fs_req_cleanup(&req); + return env->ThrowUVException( + static_cast(req.result), "open", nullptr, path.out()); + } + uv_fs_req_cleanup(&req); + } + + auto defer_close = OnScopeLeave([file, is_fd, &req]() { + if (!is_fd) { + FS_SYNC_TRACE_BEGIN(close); + CHECK_EQ(0, uv_fs_close(nullptr, &req, file, nullptr)); + FS_SYNC_TRACE_END(close); + } + uv_fs_req_cleanup(&req); + }); + + // Prefer a single allocation when fstat reports a regular file size. + // Match JS tryCreateBuffer: refuse files larger than 2 GiB - 1. + constexpr int64_t kIoMaxLength = (static_cast(1) << 31) - 1; + int64_t size = -1; + { + FS_SYNC_TRACE_BEGIN(fstat); + int r = uv_fs_fstat(nullptr, &req, file, nullptr); + FS_SYNC_TRACE_END(fstat); + if (r == 0) { + const uv_stat_t* s = static_cast(req.ptr); + if (S_ISREG(s->st_mode) && s->st_size >= 0) { + if (s->st_size > kIoMaxLength) { + uv_fs_req_cleanup(&req); + std::string msg = "File size (" + + std::to_string(static_cast(s->st_size)) + + ") is greater than 2 GiB"; + return THROW_ERR_FS_FILE_TOO_LARGE(env, msg.c_str()); + } + size = static_cast(s->st_size); + } + } + uv_fs_req_cleanup(&req); + } + + if (size > 0) { + Local buf_obj; + if (!Buffer::New(isolate, static_cast(size)).ToLocal(&buf_obj)) { + return; + } + char* data = Buffer::Data(buf_obj); + size_t remaining = static_cast(size); + size_t offset = 0; + + FS_SYNC_TRACE_BEGIN(read); + while (remaining > 0) { + uv_buf_t buf = uv_buf_init(data + offset, remaining); + auto r = uv_fs_read(nullptr, &req, file, &buf, 1, -1, nullptr); + if (req.result < 0) { + FS_SYNC_TRACE_END(read); + return env->ThrowUVException( + static_cast(req.result), "read", nullptr); + } + if (r == 0) break; + offset += static_cast(r); + remaining -= static_cast(r); + uv_fs_req_cleanup(&req); + } + FS_SYNC_TRACE_END(read); + + if (offset < static_cast(size)) { + // Truncate logical length without reallocating when the file shrank. + args.GetReturnValue().Set( + Buffer::Copy(isolate, data, offset).ToLocalChecked()); + return; + } + args.GetReturnValue().Set(buf_obj); + return; + } + + // Unknown / zero size: grow like ReadFileUtf8. + std::string result; + char stack_buf[8192]; + uv_buf_t buf = uv_buf_init(stack_buf, sizeof(stack_buf)); + + FS_SYNC_TRACE_BEGIN(read); + while (true) { + auto r = uv_fs_read(nullptr, &req, file, &buf, 1, -1, nullptr); + if (req.result < 0) { + FS_SYNC_TRACE_END(read); + return env->ThrowUVException( + static_cast(req.result), "read", nullptr); + } + if (r <= 0) break; + result.append(buf.base, static_cast(r)); + uv_fs_req_cleanup(&req); + } + FS_SYNC_TRACE_END(read); + + Local out; + if (!Buffer::Copy(isolate, result.data(), result.size()).ToLocal(&out)) { + return; + } + args.GetReturnValue().Set(out); +} + +// Fast C++ path for fs.writeFileSync(path, buffer) with ArrayBufferView data. +// Mirrors WriteFileUtf8 but takes a Buffer/TypedArray payload. +static void WriteFileBuffer(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CHECK_EQ(args.Length(), 4); + CHECK(Buffer::HasInstance(args[1])); + CHECK(args[2]->IsInt32()); + CHECK(args[3]->IsInt32()); + + Local data_obj = args[1].As(); + char* data = Buffer::Data(data_obj); + const size_t length = Buffer::Length(data_obj); + const int flags = args[2].As()->Value(); + const int mode = args[3].As()->Value(); + + uv_file file; + bool is_fd = args[0]->IsInt32(); + + if (is_fd) { + file = args[0].As()->Value(); + } else { + BufferValue path(env->isolate(), args[0]); + CHECK_NOT_NULL(*path); + ToNamespacedPath(env, &path); + if (CheckOpenPermissions(env, path, flags).IsNothing()) return; + + FSReqWrapSync req_open("open", *path); + FS_SYNC_TRACE_BEGIN(open); + file = + SyncCallAndThrowOnError(env, &req_open, uv_fs_open, *path, flags, mode); + FS_SYNC_TRACE_END(open); + if (is_uv_error(file)) return; + } + + int bytes_written = 0; + size_t offset = 0; + uv_buf_t uvbuf = uv_buf_init(data, length); + + FS_SYNC_TRACE_BEGIN(write); + while (offset < length) { + FSReqWrapSync req_write("write"); + bytes_written = SyncCallAndThrowOnError( + env, &req_write, uv_fs_write, file, &uvbuf, 1, -1); + if (bytes_written < 0) break; + offset += static_cast(bytes_written); + DCHECK_LE(offset, length); + uvbuf.base += bytes_written; + uvbuf.len -= static_cast(bytes_written); + } + FS_SYNC_TRACE_END(write); + + if (!is_fd) { + FSReqWrapSync req_close("close"); + FS_SYNC_TRACE_BEGIN(close); + int result = SyncCallAndThrowOnError(env, &req_close, uv_fs_close, file); + FS_SYNC_TRACE_END(close); + if (is_uv_error(result)) return; + } +} + +// Map st_mode to a uv_dirent type (used when scandir reports +// UV_DIRENT_UNKNOWN). +static int ModeToDirentType(uint64_t mode) { + switch (mode & S_IFMT) { + case S_IFDIR: + return UV_DIRENT_DIR; + case S_IFREG: + return UV_DIRENT_FILE; +#ifdef S_IFLNK + case S_IFLNK: + return UV_DIRENT_LINK; +#endif +#ifdef S_IFIFO + case S_IFIFO: + return UV_DIRENT_FIFO; +#endif +#ifdef S_IFSOCK + case S_IFSOCK: + return UV_DIRENT_SOCKET; +#endif +#ifdef S_IFCHR + case S_IFCHR: + return UV_DIRENT_CHAR; +#endif +#ifdef S_IFBLK + case S_IFBLK: + return UV_DIRENT_BLOCK; +#endif + default: + return UV_DIRENT_UNKNOWN; + } +} + +// Fast recursive directory walk for readdirSync({ recursive: true }). +// Uses dirent types from scandir when available so most entries avoid an +// extra stat() (the JS path calls internalModuleStat per entry). +static void ReadDirRecursiveSync(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + CHECK_GE(args.Length(), 2); + BufferValue base_path(isolate, args[0]); + CHECK_NOT_NULL(*base_path); + ToNamespacedPath(env, &base_path); + + const enum encoding encoding = ParseEncoding(isolate, args[1], UTF8); + const bool with_types = args.Length() > 2 && args[2]->IsTrue(); + + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, + permission::PermissionScope::kFileSystemRead, + base_path.ToStringView()); + + std::string base(*base_path, base_path.length()); + // BFS queue matches the historical JS walk order. + std::vector queue; + queue.push_back(base); + size_t qhead = 0; + + LocalVector out_names(isolate); + LocalVector out_types(isolate); + LocalVector out_paths(isolate); // parentPath for Dirent + + const char sep = +#ifdef _WIN32 + '\\'; +#else + '/'; +#endif + + while (qhead < queue.size()) { + std::string current = std::move(queue[qhead++]); + + uv_fs_t req; + FS_SYNC_TRACE_BEGIN(readdir); + int err = uv_fs_scandir(nullptr, &req, current.c_str(), 0, nullptr); + FS_SYNC_TRACE_END(readdir); + if (err < 0) { + uv_fs_req_cleanup(&req); + return env->ThrowUVException(err, "scandir", nullptr, current.c_str()); + } + + // Encode parent path once per directory (withFileTypes). + Local parent_v; + if (with_types) { + if (!StringBytes::Encode(isolate, current.c_str(), encoding) + .ToLocal(&parent_v)) { + uv_fs_req_cleanup(&req); + return; + } + } + + for (;;) { + uv_dirent_t ent; + int r = uv_fs_scandir_next(&req, &ent); + if (r == UV_EOF) break; + if (r < 0) { + uv_fs_req_cleanup(&req); + return env->ThrowUVException(r, "scandir", nullptr, current.c_str()); + } + + std::string full = current; + if (full.empty() || full.back() != sep) full.push_back(sep); + full.append(ent.name); + + int ent_type = ent.type; + + // Resolve UNKNOWN types (common on some filesystems / Windows) so + // Dirent.isDirectory() / isFile() match getDirent()'s lstat behavior. + if (with_types && ent_type == UV_DIRENT_UNKNOWN) { + uv_fs_t lreq; + if (uv_fs_lstat(nullptr, &lreq, full.c_str(), nullptr) == 0) { + const uv_stat_t* s = static_cast(lreq.ptr); + ent_type = ModeToDirentType(s->st_mode); + } + uv_fs_req_cleanup(&lreq); + } + + if (with_types) { + Local base_name_v; + if (!StringBytes::Encode(isolate, ent.name, encoding) + .ToLocal(&base_name_v)) { + uv_fs_req_cleanup(&req); + return; + } + out_names.push_back(base_name_v); + out_types.push_back(Integer::New(isolate, ent_type)); + out_paths.push_back(parent_v); + } else { + // Relative path from base (JS returns relative paths). + const char* rel = ent.name; + if (full.size() > base.size() && + full.compare(0, base.size(), base) == 0) { + size_t start = base.size(); + if (start < full.size() && + (full[start] == '/' || full[start] == '\\')) { + start++; + } + rel = full.c_str() + start; + } + Local name_v; + if (!StringBytes::Encode(isolate, rel, encoding).ToLocal(&name_v)) { + uv_fs_req_cleanup(&req); + return; + } + out_names.push_back(name_v); + } + + // Descend into directories. For symlinks / unknown types, follow with + // stat() like internalModuleStat (JS: isDirectory() || stat===1). + bool is_dir = ent_type == UV_DIRENT_DIR; + if (!is_dir && + (ent_type == UV_DIRENT_UNKNOWN || ent_type == UV_DIRENT_LINK)) { + uv_fs_t sreq; + if (uv_fs_stat(nullptr, &sreq, full.c_str(), nullptr) == 0) { + const uv_stat_t* s = static_cast(sreq.ptr); + is_dir = S_ISDIR(s->st_mode); + } + uv_fs_req_cleanup(&sreq); + } + + if (is_dir) { + queue.push_back(std::move(full)); + } + } + uv_fs_req_cleanup(&req); + } + + Local names = Array::New(isolate, out_names.data(), out_names.size()); + if (with_types) { + Local parts[] = { + names, + Array::New(isolate, out_types.data(), out_types.size()), + Array::New(isolate, out_paths.data(), out_paths.size()), + }; + args.GetReturnValue().Set(Array::New(isolate, parts, arraysize(parts))); + } else { + args.GetReturnValue().Set(names); + } +} + // Wrapper for readv(2). // // bytesRead = fs.readv(fd, buffers[, position], callback) @@ -4160,6 +4536,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, SetMethod(isolate, target, "openFileHandle", OpenFileHandle); SetMethod(isolate, target, "read", Read); SetMethod(isolate, target, "readFileUtf8", ReadFileUtf8); + SetMethod(isolate, target, "readFileBuffer", ReadFileBuffer); SetMethod(isolate, target, "readBuffers", ReadBuffers); SetMethod(isolate, target, "fdatasync", Fdatasync); SetMethod(isolate, target, "fsync", Fsync); @@ -4182,6 +4559,8 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, SetMethod(isolate, target, "writeBuffers", WriteBuffers); SetMethod(isolate, target, "writeString", WriteString); SetMethod(isolate, target, "writeFileUtf8", WriteFileUtf8); + SetMethod(isolate, target, "writeFileBuffer", WriteFileBuffer); + SetMethod(isolate, target, "readdirRecursiveSync", ReadDirRecursiveSync); SetMethod(isolate, target, "realpath", RealPath); SetMethod(isolate, target, "copyFile", CopyFile); @@ -4284,6 +4663,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(OpenFileHandle); registry->Register(Read); registry->Register(ReadFileUtf8); + registry->Register(ReadFileBuffer); registry->Register(ReadBuffers); registry->Register(Fdatasync); registry->Register(Fsync); @@ -4306,6 +4686,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(WriteBuffers); registry->Register(WriteString); registry->Register(WriteFileUtf8); + registry->Register(WriteFileBuffer); + registry->Register(ReadDirRecursiveSync); registry->Register(RealPath); registry->Register(CopyFile); diff --git a/typings/internalBinding/fs.d.ts b/typings/internalBinding/fs.d.ts index 6e1702996dfcf1..e8fa3ca9a083e0 100644 --- a/typings/internalBinding/fs.d.ts +++ b/typings/internalBinding/fs.d.ts @@ -180,6 +180,11 @@ declare namespace InternalFSBinding { function readdir(path: StringOrBuffer, encoding: unknown, withFileTypes: false, usePromises: typeof kUsePromises): Promise; function readFileUtf8(path: StringOrBuffer, flags: number): string; + function readFileBuffer(path: StringOrBuffer, flags: number): Buffer; + + function readdirRecursiveSync(path: StringOrBuffer, encoding: unknown, withFileTypes: true): [string[], number[], string[]]; + function readdirRecursiveSync(path: StringOrBuffer, encoding: unknown, withFileTypes: false): string[]; + function readdirRecursiveSync(path: StringOrBuffer, encoding: unknown, withFileTypes: boolean): string[] | [string[], number[], string[]]; function readlink(path: StringOrBuffer, encoding: unknown, req: FSReqCallback): void; function readlink(path: StringOrBuffer, encoding: unknown, req: undefined, ctx: FSSyncContext): string | Buffer; @@ -241,6 +246,9 @@ declare namespace InternalFSBinding { function writeFileUtf8(path: string, data: string, flag: number, mode: number): void; function writeFileUtf8(fd: number, data: string, flag: number, mode: number): void; + + function writeFileBuffer(path: string, data: ArrayBufferView, flag: number, mode: number): void; + function writeFileBuffer(fd: number, data: ArrayBufferView, flag: number, mode: number): void; } export interface FsBinding { @@ -283,6 +291,8 @@ export interface FsBinding { read: typeof InternalFSBinding.read; readBuffers: typeof InternalFSBinding.readBuffers; readdir: typeof InternalFSBinding.readdir; + readdirRecursiveSync: typeof InternalFSBinding.readdirRecursiveSync; + readFileBuffer: typeof InternalFSBinding.readFileBuffer; readFileUtf8: typeof InternalFSBinding.readFileUtf8; readlink: typeof InternalFSBinding.readlink; realpath: typeof InternalFSBinding.realpath; @@ -295,6 +305,7 @@ export interface FsBinding { utimes: typeof InternalFSBinding.utimes; writeBuffer: typeof InternalFSBinding.writeBuffer; writeBuffers: typeof InternalFSBinding.writeBuffers; + writeFileBuffer: typeof InternalFSBinding.writeFileBuffer; writeFileUtf8: typeof InternalFSBinding.writeFileUtf8; writeString: typeof InternalFSBinding.writeString;