// This file contains the underlying implementation for sync & async functions // for interacting with the filesystem from JavaScript. // The top-level functions assume the arguments are already validated const std = @import("std"); const bun = @import("root").bun; const strings = bun.strings; const windows = bun.windows; const string = bun.string; const JSC = bun.JSC; const PathString = JSC.PathString; const Environment = bun.Environment; const C = bun.C; const system = std.posix.system; const Maybe = JSC.Maybe; const Encoding = JSC.Node.Encoding; const PosixToWinNormalizer = bun.path.PosixToWinNormalizer; const FileDescriptor = bun.FileDescriptor; const FDImpl = bun.FDImpl; const AbortSignal = JSC.AbortSignal; const Syscall = if (Environment.isWindows) bun.sys.sys_uv else bun.sys; const Constants = @import("./node_fs_constant.zig").Constants; const builtin = @import("builtin"); const posix = std.posix; const darwin = std.os.darwin; const linux = std.os.linux; const PathLike = JSC.Node.PathLike; const PathOrFileDescriptor = JSC.Node.PathOrFileDescriptor; const DirIterator = @import("./dir_iterator.zig"); const Path = @import("../../resolver/resolve_path.zig"); const FileSystem = @import("../../fs.zig").FileSystem; const ArgumentsSlice = JSC.Node.ArgumentsSlice; const TimeLike = JSC.Node.TimeLike; const Mode = bun.Mode; const uv = bun.windows.libuv; const E = C.E; const uid_t = JSC.Node.uid_t; const gid_t = JSC.Node.gid_t; const ReadPosition = i64; const StringOrBuffer = JSC.Node.StringOrBuffer; const NodeFSFunctionEnum = std.meta.DeclEnum(JSC.Node.NodeFS); const UvFsCallback = fn (*uv.fs_t) callconv(.C) void; pub const default_permission = if (Environment.isPosix) Syscall.S.IRUSR | Syscall.S.IWUSR | Syscall.S.IRGRP | Syscall.S.IWGRP | Syscall.S.IROTH | Syscall.S.IWOTH else // Windows does not have permissions 0; // All async FS functions are run in a thread pool, but some implementations may // decide to do something slightly different. For example, reading a file has // an extra stack buffer in the async case. pub const Flavor = enum { sync, @"async" }; const ArrayBuffer = JSC.MarkedArrayBuffer; const Buffer = JSC.Buffer; const FileSystemFlags = JSC.Node.FileSystemFlags; pub const Async = struct { pub const access = NewAsyncFSTask(Return.Access, Arguments.Access, NodeFS.access); pub const appendFile = NewAsyncFSTask(Return.AppendFile, Arguments.AppendFile, NodeFS.appendFile); pub const chmod = NewAsyncFSTask(Return.Chmod, Arguments.Chmod, NodeFS.chmod); pub const chown = NewAsyncFSTask(Return.Chown, Arguments.Chown, NodeFS.chown); pub const close = NewUVFSRequest(Return.Close, Arguments.Close, .close); pub const copyFile = NewAsyncFSTask(Return.CopyFile, Arguments.CopyFile, NodeFS.copyFile); pub const exists = NewAsyncFSTask(Return.Exists, Arguments.Exists, NodeFS.exists); pub const fchmod = NewAsyncFSTask(Return.Fchmod, Arguments.FChmod, NodeFS.fchmod); pub const fchown = NewAsyncFSTask(Return.Fchown, Arguments.Fchown, NodeFS.fchown); pub const fdatasync = NewAsyncFSTask(Return.Fdatasync, Arguments.FdataSync, NodeFS.fdatasync); pub const fstat = NewAsyncFSTask(Return.Fstat, Arguments.Fstat, NodeFS.fstat); pub const fsync = NewAsyncFSTask(Return.Fsync, Arguments.Fsync, NodeFS.fsync); pub const ftruncate = NewAsyncFSTask(Return.Ftruncate, Arguments.FTruncate, NodeFS.ftruncate); pub const futimes = NewAsyncFSTask(Return.Futimes, Arguments.Futimes, NodeFS.futimes); pub const lchmod = NewAsyncFSTask(Return.Lchmod, Arguments.LCHmod, NodeFS.lchmod); pub const lchown = NewAsyncFSTask(Return.Lchown, Arguments.LChown, NodeFS.lchown); pub const link = NewAsyncFSTask(Return.Link, Arguments.Link, NodeFS.link); pub const lstat = NewAsyncFSTask(Return.Stat, Arguments.Stat, NodeFS.lstat); pub const lutimes = NewAsyncFSTask(Return.Lutimes, Arguments.Lutimes, NodeFS.lutimes); pub const mkdir = NewAsyncFSTask(Return.Mkdir, Arguments.Mkdir, NodeFS.mkdir); pub const mkdtemp = NewAsyncFSTask(Return.Mkdtemp, Arguments.MkdirTemp, NodeFS.mkdtemp); pub const open = NewUVFSRequest(Return.Open, Arguments.Open, .open); pub const read = NewUVFSRequest(Return.Read, Arguments.Read, .read); pub const readdir = NewAsyncFSTask(Return.Readdir, Arguments.Readdir, NodeFS.readdir); pub const readFile = NewAsyncFSTask(Return.ReadFile, Arguments.ReadFile, NodeFS.readFile); pub const readlink = NewAsyncFSTask(Return.Readlink, Arguments.Readlink, NodeFS.readlink); pub const readv = NewUVFSRequest(Return.Readv, Arguments.Readv, .readv); pub const realpath = NewAsyncFSTask(Return.Realpath, Arguments.Realpath, NodeFS.realpath); pub const realpathNonNative = NewAsyncFSTask(Return.Realpath, Arguments.Realpath, NodeFS.realpathNonNative); pub const rename = NewAsyncFSTask(Return.Rename, Arguments.Rename, NodeFS.rename); pub const rm = NewAsyncFSTask(Return.Rm, Arguments.Rm, NodeFS.rm); pub const rmdir = NewAsyncFSTask(Return.Rmdir, Arguments.RmDir, NodeFS.rmdir); pub const stat = NewAsyncFSTask(Return.Stat, Arguments.Stat, NodeFS.stat); pub const symlink = NewAsyncFSTask(Return.Symlink, Arguments.Symlink, NodeFS.symlink); pub const truncate = NewAsyncFSTask(Return.Truncate, Arguments.Truncate, NodeFS.truncate); pub const unlink = NewAsyncFSTask(Return.Unlink, Arguments.Unlink, NodeFS.unlink); pub const utimes = NewAsyncFSTask(Return.Utimes, Arguments.Utimes, NodeFS.utimes); pub const write = NewUVFSRequest(Return.Write, Arguments.Write, .write); pub const writeFile = NewAsyncFSTask(Return.WriteFile, Arguments.WriteFile, NodeFS.writeFile); pub const writev = NewUVFSRequest(Return.Writev, Arguments.Writev, .writev); pub const statfs = NewUVFSRequest(Return.StatFS, Arguments.StatFS, .statfs); comptime { bun.assert(readFile.have_abort_signal); bun.assert(writeFile.have_abort_signal); } pub const cp = AsyncCpTask; pub const readdir_recursive = AsyncReaddirRecursiveTask; /// Used internally. Not from JavaScript. pub const AsyncMkdirp = struct { completion_ctx: *anyopaque, completion: *const fn (*anyopaque, JSC.Maybe(void)) void, /// Memory is not owned by this struct path: []const u8, task: JSC.WorkPoolTask = .{ .callback = &workPoolCallback }, pub usingnamespace bun.New(@This()); pub fn workPoolCallback(task: *JSC.WorkPoolTask) void { var this: *AsyncMkdirp = @fieldParentPtr("task", task); var node_fs = NodeFS{}; const result = node_fs.mkdirRecursive( Arguments.Mkdir{ .path = PathLike{ .string = PathString.init(this.path) }, .recursive = true, }, ); switch (result) { .err => |err| { this.completion(this.completion_ctx, .{ .err = err.withPath(bun.default_allocator.dupe(u8, err.path) catch bun.outOfMemory()) }); }, .result => { this.completion(this.completion_ctx, JSC.Maybe(void).success); }, } } pub fn schedule(this: *AsyncMkdirp) void { JSC.WorkPool.schedule(&this.task); } }; fn NewUVFSRequest(comptime ReturnType: type, comptime ArgumentType: type, comptime FunctionEnum: NodeFSFunctionEnum) type { if (!Environment.isWindows) { return NewAsyncFSTask(ReturnType, ArgumentType, @field(NodeFS, @tagName(FunctionEnum))); } switch (FunctionEnum) { .open, .close, .read, .write, .readv, .writev, .statfs, => {}, else => @compileError("UVFSRequest type not implemented"), } return struct { promise: JSC.JSPromise.Strong, args: ArgumentType, globalObject: *JSC.JSGlobalObject, req: uv.fs_t = std.mem.zeroes(uv.fs_t), result: JSC.Maybe(ReturnType), ref: bun.Async.KeepAlive = .{}, tracker: JSC.AsyncTaskTracker, pub const Task = @This(); pub const heap_label = "Async" ++ bun.meta.typeBaseName(@typeName(ArgumentType)) ++ "UvTask"; pub usingnamespace bun.New(@This()); pub fn create(globalObject: *JSC.JSGlobalObject, this: *JSC.Node.NodeJSFS, task_args: ArgumentType, vm: *JSC.VirtualMachine) JSC.JSValue { var task = Task.new(.{ .promise = JSC.JSPromise.Strong.init(globalObject), .args = task_args, .result = undefined, .globalObject = globalObject, .tracker = JSC.AsyncTaskTracker.init(vm), }); task.ref.ref(vm); task.args.toThreadSafe(); task.tracker.didSchedule(globalObject); const log = bun.sys.syslog; const loop = uv.Loop.get(); task.req.data = task; switch (comptime FunctionEnum) { .open => { const args: Arguments.Open = task.args; const path = if (bun.strings.eqlComptime(args.path.slice(), "/dev/null")) "\\\\.\\NUL" else args.path.sliceZ(&this.node_fs.sync_error_buf); var flags: c_int = @intFromEnum(args.flags); flags = uv.O.fromBunO(flags); var mode: c_int = args.mode; if (mode == 0) mode = 0o644; const rc = uv.uv_fs_open(loop, &task.req, path.ptr, flags, mode, &uv_callback); bun.debugAssert(rc == .zero); log("uv open({s}, {d}, {d}) = scheduled", .{ path, flags, mode }); }, .close => { const args: Arguments.Close = task.args; const fd = args.fd.impl().uv(); if (fd == 1 or fd == 2) { log("uv close({}) SKIPPED", .{fd}); task.result = Maybe(Return.Close).success; task.globalObject.bunVM().eventLoop().enqueueTask(JSC.Task.init(task)); return task.promise.value(); } const rc = uv.uv_fs_close(loop, &task.req, fd, &uv_callback); bun.debugAssert(rc == .zero); log("uv close({d}) = scheduled", .{fd}); }, .read => { const args: Arguments.Read = task.args; const B = uv.uv_buf_t.init; const fd = args.fd.impl().uv(); var buf = args.buffer.slice(); buf = buf[@min(buf.len, args.offset)..]; buf = buf[0..@min(buf.len, args.length)]; const rc = uv.uv_fs_read(loop, &task.req, fd, &.{B(buf)}, 1, args.position orelse -1, &uv_callback); bun.debugAssert(rc == .zero); log("uv read({d}) = scheduled", .{fd}); }, .write => { const args: Arguments.Write = task.args; const B = uv.uv_buf_t.init; const fd = args.fd.impl().uv(); var buf = args.buffer.slice(); buf = buf[@min(buf.len, args.offset)..]; buf = buf[0..@min(buf.len, args.length)]; const rc = uv.uv_fs_write(loop, &task.req, fd, &.{B(buf)}, 1, args.position orelse -1, &uv_callback); bun.debugAssert(rc == .zero); log("uv write({d}) = scheduled", .{fd}); }, .readv => { const args: Arguments.Readv = task.args; const fd = args.fd.impl().uv(); const bufs = args.buffers.buffers.items; const pos: i64 = args.position orelse -1; var sum: u64 = 0; for (bufs) |b| sum += b.slice().len; const rc = uv.uv_fs_read(loop, &task.req, fd, bufs.ptr, @intCast(bufs.len), pos, &uv_callback); bun.debugAssert(rc == .zero); log("uv readv({d}, {*}, {d}, {d}, {d} total bytes) = scheduled", .{ fd, bufs.ptr, bufs.len, pos, sum }); }, .writev => { const args_: Arguments.Writev = task.args; const fd = args_.fd.impl().uv(); const bufs = args_.buffers.buffers.items; if (bufs.len == 0) { task.result = Maybe(Return.Writev).success; task.globalObject.bunVM().eventLoop().enqueueTask(JSC.Task.init(task)); return task.promise.value(); } const pos: i64 = args_.position orelse -1; var sum: u64 = 0; for (bufs) |b| sum += b.slice().len; const rc = uv.uv_fs_write(loop, &task.req, fd, bufs.ptr, @intCast(bufs.len), pos, &uv_callback); bun.debugAssert(rc == .zero); log("uv writev({d}, {*}, {d}, {d}, {d} total bytes) = scheduled", .{ fd, bufs.ptr, bufs.len, pos, sum }); }, .statfs => { const args_: Arguments.StatFS = task.args; const path = args_.path.sliceZ(&this.node_fs.sync_error_buf); const rc = uv.uv_fs_statfs(loop, &task.req, path.ptr, &uv_callbackreq); bun.debugAssert(rc == .zero); log("uv statfs({s}) = ~~", .{path}); }, else => comptime unreachable, } return task.promise.value(); } fn uv_callback(req: *uv.fs_t) callconv(.C) void { defer uv.uv_fs_req_cleanup(req); const this: *Task = @ptrCast(@alignCast(req.data.?)); var node_fs = NodeFS{}; this.result = @field(NodeFS, "uv_" ++ @tagName(FunctionEnum))(&node_fs, this.args, @intFromEnum(req.result)); if (this.result == .err) { this.result.err.path = bun.default_allocator.dupe(u8, this.result.err.path) catch ""; std.mem.doNotOptimizeAway(&node_fs); } this.globalObject.bunVM().eventLoop().enqueueTask(JSC.Task.init(this)); } fn uv_callbackreq(req: *uv.fs_t) callconv(.C) void { defer uv.uv_fs_req_cleanup(req); const this: *Task = @ptrCast(@alignCast(req.data.?)); var node_fs = NodeFS{}; this.result = @field(NodeFS, "uv_" ++ @tagName(FunctionEnum))(&node_fs, this.args, req, @intFromEnum(req.result)); if (this.result == .err) { this.result.err.path = bun.default_allocator.dupe(u8, this.result.err.path) catch ""; std.mem.doNotOptimizeAway(&node_fs); } this.globalObject.bunVM().eventLoop().enqueueTask(JSC.Task.init(this)); } pub fn runFromJSThread(this: *Task) void { const globalObject = this.globalObject; var success = @as(JSC.Maybe(ReturnType).Tag, this.result) == .result; const result = switch (this.result) { .err => |err| err.toJSC(globalObject), .result => |*res| brk: { const out = globalObject.toJS(res, .temporary); success = out != .zero; break :brk out; }, }; var promise_value = this.promise.value(); var promise = this.promise.get(); promise_value.ensureStillAlive(); const tracker = this.tracker; tracker.willDispatch(globalObject); defer tracker.didDispatch(globalObject); this.deinit(); switch (success) { false => { promise.reject(globalObject, result); }, true => { promise.resolve(globalObject, result); }, } } pub fn deinit(this: *Task) void { if (this.result == .err) { bun.default_allocator.free(this.result.err.path); } this.ref.unref(this.globalObject.bunVM()); if (@hasDecl(ArgumentType, "deinitAndUnprotect")) { this.args.deinitAndUnprotect(); } else { this.args.deinit(); } this.promise.deinit(); this.destroy(); } }; } fn NewAsyncFSTask(comptime ReturnType: type, comptime ArgumentType: type, comptime function: anytype) type { return struct { pub const Task = @This(); promise: JSC.JSPromise.Strong, args: ArgumentType, globalObject: *JSC.JSGlobalObject, task: JSC.WorkPoolTask = .{ .callback = &workPoolCallback }, result: JSC.Maybe(ReturnType), ref: bun.Async.KeepAlive = .{}, tracker: JSC.AsyncTaskTracker, /// NewAsyncFSTask supports cancelable operations via AbortSignal, /// so long as a "signal" field exists. The task wrapper will ensure /// a promise rejection happens if signaled, but if `function` is /// already called, no guarantees are made. It is recommended for /// the functions to check .signal.aborted() for early returns. pub const have_abort_signal = @hasField(ArgumentType, "signal"); pub const heap_label = "Async" ++ bun.meta.typeBaseName(@typeName(ArgumentType)) ++ "Task"; pub fn create( globalObject: *JSC.JSGlobalObject, _: *JSC.Node.NodeJSFS, args: ArgumentType, vm: *JSC.VirtualMachine, ) JSC.JSValue { var task = bun.new(Task, .{ .promise = JSC.JSPromise.Strong.init(globalObject), .args = args, .result = undefined, .globalObject = globalObject, .tracker = JSC.AsyncTaskTracker.init(vm), }); task.ref.ref(vm); task.args.toThreadSafe(); task.tracker.didSchedule(globalObject); JSC.WorkPool.schedule(&task.task); return task.promise.value(); } fn workPoolCallback(task: *JSC.WorkPoolTask) void { var this: *Task = @alignCast(@fieldParentPtr("task", task)); var node_fs = NodeFS{}; this.result = function(&node_fs, this.args, .@"async"); if (this.result == .err) { this.result.err.path = bun.default_allocator.dupe(u8, this.result.err.path) catch ""; std.mem.doNotOptimizeAway(&node_fs); } this.globalObject.bunVMConcurrently().eventLoop().enqueueTaskConcurrent(JSC.ConcurrentTask.createFrom(this)); } pub fn runFromJSThread(this: *Task) void { const globalObject = this.globalObject; var success = @as(JSC.Maybe(ReturnType).Tag, this.result) == .result; const result = switch (this.result) { .err => |err| err.toJSC(globalObject), .result => |*res| brk: { const out = globalObject.toJS(res, .temporary); success = out != .zero; break :brk out; }, }; var promise_value = this.promise.value(); var promise = this.promise.get(); promise_value.ensureStillAlive(); const tracker = this.tracker; tracker.willDispatch(globalObject); defer tracker.didDispatch(globalObject); if (have_abort_signal) check_abort: { const signal = this.args.signal orelse break :check_abort; if (signal.reasonIfAborted(globalObject)) |reason| { this.deinit(); promise.reject(globalObject, reason.toJS(globalObject)); return; } } this.deinit(); switch (success) { false => { promise.reject(globalObject, result); }, true => { promise.resolve(globalObject, result); }, } } pub fn deinit(this: *Task) void { if (this.result == .err) { bun.default_allocator.free(this.result.err.path); } this.ref.unref(this.globalObject.bunVM()); if (@hasDecl(ArgumentType, "deinitAndUnprotect")) { this.args.deinitAndUnprotect(); } else { this.args.deinit(); } this.promise.deinit(); bun.destroy(this); } }; } }; pub const AsyncCpTask = NewAsyncCpTask(false); pub const ShellAsyncCpTask = NewAsyncCpTask(true); pub fn NewAsyncCpTask(comptime is_shell: bool) type { const ShellTask = bun.shell.Interpreter.Builtin.Cp.ShellCpTask; const ShellTaskT = if (is_shell) *ShellTask else u0; return struct { promise: JSC.JSPromise.Strong = .{}, args: Arguments.Cp, evtloop: JSC.EventLoopHandle, task: JSC.WorkPoolTask = .{ .callback = &workPoolCallback }, result: JSC.Maybe(Return.Cp), /// If this task is called by the shell then we shouldn't call this as /// it is not threadsafe and is unnecessary as the process will be kept /// alive by the shell instance ref: if (!is_shell) bun.Async.KeepAlive else struct {} = .{}, arena: bun.ArenaAllocator, tracker: JSC.AsyncTaskTracker, has_result: std.atomic.Value(bool), /// On each creation of a `AsyncCpSingleFileTask`, this is incremented. /// When each task is finished, decrement. /// The maintask thread starts this at 1 and decrements it at the end, to avoid the promise being resolved while new tasks may be added. subtask_count: std.atomic.Value(usize), deinitialized: bool = false, shelltask: ShellTaskT, const ThisAsyncCpTask = @This(); /// This task is used by `AsyncCpTask/fs.promises.cp` to copy a single file. /// When clonefile cannot be used, this task is started once per file. pub const SingleTask = struct { cp_task: *ThisAsyncCpTask, src: bun.OSPathSliceZ, dest: bun.OSPathSliceZ, task: JSC.WorkPoolTask = .{ .callback = &SingleTask.workPoolCallback }, const ThisSingleTask = @This(); pub fn create( parent: *ThisAsyncCpTask, src: bun.OSPathSliceZ, dest: bun.OSPathSliceZ, ) void { var task = bun.new(ThisSingleTask, .{ .cp_task = parent, .src = src, .dest = dest, }); JSC.WorkPool.schedule(&task.task); } fn workPoolCallback(task: *JSC.WorkPoolTask) void { var this: *ThisSingleTask = @fieldParentPtr("task", task); // TODO: error strings on node_fs will die var node_fs = NodeFS{}; const args = this.cp_task.args; const result = node_fs._copySingleFileSync( this.src, this.dest, @enumFromInt((if (args.flags.errorOnExist or !args.flags.force) Constants.COPYFILE_EXCL else @as(u8, 0))), null, this.cp_task.args, ); brk: { switch (result) { .err => |err| { if (err.errno == @intFromEnum(E.EXIST) and !args.flags.errorOnExist) { break :brk; } this.cp_task.finishConcurrently(result); this.deinit(); return; }, .result => { this.cp_task.onCopy(this.src, this.dest); }, } } const old_count = this.cp_task.subtask_count.fetchSub(1, .monotonic); if (old_count == 1) { this.cp_task.finishConcurrently(Maybe(Return.Cp).success); } this.deinit(); } pub fn deinit(this: *ThisSingleTask) void { // There is only one path buffer for both paths. 2 extra bytes are the nulls at the end of each bun.default_allocator.free(this.src.ptr[0 .. this.src.len + this.dest.len + 2]); bun.destroy(this); } }; pub fn onCopy(this: *ThisAsyncCpTask, src: anytype, dest: anytype) void { if (comptime !is_shell) return; const task = this.shelltask; task.cpOnCopy(src, dest); } pub fn onFinish(this: *ThisAsyncCpTask, result: Maybe(void)) void { if (comptime !is_shell) return; const task = this.shelltask; task.cpOnFinish(result); } pub fn create( globalObject: *JSC.JSGlobalObject, _: *JSC.Node.NodeJSFS, cp_args: Arguments.Cp, vm: *JSC.VirtualMachine, arena: bun.ArenaAllocator, ) JSC.JSValue { const task = createWithShellTask(globalObject, cp_args, vm, arena, 0, true); return task.promise.value(); } pub fn createWithShellTask( globalObject: *JSC.JSGlobalObject, cp_args: Arguments.Cp, vm: *JSC.VirtualMachine, arena: bun.ArenaAllocator, shelltask: ShellTaskT, comptime enable_promise: bool, ) *ThisAsyncCpTask { var task = bun.new( ThisAsyncCpTask, ThisAsyncCpTask{ .promise = if (comptime enable_promise) JSC.JSPromise.Strong.init(globalObject) else .{}, .args = cp_args, .has_result = .{ .raw = false }, .result = undefined, .evtloop = .{ .js = vm.event_loop }, .tracker = JSC.AsyncTaskTracker.init(vm), .arena = arena, .subtask_count = .{ .raw = 1 }, .shelltask = shelltask, }, ); if (comptime !is_shell) task.ref.ref(vm); task.args.src.toThreadSafe(); task.args.dest.toThreadSafe(); task.tracker.didSchedule(globalObject); JSC.WorkPool.schedule(&task.task); return task; } pub fn createMini( cp_args: Arguments.Cp, mini: *JSC.MiniEventLoop, arena: bun.ArenaAllocator, shelltask: *ShellTask, ) *ThisAsyncCpTask { var task = bun.new( ThisAsyncCpTask, ThisAsyncCpTask{ .args = cp_args, .has_result = .{ .raw = false }, .result = undefined, .evtloop = .{ .mini = mini }, .tracker = JSC.AsyncTaskTracker{ .id = 0 }, .arena = arena, .subtask_count = .{ .raw = 1 }, .shelltask = shelltask, }, ); if (comptime !is_shell) task.ref.ref(mini); task.args.src.toThreadSafe(); task.args.dest.toThreadSafe(); JSC.WorkPool.schedule(&task.task); return task; } fn workPoolCallback(task: *JSC.WorkPoolTask) void { const this: *ThisAsyncCpTask = @alignCast(@fieldParentPtr("task", task)); var node_fs = NodeFS{}; ThisAsyncCpTask.cpAsync(&node_fs, this); } /// May be called from any thread (the subtasks) fn finishConcurrently(this: *ThisAsyncCpTask, result: Maybe(Return.Cp)) void { if (this.has_result.cmpxchgStrong(false, true, .monotonic, .monotonic)) |_| { return; } this.result = result; if (this.result == .err) { this.result.err.path = bun.default_allocator.dupe(u8, this.result.err.path) catch ""; } if (this.evtloop == .js) { this.evtloop.enqueueTaskConcurrent(.{ .js = JSC.ConcurrentTask.fromCallback(this, runFromJSThread) }); } else { this.evtloop.enqueueTaskConcurrent(.{ .mini = JSC.AnyTaskWithExtraContext.fromCallbackAutoDeinit(this, "runFromJSThreadMini") }); } } pub fn runFromJSThreadMini(this: *ThisAsyncCpTask, _: *void) void { this.runFromJSThread(); } fn runFromJSThread(this: *ThisAsyncCpTask) void { if (comptime is_shell) { this.shelltask.cpOnFinish(this.result); this.deinit(); return; } const globalObject = this.evtloop.globalObject() orelse { @panic("No global object, this indicates a bug in Bun. Please file a GitHub issue."); }; var success = @as(JSC.Maybe(Return.Cp).Tag, this.result) == .result; const result = switch (this.result) { .err => |err| err.toJSC(globalObject), .result => |*res| brk: { const out = globalObject.toJS(res, .temporary); success = out != .zero; break :brk out; }, }; var promise_value = this.promise.value(); var promise = this.promise.get(); promise_value.ensureStillAlive(); const tracker = this.tracker; tracker.willDispatch(globalObject); defer tracker.didDispatch(globalObject); this.deinit(); switch (success) { false => { promise.reject(globalObject, result); }, true => { promise.resolve(globalObject, result); }, } } pub fn deinit(this: *ThisAsyncCpTask) void { bun.assert(!this.deinitialized); this.deinitialized = true; if (comptime !is_shell) this.ref.unref(this.evtloop); this.args.deinit(); this.promise.deinit(); this.arena.deinit(); bun.destroy(this); } /// Directory scanning + clonefile will block this thread, then each individual file copy (what the sync version /// calls "_copySingleFileSync") will be dispatched as a separate task. pub fn cpAsync( nodefs: *NodeFS, this: *ThisAsyncCpTask, ) void { const args = this.args; var src_buf: bun.OSPathBuffer = undefined; var dest_buf: bun.OSPathBuffer = undefined; const src = args.src.osPath(&src_buf); const dest = args.dest.osPath(&dest_buf); if (Environment.isWindows) { const attributes = windows.GetFileAttributesW(src); if (attributes == windows.INVALID_FILE_ATTRIBUTES) { this.finishConcurrently(.{ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOENT), .syscall = .copyfile, .path = nodefs.osPathIntoSyncErrorBuf(src), } }); return; } const file_or_symlink = (attributes & windows.FILE_ATTRIBUTE_DIRECTORY) == 0 or (attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT) != 0; if (file_or_symlink) { const r = nodefs._copySingleFileSync( src, dest, if (comptime is_shell) // Shell always forces copy @enumFromInt(Constants.Copyfile.force) else @enumFromInt((if (args.flags.errorOnExist or !args.flags.force) Constants.COPYFILE_EXCL else @as(u8, 0))), attributes, this.args, ); if (r == .err and r.err.errno == @intFromEnum(E.EXIST) and !args.flags.errorOnExist) { this.finishConcurrently(Maybe(Return.Cp).success); return; } this.onCopy(src, dest); this.finishConcurrently(r); return; } } else { const stat_ = switch (Syscall.lstat(src)) { .result => |result| result, .err => |err| { @memcpy(nodefs.sync_error_buf[0..src.len], src); this.finishConcurrently(.{ .err = err.withPath(nodefs.sync_error_buf[0..src.len]) }); return; }, }; if (!bun.S.ISDIR(stat_.mode)) { // This is the only file, there is no point in dispatching subtasks const r = nodefs._copySingleFileSync( src, dest, @enumFromInt((if (args.flags.errorOnExist or !args.flags.force) Constants.COPYFILE_EXCL else @as(u8, 0))), stat_, this.args, ); if (r == .err and r.err.errno == @intFromEnum(E.EXIST) and !args.flags.errorOnExist) { this.onCopy(src, dest); this.finishConcurrently(Maybe(Return.Cp).success); return; } this.onCopy(src, dest); this.finishConcurrently(r); return; } } if (!args.flags.recursive) { this.finishConcurrently(.{ .err = .{ .errno = @intFromEnum(E.ISDIR), .syscall = .copyfile, .path = nodefs.osPathIntoSyncErrorBuf(src), } }); return; } const success = ThisAsyncCpTask._cpAsyncDirectory(nodefs, args.flags, this, &src_buf, @intCast(src.len), &dest_buf, @intCast(dest.len)); const old_count = this.subtask_count.fetchSub(1, .monotonic); if (success and old_count == 1) { this.finishConcurrently(Maybe(Return.Cp).success); } } // returns boolean `should_continue` fn _cpAsyncDirectory( nodefs: *NodeFS, args: Arguments.Cp.Flags, this: *ThisAsyncCpTask, src_buf: *bun.OSPathBuffer, src_dir_len: PathString.PathInt, dest_buf: *bun.OSPathBuffer, dest_dir_len: PathString.PathInt, ) bool { const src = src_buf[0..src_dir_len :0]; const dest = dest_buf[0..dest_dir_len :0]; if (comptime Environment.isMac) { if (Maybe(Return.Cp).errnoSysP(C.clonefile(src, dest, 0), .clonefile, src)) |err| { switch (err.getErrno()) { .ACCES, .NAMETOOLONG, .ROFS, .PERM, .INVAL, => { @memcpy(nodefs.sync_error_buf[0..src.len], src); this.finishConcurrently(.{ .err = err.err.withPath(nodefs.sync_error_buf[0..src.len]) }); return false; }, // Other errors may be due to clonefile() not being supported // We'll fall back to other implementations else => {}, } } else { return true; } } const open_flags = bun.O.DIRECTORY | bun.O.RDONLY; const fd = switch (Syscall.openatOSPath(bun.FD.cwd(), src, open_flags, 0)) { .err => |err| { this.finishConcurrently(.{ .err = err.withPath(nodefs.osPathIntoSyncErrorBuf(src)) }); return false; }, .result => |fd_| fd_, }; defer _ = Syscall.close(fd); var buf: bun.OSPathBuffer = undefined; const normdest: bun.OSPathSliceZ = if (Environment.isWindows) switch (bun.sys.normalizePathWindows(u16, bun.invalid_fd, dest, &buf, .{ .add_nt_prefix = false })) { .err => |err| { this.finishConcurrently(.{ .err = err }); return false; }, .result => |normdest| normdest, } else dest; const mkdir_ = nodefs.mkdirRecursiveOSPath(normdest, Arguments.Mkdir.DefaultMode, false); switch (mkdir_) { .err => |err| { this.finishConcurrently(.{ .err = err }); return false; }, .result => { this.onCopy(src, normdest); }, } const dir = fd.asDir(); var iterator = DirIterator.iterate(dir, if (Environment.isWindows) .u16 else .u8); var entry = iterator.next(); while (switch (entry) { .err => |err| { // @memcpy(this.sync_error_buf[0..src.len], src); this.finishConcurrently(.{ .err = err.withPath(nodefs.osPathIntoSyncErrorBuf(src)) }); return false; }, .result => |ent| ent, }) |current| : (entry = iterator.next()) { switch (current.kind) { .directory => { const cname = current.name.slice(); @memcpy(src_buf[src_dir_len + 1 .. src_dir_len + 1 + cname.len], cname); src_buf[src_dir_len] = std.fs.path.sep; src_buf[src_dir_len + 1 + cname.len] = 0; @memcpy(dest_buf[dest_dir_len + 1 .. dest_dir_len + 1 + cname.len], cname); dest_buf[dest_dir_len] = std.fs.path.sep; dest_buf[dest_dir_len + 1 + cname.len] = 0; const should_continue = ThisAsyncCpTask._cpAsyncDirectory( nodefs, args, this, src_buf, @truncate(src_dir_len + 1 + cname.len), dest_buf, @truncate(dest_dir_len + 1 + cname.len), ); if (!should_continue) return false; }, else => { _ = this.subtask_count.fetchAdd(1, .monotonic); const cname = current.name.slice(); // Allocate a path buffer for the path data var path_buf = bun.default_allocator.alloc( bun.OSPathChar, src_dir_len + 1 + cname.len + 1 + dest_dir_len + 1 + cname.len + 1, ) catch bun.outOfMemory(); @memcpy(path_buf[0..src_dir_len], src_buf[0..src_dir_len]); path_buf[src_dir_len] = std.fs.path.sep; @memcpy(path_buf[src_dir_len + 1 .. src_dir_len + 1 + cname.len], cname); path_buf[src_dir_len + 1 + cname.len] = 0; @memcpy(path_buf[src_dir_len + 1 + cname.len + 1 .. src_dir_len + 1 + cname.len + 1 + dest_dir_len], dest_buf[0..dest_dir_len]); path_buf[src_dir_len + 1 + cname.len + 1 + dest_dir_len] = std.fs.path.sep; @memcpy(path_buf[src_dir_len + 1 + cname.len + 1 + dest_dir_len + 1 .. src_dir_len + 1 + cname.len + 1 + dest_dir_len + 1 + cname.len], cname); path_buf[src_dir_len + 1 + cname.len + 1 + dest_dir_len + 1 + cname.len] = 0; SingleTask.create( this, path_buf[0 .. src_dir_len + 1 + cname.len :0], path_buf[src_dir_len + 1 + cname.len + 1 .. src_dir_len + 1 + cname.len + 1 + dest_dir_len + 1 + cname.len :0], ); }, } } return true; } }; } pub const AsyncReaddirRecursiveTask = struct { promise: JSC.JSPromise.Strong, args: Arguments.Readdir, globalObject: *JSC.JSGlobalObject, task: JSC.WorkPoolTask = .{ .callback = &workPoolCallback }, ref: bun.Async.KeepAlive = .{}, tracker: JSC.AsyncTaskTracker, // It's not 100% clear this one is necessary has_result: std.atomic.Value(bool), subtask_count: std.atomic.Value(usize), /// The final result list result_list: ResultListEntry.Value = undefined, /// When joining the result list, we use this to preallocate the joined array. result_list_count: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), /// A lockless queue of result lists. /// /// Using a lockless queue instead of mutex + joining the lists as we go was a meaningful performance improvement result_list_queue: ResultListEntry.Queue = ResultListEntry.Queue{}, /// All the subtasks will use this fd to open files root_fd: FileDescriptor = bun.invalid_fd, /// This isued when joining the file paths for error messages root_path: PathString = PathString.empty, pending_err: ?Syscall.Error = null, pending_err_mutex: bun.Mutex = .{}, pub usingnamespace bun.New(@This()); pub const ResultListEntry = struct { pub const Value = union(Return.Readdir.Tag) { with_file_types: std.ArrayList(bun.JSC.Node.Dirent), buffers: std.ArrayList(Buffer), files: std.ArrayList(bun.String), pub fn deinit(this: *@This()) void { switch (this.*) { .with_file_types => |*res| { for (res.items) |item| { item.deref(); } res.clearAndFree(); }, .buffers => |*res| { for (res.items) |item| { bun.default_allocator.free(item.buffer.byteSlice()); } res.clearAndFree(); }, .files => |*res| { for (res.items) |item| { item.deref(); } res.clearAndFree(); }, } } }; next: ?*ResultListEntry = null, value: Value, pub const Queue = bun.UnboundedQueue(ResultListEntry, .next); }; pub const Subtask = struct { readdir_task: *AsyncReaddirRecursiveTask, basename: bun.PathString = bun.PathString.empty, task: JSC.WorkPoolTask = .{ .callback = call }, pub usingnamespace bun.New(@This()); pub fn call(task: *JSC.WorkPoolTask) void { var this: *Subtask = @alignCast(@fieldParentPtr("task", task)); defer { bun.default_allocator.free(this.basename.sliceAssumeZ()); this.destroy(); } var buf: bun.PathBuffer = undefined; this.readdir_task.performWork(this.basename.sliceAssumeZ(), &buf, false); } }; pub fn enqueue( readdir_task: *AsyncReaddirRecursiveTask, basename: [:0]const u8, ) void { var task = Subtask.new( .{ .readdir_task = readdir_task, .basename = bun.PathString.init(bun.default_allocator.dupeZ(u8, basename) catch bun.outOfMemory()), }, ); bun.assert(readdir_task.subtask_count.fetchAdd(1, .monotonic) > 0); JSC.WorkPool.schedule(&task.task); } pub fn create( globalObject: *JSC.JSGlobalObject, args: Arguments.Readdir, vm: *JSC.VirtualMachine, ) JSC.JSValue { var task = AsyncReaddirRecursiveTask.new(.{ .promise = JSC.JSPromise.Strong.init(globalObject), .args = args, .has_result = .{ .raw = false }, .globalObject = globalObject, .tracker = JSC.AsyncTaskTracker.init(vm), .subtask_count = .{ .raw = 1 }, .root_path = PathString.init(bun.default_allocator.dupeZ(u8, args.path.slice()) catch bun.outOfMemory()), .result_list = switch (args.tag()) { .files => .{ .files = std.ArrayList(bun.String).init(bun.default_allocator) }, .with_file_types => .{ .with_file_types = .init(bun.default_allocator) }, .buffers => .{ .buffers = std.ArrayList(Buffer).init(bun.default_allocator) }, }, }); task.ref.ref(vm); task.args.toThreadSafe(); task.tracker.didSchedule(globalObject); JSC.WorkPool.schedule(&task.task); return task.promise.value(); } pub fn performWork(this: *AsyncReaddirRecursiveTask, basename: [:0]const u8, buf: *bun.PathBuffer, comptime is_root: bool) void { switch (this.args.tag()) { inline else => |tag| { const ResultType = comptime switch (tag) { .files => bun.String, .with_file_types => bun.JSC.Node.Dirent, .buffers => Buffer, }; var stack = std.heap.stackFallback(8192, bun.default_allocator); // This is a stack-local copy to avoid resizing heap-allocated arrays in the common case of a small directory var entries = std.ArrayList(ResultType).init(stack.get()); defer entries.deinit(); switch (NodeFS.readdirWithEntriesRecursiveAsync( buf, this.args, this, basename, ResultType, &entries, is_root, )) { .err => |err| { for (entries.items) |*item| { switch (ResultType) { bun.String => item.deref(), bun.JSC.Node.Dirent => item.deref(), Buffer => bun.default_allocator.free(item.buffer.byteSlice()), else => @compileError("unreachable"), } } { this.pending_err_mutex.lock(); defer this.pending_err_mutex.unlock(); if (this.pending_err == null) { const err_path = if (err.path.len > 0) err.path else this.args.path.slice(); this.pending_err = err.withPath(bun.default_allocator.dupe(u8, err_path) catch ""); } } if (this.subtask_count.fetchSub(1, .monotonic) == 1) { this.finishConcurrently(); } }, .result => { this.writeResults(ResultType, &entries); }, } }, } } fn workPoolCallback(task: *JSC.WorkPoolTask) void { var this: *AsyncReaddirRecursiveTask = @alignCast(@fieldParentPtr("task", task)); var buf: bun.PathBuffer = undefined; this.performWork(this.root_path.sliceAssumeZ(), &buf, true); } pub fn writeResults(this: *AsyncReaddirRecursiveTask, comptime ResultType: type, result: *std.ArrayList(ResultType)) void { if (result.items.len > 0) { const Field = switch (ResultType) { bun.String => .files, bun.JSC.Node.Dirent => .with_file_types, Buffer => .buffers, else => @compileError("unreachable"), }; const list = bun.default_allocator.create(ResultListEntry) catch bun.outOfMemory(); errdefer { bun.default_allocator.destroy(list); } var clone = std.ArrayList(ResultType).initCapacity(bun.default_allocator, result.items.len) catch bun.outOfMemory(); clone.appendSliceAssumeCapacity(result.items); _ = this.result_list_count.fetchAdd(clone.items.len, .monotonic); list.* = ResultListEntry{ .next = null, .value = @unionInit(ResultListEntry.Value, @tagName(Field), clone) }; this.result_list_queue.push(list); } if (this.subtask_count.fetchSub(1, .monotonic) == 1) { this.finishConcurrently(); } } /// May be called from any thread (the subtasks) pub fn finishConcurrently(this: *AsyncReaddirRecursiveTask) void { if (this.has_result.cmpxchgStrong(false, true, .monotonic, .monotonic)) |_| { return; } bun.assert(this.subtask_count.load(.monotonic) == 0); const root_fd = this.root_fd; if (root_fd != bun.invalid_fd) { this.root_fd = bun.invalid_fd; _ = Syscall.close(root_fd); bun.default_allocator.free(this.root_path.slice()); this.root_path = PathString.empty; } if (this.pending_err != null) { this.clearResultList(); } { var list = this.result_list_queue.popBatch(); var iter = list.iterator(); // we have to free only the previous one because the next value will // be read by the iterator. var to_destroy: ?*ResultListEntry = null; switch (this.args.tag()) { inline else => |tag| { var results = &@field(this.result_list, @tagName(tag)); results.ensureTotalCapacityPrecise(this.result_list_count.swap(0, .monotonic)) catch bun.outOfMemory(); while (iter.next()) |val| { if (to_destroy) |dest| { bun.default_allocator.destroy(dest); } to_destroy = val; var to_copy = &@field(val.value, @tagName(tag)); results.appendSliceAssumeCapacity(to_copy.items); to_copy.clearAndFree(); } if (to_destroy) |dest| { bun.default_allocator.destroy(dest); } }, } } this.globalObject.bunVMConcurrently().enqueueTaskConcurrent(JSC.ConcurrentTask.create(JSC.Task.init(this))); } fn clearResultList(this: *AsyncReaddirRecursiveTask) void { this.result_list.deinit(); var batch = this.result_list_queue.popBatch(); var iter = batch.iterator(); var to_destroy: ?*ResultListEntry = null; while (iter.next()) |val| { val.value.deinit(); if (to_destroy) |dest| { bun.default_allocator.destroy(dest); } to_destroy = val; } if (to_destroy) |dest| { bun.default_allocator.destroy(dest); } this.result_list_count.store(0, .monotonic); } pub fn runFromJSThread(this: *AsyncReaddirRecursiveTask) void { const globalObject = this.globalObject; var success = this.pending_err == null; const result = if (this.pending_err) |*err| err.toJSC(globalObject) else brk: { const res = switch (this.result_list) { .with_file_types => |*res| Return.Readdir{ .with_file_types = res.moveToUnmanaged().items }, .buffers => |*res| Return.Readdir{ .buffers = res.moveToUnmanaged().items }, .files => |*res| Return.Readdir{ .files = res.moveToUnmanaged().items }, }; const out = res.toJS(globalObject); if (out == .zero) { success = false; } break :brk out; }; var promise_value = this.promise.value(); var promise = this.promise.get(); promise_value.ensureStillAlive(); const tracker = this.tracker; tracker.willDispatch(globalObject); defer tracker.didDispatch(globalObject); this.deinit(); switch (success) { false => { promise.reject(globalObject, result); }, true => { promise.resolve(globalObject, result); }, } } pub fn deinit(this: *AsyncReaddirRecursiveTask) void { bun.assert(this.root_fd == bun.invalid_fd); // should already have closed it if (this.pending_err) |*err| { bun.default_allocator.free(err.path); } this.ref.unref(this.globalObject.bunVM()); this.args.deinit(); bun.default_allocator.free(this.root_path.slice()); this.clearResultList(); this.promise.deinit(); this.destroy(); } }; // TODO: to improve performance for all of these // The tagged unions for each type should become regular unions // and the tags should be passed in as comptime arguments to the functions performing the syscalls // This would reduce stack size, at the cost of instruction cache misses pub const Arguments = struct { pub const Rename = struct { old_path: PathLike, new_path: PathLike, pub fn deinit(this: @This()) void { this.old_path.deinit(); this.new_path.deinit(); } pub fn deinitAndUnprotect(this: @This()) void { this.old_path.deinitAndUnprotect(); this.new_path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *@This()) void { this.old_path.toThreadSafe(); this.new_path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Rename { const old_path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArgumentTypeValue("oldPath", "string or an instance of Buffer or URL", arguments.next() orelse .undefined); }; const new_path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArgumentTypeValue("newPath", "string or an instance of Buffer or URL", arguments.next() orelse .undefined); }; return Rename{ .old_path = old_path, .new_path = new_path }; } }; pub const Truncate = struct { /// Passing a file descriptor is deprecated and may result in an error being thrown in the future. path: PathOrFileDescriptor, len: u63 = 0, flags: i32 = 0, pub fn deinit(this: @This()) void { this.path.deinit(); } pub fn deinitAndUnprotect(this: *@This()) void { this.path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *@This()) void { this.path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Truncate { const path = try PathOrFileDescriptor.fromJS(ctx, arguments, bun.default_allocator) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; const len: u63 = brk: { const len_value = arguments.next() orelse break :brk 0; break :brk @max(0, try JSC.Node.validators.validateInteger(ctx, len_value, "len", null, null)); }; return .{ .path = path, .len = len }; } }; pub const Writev = struct { fd: FileDescriptor, buffers: JSC.Node.VectorArrayBuffer, position: ?u52 = 0, pub fn deinit(_: *const @This()) void {} pub fn deinitAndUnprotect(this: *const @This()) void { this.buffers.value.unprotect(); this.buffers.buffers.deinit(); } pub fn toThreadSafe(this: *@This()) void { this.buffers.value.protect(); const clone = bun.default_allocator.dupe(bun.PlatformIOVec, this.buffers.buffers.items) catch bun.outOfMemory(); this.buffers.buffers.deinit(); this.buffers.buffers.items = clone; this.buffers.buffers.capacity = clone.len; this.buffers.buffers.allocator = bun.default_allocator; } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Writev { const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { return throwInvalidFdError(ctx, fd_value); }; const buffers = try JSC.Node.VectorArrayBuffer.fromJS( ctx, arguments.protectEatNext() orelse { return ctx.throwInvalidArguments("Expected an ArrayBufferView[]", .{}); }, arguments.arena.allocator(), ); var position: ?u52 = null; if (arguments.nextEat()) |pos_value| { if (!pos_value.isUndefinedOrNull()) { if (pos_value.isNumber()) { position = pos_value.to(u52); } else { return ctx.throwInvalidArguments("position must be a number", .{}); } } } return Writev{ .fd = fd, .buffers = buffers, .position = position }; } }; pub const Readv = struct { fd: FileDescriptor, buffers: JSC.Node.VectorArrayBuffer, position: ?u52 = 0, pub fn deinit(this: *const @This()) void { _ = this; } pub fn deinitAndUnprotect(this: *const @This()) void { this.buffers.value.unprotect(); this.buffers.buffers.deinit(); } pub fn toThreadSafe(this: *@This()) void { this.buffers.value.protect(); const clone = bun.default_allocator.dupe(bun.PlatformIOVec, this.buffers.buffers.items) catch bun.outOfMemory(); this.buffers.buffers.deinit(); this.buffers.buffers.items = clone; this.buffers.buffers.capacity = clone.len; this.buffers.buffers.allocator = bun.default_allocator; } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Readv { const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { return throwInvalidFdError(ctx, fd_value); }; const buffers = try JSC.Node.VectorArrayBuffer.fromJS( ctx, arguments.protectEatNext() orelse { return ctx.throwInvalidArguments("Expected an ArrayBufferView[]", .{}); }, arguments.arena.allocator(), ); var position: ?u52 = null; if (arguments.nextEat()) |pos_value| { if (!pos_value.isUndefinedOrNull()) { if (pos_value.isNumber()) { position = pos_value.to(u52); } else { return ctx.throwInvalidArguments("position must be a number", .{}); } } } return Readv{ .fd = fd, .buffers = buffers, .position = position }; } }; pub const FTruncate = struct { fd: FileDescriptor, len: ?JSC.WebCore.Blob.SizeType = null, pub fn deinit(this: @This()) void { _ = this; } pub fn deinitAndUnprotect(this: *@This()) void { _ = this; } pub fn toThreadSafe(this: *const @This()) void { _ = this; } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!FTruncate { const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { return throwInvalidFdError(ctx, fd_value); }; const len: JSC.WebCore.Blob.SizeType = @intCast(@max(try JSC.Node.validators.validateInteger( ctx, arguments.next() orelse JSC.JSValue.jsNumber(0), "len", std.math.minInt(i52), std.math.maxInt(JSC.WebCore.Blob.SizeType), ), 0)); return FTruncate{ .fd = fd, .len = len }; } }; pub const Chown = struct { path: PathLike, uid: uid_t = 0, gid: gid_t = 0, pub fn deinit(this: @This()) void { this.path.deinit(); } pub fn deinitAndUnprotect(this: *@This()) void { this.path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *@This()) void { this.path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Chown { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); const uid: uid_t = brk: { const uid_value = arguments.next() orelse break :brk { return ctx.throwInvalidArguments("uid is required", .{}); }; arguments.eat(); break :brk wrapTo(uid_t, try JSC.Node.validators.validateInteger(ctx, uid_value, "uid", -1, std.math.maxInt(u32))); }; const gid: gid_t = brk: { const gid_value = arguments.next() orelse break :brk { return ctx.throwInvalidArguments("gid is required", .{}); }; arguments.eat(); break :brk wrapTo(gid_t, try JSC.Node.validators.validateInteger(ctx, gid_value, "gid", -1, std.math.maxInt(u32))); }; return Chown{ .path = path, .uid = uid, .gid = gid }; } }; pub const Fchown = struct { fd: FileDescriptor, uid: uid_t, gid: gid_t, pub fn deinit(_: @This()) void {} pub fn toThreadSafe(_: *const @This()) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Fchown { const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { return throwInvalidFdError(ctx, fd_value); }; const uid: uid_t = brk: { const uid_value = arguments.next() orelse break :brk { return ctx.throwInvalidArguments("uid is required", .{}); }; arguments.eat(); break :brk wrapTo(uid_t, try JSC.Node.validators.validateInteger(ctx, uid_value, "uid", -1, std.math.maxInt(u32))); }; const gid: gid_t = brk: { const gid_value = arguments.next() orelse break :brk { return ctx.throwInvalidArguments("gid is required", .{}); }; arguments.eat(); break :brk wrapTo(gid_t, try JSC.Node.validators.validateInteger(ctx, gid_value, "gid", -1, std.math.maxInt(u32))); }; return Fchown{ .fd = fd, .uid = uid, .gid = gid }; } }; fn wrapTo(T: type, in: i64) T { comptime bun.assert(@typeInfo(T).int.signedness == .unsigned); return @intCast(@mod(in, std.math.maxInt(T))); } pub const LChown = Chown; pub const Lutimes = struct { path: PathLike, atime: TimeLike, mtime: TimeLike, pub fn deinit(this: @This()) void { this.path.deinit(); } pub fn deinitAndUnprotect(this: *@This()) void { this.path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *@This()) void { this.path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Lutimes { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); const atime = JSC.Node.timeLikeFromJS(ctx, arguments.next() orelse { return ctx.throwInvalidArguments("atime is required", .{}); }) orelse { return ctx.throwInvalidArguments("atime must be a number or a Date", .{}); }; arguments.eat(); const mtime = JSC.Node.timeLikeFromJS(ctx, arguments.next() orelse { return ctx.throwInvalidArguments("mtime is required", .{}); }) orelse { return ctx.throwInvalidArguments("mtime must be a number or a Date", .{}); }; arguments.eat(); return .{ .path = path, .atime = atime, .mtime = mtime }; } }; pub const Chmod = struct { path: PathLike, mode: Mode = 0x777, pub fn deinit(this: @This()) void { this.path.deinit(); } pub fn toThreadSafe(this: *@This()) void { this.path.toThreadSafe(); } pub fn deinitAndUnprotect(this: *@This()) void { this.path.deinitAndUnprotect(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Chmod { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); const mode_arg = arguments.next() orelse .undefined; const mode: Mode = try JSC.Node.modeFromJS(ctx, mode_arg) orelse { return JSC.Node.validators.throwErrInvalidArgType(ctx, "mode", .{}, "number", mode_arg); }; arguments.eat(); return Chmod{ .path = path, .mode = mode }; } }; pub const FChmod = struct { fd: FileDescriptor, mode: Mode = 0x777, pub fn deinit(_: *const @This()) void {} pub fn toThreadSafe(_: *const @This()) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!FChmod { const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { return throwInvalidFdError(ctx, fd_value); }; const mode_arg = arguments.next() orelse .undefined; const mode: Mode = try JSC.Node.modeFromJS(ctx, mode_arg) orelse { return JSC.Node.validators.throwErrInvalidArgType(ctx, "mode", .{}, "number", mode_arg); }; arguments.eat(); return FChmod{ .fd = fd, .mode = mode }; } }; pub const LCHmod = Chmod; pub const StatFS = struct { path: PathLike, big_int: bool = false, pub fn deinit(this: Arguments.StatFS) void { this.path.deinit(); } pub fn deinitAndUnprotect(this: *Arguments.StatFS) void { this.path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *Arguments.StatFS) void { this.path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Arguments.StatFS { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); const big_int = brk: { if (arguments.next()) |next_val| { if (next_val.isObject()) { if (next_val.isCallable(ctx.vm())) break :brk false; arguments.eat(); if (try next_val.getBooleanStrict(ctx, "bigint")) |big_int| { break :brk big_int; } } } break :brk false; }; return .{ .path = path, .big_int = big_int }; } }; pub const Stat = struct { path: PathLike, big_int: bool = false, throw_if_no_entry: bool = true, pub fn deinit(this: Stat) void { this.path.deinit(); } pub fn deinitAndUnprotect(this: Stat) void { this.path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *Stat) void { this.path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Stat { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); var throw_if_no_entry = true; const big_int = brk: { if (arguments.next()) |next_val| { if (next_val.isObject()) { if (next_val.isCallable(ctx.vm())) break :brk false; arguments.eat(); if (try next_val.getBooleanStrict(ctx, "throwIfNoEntry")) |throw_if_no_entry_val| { throw_if_no_entry = throw_if_no_entry_val; } if (try next_val.getBooleanStrict(ctx, "bigint")) |big_int| { break :brk big_int; } } } break :brk false; }; return Stat{ .path = path, .big_int = big_int, .throw_if_no_entry = throw_if_no_entry }; } }; pub const Fstat = struct { fd: FileDescriptor, big_int: bool = false, pub fn deinit(_: @This()) void {} pub fn toThreadSafe(_: *@This()) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Fstat { const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { return throwInvalidFdError(ctx, fd_value); }; const big_int = brk: { if (arguments.next()) |next_val| { if (next_val.isObject()) { if (next_val.isCallable(ctx.vm())) break :brk false; arguments.eat(); if (try next_val.getBooleanStrict(ctx, "bigint")) |big_int| { break :brk big_int; } } } break :brk false; }; return Fstat{ .fd = fd, .big_int = big_int }; } }; pub const Lstat = Stat; pub const Link = struct { old_path: PathLike, new_path: PathLike, pub fn deinit(this: Link) void { this.old_path.deinit(); this.new_path.deinit(); } pub fn deinitAndUnprotect(this: *Link) void { this.old_path.deinitAndUnprotect(); this.new_path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *Link) void { this.old_path.toThreadSafe(); this.new_path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Link { const old_path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("oldPath must be a string or TypedArray", .{}); }; const new_path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("newPath must be a string or TypedArray", .{}); }; return Link{ .old_path = old_path, .new_path = new_path }; } }; pub const Symlink = struct { /// Where the symbolic link is targetting. target_path: PathLike, /// The path to create the symbolic link at. new_path: PathLike, /// Windows has multiple link types. By default, only junctions can be created by non-admin. link_type: if (Environment.isWindows) LinkType else void, const LinkType = enum { unspecified, file, dir, junction, }; pub fn deinit(this: Symlink) void { this.target_path.deinit(); this.new_path.deinit(); } pub fn deinitAndUnprotect(this: Symlink) void { this.target_path.deinitAndUnprotect(); this.new_path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *@This()) void { this.target_path.toThreadSafe(); this.new_path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Symlink { const old_path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("target must be a string or TypedArray", .{}); }; const new_path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; // The type argument is only available on Windows and // ignored on other platforms. It can be set to 'dir', // 'file', or 'junction'. If the type argument is not set, // Node.js will autodetect target type and use 'file' or // 'dir'. If the target does not exist, 'file' will be used. // Windows junction points require the destination path to // be absolute. When using 'junction', the target argument // will automatically be normalized to absolute path. const link_type: LinkType = link_type: { if (arguments.next()) |next_val| { if (next_val.isUndefinedOrNull()) { break :link_type .unspecified; } if (next_val.isString()) { arguments.eat(); var str = next_val.toBunString(ctx); defer str.deref(); if (str.eqlComptime("dir")) break :link_type .dir; if (str.eqlComptime("file")) break :link_type .file; if (str.eqlComptime("junction")) break :link_type .junction; return ctx.ERR_INVALID_ARG_VALUE("Symlink type must be one of \"dir\", \"file\", or \"junction\". Received \"{}\"", .{str}).throw(); } // not a string. fallthrough to auto detect. return ctx.ERR_INVALID_ARG_VALUE("Symlink type must be one of \"dir\", \"file\", or \"junction\".", .{}).throw(); } break :link_type .unspecified; }; return Symlink{ .target_path = old_path, .new_path = new_path, .link_type = if (Environment.isWindows) link_type, }; } }; pub const Readlink = struct { path: PathLike, encoding: Encoding = Encoding.utf8, pub fn deinit(this: Readlink) void { this.path.deinit(); } pub fn deinitAndUnprotect(this: *Readlink) void { this.path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *Readlink) void { this.path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Readlink { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); var encoding = Encoding.utf8; if (arguments.next()) |val| { arguments.eat(); switch (val.jsType()) { JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject => { encoding = try Encoding.assert(val, ctx, encoding); }, else => { if (val.isObject()) { encoding = try getEncoding(val, ctx, encoding); } }, } } return Readlink{ .path = path, .encoding = encoding }; } }; pub const Realpath = struct { path: PathLike, encoding: Encoding = .utf8, pub fn deinit(this: Realpath) void { this.path.deinit(); } pub fn deinitAndUnprotect(this: *Realpath) void { this.path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *Realpath) void { this.path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Realpath { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); var encoding = Encoding.utf8; if (arguments.next()) |val| { arguments.eat(); switch (val.jsType()) { JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject, => { encoding = try Encoding.assert(val, ctx, encoding); }, else => { if (val.isObject()) { encoding = try getEncoding(val, ctx, encoding); } }, } } return Realpath{ .path = path, .encoding = encoding }; } }; fn getEncoding(object: JSC.JSValue, globalObject: *JSC.JSGlobalObject, default: Encoding) bun.JSError!Encoding { if (object.fastGet(globalObject, .encoding)) |value| { return Encoding.assert(value, globalObject, default); } return default; } pub const Unlink = struct { path: PathLike, pub fn deinit(this: Unlink) void { this.path.deinit(); } pub fn deinitAndUnprotect(this: *Unlink) void { this.path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *Unlink) void { this.path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Unlink { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); return Unlink{ .path = path, }; } }; pub const Rm = RmDir; pub const RmDir = struct { path: PathLike, force: bool = false, max_retries: u32 = 0, recursive: bool = false, retry_delay: c_uint = 100, pub fn deinitAndUnprotect(this: *RmDir) void { this.path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *RmDir) void { this.path.toThreadSafe(); } pub fn deinit(this: RmDir) void { this.path.deinit(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!RmDir { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); var recursive = false; var force = false; var max_retries: u32 = 0; var retry_delay: c_uint = 100; if (arguments.next()) |val| { arguments.eat(); if (val.isObject()) { if (try val.get(ctx, "recursive")) |boolean| { if (boolean.isBoolean()) { recursive = boolean.toBoolean(); } else { return ctx.throwInvalidArguments("The \"options.recursive\" property must be of type boolean.", .{}); } } if (try val.get(ctx, "force")) |boolean| { if (boolean.isBoolean()) { force = boolean.toBoolean(); } else { return ctx.throwInvalidArguments("The \"options.force\" property must be of type boolean.", .{}); } } if (try val.get(ctx, "retryDelay")) |delay| { retry_delay = @intCast(try JSC.Node.validators.validateInteger(ctx, delay, "options.retryDelay", 0, std.math.maxInt(c_uint))); } if (try val.get(ctx, "maxRetries")) |retries| { max_retries = @intCast(try JSC.Node.validators.validateInteger(ctx, retries, "options.maxRetries", 0, std.math.maxInt(u32))); } } else if (val != .undefined) { return ctx.throwInvalidArguments("The \"options\" argument must be of type object.", .{}); } } return .{ .path = path, .recursive = recursive, .force = force, .max_retries = max_retries, .retry_delay = retry_delay, }; } }; /// https://github.com/nodejs/node/blob/master/lib/fs.js#L1285 pub const Mkdir = struct { path: PathLike, /// Indicates whether parent folders should be created. /// If a folder was created, the path to the first created folder will be returned. /// @default false recursive: bool = false, /// A file mode. If a string is passed, it is parsed as an octal integer. If not specified mode: Mode = DefaultMode, /// If set to true, the return value is never set to a string always_return_none: bool = false, pub const DefaultMode = 0o777; pub fn deinit(this: Mkdir) void { this.path.deinit(); } pub fn deinitAndUnprotect(this: *Mkdir) void { this.path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *Mkdir) void { this.path.toThreadSafe(); } pub fn fromJS(ctx: *JSC.JSGlobalObject, arguments: *ArgumentsSlice) bun.JSError!Mkdir { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); var recursive = false; var mode: Mode = 0o777; if (arguments.next()) |val| { arguments.eat(); if (val.isObject()) { if (try val.getBooleanStrict(ctx, "recursive")) |boolean| { recursive = boolean; } if (try val.get(ctx, "mode")) |mode_| { mode = try JSC.Node.modeFromJS(ctx, mode_) orelse mode; } } if (val.isNumber() or val.isString()) { mode = try JSC.Node.modeFromJS(ctx, val) orelse mode; } } return Mkdir{ .path = path, .recursive = recursive, .mode = mode, }; } }; const MkdirTemp = struct { prefix: PathLike = .{ .buffer = .{ .buffer = JSC.ArrayBuffer.empty } }, encoding: Encoding = .utf8, pub fn deinit(this: MkdirTemp) void { this.prefix.deinit(); } pub fn deinitAndUnprotect(this: *MkdirTemp) void { this.prefix.deinitAndUnprotect(); } pub fn toThreadSafe(this: *MkdirTemp) void { this.prefix.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!MkdirTemp { const prefix = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArgumentTypeValue("prefix", "string, Buffer, or URL", arguments.next() orelse .undefined); }; errdefer prefix.deinit(); var encoding = Encoding.utf8; if (arguments.next()) |val| { arguments.eat(); switch (val.jsType()) { JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject => { encoding = try Encoding.assert(val, ctx, encoding); }, else => { if (val.isObject()) { encoding = try getEncoding(val, ctx, encoding); } }, } } return .{ .prefix = prefix, .encoding = encoding, }; } }; pub const Readdir = struct { path: PathLike, encoding: Encoding = .utf8, with_file_types: bool = false, recursive: bool = false, pub fn deinit(this: Readdir) void { this.path.deinit(); } pub fn deinitAndUnprotect(this: Readdir) void { this.path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *Readdir) void { this.path.toThreadSafe(); } pub fn tag(this: *const Readdir) Return.Readdir.Tag { return switch (this.encoding) { .buffer => .buffers, else => if (this.with_file_types) .with_file_types else .files, }; } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Readdir { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); var encoding: Encoding = .utf8; var with_file_types = false; var recursive = false; if (arguments.next()) |val| { arguments.eat(); switch (val.jsType()) { .String, .StringObject, .DerivedStringObject, => { encoding = try Encoding.assert(val, ctx, encoding); }, else => { if (val.isObject()) { encoding = try getEncoding(val, ctx, encoding); if (try val.getBooleanStrict(ctx, "recursive")) |recursive_| { recursive = recursive_; } if (try val.getBooleanStrict(ctx, "withFileTypes")) |with_file_types_| { with_file_types = with_file_types_; } } }, } } return .{ .path = path, .encoding = encoding, .with_file_types = with_file_types, .recursive = recursive, }; } }; pub const Close = struct { fd: FileDescriptor, pub fn deinit(_: Close) void {} pub fn toThreadSafe(_: Close) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Close { const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { return throwInvalidFdError(ctx, fd_value); }; return Close{ .fd = fd }; } }; pub const Open = struct { path: PathLike, flags: FileSystemFlags = .r, mode: Mode = default_permission, pub fn deinit(this: Open) void { this.path.deinit(); } pub fn deinitAndUnprotect(this: Open) void { this.path.deinitAndUnprotect(); } pub fn toThreadSafe(this: *Open) void { this.path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Open { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); var flags = FileSystemFlags.r; var mode: Mode = default_permission; if (arguments.next()) |val| { arguments.eat(); if (val.isObject()) { if (try val.getTruthy(ctx, "flags")) |flags_| { flags = try FileSystemFlags.fromJS(ctx, flags_) orelse flags; } if (try val.getTruthy(ctx, "mode")) |mode_| { mode = try JSC.Node.modeFromJS(ctx, mode_) orelse mode; } } else if (val != .zero) { if (!val.isUndefinedOrNull()) { // error is handled below flags = try FileSystemFlags.fromJS(ctx, val) orelse flags; } if (arguments.nextEat()) |next| { mode = try JSC.Node.modeFromJS(ctx, next) orelse mode; } } } return Open{ .path = path, .flags = flags, .mode = mode, }; } }; /// Change the file system timestamps of the object referenced by `path`. /// /// The `atime` and `mtime` arguments follow these rules: /// /// * Values can be either numbers representing Unix epoch time in seconds,`Date`s, or a numeric string like `'123456789.0'`. /// * If the value can not be converted to a number, or is `NaN`, `Infinity` or`-Infinity`, an `Error` will be thrown. /// @since v0.4.2 pub const Utimes = Lutimes; pub const Futimes = struct { fd: FileDescriptor, atime: TimeLike, mtime: TimeLike, pub fn deinit(_: Futimes) void {} pub fn toThreadSafe(self: *const @This()) void { _ = self; } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Futimes { const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { return throwInvalidFdError(ctx, fd_value); }; const atime = JSC.Node.timeLikeFromJS(ctx, arguments.next() orelse { return ctx.throwInvalidArguments("atime is required", .{}); }) orelse { return ctx.throwInvalidArguments("atime must be a number or a Date", .{}); }; arguments.eat(); const mtime = JSC.Node.timeLikeFromJS(ctx, arguments.next() orelse { return ctx.throwInvalidArguments("mtime is required", .{}); }) orelse { return ctx.throwInvalidArguments("mtime must be a number or a Date", .{}); }; arguments.eat(); return Futimes{ .fd = fd, .atime = atime, .mtime = mtime, }; } }; /// Write `buffer` to the file specified by `fd`. If `buffer` is a normal object, it /// must have an own `toString` function property. /// /// `offset` determines the part of the buffer to be written, and `length` is /// an integer specifying the number of bytes to write. /// /// `position` refers to the offset from the beginning of the file where this data /// should be written. If `typeof position !== 'number'`, the data will be written /// at the current position. See [`pwrite(2)`](http://man7.org/linux/man-pages/man2/pwrite.2.html). /// /// The callback will be given three arguments `(err, bytesWritten, buffer)` where`bytesWritten` specifies how many _bytes_ were written from `buffer`. /// /// If this method is invoked as its `util.promisify()` ed version, it returns /// a promise for an `Object` with `bytesWritten` and `buffer` properties. /// /// It is unsafe to use `fs.write()` multiple times on the same file without waiting /// for the callback. For this scenario, {@link createWriteStream} is /// recommended. /// /// On Linux, positional writes don't work when the file is opened in append mode. /// The kernel ignores the position argument and always appends the data to /// the end of the file. /// @since v0.0.2 pub const Write = struct { fd: FileDescriptor, buffer: StringOrBuffer, // buffer_val: JSC.JSValue = JSC.JSValue.zero, offset: u64 = 0, length: u64 = std.math.maxInt(u64), position: ?ReadPosition = null, encoding: Encoding = Encoding.buffer, pub fn deinit(this: *const @This()) void { this.buffer.deinit(); } pub fn deinitAndUnprotect(this: *@This()) void { this.buffer.deinitAndUnprotect(); } pub fn toThreadSafe(self: *@This()) void { self.buffer.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Write { const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { return throwInvalidFdError(ctx, fd_value); }; const buffer_value = arguments.next(); const buffer = StringOrBuffer.fromJS(ctx, bun.default_allocator, buffer_value orelse { return ctx.throwInvalidArguments("data is required", .{}); }) orelse { return ctx.throwInvalidArgumentTypeValue("buffer", "string or TypedArray", buffer_value.?); }; if (buffer_value.?.isString() and !buffer_value.?.isStringLiteral()) { return ctx.throwInvalidArgumentTypeValue("buffer", "string or TypedArray", buffer_value.?); } var args = Write{ .fd = fd, .buffer = buffer, .encoding = switch (buffer) { .buffer => Encoding.buffer, inline else => Encoding.utf8, }, }; errdefer args.deinit(); arguments.eat(); parse: { var current = arguments.next() orelse break :parse; switch (buffer) { // fs.write(fd, string[, position[, encoding]], callback) else => { if (current.isNumber()) { args.position = current.to(i52); arguments.eat(); current = arguments.next() orelse break :parse; } if (current.isString()) { args.encoding = try Encoding.assert(current, ctx, args.encoding); arguments.eat(); } }, // fs.write(fd, buffer[, offset[, length[, position]]], callback) .buffer => { if (current.isUndefinedOrNull() or current.isFunction()) break :parse; args.offset = @intCast(try JSC.Node.validators.validateInteger(ctx, current, "offset", 0, 9007199254740991)); arguments.eat(); current = arguments.next() orelse break :parse; if (!(current.isNumber() or current.isBigInt())) break :parse; const length = current.to(i64); const buf_len = args.buffer.buffer.slice().len; const max_offset = @min(buf_len, std.math.maxInt(i64)); if (args.offset > max_offset) { return ctx.throwRangeError( @as(f64, @floatFromInt(args.offset)), .{ .field_name = "offset", .max = @intCast(max_offset) }, ); } const max_len = @min(buf_len - args.offset, std.math.maxInt(i32)); if (length > max_len or length < 0) { return ctx.throwRangeError( @as(f64, @floatFromInt(length)), .{ .field_name = "length", .min = 0, .max = @intCast(max_len) }, ); } args.length = @intCast(length); arguments.eat(); current = arguments.next() orelse break :parse; if (!(current.isNumber() or current.isBigInt())) break :parse; const position = current.to(i52); if (position >= 0) args.position = position; arguments.eat(); }, } } return args; } }; pub const Read = struct { fd: FileDescriptor, buffer: Buffer, offset: u64 = 0, length: u64 = std.math.maxInt(u64), position: ?ReadPosition = null, pub fn deinit(_: Read) void {} pub fn toThreadSafe(this: Read) void { this.buffer.buffer.value.protect(); } pub fn deinitAndUnprotect(this: *Read) void { this.buffer.buffer.value.unprotect(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Read { // About half of the normalization has already been done. The second half is done in the native code. // fs_binding.read(fd, buffer, offset, length, position) // fd = getValidatedFd(fd); const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { return throwInvalidFdError(ctx, fd_value); }; // validateBuffer(buffer); const buffer_value = arguments.nextEat() orelse // theoretically impossible, argument has been passed already return ctx.throwInvalidArguments("buffer is required", .{}); const buffer: JSC.MarkedArrayBuffer = Buffer.fromJS(ctx, buffer_value) orelse return ctx.throwInvalidArgumentTypeValue("buffer", "TypedArray", buffer_value); var args: Read = .{ .fd = fd, .buffer = buffer }; const offset_value: JSC.JSValue = arguments.nextEat() orelse .null; // if (offset == null) { // offset = 0; // } else { // validateInteger(offset, 'offset', 0); // } args.offset = if (offset_value.isUndefinedOrNull()) 0 else @intCast(try JSC.Node.validators.validateInteger(ctx, offset_value, "offset", 0, JSC.MAX_SAFE_INTEGER)); // length |= 0; const length: f64 = if (arguments.nextEat()) |arg| try arg.toNumber(ctx) else 0; // if (length === 0) { // return process.nextTick(function tick() { // callback(null, 0, buffer); // }); // } if (length == 0) { return .{ .fd = fd, .buffer = buffer, .length = 0 }; } const buf_len = buffer.slice().len; if (buf_len == 0) { return ctx.ERR_INVALID_ARG_VALUE("The argument 'buffer' is empty and cannot be written.", .{}).throw(); } // validateOffsetLengthRead(offset, length, buffer.byteLength); if (@mod(length, 1) != 0) { return ctx.throwRangeError(length, .{ .field_name = "length", .msg = "an integer" }); } const int_length: i64 = @intFromFloat(length); if (int_length > buf_len) { return ctx.throwRangeError( length, .{ .field_name = "length", .max = @intCast(@min(buf_len, std.math.maxInt(i64))) }, ); } if (@as(i64, @intCast(args.offset)) +| int_length > buf_len) { return ctx.throwRangeError( length, .{ .field_name = "length", .max = @intCast(buf_len -| args.offset) }, ); } if (int_length < 0) { return ctx.throwRangeError(length, .{ .field_name = "length", .min = 0 }); } args.length = @intCast(int_length); // if (position == null) { // position = -1; // } else { // validatePosition(position, 'position', length); // } const position_value: JSC.JSValue = arguments.nextEat() orelse .null; const position_int: i64 = if (position_value.isUndefinedOrNull()) -1 else if (position_value.isNumber()) try JSC.Node.validators.validateInteger(ctx, position_value, "position", -1, JSC.MAX_SAFE_INTEGER) else if (position_value.isBigInt()) pos: { const max_position = std.math.maxInt(i64) - args.length; const position = position_value.to(i64); if (position < -1 or position > max_position) { return ctx.throwRangeError(position, .{ .field_name = "position", .min = -1, .max = @intCast(max_position), }); } break :pos position; } else return ctx.throwInvalidArgumentTypeValue("position", "number or bigint", position_value); // Bun needs `null` to tell the native function if to use pread or read args.position = if (position_int >= 0) position_int else null; return args; } }; /// Asynchronously reads the entire contents of a file. /// @param path A path to a file. If a URL is provided, it must use the `file:` protocol. /// If a file descriptor is provided, the underlying file will _not_ be closed automatically. /// @param options Either the encoding for the result, or an object that contains the encoding and an optional flag. /// If a flag is not provided, it defaults to `'r'`. pub const ReadFile = struct { path: PathOrFileDescriptor, encoding: Encoding = Encoding.utf8, offset: JSC.WebCore.Blob.SizeType = 0, max_size: ?JSC.WebCore.Blob.SizeType = null, limit_size_for_javascript: bool = false, flag: FileSystemFlags = FileSystemFlags.r, signal: ?*AbortSignal = null, pub fn deinit(self: ReadFile) void { self.path.deinit(); if (self.signal) |signal| { signal.pendingActivityUnref(); signal.unref(); } } pub fn deinitAndUnprotect(self: ReadFile) void { self.path.deinitAndUnprotect(); if (self.signal) |signal| { signal.pendingActivityUnref(); signal.unref(); } } pub fn toThreadSafe(self: *ReadFile) void { self.path.toThreadSafe(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!ReadFile { const path = try PathOrFileDescriptor.fromJS(ctx, arguments, bun.default_allocator) orelse { return ctx.throwInvalidArguments("path must be a string or a file descriptor", .{}); }; errdefer path.deinit(); var encoding = Encoding.buffer; var flag = FileSystemFlags.r; var abort_signal: ?*AbortSignal = null; errdefer if (abort_signal) |signal| { signal.pendingActivityUnref(); signal.unref(); }; if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { encoding = try Encoding.assert(arg, ctx, encoding); } else if (arg.isObject()) { encoding = try getEncoding(arg, ctx, encoding); if (try arg.getTruthy(ctx, "flag")) |flag_| { flag = try FileSystemFlags.fromJS(ctx, flag_) orelse { return ctx.throwInvalidArguments("Invalid flag", .{}); }; } if (try arg.getTruthy(ctx, "signal")) |value| { if (AbortSignal.fromJS(value)) |signal| { abort_signal = signal.ref(); signal.pendingActivityRef(); } else { return ctx.throwInvalidArgumentTypeValue("signal", "AbortSignal", value); } } } } return .{ .path = path, .encoding = encoding, .flag = flag, .limit_size_for_javascript = true, .signal = abort_signal, }; } pub fn aborted(self: ReadFile) bool { if (self.signal) |signal| { return signal.aborted(); } return false; } }; pub const WriteFile = struct { encoding: Encoding = Encoding.utf8, flag: FileSystemFlags = FileSystemFlags.w, mode: Mode = 0o666, file: PathOrFileDescriptor, flush: bool = false, /// Encoded at the time of construction. data: StringOrBuffer, dirfd: FileDescriptor, signal: ?*AbortSignal = null, pub fn deinit(self: WriteFile) void { self.file.deinit(); self.data.deinit(); if (self.signal) |signal| { signal.pendingActivityUnref(); signal.unref(); } } pub fn toThreadSafe(self: *WriteFile) void { self.file.toThreadSafe(); self.data.toThreadSafe(); } pub fn deinitAndUnprotect(self: *WriteFile) void { self.file.deinitAndUnprotect(); self.data.deinitAndUnprotect(); if (self.signal) |signal| { signal.pendingActivityUnref(); signal.unref(); } } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!WriteFile { const path = try PathOrFileDescriptor.fromJS(ctx, arguments, bun.default_allocator) orelse { return ctx.throwInvalidArguments("path must be a string or a file descriptor", .{}); }; errdefer path.deinit(); const data_value = arguments.nextEat() orelse { return ctx.throwInvalidArguments("data is required", .{}); }; var encoding = Encoding.buffer; var flag = FileSystemFlags.w; var mode: Mode = default_permission; var abort_signal: ?*AbortSignal = null; errdefer if (abort_signal) |signal| { signal.pendingActivityUnref(); signal.unref(); }; var flush: bool = false; if (data_value.isString()) { encoding = Encoding.utf8; } if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { encoding = try Encoding.assert(arg, ctx, encoding); } else if (arg.isObject()) { encoding = try getEncoding(arg, ctx, encoding); if (try arg.getTruthy(ctx, "flag")) |flag_| { flag = try FileSystemFlags.fromJS(ctx, flag_) orelse { return ctx.throwInvalidArguments("Invalid flag", .{}); }; } if (try arg.getTruthy(ctx, "mode")) |mode_| { mode = try JSC.Node.modeFromJS(ctx, mode_) orelse mode; } if (try arg.getTruthy(ctx, "signal")) |value| { if (AbortSignal.fromJS(value)) |signal| { abort_signal = signal.ref(); signal.pendingActivityRef(); } else { return ctx.throwInvalidArgumentTypeValue("signal", "AbortSignal", value); } } if (try arg.getOptional(ctx, "flush", JSC.JSValue)) |flush_| { if (flush_.isBoolean() or flush_.isUndefinedOrNull()) { flush = flush_ == .true; } else { return ctx.throwInvalidArgumentTypeValue("flush", "boolean", flush_); } } } } // String objects not allowed (typeof new String("hi") === "object") // https://github.com/nodejs/node/blob/6f946c95b9da75c70e868637de8161bc8d048379/lib/internal/fs/utils.js#L916 const allow_string_object = false; const data = try StringOrBuffer.fromJSWithEncodingMaybeAsync(ctx, bun.default_allocator, data_value, encoding, arguments.will_be_async, allow_string_object) orelse { return ctx.ERR_INVALID_ARG_TYPE("The \"data\" argument must be of type string or an instance of Buffer, TypedArray, or DataView", .{}).throw(); }; return .{ .file = path, .encoding = encoding, .flag = flag, .mode = mode, .data = data, .dirfd = bun.FD.cwd(), .signal = abort_signal, .flush = flush, }; } pub fn aborted(self: WriteFile) bool { if (self.signal) |signal| { return signal.aborted(); } return false; } }; pub const AppendFile = WriteFile; pub const OpenDir = struct { path: PathLike, encoding: Encoding = Encoding.utf8, /// Number of directory entries that are buffered internally when reading from the directory. Higher values lead to better performance but higher memory usage. Default: 32 buffer_size: c_int = 32, pub fn deinit(self: OpenDir) void { self.path.deinit(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!OpenDir { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); var encoding = Encoding.buffer; var buffer_size: c_int = 32; if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { encoding = Encoding.assert(arg, ctx, encoding) catch encoding; } else if (arg.isObject()) { if (getEncoding(arg, ctx)) |encoding_| { encoding = encoding_; } if (try arg.get(ctx, "bufferSize")) |buffer_size_| { buffer_size = buffer_size_.toInt32(); if (buffer_size < 0) { return ctx.throwInvalidArguments("bufferSize must be > 0", .{}); } } } } return OpenDir{ .path = path, .encoding = encoding, .buffer_size = buffer_size, }; } }; pub const Exists = struct { path: ?PathLike, pub fn deinit(this: Exists) void { if (this.path) |path| { path.deinit(); } } pub fn toThreadSafe(this: *Exists) void { if (this.path) |*path| { path.toThreadSafe(); } } pub fn deinitAndUnprotect(this: *Exists) void { if (this.path) |*path| { path.deinitAndUnprotect(); } } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Exists { return Exists{ .path = try PathLike.fromJS(ctx, arguments), }; } }; pub const Access = struct { path: PathLike, mode: FileSystemFlags = FileSystemFlags.r, pub fn deinit(this: Access) void { this.path.deinit(); } pub fn toThreadSafe(this: *Access) void { this.path.toThreadSafe(); } pub fn deinitAndUnprotect(this: *Access) void { this.path.deinitAndUnprotect(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Access { const path = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("path must be a string or TypedArray", .{}); }; errdefer path.deinit(); var mode = FileSystemFlags.r; if (arguments.next()) |arg| { arguments.eat(); mode = try FileSystemFlags.fromJSNumberOnly(ctx, arg, .access); } return Access{ .path = path, .mode = mode, }; } }; pub const FdataSync = struct { fd: FileDescriptor, pub fn deinit(_: FdataSync) void {} pub fn toThreadSafe(self: *const @This()) void { _ = self; } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!FdataSync { const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { return throwInvalidFdError(ctx, fd_value); }; return FdataSync{ .fd = fd }; } }; pub const CopyFile = struct { src: PathLike, dest: PathLike, mode: Constants.Copyfile, pub fn deinit(this: *const CopyFile) void { this.src.deinit(); this.dest.deinit(); } pub fn toThreadSafe(this: *CopyFile) void { this.src.toThreadSafe(); this.dest.toThreadSafe(); } pub fn deinitAndUnprotect(this: *const CopyFile) void { this.src.deinitAndUnprotect(); this.dest.deinitAndUnprotect(); } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!CopyFile { const src = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("src must be a string or TypedArray", .{}); }; errdefer src.deinit(); const dest = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("dest must be a string or TypedArray", .{}); }; errdefer dest.deinit(); var mode: Constants.Copyfile = @enumFromInt(0); if (arguments.next()) |arg| { arguments.eat(); mode = @enumFromInt(@intFromEnum(try FileSystemFlags.fromJSNumberOnly(ctx, arg, .copy_file))); } return CopyFile{ .src = src, .dest = dest, .mode = mode, }; } }; pub const Cp = struct { src: PathLike, dest: PathLike, flags: Flags, const Flags = struct { mode: Constants.Copyfile, recursive: bool, errorOnExist: bool, force: bool, deinit_paths: bool = true, }; pub fn deinit(this: *const Cp) void { if (this.flags.deinit_paths) { this.src.deinit(); this.dest.deinit(); } } pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Cp { const src = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("src must be a string or TypedArray", .{}); }; errdefer src.deinit(); const dest = try PathLike.fromJS(ctx, arguments) orelse { return ctx.throwInvalidArguments("dest must be a string or TypedArray", .{}); }; errdefer dest.deinit(); var recursive: bool = false; var errorOnExist: bool = false; var force: bool = true; var mode: i32 = 0; if (arguments.next()) |arg| { arguments.eat(); recursive = arg.toBoolean(); } if (arguments.next()) |arg| { arguments.eat(); errorOnExist = arg.toBoolean(); } if (arguments.next()) |arg| { arguments.eat(); force = arg.toBoolean(); } if (arguments.next()) |arg| { arguments.eat(); if (arg.isNumber()) { mode = arg.coerce(i32, ctx); } } return Cp{ .src = src, .dest = dest, .flags = .{ .mode = @enumFromInt(mode), .recursive = recursive, .errorOnExist = errorOnExist, .force = force, }, }; } }; pub const WriteEv = struct { fd: FileDescriptor, buffers: []const ArrayBuffer, position: ReadPosition, }; pub const ReadEv = struct { fd: FileDescriptor, buffers: []ArrayBuffer, position: ReadPosition, }; pub const UnwatchFile = void; pub const Watch = JSC.Node.FSWatcher.Arguments; pub const WatchFile = JSC.Node.StatWatcher.Arguments; pub const Fsync = struct { fd: FileDescriptor, pub fn deinit(_: Fsync) void {} pub fn toThreadSafe(_: *const @This()) void {} pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice) bun.JSError!Fsync { const fd_value = arguments.nextEat() orelse JSC.JSValue.undefined; const fd = try JSC.Node.fileDescriptorFromJS(ctx, fd_value) orelse { return throwInvalidFdError(ctx, fd_value); }; return Fsync{ .fd = fd }; } }; }; pub const StatOrNotFound = union(enum) { stats: bun.JSC.Node.Stats, not_found: void, pub fn toJS(this: *StatOrNotFound, globalObject: *JSC.JSGlobalObject) JSC.JSValue { return switch (this.*) { .stats => this.stats.toJS(globalObject), .not_found => JSC.JSValue.undefined, }; } pub fn toJSNewlyCreated(this: *const StatOrNotFound, globalObject: *JSC.JSGlobalObject) JSC.JSValue { return switch (this.*) { .stats => this.stats.toJSNewlyCreated(globalObject), .not_found => JSC.JSValue.undefined, }; } }; pub const StringOrUndefined = union(enum) { string: bun.String, none: void, pub fn toJS(this: *const StringOrUndefined, globalObject: *JSC.JSGlobalObject) JSC.JSValue { return switch (this.*) { .string => this.string.toJS(globalObject), .none => JSC.JSValue.undefined, }; } }; /// For use in `Return`'s definitions to act as `void` while returning `null` to JavaScript const Null = struct { pub fn toJS(_: @This(), _: *JSC.JSGlobalObject) JSC.JSValue { return .null; } }; const Return = struct { pub const Access = Null; pub const AppendFile = void; pub const Close = void; pub const CopyFile = void; pub const Cp = void; pub const Exists = bool; pub const Fchmod = void; pub const Chmod = void; pub const Fchown = void; pub const Fdatasync = void; pub const Fstat = bun.JSC.Node.Stats; pub const Rm = void; pub const Fsync = void; pub const Ftruncate = void; pub const Futimes = void; pub const Lchmod = void; pub const Lchown = void; pub const Link = void; pub const Lstat = StatOrNotFound; pub const Mkdir = StringOrUndefined; pub const Mkdtemp = JSC.ZigString; pub const Open = FDImpl; pub const WriteFile = void; pub const Readv = Read; pub const StatFS = bun.JSC.Node.StatFS; pub const Read = struct { bytes_read: u52, pub fn toJS(this: Read, _: JSC.C.JSContextRef) JSC.JSValue { return JSC.JSValue.jsNumberFromUint64(this.bytes_read); } }; pub const ReadPromise = struct { bytes_read: u52, buffer_val: JSC.JSValue = JSC.JSValue.zero, const fields = .{ .bytesRead = JSC.ZigString.init("bytesRead"), .buffer = JSC.ZigString.init("buffer"), }; pub fn toJS(this: *const ReadPromise, ctx: *JSC.JSGlobalObject) JSC.JSValue { defer if (!this.buffer_val.isEmptyOrUndefinedOrNull()) this.buffer_val.unprotect(); return JSC.JSValue.createObject2( ctx, &fields.bytesRead, &fields.buffer, JSC.JSValue.jsNumberFromUint64(@as(u52, @intCast(@min(std.math.maxInt(u52), this.bytes_read)))), this.buffer_val, ); } }; pub const WritePromise = struct { bytes_written: u52, buffer: StringOrBuffer, buffer_val: JSC.JSValue = JSC.JSValue.zero, const fields = .{ .bytesWritten = JSC.ZigString.init("bytesWritten"), .buffer = JSC.ZigString.init("buffer"), }; // Excited for the issue that's like "cannot read file bigger than 2 GB" pub fn toJS(this: *const WritePromise, globalObject: JSC.C.JSContextRef) JSC.C.JSValueRef { defer if (!this.buffer_val.isEmptyOrUndefinedOrNull()) this.buffer_val.unprotect(); return JSC.JSValue.createObject2( globalObject, &fields.bytesWritten, &fields.buffer, JSC.JSValue.jsNumberFromUint64(@as(u52, @intCast(@min(std.math.maxInt(u52), this.bytes_written)))), if (this.buffer == .buffer) this.buffer_val else this.buffer.toJS(globalObject), ); } }; pub const Write = struct { bytes_written: u52, const fields = .{ .bytesWritten = JSC.ZigString.init("bytesWritten"), }; // Excited for the issue that's like "cannot read file bigger than 2 GB" pub fn toJS(this: *const Write, _: *JSC.JSGlobalObject) JSC.JSValue { return JSC.JSValue.jsNumberFromUint64(this.bytes_written); } }; pub const Readdir = union(Tag) { with_file_types: []bun.JSC.Node.Dirent, buffers: []Buffer, files: []const bun.String, pub const Tag = enum { with_file_types, buffers, files, }; pub fn toJS(this: Readdir, globalObject: *JSC.JSGlobalObject) JSC.JSValue { switch (this) { .with_file_types => { defer bun.default_allocator.free(this.with_file_types); var array = JSC.JSValue.createEmptyArray(globalObject, this.with_file_types.len); var previous_jsstring: ?*JSC.JSString = null; for (this.with_file_types, 0..) |*item, i| { const res = item.toJSNewlyCreated(globalObject, &previous_jsstring); if (res == .zero) return .zero; array.putIndex( globalObject, @truncate(i), res, ); } return array; }, .buffers => { defer bun.default_allocator.free(this.buffers); return JSC.toJS(globalObject, []Buffer, this.buffers, .temporary); }, .files => { // automatically freed return JSC.toJS(globalObject, []const bun.String, this.files, .temporary); }, } } }; pub const ReadFile = StringOrBuffer; pub const ReadFileWithOptions = union(enum) { string: string, transcoded_string: bun.String, buffer: Buffer, null_terminated: [:0]const u8, }; pub const Readlink = StringOrBuffer; pub const Realpath = StringOrBuffer; pub const RealpathNative = Realpath; pub const Rename = void; pub const Rmdir = void; pub const Stat = StatOrNotFound; pub const Symlink = void; pub const Truncate = void; pub const Unlink = void; pub const UnwatchFile = void; pub const Watch = JSC.JSValue; pub const WatchFile = JSC.JSValue; pub const Utimes = void; pub const Chown = void; pub const Lutimes = void; pub const Writev = Write; }; /// Bun's implementation of the Node.js "fs" module /// https://nodejs.org/api/fs.html /// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/fs.d.ts pub const NodeFS = struct { /// Buffer to store a temporary file path that might appear in a returned error message. /// /// We want to avoid allocating a new path buffer for every error message so that JSC can clone + GC it. /// That means a stack-allocated buffer won't suffice. Instead, we re-use /// the heap allocated buffer on the NodeFS struct sync_error_buf: bun.PathBuffer align(@alignOf(u16)) = undefined, vm: ?*JSC.VirtualMachine = null, pub const ReturnType = Return; pub fn access(this: *NodeFS, args: Arguments.Access, _: Flavor) Maybe(Return.Access) { const path: bun.OSPathSliceZ = if (args.path.slice().len == 0) comptime bun.OSPathLiteral("") else args.path.osPathKernel32(&this.sync_error_buf); return switch (Syscall.access(path, args.mode.asInt())) { .err => |err| .{ .err = err.withPath(args.path.slice()) }, .result => .{ .result = .{} }, }; } pub fn appendFile(this: *NodeFS, args: Arguments.AppendFile, _: Flavor) Maybe(Return.AppendFile) { var data = args.data.slice(); switch (args.file) { .fd => |fd| { while (data.len > 0) { const written = switch (Syscall.write(fd, data)) { .result => |result| result, .err => |err| return .{ .err = err }, }; data = data[written..]; } return Maybe(Return.AppendFile).success; }, .path => |path_| { const path = path_.sliceZ(&this.sync_error_buf); const fd = switch (Syscall.open(path, @intFromEnum(FileSystemFlags.a), args.mode)) { .result => |result| result, .err => |err| return .{ .err = err }, }; defer { _ = Syscall.close(fd); } while (data.len > 0) { const written = switch (Syscall.write(fd, data)) { .result => |result| result, .err => |err| return .{ .err = err }, }; data = data[written..]; } return Maybe(Return.AppendFile).success; }, } } pub fn close(_: *NodeFS, args: Arguments.Close, _: Flavor) Maybe(Return.Close) { return if (Syscall.close(args.fd)) |err| .{ .err = err } else Maybe(Return.Close).success; } pub fn uv_close(_: *NodeFS, args: Arguments.Close, rc: i64) Maybe(Return.Close) { if (rc < 0) { return Maybe(Return.Close){ .err = .{ .errno = @intCast(-rc), .syscall = .close, .fd = args.fd, .from_libuv = true, } }; } return Maybe(Return.Close).success; } // since we use a 64 KB stack buffer, we should not let this function get inlined pub noinline fn copyFileUsingReadWriteLoop(src: [:0]const u8, dest: [:0]const u8, src_fd: FileDescriptor, dest_fd: FileDescriptor, stat_size: usize, wrote: *u64) Maybe(Return.CopyFile) { var stack_buf: [64 * 1024]u8 = undefined; var buf_to_free: []u8 = &[_]u8{}; var buf: []u8 = &stack_buf; maybe_allocate_large_temp_buf: { if (stat_size > stack_buf.len * 16) { // Don't allocate more than 8 MB at a time const clamped_size: usize = @min(stat_size, 8 * 1024 * 1024); const buf_ = bun.default_allocator.alloc(u8, clamped_size) catch break :maybe_allocate_large_temp_buf; buf = buf_; buf_to_free = buf_; } } defer { if (buf_to_free.len > 0) bun.default_allocator.free(buf_to_free); } var remain = @as(u64, @intCast(@max(stat_size, 0))); toplevel: while (remain > 0) { const amt = switch (Syscall.read(src_fd, buf[0..@min(buf.len, remain)])) { .result => |result| result, .err => |err| return Maybe(Return.CopyFile){ .err = if (src.len > 0) err.withPath(src) else err }, }; // 0 == EOF if (amt == 0) { break :toplevel; } wrote.* += amt; remain -|= amt; var slice = buf[0..amt]; while (slice.len > 0) { const written = switch (Syscall.write(dest_fd, slice)) { .result => |result| result, .err => |err| return Maybe(Return.CopyFile){ .err = if (dest.len > 0) err.withPath(dest) else err }, }; if (written == 0) break :toplevel; slice = slice[written..]; } } else { outer: while (true) { const amt = switch (Syscall.read(src_fd, buf)) { .result => |result| result, .err => |err| return Maybe(Return.CopyFile){ .err = if (src.len > 0) err.withPath(src) else err }, }; // we don't know the size // so we just go forever until we get an EOF if (amt == 0) { break; } wrote.* += amt; var slice = buf[0..amt]; while (slice.len > 0) { const written = switch (Syscall.write(dest_fd, slice)) { .result => |result| result, .err => |err| return Maybe(Return.CopyFile){ .err = if (dest.len > 0) err.withPath(dest) else err }, }; slice = slice[written..]; if (written == 0) break :outer; } } } return Maybe(Return.CopyFile).success; } // copy_file_range() is frequently not supported across devices, such as tmpfs. // This is relevant for `bun install` // However, sendfile() is supported across devices. // Only on Linux. There are constraints though. It cannot be used if the file type does not support pub noinline fn copyFileUsingSendfileOnLinuxWithReadWriteFallback(src: [:0]const u8, dest: [:0]const u8, src_fd: FileDescriptor, dest_fd: FileDescriptor, stat_size: usize, wrote: *u64) Maybe(Return.CopyFile) { while (true) { const amt = switch (bun.sys.sendfile(src_fd, dest_fd, std.math.maxInt(i32) - 1)) { .err => { return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, stat_size, wrote); }, .result => |amount| amount, }; wrote.* += amt; if (amt == 0) { break; } } return Maybe(Return.CopyFile).success; } pub fn copyFile(this: *NodeFS, args: Arguments.CopyFile, _: Flavor) Maybe(Return.CopyFile) { return switch (this.copyFileInner(args)) { .result => .{ .result = {} }, .err => |err| .{ .err = .{ .errno = err.errno, .syscall = .copyfile, .path = args.src.slice(), .dest = args.dest.slice(), } }, }; } /// https://github.com/libuv/libuv/pull/2233 /// https://github.com/pnpm/pnpm/issues/2761 /// https://github.com/libuv/libuv/pull/2578 /// https://github.com/nodejs/node/issues/34624 fn copyFileInner(fs: *NodeFS, args: Arguments.CopyFile) Maybe(Return.CopyFile) { const ret = Maybe(Return.CopyFile); // TODO: do we need to fchown? if (comptime Environment.isMac) { var src_buf: bun.PathBuffer = undefined; var dest_buf: bun.PathBuffer = undefined; const src = args.src.sliceZ(&src_buf); const dest = args.dest.sliceZ(&dest_buf); if (args.mode.isForceClone()) { // https://www.manpagez.com/man/2/clonefile/ return ret.errnoSysP(C.clonefile(src, dest, 0), .copyfile, src) orelse ret.success; } else { const stat_ = switch (Syscall.stat(src)) { .result => |result| result, .err => |err| return Maybe(Return.CopyFile){ .err = err.withPath(src) }, }; if (!posix.S.ISREG(stat_.mode)) { return Maybe(Return.CopyFile){ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOTSUP), .syscall = .copyfile, } }; } // 64 KB is about the break-even point for clonefile() to be worth it // at least, on an M1 with an NVME SSD. if (stat_.size > 128 * 1024) { if (!args.mode.shouldntOverwrite()) { // clonefile() will fail if it already exists _ = Syscall.unlink(dest); } if (ret.errnoSysP(C.clonefile(src, dest, 0), .copyfile, src) == null) { _ = Syscall.chmod(dest, stat_.mode); return ret.success; } } else { const src_fd = switch (Syscall.open(src, bun.O.RDONLY, 0o644)) { .result => |result| result, .err => |err| return .{ .err = err.withPath(args.src.slice()) }, }; defer { _ = Syscall.close(src_fd); } var flags: Mode = bun.O.CREAT | bun.O.WRONLY; var wrote: usize = 0; if (args.mode.shouldntOverwrite()) { flags |= bun.O.EXCL; } const dest_fd = switch (Syscall.open(dest, flags, JSC.Node.default_permission)) { .result => |result| result, .err => |err| return Maybe(Return.CopyFile){ .err = err.withPath(args.dest.slice()) }, }; defer { _ = Syscall.ftruncate(dest_fd, @as(std.c.off_t, @intCast(@as(u63, @truncate(wrote))))); _ = Syscall.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); } return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, @intCast(@max(stat_.size, 0)), &wrote); } } // we fallback to copyfile() when the file is > 128 KB and clonefile fails // clonefile() isn't supported on all devices // nor is it supported across devices var mode: u32 = C.darwin.COPYFILE_ACL | C.darwin.COPYFILE_DATA; if (args.mode.shouldntOverwrite()) { mode |= C.darwin.COPYFILE_EXCL; } return ret.errnoSysP(C.copyfile(src, dest, null, mode), .copyfile, src) orelse ret.success; } if (comptime Environment.isLinux) { var src_buf: bun.PathBuffer = undefined; var dest_buf: bun.PathBuffer = undefined; const src = args.src.sliceZ(&src_buf); const dest = args.dest.sliceZ(&dest_buf); const src_fd = switch (Syscall.open(src, bun.O.RDONLY, 0o644)) { .result => |result| result, .err => |err| return .{ .err = err }, }; defer { _ = Syscall.close(src_fd); } const stat_: linux.Stat = switch (Syscall.fstat(src_fd)) { .result => |result| result, .err => |err| return Maybe(Return.CopyFile){ .err = err }, }; if (!posix.S.ISREG(stat_.mode)) { return Maybe(Return.CopyFile){ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOTSUP), .syscall = .copyfile } }; } var flags: i32 = bun.O.CREAT | bun.O.WRONLY; var wrote: usize = 0; if (args.mode.shouldntOverwrite()) { flags |= bun.O.EXCL; } const dest_fd = switch (Syscall.open(dest, flags, JSC.Node.default_permission)) { .result => |result| result, .err => |err| return Maybe(Return.CopyFile){ .err = err }, }; var size: usize = @intCast(@max(stat_.size, 0)); // https://manpages.debian.org/testing/manpages-dev/ioctl_ficlone.2.en.html if (args.mode.isForceClone()) { if (ret.errnoSysP(bun.C.linux.ioctl_ficlone(dest_fd, src_fd), .ioctl_ficlone, dest)) |err| { _ = Syscall.close(dest_fd); // This is racey, but it's the best we can do _ = bun.sys.unlink(dest); return err; } _ = Syscall.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); return ret.success; } // If we know it's a regular file and ioctl_ficlone is available, attempt to use it. if (posix.S.ISREG(stat_.mode) and bun.can_use_ioctl_ficlone()) { const rc = bun.C.linux.ioctl_ficlone(dest_fd, src_fd); if (rc == 0) { _ = Syscall.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); return ret.success; } // If this fails for any reason, we say it's disabled // We don't want to add the system call overhead of running this function on a lot of files that don't support it bun.disable_ioctl_ficlone(); } defer { _ = linux.ftruncate(dest_fd.cast(), @as(i64, @intCast(@as(u63, @truncate(wrote))))); _ = linux.fchmod(dest_fd.cast(), stat_.mode); _ = Syscall.close(dest_fd); } var off_in_copy = @as(i64, @bitCast(@as(u64, 0))); var off_out_copy = @as(i64, @bitCast(@as(u64, 0))); if (!bun.canUseCopyFileRangeSyscall()) { return copyFileUsingSendfileOnLinuxWithReadWriteFallback(src, dest, src_fd, dest_fd, size, &wrote); } if (size == 0) { // copy until EOF while (true) { // Linux Kernel 5.3 or later // Not supported in gVisor const written = linux.copy_file_range(src_fd.cast(), &off_in_copy, dest_fd.cast(), &off_out_copy, std.mem.page_size, 0); if (ret.errnoSysP(written, .copy_file_range, dest)) |err| { return switch (err.getErrno()) { .INTR => continue, inline .XDEV, .NOSYS => |errno| brk: { if (comptime errno == .NOSYS) { bun.disableCopyFileRangeSyscall(); } break :brk copyFileUsingSendfileOnLinuxWithReadWriteFallback(src, dest, src_fd, dest_fd, size, &wrote); }, else => return err, }; } // wrote zero bytes means EOF if (written == 0) break; wrote +|= written; } } else { while (size > 0) { // Linux Kernel 5.3 or later // Not supported in gVisor const written = linux.copy_file_range(src_fd.cast(), &off_in_copy, dest_fd.cast(), &off_out_copy, size, 0); if (ret.errnoSysP(written, .copy_file_range, dest)) |err| { return switch (err.getErrno()) { .INTR => continue, inline .XDEV, .NOSYS => |errno| brk: { if (comptime errno == .NOSYS) { bun.disableCopyFileRangeSyscall(); } break :brk copyFileUsingSendfileOnLinuxWithReadWriteFallback(src, dest, src_fd, dest_fd, size, &wrote); }, else => return err, }; } // wrote zero bytes means EOF if (written == 0) break; wrote +|= written; size -|= written; } } return ret.success; } if (comptime Environment.isWindows) { const dest_buf = bun.OSPathBufferPool.get(); defer bun.OSPathBufferPool.put(dest_buf); const src = bun.strings.toKernel32Path(bun.reinterpretSlice(u16, &fs.sync_error_buf), args.src.slice()); const dest = bun.strings.toKernel32Path(dest_buf, args.dest.slice()); if (windows.CopyFileW(src.ptr, dest.ptr, if (args.mode.shouldntOverwrite()) 1 else 0) == windows.FALSE) { if (ret.errnoSysP(0, .copyfile, args.src.slice())) |rest| { return shouldIgnoreEbusy(args.src, args.dest, rest); } } return ret.success; } @compileError(unreachable); } pub fn exists(this: *NodeFS, args: Arguments.Exists, _: Flavor) Maybe(Return.Exists) { // NOTE: exists cannot return an error const path: PathLike = args.path orelse return .{ .result = false }; if (bun.StandaloneModuleGraph.get()) |graph| { if (graph.find(path.slice()) != null) { return .{ .result = true }; } } const slice = if (path.slice().len == 0) comptime bun.OSPathLiteral("") else path.osPathKernel32(&this.sync_error_buf); return .{ .result = bun.sys.existsOSPath(slice, false) }; } pub fn chown(this: *NodeFS, args: Arguments.Chown, _: Flavor) Maybe(Return.Chown) { if (comptime Environment.isWindows) { return switch (Syscall.chown(args.path.sliceZ(&this.sync_error_buf), args.uid, args.gid)) { .err => |err| .{ .err = err.withPath(args.path.slice()) }, .result => |res| .{ .result = res }, }; } const path = args.path.sliceZ(&this.sync_error_buf); return Syscall.chown(path, args.uid, args.gid); } pub fn chmod(this: *NodeFS, args: Arguments.Chmod, _: Flavor) Maybe(Return.Chmod) { const path = args.path.sliceZ(&this.sync_error_buf); if (comptime Environment.isWindows) { return switch (Syscall.chmod(path, args.mode)) { .err => |err| .{ .err = err.withPath(args.path.slice()) }, .result => |res| .{ .result = res }, }; } return switch (Syscall.chmod(path, args.mode)) { .err => |err| .{ .err = err.withPath(args.path.slice()) }, .result => Maybe(Return.Chmod).success, }; } pub fn fchmod(_: *NodeFS, args: Arguments.FChmod, _: Flavor) Maybe(Return.Fchmod) { return Syscall.fchmod(args.fd, args.mode); } pub fn fchown(_: *NodeFS, args: Arguments.Fchown, _: Flavor) Maybe(Return.Fchown) { return Syscall.fchown(args.fd, args.uid, args.gid); } pub fn fdatasync(_: *NodeFS, args: Arguments.FdataSync, _: Flavor) Maybe(Return.Fdatasync) { if (Environment.isWindows) { return Syscall.fdatasync(args.fd); } return Maybe(Return.Fdatasync).errnoSysFd(system.fdatasync(args.fd.int()), .fdatasync, args.fd) orelse Maybe(Return.Fdatasync).success; } pub fn fstat(_: *NodeFS, args: Arguments.Fstat, _: Flavor) Maybe(Return.Fstat) { return switch (Syscall.fstat(args.fd)) { .result => |result| .{ .result = .init(result, args.big_int) }, .err => |err| .{ .err = err }, }; } pub fn fsync(_: *NodeFS, args: Arguments.Fsync, _: Flavor) Maybe(Return.Fsync) { if (Environment.isWindows) { return Syscall.fsync(args.fd); } return Maybe(Return.Fsync).errnoSys(system.fsync(args.fd.int()), .fsync) orelse Maybe(Return.Fsync).success; } pub fn ftruncate(_: *NodeFS, args: Arguments.FTruncate, _: Flavor) Maybe(Return.Ftruncate) { return Syscall.ftruncate(args.fd, args.len orelse 0); } pub fn futimes(_: *NodeFS, args: Arguments.Futimes, _: Flavor) Maybe(Return.Futimes) { if (comptime Environment.isWindows) { var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_futime(uv.Loop.get(), &req, bun.uvfdcast(args.fd), args.atime, args.mtime, null); return if (rc.errno()) |e| .{ .err = .{ .errno = e, .syscall = .futime, .fd = args.fd, } } else Maybe(Return.Futimes).success; } return switch (Syscall.futimens(args.fd, args.atime, args.mtime)) { .err => |err| .{ .err = err }, .result => Maybe(Return.Futimes).success, }; } pub fn lchmod(this: *NodeFS, args: Arguments.LCHmod, _: Flavor) Maybe(Return.Lchmod) { if (comptime Environment.isWindows) { return Maybe(Return.Lchmod).todo(); } const path = args.path.sliceZ(&this.sync_error_buf); return Maybe(Return.Lchmod).errnoSysP(C.lchmod(path, args.mode), .lchmod, path) orelse Maybe(Return.Lchmod).success; } pub fn lchown(this: *NodeFS, args: Arguments.LChown, _: Flavor) Maybe(Return.Lchown) { if (comptime Environment.isWindows) { return Maybe(Return.Lchown).todo(); } const path = args.path.sliceZ(&this.sync_error_buf); return Maybe(Return.Lchown).errnoSysP(C.lchown(path, args.uid, args.gid), .lchown, path) orelse Maybe(Return.Lchown).success; } pub fn link(this: *NodeFS, args: Arguments.Link, _: Flavor) Maybe(Return.Link) { var to_buf: bun.PathBuffer = undefined; const from = args.old_path.sliceZ(&this.sync_error_buf); const to = args.new_path.sliceZ(&to_buf); if (Environment.isWindows) { return switch (Syscall.link(from, to)) { .err => |err| .{ .err = err.withPathDest(args.old_path.slice(), args.new_path.slice()) }, .result => |result| .{ .result = result }, }; } return Maybe(Return.Link).errnoSysPD(system.link(from, to), .link, args.old_path.slice(), args.new_path.slice()) orelse Maybe(Return.Link).success; } pub fn lstat(this: *NodeFS, args: Arguments.Lstat, _: Flavor) Maybe(Return.Lstat) { return switch (Syscall.lstat(args.path.sliceZ(&this.sync_error_buf))) { .result => |result| Maybe(Return.Lstat){ .result = .{ .stats = .init(result, args.big_int) } }, .err => |err| brk: { if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { return Maybe(Return.Lstat){ .result = .{ .not_found = {} } }; } break :brk Maybe(Return.Lstat){ .err = err.withPath(args.path.slice()) }; }, }; } pub fn mkdir(this: *NodeFS, args: Arguments.Mkdir, _: Flavor) Maybe(Return.Mkdir) { if (args.path.slice().len == 0) return .{ .err = .{ .errno = @intFromEnum(bun.C.E.NOENT), .syscall = .mkdir, .path = "", } }; return if (args.recursive) mkdirRecursive(this, args) else mkdirNonRecursive(this, args); } // Node doesn't absolute the path so we don't have to either pub fn mkdirNonRecursive(this: *NodeFS, args: Arguments.Mkdir) Maybe(Return.Mkdir) { const path = args.path.sliceZ(&this.sync_error_buf); return switch (Syscall.mkdir(path, args.mode)) { .result => Maybe(Return.Mkdir){ .result = .{ .none = {} } }, .err => |err| Maybe(Return.Mkdir){ .err = err.withPath(args.path.slice()) }, }; } pub fn mkdirRecursive(this: *NodeFS, args: Arguments.Mkdir) Maybe(Return.Mkdir) { return mkdirRecursiveImpl(this, args, void, {}); } pub fn mkdirRecursiveImpl(this: *NodeFS, args: Arguments.Mkdir, comptime Ctx: type, ctx: Ctx) Maybe(Return.Mkdir) { const buf = bun.PathBufferPool.get(); defer bun.PathBufferPool.put(buf); const path = args.path.osPathKernel32(buf); return switch (args.always_return_none) { inline else => |always_return_none| this.mkdirRecursiveOSPathImpl(Ctx, ctx, path, args.mode, !always_return_none), }; } pub fn _isSep(char: bun.OSPathChar) bool { return if (Environment.isWindows) char == '/' or char == '\\' else char == '/'; } pub fn mkdirRecursiveOSPath(this: *NodeFS, path: bun.OSPathSliceZ, mode: Mode, comptime return_path: bool) Maybe(Return.Mkdir) { return mkdirRecursiveOSPathImpl(this, void, {}, path, mode, return_path); } pub fn mkdirRecursiveOSPathImpl( this: *NodeFS, comptime Ctx: type, ctx: Ctx, path: bun.OSPathSliceZ, mode: Mode, comptime return_path: bool, ) Maybe(Return.Mkdir) { const callbacks = struct { pub fn onCreateDir(c: Ctx, dirpath: bun.OSPathSliceZ) void { if (Ctx != void) { c.onCreateDir(dirpath); } return; } }; const Char = bun.OSPathChar; const len: u16 = @truncate(path.len); // First, attempt to create the desired directory // If that fails, then walk back up the path until we have a match switch (Syscall.mkdirOSPath(path, mode)) { .err => |err| { switch (err.getErrno()) { else => { return .{ .err = err.withPath(this.osPathIntoSyncErrorBuf(path[0..len])) }; }, // `mkpath_np` in macOS also checks for `EISDIR`. // it is unclear if macOS lies about if the existing item is // a directory or not, so it is checked. .ISDIR, // check if it was actually a directory or not. .EXIST, => return switch (bun.sys.directoryExistsAt(bun.invalid_fd, path)) { .err => .{ .err = .{ .errno = err.errno, .syscall = .mkdir, .path = this.osPathIntoSyncErrorBuf(strings.withoutNTPrefix(bun.OSPathChar, path[0..len])), } }, // if is a directory, OK. otherwise failure .result => |result| if (result) .{ .result = .{ .none = {} } } else .{ .err = .{ .errno = err.errno, .syscall = .mkdir, .path = this.osPathIntoSyncErrorBuf(strings.withoutNTPrefix(bun.OSPathChar, path[0..len])), } }, }, // continue .NOENT => { if (len == 0) { // no path to copy return .{ .err = err }; } }, } }, .result => { callbacks.onCreateDir(ctx, path); if (!return_path) { return .{ .result = .{ .none = {} } }; } return .{ .result = .{ .string = bun.String.createFromOSPath(strings.withoutNTPrefix(bun.OSPathChar, path)) }, }; }, } var working_mem: *bun.OSPathBuffer = @alignCast(@ptrCast(&this.sync_error_buf)); @memcpy(working_mem[0..len], path[0..len]); var i: u16 = len - 1; // iterate backwards until creating the directory works successfully while (i > 0) : (i -= 1) { if (_isSep(path[i])) { working_mem[i] = 0; const parent: [:0]Char = working_mem[0..i :0]; switch (Syscall.mkdirOSPath(parent, mode)) { .err => |err| { working_mem[i] = std.fs.path.sep; switch (err.getErrno()) { .EXIST => { // Handle race condition break; }, .NOENT => { continue; }, else => return .{ .err = err.withPath( if (Environment.isWindows) this.osPathIntoSyncErrorBufOverlap(strings.withoutNTPrefix(bun.OSPathChar, parent)) else strings.withoutNTPrefix(bun.OSPathChar, parent), ) }, } }, .result => { callbacks.onCreateDir(ctx, parent); // We found a parent that worked working_mem[i] = std.fs.path.sep; break; }, } } } const first_match: u16 = i; i += 1; // after we find one that works, we go forward _after_ the first working directory while (i < len) : (i += 1) { if (_isSep(path[i])) { working_mem[i] = 0; const parent: [:0]Char = working_mem[0..i :0]; switch (Syscall.mkdirOSPath(parent, mode)) { .err => |err| { working_mem[i] = std.fs.path.sep; switch (err.getErrno()) { // handle the race condition .EXIST => {}, // NOENT shouldn't happen here else => return .{ .err = err.withPath(this.osPathIntoSyncErrorBuf(strings.withoutNTPrefix(bun.OSPathChar, path))), }, } }, .result => { callbacks.onCreateDir(ctx, parent); working_mem[i] = std.fs.path.sep; }, } } } working_mem[len] = 0; // Our final directory will not have a trailing separator // so we have to create it once again switch (Syscall.mkdirOSPath(working_mem[0..len :0], mode)) { .err => |err| { switch (err.getErrno()) { // handle the race condition .EXIST => {}, // NOENT shouldn't happen here else => return .{ .err = err.withPath(this.osPathIntoSyncErrorBuf(strings.withoutNTPrefix(bun.OSPathChar, path))), }, } }, .result => {}, } callbacks.onCreateDir(ctx, working_mem[0..len :0]); if (!return_path) { return .{ .result = .{ .none = {} } }; } return .{ .result = .{ .string = bun.String.createFromOSPath(strings.withoutNTPrefix(bun.OSPathChar, working_mem[0..first_match])) }, }; } pub fn mkdtemp(this: *NodeFS, args: Arguments.MkdirTemp, _: Flavor) Maybe(Return.Mkdtemp) { var prefix_buf = &this.sync_error_buf; const prefix_slice = args.prefix.slice(); const len = @min(prefix_slice.len, prefix_buf.len -| 7); if (len > 0) { @memcpy(prefix_buf[0..len], prefix_slice[0..len]); } prefix_buf[len..][0..6].* = "XXXXXX".*; prefix_buf[len..][6] = 0; // The mkdtemp() function returns a pointer to the modified template // string on success, and NULL on failure, in which case errno is set to // indicate the error if (Environment.isWindows) { var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_mkdtemp(bun.Async.Loop.get(), &req, @ptrCast(prefix_buf.ptr), null); if (rc.errno()) |errno| { return .{ .err = .{ .errno = errno, .syscall = .mkdtemp, .path = prefix_buf[0 .. len + 6], } }; } return .{ .result = JSC.ZigString.dupeForJS(bun.sliceTo(req.path, 0), bun.default_allocator) catch bun.outOfMemory(), }; } const rc = C.mkdtemp(prefix_buf); if (rc) |ptr| { return .{ .result = JSC.ZigString.dupeForJS(bun.sliceTo(ptr, 0), bun.default_allocator) catch bun.outOfMemory(), }; } // bun.C.getErrno(rc) returns SUCCESS if rc is -1 so we call std.c._errno() directly const errno = @as(std.c.E, @enumFromInt(std.c._errno().*)); return .{ .err = Syscall.Error{ .errno = @as(Syscall.Error.Int, @truncate(@intFromEnum(errno))), .syscall = .mkdtemp, .path = prefix_buf[0 .. len + 6], }, }; } pub fn open(this: *NodeFS, args: Arguments.Open, _: Flavor) Maybe(Return.Open) { const path = if (Environment.isWindows and bun.strings.eqlComptime(args.path.slice(), "/dev/null")) "\\\\.\\NUL" else args.path.sliceZ(&this.sync_error_buf); return switch (Syscall.open(path, args.flags.asInt(), args.mode)) { .err => |err| .{ .err = err.withPath(args.path.slice()), }, .result => |fd| fd: { break :fd .{ .result = FDImpl.decode(fd) }; }, }; } pub fn uv_open(this: *NodeFS, args: Arguments.Open, rc: i64) Maybe(Return.Open) { _ = this; if (rc < 0) { return Maybe(Return.Open){ .err = .{ .errno = @intCast(-rc), .syscall = .open, .path = args.path.slice(), .from_libuv = true, } }; } return Maybe(Return.Open).initResult(FDImpl.decode(bun.toFD(@as(u32, @intCast(rc))))); } pub fn uv_statfs(_: *NodeFS, args: Arguments.StatFS, req: *uv.fs_t, rc: i64) Maybe(Return.StatFS) { if (rc < 0) { return Maybe(Return.StatFS){ .err = .{ .errno = @intCast(-rc), .syscall = .open, .path = args.path.slice(), .from_libuv = true, } }; } return Maybe(Return.StatFS).initResult(Return.StatFS.init(req.ptrAs(*align(1) bun.StatFS).*, args.big_int)); } pub fn openDir(_: *NodeFS, _: Arguments.OpenDir, _: Flavor) Maybe(Return.OpenDir) { return Maybe(Return.OpenDir).todo(); } fn readInner(_: *NodeFS, args: Arguments.Read) Maybe(Return.Read) { if (Environment.allow_assert) bun.assert(args.position == null); var buf = args.buffer.slice(); buf = buf[@min(args.offset, buf.len)..]; buf = buf[0..@min(buf.len, args.length)]; return switch (Syscall.read(args.fd, buf)) { .err => |err| .{ .err = err }, .result => |amt| .{ .result = .{ .bytes_read = @as(u52, @truncate(amt)), } }, }; } fn preadInner(_: *NodeFS, args: Arguments.Read) Maybe(Return.Read) { var buf = args.buffer.slice(); buf = buf[@min(args.offset, buf.len)..]; buf = buf[0..@min(buf.len, args.length)]; return switch (Syscall.pread(args.fd, buf, args.position.?)) { .err => |err| .{ .err = .{ .errno = err.errno, .fd = args.fd, .syscall = .read, } }, .result => |amt| .{ .result = .{ .bytes_read = @as(u52, @truncate(amt)), } }, }; } pub fn read(this: *NodeFS, args: Arguments.Read, _: Flavor) Maybe(Return.Read) { const len1 = args.buffer.slice().len; const len2 = args.length; if (len1 == 0 or len2 == 0) { return Maybe(Return.Read).initResult(.{ .bytes_read = 0 }); } return if (args.position != null) this.preadInner(args) else this.readInner(args); } pub fn uv_read(this: *NodeFS, args: Arguments.Read, rc: i64) Maybe(Return.Read) { _ = this; if (rc < 0) { return Maybe(Return.Read){ .err = .{ .errno = @intCast(-rc), .syscall = .read, .fd = args.fd, .from_libuv = true, } }; } return Maybe(Return.Read).initResult(.{ .bytes_read = @intCast(rc) }); } pub fn uv_readv(this: *NodeFS, args: Arguments.Readv, rc: i64) Maybe(Return.Readv) { _ = this; if (rc < 0) { return Maybe(Return.Readv){ .err = .{ .errno = @intCast(-rc), .syscall = .readv, .fd = args.fd, .from_libuv = true, } }; } return Maybe(Return.Readv).initResult(.{ .bytes_read = @intCast(rc) }); } pub fn readv(this: *NodeFS, args: Arguments.Readv, _: Flavor) Maybe(Return.Readv) { if (args.buffers.buffers.items.len == 0) { return .{ .result = .{ .bytes_read = 0 } }; } return if (args.position != null) preadvInner(this, args) else readvInner(this, args); } pub fn writev(this: *NodeFS, args: Arguments.Writev, _: Flavor) Maybe(Return.Writev) { if (args.buffers.buffers.items.len == 0) { return .{ .result = .{ .bytes_written = 0 } }; } return if (args.position != null) pwritevInner(this, args) else writevInner(this, args); } pub fn write(this: *NodeFS, args: Arguments.Write, _: Flavor) Maybe(Return.Write) { return if (args.position != null) pwriteInner(this, args) else writeInner(this, args); } pub fn uv_write(this: *NodeFS, args: Arguments.Write, rc: i64) Maybe(Return.Write) { _ = this; if (rc < 0) { return Maybe(Return.Write){ .err = .{ .errno = @intCast(-rc), .syscall = .write, .fd = args.fd, .from_libuv = true, } }; } return Maybe(Return.Write).initResult(.{ .bytes_written = @intCast(rc) }); } pub fn uv_writev(this: *NodeFS, args: Arguments.Writev, rc: i64) Maybe(Return.Writev) { _ = this; if (rc < 0) { return Maybe(Return.Writev){ .err = .{ .errno = @intCast(-rc), .syscall = .writev, .fd = args.fd, .from_libuv = true, } }; } return Maybe(Return.Writev).initResult(.{ .bytes_written = @intCast(rc) }); } fn writeInner(_: *NodeFS, args: Arguments.Write) Maybe(Return.Write) { var buf = args.buffer.slice(); buf = buf[@min(args.offset, buf.len)..]; buf = buf[0..@min(buf.len, args.length)]; return switch (Syscall.write(args.fd, buf)) { .err => |err| .{ .err = err, }, .result => |amt| .{ .result = .{ .bytes_written = @as(u52, @truncate(amt)), }, }, }; } fn pwriteInner(_: *NodeFS, args: Arguments.Write) Maybe(Return.Write) { const position = args.position.?; var buf = args.buffer.slice(); buf = buf[@min(args.offset, buf.len)..]; buf = buf[0..@min(args.length, buf.len)]; return switch (Syscall.pwrite(args.fd, buf, position)) { .err => |err| .{ .err = .{ .errno = err.errno, .fd = args.fd, .syscall = .write, } }, .result => |amt| .{ .result = .{ .bytes_written = @as(u52, @truncate(amt)), } }, }; } fn preadvInner(_: *NodeFS, args: Arguments.Readv) Maybe(Return.Readv) { const position = args.position.?; return switch (Syscall.preadv(args.fd, args.buffers.buffers.items, position)) { .err => |err| .{ .err = err, }, .result => |amt| .{ .result = .{ .bytes_read = @as(u52, @truncate(amt)), } }, }; } fn readvInner(_: *NodeFS, args: Arguments.Readv) Maybe(Return.Readv) { return switch (Syscall.readv(args.fd, args.buffers.buffers.items)) { .err => |err| .{ .err = err, }, .result => |amt| .{ .result = .{ .bytes_read = @as(u52, @truncate(amt)), } }, }; } fn pwritevInner(_: *NodeFS, args: Arguments.Writev) Maybe(Return.Write) { const position = args.position.?; return switch (Syscall.pwritev(args.fd, @ptrCast(args.buffers.buffers.items), position)) { .err => |err| .{ .err = err, }, .result => |amt| .{ .result = .{ .bytes_written = @as(u52, @truncate(amt)), } }, }; } fn writevInner(_: *NodeFS, args: Arguments.Writev) Maybe(Return.Write) { return switch (Syscall.writev(args.fd, @ptrCast(args.buffers.buffers.items))) { .err => |err| .{ .err = err, }, .result => |amt| .{ .result = .{ .bytes_written = @as(u52, @truncate(amt)), } }, }; } pub fn readdir(this: *NodeFS, args: Arguments.Readdir, comptime flavor: Flavor) Maybe(Return.Readdir) { if (comptime flavor != .sync) { if (args.recursive) { @panic("Assertion failure: this code path should never be reached."); } } const maybe = switch (args.recursive) { inline else => |recursive| switch (args.tag()) { .buffers => readdirInner(&this.sync_error_buf, args, Buffer, recursive, flavor), .with_file_types => readdirInner(&this.sync_error_buf, args, bun.JSC.Node.Dirent, recursive, flavor), .files => readdirInner(&this.sync_error_buf, args, bun.String, recursive, flavor), }, }; return switch (maybe) { .err => |err| .{ .err = .{ .syscall = .scandir, .errno = err.errno, .path = args.path.slice(), } }, .result => |result| .{ .result = result }, }; } fn readdirWithEntries( args: Arguments.Readdir, fd: bun.FileDescriptor, basename: [:0]const u8, comptime ExpectedType: type, entries: *std.ArrayList(ExpectedType), ) Maybe(void) { const dir = fd.asDir(); const is_u16 = comptime Environment.isWindows and (ExpectedType == bun.String or ExpectedType == bun.JSC.Node.Dirent); var dirent_path: bun.String = bun.String.dead; defer { dirent_path.deref(); } var iterator = DirIterator.iterate(dir, comptime if (is_u16) .u16 else .u8); var entry = iterator.next(); const re_encoding_buffer: ?*bun.PathBuffer = if (is_u16 and args.encoding != .utf8) bun.PathBufferPool.get() else null; defer if (is_u16 and args.encoding != .utf8) bun.PathBufferPool.put(re_encoding_buffer.?); while (switch (entry) { .err => |err| { for (entries.items) |*item| { switch (ExpectedType) { bun.JSC.Node.Dirent => { item.deref(); }, Buffer => { item.destroy(); }, bun.String => { item.deref(); }, else => @compileError("unreachable"), } } entries.deinit(); return .{ .err = err.withPath(args.path.slice()), }; }, .result => |ent| ent, }) |current| : (entry = iterator.next()) { if (ExpectedType == JSC.Node.Dirent) { if (dirent_path.isEmpty()) { dirent_path = JSC.WebCore.Encoder.toBunString(strings.withoutNTPrefix(std.meta.Child(@TypeOf(basename)), basename), args.encoding); } } if (comptime !is_u16) { const utf8_name = current.name.slice(); switch (ExpectedType) { JSC.Node.Dirent => { dirent_path.ref(); entries.append(.{ .name = JSC.WebCore.Encoder.toBunString(utf8_name, args.encoding), .path = dirent_path, .kind = current.kind, }) catch bun.outOfMemory(); }, Buffer => { entries.append(Buffer.fromString(utf8_name, bun.default_allocator) catch bun.outOfMemory()) catch bun.outOfMemory(); }, bun.String => { entries.append(JSC.WebCore.Encoder.toBunString(utf8_name, args.encoding)) catch bun.outOfMemory(); }, else => @compileError("unreachable"), } } else { const utf16_name = current.name.slice(); switch (ExpectedType) { JSC.Node.Dirent => { dirent_path.ref(); entries.append(.{ .name = bun.String.createUTF16(utf16_name), .path = dirent_path, .kind = current.kind, }) catch bun.outOfMemory(); }, bun.String => switch (args.encoding) { .buffer => unreachable, // in node.js, libuv converts to utf8 before node.js converts those bytes into other stuff // all encodings besides hex, base64, and base64url are mis-interpreting filesystem bytes. .utf8 => entries.append(bun.String.createUTF16(utf16_name)) catch bun.outOfMemory(), else => |enc| { const utf8_path = bun.strings.fromWPath(re_encoding_buffer.?, utf16_name); entries.append(JSC.WebCore.Encoder.toBunString(utf8_path, enc)) catch bun.outOfMemory(); }, }, else => @compileError("unreachable"), } } } return Maybe(void).success; } pub fn readdirWithEntriesRecursiveAsync( buf: *bun.PathBuffer, args: Arguments.Readdir, async_task: *AsyncReaddirRecursiveTask, basename: [:0]const u8, comptime ExpectedType: type, entries: *std.ArrayList(ExpectedType), comptime is_root: bool, ) Maybe(void) { const root_basename = async_task.root_path.slice(); const flags = bun.O.DIRECTORY | bun.O.RDONLY; const atfd = if (comptime is_root) bun.FD.cwd() else async_task.root_fd; const fd = switch (switch (Environment.os) { else => Syscall.openat(atfd, basename, flags, 0), // windows bun.sys.open does not pass iterable=true, .windows => bun.sys.openDirAtWindowsA(atfd, basename, .{ .no_follow = true, .iterable = true, .read_only = true }), }) { .err => |err| { if (comptime !is_root) { switch (err.getErrno()) { // These things can happen and there's nothing we can do about it. // // This is different than what Node does, at the time of writing. // Node doesn't gracefully handle errors like these. It fails the entire operation. .NOENT, .NOTDIR, .PERM => { return Maybe(void).success; }, else => {}, } const path_parts = [_]string{ root_basename, basename }; return .{ .err = err.withPath(bun.path.joinZBuf(buf, &path_parts, .auto)), }; } return .{ .err = err.withPath(args.path.slice()), }; }, .result => |fd_| fd_, }; if (comptime is_root) { async_task.root_fd = fd; } defer { if (comptime !is_root) { _ = Syscall.close(fd); } } var iterator = DirIterator.iterate(fd.asDir(), .u8); var entry = iterator.next(); var dirent_path_prev: bun.String = bun.String.empty; defer { dirent_path_prev.deref(); } while (switch (entry) { .err => |err| { if (comptime !is_root) { const path_parts = [_]string{ root_basename, basename }; return .{ .err = err.withPath(bun.path.joinZBuf(buf, &path_parts, .auto)), }; } return .{ .err = err.withPath(args.path.slice()), }; }, .result => |ent| ent, }) |current| : (entry = iterator.next()) { const utf8_name = current.name.slice(); const name_to_copy: [:0]const u8 = brk: { if (async_task.root_path.sliceAssumeZ().ptr == basename.ptr) { break :brk @ptrCast(utf8_name); } const path_parts = [_]string{ basename, utf8_name }; break :brk bun.path.joinZBuf(buf, &path_parts, .auto); }; enqueue: { switch (current.kind) { // a symlink might be a directory or might not be // if it's not a directory, the task will fail at that point. .sym_link, // we know for sure it's a directory .directory, => { // if the name is too long, we can't enqueue it regardless // the operating system would just return ENAMETOOLONG // // Technically, we could work around that due to the // usage of openat, but then we risk leaving too many // file descriptors open. if (current.name.len + 1 + name_to_copy.len > bun.MAX_PATH_BYTES) break :enqueue; async_task.enqueue(name_to_copy); }, else => {}, } } switch (comptime ExpectedType) { bun.JSC.Node.Dirent => { const path_u8 = bun.path.dirname(bun.path.join(&[_]string{ root_basename, name_to_copy }, .auto), .auto); if (dirent_path_prev.isEmpty() or !bun.strings.eql(dirent_path_prev.byteSlice(), path_u8)) { dirent_path_prev.deref(); dirent_path_prev = bun.String.createUTF8(path_u8); } dirent_path_prev.ref(); entries.append(.{ .name = bun.String.createUTF8(utf8_name), .path = dirent_path_prev, .kind = current.kind, }) catch bun.outOfMemory(); }, Buffer => { entries.append(Buffer.fromString(name_to_copy, bun.default_allocator) catch bun.outOfMemory()) catch bun.outOfMemory(); }, bun.String => { entries.append(bun.String.createUTF8(name_to_copy)) catch bun.outOfMemory(); }, else => bun.outOfMemory(), } } return Maybe(void).success; } fn readdirWithEntriesRecursiveSync( buf: *bun.PathBuffer, args: Arguments.Readdir, root_basename: [:0]const u8, comptime ExpectedType: type, entries: *std.ArrayList(ExpectedType), ) Maybe(void) { var iterator_stack = std.heap.stackFallback(128, bun.default_allocator); var stack = std.fifo.LinearFifo([:0]const u8, .{ .Dynamic = {} }).init(iterator_stack.get()); var basename_stack = std.heap.stackFallback(8192 * 2, bun.default_allocator); const basename_allocator = basename_stack.get(); defer { while (stack.readItem()) |name| { basename_allocator.free(name); } stack.deinit(); } stack.writeItem(root_basename) catch unreachable; var root_fd: bun.FileDescriptor = bun.invalid_fd; defer { // all other paths are relative to the root directory // so we can only close it once we're 100% done if (root_fd != bun.invalid_fd) { _ = Syscall.close(root_fd); } } while (stack.readItem()) |basename| { defer { if (root_basename.ptr != basename.ptr) { basename_allocator.free(basename); } } const flags = bun.O.DIRECTORY | bun.O.RDONLY; const fd = switch (Syscall.openat(if (root_fd == bun.invalid_fd) bun.FD.cwd() else root_fd, basename, flags, 0)) { .err => |err| { if (root_fd == bun.invalid_fd) { return .{ .err = err.withPath(args.path.slice()), }; } switch (err.getErrno()) { // These things can happen and there's nothing we can do about it. // // This is different than what Node does, at the time of writing. // Node doesn't gracefully handle errors like these. It fails the entire operation. .NOENT, .NOTDIR, .PERM => continue, else => { // const path_parts = [_]string{ args.path.slice(), basename }; // TODO: propagate file path (removed previously because it leaked the path) return .{ .err = err }; }, } }, .result => |fd_| fd_, }; if (root_fd == bun.invalid_fd) { root_fd = fd; } defer { if (fd != root_fd) { _ = Syscall.close(fd); } } var iterator = DirIterator.iterate(fd.asDir(), .u8); var entry = iterator.next(); var dirent_path_prev: bun.String = bun.String.dead; defer { dirent_path_prev.deref(); } while (switch (entry) { .err => |err| { return .{ .err = err.withPath(args.path.slice()), }; }, .result => |ent| ent, }) |current| : (entry = iterator.next()) { const utf8_name = current.name.slice(); const name_to_copy = brk: { if (root_basename.ptr == basename.ptr) { break :brk utf8_name; } const path_parts = [_]string{ basename, utf8_name }; break :brk bun.path.joinZBuf(buf, &path_parts, .auto); }; enqueue: { switch (current.kind) { // a symlink might be a directory or might not be // if it's not a directory, the task will fail at that point. .sym_link, // we know for sure it's a directory .directory, => { if (current.name.len + 1 + name_to_copy.len > bun.MAX_PATH_BYTES) break :enqueue; stack.writeItem(basename_allocator.dupeZ(u8, name_to_copy) catch break :enqueue) catch break :enqueue; }, else => {}, } } switch (comptime ExpectedType) { bun.JSC.Node.Dirent => { const path_u8 = bun.path.dirname(bun.path.join(&[_]string{ root_basename, name_to_copy }, .auto), .auto); if (dirent_path_prev.isEmpty() or !bun.strings.eql(dirent_path_prev.byteSlice(), path_u8)) { dirent_path_prev.deref(); dirent_path_prev = JSC.WebCore.Encoder.toBunString(strings.withoutNTPrefix(std.meta.Child(@TypeOf(path_u8)), path_u8), args.encoding); } dirent_path_prev.ref(); entries.append(.{ .name = JSC.WebCore.Encoder.toBunString(utf8_name, args.encoding), .path = dirent_path_prev, .kind = current.kind, }) catch bun.outOfMemory(); }, Buffer => { entries.append(Buffer.fromString(strings.withoutNTPrefix(std.meta.Child(@TypeOf(name_to_copy)), name_to_copy), bun.default_allocator) catch bun.outOfMemory()) catch bun.outOfMemory(); }, bun.String => { entries.append(JSC.WebCore.Encoder.toBunString(strings.withoutNTPrefix(std.meta.Child(@TypeOf(name_to_copy)), name_to_copy), args.encoding)) catch bun.outOfMemory(); }, else => @compileError(unreachable), } } } return Maybe(void).success; } fn shouldThrowOutOfMemoryEarlyForJavaScript(encoding: Encoding, size: usize, syscall: Syscall.Tag) ?Syscall.Error { // Strings & typed arrays max out at 4.7 GB. // But, it's **string length** // So you can load an 8 GB hex string, for example, it should be fine. const adjusted_size = switch (encoding) { .utf16le, .ucs2, .utf8 => size / 4 -| 1, .hex => size / 2 -| 1, .base64, .base64url => size / 3 -| 1, .ascii, .latin1, .buffer => size, }; if ( // Typed arrays in JavaScript are limited to 4.7 GB. adjusted_size > JSC.synthetic_allocation_limit or // If they do not have enough memory to open the file and they're on Linux, let's throw an error instead of dealing with the OOM killer. (Environment.isLinux and size >= bun.getTotalMemorySize())) { return Syscall.Error.fromCode(.NOMEM, syscall); } return null; } fn readdirInner( buf: *bun.PathBuffer, args: Arguments.Readdir, comptime ExpectedType: type, comptime recursive: bool, comptime flavor: Flavor, ) Maybe(Return.Readdir) { const file_type = switch (ExpectedType) { bun.JSC.Node.Dirent => "with_file_types", bun.String => "files", Buffer => "buffers", else => @compileError("unreachable"), }; const path = args.path.sliceZ(buf); if (comptime recursive and flavor == .sync) { var buf_to_pass: bun.PathBuffer = undefined; var entries = std.ArrayList(ExpectedType).init(bun.default_allocator); return switch (readdirWithEntriesRecursiveSync(&buf_to_pass, args, path, ExpectedType, &entries)) { .err => |err| { for (entries.items) |*result| { switch (ExpectedType) { bun.JSC.Node.Dirent => { result.name.deref(); }, Buffer => { result.destroy(); }, bun.String => { result.deref(); }, else => @compileError("unreachable"), } } entries.deinit(); return .{ .err = err, }; }, .result => .{ .result = @unionInit(Return.Readdir, file_type, entries.items) }, }; } if (comptime recursive) { @panic("This code path should never be reached. It should only go through readdirWithEntriesRecursiveAsync."); } const flags = bun.O.DIRECTORY | bun.O.RDONLY; const fd = switch (switch (Environment.os) { else => Syscall.open(path, flags, 0), // windows bun.sys.open does not pass iterable=true, .windows => bun.sys.openDirAtWindowsA(bun.FD.cwd(), path, .{ .iterable = true, .read_only = true }), }) { .err => |err| return .{ .err = err.withPath(args.path.slice()), }, .result => |fd_| fd_, }; defer _ = Syscall.close(fd); var entries = std.ArrayList(ExpectedType).init(bun.default_allocator); return switch (readdirWithEntries(args, fd, path, ExpectedType, &entries)) { .err => |err| return .{ .err = err, }, .result => .{ .result = @unionInit(Return.Readdir, file_type, entries.items) }, }; } pub const StringType = enum { default, null_terminated, }; pub fn readFile(this: *NodeFS, args: Arguments.ReadFile, comptime flavor: Flavor) Maybe(Return.ReadFile) { const ret = readFileWithOptions(this, args, flavor, .default); return switch (ret) { .err => .{ .err = ret.err }, .result => |result| switch (result) { .buffer => |buffer| .{ .result = .{ .buffer = buffer }, }, .transcoded_string => |str| { if (str.tag == .Dead) { return .{ .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path) }; } return .{ .result = .{ .string = .{ .underlying = str } } }; }, .string => brk: { const str = bun.SliceWithUnderlyingString.transcodeFromOwnedSlice(@constCast(ret.result.string), args.encoding); if (str.underlying.tag == .Dead and str.utf8.len == 0) { return .{ .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path) }; } break :brk .{ .result = .{ .string = str } }; }, else => unreachable, }, }; } pub fn readFileWithOptions(this: *NodeFS, args: Arguments.ReadFile, comptime flavor: Flavor, comptime string_type: StringType) Maybe(Return.ReadFileWithOptions) { var path: [:0]const u8 = undefined; const fd_maybe_windows: FileDescriptor = switch (args.path) { .path => brk: { path = args.path.path.sliceZ(&this.sync_error_buf); if (bun.StandaloneModuleGraph.get()) |graph| { if (graph.find(path)) |file| { if (args.encoding == .buffer) { return .{ .result = .{ .buffer = Buffer.fromBytes( bun.default_allocator.dupe(u8, file.contents) catch bun.outOfMemory(), bun.default_allocator, .Uint8Array, ), }, }; } else if (comptime string_type == .default) return .{ .result = .{ .string = bun.default_allocator.dupe(u8, file.contents) catch bun.outOfMemory(), }, } else return .{ .result = .{ .null_terminated = bun.default_allocator.dupeZ(u8, file.contents) catch bun.outOfMemory(), }, }; } } break :brk switch (bun.sys.open( path, args.flag.asInt() | bun.O.NOCTTY, default_permission, )) { .err => |err| return .{ .err = err.withPath(args.path.path.slice()), }, .result => |fd| fd, }; }, .fd => |fd| fd, }; const fd = bun.toLibUVOwnedFD(fd_maybe_windows) catch { if (args.path == .path) _ = Syscall.close(fd_maybe_windows); return .{ .err = .{ .errno = @intFromEnum(posix.E.MFILE), .syscall = .open, }, }; }; defer { if (args.path == .path) _ = Syscall.close(fd); } if (args.aborted()) return Maybe(Return.ReadFileWithOptions).aborted; // Only used in DOMFormData if (args.offset > 0) { _ = Syscall.setFileOffset(fd, args.offset); } var did_succeed = false; var total: usize = 0; var async_stack_buffer: [if (flavor == .sync) 0 else 256 * 1024]u8 = undefined; // --- Optimization: attempt to read up to 256 KB before calling stat() // If we manage to read the entire file, we don't need to call stat() at all. // This will make it slightly slower to read e.g. 512 KB files, but usually the OS won't return a full 512 KB in one read anyway. const temporary_read_buffer_before_stat_call = brk: { const temporary_read_buffer = temporary_read_buffer: { var temporary_read_buffer: []u8 = &async_stack_buffer; if (comptime flavor == .sync) { if (this.vm) |vm| { temporary_read_buffer = vm.rareData().pipeReadBuffer(); } } var available = temporary_read_buffer; while (available.len > 0) { switch (Syscall.read(fd, available)) { .err => |err| return .{ .err = err }, .result => |amt| { if (amt == 0) { did_succeed = true; break; } total += amt; available = available[amt..]; }, } } break :temporary_read_buffer temporary_read_buffer[0..total]; }; if (did_succeed) { switch (args.encoding) { .buffer => { if (comptime flavor == .sync and string_type == .default) { if (this.vm) |vm| { // Attempt to create the buffer in JSC's heap. // This avoids creating a WastefulTypedArray. const array_buffer = JSC.ArrayBuffer.createBuffer(vm.global, temporary_read_buffer); array_buffer.ensureStillAlive(); return .{ .result = .{ .buffer = JSC.MarkedArrayBuffer{ .buffer = array_buffer.asArrayBuffer(vm.global) orelse { // This case shouldn't really happen. return .{ .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path), }; }, }, }, }; } } return .{ .result = .{ .buffer = Buffer.fromBytes( bun.default_allocator.dupe(u8, temporary_read_buffer) catch return .{ .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path), }, bun.default_allocator, .Uint8Array, ), }, }; }, else => { if (comptime string_type == .default) { return .{ .result = .{ .transcoded_string = JSC.WebCore.Encoder.toBunString(temporary_read_buffer, args.encoding), }, }; } else { return .{ .result = .{ .null_terminated = bun.default_allocator.dupeZ(u8, temporary_read_buffer) catch return .{ .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path), }, }, }; } }, } } break :brk temporary_read_buffer; }; // ---------------------------- if (args.aborted()) return Maybe(Return.ReadFileWithOptions).aborted; const stat_ = switch (Syscall.fstat(fd)) { .err => |err| return .{ .err = err, }, .result => |stat_| stat_, }; // For certain files, the size might be 0 but the file might still have contents. // https://github.com/oven-sh/bun/issues/1220 const max_size = args.max_size orelse std.math.maxInt(JSC.WebCore.Blob.SizeType); const has_max_size = args.max_size != null; const size = @as( u64, @max( @min( stat_.size, // Only used in DOMFormData max_size, ), @as(i64, @intCast(total)), 0, ), ) + @intFromBool(comptime string_type == .null_terminated); if (args.limit_size_for_javascript and // assume that anything more than 40 bits is not trustworthy. (size < std.math.maxInt(u40))) { if (shouldThrowOutOfMemoryEarlyForJavaScript(args.encoding, size, .read)) |err| { return .{ .err = err.withPathLike(args.path) }; } } var buf = std.ArrayList(u8).init(bun.default_allocator); defer if (!did_succeed) buf.clearAndFree(); buf.ensureTotalCapacityPrecise( @min( @max(temporary_read_buffer_before_stat_call.len, size) + 16, max_size, 1024 * 1024 * 1024 * 8, ), ) catch return .{ .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path) }; if (temporary_read_buffer_before_stat_call.len > 0) { buf.appendSlice(temporary_read_buffer_before_stat_call) catch return .{ .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path), }; } buf.expandToCapacity(); while (total < size) { if (args.aborted()) return Maybe(Return.ReadFileWithOptions).aborted; switch (Syscall.read(fd, buf.items.ptr[total..@min(buf.capacity, max_size)])) { .err => |err| return .{ .err = err }, .result => |amt| { total += amt; if (args.limit_size_for_javascript) { if (shouldThrowOutOfMemoryEarlyForJavaScript(args.encoding, total, .read)) |err| { return .{ .err = err.withPathLike(args.path), }; } } // There are cases where stat()'s size is wrong or out of date if (total > size and amt != 0 and !has_max_size) { buf.items.len = total; buf.ensureUnusedCapacity(8192) catch { return .{ .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path) }; }; continue; } if (amt == 0) { did_succeed = true; break; } }, } } else { while (true) { if (args.aborted()) return Maybe(Return.ReadFileWithOptions).aborted; switch (Syscall.read(fd, buf.items.ptr[total..@min(buf.capacity, max_size)])) { .err => |err| return .{ .err = err }, .result => |amt| { total += amt; if (args.limit_size_for_javascript) { if (shouldThrowOutOfMemoryEarlyForJavaScript(args.encoding, total, .read)) |err| { return .{ .err = err.withPathLike(args.path) }; } } if (total > size and amt != 0 and !has_max_size) { buf.items.len = total; buf.ensureUnusedCapacity(8192) catch { return .{ .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path) }; }; continue; } if (amt == 0) { did_succeed = true; break; } }, } } } buf.items.len = if (comptime string_type == .null_terminated) total + 1 else total; if (total == 0) { buf.clearAndFree(); return switch (args.encoding) { .buffer => .{ .result = .{ .buffer = Buffer.empty, }, }, else => brk: { if (comptime string_type == .default) { break :brk .{ .result = .{ .string = "", }, }; } else { break :brk .{ .result = .{ .null_terminated = "", }, }; } }, }; } return switch (args.encoding) { .buffer => .{ .result = .{ .buffer = Buffer.fromBytes(buf.items, bun.default_allocator, .Uint8Array), }, }, else => brk: { if (comptime string_type == .default) { break :brk .{ .result = .{ .string = buf.items, }, }; } else { break :brk .{ .result = .{ .null_terminated = buf.toOwnedSliceSentinel(0) catch return .{ // Since we are expecting a null-terminated string, we can't just ignore the resize failure. .err = Syscall.Error.fromCode(.NOMEM, .read).withPathLike(args.path), }, }, }; } }, }; } pub fn writeFileWithPathBuffer(pathbuf: *bun.PathBuffer, args: Arguments.WriteFile) Maybe(Return.WriteFile) { const fd = switch (args.file) { .path => brk: { const path = args.file.path.sliceZWithForceCopy(pathbuf, true); const open_result = bun.sys.openat( args.dirfd, path, args.flag.asInt(), args.mode, ); break :brk switch (open_result) { .err => |err| return .{ .err = err.withPath(args.file.path.slice()), }, .result => |fd| fd, }; }, .fd => |fd| fd, }; defer { if (args.file == .path) _ = bun.sys.close(fd); } if (args.aborted()) return Maybe(Return.WriteFile).aborted; var buf = args.data.slice(); var written: usize = 0; // Attempt to pre-allocate large files if (Environment.isLinux) { preallocate: { // Worthwhile after 6 MB at least on ext4 linux if (buf.len >= bun.C.preallocate_length) { const offset: usize = if (args.file == .path) // on mac, it's relatively positioned 0 else brk: { // on linux, it's absolutely positione switch (Syscall.lseek( fd, @as(std.posix.off_t, @intCast(0)), std.os.linux.SEEK.CUR, )) { .err => break :preallocate, .result => |pos| break :brk @as(usize, @intCast(pos)), } }; bun.C.preallocate_file( fd.cast(), @as(std.posix.off_t, @intCast(offset)), @as(std.posix.off_t, @intCast(buf.len)), ) catch {}; } } } while (buf.len > 0) { switch (bun.sys.write(fd, buf)) { .err => |err| return .{ .err = err, }, .result => |amt| { buf = buf[amt..]; written += amt; if (amt == 0) { break; } }, } } // https://github.com/oven-sh/bun/issues/2931 // https://github.com/oven-sh/bun/issues/10222 // Only truncate if we're not appending and writing to a path if ((@intFromEnum(args.flag) & bun.O.APPEND) == 0 and args.file != .fd) { // If this errors, we silently ignore it. // Not all files are seekable (and thus, not all files can be truncated). if (Environment.isWindows) { _ = bun.windows.SetEndOfFile(fd.cast()); } else { _ = Syscall.ftruncate(fd, @intCast(@as(u63, @truncate(written)))); } } if (args.flush) { if (Environment.isWindows) { _ = std.os.windows.kernel32.FlushFileBuffers(fd.cast()); } else { _ = system.fsync(fd.cast()); } } return Maybe(Return.WriteFile).success; } pub fn writeFile(this: *NodeFS, args: Arguments.WriteFile, _: Flavor) Maybe(Return.WriteFile) { return writeFileWithPathBuffer(&this.sync_error_buf, args); } pub fn readlink(this: *NodeFS, args: Arguments.Readlink, _: Flavor) Maybe(Return.Readlink) { var outbuf: bun.PathBuffer = undefined; const inbuf = &this.sync_error_buf; const path = args.path.sliceZ(inbuf); const link_path = switch (Syscall.readlink(path, &outbuf)) { .err => |err| return .{ .err = err.withPath(args.path.slice()) }, .result => |result| result, }; return .{ .result = switch (args.encoding) { .buffer => .{ .buffer = Buffer.fromString(link_path, bun.default_allocator) catch unreachable, }, else => if (args.path == .slice_with_underlying_string and strings.eqlLong(args.path.slice_with_underlying_string.slice(), link_path, true)) .{ .string = args.path.slice_with_underlying_string.dupeRef(), } else .{ .string = .{ .utf8 = .{}, .underlying = bun.String.createUTF8(link_path) }, }, }, }; } pub fn realpathNonNative(this: *NodeFS, args: Arguments.Realpath, _: Flavor) Maybe(Return.Realpath) { return switch (this.realpathInner(args, .emulated)) { .result => |res| .{ .result = res }, .err => |err| .{ .err = .{ .errno = err.errno, .syscall = .lstat, .path = args.path.slice(), } }, }; } pub fn realpath(this: *NodeFS, args: Arguments.Realpath, _: Flavor) Maybe(Return.Realpath) { return switch (this.realpathInner(args, .native)) { .result => |res| .{ .result = res }, .err => |err| .{ .err = .{ .errno = err.errno, .syscall = .realpath, .path = args.path.slice(), }, }, }; } // For `fs.realpath`, Node.js uses `lstat`, exposing the native system call under // `fs.realpath.native`. In Bun, the system call is the default, but the error // code must be changed to make it seem like it is using lstat (tests expect this), // in addition, some more subtle things depend on the variant. pub fn realpathInner(this: *NodeFS, args: Arguments.Realpath, variant: enum { native, emulated }) Maybe(Return.Realpath) { if (Environment.isWindows) { var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_realpath(bun.Async.Loop.get(), &req, args.path.sliceZ(&this.sync_error_buf).ptr, null); if (rc.errno()) |errno| return .{ .err = Syscall.Error{ .errno = errno, .syscall = .realpath, .path = args.path.slice(), } }; var buf = bun.span(req.ptrAs([*:0]u8)); if (variant == .emulated) { // remove the trailing slash if (buf[buf.len - 1] == '\\') { buf[buf.len - 1] = 0; buf.len -= 1; } } return .{ .result = switch (args.encoding) { .buffer => .{ .buffer = Buffer.fromString(buf, bun.default_allocator) catch unreachable, }, .utf8 => utf8: { if (args.path == .slice_with_underlying_string) { const slice = args.path.slice_with_underlying_string; if (strings.eqlLong(slice.slice(), buf, true)) { return .{ .result = .{ .string = slice.dupeRef() } }; } } break :utf8 .{ .string = .{ .utf8 = .{}, .underlying = bun.String.createUTF8(buf) }, }; }, else => |enc| .{ .string = .{ .utf8 = .{}, .underlying = JSC.WebCore.Encoder.toBunString(buf, enc) }, }, }, }; } var outbuf: bun.PathBuffer = undefined; var inbuf = &this.sync_error_buf; if (comptime Environment.allow_assert) bun.assert(FileSystem.instance_loaded); const path_slice = args.path.slice(); var parts = [_]string{ FileSystem.instance.top_level_dir, path_slice }; const path_ = FileSystem.instance.absBuf(&parts, inbuf); inbuf[path_.len] = 0; const path: [:0]u8 = inbuf[0..path_.len :0]; const flags = if (comptime Environment.isLinux) // O_PATH is faster bun.O.PATH else bun.O.RDONLY; const fd = switch (bun.sys.open(path, flags, 0)) { .err => |err| return .{ .err = err.withPath(path) }, .result => |fd_| fd_, }; defer { _ = Syscall.close(fd); } const buf = switch (Syscall.getFdPath(fd, &outbuf)) { .err => |err| return .{ .err = err.withPath(path) }, .result => |buf_| buf_, }; return .{ .result = switch (args.encoding) { .buffer => .{ .buffer = Buffer.fromString(buf, bun.default_allocator) catch unreachable, }, .utf8 => utf8: { if (args.path == .slice_with_underlying_string) { const slice = args.path.slice_with_underlying_string; if (strings.eqlLong(slice.slice(), buf, true)) { return .{ .result = .{ .string = slice.dupeRef() } }; } } break :utf8 .{ .string = .{ .utf8 = .{}, .underlying = bun.String.createUTF8(buf) }, }; }, else => |enc| .{ .string = .{ .utf8 = .{}, .underlying = JSC.WebCore.Encoder.toBunString(buf, enc) }, }, }, }; } pub const realpathNative = realpath; pub fn rename(this: *NodeFS, args: Arguments.Rename, _: Flavor) Maybe(Return.Rename) { const from_buf = &this.sync_error_buf; var to_buf: bun.PathBuffer = undefined; const from = args.old_path.sliceZ(from_buf); const to = args.new_path.sliceZ(&to_buf); return switch (Syscall.rename(from, to)) { .result => |result| .{ .result = result }, .err => |err| .{ .err = err.withPathDest(args.old_path.slice(), args.new_path.slice()) }, }; } pub fn rmdir(this: *NodeFS, args: Arguments.RmDir, _: Flavor) Maybe(Return.Rmdir) { if (args.recursive) { zigDeleteTree(std.fs.cwd(), args.path.slice(), .directory) catch |err| { var errno: bun.C.E = switch (@as(anyerror, err)) { error.AccessDenied => .PERM, error.FileTooBig => .FBIG, error.SymLinkLoop => .LOOP, error.ProcessFdQuotaExceeded => .NFILE, error.NameTooLong => .NAMETOOLONG, error.SystemFdQuotaExceeded => .MFILE, error.SystemResources => .NOMEM, error.ReadOnlyFileSystem => .ROFS, error.FileSystem => .IO, error.FileBusy => .BUSY, error.DeviceBusy => .BUSY, // One of the path components was not a directory. // This error is unreachable if `sub_path` does not contain a path separator. error.NotDir => .NOTDIR, // On Windows, file paths must be valid Unicode. error.InvalidUtf8 => .INVAL, error.InvalidWtf8 => .INVAL, // On Windows, file paths cannot contain these characters: // '/', '*', '?', '"', '<', '>', '|' error.BadPathName => .INVAL, error.FileNotFound => .NOENT, error.IsDir => .ISDIR, else => .FAULT, }; if (Environment.isWindows and errno == .NOTDIR) { errno = .NOENT; } return Maybe(Return.Rm){ .err = bun.sys.Error.fromCode(errno, .rmdir), }; }; return Maybe(Return.Rmdir).success; } if (comptime Environment.isWindows) { return switch (Syscall.rmdir(args.path.sliceZ(&this.sync_error_buf))) { .err => |err| .{ .err = err.withPath(args.path.slice()) }, .result => |result| .{ .result = result }, }; } return Maybe(Return.Rmdir).errnoSysP(system.rmdir(args.path.sliceZ(&this.sync_error_buf)), .rmdir, args.path.slice()) orelse Maybe(Return.Rmdir).success; } pub fn rm(this: *NodeFS, args: Arguments.Rm, _: Flavor) Maybe(Return.Rm) { // We cannot use removefileat() on macOS because it does not handle write-protected files as expected. if (args.recursive) { zigDeleteTree(std.fs.cwd(), args.path.slice(), .file) catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); const errno: E = switch (@as(anyerror, err)) { // error.InvalidHandle => .BADF, error.AccessDenied => .ACCES, error.FileTooBig => .FBIG, error.SymLinkLoop => .LOOP, error.ProcessFdQuotaExceeded => .NFILE, error.NameTooLong => .NAMETOOLONG, error.SystemFdQuotaExceeded => .MFILE, error.SystemResources => .NOMEM, error.ReadOnlyFileSystem => .ROFS, error.FileSystem => .IO, error.FileBusy => .BUSY, error.DeviceBusy => .BUSY, // One of the path components was not a directory. // This error is unreachable if `sub_path` does not contain a path separator. error.NotDir => .NOTDIR, // On Windows, file paths must be valid WTF-8. error.InvalidUtf8 => .INVAL, error.InvalidWtf8 => .INVAL, // On Windows, file paths cannot contain these characters: // '/', '*', '?', '"', '<', '>', '|' error.BadPathName => .INVAL, error.FileNotFound => brk: { if (args.force) { return Maybe(Return.Rm).success; } break :brk .NOENT; }, error.IsDir => .ISDIR, else => .FAULT, }; return Maybe(Return.Rm){ .err = bun.sys.Error.fromCode(errno, .rm).withPath(args.path.slice()), }; }; return Maybe(Return.Rm).success; } const dest = args.path.sliceZ(&this.sync_error_buf); std.posix.unlinkZ(dest) catch |err1| { bun.handleErrorReturnTrace(err1, @errorReturnTrace()); // empircally, it seems to return AccessDenied when the // file is actually a directory on macOS. if (args.recursive and (err1 == error.IsDir or err1 == error.NotDir or err1 == error.AccessDenied)) { std.posix.rmdirZ(dest) catch |err2| { bun.handleErrorReturnTrace(err2, @errorReturnTrace()); const code: E = switch (err2) { error.AccessDenied => .ACCES, error.SymLinkLoop => .LOOP, error.NameTooLong => .NAMETOOLONG, error.SystemResources => .NOMEM, error.ReadOnlyFileSystem => .ROFS, error.FileBusy => .BUSY, error.FileNotFound => brk: { if (args.force) { return Maybe(Return.Rm).success; } break :brk .NOENT; }, error.InvalidUtf8 => .INVAL, error.InvalidWtf8 => .INVAL, error.BadPathName => .INVAL, else => .FAULT, }; return .{ .err = bun.sys.Error.fromCode(code, .rm).withPath(args.path.slice()), }; }; return Maybe(Return.Rm).success; } { const code: E = switch (err1) { error.AccessDenied => .ACCES, error.SymLinkLoop => .LOOP, error.NameTooLong => .NAMETOOLONG, error.SystemResources => .NOMEM, error.ReadOnlyFileSystem => .ROFS, error.FileBusy => .BUSY, error.InvalidUtf8 => .INVAL, error.InvalidWtf8 => .INVAL, error.BadPathName => .INVAL, error.FileNotFound => brk: { if (args.force) { return Maybe(Return.Rm).success; } break :brk .NOENT; }, else => .FAULT, }; return .{ .err = bun.sys.Error.fromCode(code, .rm).withPath(args.path.slice()), }; } }; return Maybe(Return.Rm).success; } pub fn statfs(this: *NodeFS, args: Arguments.StatFS, _: Flavor) Maybe(Return.StatFS) { return switch (Syscall.statfs(args.path.sliceZ(&this.sync_error_buf))) { .result => |result| Maybe(Return.StatFS){ .result = Return.StatFS.init(result, args.big_int) }, .err => |err| Maybe(Return.StatFS){ .err = err }, }; } pub fn stat(this: *NodeFS, args: Arguments.Stat, _: Flavor) Maybe(Return.Stat) { const path = args.path.sliceZ(&this.sync_error_buf); if (bun.StandaloneModuleGraph.get()) |graph| { if (graph.stat(path)) |result| { return .{ .result = .{ .stats = .init(result, args.big_int) } }; } } return switch (Syscall.stat(path)) { .result => |result| .{ .result = .{ .stats = .init(result, args.big_int) }, }, .err => |err| brk: { if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { return .{ .result = .{ .not_found = {} } }; } break :brk .{ .err = err.withPath(args.path.slice()) }; }, }; } pub fn symlink(this: *NodeFS, args: Arguments.Symlink, _: Flavor) Maybe(Return.Symlink) { var to_buf: bun.PathBuffer = undefined; if (Environment.isWindows) { const target_path = args.target_path.slice(); const new_path = args.new_path.slice(); // Note: to_buf and sync_error_buf hold intermediate states, but the // ending state is: // - new_path is in &sync_error_buf // - target_path is in &to_buf // Stat target if unspecified. const resolved_link_type: enum { file, dir, junction } = switch (args.link_type) { .unspecified => auto_detect: { const src = bun.path.joinAbsStringBuf( bun.getcwd(&to_buf) catch @panic("failed to resolve current working directory"), &this.sync_error_buf, &.{ bun.Dirname.dirname(u8, new_path) orelse new_path, target_path, }, .windows, ); break :auto_detect switch (bun.sys.directoryExistsAt(bun.invalid_fd, src)) { .err => .file, .result => |is_dir| if (is_dir) .dir else .file, }; }, .file => .file, .dir => .dir, .junction => .junction, }; // preprocessSymlinkDestination // - junctions: make absolute with long path prefix // - absolute paths: add long path prefix // - all: no forward slashes const processed_target: [:0]u8 = target: { if (resolved_link_type == .junction) { // this is similar to the `const src` above, but these cases // are mutually exclusive, so it isn't repeating any work. const target = bun.path.joinAbsStringBuf( bun.getcwd(&to_buf) catch @panic("failed to resolve current working directory"), this.sync_error_buf[4..], &.{ bun.Dirname.dirname(u8, new_path) orelse new_path, target_path, }, .windows, ); this.sync_error_buf[0..4].* = bun.windows.long_path_prefix_u8; this.sync_error_buf[4 + target.len] = 0; break :target this.sync_error_buf[0 .. 4 + target.len :0]; } if (std.fs.path.isAbsolute(target_path)) { // This normalizes slashes and adds the long path prefix break :target args.target_path.sliceZWithForceCopy(&this.sync_error_buf, true); } @memcpy(this.sync_error_buf[0..target_path.len], target_path); this.sync_error_buf[target_path.len] = 0; const target_path_z = this.sync_error_buf[0..target_path.len :0]; bun.path.dangerouslyConvertPathToWindowsInPlace(u8, target_path_z); break :target target_path_z; }; return switch (Syscall.symlinkUV( processed_target, args.new_path.sliceZ(&to_buf), switch (resolved_link_type) { .file => 0, .dir => uv.UV_FS_SYMLINK_DIR, .junction => uv.UV_FS_SYMLINK_JUNCTION, }, )) { .err => |err| .{ .err = err.withPathDest(args.target_path.slice(), args.new_path.slice()) }, .result => |result| .{ .result = result }, }; } return switch (Syscall.symlink( args.target_path.sliceZ(&this.sync_error_buf), args.new_path.sliceZ(&to_buf), )) { .result => |result| .{ .result = result }, .err => |err| .{ .err = err.withPathDest(args.target_path.slice(), args.new_path.slice()) }, }; } fn truncateInner(this: *NodeFS, path: PathLike, len: u63, flags: i32) Maybe(Return.Truncate) { if (comptime Environment.isWindows) { const file = bun.sys.open( path.sliceZ(&this.sync_error_buf), bun.O.WRONLY | flags, 0o644, ); if (file == .err) { return .{ .err = .{ .errno = file.err.errno, .path = path.slice(), .syscall = .truncate, } }; } defer _ = Syscall.close(file.result); const ret = Syscall.ftruncate(file.result, len); return switch (ret) { .result => ret, .err => |err| .{ .err = err.withPathAndSyscall(path.slice(), .truncate) }, }; } return Maybe(Return.Truncate).errnoSysP(C.truncate(path.sliceZ(&this.sync_error_buf), len), .truncate, path.slice()) orelse Maybe(Return.Truncate).success; } pub fn truncate(this: *NodeFS, args: Arguments.Truncate, _: Flavor) Maybe(Return.Truncate) { return switch (args.path) { .fd => |fd| Syscall.ftruncate(fd, args.len), .path => this.truncateInner( args.path.path, args.len, args.flags, ), }; } pub fn unlink(this: *NodeFS, args: Arguments.Unlink, _: Flavor) Maybe(Return.Unlink) { if (Environment.isWindows) { return switch (Syscall.unlink(args.path.sliceZ(&this.sync_error_buf))) { .err => |err| .{ .err = err.withPath(args.path.slice()) }, .result => |result| .{ .result = result }, }; } return Maybe(Return.Unlink).errnoSysP(system.unlink(args.path.sliceZ(&this.sync_error_buf)), .unlink, args.path.slice()) orelse Maybe(Return.Unlink).success; } pub fn watchFile(_: *NodeFS, args: Arguments.WatchFile, comptime flavor: Flavor) Maybe(Return.WatchFile) { bun.assert(flavor == .sync); const watcher = args.createStatWatcher() catch |err| { const buf = std.fmt.allocPrint(bun.default_allocator, "Failed to watch file {}", .{bun.fmt.QuotedFormatter{ .text = args.path.slice() }}) catch bun.outOfMemory(); defer bun.default_allocator.free(buf); args.global_this.throwValue((JSC.SystemError{ .message = bun.String.init(buf), .code = bun.String.init(@errorName(err)), .path = bun.String.init(args.path.slice()), }).toErrorInstance(args.global_this)) catch {}; return Maybe(Return.Watch){ .result = JSC.JSValue.undefined }; }; return Maybe(Return.Watch){ .result = watcher }; } pub fn unwatchFile(_: *NodeFS, _: Arguments.UnwatchFile, _: Flavor) Maybe(Return.UnwatchFile) { return Maybe(Return.UnwatchFile).todo(); } pub fn utimes(this: *NodeFS, args: Arguments.Utimes, _: Flavor) Maybe(Return.Utimes) { if (comptime Environment.isWindows) { var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_utime( bun.Async.Loop.get(), &req, args.path.sliceZ(&this.sync_error_buf).ptr, args.atime, args.mtime, null, ); return if (rc.errno()) |errno| .{ .err = Syscall.Error{ .errno = errno, .syscall = .utime, .path = args.path.slice(), } } else Maybe(Return.Utimes).success; } bun.assert(args.mtime.nsec <= 1e9); bun.assert(args.atime.nsec <= 1e9); return switch (Syscall.utimens( args.path.sliceZ(&this.sync_error_buf), args.atime, args.mtime, )) { .err => |err| .{ .err = err.withPath(args.path.slice()) }, .result => Maybe(Return.Utimes).success, }; } pub fn lutimes(this: *NodeFS, args: Arguments.Lutimes, _: Flavor) Maybe(Return.Lutimes) { if (comptime Environment.isWindows) { var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_lutime( bun.Async.Loop.get(), &req, args.path.sliceZ(&this.sync_error_buf).ptr, args.atime, args.mtime, null, ); return if (rc.errno()) |errno| .{ .err = Syscall.Error{ .errno = errno, .syscall = .utime, .path = args.path.slice(), } } else Maybe(Return.Utimes).success; } bun.assert(args.mtime.nsec <= 1e9); bun.assert(args.atime.nsec <= 1e9); return switch (Syscall.lutimes(args.path.sliceZ(&this.sync_error_buf), args.atime, args.mtime)) { .err => |err| .{ .err = err.withPath(args.path.slice()) }, .result => Maybe(Return.Lutimes).success, }; } pub fn watch(_: *NodeFS, args: Arguments.Watch, _: Flavor) Maybe(Return.Watch) { return switch (args.createFSWatcher()) { .result => |result| .{ .result = result.js_this }, .err => |err| .{ .err = .{ .errno = err.errno, .syscall = err.syscall, .path = if (err.path.len > 0) args.path.slice() else "", } }, }; } /// This function is `cpSync`, but only if you pass `{ recursive: ..., force: ..., errorOnExist: ..., mode: ... }' /// The other options like `filter` use a JS fallback, see `src/js/internal/fs/cp.ts` pub fn cp(this: *NodeFS, args: Arguments.Cp, _: Flavor) Maybe(Return.Cp) { var src_buf: bun.OSPathBuffer = undefined; var dest_buf: bun.OSPathBuffer = undefined; const src = args.src.osPath(&src_buf); const dest = args.dest.osPath(&dest_buf); return this.cpSyncInner(&src_buf, @intCast(src.len), &dest_buf, @intCast(dest.len), args); } pub fn osPathIntoSyncErrorBuf(this: *NodeFS, slice: anytype) []const u8 { if (Environment.isWindows) { return bun.strings.fromWPath(&this.sync_error_buf, slice); } else { @memcpy(this.sync_error_buf[0..slice.len], slice); return this.sync_error_buf[0..slice.len]; } } pub fn osPathIntoSyncErrorBufOverlap(this: *NodeFS, slice: anytype) []const u8 { if (Environment.isWindows) { const tmp = bun.OSPathBufferPool.get(); defer bun.OSPathBufferPool.put(tmp); @memcpy(tmp[0..slice.len], slice); return bun.strings.fromWPath(&this.sync_error_buf, tmp[0..slice.len]); } } fn cpSyncInner( this: *NodeFS, src_buf: *bun.OSPathBuffer, src_dir_len: PathString.PathInt, dest_buf: *bun.OSPathBuffer, dest_dir_len: PathString.PathInt, args: Arguments.Cp, ) Maybe(Return.Cp) { const cp_flags = args.flags; const src = src_buf[0..src_dir_len :0]; const dest = dest_buf[0..dest_dir_len :0]; if (Environment.isWindows) { const attributes = windows.GetFileAttributesW(src); if (attributes == windows.INVALID_FILE_ATTRIBUTES) { return .{ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOENT), .syscall = .copyfile, .path = this.osPathIntoSyncErrorBuf(src), } }; } if ((attributes & windows.FILE_ATTRIBUTE_DIRECTORY) == 0) { const r = this._copySingleFileSync( src, dest, @enumFromInt(if (cp_flags.errorOnExist or !cp_flags.force) Constants.COPYFILE_EXCL else @as(u8, 0)), attributes, args, ); if (r == .err and r.err.errno == @intFromEnum(E.EXIST) and !cp_flags.errorOnExist) { return Maybe(Return.Cp).success; } return r; } } else { const stat_ = switch (Syscall.lstat(src)) { .result => |result| result, .err => |err| { @memcpy(this.sync_error_buf[0..src.len], src); return .{ .err = err.withPath(this.sync_error_buf[0..src.len]) }; }, }; if (!posix.S.ISDIR(stat_.mode)) { const r = this._copySingleFileSync( src, dest, @enumFromInt((if (cp_flags.errorOnExist or !cp_flags.force) Constants.COPYFILE_EXCL else @as(u8, 0))), stat_, args, ); if (r == .err and r.err.errno == @intFromEnum(E.EXIST) and !cp_flags.errorOnExist) { return Maybe(Return.Cp).success; } return r; } } if (!cp_flags.recursive) { return .{ .err = .{ .errno = @intFromEnum(E.ISDIR), .syscall = .copyfile, .path = this.osPathIntoSyncErrorBuf(src), } }; } if (comptime Environment.isMac) try_with_clonefile: { if (Maybe(Return.Cp).errnoSysP(C.clonefile(src, dest, 0), .clonefile, src)) |err| { switch (err.getErrno()) { .NAMETOOLONG, .ROFS, .INVAL, .ACCES, .PERM => |errno| { if (errno == .ACCES or errno == .PERM) { if (args.flags.force) { break :try_with_clonefile; } } @memcpy(this.sync_error_buf[0..src.len], src); return .{ .err = err.err.withPath(this.sync_error_buf[0..src.len]) }; }, // Other errors may be due to clonefile() not being supported // We'll fall back to other implementations else => {}, } } else { return Maybe(Return.Cp).success; } } const fd = switch (Syscall.openatOSPath( bun.toFD((std.fs.cwd().fd)), src, bun.O.DIRECTORY | bun.O.RDONLY, 0, )) { .err => |err| { return .{ .err = err.withPath(this.osPathIntoSyncErrorBuf(src)) }; }, .result => |fd_| fd_, }; defer _ = Syscall.close(fd); switch (this.mkdirRecursiveOSPath(dest, Arguments.Mkdir.DefaultMode, false)) { .err => |err| return .{ .err = err }, .result => {}, } var iterator = iterator: { const dir = fd.asDir(); break :iterator DirIterator.iterate(dir, if (Environment.isWindows) .u16 else .u8); }; var entry = iterator.next(); while (switch (entry) { .err => |err| { return .{ .err = err.withPath(this.osPathIntoSyncErrorBuf(src)) }; }, .result => |ent| ent, }) |current| : (entry = iterator.next()) { const name_slice = current.name.slice(); @memcpy(src_buf[src_dir_len + 1 .. src_dir_len + 1 + name_slice.len], name_slice); src_buf[src_dir_len] = std.fs.path.sep; src_buf[src_dir_len + 1 + name_slice.len] = 0; @memcpy(dest_buf[dest_dir_len + 1 .. dest_dir_len + 1 + name_slice.len], name_slice); dest_buf[dest_dir_len] = std.fs.path.sep; dest_buf[dest_dir_len + 1 + name_slice.len] = 0; switch (current.kind) { .directory => { const r = this.cpSyncInner( src_buf, src_dir_len + @as(PathString.PathInt, @intCast(1 + name_slice.len)), dest_buf, dest_dir_len + @as(PathString.PathInt, @intCast(1 + name_slice.len)), args, ); switch (r) { .err => return r, .result => {}, } }, else => { const r = this._copySingleFileSync( src_buf[0 .. src_dir_len + 1 + name_slice.len :0], dest_buf[0 .. dest_dir_len + 1 + name_slice.len :0], @enumFromInt((if (cp_flags.errorOnExist or !cp_flags.force) Constants.COPYFILE_EXCL else @as(u8, 0))), null, args, ); switch (r) { .err => { if (r.err.errno == @intFromEnum(E.EXIST) and !cp_flags.errorOnExist) { continue; } return r; }, .result => {}, } }, } } return Maybe(Return.Cp).success; } /// On Windows, copying a file onto itself will return EBUSY, which is an /// unintuitive and cryptic error to return to the user for an operation /// that should seemingly be a no-op. /// /// So we check if the source and destination are the same file, and if they /// are, we return success. /// /// This is copied directly from libuv's implementation of `uv_fs_copyfile` /// for Windows: /// /// https://github.com/libuv/libuv/blob/497f3168d13ea9a92ad18c28e8282777ec2acf73/src/win/fs.c#L2069 /// /// **This function does nothing on non-Windows platforms**. fn shouldIgnoreEbusy(src: PathLike, dest: PathLike, result: Maybe(Return.CopyFile)) Maybe(Return.CopyFile) { if (comptime !Environment.isWindows) return result; if (result != .err or result.err.getErrno() != .BUSY) return result; var buf: bun.PathBuffer = undefined; const statbuf = switch (Syscall.stat(src.sliceZ(&buf))) { .result => |b| b, .err => return result, }; const new_statbuf = switch (Syscall.stat(dest.sliceZ(&buf))) { .result => |b| b, .err => return result, }; if (statbuf.dev == new_statbuf.dev and statbuf.ino == new_statbuf.ino) { return Maybe(Return.CopyFile).success; } return result; } /// This is `copyFile`, but it copies symlinks as-is pub fn _copySingleFileSync( this: *NodeFS, src: bun.OSPathSliceZ, dest: bun.OSPathSliceZ, mode: Constants.Copyfile, /// Stat on posix, file attributes on windows reuse_stat: ?if (Environment.isWindows) windows.DWORD else std.posix.Stat, args: Arguments.Cp, ) Maybe(Return.CopyFile) { const ret = Maybe(Return.CopyFile); // TODO: do we need to fchown? if (Environment.isMac) { if (mode.isForceClone()) { // https://www.manpagez.com/man/2/clonefile/ return ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) orelse ret.success; } else { const stat_ = reuse_stat orelse switch (Syscall.lstat(src)) { .result => |result| result, .err => |err| { @memcpy(this.sync_error_buf[0..src.len], src); return .{ .err = err.withPath(this.sync_error_buf[0..src.len]) }; }, }; if (!posix.S.ISREG(stat_.mode)) { if (posix.S.ISLNK(stat_.mode)) { var mode_: u32 = C.darwin.COPYFILE_ACL | C.darwin.COPYFILE_DATA | C.darwin.COPYFILE_NOFOLLOW_SRC; if (mode.shouldntOverwrite()) { mode_ |= C.darwin.COPYFILE_EXCL; } return ret.errnoSysP(C.copyfile(src, dest, null, mode_), .copyfile, src) orelse ret.success; } @memcpy(this.sync_error_buf[0..src.len], src); return Maybe(Return.CopyFile){ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOTSUP), .path = this.sync_error_buf[0..src.len], .syscall = .copyfile, } }; } // 64 KB is about the break-even point for clonefile() to be worth it // at least, on an M1 with an NVME SSD. if (stat_.size > 128 * 1024) { if (!mode.shouldntOverwrite()) { // clonefile() will fail if it already exists _ = Syscall.unlink(dest); } if (ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) == null) { _ = Syscall.chmod(dest, stat_.mode); return ret.success; } } else { const src_fd = switch (Syscall.open(src, bun.O.RDONLY, 0o644)) { .result => |result| result, .err => |err| { @memcpy(this.sync_error_buf[0..src.len], src); return .{ .err = err.withPath(this.sync_error_buf[0..src.len]) }; }, }; defer { _ = Syscall.close(src_fd); } var flags: Mode = bun.O.CREAT | bun.O.WRONLY; var wrote: usize = 0; if (mode.shouldntOverwrite()) { flags |= bun.O.EXCL; } const dest_fd = dest_fd: { switch (Syscall.open(dest, flags, JSC.Node.default_permission)) { .result => |result| break :dest_fd result, .err => |err| { if (err.getErrno() == .NOENT) { // Create the parent directory if it doesn't exist var len = dest.len; while (len > 0 and dest[len - 1] != std.fs.path.sep) { len -= 1; } const mkdirResult = this.mkdirRecursive(.{ .path = PathLike{ .string = PathString.init(dest[0..len]) }, .recursive = true, }); if (mkdirResult == .err) { return Maybe(Return.CopyFile){ .err = mkdirResult.err }; } switch (Syscall.open(dest, flags, JSC.Node.default_permission)) { .result => |result| break :dest_fd result, .err => {}, } } @memcpy(this.sync_error_buf[0..dest.len], dest); return Maybe(Return.CopyFile){ .err = err.withPath(this.sync_error_buf[0..dest.len]) }; }, } }; defer { _ = Syscall.ftruncate(dest_fd, @intCast(@as(u63, @truncate(wrote)))); _ = Syscall.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); } return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, @intCast(@max(stat_.size, 0)), &wrote); } } // we fallback to copyfile() when the file is > 128 KB and clonefile fails // clonefile() isn't supported on all devices // nor is it supported across devices var mode_: u32 = C.darwin.COPYFILE_ACL | C.darwin.COPYFILE_DATA | C.darwin.COPYFILE_NOFOLLOW_SRC; if (mode.shouldntOverwrite()) { mode_ |= C.darwin.COPYFILE_EXCL; } const first_try = ret.errnoSysP(C.copyfile(src, dest, null, mode_), .copyfile, src) orelse return ret.success; if (first_try == .err and first_try.err.errno == @intFromEnum(C.E.NOENT)) { bun.makePath(std.fs.cwd(), bun.path.dirname(dest, .auto)) catch {}; return ret.errnoSysP(C.copyfile(src, dest, null, mode_), .copyfile, src) orelse ret.success; } return first_try; } if (Environment.isLinux) { // https://manpages.debian.org/testing/manpages-dev/ioctl_ficlone.2.en.html if (mode.isForceClone()) { return Maybe(Return.CopyFile).todo(); } const src_fd = switch (Syscall.open(src, bun.O.RDONLY | bun.O.NOFOLLOW, 0o644)) { .result => |result| result, .err => |err| { if (err.getErrno() == .LOOP) { // ELOOP is returned when you open a symlink with NOFOLLOW. // as in, it does not actually let you open it. return Syscall.symlink(src, dest); } return .{ .err = err }; }, }; defer { _ = Syscall.close(src_fd); } const stat_: linux.Stat = switch (Syscall.fstat(src_fd)) { .result => |result| result, .err => |err| return Maybe(Return.CopyFile){ .err = err.withFd(src_fd) }, }; if (!posix.S.ISREG(stat_.mode)) { return Maybe(Return.CopyFile){ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOTSUP), .syscall = .copyfile, } }; } var flags: i32 = bun.O.CREAT | bun.O.WRONLY; var wrote: usize = 0; if (mode.shouldntOverwrite()) { flags |= bun.O.EXCL; } const dest_fd = dest_fd: { switch (Syscall.open(dest, flags, JSC.Node.default_permission)) { .result => |result| break :dest_fd result, .err => |err| { if (err.getErrno() == .NOENT) { // Create the parent directory if it doesn't exist var len = dest.len; while (len > 0 and dest[len - 1] != std.fs.path.sep) { len -= 1; } const mkdirResult = this.mkdirRecursive(.{ .path = PathLike{ .string = PathString.init(dest[0..len]) }, .recursive = true, }); if (mkdirResult == .err) { return Maybe(Return.CopyFile){ .err = mkdirResult.err }; } switch (Syscall.open(dest, flags, JSC.Node.default_permission)) { .result => |result| break :dest_fd result, .err => {}, } } @memcpy(this.sync_error_buf[0..dest.len], dest); return Maybe(Return.CopyFile){ .err = err.withPath(this.sync_error_buf[0..dest.len]) }; }, } }; var size: usize = @intCast(@max(stat_.size, 0)); if (posix.S.ISREG(stat_.mode) and bun.can_use_ioctl_ficlone()) { const rc = bun.C.linux.ioctl_ficlone(dest_fd, src_fd); if (rc == 0) { _ = Syscall.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); return ret.success; } bun.disable_ioctl_ficlone(); } defer { _ = Syscall.ftruncate(dest_fd, @as(i64, @intCast(@as(u63, @truncate(wrote))))); _ = Syscall.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); } var off_in_copy = @as(i64, @bitCast(@as(u64, 0))); var off_out_copy = @as(i64, @bitCast(@as(u64, 0))); if (!bun.canUseCopyFileRangeSyscall()) { return copyFileUsingSendfileOnLinuxWithReadWriteFallback(src, dest, src_fd, dest_fd, size, &wrote); } if (size == 0) { // copy until EOF while (true) { // Linux Kernel 5.3 or later // Not supported in gVisor const written = linux.copy_file_range(src_fd.cast(), &off_in_copy, dest_fd.cast(), &off_out_copy, std.mem.page_size, 0); if (ret.errnoSysP(written, .copy_file_range, dest)) |err| { return switch (err.getErrno()) { inline .XDEV, .NOSYS => |errno| brk: { if (comptime errno == .NOSYS) { bun.disableCopyFileRangeSyscall(); } break :brk copyFileUsingSendfileOnLinuxWithReadWriteFallback(src, dest, src_fd, dest_fd, size, &wrote); }, else => return err, }; } // wrote zero bytes means EOF if (written == 0) break; wrote +|= written; } } else { while (size > 0) { // Linux Kernel 5.3 or later // Not supported in gVisor const written = linux.copy_file_range(src_fd.cast(), &off_in_copy, dest_fd.cast(), &off_out_copy, size, 0); if (ret.errnoSysP(written, .copy_file_range, dest)) |err| { return switch (err.getErrno()) { inline .XDEV, .NOSYS => |errno| brk: { if (comptime errno == .NOSYS) { bun.disableCopyFileRangeSyscall(); } break :brk copyFileUsingSendfileOnLinuxWithReadWriteFallback(src, dest, src_fd, dest_fd, size, &wrote); }, else => return err, }; } // wrote zero bytes means EOF if (written == 0) break; wrote +|= written; size -|= written; } } return ret.success; } if (Environment.isWindows) { const src_enoent_maybe = ret.initErrWithP(.ENOENT, .copyfile, this.osPathIntoSyncErrorBuf(src)); const dst_enoent_maybe = ret.initErrWithP(.ENOENT, .copyfile, this.osPathIntoSyncErrorBuf(dest)); const stat_ = reuse_stat orelse switch (windows.GetFileAttributesW(src)) { windows.INVALID_FILE_ATTRIBUTES => return ret.errnoSysP(0, .copyfile, this.osPathIntoSyncErrorBuf(src)).?, else => |result| result, }; if (stat_ & windows.FILE_ATTRIBUTE_REPARSE_POINT == 0) { if (windows.CopyFileW(src, dest, @intFromBool(mode.shouldntOverwrite())) == 0) { var err = windows.GetLastError(); var errpath: bun.OSPathSliceZ = undefined; switch (err) { .FILE_EXISTS, .ALREADY_EXISTS => errpath = dest, .PATH_NOT_FOUND => { bun.makePathW(std.fs.cwd(), bun.path.dirnameW(dest)) catch {}; const second_try = windows.CopyFileW(src, dest, @intFromBool(mode.shouldntOverwrite())); if (second_try > 0) return ret.success; err = windows.GetLastError(); errpath = dest; if (err == .FILE_EXISTS or err == .ALREADY_EXISTS) errpath = src; }, else => errpath = src, } const result = ret.errnoSysP(0, .copyfile, this.osPathIntoSyncErrorBuf(dest)) orelse src_enoent_maybe; return shouldIgnoreEbusy(args.src, args.dest, result); } return ret.success; } else { const handle = switch (bun.sys.openatWindows(bun.invalid_fd, src, bun.O.RDONLY, 0)) { .err => |err| return .{ .err = err }, .result => |src_fd| src_fd, }; const wbuf = bun.OSPathBufferPool.get(); defer bun.OSPathBufferPool.put(wbuf); const len = bun.windows.GetFinalPathNameByHandleW(handle.cast(), wbuf, wbuf.len, 0); if (len == 0) { return ret.errnoSysP(0, .copyfile, this.osPathIntoSyncErrorBuf(dest)) orelse dst_enoent_maybe; } const flags = if (stat_ & windows.FILE_ATTRIBUTE_DIRECTORY != 0) std.os.windows.SYMBOLIC_LINK_FLAG_DIRECTORY else 0; if (windows.CreateSymbolicLinkW(dest, wbuf[0..len :0], flags) == 0) { return ret.errnoSysP(0, .copyfile, this.osPathIntoSyncErrorBuf(dest)) orelse dst_enoent_maybe; } return ret.success; } } return ret.todo(); } /// Directory scanning + clonefile will block this thread, then each individual file copy (what the sync version /// calls "_copySingleFileSync") will be dispatched as a separate task. pub fn cpAsync(this: *NodeFS, task: *AsyncCpTask) void { AsyncCpTask.cpAsync(this, task); } // returns boolean `should_continue` fn _cpAsyncDirectory( this: *NodeFS, args: Arguments.Cp.Flags, task: *AsyncCpTask, src_buf: *bun.OSPathBuffer, src_dir_len: PathString.PathInt, dest_buf: *bun.OSPathBuffer, dest_dir_len: PathString.PathInt, ) bool { return AsyncCpTask._cpAsyncDirectory(this, args, task, src_buf, src_dir_len, dest_buf, dest_dir_len); } }; fn throwInvalidFdError(global: *JSC.JSGlobalObject, value: JSC.JSValue) bun.JSError { if (value.isNumber()) { return global.ERR_OUT_OF_RANGE("The value of \"fd\" is out of range. It must be an integer. Received {d}", .{bun.fmt.double(value.asNumber())}).throw(); } return global.throwInvalidArgumentTypeValue("fd", "number", value); } pub export fn Bun__mkdirp(globalThis: *JSC.JSGlobalObject, path: [*:0]const u8) bool { return globalThis.bunVM().nodeFS().mkdirRecursive( Arguments.Mkdir{ .path = PathLike{ .string = PathString.init(bun.span(path)) }, .recursive = true, }, ) != .err; } comptime { _ = Bun__mkdirp; } /// Copied from std.fs.Dir.deleteTree. This function returns `FileNotFound` instead of ignoring it, which /// is required to match the behavior of Node.js's `fs.rm` { recursive: true, force: false }. pub fn zigDeleteTree(self: std.fs.Dir, sub_path: []const u8, kind_hint: std.fs.File.Kind) !void { var initial_iterable_dir = (try zigDeleteTreeOpenInitialSubpath(self, sub_path, kind_hint)) orelse return; const StackItem = struct { name: []const u8, parent_dir: std.fs.Dir, iter: std.fs.Dir.Iterator, fn closeAll(items: []@This()) void { for (items) |*item| item.iter.dir.close(); } }; var stack_buffer: [16]StackItem = undefined; var stack = std.ArrayListUnmanaged(StackItem).initBuffer(&stack_buffer); defer StackItem.closeAll(stack.items); stack.appendAssumeCapacity(.{ .name = sub_path, .parent_dir = self, .iter = initial_iterable_dir.iterateAssumeFirstIteration(), }); process_stack: while (stack.items.len != 0) { var top = &stack.items[stack.items.len - 1]; while (try top.iter.next()) |entry| { var treat_as_dir = entry.kind == .directory; handle_entry: while (true) { if (treat_as_dir) { if (stack.unusedCapacitySlice().len >= 1) { var iterable_dir = top.iter.dir.openDir(entry.name, .{ .no_follow = true, .iterate = true, }) catch |err| switch (err) { error.NotDir => { treat_as_dir = false; continue :handle_entry; }, error.FileNotFound, error.AccessDenied, error.SymLinkLoop, error.ProcessFdQuotaExceeded, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, error.InvalidUtf8, error.InvalidWtf8, error.BadPathName, error.NetworkNotFound, error.DeviceBusy, => |e| return e, }; stack.appendAssumeCapacity(.{ .name = entry.name, .parent_dir = top.iter.dir, .iter = iterable_dir.iterateAssumeFirstIteration(), }); continue :process_stack; } else { try zigDeleteTreeMinStackSizeWithKindHint(top.iter.dir, entry.name, entry.kind); break :handle_entry; } } else { if (top.iter.dir.deleteFile(entry.name)) { break :handle_entry; } else |err| switch (err) { error.IsDir => { treat_as_dir = true; continue :handle_entry; }, error.FileNotFound, error.NotDir, error.AccessDenied, error.InvalidUtf8, error.InvalidWtf8, error.SymLinkLoop, error.NameTooLong, error.SystemResources, error.ReadOnlyFileSystem, error.FileSystem, error.FileBusy, error.BadPathName, error.NetworkNotFound, error.Unexpected, => |e| return e, } } } } // On Windows, we can't delete until the dir's handle has been closed, so // close it before we try to delete. top.iter.dir.close(); // In order to avoid double-closing the directory when cleaning up // the stack in the case of an error, we save the relevant portions and // pop the value from the stack. const parent_dir = top.parent_dir; const name = top.name; stack.items.len -= 1; var need_to_retry: bool = false; parent_dir.deleteDir(name) catch |err| switch (err) { error.FileNotFound => {}, error.DirNotEmpty => need_to_retry = true, else => |e| return e, }; if (need_to_retry) { // Since we closed the handle that the previous iterator used, we // need to re-open the dir and re-create the iterator. var iterable_dir = iterable_dir: { var treat_as_dir = true; handle_entry: while (true) { if (treat_as_dir) { break :iterable_dir parent_dir.openDir(name, .{ .no_follow = true, .iterate = true, }) catch |err| switch (err) { error.NotDir => { treat_as_dir = false; continue :handle_entry; }, error.FileNotFound => { // That's fine, we were trying to remove this directory anyway. continue :process_stack; }, error.AccessDenied, error.SymLinkLoop, error.ProcessFdQuotaExceeded, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, error.InvalidUtf8, error.InvalidWtf8, error.BadPathName, error.NetworkNotFound, error.DeviceBusy, => |e| return e, }; } else { if (parent_dir.deleteFile(name)) { continue :process_stack; } else |err| switch (err) { error.FileNotFound => continue :process_stack, // Impossible because we do not pass any path separators. error.NotDir => unreachable, error.IsDir => { treat_as_dir = true; continue :handle_entry; }, error.AccessDenied, error.InvalidUtf8, error.InvalidWtf8, error.SymLinkLoop, error.NameTooLong, error.SystemResources, error.ReadOnlyFileSystem, error.FileSystem, error.FileBusy, error.BadPathName, error.NetworkNotFound, error.Unexpected, => |e| return e, } } } }; // We know there is room on the stack since we are just re-adding // the StackItem that we previously popped. stack.appendAssumeCapacity(.{ .name = name, .parent_dir = parent_dir, .iter = iterable_dir.iterateAssumeFirstIteration(), }); continue :process_stack; } } } fn zigDeleteTreeOpenInitialSubpath(self: std.fs.Dir, sub_path: []const u8, kind_hint: std.fs.File.Kind) !?std.fs.Dir { return iterable_dir: { // Treat as a file by default var treat_as_dir = kind_hint == .directory; handle_entry: while (true) { if (treat_as_dir) { break :iterable_dir self.openDir(sub_path, .{ .no_follow = true, .iterate = true, }) catch |err| switch (err) { error.NotDir, error.FileNotFound, error.AccessDenied, error.SymLinkLoop, error.ProcessFdQuotaExceeded, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, error.InvalidUtf8, error.InvalidWtf8, error.BadPathName, error.DeviceBusy, error.NetworkNotFound, => |e| return e, }; } else { if (self.deleteFile(sub_path)) { return null; } else |err| switch (err) { error.IsDir => { treat_as_dir = true; continue :handle_entry; }, error.FileNotFound, error.AccessDenied, error.InvalidUtf8, error.InvalidWtf8, error.SymLinkLoop, error.NameTooLong, error.SystemResources, error.ReadOnlyFileSystem, error.NotDir, error.FileSystem, error.FileBusy, error.BadPathName, error.NetworkNotFound, error.Unexpected, => |e| return e, } } } }; } fn zigDeleteTreeMinStackSizeWithKindHint(self: std.fs.Dir, sub_path: []const u8, kind_hint: std.fs.File.Kind) !void { start_over: while (true) { var dir = (try zigDeleteTreeOpenInitialSubpath(self, sub_path, kind_hint)) orelse return; var cleanup_dir_parent: ?std.fs.Dir = null; defer if (cleanup_dir_parent) |*d| d.close(); var cleanup_dir = true; defer if (cleanup_dir) dir.close(); // Valid use of MAX_PATH_BYTES because dir_name_buf will only // ever store a single path component that was returned from the // filesystem. var dir_name_buf: [std.fs.max_path_bytes]u8 = undefined; var dir_name: []const u8 = sub_path; // Here we must avoid recursion, in order to provide O(1) memory guarantee of this function. // Go through each entry and if it is not a directory, delete it. If it is a directory, // open it, and close the original directory. Repeat. Then start the entire operation over. scan_dir: while (true) { var dir_it = dir.iterateAssumeFirstIteration(); dir_it: while (try dir_it.next()) |entry| { var treat_as_dir = entry.kind == .directory; handle_entry: while (true) { if (treat_as_dir) { const new_dir = dir.openDir(entry.name, .{ .no_follow = true, .iterate = true, }) catch |err| switch (err) { error.NotDir => { treat_as_dir = false; continue :handle_entry; }, error.FileNotFound => { // That's fine, we were trying to remove this directory anyway. continue :dir_it; }, error.AccessDenied, error.SymLinkLoop, error.ProcessFdQuotaExceeded, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, error.InvalidUtf8, error.InvalidWtf8, error.BadPathName, error.NetworkNotFound, error.DeviceBusy, => |e| return e, }; if (cleanup_dir_parent) |*d| d.close(); cleanup_dir_parent = dir; dir = new_dir; const result = dir_name_buf[0..entry.name.len]; @memcpy(result, entry.name); dir_name = result; continue :scan_dir; } else { if (dir.deleteFile(entry.name)) { continue :dir_it; } else |err| switch (err) { error.FileNotFound => continue :dir_it, // Impossible because we do not pass any path separators. error.NotDir => unreachable, error.IsDir => { treat_as_dir = true; continue :handle_entry; }, error.AccessDenied, error.InvalidUtf8, error.InvalidWtf8, error.SymLinkLoop, error.NameTooLong, error.SystemResources, error.ReadOnlyFileSystem, error.FileSystem, error.FileBusy, error.BadPathName, error.NetworkNotFound, error.Unexpected, => |e| return e, } } } } // Reached the end of the directory entries, which means we successfully deleted all of them. // Now to remove the directory itself. dir.close(); cleanup_dir = false; if (cleanup_dir_parent) |d| { d.deleteDir(dir_name) catch |err| switch (err) { // These two things can happen due to file system race conditions. error.FileNotFound, error.DirNotEmpty => continue :start_over, else => |e| return e, }; continue :start_over; } else { self.deleteDir(sub_path) catch |err| switch (err) { error.FileNotFound => return, error.DirNotEmpty => continue :start_over, else => |e| return e, }; return; } } } }