std.Io: implement fileStat

This commit is contained in:
Andrew Kelley 2025-10-08 20:35:34 -07:00
parent 69b54b0cd1
commit 89412fda77
7 changed files with 202 additions and 156 deletions

View File

@ -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;

View File

@ -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));
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,
};
}

View File

@ -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 {};

View File

@ -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,

View File

@ -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,

View File

@ -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 };
}

View File

@ -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 {