| author | |
| committer | |
| log | 4d9eff4bdb368dd4dad99b6b19c83468607cd1d2 |
| tree | 49b95454ae4485624d4dc145255f7a6f586c6d3a |
| parent | a694f575adbe4ca6852cc7c9022101ebcfc3a8d7 |
Added POSIX functions targeting Windows pass `open` and `openat`
smoke tests.5 files changed, 117 insertions(+), 21 deletions(-)
lib/std/child_process.zig+1| ... | @@ -364,6 +364,7 @@ pub const ChildProcess = struct { | ... | @@ -364,6 +364,7 @@ pub const ChildProcess = struct { |
| 364 | error.FileTooBig => unreachable, | 364 | error.FileTooBig => unreachable, |
| 365 | error.DeviceBusy => unreachable, | 365 | error.DeviceBusy => unreachable, |
| 366 | error.FileLocksNotSupported => unreachable, | 366 | error.FileLocksNotSupported => unreachable, |
| 367 | error.BadPathName => unreachable, // Windows-only | ||
| 367 | else => |e| return e, | 368 | else => |e| return e, |
| 368 | } | 369 | } |
| 369 | else | 370 | else |
lib/std/os.zig+71-4| ... | @@ -1041,6 +1041,9 @@ pub const OpenError = error{ | ... | @@ -1041,6 +1041,9 @@ pub const OpenError = error{ |
| 1041 | 1041 | ||
| 1042 | /// The underlying filesystem does not support file locks | 1042 | /// The underlying filesystem does not support file locks |
| 1043 | FileLocksNotSupported, | 1043 | FileLocksNotSupported, |
| 1044 | |||
| 1045 | BadPathName, | ||
| 1046 | InvalidUtf8, | ||
| 1044 | } || UnexpectedError; | 1047 | } || UnexpectedError; |
| 1045 | 1048 | ||
| 1046 | /// Open and possibly create a file. Keeps trying if it gets interrupted. | 1049 | /// Open and possibly create a file. Keeps trying if it gets interrupted. |
| ... | @@ -1092,18 +1095,65 @@ pub fn openZ(file_path: [*:0]const u8, flags: u32, perm: mode_t) OpenError!fd_t | ... | @@ -1092,18 +1095,65 @@ pub fn openZ(file_path: [*:0]const u8, flags: u32, perm: mode_t) OpenError!fd_t |
| 1092 | } | 1095 | } |
| 1093 | } | 1096 | } |
| 1094 | 1097 | ||
| 1098 | fn openOptionsFromFlags(flags: u32) windows.OpenFileOptions { | ||
| 1099 | const w = windows; | ||
| 1100 | |||
| 1101 | var access_mask: w.ULONG = w.READ_CONTROL | w.FILE_WRITE_ATTRIBUTES | w.SYNCHRONIZE; | ||
| 1102 | if (flags & O_RDWR != 0) { | ||
| 1103 | access_mask |= w.GENERIC_READ | w.GENERIC_WRITE; | ||
| 1104 | } else if (flags & O_WRONLY != 0) { | ||
| 1105 | access_mask |= w.GENERIC_WRITE; | ||
| 1106 | } else { | ||
| 1107 | access_mask |= w.GENERIC_READ | w.GENERIC_WRITE; | ||
| 1108 | } | ||
| 1109 | |||
| 1110 | const open_dir: bool = flags & O_DIRECTORY != 0; | ||
| 1111 | const follow_symlinks: bool = flags & O_NOFOLLOW == 0; | ||
| 1112 | |||
| 1113 | const creation: w.ULONG = blk: { | ||
| 1114 | if (flags & O_CREAT != 0) { | ||
| 1115 | if (flags & O_EXCL != 0) { | ||
| 1116 | break :blk w.FILE_CREATE; | ||
| 1117 | } | ||
| 1118 | } | ||
| 1119 | break :blk w.FILE_OPEN; | ||
| 1120 | }; | ||
| 1121 | |||
| 1122 | return .{ | ||
| 1123 | .access_mask = access_mask, | ||
| 1124 | .io_mode = .blocking, | ||
| 1125 | .creation = creation, | ||
| 1126 | .open_dir = open_dir, | ||
| 1127 | .follow_symlinks = follow_symlinks, | ||
| 1128 | }; | ||
| 1129 | } | ||
| 1130 | |||
| 1095 | /// Windows-only. The path parameter is | 1131 | /// Windows-only. The path parameter is |
| 1096 | /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. | 1132 | /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. |
| 1097 | /// Translates the POSIX open API call to a Windows API call. | 1133 | /// Translates the POSIX open API call to a Windows API call. |
| 1098 | pub fn openW(file_path_w: []const u16, flags: u32, perm: usize) OpenError!fd_t { | 1134 | /// TODO currently, this function does not handle all flag combinations |
| 1099 | @compileError("TODO implement openW for windows"); | 1135 | /// or makes use of perm argument. |
| 1136 | pub fn openW(file_path_w: []const u16, flags: u32, perm: mode_t) OpenError!fd_t { | ||
| 1137 | var options = openOptionsFromFlags(flags); | ||
| 1138 | options.dir = std.fs.cwd().fd; | ||
| 1139 | return windows.OpenFile(file_path_w, options) catch |err| switch (err) { | ||
| 1140 | error.WouldBlock => unreachable, | ||
| 1141 | error.PipeBusy => unreachable, | ||
| 1142 | else => |e| return e, | ||
| 1143 | }; | ||
| 1100 | } | 1144 | } |
| 1101 | 1145 | ||
| 1102 | /// Open and possibly create a file. Keeps trying if it gets interrupted. | 1146 | /// Open and possibly create a file. Keeps trying if it gets interrupted. |
| 1103 | /// `file_path` is relative to the open directory handle `dir_fd`. | 1147 | /// `file_path` is relative to the open directory handle `dir_fd`. |
| 1104 | /// See also `openatC`. | 1148 | /// See also `openatC`. |
| 1105 | /// TODO support windows | ||
| 1106 | pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: u32, mode: mode_t) OpenError!fd_t { | 1149 | pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: u32, mode: mode_t) OpenError!fd_t { |
| 1150 | if (builtin.os.tag == .wasi) { | ||
| 1151 | @compileError("use openatWasi instead"); | ||
| 1152 | } | ||
| 1153 | if (builtin.os.tag == .windows) { | ||
| 1154 | const file_path_w = try windows.sliceToPrefixedFileW(file_path); | ||
| 1155 | return openatW(dir_fd, file_path_w.span(), flags, mode); | ||
| 1156 | } | ||
| 1107 | const file_path_c = try toPosixPath(file_path); | 1157 | const file_path_c = try toPosixPath(file_path); |
| 1108 | return openatZ(dir_fd, &file_path_c, flags, mode); | 1158 | return openatZ(dir_fd, &file_path_c, flags, mode); |
| 1109 | } | 1159 | } |
| ... | @@ -1145,8 +1195,11 @@ pub const openatC = @compileError("deprecated: renamed to openatZ"); | ... | @@ -1145,8 +1195,11 @@ pub const openatC = @compileError("deprecated: renamed to openatZ"); |
| 1145 | /// Open and possibly create a file. Keeps trying if it gets interrupted. | 1195 | /// Open and possibly create a file. Keeps trying if it gets interrupted. |
| 1146 | /// `file_path` is relative to the open directory handle `dir_fd`. | 1196 | /// `file_path` is relative to the open directory handle `dir_fd`. |
| 1147 | /// See also `openat`. | 1197 | /// See also `openat`. |
| 1148 | /// TODO support windows | ||
| 1149 | pub fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: u32, mode: mode_t) OpenError!fd_t { | 1198 | pub fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: u32, mode: mode_t) OpenError!fd_t { |
| 1199 | if (builtin.os.tag == .windows) { | ||
| 1200 | const file_path_w = try windows.cStrToPrefixedFileW(file_path); | ||
| 1201 | return openatW(dir_fd, file_path_w.span(), flags, mode); | ||
| 1202 | } | ||
| 1150 | while (true) { | 1203 | while (true) { |
| 1151 | const rc = system.openat(dir_fd, file_path, flags, mode); | 1204 | const rc = system.openat(dir_fd, file_path, flags, mode); |
| 1152 | switch (errno(rc)) { | 1205 | switch (errno(rc)) { |
| ... | @@ -1177,6 +1230,20 @@ pub fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: u32, mode: mode_t) | ... | @@ -1177,6 +1230,20 @@ pub fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: u32, mode: mode_t) |
| 1177 | } | 1230 | } |
| 1178 | } | 1231 | } |
| 1179 | 1232 | ||
| 1233 | /// Windows-only. Similar to `openat` but with pathname argument null-terminated | ||
| 1234 | /// WTF16 encoded. | ||
| 1235 | /// TODO currently, this function does not handle all flag combinations | ||
| 1236 | /// or makes use of perm argument. | ||
| 1237 | pub fn openatW(dir_fd: fd_t, file_path_w: []const u16, flags: u32, mode: mode_t) OpenError!fd_t { | ||
| 1238 | var options = openOptionsFromFlags(flags); | ||
| 1239 | options.dir = dir_fd; | ||
| 1240 | return windows.OpenFile(file_path_w, options) catch |err| switch (err) { | ||
| 1241 | error.WouldBlock => unreachable, | ||
| 1242 | error.PipeBusy => unreachable, | ||
| 1243 | else => |e| return e, | ||
| 1244 | }; | ||
| 1245 | } | ||
| 1246 | |||
| 1180 | pub fn dup2(old_fd: fd_t, new_fd: fd_t) !void { | 1247 | pub fn dup2(old_fd: fd_t, new_fd: fd_t) !void { |
| 1181 | while (true) { | 1248 | while (true) { |
| 1182 | switch (errno(system.dup2(old_fd, new_fd))) { | 1249 | switch (errno(system.dup2(old_fd, new_fd))) { |
lib/std/os/bits/windows.zig+25| ... | @@ -237,3 +237,28 @@ pub const IPPROTO_TCP = ws2_32.IPPROTO_TCP; | ... | @@ -237,3 +237,28 @@ pub const IPPROTO_TCP = ws2_32.IPPROTO_TCP; |
| 237 | pub const IPPROTO_UDP = ws2_32.IPPROTO_UDP; | 237 | pub const IPPROTO_UDP = ws2_32.IPPROTO_UDP; |
| 238 | pub const IPPROTO_ICMPV6 = ws2_32.IPPROTO_ICMPV6; | 238 | pub const IPPROTO_ICMPV6 = ws2_32.IPPROTO_ICMPV6; |
| 239 | pub const IPPROTO_RM = ws2_32.IPPROTO_RM; | 239 | pub const IPPROTO_RM = ws2_32.IPPROTO_RM; |
| 240 | |||
| 241 | pub const O_RDONLY = 0o0; | ||
| 242 | pub const O_WRONLY = 0o1; | ||
| 243 | pub const O_RDWR = 0o2; | ||
| 244 | |||
| 245 | pub const O_CREAT = 0o100; | ||
| 246 | pub const O_EXCL = 0o200; | ||
| 247 | pub const O_NOCTTY = 0o400; | ||
| 248 | pub const O_TRUNC = 0o1000; | ||
| 249 | pub const O_APPEND = 0o2000; | ||
| 250 | pub const O_NONBLOCK = 0o4000; | ||
| 251 | pub const O_DSYNC = 0o10000; | ||
| 252 | pub const O_SYNC = 0o4010000; | ||
| 253 | pub const O_RSYNC = 0o4010000; | ||
| 254 | pub const O_DIRECTORY = 0o200000; | ||
| 255 | pub const O_NOFOLLOW = 0o400000; | ||
| 256 | pub const O_CLOEXEC = 0o2000000; | ||
| 257 | |||
| 258 | pub const O_ASYNC = 0o20000; | ||
| 259 | pub const O_DIRECT = 0o40000; | ||
| 260 | pub const O_LARGEFILE = 0; | ||
| 261 | pub const O_NOATIME = 0o1000000; | ||
| 262 | pub const O_PATH = 0o10000000; | ||
| 263 | pub const O_TMPFILE = 0o20200000; | ||
| 264 | pub const O_NDELAY = O_NONBLOCK; | ||
| \ No newline at end of file | |||
lib/std/os/test.zig+16-16| ... | @@ -21,7 +21,6 @@ const Dir = std.fs.Dir; | ... | @@ -21,7 +21,6 @@ const Dir = std.fs.Dir; |
| 21 | const ArenaAllocator = std.heap.ArenaAllocator; | 21 | const ArenaAllocator = std.heap.ArenaAllocator; |
| 22 | 22 | ||
| 23 | test "open smoke test" { | 23 | test "open smoke test" { |
| 24 | if (builtin.os.tag == .windows) return error.SkipZigTest; | ||
| 25 | if (builtin.os.tag == .wasi) return error.SkipZigTest; | 24 | if (builtin.os.tag == .wasi) return error.SkipZigTest; |
| 26 | 25 | ||
| 27 | // TODO verify file attributes using `fstat` | 26 | // TODO verify file attributes using `fstat` |
| ... | @@ -40,41 +39,41 @@ test "open smoke test" { | ... | @@ -40,41 +39,41 @@ test "open smoke test" { |
| 40 | 39 | ||
| 41 | var file_path: []u8 = undefined; | 40 | var file_path: []u8 = undefined; |
| 42 | var fd: os.fd_t = undefined; | 41 | var fd: os.fd_t = undefined; |
| 42 | const mode: os.mode_t = if (builtin.os.tag == .windows) 0 else 0o666; | ||
| 43 | 43 | ||
| 44 | // Create some file using `open`. | 44 | // Create some file using `open`. |
| 45 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_file" }); | 45 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_file" }); |
| 46 | fd = try os.open(file_path, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o666); | 46 | fd = try os.open(file_path, os.O_RDWR | os.O_CREAT | os.O_EXCL, mode); |
| 47 | os.close(fd); | 47 | os.close(fd); |
| 48 | 48 | ||
| 49 | // Try this again with the same flags. This op should fail with error.PathAlreadyExists. | 49 | // Try this again with the same flags. This op should fail with error.PathAlreadyExists. |
| 50 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_file" }); | 50 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_file" }); |
| 51 | expectError(error.PathAlreadyExists, os.open(file_path, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o666)); | 51 | expectError(error.PathAlreadyExists, os.open(file_path, os.O_RDWR | os.O_CREAT | os.O_EXCL, mode)); |
| 52 | 52 | ||
| 53 | // Try opening without `O_EXCL` flag. | 53 | // Try opening without `O_EXCL` flag. |
| 54 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_file" }); | 54 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_file" }); |
| 55 | fd = try os.open(file_path, os.O_RDWR | os.O_CREAT, 0o666); | 55 | fd = try os.open(file_path, os.O_RDWR | os.O_CREAT, mode); |
| 56 | os.close(fd); | 56 | os.close(fd); |
| 57 | 57 | ||
| 58 | // Try opening as a directory which should fail. | 58 | // Try opening as a directory which should fail. |
| 59 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_file" }); | 59 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_file" }); |
| 60 | expectError(error.NotDir, os.open(file_path, os.O_RDWR | os.O_DIRECTORY, 0o666)); | 60 | expectError(error.NotDir, os.open(file_path, os.O_RDWR | os.O_DIRECTORY, mode)); |
| 61 | 61 | ||
| 62 | // Create some directory | 62 | // Create some directory |
| 63 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_dir" }); | 63 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_dir" }); |
| 64 | try os.mkdir(file_path, 0o666); | 64 | try os.mkdir(file_path, mode); |
| 65 | 65 | ||
| 66 | // Open dir using `open` | 66 | // Open dir using `open` |
| 67 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_dir" }); | 67 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_dir" }); |
| 68 | fd = try os.open(file_path, os.O_RDONLY | os.O_DIRECTORY, 0o666); | 68 | fd = try os.open(file_path, os.O_RDONLY | os.O_DIRECTORY, mode); |
| 69 | os.close(fd); | 69 | os.close(fd); |
| 70 | 70 | ||
| 71 | // Try opening as file which should fail. | 71 | // Try opening as file which should fail. |
| 72 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_dir" }); | 72 | file_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "some_dir" }); |
| 73 | expectError(error.IsDir, os.open(file_path, os.O_RDWR, 0o666)); | 73 | expectError(error.IsDir, os.open(file_path, os.O_RDWR, mode)); |
| 74 | } | 74 | } |
| 75 | 75 | ||
| 76 | test "openat smoke test" { | 76 | test "openat smoke test" { |
| 77 | if (builtin.os.tag == .windows) return error.SkipZigTest; | ||
| 78 | if (builtin.os.tag == .wasi) return error.SkipZigTest; | 77 | if (builtin.os.tag == .wasi) return error.SkipZigTest; |
| 79 | 78 | ||
| 80 | // TODO verify file attributes using `fstatat` | 79 | // TODO verify file attributes using `fstatat` |
| ... | @@ -83,30 +82,31 @@ test "openat smoke test" { | ... | @@ -83,30 +82,31 @@ test "openat smoke test" { |
| 83 | defer tmp.cleanup(); | 82 | defer tmp.cleanup(); |
| 84 | 83 | ||
| 85 | var fd: os.fd_t = undefined; | 84 | var fd: os.fd_t = undefined; |
| 85 | const mode: os.mode_t = if (builtin.os.tag == .windows) 0 else 0o666; | ||
| 86 | 86 | ||
| 87 | // Create some file using `openat`. | 87 | // Create some file using `openat`. |
| 88 | fd = try os.openat(tmp.dir.fd, "some_file", os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o666); | 88 | fd = try os.openat(tmp.dir.fd, "some_file", os.O_RDWR | os.O_CREAT | os.O_EXCL, mode); |
| 89 | os.close(fd); | 89 | os.close(fd); |
| 90 | 90 | ||
| 91 | // Try this again with the same flags. This op should fail with error.PathAlreadyExists. | 91 | // Try this again with the same flags. This op should fail with error.PathAlreadyExists. |
| 92 | expectError(error.PathAlreadyExists, os.openat(tmp.dir.fd, "some_file", os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o666)); | 92 | expectError(error.PathAlreadyExists, os.openat(tmp.dir.fd, "some_file", os.O_RDWR | os.O_CREAT | os.O_EXCL, mode)); |
| 93 | 93 | ||
| 94 | // Try opening without `O_EXCL` flag. | 94 | // Try opening without `O_EXCL` flag. |
| 95 | fd = try os.openat(tmp.dir.fd, "some_file", os.O_RDWR | os.O_CREAT, 0o666); | 95 | fd = try os.openat(tmp.dir.fd, "some_file", os.O_RDWR | os.O_CREAT, mode); |
| 96 | os.close(fd); | 96 | os.close(fd); |
| 97 | 97 | ||
| 98 | // Try opening as a directory which should fail. | 98 | // Try opening as a directory which should fail. |
| 99 | expectError(error.NotDir, os.openat(tmp.dir.fd, "some_file", os.O_RDWR | os.O_DIRECTORY, 0o666)); | 99 | expectError(error.NotDir, os.openat(tmp.dir.fd, "some_file", os.O_RDWR | os.O_DIRECTORY, mode)); |
| 100 | 100 | ||
| 101 | // Create some directory | 101 | // Create some directory |
| 102 | try os.mkdirat(tmp.dir.fd, "some_dir", 0o666); | 102 | try os.mkdirat(tmp.dir.fd, "some_dir", mode); |
| 103 | 103 | ||
| 104 | // Open dir using `open` | 104 | // Open dir using `open` |
| 105 | fd = try os.openat(tmp.dir.fd, "some_dir", os.O_RDONLY | os.O_DIRECTORY, 0o666); | 105 | fd = try os.openat(tmp.dir.fd, "some_dir", os.O_RDONLY | os.O_DIRECTORY, mode); |
| 106 | os.close(fd); | 106 | os.close(fd); |
| 107 | 107 | ||
| 108 | // Try opening as file which should fail. | 108 | // Try opening as file which should fail. |
| 109 | expectError(error.IsDir, os.openat(tmp.dir.fd, "some_dir", os.O_RDWR, 0o666)); | 109 | expectError(error.IsDir, os.openat(tmp.dir.fd, "some_dir", os.O_RDWR, mode)); |
| 110 | } | 110 | } |
| 111 | 111 | ||
| 112 | test "symlink with relative paths" { | 112 | test "symlink with relative paths" { |
lib/std/os/windows.zig+4-1| ... | @@ -27,6 +27,7 @@ pub const self_process_handle = @intToPtr(HANDLE, maxInt(usize)); | ... | @@ -27,6 +27,7 @@ pub const self_process_handle = @intToPtr(HANDLE, maxInt(usize)); |
| 27 | 27 | ||
| 28 | pub const OpenError = error{ | 28 | pub const OpenError = error{ |
| 29 | IsDir, | 29 | IsDir, |
| 30 | NotDir, | ||
| 30 | FileNotFound, | 31 | FileNotFound, |
| 31 | NoDevice, | 32 | NoDevice, |
| 32 | AccessDenied, | 33 | AccessDenied, |
| ... | @@ -125,7 +126,8 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN | ... | @@ -125,7 +126,8 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN |
| 125 | .PIPE_BUSY => return error.PipeBusy, | 126 | .PIPE_BUSY => return error.PipeBusy, |
| 126 | .OBJECT_PATH_SYNTAX_BAD => unreachable, | 127 | .OBJECT_PATH_SYNTAX_BAD => unreachable, |
| 127 | .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, | 128 | .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, |
| 128 | .FILE_IS_A_DIRECTORY => if (options.open_dir) unreachable else return error.IsDir, | 129 | .FILE_IS_A_DIRECTORY => return error.IsDir, |
| 130 | .NOT_A_DIRECTORY => return error.NotDir, | ||
| 129 | else => return unexpectedStatus(rc), | 131 | else => return unexpectedStatus(rc), |
| 130 | } | 132 | } |
| 131 | } | 133 | } |
| ... | @@ -609,6 +611,7 @@ pub fn CreateSymbolicLink( | ... | @@ -609,6 +611,7 @@ pub fn CreateSymbolicLink( |
| 609 | .open_dir = is_directory, | 611 | .open_dir = is_directory, |
| 610 | }) catch |err| switch (err) { | 612 | }) catch |err| switch (err) { |
| 611 | error.IsDir => return error.PathAlreadyExists, | 613 | error.IsDir => return error.PathAlreadyExists, |
| 614 | error.NotDir => unreachable, | ||
| 612 | error.WouldBlock => unreachable, | 615 | error.WouldBlock => unreachable, |
| 613 | error.PipeBusy => unreachable, | 616 | error.PipeBusy => unreachable, |
| 614 | else => |e| return e, | 617 | else => |e| return e, |