Commit 89412fda77

Andrew Kelley <andrew@ziglang.org>
2025-10-09 05:35:34
std.Io: implement fileStat
1 parent 69b54b0
Changed files (7)
lib/std/debug/SelfInfo/Elf.zig
@@ -336,6 +336,7 @@ const Module = struct {
         var elf_file = load_result catch |err| switch (err) {
             error.OutOfMemory,
             error.Unexpected,
+            error.Canceled,
             => |e| return e,
 
             error.Overflow,
lib/std/debug/ElfFile.zig
@@ -108,6 +108,7 @@ pub const LoadError = error{
     LockedMemoryLimitExceeded,
     ProcessFdQuotaExceeded,
     SystemFdQuotaExceeded,
+    Canceled,
     Unexpected,
 };
 
@@ -408,7 +409,7 @@ fn loadInner(
     arena: Allocator,
     elf_file: std.fs.File,
     opt_crc: ?u32,
-) (LoadError || error{CrcMismatch})!LoadInnerResult {
+) (LoadError || error{ CrcMismatch, Canceled })!LoadInnerResult {
     const mapped_mem: []align(std.heap.page_size_min) const u8 = mapped: {
         const file_len = std.math.cast(
             usize,
lib/std/fs/File.zig
@@ -1,10 +1,12 @@
+const File = @This();
+
 const builtin = @import("builtin");
-const Os = std.builtin.Os;
 const native_os = builtin.os.tag;
 const is_windows = native_os == .windows;
 
-const File = @This();
 const std = @import("../std.zig");
+const Io = std.Io;
+const Os = std.builtin.Os;
 const Allocator = std.mem.Allocator;
 const posix = std.posix;
 const math = std.math;
@@ -17,12 +19,12 @@ const Alignment = std.mem.Alignment;
 /// The OS-specific file descriptor or file handle.
 handle: Handle,
 
-pub const Handle = std.Io.File.Handle;
-pub const Mode = std.Io.File.Mode;
-pub const INode = std.Io.File.INode;
+pub const Handle = Io.File.Handle;
+pub const Mode = Io.File.Mode;
+pub const INode = Io.File.INode;
 pub const Uid = posix.uid_t;
 pub const Gid = posix.gid_t;
-pub const Kind = std.Io.File.Kind;
+pub const Kind = Io.File.Kind;
 
 /// This is the default mode given to POSIX operating systems for creating
 /// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first,
@@ -386,7 +388,7 @@ pub fn mode(self: File) ModeError!Mode {
     return (try self.stat()).mode;
 }
 
-pub const Stat = std.Io.File.Stat;
+pub const Stat = Io.File.Stat;
 
 pub const StatError = posix.FStatError;
 
@@ -436,39 +438,9 @@ pub fn stat(self: File) StatError!Stat {
         };
     }
 
-    if (builtin.os.tag == .wasi and !builtin.link_libc) {
-        const st = try std.os.fstat_wasi(self.handle);
-        return Stat.fromWasi(st);
-    }
-
-    if (builtin.os.tag == .linux) {
-        var stx = std.mem.zeroes(linux.Statx);
-
-        const rc = linux.statx(
-            self.handle,
-            "",
-            linux.AT.EMPTY_PATH,
-            linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME,
-            &stx,
-        );
-
-        return switch (linux.E.init(rc)) {
-            .SUCCESS => Stat.fromLinux(stx),
-            .ACCES => unreachable,
-            .BADF => unreachable,
-            .FAULT => unreachable,
-            .INVAL => unreachable,
-            .LOOP => unreachable,
-            .NAMETOOLONG => unreachable,
-            .NOENT => unreachable,
-            .NOMEM => error.SystemResources,
-            .NOTDIR => unreachable,
-            else => |err| posix.unexpectedErrno(err),
-        };
-    }
-
-    const st = try posix.fstat(self.handle);
-    return Stat.fromPosix(st);
+    var threaded: Io.Threaded = .init_single_threaded;
+    const io = threaded.io();
+    return Io.File.stat(.{ .handle = self.handle }, io);
 }
 
 pub const ChmodError = posix.FChmodError;
@@ -785,8 +757,8 @@ pub fn pwritev(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError
     return posix.pwritev(self.handle, iovecs, offset);
 }
 
-/// Deprecated in favor of `std.Io.File.Reader`.
-pub const Reader = std.Io.File.Reader;
+/// Deprecated in favor of `Io.File.Reader`.
+pub const Reader = Io.File.Reader;
 
 pub const Writer = struct {
     file: File,
@@ -799,7 +771,7 @@ pub const Writer = struct {
     copy_file_range_err: ?CopyFileRangeError = null,
     fcopyfile_err: ?FcopyfileError = null,
     seek_err: ?Writer.SeekError = null,
-    interface: std.Io.Writer,
+    interface: Io.Writer,
 
     pub const Mode = Reader.Mode;
 
@@ -845,13 +817,13 @@ pub const Writer = struct {
         };
     }
 
-    pub fn initInterface(buffer: []u8) std.Io.Writer {
+    pub fn initInterface(buffer: []u8) Io.Writer {
         return .{
             .vtable = &.{
                 .drain = drain,
                 .sendFile = switch (builtin.zig_backend) {
                     else => sendFile,
-                    .stage2_aarch64 => std.Io.Writer.unimplementedSendFile,
+                    .stage2_aarch64 => Io.Writer.unimplementedSendFile,
                 },
             },
             .buffer = buffer,
@@ -859,7 +831,7 @@ pub const Writer = struct {
     }
 
     /// TODO when this logic moves from fs.File to Io.File the io parameter should be deleted
-    pub fn moveToReader(w: *Writer, io: std.Io) Reader {
+    pub fn moveToReader(w: *Writer, io: Io) Reader {
         defer w.* = undefined;
         return .{
             .io = io,
@@ -871,7 +843,7 @@ pub const Writer = struct {
         };
     }
 
-    pub fn drain(io_w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
+    pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
         const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
         const handle = w.file.handle;
         const buffered = io_w.buffered();
@@ -1021,10 +993,10 @@ pub const Writer = struct {
     }
 
     pub fn sendFile(
-        io_w: *std.Io.Writer,
-        file_reader: *std.Io.File.Reader,
-        limit: std.Io.Limit,
-    ) std.Io.Writer.FileError!usize {
+        io_w: *Io.Writer,
+        file_reader: *Io.File.Reader,
+        limit: Io.Limit,
+    ) Io.Writer.FileError!usize {
         const reader_buffered = file_reader.interface.buffered();
         if (reader_buffered.len >= @intFromEnum(limit))
             return sendFileBuffered(io_w, file_reader, limit.slice(reader_buffered));
@@ -1288,16 +1260,16 @@ pub const Writer = struct {
     }
 
     fn sendFileBuffered(
-        io_w: *std.Io.Writer,
-        file_reader: *std.Io.File.Reader,
+        io_w: *Io.Writer,
+        file_reader: *Io.File.Reader,
         reader_buffered: []const u8,
-    ) std.Io.Writer.FileError!usize {
+    ) Io.Writer.FileError!usize {
         const n = try drain(io_w, &.{reader_buffered}, 1);
         file_reader.seekBy(@intCast(n)) catch return error.ReadFailed;
         return n;
     }
 
-    pub fn seekTo(w: *Writer, offset: u64) (Writer.SeekError || std.Io.Writer.Error)!void {
+    pub fn seekTo(w: *Writer, offset: u64) (Writer.SeekError || Io.Writer.Error)!void {
         try w.interface.flush();
         try seekToUnbuffered(w, offset);
     }
@@ -1321,7 +1293,7 @@ pub const Writer = struct {
         }
     }
 
-    pub const EndError = SetEndPosError || std.Io.Writer.Error;
+    pub const EndError = SetEndPosError || Io.Writer.Error;
 
     /// Flushes any buffered data and sets the end position of the file.
     ///
@@ -1352,14 +1324,14 @@ pub const Writer = struct {
 ///
 /// Positional is more threadsafe, since the global seek position is not
 /// affected.
-pub fn reader(file: File, io: std.Io, buffer: []u8) Reader {
+pub fn reader(file: File, io: Io, buffer: []u8) Reader {
     return .init(.{ .handle = file.handle }, io, buffer);
 }
 
 /// Positional is more threadsafe, since the global seek position is not
 /// affected, but when such syscalls are not available, preemptively
 /// initializing in streaming mode skips a failed syscall.
-pub fn readerStreaming(file: File, io: std.Io, buffer: []u8) Reader {
+pub fn readerStreaming(file: File, io: Io, buffer: []u8) Reader {
     return .initStreaming(.{ .handle = file.handle }, io, buffer);
 }
 
@@ -1541,10 +1513,10 @@ pub fn downgradeLock(file: File) LockError!void {
     }
 }
 
-pub fn adaptToNewApi(file: File) std.Io.File {
+pub fn adaptToNewApi(file: File) Io.File {
     return .{ .handle = file.handle };
 }
 
-pub fn adaptFromNewApi(file: std.Io.File) File {
+pub fn adaptFromNewApi(file: Io.File) File {
     return .{ .handle = file.handle };
 }
lib/std/Io/File.zig
@@ -47,90 +47,14 @@ pub const Stat = struct {
     kind: Kind,
 
     /// Last access time in nanoseconds, relative to UTC 1970-01-01.
+    /// TODO change this to Io.Timestamp except don't waste storage on clock
     atime: i128,
     /// Last modification time in nanoseconds, relative to UTC 1970-01-01.
+    /// TODO change this to Io.Timestamp except don't waste storage on clock
     mtime: i128,
     /// Last status/metadata change time in nanoseconds, relative to UTC 1970-01-01.
+    /// TODO change this to Io.Timestamp except don't waste storage on clock
     ctime: i128,
-
-    pub fn fromPosix(st: std.posix.Stat) Stat {
-        const atime = st.atime();
-        const mtime = st.mtime();
-        const ctime = st.ctime();
-        return .{
-            .inode = st.ino,
-            .size = @bitCast(st.size),
-            .mode = st.mode,
-            .kind = k: {
-                const m = st.mode & std.posix.S.IFMT;
-                switch (m) {
-                    std.posix.S.IFBLK => break :k .block_device,
-                    std.posix.S.IFCHR => break :k .character_device,
-                    std.posix.S.IFDIR => break :k .directory,
-                    std.posix.S.IFIFO => break :k .named_pipe,
-                    std.posix.S.IFLNK => break :k .sym_link,
-                    std.posix.S.IFREG => break :k .file,
-                    std.posix.S.IFSOCK => break :k .unix_domain_socket,
-                    else => {},
-                }
-                if (builtin.os.tag == .illumos) switch (m) {
-                    std.posix.S.IFDOOR => break :k .door,
-                    std.posix.S.IFPORT => break :k .event_port,
-                    else => {},
-                };
-
-                break :k .unknown;
-            },
-            .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec,
-            .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec,
-            .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec,
-        };
-    }
-
-    pub fn fromLinux(stx: std.os.linux.Statx) Stat {
-        const atime = stx.atime;
-        const mtime = stx.mtime;
-        const ctime = stx.ctime;
-
-        return .{
-            .inode = stx.ino,
-            .size = stx.size,
-            .mode = stx.mode,
-            .kind = switch (stx.mode & std.os.linux.S.IFMT) {
-                std.os.linux.S.IFDIR => .directory,
-                std.os.linux.S.IFCHR => .character_device,
-                std.os.linux.S.IFBLK => .block_device,
-                std.os.linux.S.IFREG => .file,
-                std.os.linux.S.IFIFO => .named_pipe,
-                std.os.linux.S.IFLNK => .sym_link,
-                std.os.linux.S.IFSOCK => .unix_domain_socket,
-                else => .unknown,
-            },
-            .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec,
-            .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec,
-            .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec,
-        };
-    }
-
-    pub fn fromWasi(st: std.os.wasi.filestat_t) Stat {
-        return .{
-            .inode = st.ino,
-            .size = @bitCast(st.size),
-            .mode = 0,
-            .kind = switch (st.filetype) {
-                .BLOCK_DEVICE => .block_device,
-                .CHARACTER_DEVICE => .character_device,
-                .DIRECTORY => .directory,
-                .SYMBOLIC_LINK => .sym_link,
-                .REGULAR_FILE => .file,
-                .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,
-                else => .unknown,
-            },
-            .atime = st.atim,
-            .mtime = st.mtim,
-            .ctime = st.ctim,
-        };
-    }
 };
 
 pub fn stdout() File {
@@ -145,13 +69,17 @@ pub fn stdin() File {
     return .{ .handle = if (is_windows) std.os.windows.peb().ProcessParameters.hStdInput else std.posix.STDIN_FILENO };
 }
 
-pub const StatError = std.posix.FStatError || Io.Cancelable;
+pub const StatError = error{
+    SystemResources,
+    /// In WASI, this error may occur when the file descriptor does
+    /// not hold the required rights to get its filestat information.
+    AccessDenied,
+    PermissionDenied,
+} || Io.Cancelable || Io.UnexpectedError;
 
 /// Returns `Stat` containing basic information about the `File`.
 pub fn stat(file: File, io: Io) StatError!Stat {
-    _ = file;
-    _ = io;
-    @panic("TODO");
+    return io.vtable.fileStat(io.userdata, file);
 }
 
 pub const OpenFlags = std.fs.File.OpenFlags;
lib/std/Io/Threaded.zig
@@ -163,7 +163,12 @@ pub fn io(pool: *Pool) Io {
             .dirMake = dirMake,
             .dirStat = dirStat,
             .dirStatPath = dirStatPath,
-            .fileStat = fileStat,
+            .fileStat = switch (builtin.os.tag) {
+                .linux => fileStatLinux,
+                .windows => fileStatWindows,
+                .wasi => fileStatWasi,
+                else => fileStatPosix,
+            },
             .createFile = createFile,
             .fileOpen = fileOpen,
             .fileClose = fileClose,
@@ -781,14 +786,80 @@ fn dirStatPath(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.
     @panic("TODO");
 }
 
-fn fileStat(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
+fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
     const pool: *Pool = @ptrCast(@alignCast(userdata));
-    try pool.checkCancel();
+    const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat;
+    while (true) {
+        try pool.checkCancel();
+        var stat = std.mem.zeroes(posix.Stat);
+        switch (posix.errno(fstat_sym(file.handle, &stat))) {
+            .SUCCESS => return statFromPosix(&stat),
+            .INTR => continue,
+            .INVAL => |err| return errnoBug(err),
+            .BADF => |err| return errnoBug(err),
+            .NOMEM => return error.SystemResources,
+            .ACCES => return error.AccessDenied,
+            else => |err| return posix.unexpectedErrno(err),
+        }
+    }
+}
+
+fn fileStatLinux(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
+    const pool: *Pool = @ptrCast(@alignCast(userdata));
+    const linux = std.os.linux;
+    while (true) {
+        try pool.checkCancel();
+        var statx = std.mem.zeroes(linux.Statx);
+        const rc = linux.statx(
+            file.handle,
+            "",
+            linux.AT.EMPTY_PATH,
+            linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME,
+            &statx,
+        );
+        switch (linux.E.init(rc)) {
+            .SUCCESS => return statFromLinux(&statx),
+            .INTR => continue,
+            .ACCES => |err| return errnoBug(err),
+            .BADF => |err| return errnoBug(err),
+            .FAULT => |err| return errnoBug(err),
+            .INVAL => |err| return errnoBug(err),
+            .LOOP => |err| return errnoBug(err),
+            .NAMETOOLONG => |err| return errnoBug(err),
+            .NOENT => |err| return errnoBug(err),
+            .NOMEM => return error.SystemResources,
+            .NOTDIR => |err| return errnoBug(err),
+            else => |err| return posix.unexpectedErrno(err),
+        }
+    }
+}
 
+fn fileStatWindows(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
+    const pool: *Pool = @ptrCast(@alignCast(userdata));
+    try pool.checkCancel();
     _ = file;
     @panic("TODO");
 }
 
+fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
+    if (builtin.link_libc) return fileStatPosix(userdata, file);
+    const pool: *Pool = @ptrCast(@alignCast(userdata));
+    while (true) {
+        try pool.checkCancel();
+        var stat: std.os.wasi.filestat_t = undefined;
+        switch (std.os.wasi.fd_filestat_get(file.handle, &stat)) {
+            .SUCCESS => return statFromWasi(&stat),
+            .INTR => continue,
+            .INVAL => |err| return errnoBug(err),
+            .BADF => |err| return errnoBug(err),
+            .NOMEM => return error.SystemResources,
+            .ACCES => return error.AccessDenied,
+            .NOTCAPABLE => return error.AccessDenied,
+            else => |err| return posix.unexpectedErrno(err),
+        }
+    }
+}
+
 fn createFile(
     userdata: ?*anyopaque,
     dir: Io.Dir,
@@ -2114,3 +2185,81 @@ fn clockToWasi(clock: Io.Timestamp.Clock) std.os.wasi.clockid_t {
         .cpu_thread => .THREAD_CPUTIME_ID,
     };
 }
+
+fn statFromLinux(stx: *const std.os.linux.Statx) Io.File.Stat {
+    const atime = stx.atime;
+    const mtime = stx.mtime;
+    const ctime = stx.ctime;
+    return .{
+        .inode = stx.ino,
+        .size = stx.size,
+        .mode = stx.mode,
+        .kind = switch (stx.mode & std.os.linux.S.IFMT) {
+            std.os.linux.S.IFDIR => .directory,
+            std.os.linux.S.IFCHR => .character_device,
+            std.os.linux.S.IFBLK => .block_device,
+            std.os.linux.S.IFREG => .file,
+            std.os.linux.S.IFIFO => .named_pipe,
+            std.os.linux.S.IFLNK => .sym_link,
+            std.os.linux.S.IFSOCK => .unix_domain_socket,
+            else => .unknown,
+        },
+        .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec,
+        .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec,
+        .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec,
+    };
+}
+
+fn statFromPosix(st: *const std.posix.Stat) Io.File.Stat {
+    const atime = st.atime();
+    const mtime = st.mtime();
+    const ctime = st.ctime();
+    return .{
+        .inode = st.ino,
+        .size = @bitCast(st.size),
+        .mode = st.mode,
+        .kind = k: {
+            const m = st.mode & std.posix.S.IFMT;
+            switch (m) {
+                std.posix.S.IFBLK => break :k .block_device,
+                std.posix.S.IFCHR => break :k .character_device,
+                std.posix.S.IFDIR => break :k .directory,
+                std.posix.S.IFIFO => break :k .named_pipe,
+                std.posix.S.IFLNK => break :k .sym_link,
+                std.posix.S.IFREG => break :k .file,
+                std.posix.S.IFSOCK => break :k .unix_domain_socket,
+                else => {},
+            }
+            if (builtin.os.tag == .illumos) switch (m) {
+                std.posix.S.IFDOOR => break :k .door,
+                std.posix.S.IFPORT => break :k .event_port,
+                else => {},
+            };
+
+            break :k .unknown;
+        },
+        .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec,
+        .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec,
+        .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec,
+    };
+}
+
+fn statFromWasi(st: *const std.os.wasi.filestat_t) Io.File.Stat {
+    return .{
+        .inode = st.ino,
+        .size = @bitCast(st.size),
+        .mode = 0,
+        .kind = switch (st.filetype) {
+            .BLOCK_DEVICE => .block_device,
+            .CHARACTER_DEVICE => .character_device,
+            .DIRECTORY => .directory,
+            .SYMBOLIC_LINK => .sym_link,
+            .REGULAR_FILE => .file,
+            .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,
+            else => .unknown,
+        },
+        .atime = st.atim,
+        .mtime = st.mtim,
+        .ctime = st.ctim,
+    };
+}
lib/std/debug.zig
@@ -82,6 +82,7 @@ pub const SelfInfoError = error{
     /// The required debug info could not be read from disk due to some IO error.
     ReadFailed,
     OutOfMemory,
+    Canceled,
     Unexpected,
 };
 
@@ -691,6 +692,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri
                 error.UnsupportedDebugInfo => "unwind info unsupported",
                 error.ReadFailed => "filesystem error",
                 error.OutOfMemory => "out of memory",
+                error.Canceled => "operation canceled",
                 error.Unexpected => "unexpected error",
             };
             if (it.stratOk(options.allow_unsafe_unwind)) {
@@ -1079,7 +1081,7 @@ fn printSourceAtAddress(gpa: Allocator, debug_info: *SelfInfo, writer: *Writer,
         error.UnsupportedDebugInfo,
         error.InvalidDebugInfo,
         => .unknown,
-        error.ReadFailed, error.Unexpected => s: {
+        error.ReadFailed, error.Unexpected, error.Canceled => s: {
             tty_config.setColor(writer, .dim) catch {};
             try writer.print("Failed to read debug info from filesystem, trace may be incomplete\n\n", .{});
             tty_config.setColor(writer, .reset) catch {};
lib/std/posix.zig
@@ -4458,14 +4458,7 @@ pub fn wait4(pid: pid_t, flags: u32, ru: ?*rusage) WaitPidResult {
     }
 }
 
-pub const FStatError = error{
-    SystemResources,
-
-    /// In WASI, this error may occur when the file descriptor does
-    /// not hold the required rights to get its filestat information.
-    AccessDenied,
-    PermissionDenied,
-} || UnexpectedError;
+pub const FStatError = std.Io.File.StatError;
 
 /// Return information about a file descriptor.
 pub fn fstat(fd: fd_t) FStatError!Stat {