commit 99f25bfc23a902d7cf047ae2b95e1d9d7e3b09ee (tree)
parent 98f05a0f53d08683e4b8127210448c0de00dc9cf
Author: Andrew Kelley <andrew@ziglang.org>
Date: Fri, 19 Dec 2025 18:16:47 -0800
std.Io: implement directory reading for WASI
Diffstat:
2 files changed, 107 insertions(+), 6 deletions(-)
diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig
@@ -106,6 +106,8 @@ pub const Reader = struct {
.linux => std.mem.alignForward(usize, @sizeOf(std.os.linux.dirent64), 8) +
std.mem.alignForward(usize, max_name_bytes, 8),
.windows => std.mem.alignForward(usize, max_name_bytes, @alignOf(usize)),
+ .wasi => @sizeOf(std.os.wasi.dirent_t) +
+ std.mem.alignForward(usize, max_name_bytes, @alignOf(std.os.wasi.dirent_t)),
else => if (builtin.link_libc) @sizeOf(std.c.dirent) else std.mem.alignForward(usize, max_name_bytes, @alignOf(usize)),
};
diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig
@@ -3378,7 +3378,7 @@ fn dirReadLinux(userdata: ?*anyopaque, dr: *Dir.Reader, buffer: []Dir.Entry) Dir
// To be consistent across platforms, iteration
// ends if the directory being iterated is deleted
// during iteration. This matches the behavior of
- // non-Linux UNIX platforms.
+ // non-Linux, non-WASI UNIX platforms.
.NOENT => {
dr.state = .finished;
return 0;
@@ -3481,7 +3481,7 @@ fn dirReadDarwin(userdata: ?*anyopaque, dr: *Dir.Reader, buffer: []Dir.Entry) Di
else => |e| {
current_thread.endSyscall();
switch (e) {
- .BADF => |err| return errnoBug(err), // Dir is invalid or was opened without iteration ability
+ .BADF => |err| return errnoBug(err), // Dir is invalid or was opened without iteration ability.
.FAULT => |err| return errnoBug(err),
.NOTDIR => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
@@ -3839,10 +3839,109 @@ fn dirReadWindows(userdata: ?*anyopaque, dr: *Dir.Reader, buffer: []Dir.Entry) D
}
fn dirReadWasi(userdata: ?*anyopaque, dr: *Dir.Reader, buffer: []Dir.Entry) Dir.Reader.Error!usize {
- _ = userdata;
- _ = dr;
- _ = buffer;
- @panic("TODO");
+ // We intentinally use fd_readdir even when linked with libc, since its
+ // implementation is exactly the same as below, and we avoid the code
+ // complexity here.
+ const wasi = std.os.wasi;
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+ const Header = extern struct {
+ cookie: u64,
+ };
+ const header: *align(@alignOf(usize)) Header = @ptrCast(dr.buffer.ptr);
+ const header_end: usize = @sizeOf(Header);
+ if (dr.index < header_end) {
+ // Initialize header.
+ dr.index = header_end;
+ dr.end = header_end;
+ header.* = .{ .cookie = wasi.DIRCOOKIE_START };
+ }
+ var buffer_index: usize = 0;
+ while (buffer.len - buffer_index != 0) {
+ // According to the WASI spec, the last entry might be truncated, so we
+ // need to check if the remaining buffer contains the whole dirent.
+ if (dr.end - dr.index < @sizeOf(wasi.dirent_t)) {
+ // Refill the buffer, unless we've already created references to
+ // buffered data.
+ if (buffer_index != 0) break;
+ if (dr.state == .reset) {
+ header.* = .{ .cookie = wasi.DIRCOOKIE_START };
+ dr.state = .reading;
+ }
+ const dents_buffer = dr.buffer[header_end..];
+ var n: usize = undefined;
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (wasi.fd_readdir(dr.dir.handle, dents_buffer.ptr, dents_buffer.len, header.cookie, &n)) {
+ .SUCCESS => {
+ current_thread.endSyscall();
+ break;
+ },
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .BADF => |err| return errnoBug(err), // Dir is invalid or was opened without iteration ability.
+ .FAULT => |err| return errnoBug(err),
+ .NOTDIR => |err| return errnoBug(err),
+ .INVAL => |err| return errnoBug(err),
+ // To be consistent across platforms, iteration
+ // ends if the directory being iterated is deleted
+ // during iteration. This matches the behavior of
+ // non-Linux, non-WASI UNIX platforms.
+ .NOENT => {
+ dr.state = .finished;
+ return 0;
+ },
+ .NOTCAPABLE => return error.AccessDenied,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
+ if (n == 0) {
+ dr.state = .finished;
+ return 0;
+ }
+ dr.index = header_end;
+ dr.end = header_end + n;
+ }
+ const entry: *align(1) wasi.dirent_t = @ptrCast(&dr.buffer[dr.index]);
+ const entry_size = @sizeOf(wasi.dirent_t);
+ const name_index = dr.index + entry_size;
+ if (name_index + entry.namlen > dr.end) {
+ // This case, the name is truncated, so we need to call readdir to store the entire name.
+ dr.end = dr.index; // Force fd_readdir in the next loop.
+ continue;
+ }
+ const name = dr.buffer[name_index..][0..entry.namlen];
+ const next_index = name_index + entry.namlen;
+ dr.index = next_index;
+ header.cookie = entry.next;
+
+ if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, ".."))
+ continue;
+
+ const entry_kind: File.Kind = switch (entry.type) {
+ .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,
+ };
+ buffer[buffer_index] = .{
+ .name = name,
+ .kind = entry_kind,
+ .inode = entry.ino,
+ };
+ buffer_index += 1;
+ }
+ return buffer_index;
}
fn dirReadUnimplemented(userdata: ?*anyopaque, dir_reader: *Dir.Reader, buffer: []Dir.Entry) Dir.Reader.Error!usize {