authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-10-16 22:39:46-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-10-29 06:20:50-07:00
log143127529b109971c931cce18cc7a328f6031df3
treebacf6f1125a67270add4c7e1ec5f08376ed0a86d
parentec9dfc540b95a5577074e8915670ac2920b32f64

std.Io.Threaded: implement dirMake for WASI


5 files changed, 59 insertions(+), 100 deletions(-)

lib/std/Io.zig+4
...@@ -883,6 +883,10 @@ pub const Timestamp = struct {...@@ -883,6 +883,10 @@ pub const Timestamp = struct {
883 return .{ .nanoseconds = t.nanoseconds, .clock = clock };883 return .{ .nanoseconds = t.nanoseconds, .clock = clock };
884 }884 }
885885
886 pub fn fromNanoseconds(x: i96) Timestamp {
887 return .{ .nanoseconds = x };
888 }
889
886 pub fn toSeconds(t: Timestamp) i64 {890 pub fn toSeconds(t: Timestamp) i64 {
887 return @intCast(@divTrunc(t.nanoseconds, std.time.ns_per_s));891 return @intCast(@divTrunc(t.nanoseconds, std.time.ns_per_s));
888 }892 }
lib/std/Io/Threaded.zig+47-16
...@@ -167,7 +167,7 @@ pub fn io(t: *Threaded) Io {...@@ -167,7 +167,7 @@ pub fn io(t: *Threaded) Io {
167167
168 .dirMake = switch (builtin.os.tag) {168 .dirMake = switch (builtin.os.tag) {
169 .windows => @panic("TODO"),169 .windows => @panic("TODO"),
170 .wasi => @panic("TODO"),170 .wasi => dirMakeWasi,
171 else => dirMakePosix,171 else => dirMakePosix,
172 },172 },
173 .dirStat = dirStat,173 .dirStat = dirStat,
...@@ -906,6 +906,37 @@ fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode:...@@ -906,6 +906,37 @@ fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode:
906 }906 }
907}907}
908908
909fn dirMakeWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void {
910 if (builtin.link_libc) return dirMakePosix(userdata, dir, sub_path, mode);
911 const t: *Threaded = @ptrCast(@alignCast(userdata));
912 while (true) {
913 try t.checkCancel();
914 switch (std.os.wasi.path_create_directory(dir.handle, sub_path.ptr, sub_path.len)) {
915 .SUCCESS => return,
916 .INTR => continue,
917 .CANCELED => return error.Canceled,
918
919 .ACCES => return error.AccessDenied,
920 .BADF => |err| return errnoBug(err),
921 .PERM => return error.PermissionDenied,
922 .DQUOT => return error.DiskQuota,
923 .EXIST => return error.PathAlreadyExists,
924 .FAULT => |err| return errnoBug(err),
925 .LOOP => return error.SymLinkLoop,
926 .MLINK => return error.LinkQuotaExceeded,
927 .NAMETOOLONG => return error.NameTooLong,
928 .NOENT => return error.FileNotFound,
929 .NOMEM => return error.SystemResources,
930 .NOSPC => return error.NoSpaceLeft,
931 .NOTDIR => return error.NotDir,
932 .ROFS => return error.ReadOnlyFileSystem,
933 .NOTCAPABLE => return error.AccessDenied,
934 .ILSEQ => return error.BadPathName,
935 else => |err| return posix.unexpectedErrno(err),
936 }
937 }
938}
939
909fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat {940fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat {
910 const t: *Threaded = @ptrCast(@alignCast(userdata));941 const t: *Threaded = @ptrCast(@alignCast(userdata));
911 try t.checkCancel();942 try t.checkCancel();
...@@ -1005,13 +1036,13 @@ fn dirStatPathWasi(...@@ -1005,13 +1036,13 @@ fn dirStatPathWasi(
1005 const t: *Threaded = @ptrCast(@alignCast(userdata));1036 const t: *Threaded = @ptrCast(@alignCast(userdata));
1006 const wasi = std.os.wasi;1037 const wasi = std.os.wasi;
1007 const flags: wasi.lookupflags_t = .{1038 const flags: wasi.lookupflags_t = .{
1008 .SYMLINK_FOLLOW = @intFromBool(options.follow_symlinks),1039 .SYMLINK_FOLLOW = options.follow_symlinks,
1009 };1040 };
1010 var stat: wasi.filestat_t = undefined;1041 var stat: wasi.filestat_t = undefined;
1011 while (true) {1042 while (true) {
1012 try t.checkCancel();1043 try t.checkCancel();
1013 switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) {1044 switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) {
1014 .SUCCESS => return statFromWasi(stat),1045 .SUCCESS => return statFromWasi(&stat),
1015 .INTR => continue,1046 .INTR => continue,
1016 .CANCELED => return error.Canceled,1047 .CANCELED => return error.Canceled,
10171048
...@@ -1166,19 +1197,19 @@ fn dirAccessWasi(...@@ -1166,19 +1197,19 @@ fn dirAccessWasi(
1166 userdata: ?*anyopaque,1197 userdata: ?*anyopaque,
1167 dir: Io.Dir,1198 dir: Io.Dir,
1168 sub_path: []const u8,1199 sub_path: []const u8,
1169 options: Io.File.OpenFlags,1200 options: Io.Dir.AccessOptions,
1170) Io.File.AccessError!void {1201) Io.Dir.AccessError!void {
1171 if (builtin.link_libc) return dirAccessPosix(userdata, dir, sub_path, options);1202 if (builtin.link_libc) return dirAccessPosix(userdata, dir, sub_path, options);
1172 const t: *Threaded = @ptrCast(@alignCast(userdata));1203 const t: *Threaded = @ptrCast(@alignCast(userdata));
1173 const wasi = std.os.wasi;1204 const wasi = std.os.wasi;
1174 const flags: wasi.lookupflags_t = .{1205 const flags: wasi.lookupflags_t = .{
1175 .SYMLINK_FOLLOW = @intFromBool(options.follow_symlinks),1206 .SYMLINK_FOLLOW = options.follow_symlinks,
1176 };1207 };
1177 const stat = while (true) {1208 var stat: wasi.filestat_t = undefined;
1178 var stat: wasi.filestat_t = undefined;1209 while (true) {
1179 try t.checkCancel();1210 try t.checkCancel();
1180 switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) {1211 switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) {
1181 .SUCCESS => break statFromWasi(stat),1212 .SUCCESS => break,
1182 .INTR => continue,1213 .INTR => continue,
1183 .CANCELED => return error.Canceled,1214 .CANCELED => return error.Canceled,
11841215
...@@ -1194,9 +1225,9 @@ fn dirAccessWasi(...@@ -1194,9 +1225,9 @@ fn dirAccessWasi(
1194 .ILSEQ => return error.BadPathName,1225 .ILSEQ => return error.BadPathName,
1195 else => |err| return posix.unexpectedErrno(err),1226 else => |err| return posix.unexpectedErrno(err),
1196 }1227 }
1197 };1228 }
11981229
1199 if (!options.mode.read and !options.mode.write and !options.mode.execute)1230 if (!options.read and !options.write and !options.execute)
1200 return;1231 return;
12011232
1202 var directory: wasi.fdstat_t = undefined;1233 var directory: wasi.fdstat_t = undefined;
...@@ -1204,14 +1235,14 @@ fn dirAccessWasi(...@@ -1204,14 +1235,14 @@ fn dirAccessWasi(
1204 return error.AccessDenied;1235 return error.AccessDenied;
12051236
1206 var rights: wasi.rights_t = .{};1237 var rights: wasi.rights_t = .{};
1207 if (options.mode.read) {1238 if (options.read) {
1208 if (stat.filetype == .DIRECTORY) {1239 if (stat.filetype == .DIRECTORY) {
1209 rights.FD_READDIR = true;1240 rights.FD_READDIR = true;
1210 } else {1241 } else {
1211 rights.FD_READ = true;1242 rights.FD_READ = true;
1212 }1243 }
1213 }1244 }
1214 if (options.mode.write)1245 if (options.write)
1215 rights.FD_WRITE = true;1246 rights.FD_WRITE = true;
12161247
1217 // No validation for execution.1248 // No validation for execution.
...@@ -3262,9 +3293,9 @@ fn statFromWasi(st: *const std.os.wasi.filestat_t) Io.File.Stat {...@@ -3262,9 +3293,9 @@ fn statFromWasi(st: *const std.os.wasi.filestat_t) Io.File.Stat {
3262 .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,3293 .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,
3263 else => .unknown,3294 else => .unknown,
3264 },3295 },
3265 .atime = st.atim,3296 .atime = .fromNanoseconds(st.atim),
3266 .mtime = st.mtim,3297 .mtime = .fromNanoseconds(st.mtim),
3267 .ctime = st.ctim,3298 .ctime = .fromNanoseconds(st.ctim),
3268 };3299 };
3269}3300}
32703301
lib/std/os.zig+7-1
...@@ -201,7 +201,13 @@ pub fn getFdPath(fd: std.posix.fd_t, out_buffer: *[max_path_bytes]u8) std.posix....@@ -201,7 +201,13 @@ pub fn getFdPath(fd: std.posix.fd_t, out_buffer: *[max_path_bytes]u8) std.posix.
201 }201 }
202}202}
203203
204pub fn fstat_wasi(fd: posix.fd_t) posix.FStatError!wasi.filestat_t {204pub const FstatError = error{
205 SystemResources,
206 AccessDenied,
207 Unexpected,
208};
209
210pub fn fstat_wasi(fd: posix.fd_t) FstatError!wasi.filestat_t {
205 var stat: wasi.filestat_t = undefined;211 var stat: wasi.filestat_t = undefined;
206 switch (wasi.fd_filestat_get(fd, &stat)) {212 switch (wasi.fd_filestat_get(fd, &stat)) {
207 .SUCCESS => return stat,213 .SUCCESS => return stat,
lib/std/posix.zig+1-25
...@@ -2809,37 +2809,13 @@ pub fn mkdirat(dir_fd: fd_t, sub_dir_path: []const u8, mode: mode_t) MakeDirErro...@@ -2809,37 +2809,13 @@ pub fn mkdirat(dir_fd: fd_t, sub_dir_path: []const u8, mode: mode_t) MakeDirErro
2809 const sub_dir_path_w = try windows.sliceToPrefixedFileW(dir_fd, sub_dir_path);2809 const sub_dir_path_w = try windows.sliceToPrefixedFileW(dir_fd, sub_dir_path);
2810 return mkdiratW(dir_fd, sub_dir_path_w.span(), mode);2810 return mkdiratW(dir_fd, sub_dir_path_w.span(), mode);
2811 } else if (native_os == .wasi and !builtin.link_libc) {2811 } else if (native_os == .wasi and !builtin.link_libc) {
2812 return mkdiratWasi(dir_fd, sub_dir_path, mode);2812 @compileError("use std.Io instead");
2813 } else {2813 } else {
2814 const sub_dir_path_c = try toPosixPath(sub_dir_path);2814 const sub_dir_path_c = try toPosixPath(sub_dir_path);
2815 return mkdiratZ(dir_fd, &sub_dir_path_c, mode);2815 return mkdiratZ(dir_fd, &sub_dir_path_c, mode);
2816 }2816 }
2817}2817}
28182818
2819pub fn mkdiratWasi(dir_fd: fd_t, sub_dir_path: []const u8, mode: mode_t) MakeDirError!void {
2820 _ = mode;
2821 switch (wasi.path_create_directory(dir_fd, sub_dir_path.ptr, sub_dir_path.len)) {
2822 .SUCCESS => return,
2823 .ACCES => return error.AccessDenied,
2824 .BADF => unreachable,
2825 .PERM => return error.PermissionDenied,
2826 .DQUOT => return error.DiskQuota,
2827 .EXIST => return error.PathAlreadyExists,
2828 .FAULT => unreachable,
2829 .LOOP => return error.SymLinkLoop,
2830 .MLINK => return error.LinkQuotaExceeded,
2831 .NAMETOOLONG => return error.NameTooLong,
2832 .NOENT => return error.FileNotFound,
2833 .NOMEM => return error.SystemResources,
2834 .NOSPC => return error.NoSpaceLeft,
2835 .NOTDIR => return error.NotDir,
2836 .ROFS => return error.ReadOnlyFileSystem,
2837 .NOTCAPABLE => return error.AccessDenied,
2838 .ILSEQ => return error.BadPathName,
2839 else => |err| return unexpectedErrno(err),
2840 }
2841}
2842
2843/// Same as `mkdirat` except the parameters are null-terminated.2819/// Same as `mkdirat` except the parameters are null-terminated.
2844pub fn mkdiratZ(dir_fd: fd_t, sub_dir_path: [*:0]const u8, mode: mode_t) MakeDirError!void {2820pub fn mkdiratZ(dir_fd: fd_t, sub_dir_path: [*:0]const u8, mode: mode_t) MakeDirError!void {
2845 if (native_os == .windows) {2821 if (native_os == .windows) {
lib/std/posix/test.zig-58
...@@ -109,64 +109,6 @@ test "open smoke test" {...@@ -109,64 +109,6 @@ test "open smoke test" {
109 }109 }
110}110}
111111
112test "openat smoke test" {
113 if (native_os == .windows) return error.SkipZigTest;
114
115 // TODO verify file attributes using `fstatat`
116
117 var tmp = tmpDir(.{});
118 defer tmp.cleanup();
119
120 var fd: posix.fd_t = undefined;
121 const mode: posix.mode_t = if (native_os == .windows) 0 else 0o666;
122
123 // Create some file using `openat`.
124 fd = try posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{
125 .ACCMODE = .RDWR,
126 .CREAT = true,
127 .EXCL = true,
128 }), mode);
129 posix.close(fd);
130
131 // Try this again with the same flags. This op should fail with error.PathAlreadyExists.
132 try expectError(error.PathAlreadyExists, posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{
133 .ACCMODE = .RDWR,
134 .CREAT = true,
135 .EXCL = true,
136 }), mode));
137
138 // Try opening without `EXCL` flag.
139 fd = try posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{
140 .ACCMODE = .RDWR,
141 .CREAT = true,
142 }), mode);
143 posix.close(fd);
144
145 // Try opening as a directory which should fail.
146 try expectError(error.NotDir, posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{
147 .ACCMODE = .RDWR,
148 .DIRECTORY = true,
149 }), mode));
150
151 // Create some directory
152 try posix.mkdirat(tmp.dir.fd, "some_dir", mode);
153
154 // Open dir using `open`
155 fd = try posix.openat(tmp.dir.fd, "some_dir", CommonOpenFlags.lower(.{
156 .ACCMODE = .RDONLY,
157 .DIRECTORY = true,
158 }), mode);
159 posix.close(fd);
160
161 // Try opening as file which should fail (skip on wasi+libc due to
162 // https://github.com/bytecodealliance/wasmtime/issues/9054)
163 if (native_os != .wasi or !builtin.link_libc) {
164 try expectError(error.IsDir, posix.openat(tmp.dir.fd, "some_dir", CommonOpenFlags.lower(.{
165 .ACCMODE = .RDWR,
166 }), mode));
167 }
168}
169
170test "readlink on Windows" {112test "readlink on Windows" {
171 if (native_os != .windows) return error.SkipZigTest;113 if (native_os != .windows) return error.SkipZigTest;
172114