From b54f3f33f0c6dd89e9ccf6ccf254a74c57c289b1 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 23 Jan 2025 05:09:07 -0800 Subject: [PATCH] Clean up node:fs `utimes`, `futimes` , and `lutimes` (#16634) --- cmake/targets/BuildBun.cmake | 7 + src/bun.js/api/Timer.zig | 2 +- src/bun.js/node/node.classes.ts | 172 ++++---------- src/bun.js/node/node_fs.zig | 94 +++----- src/bun.js/node/node_fs_stat_watcher.zig | 14 +- src/bun.js/node/types.zig | 62 ++++- src/bun.js/test/jest.zig | 2 +- src/bun.zig | 20 +- src/c-headers-for-zig.h | 2 + src/codegen/class-definitions.ts | 30 ++- src/codegen/generate-classes.ts | 104 +++++++- src/darwin_c.zig | 6 +- src/sys.zig | 136 ++++++++++- .../node/test/parallel/test-fs-stat-bigint.js | 1 + .../node/test/parallel/test-fs-stat-date.mjs | 96 ++++++++ test/js/node/test/parallel/test-fs-stat.js | 223 ++++++++++++++++++ 16 files changed, 741 insertions(+), 230 deletions(-) create mode 100644 test/js/node/test/parallel/test-fs-stat-date.mjs create mode 100644 test/js/node/test/parallel/test-fs-stat.js diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index bcbb8d8c5bb596..ad1ce07f33ddc6 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -228,6 +228,7 @@ set(BUN_ZIG_GENERATED_CLASSES_OUTPUTS ${CODEGEN_PATH}/ZigGeneratedClasses+DOMIsoSubspaces.h ${CODEGEN_PATH}/ZigGeneratedClasses+lazyStructureImpl.h ${CODEGEN_PATH}/ZigGeneratedClasses.zig + ${CODEGEN_PATH}/ZigGeneratedClasses.lut.txt ) register_command( @@ -406,6 +407,7 @@ set(BUN_OBJECT_LUT_SOURCES ${CWD}/src/bun.js/bindings/ProcessBindingConstants.cpp ${CWD}/src/bun.js/bindings/ProcessBindingNatives.cpp ${CWD}/src/bun.js/modules/NodeModuleModule.cpp + ${CODEGEN_PATH}/ZigGeneratedClasses.lut.txt ) set(BUN_OBJECT_LUT_OUTPUTS @@ -416,6 +418,7 @@ set(BUN_OBJECT_LUT_OUTPUTS ${CODEGEN_PATH}/ProcessBindingConstants.lut.h ${CODEGEN_PATH}/ProcessBindingNatives.lut.h ${CODEGEN_PATH}/NodeModuleModule.lut.h + ${CODEGEN_PATH}/ZigGeneratedClasses.lut.h ) macro(WEBKIT_ADD_SOURCE_DEPENDENCIES _source _deps) @@ -447,6 +450,8 @@ foreach(i RANGE 0 ${BUN_OBJECT_LUT_SOURCES_MAX_INDEX}) bun-codegen-lut-${filename} COMMENT "Generating ${filename}.lut.h" + DEPENDS + ${BUN_OBJECT_LUT_SOURCE} COMMAND ${BUN_EXECUTABLE} run @@ -478,6 +483,8 @@ WEBKIT_ADD_SOURCE_DEPENDENCIES( ${CODEGEN_PATH}/ZigGlobalObject.lut.h ) + + WEBKIT_ADD_SOURCE_DEPENDENCIES( ${CWD}/src/bun.js/bindings/InternalModuleRegistry.cpp ${CODEGEN_PATH}/InternalModuleRegistryConstants.h diff --git a/src/bun.js/api/Timer.zig b/src/bun.js/api/Timer.zig index 2e52553ce719af..9b15fe9aae7b7b 100644 --- a/src/bun.js/api/Timer.zig +++ b/src/bun.js/api/Timer.zig @@ -80,7 +80,7 @@ pub const All = struct { else timespec{ .nsec = 0, .sec = 0 }; - this.uv_timer.start(wait.ms(), 0, &onUVTimer); + this.uv_timer.start(wait.msUnsigned(), 0, &onUVTimer); if (this.active_timer_count > 0) { this.uv_timer.ref(); diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts index 9b20d80600165e..ec1e7b4b42cd1d 100644 --- a/src/bun.js/node/node.classes.ts +++ b/src/bun.js/node/node.classes.ts @@ -244,63 +244,26 @@ export default [ pure: true, }, }, - dev: { - getter: "dev", - }, - ino: { - getter: "ino", - }, - mode: { - getter: "mode", - }, - nlink: { - getter: "nlink", - }, - uid: { - getter: "uid", - }, - gid: { - getter: "gid", - }, - rdev: { - getter: "rdev", - }, - size: { - getter: "size", - }, - blksize: { - getter: "blksize", - }, - blocks: { - getter: "blocks", - }, - atime: { - getter: "atime", - cache: true, - }, - mtime: { - getter: "mtime", - cache: true, - }, - ctime: { - getter: "ctime", - cache: true, - }, - birthtime: { - getter: "birthtime", - }, - atimeMs: { - getter: "atimeMs", - }, - mtimeMs: { - getter: "mtimeMs", - }, - ctimeMs: { - getter: "ctimeMs", - }, - birthtimeMs: { - getter: "birthtimeMs", - }, + }, + own: { + dev: "dev", + ino: "ino", + mode: "mode", + nlink: "nlink", + uid: "uid", + gid: "gid", + rdev: "rdev", + size: "size", + blksize: "blksize", + blocks: "blocks", + atimeMs: "atimeMs", + mtimeMs: "mtimeMs", + ctimeMs: "ctimeMs", + birthtimeMs: "birthtimeMs", + atime: "atime", + mtime: "mtime", + ctime: "ctime", + birthtime: "birthtime", }, }), define({ @@ -386,76 +349,30 @@ export default [ pure: true, }, }, - dev: { - getter: "dev", - }, - ino: { - getter: "ino", - }, - mode: { - getter: "mode", - }, - nlink: { - getter: "nlink", - }, - uid: { - getter: "uid", - }, - gid: { - getter: "gid", - }, - rdev: { - getter: "rdev", - }, - size: { - getter: "size", - }, - blksize: { - getter: "blksize", - }, - blocks: { - getter: "blocks", - }, - atime: { - getter: "atime", - cache: true, - }, - mtime: { - getter: "mtime", - cache: true, - }, - ctime: { - getter: "ctime", - cache: true, - }, - birthtime: { - getter: "birthtime", - cache: true, - }, - atimeMs: { - getter: "atimeMs", - }, - mtimeMs: { - getter: "mtimeMs", - }, - ctimeMs: { - getter: "ctimeMs", - }, - birthtimeMs: { - getter: "birthtimeMs", - }, - atimeNs: { - getter: "atimeNs", - }, - mtimeNs: { - getter: "mtimeNs", - }, - ctimeNs: { - getter: "ctimeNs", - }, - birthtimeNs: { - getter: "birthtimeNs", - }, + }, + own: { + atime: "atime", + atimeMs: "atimeMs", + atimeNs: "atimeNs", + birthtime: "birthtime", + birthtimeMs: "birthtimeMs", + birthtimeNs: "birthtimeNs", + blksize: "blksize", + blocks: "blocks", + ctime: "ctime", + ctimeMs: "ctimeMs", + ctimeNs: "ctimeNs", + dev: "dev", + gid: "gid", + ino: "ino", + mode: "mode", + mtime: "mtime", + mtimeMs: "mtimeMs", + mtimeNs: "mtimeNs", + nlink: "nlink", + rdev: "rdev", + size: "size", + uid: "uid", }, }), define({ @@ -685,4 +602,3 @@ export default [ }, }), ]; - diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 6f2b1714cddebf..50f5f705a398e1 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -36,8 +36,8 @@ const TimeLike = JSC.Node.TimeLike; const Mode = bun.Mode; const uv = bun.windows.libuv; const E = C.E; -const uid_t = if (Environment.isPosix) std.posix.uid_t else bun.windows.libuv.uv_uid_t; -const gid_t = if (Environment.isPosix) std.posix.gid_t else bun.windows.libuv.uv_gid_t; +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); @@ -1915,7 +1915,7 @@ pub const Arguments = struct { // will automatically be normalized to absolute path. const link_type: LinkType = link_type: { if (arguments.next()) |next_val| { - if (next_val.isUndefined()) { + if (next_val.isUndefinedOrNull()) { break :link_type .unspecified; } if (next_val.isString()) { @@ -3572,7 +3572,7 @@ pub const NodeFS = struct { } if (ret.errnoSysP(C.clonefile(src, dest, 0), .copyfile, src) == null) { - _ = C.chmod(dest, stat_.mode); + _ = Syscall.chmod(dest, stat_.mode); return ret.success; } } else { @@ -3595,8 +3595,8 @@ pub const NodeFS = struct { .err => |err| return Maybe(Return.CopyFile){ .err = err.withPath(args.dest.slice()) }, }; defer { - _ = std.c.ftruncate(dest_fd.int(), @as(std.c.off_t, @intCast(@as(u63, @truncate(wrote))))); - _ = C.fchmod(dest_fd.int(), stat_.mode); + _ = Syscall.ftruncate(dest_fd, @as(std.c.off_t, @intCast(@as(u63, @truncate(wrote))))); + _ = Syscall.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); } @@ -3659,7 +3659,7 @@ pub const NodeFS = struct { _ = bun.sys.unlink(dest); return err; } - _ = C.fchmod(dest_fd.cast(), stat_.mode); + _ = Syscall.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); return ret.success; } @@ -3668,7 +3668,7 @@ pub const NodeFS = struct { 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) { - _ = C.fchmod(dest_fd.cast(), stat_.mode); + _ = Syscall.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); return ret.success; } @@ -3791,8 +3791,10 @@ pub const NodeFS = struct { }; } - return Maybe(Return.Chmod).errnoSysP(C.chmod(path, args.mode), .chmod, path) orelse - Maybe(Return.Chmod).success; + 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) { @@ -3800,12 +3802,7 @@ pub const NodeFS = struct { } pub fn fchown(_: *NodeFS, args: Arguments.Fchown, _: Flavor) Maybe(Return.Fchown) { - if (comptime Environment.isWindows) { - return Syscall.fchown(args.fd, args.uid, args.gid); - } - - return Maybe(Return.Fchown).errnoSys(C.fchown(args.fd.int(), args.uid, args.gid), .fchown) orelse - Maybe(Return.Fchown).success; + return Syscall.fchown(args.fd, args.uid, args.gid); } pub fn fdatasync(_: *NodeFS, args: Arguments.FdataSync, _: Flavor) Maybe(Return.Fdatasync) { @@ -3817,7 +3814,7 @@ pub const NodeFS = struct { pub fn fstat(_: *NodeFS, args: Arguments.Fstat, _: Flavor) Maybe(Return.Fstat) { return switch (Syscall.fstat(args.fd)) { - .result => |result| .{ .result = Stats.init(result, false) }, + .result => |result| .{ .result = Stats.init(result, args.big_int) }, .err => |err| .{ .err = err }, }; } @@ -3849,15 +3846,10 @@ pub const NodeFS = struct { Maybe(Return.Futimes).success; } - var times = [2]std.posix.timespec{ - args.atime, - args.mtime, + return switch (Syscall.futimens(args.fd, args.atime, args.mtime)) { + .err => |err| .{ .err = err }, + .result => Maybe(Return.Futimes).success, }; - - return if (Maybe(Return.Futimes).errnoSysFd(system.futimens(args.fd.int(), ×), .futime, args.fd)) |err| - err - else - Maybe(Return.Futimes).success; } pub fn lchmod(this: *NodeFS, args: Arguments.LCHmod, _: Flavor) Maybe(Return.Lchmod) { @@ -5919,21 +5911,15 @@ pub const NodeFS = struct { bun.assert(args.mtime.tv_nsec <= 1e9); bun.assert(args.atime.tv_nsec <= 1e9); - var times = [2]std.c.timeval{ - .{ - .tv_sec = args.atime.tv_sec, - .tv_usec = @intCast(@divTrunc(args.atime.tv_nsec, std.time.ns_per_us)), - }, - .{ - .tv_sec = args.mtime.tv_sec, - .tv_usec = @intCast(@divTrunc(args.mtime.tv_nsec, std.time.ns_per_us)), - }, - }; - return if (Maybe(Return.Utimes).errnoSysP(std.c.utimes(args.path.sliceZ(&this.sync_error_buf), ×), .utime, args.path.slice())) |err| - .{ .err = err.err.withPath(args.path.slice()) } - else - Maybe(Return.Utimes).success; + 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) { @@ -5960,21 +5946,11 @@ pub const NodeFS = struct { bun.assert(args.mtime.tv_nsec <= 1e9); bun.assert(args.atime.tv_nsec <= 1e9); - var times = [2]std.c.timeval{ - .{ - .tv_sec = args.atime.tv_sec, - .tv_usec = @intCast(@divTrunc(args.atime.tv_nsec, std.time.ns_per_us)), - }, - .{ - .tv_sec = args.mtime.tv_sec, - .tv_usec = @intCast(@divTrunc(args.mtime.tv_nsec, std.time.ns_per_us)), - }, - }; - return if (Maybe(Return.Lutimes).errnoSysP(C.lutimes(args.path.sliceZ(&this.sync_error_buf), ×), .lutime, args.path.slice())) |err| - .{ .err = err.err.withPath(args.path.slice()) } - else - Maybe(Return.Lutimes).success; + 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) { @@ -6270,7 +6246,7 @@ pub const NodeFS = struct { } if (ret.errnoSysP(C.clonefile(src, dest, 0), .clonefile, src) == null) { - _ = C.chmod(dest, stat_.mode); + _ = Syscall.chmod(dest, stat_.mode); return ret.success; } } else { @@ -6321,8 +6297,8 @@ pub const NodeFS = struct { } }; defer { - _ = std.c.ftruncate(dest_fd.int(), @as(std.c.off_t, @intCast(@as(u63, @truncate(wrote))))); - _ = C.fchmod(dest_fd.int(), stat_.mode); + _ = Syscall.ftruncate(dest_fd, @intCast(@as(u63, @truncate(wrote)))); + _ = Syscall.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); } @@ -6421,7 +6397,7 @@ pub const NodeFS = struct { 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) { - _ = C.fchmod(dest_fd.cast(), stat_.mode); + _ = Syscall.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); return ret.success; } @@ -6430,8 +6406,8 @@ pub const NodeFS = struct { } defer { - _ = linux.ftruncate(dest_fd.cast(), @as(i64, @intCast(@as(u63, @truncate(wrote))))); - _ = linux.fchmod(dest_fd.cast(), stat_.mode); + _ = Syscall.ftruncate(dest_fd, @as(i64, @intCast(@as(u63, @truncate(wrote))))); + _ = Syscall.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); } diff --git a/src/bun.js/node/node_fs_stat_watcher.zig b/src/bun.js/node/node_fs_stat_watcher.zig index f7bfb1eecea3f5..aefac5ef91fcf4 100644 --- a/src/bun.js/node/node_fs_stat_watcher.zig +++ b/src/bun.js/node/node_fs_stat_watcher.zig @@ -341,17 +341,18 @@ pub const StatWatcher = struct { watcher: *StatWatcher, task: JSC.WorkPoolTask = .{ .callback = &workPoolCallback }, + pub usingnamespace bun.New(@This()); + pub fn createAndSchedule( watcher: *StatWatcher, ) void { - var task = bun.default_allocator.create(InitialStatTask) catch bun.outOfMemory(); - task.* = .{ .watcher = watcher }; + const task = InitialStatTask.new(.{ .watcher = watcher }); JSC.WorkPool.schedule(&task.task); } fn workPoolCallback(task: *JSC.WorkPoolTask) void { const initial_stat_task: *InitialStatTask = @fieldParentPtr("task", task); - defer bun.default_allocator.destroy(initial_stat_task); + defer initial_stat_task.destroy(); const this = initial_stat_task.watcher; if (this.closed) { @@ -454,7 +455,8 @@ pub const StatWatcher = struct { pub fn init(args: Arguments) !*StatWatcher { log("init", .{}); - var buf: bun.PathBuffer = undefined; + const buf = bun.PathBufferPool.get(); + defer bun.PathBufferPool.put(buf); var slice = args.path.slice(); if (bun.strings.startsWith(slice, "file://")) { slice = slice[6..]; @@ -463,7 +465,7 @@ pub const StatWatcher = struct { var parts = [_]string{slice}; const file_path = Path.joinAbsStringBuf( Fs.FileSystem.instance.top_level_dir, - &buf, + buf, &parts, .auto, ); @@ -487,7 +489,7 @@ pub const StatWatcher = struct { // Instant.now will not fail on our target platforms. .last_check = std.time.Instant.now() catch unreachable, // InitStatTask is responsible for setting this - .last_stat = undefined, + .last_stat = std.mem.zeroes(bun.Stat), .last_jsvalue = JSC.Strong.init(), }; errdefer this.deinit(); diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 16cc03f2766f49..eee96c0c5a11b8 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -1295,20 +1295,49 @@ fn timeLikeFromMilliseconds(milliseconds: f64) TimeLike { if (Environment.isWindows) { return milliseconds / 1000.0; } + + var sec: f64 = @divFloor(milliseconds, std.time.ms_per_s); + var nsec: f64 = @mod(milliseconds, std.time.ms_per_s) * std.time.ns_per_ms; + + if (nsec < 0) { + nsec += std.time.ns_per_s; + sec -= 1; + } + return .{ - .tv_sec = @intFromFloat(@divFloor(milliseconds, std.time.ms_per_s)), - .tv_nsec = @intFromFloat(@mod(milliseconds, std.time.ms_per_s) * std.time.ns_per_ms), + .tv_sec = @intFromFloat(sec), + .tv_nsec = @intFromFloat(nsec), }; } fn timeLikeFromNow() TimeLike { - const nanos = std.time.nanoTimestamp(); if (Environment.isWindows) { + const nanos = std.time.nanoTimestamp(); return @as(TimeLike, @floatFromInt(nanos)) / std.time.ns_per_s; } + + // Permissions requirements + // To set both file timestamps to the current time (i.e., times is + // NULL, or both tv_nsec fields specify UTIME_NOW), either: + // + // • the caller must have write access to the file; + // + // • the caller's effective user ID must match the owner of the + // file; or + // + // • the caller must have appropriate privileges. + // + // To make any change other than setting both timestamps to the + // current time (i.e., times is not NULL, and neither tv_nsec field + // is UTIME_NOW and neither tv_nsec field is UTIME_OMIT), either + // condition 2 or 3 above must apply. + // + // If both tv_nsec fields are specified as UTIME_OMIT, then no file + // ownership or permission checks are performed, and the file + // timestamps are not modified, but other error conditions may still return .{ - .tv_sec = @truncate(@divFloor(nanos, std.time.ns_per_s)), - .tv_nsec = @truncate(@mod(nanos, std.time.ns_per_s)), + .tv_sec = 0, + .tv_nsec = if (Environment.isLinux) std.os.linux.UTIME.NOW else bun.C.translated.UTIME_NOW, }; } @@ -1635,9 +1664,17 @@ pub fn StatType(comptime big: bool) type { const StatTimespec = if (Environment.isWindows) bun.windows.libuv.uv_timespec_t else std.posix.timespec; inline fn toNanoseconds(ts: StatTimespec) Timestamp { - const tv_sec: i64 = @intCast(ts.tv_sec); - const tv_nsec: i64 = @intCast(ts.tv_nsec); - return @as(Timestamp, @intCast(tv_sec * 1_000_000_000)) + @as(Timestamp, @intCast(tv_nsec)); + if (ts.tv_sec < 0) { + return @intCast(@max(bun.timespec.nsSigned(&bun.timespec{ + .sec = @intCast(ts.tv_sec), + .nsec = @intCast(ts.tv_nsec), + }), 0)); + } + + return bun.timespec.ns(&bun.timespec{ + .sec = @intCast(ts.tv_sec), + .nsec = @intCast(ts.tv_nsec), + }); } fn toTimeMS(ts: StatTimespec) Float { @@ -1654,8 +1691,10 @@ pub fn StatType(comptime big: bool) type { return @as(i64, sec * std.time.ms_per_s) + @as(i64, @divTrunc(nsec, std.time.ns_per_ms)); } else { - return (@as(f64, @floatFromInt(tv_sec)) * std.time.ms_per_s) + - (@as(f64, @floatFromInt(tv_nsec)) / std.time.ns_per_ms); + return @floatFromInt(bun.timespec.ms(&bun.timespec{ + .sec = @intCast(tv_sec), + .nsec = @intCast(tv_nsec), + })); } } @@ -2478,3 +2517,6 @@ pub const StatFS = union(enum) { @compileError("Only use Stats.toJSNewlyCreated() or Stats.toJS() directly on a StatsBig or StatsSmall"); } }; + +pub const uid_t = if (Environment.isPosix) std.posix.uid_t else bun.windows.libuv.uv_uid_t; +pub const gid_t = if (Environment.isPosix) std.posix.gid_t else bun.windows.libuv.uv_gid_t; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index ab6c6163ebd6eb..3dcaacf0219334 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1428,7 +1428,7 @@ pub const TestRunnerTask = struct { const elapsed = now.duration(&this.started_at).ms(); this.ref.unref(this.globalThis.bunVM()); this.globalThis.throwTerminationException(); - this.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .{ .timeout = elapsed }); + this.handleResult(.{ .fail = expect.active_test_expectation_counter.actual }, .{ .timeout = @intCast(@max(elapsed, 0)) }); } const ResultType = union(enum) { diff --git a/src/bun.zig b/src/bun.zig index 5e2d7b3e2badd0..a61b2373dab8c7 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -3745,19 +3745,29 @@ pub const timespec = extern struct { assert(this.sec >= 0); assert(this.nsec >= 0); - - const max = std.math.maxInt(u64); const s_ns = std.math.mul( u64, @as(u64, @intCast(this.sec)), std.time.ns_per_s, - ) catch return max; + ) catch return std.math.maxInt(u64); return std.math.add(u64, s_ns, @as(u64, @intCast(this.nsec))) catch - return max; + return std.math.maxInt(i64); + } + + pub fn nsSigned(this: *const timespec) i64 { + const ns_per_sec = this.sec *% std.time.ns_per_s; + const ns_from_nsec = @divFloor(this.nsec, 1_000_000); + return ns_per_sec +% ns_from_nsec; + } + + pub fn ms(this: *const timespec) i64 { + const ms_from_sec = this.sec *% 1000; + const ms_from_nsec = @divFloor(this.nsec, 1_000_000); + return ms_from_sec +% ms_from_nsec; } - pub fn ms(this: *const timespec) u64 { + pub fn msUnsigned(this: *const timespec) u64 { return this.ns() / std.time.ns_per_ms; } diff --git a/src/c-headers-for-zig.h b/src/c-headers-for-zig.h index b0464fc3fa9011..e698aadec6ad6d 100644 --- a/src/c-headers-for-zig.h +++ b/src/c-headers-for-zig.h @@ -24,6 +24,8 @@ #if DARWIN #include +#include #elif LINUX #include +#include #endif diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index 64d5272f8c8716..3915686c47b3a4 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -47,7 +47,7 @@ export type Field = length?: number; }; -export interface ClassDefinition { +export class ClassDefinition { name: string; construct?: boolean; call?: boolean; @@ -55,6 +55,7 @@ export interface ClassDefinition { overridesToJS?: boolean; klass: Record; proto: Record; + own: Record; values?: string[]; JSType?: string; noConstructor?: boolean; @@ -94,6 +95,23 @@ export interface ClassDefinition { structuredClone?: boolean | { transferable: boolean; tag: number }; callbacks?: Record; + + constructor(options: Partial) { + this.name = options.name ?? ""; + this.klass = options.klass ?? {}; + this.proto = options.proto ?? {}; + this.own = options.own ?? {}; + + Object.assign(this, options); + } + + hasOwnProperties() { + for (const key in this.own) { + return true; + } + + return false; + } } export interface CustomField { @@ -107,6 +125,7 @@ export function define( { klass = {}, proto = {}, + own = {}, values = [], overridesToJS = false, estimatedSize = false, @@ -114,9 +133,9 @@ export function define( construct = false, structuredClone = false, ...rest - } = {} as ClassDefinition, -): ClassDefinition { - return { + } = {} as Partial, +): Partial { + return new ClassDefinition({ ...rest, call, overridesToJS, @@ -124,6 +143,7 @@ export function define( estimatedSize, structuredClone, values, + own: own || {}, klass: Object.fromEntries( Object.entries(klass) .sort(([a], [b]) => a.localeCompare(b)) @@ -140,5 +160,5 @@ export function define( return [k, v]; }), ), - }; + }); } diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index d60f1ae548150c..acd608ed6cc4d5 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -308,6 +308,17 @@ function propRow( throw "Unsupported property"; } +function ownRow( + symbolName: (a: string, b: string) => string, + typeName: string, + name: string, + prop: Field, + isWrapped = true, + defaultPropertyAttributes, + supportsObjectCreate = false, +) { + throw "Unsupported property"; +} export function generateHashTable(nameToUse, symbolName, typeName, obj, props = {}, wrapped) { const rows = []; @@ -348,6 +359,45 @@ export function generateHashTable(nameToUse, symbolName, typeName, obj, props = `; } +export function generateHashTableComment(nameToUse, symbolName, obj, props = {}, wrapped) { + const rows = []; + let defaultPropertyAttributes = undefined; + + if ("enumerable" in obj) { + defaultPropertyAttributes ||= {}; + defaultPropertyAttributes.enumerable = obj.enumerable; + } + + if ("configurable" in obj) { + defaultPropertyAttributes ||= {}; + defaultPropertyAttributes.configurable = obj.configurable; + } + + for (const name in props) { + if (name.startsWith("@@")) continue; + externs += ` +extern JSC_CALLCONV JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${protoSymbolName( + obj.name, + name, + )}(void* ptr, JSC::JSGlobalObject*); +namespace WebCore { +static JSC::JSValue construct${symbolName(name)}PropertyCallback(JSC::VM &vm, JSC::JSObject* initialThisObject); +} + `; + rows.push(`${name} WebCore::construct${symbolName(name)}PropertyCallback PropertyCallback`); + } + + if (rows.length === 0) { + return ""; + } + + return ` +@begin ${nameToUse}Table +${rows.join("\n")} +@end +`; +} + function generatePrototype(typeName, obj) { const proto = prototypeName(typeName); const { proto: protoFields } = obj; @@ -1261,6 +1311,7 @@ function generateClassHeader(typeName, obj: ClassDefinition) { class ${name}${final ? " final" : ""} : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags${obj.hasOwnProperties() ? ` | HasStaticPropertyTable` : ""}; static ${name}* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx); DECLARE_EXPORT_INFO; @@ -1378,6 +1429,7 @@ function generateClassImpl(typeName, obj: ClassDefinition) { hasPendingActivity = false, getInternalProperties = false, callbacks = {}, + own, } = obj; const name = className(typeName); @@ -1479,6 +1531,22 @@ ${renderCallbacksCppImpl(typeName, callbacks)} `; } + if (obj.hasOwnProperties()) { + output += Object.entries(own) + .map( + ([name, getterName]) => ` +static JSC::JSValue construct${symbolName(obj.name, name)}PropertyCallback(JSC::VM &vm, JSC::JSObject* initialThisObject) { + auto scope = DECLARE_THROW_SCOPE(vm); + Bun::JS${obj.name}* thisObject = jsCast(initialThisObject); + JSC::EncodedJSValue result = ${protoSymbolName(obj.name, getterName)}(thisObject->wrapped(), thisObject->globalObject()); + RETURN_IF_EXCEPTION(scope, {}); + return JSC::JSValue::decode(result); +} + `, + ) + .join("\n"); + } + if (finalize) { output += ` ${name}::~${name}() @@ -1535,7 +1603,7 @@ void ${name}::destroy(JSCell* cell) static_cast<${name}*>(cell)->${name}::~${name}(); } -const ClassInfo ${name}::s_info = { "${typeName}"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(${name}) }; +const ClassInfo ${name}::s_info = { "${typeName}"_s, &Base::s_info, ${obj.hasOwnProperties() ? `&${typeName}Table` : "nullptr"}, nullptr, CREATE_METHOD_TABLE(${name}) }; void ${name}::finishCreation(VM& vm) { @@ -1667,10 +1735,22 @@ function generateHeader(typeName, obj) { return "\n" + fields.join("\n").trim(); } -function generateImpl(typeName, obj) { +let lutTextFile = ` +/* Source for ZigGeneratedClasses.lut.h +`; +function generateOwnProperties(typeName, symbolName, obj, props = {}, wrapped) { + lutTextFile += ` +${generateHashTableComment(typeName, symbolName, obj, props, wrapped)} +`; +} + +function generateImpl(typeName, obj: ClassDefinition) { if (obj.zigOnly) return ""; const proto = obj.proto; + if (obj?.hasOwnProperties?.()) { + generateOwnProperties(typeName, name => symbolName(typeName, name), obj, obj.own); + } return [ (obj.final ?? true) ? generatePrototypeHeader(typeName, true) : null, !obj.noConstructor ? generateConstructorHeader(typeName).trim() + "\n" : null, @@ -1687,6 +1767,7 @@ function generateZig( { klass = {}, proto = {}, + own = {}, construct, finalize, noConstructor = false, @@ -1721,6 +1802,11 @@ function generateZig( exports.set("onStructuredCloneDeserialize", symbolName(typeName, "onStructuredCloneDeserialize")); } + proto = { + ...Object.fromEntries(Object.entries(own || {}).map(([name, getterName]) => [name, { getter: getterName }])), + ...proto, + }; + const externs = Object.entries({ ...proto, ...Object.fromEntries((values || []).map(a => [a, { internal: true }])), @@ -2168,6 +2254,8 @@ namespace WebCore { using namespace JSC; using namespace Zig; +#include "ZigGeneratedClasses.lut.h" + `; const GENERATED_CLASSES_IMPL_FOOTER = ` @@ -2278,12 +2366,15 @@ classes.sort((a, b) => (a.name < b.name ? -1 : 1)); // sort all the prototype keys and klass keys for (const obj of classes) { - let { klass = {}, proto = {} } = obj; + let { klass = {}, proto = {}, own = {} } = obj; klass = Object.fromEntries(Object.entries(klass).sort(([a], [b]) => a.localeCompare(b))); proto = Object.fromEntries(Object.entries(proto).sort(([a], [b]) => a.localeCompare(b))); + own = Object.fromEntries(Object.entries(own).sort(([a], [b]) => a.localeCompare(b))); + obj.klass = klass; obj.proto = proto; + obj.own = own; } const GENERATED_CLASSES_FOOTER = ` @@ -2396,6 +2487,13 @@ if (!process.env.ONLY_ZIG) { jsInheritsCppImpl(), ]); + if (lutTextFile.length) { + lutTextFile += ` +/* +`; + await writeIfNotChanged(`${outBase}/ZigGeneratedClasses.lut.txt`, [lutTextFile]); + } + await writeIfNotChanged( `${outBase}/ZigGeneratedClasses+lazyStructureHeader.h`, classes.map(a => generateLazyClassStructureHeader(a.name, a)).join("\n"), diff --git a/src/darwin_c.zig b/src/darwin_c.zig index 00c6b5abc5ef8a..744decef9d3ff1 100644 --- a/src/darwin_c.zig +++ b/src/darwin_c.zig @@ -76,16 +76,16 @@ pub extern "c" fn fclonefileat(c_int, c_int, [*:0]const u8, uint32_t: c_int) c_i pub extern "c" fn clonefile(src: [*:0]const u8, dest: [*:0]const u8, flags: c_int) c_int; pub const lstat = blk: { - const T = *const fn ([*c]const u8, [*c]std.c.Stat) callconv(.C) c_int; + const T = *const fn (?[*:0]const u8, ?*bun.Stat) callconv(.C) c_int; break :blk @extern(T, .{ .name = "lstat64" }); }; pub const fstat = blk: { - const T = *const fn ([*c]const u8, [*c]std.c.Stat) callconv(.C) c_int; + const T = *const fn (i32, ?*bun.Stat) callconv(.C) c_int; break :blk @extern(T, .{ .name = "fstat64" }); }; pub const stat = blk: { - const T = *const fn ([*c]const u8, [*c]std.c.Stat) callconv(.C) c_int; + const T = *const fn (?[*:0]const u8, ?*bun.Stat) callconv(.C) c_int; break :blk @extern(T, .{ .name = "stat64" }); }; diff --git a/src/sys.zig b/src/sys.zig index 32dbb6d0d37e60..05a8f4370a0809 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -220,6 +220,7 @@ pub const Tag = enum(u8) { symlinkat, unlink, utime, + utimensat, write, getcwd, getenv, @@ -593,20 +594,58 @@ pub fn getcwdZ(buf: *bun.PathBuffer) Maybe([:0]const u8) { Result.errnoSysP(@as(c_int, 0), .getcwd, buf).?; } +const syscall_or_C = if (Environment.isLinux) syscall else bun.C; + +pub fn fchown(fd: bun.FileDescriptor, uid: JSC.Node.uid_t, gid: JSC.Node.gid_t) Maybe(void) { + if (comptime Environment.isWindows) { + return sys_uv.fchown(fd, uid, gid); + } + + while (true) { + const rc = syscall_or_C.fchown(fd.cast(), uid, gid); + if (Maybe(void).errnoSysFd(rc, .fchown, fd)) |err| { + if (err.getErrno() == .INTR) continue; + return err; + } + + return Maybe(void).success; + } + + unreachable; +} + pub fn fchmod(fd: bun.FileDescriptor, mode: bun.Mode) Maybe(void) { if (comptime Environment.isWindows) { return sys_uv.fchmod(fd, mode); } - return Maybe(void).errnoSysFd(C.fchmod(fd.cast(), mode), .fchmod, fd) orelse - Maybe(void).success; + while (true) { + const rc = syscall_or_C.fchmod(fd.cast(), mode); + if (Maybe(void).errnoSysFd(rc, .fchmod, fd)) |err| { + if (err.getErrno() == .INTR) continue; + return err; + } + + return Maybe(void).success; + } + + unreachable; } -pub fn fchmodat(fd: bun.FileDescriptor, path: [:0]const u8, mode: bun.Mode, flags: i32) Maybe(void) { +pub fn fchmodat(fd: bun.FileDescriptor, path: [:0]const u8, mode: bun.Mode, flags: if (Environment.isLinux) u32 else i32) Maybe(void) { if (comptime Environment.isWindows) @compileError("Use fchmod instead"); - return Maybe(void).errnoSysFd(C.fchmodat(fd.cast(), path.ptr, mode, flags), .fchmodat, fd) orelse - Maybe(void).success; + while (true) { + const rc = syscall_or_C.fchmodat(fd.cast(), path.ptr, mode, flags); + if (Maybe(void).errnoSysFd(rc, .fchmodat, fd)) |err| { + if (err.getErrno() == .INTR) continue; + return err; + } + + return Maybe(void).success; + } + + unreachable; } pub fn chmod(path: [:0]const u8, mode: bun.Mode) Maybe(void) { @@ -614,8 +653,17 @@ pub fn chmod(path: [:0]const u8, mode: bun.Mode) Maybe(void) { return sys_uv.chmod(path, mode); } - return Maybe(void).errnoSysP(C.chmod(path.ptr, mode), .chmod, path) orelse - Maybe(void).success; + while (true) { + const rc = syscall_or_C.chmod(path.ptr, mode); + if (Maybe(void).errnoSysP(rc, .chmod, path)) |err| { + if (err.getErrno() == .INTR) continue; + return err; + } + + return Maybe(void).success; + } + + unreachable; } pub fn chdirOSPath(path: bun.stringZ, destination: if (Environment.isPosix) bun.stringZ else bun.string) Maybe(void) { @@ -702,7 +750,11 @@ pub fn stat(path: [:0]const u8) Maybe(bun.Stat) { return sys_uv.stat(path); } else { var stat_ = mem.zeroes(bun.Stat); - const rc = C.stat(path, &stat_); + const rc = if (Environment.isLinux) + // aarch64 linux doesn't implement a "stat" syscall. It's all fstatat. + linux.fstatat(std.posix.AT.FDCWD, path, &stat_, 0) + else + syscall_or_C.stat(path, &stat_); if (comptime Environment.allow_assert) log("stat({s}) = {d}", .{ bun.asByteSlice(path), rc }); @@ -753,7 +805,7 @@ pub fn fstat(fd: bun.FileDescriptor) Maybe(bun.Stat) { var stat_ = mem.zeroes(bun.Stat); - const rc = C.fstat(fd.cast(), &stat_); + const rc = syscall_or_C.fstat(fd.cast(), &stat_); if (comptime Environment.allow_assert) log("fstat({}) = {d}", .{ fd, rc }); @@ -761,6 +813,13 @@ pub fn fstat(fd: bun.FileDescriptor) Maybe(bun.Stat) { if (Maybe(bun.Stat).errnoSysFd(rc, .fstat, fd)) |err| return err; return Maybe(bun.Stat){ .result = stat_ }; } +pub fn lutimes(path: [:0]const u8, atime: JSC.Node.TimeLike, mtime: JSC.Node.TimeLike) Maybe(void) { + if (comptime Environment.isWindows) { + return sys_uv.lutimes(path, atime, mtime); + } + + return utimensWithFlags(path, atime, mtime, std.posix.AT.SYMLINK_NOFOLLOW); +} pub fn mkdiratA(dir_fd: bun.FileDescriptor, file_path: []const u8) Maybe(void) { const buf = bun.WPathBufferPool.get(); @@ -3168,6 +3227,65 @@ pub fn directoryExistsAt(dir: anytype, subpath: anytype) JSC.Maybe(bool) { }; } +pub fn futimens(fd: bun.FileDescriptor, atime: JSC.Node.TimeLike, mtime: JSC.Node.TimeLike) Maybe(void) { + if (comptime Environment.isWindows) @compileError("TODO: futimes"); + + while (true) { + const rc = syscall.futimens(fd.cast(), &[2]syscall.timespec{ + .{ .tv_sec = @intCast(atime.tv_sec), .tv_nsec = atime.tv_nsec }, + .{ .tv_sec = @intCast(mtime.tv_sec), .tv_nsec = mtime.tv_nsec }, + }); + + log("futimens({}, accessed=({d}, {d}), modified=({d}, {d})) = {d}", .{ fd, atime.tv_sec, atime.tv_nsec, mtime.tv_sec, mtime.tv_nsec, rc }); + + if (rc == 0) { + return Maybe(void).success; + } + + switch (bun.C.getErrno(rc)) { + .INTR => continue, + else => return Maybe(void).errnoSysFd(rc, .futimens, fd).?, + } + } + + unreachable; +} + +fn utimensWithFlags(path: bun.OSPathSliceZ, atime: JSC.Node.TimeLike, mtime: JSC.Node.TimeLike, flags: u32) Maybe(void) { + if (comptime Environment.isWindows) @compileError("TODO: utimens"); + + while (true) { + var times: [2]syscall.timespec = .{ + .{ .tv_sec = @intCast(atime.tv_sec), .tv_nsec = atime.tv_nsec }, + .{ .tv_sec = @intCast(mtime.tv_sec), .tv_nsec = mtime.tv_nsec }, + }; + const rc = syscall.utimensat( + std.fs.cwd().fd, + path, + // this var should be a const, the zig type definition is wrong. + ×, + flags, + ); + + log("utimensat({d}, atime=({d}, {d}), mtime=({d}, {d})) = {d}", .{ std.fs.cwd().fd, atime.tv_sec, atime.tv_nsec, mtime.tv_sec, mtime.tv_nsec, rc }); + + if (rc == 0) { + return Maybe(void).success; + } + + switch (bun.C.getErrno(rc)) { + .INTR => continue, + else => return Maybe(void).errnoSysP(rc, .utimensat, path).?, + } + } + + unreachable; +} + +pub fn utimens(path: bun.OSPathSliceZ, atime: JSC.Node.TimeLike, mtime: JSC.Node.TimeLike) Maybe(void) { + return utimensWithFlags(path, atime, mtime, 0); +} + pub fn setNonblocking(fd: bun.FileDescriptor) Maybe(void) { const flags = switch (bun.sys.fcntl( fd, diff --git a/test/js/node/test/parallel/test-fs-stat-bigint.js b/test/js/node/test/parallel/test-fs-stat-bigint.js index 0a2bea92e5018c..b9fde2288137d9 100644 --- a/test/js/node/test/parallel/test-fs-stat-bigint.js +++ b/test/js/node/test/parallel/test-fs-stat-bigint.js @@ -18,6 +18,7 @@ function getFilename() { return filename; } + function verifyStats(bigintStats, numStats, allowableDelta) { // allowableDelta: It's possible that the file stats are updated between the // two stat() calls so allow for a small difference. diff --git a/test/js/node/test/parallel/test-fs-stat-date.mjs b/test/js/node/test/parallel/test-fs-stat-date.mjs new file mode 100644 index 00000000000000..9ccab9c6e662c9 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-stat-date.mjs @@ -0,0 +1,96 @@ +import * as common from '../common/index.mjs'; + +// Test timestamps returned by fsPromises.stat and fs.statSync + +import fs from 'node:fs'; +import fsPromises from 'node:fs/promises'; +import assert from 'node:assert'; +import tmpdir from '../common/tmpdir.js'; + +// On some platforms (for example, ppc64) boundaries are tighter +// than usual. If we catch these errors, skip corresponding test. +const ignoredErrors = new Set(['EINVAL', 'EOVERFLOW']); + +tmpdir.refresh(); +const filepath = tmpdir.resolve('timestamp'); + +await (await fsPromises.open(filepath, 'w')).close(); + +// Perform a trivial check to determine if filesystem supports setting +// and retrieving atime and mtime. If it doesn't, skip the test. +await fsPromises.utimes(filepath, 2, 2); +const { atimeMs, mtimeMs } = await fsPromises.stat(filepath); +if (atimeMs !== 2000 || mtimeMs !== 2000) { + common.skip(`Unsupported filesystem (atime=${atimeMs}, mtime=${mtimeMs})`); +} + +// Date might round down timestamp +function closeEnough(actual, expected, margin) { + // On ppc64, value is rounded to seconds + if (process.arch === 'ppc64') { + margin += 1000; + } + + // Filesystems without support for timestamps before 1970-01-01, such as NFSv3, + // should return 0 for negative numbers. Do not treat it as error. + if (actual === 0 && expected < 0) { + console.log(`ignored 0 while expecting ${expected}`); + return; + } + + assert.ok(Math.abs(Number(actual - expected)) < margin, + `expected ${expected} ± ${margin}, got ${actual}`); +} + +async function runTest(atime, mtime, margin = 0) { + margin += Number.EPSILON; + try { + const atimeDate = new Date(atime); + const mtimeDate = new Date(mtime); + await fsPromises.utimes(filepath, atimeDate, mtimeDate); + } catch (e) { + if (ignoredErrors.has(e.code)) return; + throw e; + } + + const stats = await fsPromises.stat(filepath); + closeEnough(stats.atimeMs, atime, margin); + closeEnough(stats.mtimeMs, mtime, margin); + closeEnough(stats.atime.getTime(), new Date(atime).getTime(), margin); + closeEnough(stats.mtime.getTime(), new Date(mtime).getTime(), margin); + + const statsBigint = await fsPromises.stat(filepath, { bigint: true }); + closeEnough(statsBigint.atimeMs, BigInt(atime), margin); + closeEnough(statsBigint.mtimeMs, BigInt(mtime), margin); + closeEnough(statsBigint.atime.getTime(), new Date(atime).getTime(), margin); + closeEnough(statsBigint.mtime.getTime(), new Date(mtime).getTime(), margin); + + const statsSync = fs.statSync(filepath); + closeEnough(statsSync.atimeMs, atime, margin); + closeEnough(statsSync.mtimeMs, mtime, margin); + closeEnough(statsSync.atime.getTime(), new Date(atime).getTime(), margin); + closeEnough(statsSync.mtime.getTime(), new Date(mtime).getTime(), margin); + + const statsSyncBigint = fs.statSync(filepath, { bigint: true }); + closeEnough(statsSyncBigint.atimeMs, BigInt(atime), margin); + closeEnough(statsSyncBigint.mtimeMs, BigInt(mtime), margin); + closeEnough(statsSyncBigint.atime.getTime(), new Date(atime).getTime(), margin); + closeEnough(statsSyncBigint.mtime.getTime(), new Date(mtime).getTime(), margin); +} + +// Too high/low numbers produce too different results on different platforms +{ + // TODO(LiviaMedeiros): investigate outdated stat time on FreeBSD. + // On Windows, filetime is stored and handled differently. Supporting dates + // after Y2038 is preferred over supporting dates before 1970-01-01. + if (!common.isFreeBSD && !common.isWindows) { + await runTest(-40691, -355, 1); // Potential precision loss on 32bit + await runTest(-355, -40691, 1); // Potential precision loss on 32bit + await runTest(-1, -1); + } + await runTest(0, 0); + await runTest(1, 1); + await runTest(355, 40691, 1); // Precision loss on 32bit + await runTest(40691, 355, 1); // Precision loss on 32bit + await runTest(1713037251360, 1713037251360, 1); // Precision loss +} diff --git a/test/js/node/test/parallel/test-fs-stat.js b/test/js/node/test/parallel/test-fs-stat.js new file mode 100644 index 00000000000000..1e11e465b4daba --- /dev/null +++ b/test/js/node/test/parallel/test-fs-stat.js @@ -0,0 +1,223 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const fs = require('fs'); + +fs.stat('.', common.mustSucceed(function(stats) { + assert.ok(stats.mtime instanceof Date); + assert.ok(Object.hasOwn(stats, 'blksize')); + assert.ok(Object.hasOwn(stats, 'blocks')); + // Confirm that we are not running in the context of the internal binding + // layer. + // Ref: https://github.com/nodejs/node/commit/463d6bac8b349acc462d345a6e298a76f7d06fb1 + assert.strictEqual(this, undefined); +})); + +fs.lstat('.', common.mustSucceed(function(stats) { + assert.ok(stats.mtime instanceof Date); + // Confirm that we are not running in the context of the internal binding + // layer. + // Ref: https://github.com/nodejs/node/commit/463d6bac8b349acc462d345a6e298a76f7d06fb1 + assert.strictEqual(this, undefined); +})); + +// fstat +fs.open('.', 'r', undefined, common.mustSucceed(function(fd) { + assert.ok(fd); + + fs.fstat(-0, common.mustSucceed()); + + fs.fstat(fd, common.mustSucceed(function(stats) { + assert.ok(stats.mtime instanceof Date); + fs.close(fd, assert.ifError); + // Confirm that we are not running in the context of the internal binding + // layer. + // Ref: https://github.com/nodejs/node/commit/463d6bac8b349acc462d345a6e298a76f7d06fb1 + assert.strictEqual(this, undefined); + })); + + // Confirm that we are not running in the context of the internal binding + // layer. + // Ref: https://github.com/nodejs/node/commit/463d6bac8b349acc462d345a6e298a76f7d06fb1 + assert.strictEqual(this, undefined); +})); + +// fstatSync +fs.open('.', 'r', undefined, common.mustCall(function(err, fd) { + const stats = fs.fstatSync(fd); + assert.ok(stats.mtime instanceof Date); + fs.close(fd, common.mustSucceed()); +})); + +fs.stat(__filename, common.mustSucceed((s) => { + assert.strictEqual(s.isDirectory(), false); + assert.strictEqual(s.isFile(), true); + assert.strictEqual(s.isSocket(), false); + assert.strictEqual(s.isBlockDevice(), false); + assert.strictEqual(s.isCharacterDevice(), false); + assert.strictEqual(s.isFIFO(), false); + assert.strictEqual(s.isSymbolicLink(), false); + + [ + 'dev', 'mode', 'nlink', 'uid', + 'gid', 'rdev', 'blksize', 'ino', 'size', 'blocks', + 'atime', 'mtime', 'ctime', 'birthtime', + 'atimeMs', 'mtimeMs', 'ctimeMs', 'birthtimeMs', + ].forEach(function(k) { + assert.ok(k in s, `${k} should be in Stats`); + assert.notStrictEqual(s[k], undefined, `${k} should not be undefined`); + assert.notStrictEqual(s[k], null, `${k} should not be null`); + }); + [ + 'dev', 'mode', 'nlink', 'uid', 'gid', 'rdev', 'blksize', 'ino', 'size', + 'blocks', 'atimeMs', 'mtimeMs', 'ctimeMs', 'birthtimeMs', + ].forEach((k) => { + assert.strictEqual(typeof s[k], 'number', `${k} should be a number`); + }); + ['atime', 'mtime', 'ctime', 'birthtime'].forEach((k) => { + assert.ok(s[k] instanceof Date, `${k} should be a Date`); + }); +})); + +['', false, null, undefined, {}, []].forEach((input) => { + ['fstat', 'fstatSync'].forEach((fnName) => { + assert.throws( + () => fs[fnName](input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + }); +}); + +[false, 1, {}, [], null, undefined].forEach((input) => { + assert.throws( + () => fs.lstat(input, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.lstatSync(input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.stat(input, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.statSync(input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); + +// Should not throw an error +fs.stat(__filename, undefined, common.mustCall()); + +fs.open(__filename, 'r', undefined, common.mustCall((err, fd) => { + // Should not throw an error + fs.fstat(fd, undefined, common.mustCall()); +})); + +// Should not throw an error +fs.lstat(__filename, undefined, common.mustCall()); + +{ + fs.Stats( + 0, // dev + 0, // mode + 0, // nlink + 0, // uid + 0, // gid + 0, // rdev + 0, // blksize + 0, // ino + 0, // size + 0, // blocks + Date.UTC(1970, 0, 1, 0, 0, 0), // atime + Date.UTC(1970, 0, 1, 0, 0, 0), // mtime + Date.UTC(1970, 0, 1, 0, 0, 0), // ctime + Date.UTC(1970, 0, 1, 0, 0, 0) // birthtime + ); + // common.expectWarning({ + // DeprecationWarning: [ + // ['fs.Stats constructor is deprecated.', + // 'DEP0180'], + // ] + // }); +} + +{ + // These two tests have an equivalent in ./test-fs-stat-bigint.js + + // Stats Date properties can be set before reading them + fs.stat(__filename, common.mustSucceed((s) => { + s.atime = 2; + s.mtime = 3; + s.ctime = 4; + s.birthtime = 5; + + assert.strictEqual(s.atime, 2); + assert.strictEqual(s.mtime, 3); + assert.strictEqual(s.ctime, 4); + assert.strictEqual(s.birthtime, 5); + })); + + // Stats Date properties can be set after reading them + fs.stat(__filename, common.mustSucceed((s) => { + // eslint-disable-next-line no-unused-expressions + s.atime, s.mtime, s.ctime, s.birthtime; + + s.atime = 2; + s.mtime = 3; + s.ctime = 4; + s.birthtime = 5; + + assert.strictEqual(s.atime, 2); + assert.strictEqual(s.mtime, 3); + assert.strictEqual(s.ctime, 4); + assert.strictEqual(s.birthtime, 5); + })); +} + +{ + assert.throws( + () => fs.fstat(Symbol('test'), () => {}), + { + code: 'ERR_INVALID_ARG_TYPE', + }, + ); +}