std.Io.Threaded: implement dirMake for WASI

This commit is contained in:
Andrew Kelley 2025-10-16 22:39:46 -07:00
parent ec9dfc540b
commit 143127529b
5 changed files with 59 additions and 100 deletions

View File

@ -883,6 +883,10 @@ pub const Timestamp = struct {
return .{ .nanoseconds = t.nanoseconds, .clock = clock };
}
pub fn fromNanoseconds(x: i96) Timestamp {
return .{ .nanoseconds = x };
}
pub fn toSeconds(t: Timestamp) i64 {
return @intCast(@divTrunc(t.nanoseconds, std.time.ns_per_s));
}

View File

@ -167,7 +167,7 @@ pub fn io(t: *Threaded) Io {
.dirMake = switch (builtin.os.tag) {
.windows => @panic("TODO"),
.wasi => @panic("TODO"),
.wasi => dirMakeWasi,
else => dirMakePosix,
},
.dirStat = dirStat,
@ -906,6 +906,37 @@ fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode:
}
}
fn dirMakeWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void {
if (builtin.link_libc) return dirMakePosix(userdata, dir, sub_path, mode);
const t: *Threaded = @ptrCast(@alignCast(userdata));
while (true) {
try t.checkCancel();
switch (std.os.wasi.path_create_directory(dir.handle, sub_path.ptr, sub_path.len)) {
.SUCCESS => return,
.INTR => continue,
.CANCELED => return error.Canceled,
.ACCES => return error.AccessDenied,
.BADF => |err| return errnoBug(err),
.PERM => return error.PermissionDenied,
.DQUOT => return error.DiskQuota,
.EXIST => return error.PathAlreadyExists,
.FAULT => |err| return errnoBug(err),
.LOOP => return error.SymLinkLoop,
.MLINK => return error.LinkQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.NOTDIR => return error.NotDir,
.ROFS => return error.ReadOnlyFileSystem,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
}
}
fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat {
const t: *Threaded = @ptrCast(@alignCast(userdata));
try t.checkCancel();
@ -1005,13 +1036,13 @@ fn dirStatPathWasi(
const t: *Threaded = @ptrCast(@alignCast(userdata));
const wasi = std.os.wasi;
const flags: wasi.lookupflags_t = .{
.SYMLINK_FOLLOW = @intFromBool(options.follow_symlinks),
.SYMLINK_FOLLOW = options.follow_symlinks,
};
var stat: wasi.filestat_t = undefined;
while (true) {
try t.checkCancel();
switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) {
.SUCCESS => return statFromWasi(stat),
.SUCCESS => return statFromWasi(&stat),
.INTR => continue,
.CANCELED => return error.Canceled,
@ -1166,19 +1197,19 @@ fn dirAccessWasi(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.File.OpenFlags,
) Io.File.AccessError!void {
options: Io.Dir.AccessOptions,
) Io.Dir.AccessError!void {
if (builtin.link_libc) return dirAccessPosix(userdata, dir, sub_path, options);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const wasi = std.os.wasi;
const flags: wasi.lookupflags_t = .{
.SYMLINK_FOLLOW = @intFromBool(options.follow_symlinks),
.SYMLINK_FOLLOW = options.follow_symlinks,
};
const stat = while (true) {
var stat: wasi.filestat_t = undefined;
var stat: wasi.filestat_t = undefined;
while (true) {
try t.checkCancel();
switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) {
.SUCCESS => break statFromWasi(stat),
.SUCCESS => break,
.INTR => continue,
.CANCELED => return error.Canceled,
@ -1194,9 +1225,9 @@ fn dirAccessWasi(
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
};
}
if (!options.mode.read and !options.mode.write and !options.mode.execute)
if (!options.read and !options.write and !options.execute)
return;
var directory: wasi.fdstat_t = undefined;
@ -1204,14 +1235,14 @@ fn dirAccessWasi(
return error.AccessDenied;
var rights: wasi.rights_t = .{};
if (options.mode.read) {
if (options.read) {
if (stat.filetype == .DIRECTORY) {
rights.FD_READDIR = true;
} else {
rights.FD_READ = true;
}
}
if (options.mode.write)
if (options.write)
rights.FD_WRITE = true;
// No validation for execution.
@ -3262,9 +3293,9 @@ fn statFromWasi(st: *const std.os.wasi.filestat_t) Io.File.Stat {
.SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,
else => .unknown,
},
.atime = st.atim,
.mtime = st.mtim,
.ctime = st.ctim,
.atime = .fromNanoseconds(st.atim),
.mtime = .fromNanoseconds(st.mtim),
.ctime = .fromNanoseconds(st.ctim),
};
}

View File

@ -201,7 +201,13 @@ pub fn getFdPath(fd: std.posix.fd_t, out_buffer: *[max_path_bytes]u8) std.posix.
}
}
pub fn fstat_wasi(fd: posix.fd_t) posix.FStatError!wasi.filestat_t {
pub const FstatError = error{
SystemResources,
AccessDenied,
Unexpected,
};
pub fn fstat_wasi(fd: posix.fd_t) FstatError!wasi.filestat_t {
var stat: wasi.filestat_t = undefined;
switch (wasi.fd_filestat_get(fd, &stat)) {
.SUCCESS => return stat,

View File

@ -2809,37 +2809,13 @@ pub fn mkdirat(dir_fd: fd_t, sub_dir_path: []const u8, mode: mode_t) MakeDirErro
const sub_dir_path_w = try windows.sliceToPrefixedFileW(dir_fd, sub_dir_path);
return mkdiratW(dir_fd, sub_dir_path_w.span(), mode);
} else if (native_os == .wasi and !builtin.link_libc) {
return mkdiratWasi(dir_fd, sub_dir_path, mode);
@compileError("use std.Io instead");
} else {
const sub_dir_path_c = try toPosixPath(sub_dir_path);
return mkdiratZ(dir_fd, &sub_dir_path_c, mode);
}
}
pub fn mkdiratWasi(dir_fd: fd_t, sub_dir_path: []const u8, mode: mode_t) MakeDirError!void {
_ = mode;
switch (wasi.path_create_directory(dir_fd, sub_dir_path.ptr, sub_dir_path.len)) {
.SUCCESS => return,
.ACCES => return error.AccessDenied,
.BADF => unreachable,
.PERM => return error.PermissionDenied,
.DQUOT => return error.DiskQuota,
.EXIST => return error.PathAlreadyExists,
.FAULT => unreachable,
.LOOP => return error.SymLinkLoop,
.MLINK => return error.LinkQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.NOTDIR => return error.NotDir,
.ROFS => return error.ReadOnlyFileSystem,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return unexpectedErrno(err),
}
}
/// Same as `mkdirat` except the parameters are null-terminated.
pub fn mkdiratZ(dir_fd: fd_t, sub_dir_path: [*:0]const u8, mode: mode_t) MakeDirError!void {
if (native_os == .windows) {

View File

@ -109,64 +109,6 @@ test "open smoke test" {
}
}
test "openat smoke test" {
if (native_os == .windows) return error.SkipZigTest;
// TODO verify file attributes using `fstatat`
var tmp = tmpDir(.{});
defer tmp.cleanup();
var fd: posix.fd_t = undefined;
const mode: posix.mode_t = if (native_os == .windows) 0 else 0o666;
// Create some file using `openat`.
fd = try posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{
.ACCMODE = .RDWR,
.CREAT = true,
.EXCL = true,
}), mode);
posix.close(fd);
// Try this again with the same flags. This op should fail with error.PathAlreadyExists.
try expectError(error.PathAlreadyExists, posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{
.ACCMODE = .RDWR,
.CREAT = true,
.EXCL = true,
}), mode));
// Try opening without `EXCL` flag.
fd = try posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{
.ACCMODE = .RDWR,
.CREAT = true,
}), mode);
posix.close(fd);
// Try opening as a directory which should fail.
try expectError(error.NotDir, posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{
.ACCMODE = .RDWR,
.DIRECTORY = true,
}), mode));
// Create some directory
try posix.mkdirat(tmp.dir.fd, "some_dir", mode);
// Open dir using `open`
fd = try posix.openat(tmp.dir.fd, "some_dir", CommonOpenFlags.lower(.{
.ACCMODE = .RDONLY,
.DIRECTORY = true,
}), mode);
posix.close(fd);
// Try opening as file which should fail (skip on wasi+libc due to
// https://github.com/bytecodealliance/wasmtime/issues/9054)
if (native_os != .wasi or !builtin.link_libc) {
try expectError(error.IsDir, posix.openat(tmp.dir.fd, "some_dir", CommonOpenFlags.lower(.{
.ACCMODE = .RDWR,
}), mode));
}
}
test "readlink on Windows" {
if (native_os != .windows) return error.SkipZigTest;