From a7c9d11b289b2140430f2666968bddb02a6f1a1f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 27 Dec 2025 10:40:24 -0800 Subject: [PATCH 1/5] std.Io: make file access time optional Some filesystems, such as ZFS, do not report atime. It's pretty useless in general, so make it an optional field in File.Stat. Also take the opportunity to make setting timestamps API more flexible and match the APIs widely available, which have UTIME_OMIT and UTIME_NOW constants that can be independently set for both fields. This is needed to handle smoothly the case when atime is null. --- lib/std/Io.zig | 6 +- lib/std/Io/Dir.zig | 28 ++++- lib/std/Io/File.zig | 40 ++++-- lib/std/Io/Threaded.zig | 272 +++++++++++++--------------------------- lib/std/Io/test.zig | 12 +- lib/std/os/linux.zig | 12 ++ 6 files changed, 159 insertions(+), 211 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 162fedca5ff73168a789d4df044c4197c62fbac5..7c3aa98e168fdbe8e1ea612dee5693ba7044b31f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -681,8 +681,7 @@ pub const VTable = struct { dirSetFileOwner: *const fn (?*anyopaque, Dir, []const u8, ?File.Uid, ?File.Gid, Dir.SetFileOwnerOptions) Dir.SetFileOwnerError!void, dirSetPermissions: *const fn (?*anyopaque, Dir, Dir.Permissions) Dir.SetPermissionsError!void, dirSetFilePermissions: *const fn (?*anyopaque, Dir, []const u8, File.Permissions, Dir.SetFilePermissionsOptions) Dir.SetFilePermissionsError!void, - dirSetTimestamps: *const fn (?*anyopaque, Dir, []const u8, last_accessed: Timestamp, last_modified: Timestamp, Dir.SetTimestampsOptions) Dir.SetTimestampsError!void, - dirSetTimestampsNow: *const fn (?*anyopaque, Dir, []const u8, Dir.SetTimestampsOptions) Dir.SetTimestampsError!void, + dirSetTimestamps: *const fn (?*anyopaque, Dir, []const u8, Dir.SetTimestampsOptions) Dir.SetTimestampsError!void, dirHardLink: *const fn (?*anyopaque, old_dir: Dir, old_sub_path: []const u8, new_dir: Dir, new_sub_path: []const u8, Dir.HardLinkOptions) Dir.HardLinkError!void, fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat, @@ -705,8 +704,7 @@ pub const VTable = struct { fileSetLength: *const fn (?*anyopaque, File, u64) File.SetLengthError!void, fileSetOwner: *const fn (?*anyopaque, File, ?File.Uid, ?File.Gid) File.SetOwnerError!void, fileSetPermissions: *const fn (?*anyopaque, File, File.Permissions) File.SetPermissionsError!void, - fileSetTimestamps: *const fn (?*anyopaque, File, last_accessed: Timestamp, last_modified: Timestamp) File.SetTimestampsError!void, - fileSetTimestampsNow: *const fn (?*anyopaque, File) File.SetTimestampsError!void, + fileSetTimestamps: *const fn (?*anyopaque, File, File.SetTimestampsOptions) File.SetTimestampsError!void, fileLock: *const fn (?*anyopaque, File, File.Lock) File.LockError!void, fileTryLock: *const fn (?*anyopaque, File, File.Lock) File.LockError!bool, fileUnlock: *const fn (?*anyopaque, File) void, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index e7b18066160adf95cbace687eb00843eb9175718..577e114c40cffb62ec505f56b7d38c96c7827611 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -615,7 +615,10 @@ pub fn updateFile( error.WriteFailed => return atomic_file.file_writer.err.?, }; try atomic_file.flush(); - try atomic_file.file_writer.file.setTimestamps(io, src_stat.atime, src_stat.mtime); + try atomic_file.file_writer.file.setTimestamps(io, .{ + .access_timestamp = .init(src_stat.atime), + .modify_timestamp = .init(src_stat.mtime), + }); try atomic_file.renameIntoPlace(); return .stale; } @@ -1825,6 +1828,8 @@ pub const SetTimestampsError = File.SetTimestampsError || PathNameError; pub const SetTimestampsOptions = struct { follow_symlinks: bool = true, + access_timestamp: File.SetTimestamp = .unchanged, + modify_timestamp: File.SetTimestamp = .unchanged, }; /// The granularity that ultimately is stored depends on the combination of @@ -1834,18 +1839,29 @@ pub fn setTimestamps( dir: Dir, io: Io, sub_path: []const u8, - last_accessed: Io.Timestamp, - last_modified: Io.Timestamp, options: SetTimestampsOptions, ) SetTimestampsError!void { - return io.vtable.dirSetTimestamps(io.userdata, dir, sub_path, last_accessed, last_modified, options); + return io.vtable.dirSetTimestamps(io.userdata, dir, sub_path, options); } +pub const SetTimestampsNowOptions = struct { + follow_symlinks: bool = true, +}; + /// Sets the accessed and modification timestamps of the provided path to the /// current wall clock time. /// /// The granularity that ultimately is stored depends on the combination of /// operating system and file system. -pub fn setTimestampsNow(dir: Dir, io: Io, sub_path: []const u8, options: SetTimestampsOptions) SetTimestampsError!void { - return io.vtable.fileSetTimestampsNow(io.userdata, dir, sub_path, options); +pub fn setTimestampsNow( + dir: Dir, + io: Io, + sub_path: []const u8, + options: SetTimestampsNowOptions, +) SetTimestampsError!void { + return io.vtable.fileSetTimestamps(io.userdata, dir, sub_path, .{ + .follow_symlinks = options.follow_symlinks, + .access_timestamp = .now, + .modify_timestamp = .now, + }); } diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index ee30103af0bee7e04d048da8da6cf66f9aa0f9a8..bbe550a9cc515afd5675040234a4bc43bc36dab2 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -53,7 +53,12 @@ pub const Stat = struct { permissions: Permissions, kind: Kind, /// Last access time in nanoseconds, relative to UTC 1970-01-01. - atime: Io.Timestamp, + /// + /// Filesystems generally find this value problematic to keep updated since + /// it turns read-only file system accesses into file system mutations. + /// Some systems report stale values, and some systems explicitly refuse to + /// report this value. The latter case is handled by `null`. + atime: ?Io.Timestamp, /// Last modification time in nanoseconds, relative to UTC 1970-01-01. mtime: Io.Timestamp, /// Last status/metadata change time in nanoseconds, relative to UTC 1970-01-01. @@ -478,16 +483,30 @@ pub const SetTimestampsError = error{ ReadOnlyFileSystem, } || Io.Cancelable || Io.UnexpectedError; +pub const SetTimestampsOptions = struct { + access_timestamp: SetTimestamp = .unchanged, + modify_timestamp: SetTimestamp = .unchanged, +}; + +pub const SetTimestamp = union(enum) { + /// Leave the existing timestamp unmodified. + unchanged, + /// Set to current time using `Io.Clock.real`. + now, + /// Set to provided timestamp using `Io.Clock.real`. + new: Io.Timestamp, + + /// Convenience for interacting with `Stat`, in which `null` indicates `unchanged`. + pub fn init(optional: ?Io.Timestamp) SetTimestamp { + return if (optional) |t| .{ .new = t } else .unchanged; + } +}; + /// The granularity that ultimately is stored depends on the combination of /// operating system and file system. When a value as provided that exceeds /// this range, the value is clamped to the maximum. -pub fn setTimestamps( - file: File, - io: Io, - last_accessed: Io.Timestamp, - last_modified: Io.Timestamp, -) SetTimestampsError!void { - return io.vtable.fileSetTimestamps(io.userdata, file, last_accessed, last_modified); +pub fn setTimestamps(file: File, io: Io, options: SetTimestampsOptions) SetTimestampsError!void { + return io.vtable.fileSetTimestamps(io.userdata, file, options); } /// Sets the accessed and modification timestamps of `file` to the current wall @@ -496,7 +515,10 @@ pub fn setTimestamps( /// The granularity that ultimately is stored depends on the combination of /// operating system and file system. pub fn setTimestampsNow(file: File, io: Io) SetTimestampsError!void { - return io.vtable.fileSetTimestampsNow(io.userdata, file); + return io.vtable.fileSetTimestamps(io.userdata, file, .{ + .access_timestamp = .now, + .modify_timestamp = .now, + }); } /// Returns 0 on stream end or if `buffer` has no space available for data. diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index d6d44aa775289a9dbee95f7edaac8c5e2181d30a..419552c1879293a2b585ab55fb79ccb2c72c3bd9 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -235,6 +235,24 @@ const Thread = struct { ) orelse return; } + fn endSyscallErrnoBug(thread: *Thread, err: posix.E) Io.UnexpectedError { + @branchHint(.cold); + thread.endSyscall(); + return errnoBug(err); + } + + fn endSyscallUnexpectedErrno(thread: *Thread, err: posix.E) Io.UnexpectedError { + @branchHint(.cold); + thread.endSyscall(); + return posix.unexpectedErrno(err); + } + + /// inline to make error return traces slightly shallower. + inline fn endSyscallError(thread: *Thread, err: anytype) @TypeOf(err) { + thread.endSyscall(); + return err; + } + fn currentSignalId() SignaleeId { return if (std.Thread.use_pthreads) std.c.pthread_self() else std.Thread.getCurrentId(); } @@ -811,7 +829,6 @@ pub fn io(t: *Threaded) Io { .dirSetPermissions = dirSetPermissions, .dirSetFilePermissions = dirSetFilePermissions, .dirSetTimestamps = dirSetTimestamps, - .dirSetTimestampsNow = dirSetTimestampsNow, .dirHardLink = dirHardLink, .fileStat = fileStat, @@ -833,7 +850,6 @@ pub fn io(t: *Threaded) Io { .fileSetOwner = fileSetOwner, .fileSetPermissions = fileSetPermissions, .fileSetTimestamps = fileSetTimestamps, - .fileSetTimestampsNow = fileSetTimestampsNow, .fileLock = fileLock, .fileTryLock = fileTryLock, .fileUnlock = fileUnlock, @@ -947,7 +963,6 @@ pub fn ioBasic(t: *Threaded) Io { .dirSetPermissions = dirSetPermissions, .dirSetFilePermissions = dirSetFilePermissions, .dirSetTimestamps = dirSetTimestamps, - .dirSetTimestampsNow = dirSetTimestampsNow, .dirHardLink = dirHardLink, .fileStat = fileStat, @@ -969,7 +984,6 @@ pub fn ioBasic(t: *Threaded) Io { .fileSetOwner = fileSetOwner, .fileSetPermissions = fileSetPermissions, .fileSetTimestamps = fileSetTimestamps, - .fileSetTimestampsNow = fileSetTimestampsNow, .fileLock = fileLock, .fileTryLock = fileTryLock, .fileUnlock = fileUnlock, @@ -5977,8 +5991,6 @@ fn dirSetTimestamps( userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, - last_accessed: Io.Timestamp, - last_modified: Io.Timestamp, options: Dir.SetTimestampsOptions, ) Dir.SetTimestampsError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); @@ -5992,9 +6004,13 @@ fn dirSetTimestamps( @panic("TODO implement dirSetTimestamps wasi"); } - const times: [2]posix.timespec = .{ - timestampToPosix(last_accessed.nanoseconds), - timestampToPosix(last_modified.nanoseconds), + var times_buffer: [2]posix.timespec = undefined; + const times = if (options.modify_timestamp == .now and options.access_timestamp == .now) null else p: { + times_buffer = .{ + setTimestampToPosix(options.access_timestamp), + setTimestampToPosix(options.modify_timestamp), + }; + break :p ×_buffer; }; const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0; @@ -6003,80 +6019,26 @@ fn dirSetTimestamps( const sub_path_posix = try pathToPosix(sub_path, &path_buffer); try current_thread.beginSyscall(); - while (true) { - switch (posix.errno(posix.system.utimensat(dir.handle, sub_path_posix, ×, flags))) { - .SUCCESS => return current_thread.endSyscall(), - .INTR => { - try current_thread.checkCancel(); - continue; - }, - else => |e| { - current_thread.endSyscall(); - switch (e) { - .ACCES => return error.AccessDenied, - .PERM => return error.PermissionDenied, - .BADF => |err| return errnoBug(err), // always a race condition - .FAULT => |err| return errnoBug(err), - .INVAL => |err| return errnoBug(err), - .ROFS => return error.ReadOnlyFileSystem, - else => |err| return posix.unexpectedErrno(err), - } - }, - } - } -} - -fn dirSetTimestampsNow( - userdata: ?*anyopaque, - dir: Dir, - sub_path: []const u8, - options: Dir.SetTimestampsOptions, -) Dir.SetTimestampsError!void { - const t: *Threaded = @ptrCast(@alignCast(userdata)); - const current_thread = Thread.getCurrent(t); - - if (is_windows) { - @panic("TODO implement dirSetTimestampsNow windows"); - } - - if (native_os == .wasi and !builtin.link_libc) { - @panic("TODO implement dirSetTimestampsNow wasi"); - } - - const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0; - - var path_buffer: [posix.PATH_MAX]u8 = undefined; - const sub_path_posix = try pathToPosix(sub_path, &path_buffer); - - try current_thread.beginSyscall(); - while (true) { - switch (posix.errno(posix.system.utimensat(dir.handle, sub_path_posix, null, flags))) { - .SUCCESS => return current_thread.endSyscall(), - .INTR => { - try current_thread.checkCancel(); - continue; - }, - else => |e| { - current_thread.endSyscall(); - switch (e) { - .ACCES => return error.AccessDenied, - .PERM => return error.PermissionDenied, - .BADF => |err| return errnoBug(err), // always a race condition - .FAULT => |err| return errnoBug(err), - .INVAL => |err| return errnoBug(err), - .ROFS => return error.ReadOnlyFileSystem, - else => |err| return posix.unexpectedErrno(err), - } - }, - } - } + while (true) switch (posix.errno(posix.system.utimensat(dir.handle, sub_path_posix, times, flags))) { + .SUCCESS => return current_thread.endSyscall(), + .INTR => { + try current_thread.checkCancel(); + continue; + }, + .BADF => |err| return current_thread.endSyscallErrnoBug(err), // always a race condition + .FAULT => |err| return current_thread.endSyscallErrnoBug(err), + .INVAL => |err| return current_thread.endSyscallErrnoBug(err), + .ACCES => return current_thread.endSyscallError(error.AccessDenied), + .PERM => return current_thread.endSyscallError(error.PermissionDenied), + .ROFS => return current_thread.endSyscallError(error.ReadOnlyFileSystem), + else => |err| return current_thread.endSyscallUnexpectedErrno(err), + }; } fn fileSetTimestamps( userdata: ?*anyopaque, file: File, - last_accessed: Io.Timestamp, - last_modified: Io.Timestamp, + options: File.SetTimestampsOptions, ) File.SetTimestampsError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); const current_thread = Thread.getCurrent(t); @@ -6084,8 +6046,8 @@ fn fileSetTimestamps( if (is_windows) { try current_thread.checkCancel(); - const atime_ft = windows.nanoSecondsToFileTime(last_accessed); - const mtime_ft = windows.nanoSecondsToFileTime(last_modified); + const atime_ft = windows.nanoSecondsToFileTime(options.access_time); + const mtime_ft = windows.nanoSecondsToFileTime(options.modify_time); // https://github.com/ziglang/zig/issues/1840 const rc = windows.kernel32.SetFileTime(file.handle, null, &atime_ft, &mtime_ft); @@ -6097,123 +6059,53 @@ fn fileSetTimestamps( return; } - const times: [2]posix.timespec = .{ - timestampToPosix(last_accessed.nanoseconds), - timestampToPosix(last_modified.nanoseconds), - }; - if (native_os == .wasi and !builtin.link_libc) { - const atim = times[0].toTimestamp(); - const mtim = times[1].toTimestamp(); + const atim = timestampToPosix(options.access_time.nanoseconds).toTimestamp(); + const mtim = timestampToPosix(options.modify_time.nanoseconds).toTimestamp(); try current_thread.beginSyscall(); - while (true) { - switch (std.os.wasi.fd_filestat_set_times(file.handle, atim, mtim, .{ - .ATIM = true, - .MTIM = true, - })) { - .SUCCESS => return current_thread.endSyscall(), - .INTR => { - try current_thread.checkCancel(); - continue; - }, - else => |e| { - current_thread.endSyscall(); - switch (e) { - .ACCES => return error.AccessDenied, - .PERM => return error.PermissionDenied, - .BADF => |err| return errnoBug(err), // File descriptor use-after-free. - .FAULT => |err| return errnoBug(err), - .INVAL => |err| return errnoBug(err), - .ROFS => return error.ReadOnlyFileSystem, - else => |err| return posix.unexpectedErrno(err), - } - }, - } - } - } - - try current_thread.beginSyscall(); - while (true) { - switch (posix.errno(posix.system.futimens(file.handle, ×))) { + while (true) switch (std.os.wasi.fd_filestat_set_times(file.handle, atim, mtim, .{ + .ATIM = true, + .MTIM = true, + })) { .SUCCESS => return current_thread.endSyscall(), .INTR => { try current_thread.checkCancel(); continue; }, - else => |e| { - current_thread.endSyscall(); - switch (e) { - .ACCES => return error.AccessDenied, - .PERM => return error.PermissionDenied, - .BADF => |err| return errnoBug(err), // always a race condition - .FAULT => |err| return errnoBug(err), - .INVAL => |err| return errnoBug(err), - .ROFS => return error.ReadOnlyFileSystem, - else => |err| return posix.unexpectedErrno(err), - } - }, - } + .BADF => |err| return current_thread.endSyscallErrnoBug(err), // File descriptor use-after-free. + .FAULT => |err| return current_thread.endSyscallErrnoBug(err), + .INVAL => |err| return current_thread.endSyscallErrnoBug(err), + .ACCES => return current_thread.endSyscallErrnoBug(error.AccessDenied), + .PERM => return current_thread.endSyscallErrnoBug(error.PermissionDenied), + .ROFS => return current_thread.endSyscallErrnoBug(error.ReadOnlyFileSystem), + else => |err| return current_thread.endSyscallUnexpectedErrno(err), + }; } -} -fn fileSetTimestampsNow(userdata: ?*anyopaque, file: File) File.SetTimestampsError!void { - const t: *Threaded = @ptrCast(@alignCast(userdata)); - const current_thread = Thread.getCurrent(t); - - if (is_windows) { - @panic("TODO implement fileSetTimestampsNow windows"); - } - - if (native_os == .wasi and !builtin.link_libc) { - try current_thread.beginSyscall(); - while (true) { - switch (std.os.wasi.fd_filestat_set_times(file.handle, 0, 0, .{ - .ATIM_NOW = true, - .MTIM_NOW = true, - })) { - .SUCCESS => return current_thread.endSyscall(), - .INTR => { - try current_thread.checkCancel(); - continue; - }, - else => |e| { - current_thread.endSyscall(); - switch (e) { - .ACCES => return error.AccessDenied, - .PERM => return error.PermissionDenied, - .BADF => |err| return errnoBug(err), // always a race condition - .FAULT => |err| return errnoBug(err), - .INVAL => |err| return errnoBug(err), - .ROFS => return error.ReadOnlyFileSystem, - else => |err| return posix.unexpectedErrno(err), - } - }, - } - } - } + var times_buffer: [2]posix.timespec = undefined; + const times = if (options.modify_timestamp == .now and options.access_timestamp == .now) null else p: { + times_buffer = .{ + setTimestampToPosix(options.access_timestamp), + setTimestampToPosix(options.modify_timestamp), + }; + break :p ×_buffer; + }; try current_thread.beginSyscall(); - while (true) { - switch (posix.errno(posix.system.futimens(file.handle, null))) { - .SUCCESS => return current_thread.endSyscall(), - .INTR => { - try current_thread.checkCancel(); - continue; - }, - else => |e| { - current_thread.endSyscall(); - switch (e) { - .ACCES => return error.AccessDenied, - .PERM => return error.PermissionDenied, - .BADF => |err| return errnoBug(err), // always a race condition - .FAULT => |err| return errnoBug(err), - .INVAL => |err| return errnoBug(err), - .ROFS => return error.ReadOnlyFileSystem, - else => |err| return posix.unexpectedErrno(err), - } - }, - } - } + while (true) switch (posix.errno(posix.system.futimens(file.handle, times))) { + .SUCCESS => return current_thread.endSyscall(), + .INTR => { + try current_thread.checkCancel(); + continue; + }, + .BADF => |err| return current_thread.endSyscallErrnoBug(err), // always a race condition + .FAULT => |err| return current_thread.endSyscallErrnoBug(err), + .INVAL => |err| return current_thread.endSyscallErrnoBug(err), + .ACCES => return current_thread.endSyscallError(error.AccessDenied), + .PERM => return current_thread.endSyscallError(error.PermissionDenied), + .ROFS => return current_thread.endSyscallError(error.ReadOnlyFileSystem), + else => |err| return current_thread.endSyscallUnexpectedErrno(err), + }; } const windows_lock_range_off: windows.LARGE_INTEGER = 0; @@ -11283,6 +11175,14 @@ fn timestampToPosix(nanoseconds: i96) posix.timespec { }; } +fn setTimestampToPosix(set_ts: File.SetTimestamp) posix.timespec { + return switch (set_ts) { + .unchanged => .OMIT, + .now => .NOW, + .new => |t| timestampToPosix(t.nanoseconds), + }; +} + fn pathToPosix(file_path: []const u8, buffer: *[posix.PATH_MAX]u8) Dir.PathNameError![:0]u8 { if (std.mem.containsAtLeastScalar2(u8, file_path, 0, 1)) return error.BadPathName; // >= rather than > to make room for the null byte diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index 796800f000ad78ad915783219a861d2374161c1e..a317c822cb67c99728cd98742b8b005e90fd15df 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -174,14 +174,14 @@ test "setTimestamps" { defer file.close(io); const stat_old = try file.stat(io); + // Set atime and mtime to 5s before - try file.setTimestamps( - io, - stat_old.atime.subDuration(.fromSeconds(5)), - stat_old.mtime.subDuration(.fromSeconds(5)), - ); + try file.setTimestamps(io, .{ + .access_timestamp = if (stat_old.atime) |atime| .{ .new = atime.subDuration(.fromSeconds(5)) } else .unchanged, + .modify_timestamp = .{ .new = stat_old.mtime.subDuration(.fromSeconds(5)) }, + }); const stat_new = try file.stat(io); - try expect(stat_new.atime.nanoseconds < stat_old.atime.nanoseconds); + if (stat_old.atime) |old_atime| try expect(stat_new.atime.?.nanoseconds < old_atime.nanoseconds); try expect(stat_new.mtime.nanoseconds < stat_old.mtime.nanoseconds); } diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 96b4c8ee6f259d6617c4caee74fb8acbaba77f4f..fb08830fcc5ca2a3ebbafa1d3027d705610f9b06 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -8423,6 +8423,18 @@ pub const kernel_timespec = extern struct { pub const timespec = if (native_arch == .hexagon or native_arch == .riscv32) kernel_timespec else extern struct { sec: isize, nsec: isize, + + /// For use with `utimensat` and `futimens`. + pub const NOW: timespec = .{ + .sec = 0, + .nsec = 0x3fffffff, + }; + + /// For use with `utimensat` and `futimens`. + pub const OMIT: timespec = .{ + .sec = 0, + .nsec = 0x3ffffffe, + }; }; pub const XDP = struct { -- 2.54.0 From dd98188ce06c09c666414e45ab54a4fd260b0d59 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 27 Dec 2025 10:52:17 -0800 Subject: [PATCH 2/5] std.Io.Threaded: mostly implement fileSetTimestamps for Windows it's still missing the case of setting to now (which was also not implemented before) --- lib/std/Io/Threaded.zig | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 419552c1879293a2b585ab55fb79ccb2c72c3bd9..730a1a3b28ff07a307db3591acbc6e59bea2d65b 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -6046,11 +6046,34 @@ fn fileSetTimestamps( if (is_windows) { try current_thread.checkCancel(); - const atime_ft = windows.nanoSecondsToFileTime(options.access_time); - const mtime_ft = windows.nanoSecondsToFileTime(options.modify_time); + var access_time_buffer: windows.FILETIME = undefined; + var modify_time_buffer: windows.FILETIME = undefined; + var system_time_buffer: windows.LARGE_INTEGER = undefined; + + if (options.access_timestamp == .now or options.modify_timestamp == .now) { + system_time_buffer = windows.ntdll.RtlGetSystemTimePrecise(); + } + + const access_ptr = switch (options.access_timestamp) { + .unchanged => null, + .now => @panic("TODO do SystemTimeToFileTime logic here"), + .new => |ts| p: { + access_time_buffer = windows.nanoSecondsToFileTime(ts); + break :p &access_time_buffer; + }, + }; + + const modify_ptr = switch (options.modify_timestamp) { + .unchanged => null, + .now => @panic("TODO do SystemTimeToFileTime logic here"), + .new => |ts| p: { + modify_time_buffer = windows.nanoSecondsToFileTime(ts); + break :p &modify_time_buffer; + }, + }; // https://github.com/ziglang/zig/issues/1840 - const rc = windows.kernel32.SetFileTime(file.handle, null, &atime_ft, &mtime_ft); + const rc = windows.kernel32.SetFileTime(file.handle, null, access_ptr, modify_ptr); if (rc == 0) { switch (windows.GetLastError()) { else => |err| return windows.unexpectedError(err), @@ -6060,8 +6083,8 @@ fn fileSetTimestamps( } if (native_os == .wasi and !builtin.link_libc) { - const atim = timestampToPosix(options.access_time.nanoseconds).toTimestamp(); - const mtim = timestampToPosix(options.modify_time.nanoseconds).toTimestamp(); + const atim = timestampToPosix(options.access_timestamp.nanoseconds).toTimestamp(); + const mtim = timestampToPosix(options.modify_timestamp.nanoseconds).toTimestamp(); try current_thread.beginSyscall(); while (true) switch (std.os.wasi.fd_filestat_set_times(file.handle, atim, mtim, .{ .ATIM = true, -- 2.54.0 From 10e72a8cad48e3e9e68f51d9e6769e30566029b1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 27 Dec 2025 11:01:16 -0800 Subject: [PATCH 3/5] std.Io.Threaded: implement fileSetTimestamps on WASI --- lib/std/Io/Threaded.zig | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 730a1a3b28ff07a307db3591acbc6e59bea2d65b..18596ddd1647598018d9ec656480f14df44f6c8a 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -6083,13 +6083,30 @@ fn fileSetTimestamps( } if (native_os == .wasi and !builtin.link_libc) { - const atim = timestampToPosix(options.access_timestamp.nanoseconds).toTimestamp(); - const mtim = timestampToPosix(options.modify_timestamp.nanoseconds).toTimestamp(); + var atime: std.os.wasi.timestamp_t = 0; + var mtime: std.os.wasi.timestamp_t = 0; + var flags: std.os.wasi.fstflags_t = .{}; + + switch (options.access_timestamp) { + .unchanged => {}, + .now => flags.ATIM_NOW = true, + .new => |ts| { + atime = timestampToPosix(ts.nanoseconds).toTimestamp(); + flags.ATIM = true; + }, + } + + switch (options.modify_timestamp) { + .unchanged => {}, + .now => flags.MTIM_NOW = true, + .new => |ts| { + mtime = timestampToPosix(ts.nanoseconds).toTimestamp(); + flags.MTIM = true; + }, + } + try current_thread.beginSyscall(); - while (true) switch (std.os.wasi.fd_filestat_set_times(file.handle, atim, mtim, .{ - .ATIM = true, - .MTIM = true, - })) { + while (true) switch (std.os.wasi.fd_filestat_set_times(file.handle, atime, mtime, flags)) { .SUCCESS => return current_thread.endSyscall(), .INTR => { try current_thread.checkCancel(); @@ -6098,9 +6115,9 @@ fn fileSetTimestamps( .BADF => |err| return current_thread.endSyscallErrnoBug(err), // File descriptor use-after-free. .FAULT => |err| return current_thread.endSyscallErrnoBug(err), .INVAL => |err| return current_thread.endSyscallErrnoBug(err), - .ACCES => return current_thread.endSyscallErrnoBug(error.AccessDenied), - .PERM => return current_thread.endSyscallErrnoBug(error.PermissionDenied), - .ROFS => return current_thread.endSyscallErrnoBug(error.ReadOnlyFileSystem), + .ACCES => return current_thread.endSyscallError(error.AccessDenied), + .PERM => return current_thread.endSyscallError(error.PermissionDenied), + .ROFS => return current_thread.endSyscallError(error.ReadOnlyFileSystem), else => |err| return current_thread.endSyscallUnexpectedErrno(err), }; } -- 2.54.0 From c0809c9b68ad33b9963ff3da1a6ad53ca2c8b6f3 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 27 Dec 2025 11:07:28 -0800 Subject: [PATCH 4/5] std: add more timespec OMIT and NOW definitions --- lib/std/c.zig | 36 ++++++++++++++++++++++++++++++++++++ lib/std/os/linux.zig | 12 ++++++++++++ 2 files changed, 48 insertions(+) diff --git a/lib/std/c.zig b/lib/std/c.zig index fdd544cba944e5f1276286e0a120e38330cfce24..2a7579f2606b06e0a8cf16ea324a73d2ee7743f7 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -114,6 +114,18 @@ pub const timespec = switch (native_os) { return @as(wasi.timestamp_t, @intCast(ts.sec * 1_000_000_000)) + @as(wasi.timestamp_t, @intCast(ts.nsec)); } + + /// For use with `utimensat` and `futimens`. + pub const NOW: timespec = .{ + .sec = 0, + .nsec = 0x3fffffff, + }; + + /// For use with `utimensat` and `futimens`. + pub const OMIT: timespec = .{ + .sec = 0, + .nsec = 0x3ffffffe, + }; }, // https://github.com/SerenityOS/serenity/blob/0a78056453578c18e0a04a0b45ebfb1c96d59005/Kernel/API/POSIX/time.h#L17-L20 .windows, .serenity => extern struct { @@ -123,10 +135,34 @@ pub const timespec = switch (native_os) { .dragonfly, .freebsd, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => extern struct { sec: isize, nsec: isize, + + /// For use with `utimensat` and `futimens`. + pub const NOW: timespec = .{ + .sec = 0, + .nsec = -1, + }; + + /// For use with `utimensat` and `futimens`. + pub const OMIT: timespec = .{ + .sec = 0, + .nsec = -2, + }; }, .netbsd, .illumos => extern struct { sec: i64, nsec: isize, + + /// For use with `utimensat` and `futimens`. + pub const NOW: timespec = .{ + .sec = 0, + .nsec = 0x3fffffff, + }; + + /// For use with `utimensat` and `futimens`. + pub const OMIT: timespec = .{ + .sec = 0, + .nsec = 0x3ffffffe, + }; }, .openbsd, .haiku => extern struct { sec: time_t, diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index fb08830fcc5ca2a3ebbafa1d3027d705610f9b06..174ef64d95f8f2e79c27aebba1055ac4457e74a0 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -8417,6 +8417,18 @@ pub const timezone = extern struct { pub const kernel_timespec = extern struct { sec: i64, nsec: i64, + + /// For use with `utimensat` and `futimens`. + pub const NOW: timespec = .{ + .sec = 0, + .nsec = 0x3fffffff, + }; + + /// For use with `utimensat` and `futimens`. + pub const OMIT: timespec = .{ + .sec = 0, + .nsec = 0x3ffffffe, + }; }; // https://github.com/ziglang/zig/issues/4726#issuecomment-2190337877 -- 2.54.0 From 212968c574ada30dd593dddf6d5c0cfd962459e0 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 27 Dec 2025 11:18:16 -0800 Subject: [PATCH 5/5] std.Io.Threaded: handle missing atime from statx --- lib/std/Io/Threaded.zig | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 18596ddd1647598018d9ec656480f14df44f6c8a..fdd4e6c34a0550cf2ea7f421c0699bd6a81a106f 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1934,7 +1934,7 @@ fn dirStatFileLinux( try current_thread.beginSyscall(); while (true) { var statx = std.mem.zeroes(linux.Statx); - switch (sys.errno(sys.statx(dir.handle, sub_path_posix, flags, linux_statx_mask, &statx))) { + switch (sys.errno(sys.statx(dir.handle, sub_path_posix, flags, linux_statx_request, &statx))) { .SUCCESS => { current_thread.endSyscall(); return statFromLinux(&statx); @@ -2169,7 +2169,7 @@ fn fileStatLinux(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { try current_thread.beginSyscall(); while (true) { var statx = std.mem.zeroes(linux.Statx); - switch (sys.errno(sys.statx(file.handle, "", linux.AT.EMPTY_PATH, linux_statx_mask, &statx))) { + switch (sys.errno(sys.statx(file.handle, "", linux.AT.EMPTY_PATH, linux_statx_request, &statx))) { .SUCCESS => { current_thread.endSyscall(); return statFromLinux(&statx); @@ -11101,7 +11101,7 @@ fn clockToWasi(clock: Io.Clock) std.os.wasi.clockid_t { }; } -const linux_statx_mask: std.os.linux.STATX = .{ +const linux_statx_request: std.os.linux.STATX = .{ .TYPE = true, .MODE = true, .ATIME = true, @@ -11112,14 +11112,22 @@ const linux_statx_mask: std.os.linux.STATX = .{ .NLINK = true, }; +const linux_statx_check: std.os.linux.STATX = .{ + .TYPE = true, + .MODE = true, + .ATIME = false, + .MTIME = true, + .CTIME = true, + .INO = true, + .SIZE = true, + .NLINK = true, +}; + fn statFromLinux(stx: *const std.os.linux.Statx) Io.UnexpectedError!File.Stat { const actual_mask_int: u32 = @bitCast(stx.mask); - const wanted_mask_int: u32 = @bitCast(linux_statx_mask); + const wanted_mask_int: u32 = @bitCast(linux_statx_check); if ((actual_mask_int | wanted_mask_int) != actual_mask_int) return error.Unexpected; - const atime = stx.atime; - const mtime = stx.mtime; - const ctime = stx.ctime; return .{ .inode = stx.ino, .nlink = stx.nlink, @@ -11135,9 +11143,11 @@ fn statFromLinux(stx: *const std.os.linux.Statx) Io.UnexpectedError!File.Stat { std.os.linux.S.IFSOCK => .unix_domain_socket, else => .unknown, }, - .atime = .{ .nanoseconds = @intCast(@as(i128, atime.sec) * std.time.ns_per_s + atime.nsec) }, - .mtime = .{ .nanoseconds = @intCast(@as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec) }, - .ctime = .{ .nanoseconds = @intCast(@as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec) }, + .atime = if (!stx.mask.ATIME) null else .{ + .nanoseconds = @intCast(@as(i128, stx.atime.sec) * std.time.ns_per_s + stx.atime.nsec), + }, + .mtime = .{ .nanoseconds = @intCast(@as(i128, stx.mtime.sec) * std.time.ns_per_s + stx.mtime.nsec) }, + .ctime = .{ .nanoseconds = @intCast(@as(i128, stx.ctime.sec) * std.time.ns_per_s + stx.ctime.nsec) }, }; } -- 2.54.0