| author | |
| committer | |
| log | f4e426a06c59922de80a572f8346f2797726b392 |
| tree | f979587285f09c8b857e4a3e5dfe8aaacded7f52 |
| parent | 6fc20b3b092a5717caa288c107370292580f0e2c |
| parent | 0a536a7c9020c891001be053a4c0b354961cc46f |
| signature |
std.fs: Absorb `IterableDir` into `Dir`24 files changed, 4404 insertions(+), 4445 deletions(-)
CMakeLists.txt+3-1| ... | ... | @@ -247,7 +247,9 @@ set(ZIG_STAGE2_SOURCES |
| 247 | 247 | "${CMAKE_SOURCE_DIR}/lib/std/fmt/errol/lookup.zig" |
| 248 | 248 | "${CMAKE_SOURCE_DIR}/lib/std/fmt/parse_float.zig" |
| 249 | 249 | "${CMAKE_SOURCE_DIR}/lib/std/fs.zig" |
| 250 | "${CMAKE_SOURCE_DIR}/lib/std/fs/file.zig" | |
| 250 | "${CMAKE_SOURCE_DIR}/lib/std/fs/AtomicFile.zig" | |
| 251 | "${CMAKE_SOURCE_DIR}/lib/std/fs/Dir.zig" | |
| 252 | "${CMAKE_SOURCE_DIR}/lib/std/fs/File.zig" | |
| 251 | 253 | "${CMAKE_SOURCE_DIR}/lib/std/fs/get_app_data_dir.zig" |
| 252 | 254 | "${CMAKE_SOURCE_DIR}/lib/std/fs/path.zig" |
| 253 | 255 | "${CMAKE_SOURCE_DIR}/lib/std/hash.zig" |
lib/std/Build/Step/InstallDir.zig+1-1| ... | ... | @@ -69,7 +69,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { |
| 69 | 69 | const dest_prefix = dest_builder.getInstallPath(self.options.install_dir, self.options.install_subdir); |
| 70 | 70 | const src_builder = self.step.owner; |
| 71 | 71 | const src_dir_path = self.options.source_dir.getPath2(src_builder, step); |
| 72 | var src_dir = src_builder.build_root.handle.openIterableDir(src_dir_path, .{}) catch |err| { | |
| 72 | var src_dir = src_builder.build_root.handle.openDir(src_dir_path, .{ .iterate = true }) catch |err| { | |
| 73 | 73 | return step.fail("unable to open source directory '{}{s}': {s}", .{ |
| 74 | 74 | src_builder.build_root, src_dir_path, @errorName(err), |
| 75 | 75 | }); |
lib/std/child_process.zig+2-1| ... | ... | @@ -976,7 +976,8 @@ fn windowsCreateProcessPathExt( |
| 976 | 976 | defer dir_buf.shrinkRetainingCapacity(dir_path_len); |
| 977 | 977 | const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; |
| 978 | 978 | const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); |
| 979 | break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{}, true) catch return error.FileNotFound; | |
| 979 | break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{ .iterate = true }) catch | |
| 980 | return error.FileNotFound; | |
| 980 | 981 | }; |
| 981 | 982 | defer dir.close(); |
| 982 | 983 |
lib/std/crypto/Certificate/Bundle.zig+4-4| ... | ... | @@ -160,7 +160,7 @@ pub fn addCertsFromDirPath( |
| 160 | 160 | dir: fs.Dir, |
| 161 | 161 | sub_dir_path: []const u8, |
| 162 | 162 | ) AddCertsFromDirPathError!void { |
| 163 | var iterable_dir = try dir.openIterableDir(sub_dir_path, .{}); | |
| 163 | var iterable_dir = try dir.openDir(sub_dir_path, .{ .iterate = true }); | |
| 164 | 164 | defer iterable_dir.close(); |
| 165 | 165 | return addCertsFromDir(cb, gpa, iterable_dir); |
| 166 | 166 | } |
| ... | ... | @@ -171,14 +171,14 @@ pub fn addCertsFromDirPathAbsolute( |
| 171 | 171 | abs_dir_path: []const u8, |
| 172 | 172 | ) AddCertsFromDirPathError!void { |
| 173 | 173 | assert(fs.path.isAbsolute(abs_dir_path)); |
| 174 | var iterable_dir = try fs.openIterableDirAbsolute(abs_dir_path, .{}); | |
| 174 | var iterable_dir = try fs.openDirAbsolute(abs_dir_path, .{ .iterate = true }); | |
| 175 | 175 | defer iterable_dir.close(); |
| 176 | 176 | return addCertsFromDir(cb, gpa, iterable_dir); |
| 177 | 177 | } |
| 178 | 178 | |
| 179 | 179 | pub const AddCertsFromDirError = AddCertsFromFilePathError; |
| 180 | 180 | |
| 181 | pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir) AddCertsFromDirError!void { | |
| 181 | pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.Dir) AddCertsFromDirError!void { | |
| 182 | 182 | var it = iterable_dir.iterate(); |
| 183 | 183 | while (try it.next()) |entry| { |
| 184 | 184 | switch (entry.kind) { |
| ... | ... | @@ -186,7 +186,7 @@ pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir |
| 186 | 186 | else => continue, |
| 187 | 187 | } |
| 188 | 188 | |
| 189 | try addCertsFromFilePath(cb, gpa, iterable_dir.dir, entry.name); | |
| 189 | try addCertsFromFilePath(cb, gpa, iterable_dir, entry.name); | |
| 190 | 190 | } |
| 191 | 191 | } |
| 192 | 192 |
lib/std/fs.zig+38-2671| ... | ... | @@ -7,17 +7,19 @@ const base64 = std.base64; |
| 7 | 7 | const crypto = std.crypto; |
| 8 | 8 | const Allocator = std.mem.Allocator; |
| 9 | 9 | const assert = std.debug.assert; |
| 10 | const math = std.math; | |
| 11 | 10 | |
| 12 | 11 | const is_darwin = builtin.os.tag.isDarwin(); |
| 13 | 12 | |
| 13 | pub const AtomicFile = @import("fs/AtomicFile.zig"); | |
| 14 | pub const Dir = @import("fs/Dir.zig"); | |
| 15 | pub const File = @import("fs/File.zig"); | |
| 16 | pub const path = @import("fs/path.zig"); | |
| 17 | ||
| 14 | 18 | pub const has_executable_bit = switch (builtin.os.tag) { |
| 15 | 19 | .windows, .wasi => false, |
| 16 | 20 | else => true, |
| 17 | 21 | }; |
| 18 | 22 | |
| 19 | pub const path = @import("fs/path.zig"); | |
| 20 | pub const File = @import("fs/file.zig").File; | |
| 21 | 23 | pub const wasi = @import("fs/wasi.zig"); |
| 22 | 24 | |
| 23 | 25 | // TODO audit these APIs with respect to Dir and absolute paths |
| ... | ... | @@ -92,6 +94,7 @@ pub const need_async_thread = std.io.is_async and switch (builtin.os.tag) { |
| 92 | 94 | }; |
| 93 | 95 | |
| 94 | 96 | /// TODO remove the allocator requirement from this API |
| 97 | /// TODO move to Dir | |
| 95 | 98 | pub fn atomicSymLink(allocator: Allocator, existing_path: []const u8, new_path: []const u8) !void { |
| 96 | 99 | if (cwd().symLink(existing_path, new_path, .{})) { |
| 97 | 100 | return; |
| ... | ... | @@ -102,7 +105,7 @@ pub fn atomicSymLink(allocator: Allocator, existing_path: []const u8, new_path: |
| 102 | 105 | |
| 103 | 106 | const dirname = path.dirname(new_path) orelse "."; |
| 104 | 107 | |
| 105 | var rand_buf: [AtomicFile.RANDOM_BYTES]u8 = undefined; | |
| 108 | var rand_buf: [AtomicFile.random_bytes_len]u8 = undefined; | |
| 106 | 109 | const tmp_path = try allocator.alloc(u8, dirname.len + 1 + base64_encoder.calcSize(rand_buf.len)); |
| 107 | 110 | defer allocator.free(tmp_path); |
| 108 | 111 | @memcpy(tmp_path[0..dirname.len], dirname); |
| ... | ... | @@ -120,24 +123,14 @@ pub fn atomicSymLink(allocator: Allocator, existing_path: []const u8, new_path: |
| 120 | 123 | } |
| 121 | 124 | } |
| 122 | 125 | |
| 123 | pub const PrevStatus = enum { | |
| 124 | stale, | |
| 125 | fresh, | |
| 126 | }; | |
| 127 | ||
| 128 | pub const CopyFileOptions = struct { | |
| 129 | /// When this is `null` the mode is copied from the source file. | |
| 130 | override_mode: ?File.Mode = null, | |
| 131 | }; | |
| 132 | ||
| 133 | 126 | /// Same as `Dir.updateFile`, except asserts that both `source_path` and `dest_path` |
| 134 | 127 | /// are absolute. See `Dir.updateFile` for a function that operates on both |
| 135 | 128 | /// absolute and relative paths. |
| 136 | 129 | pub fn updateFileAbsolute( |
| 137 | 130 | source_path: []const u8, |
| 138 | 131 | dest_path: []const u8, |
| 139 | args: CopyFileOptions, | |
| 140 | ) !PrevStatus { | |
| 132 | args: Dir.CopyFileOptions, | |
| 133 | ) !Dir.PrevStatus { | |
| 141 | 134 | assert(path.isAbsolute(source_path)); |
| 142 | 135 | assert(path.isAbsolute(dest_path)); |
| 143 | 136 | const my_cwd = cwd(); |
| ... | ... | @@ -147,112 +140,35 @@ pub fn updateFileAbsolute( |
| 147 | 140 | /// Same as `Dir.copyFile`, except asserts that both `source_path` and `dest_path` |
| 148 | 141 | /// are absolute. See `Dir.copyFile` for a function that operates on both |
| 149 | 142 | /// absolute and relative paths. |
| 150 | pub fn copyFileAbsolute(source_path: []const u8, dest_path: []const u8, args: CopyFileOptions) !void { | |
| 143 | pub fn copyFileAbsolute( | |
| 144 | source_path: []const u8, | |
| 145 | dest_path: []const u8, | |
| 146 | args: Dir.CopyFileOptions, | |
| 147 | ) !void { | |
| 151 | 148 | assert(path.isAbsolute(source_path)); |
| 152 | 149 | assert(path.isAbsolute(dest_path)); |
| 153 | 150 | const my_cwd = cwd(); |
| 154 | 151 | return Dir.copyFile(my_cwd, source_path, my_cwd, dest_path, args); |
| 155 | 152 | } |
| 156 | 153 | |
| 157 | pub const AtomicFile = struct { | |
| 158 | file: File, | |
| 159 | // TODO either replace this with rand_buf or use []u16 on Windows | |
| 160 | tmp_path_buf: [TMP_PATH_LEN:0]u8, | |
| 161 | dest_basename: []const u8, | |
| 162 | file_open: bool, | |
| 163 | file_exists: bool, | |
| 164 | close_dir_on_deinit: bool, | |
| 165 | dir: Dir, | |
| 166 | ||
| 167 | const InitError = File.OpenError; | |
| 168 | ||
| 169 | const RANDOM_BYTES = 12; | |
| 170 | const TMP_PATH_LEN = base64_encoder.calcSize(RANDOM_BYTES); | |
| 171 | ||
| 172 | /// Note that the `Dir.atomicFile` API may be more handy than this lower-level function. | |
| 173 | pub fn init( | |
| 174 | dest_basename: []const u8, | |
| 175 | mode: File.Mode, | |
| 176 | dir: Dir, | |
| 177 | close_dir_on_deinit: bool, | |
| 178 | ) InitError!AtomicFile { | |
| 179 | var rand_buf: [RANDOM_BYTES]u8 = undefined; | |
| 180 | var tmp_path_buf: [TMP_PATH_LEN:0]u8 = undefined; | |
| 181 | ||
| 182 | while (true) { | |
| 183 | crypto.random.bytes(rand_buf[0..]); | |
| 184 | const tmp_path = base64_encoder.encode(&tmp_path_buf, &rand_buf); | |
| 185 | tmp_path_buf[tmp_path.len] = 0; | |
| 186 | ||
| 187 | const file = dir.createFile( | |
| 188 | tmp_path, | |
| 189 | .{ .mode = mode, .exclusive = true }, | |
| 190 | ) catch |err| switch (err) { | |
| 191 | error.PathAlreadyExists => continue, | |
| 192 | else => |e| return e, | |
| 193 | }; | |
| 194 | ||
| 195 | return AtomicFile{ | |
| 196 | .file = file, | |
| 197 | .tmp_path_buf = tmp_path_buf, | |
| 198 | .dest_basename = dest_basename, | |
| 199 | .file_open = true, | |
| 200 | .file_exists = true, | |
| 201 | .close_dir_on_deinit = close_dir_on_deinit, | |
| 202 | .dir = dir, | |
| 203 | }; | |
| 204 | } | |
| 205 | } | |
| 206 | ||
| 207 | /// Always call deinit, even after a successful finish(). | |
| 208 | pub fn deinit(self: *AtomicFile) void { | |
| 209 | if (self.file_open) { | |
| 210 | self.file.close(); | |
| 211 | self.file_open = false; | |
| 212 | } | |
| 213 | if (self.file_exists) { | |
| 214 | self.dir.deleteFile(&self.tmp_path_buf) catch {}; | |
| 215 | self.file_exists = false; | |
| 216 | } | |
| 217 | if (self.close_dir_on_deinit) { | |
| 218 | self.dir.close(); | |
| 219 | } | |
| 220 | self.* = undefined; | |
| 221 | } | |
| 222 | ||
| 223 | pub const FinishError = std.os.RenameError; | |
| 224 | ||
| 225 | pub fn finish(self: *AtomicFile) FinishError!void { | |
| 226 | assert(self.file_exists); | |
| 227 | if (self.file_open) { | |
| 228 | self.file.close(); | |
| 229 | self.file_open = false; | |
| 230 | } | |
| 231 | try os.renameat(self.dir.fd, self.tmp_path_buf[0..], self.dir.fd, self.dest_basename); | |
| 232 | self.file_exists = false; | |
| 233 | } | |
| 234 | }; | |
| 235 | ||
| 236 | const default_new_dir_mode = 0o755; | |
| 237 | ||
| 238 | 154 | /// Create a new directory, based on an absolute path. |
| 239 | 155 | /// Asserts that the path is absolute. See `Dir.makeDir` for a function that operates |
| 240 | 156 | /// on both absolute and relative paths. |
| 241 | 157 | pub fn makeDirAbsolute(absolute_path: []const u8) !void { |
| 242 | 158 | assert(path.isAbsolute(absolute_path)); |
| 243 | return os.mkdir(absolute_path, default_new_dir_mode); | |
| 159 | return os.mkdir(absolute_path, Dir.default_mode); | |
| 244 | 160 | } |
| 245 | 161 | |
| 246 | 162 | /// Same as `makeDirAbsolute` except the parameter is a null-terminated UTF-8-encoded string. |
| 247 | 163 | pub fn makeDirAbsoluteZ(absolute_path_z: [*:0]const u8) !void { |
| 248 | 164 | assert(path.isAbsoluteZ(absolute_path_z)); |
| 249 | return os.mkdirZ(absolute_path_z, default_new_dir_mode); | |
| 165 | return os.mkdirZ(absolute_path_z, Dir.default_mode); | |
| 250 | 166 | } |
| 251 | 167 | |
| 252 | 168 | /// Same as `makeDirAbsolute` except the parameter is a null-terminated WTF-16-encoded string. |
| 253 | 169 | pub fn makeDirAbsoluteW(absolute_path_w: [*:0]const u16) !void { |
| 254 | 170 | assert(path.isAbsoluteWindowsW(absolute_path_w)); |
| 255 | return os.mkdirW(absolute_path_w, default_new_dir_mode); | |
| 171 | return os.mkdirW(absolute_path_w, Dir.default_mode); | |
| 256 | 172 | } |
| 257 | 173 | |
| 258 | 174 | /// Same as `Dir.deleteDir` except the path is absolute. |
| ... | ... | @@ -310,2486 +226,6 @@ pub fn renameW(old_dir: Dir, old_sub_path_w: []const u16, new_dir: Dir, new_sub_ |
| 310 | 226 | return os.renameatW(old_dir.fd, old_sub_path_w, new_dir.fd, new_sub_path_w); |
| 311 | 227 | } |
| 312 | 228 | |
| 313 | /// A directory that can be iterated. It is *NOT* legal to initialize this with a regular `Dir` | |
| 314 | /// that has been opened without iteration permission. | |
| 315 | pub const IterableDir = struct { | |
| 316 | dir: Dir, | |
| 317 | ||
| 318 | pub const Entry = struct { | |
| 319 | name: []const u8, | |
| 320 | kind: Kind, | |
| 321 | ||
| 322 | pub const Kind = File.Kind; | |
| 323 | }; | |
| 324 | ||
| 325 | const IteratorError = error{ AccessDenied, SystemResources } || os.UnexpectedError; | |
| 326 | ||
| 327 | pub const Iterator = switch (builtin.os.tag) { | |
| 328 | .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => struct { | |
| 329 | dir: Dir, | |
| 330 | seek: i64, | |
| 331 | buf: [1024]u8, // TODO align(@alignOf(os.system.dirent)), | |
| 332 | index: usize, | |
| 333 | end_index: usize, | |
| 334 | first_iter: bool, | |
| 335 | ||
| 336 | const Self = @This(); | |
| 337 | ||
| 338 | pub const Error = IteratorError; | |
| 339 | ||
| 340 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 341 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 342 | pub fn next(self: *Self) Error!?Entry { | |
| 343 | switch (builtin.os.tag) { | |
| 344 | .macos, .ios => return self.nextDarwin(), | |
| 345 | .freebsd, .netbsd, .dragonfly, .openbsd => return self.nextBsd(), | |
| 346 | .solaris, .illumos => return self.nextSolaris(), | |
| 347 | else => @compileError("unimplemented"), | |
| 348 | } | |
| 349 | } | |
| 350 | ||
| 351 | fn nextDarwin(self: *Self) !?Entry { | |
| 352 | start_over: while (true) { | |
| 353 | if (self.index >= self.end_index) { | |
| 354 | if (self.first_iter) { | |
| 355 | std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 356 | self.first_iter = false; | |
| 357 | } | |
| 358 | const rc = os.system.__getdirentries64( | |
| 359 | self.dir.fd, | |
| 360 | &self.buf, | |
| 361 | self.buf.len, | |
| 362 | &self.seek, | |
| 363 | ); | |
| 364 | if (rc == 0) return null; | |
| 365 | if (rc < 0) { | |
| 366 | switch (os.errno(rc)) { | |
| 367 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 368 | .FAULT => unreachable, | |
| 369 | .NOTDIR => unreachable, | |
| 370 | .INVAL => unreachable, | |
| 371 | else => |err| return os.unexpectedErrno(err), | |
| 372 | } | |
| 373 | } | |
| 374 | self.index = 0; | |
| 375 | self.end_index = @as(usize, @intCast(rc)); | |
| 376 | } | |
| 377 | const darwin_entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 378 | const next_index = self.index + darwin_entry.reclen(); | |
| 379 | self.index = next_index; | |
| 380 | ||
| 381 | const name = @as([*]u8, @ptrCast(&darwin_entry.d_name))[0..darwin_entry.d_namlen]; | |
| 382 | ||
| 383 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (darwin_entry.d_ino == 0)) { | |
| 384 | continue :start_over; | |
| 385 | } | |
| 386 | ||
| 387 | const entry_kind: Entry.Kind = switch (darwin_entry.d_type) { | |
| 388 | os.DT.BLK => .block_device, | |
| 389 | os.DT.CHR => .character_device, | |
| 390 | os.DT.DIR => .directory, | |
| 391 | os.DT.FIFO => .named_pipe, | |
| 392 | os.DT.LNK => .sym_link, | |
| 393 | os.DT.REG => .file, | |
| 394 | os.DT.SOCK => .unix_domain_socket, | |
| 395 | os.DT.WHT => .whiteout, | |
| 396 | else => .unknown, | |
| 397 | }; | |
| 398 | return Entry{ | |
| 399 | .name = name, | |
| 400 | .kind = entry_kind, | |
| 401 | }; | |
| 402 | } | |
| 403 | } | |
| 404 | ||
| 405 | fn nextSolaris(self: *Self) !?Entry { | |
| 406 | start_over: while (true) { | |
| 407 | if (self.index >= self.end_index) { | |
| 408 | if (self.first_iter) { | |
| 409 | std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 410 | self.first_iter = false; | |
| 411 | } | |
| 412 | const rc = os.system.getdents(self.dir.fd, &self.buf, self.buf.len); | |
| 413 | switch (os.errno(rc)) { | |
| 414 | .SUCCESS => {}, | |
| 415 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 416 | .FAULT => unreachable, | |
| 417 | .NOTDIR => unreachable, | |
| 418 | .INVAL => unreachable, | |
| 419 | else => |err| return os.unexpectedErrno(err), | |
| 420 | } | |
| 421 | if (rc == 0) return null; | |
| 422 | self.index = 0; | |
| 423 | self.end_index = @as(usize, @intCast(rc)); | |
| 424 | } | |
| 425 | const entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 426 | const next_index = self.index + entry.reclen(); | |
| 427 | self.index = next_index; | |
| 428 | ||
| 429 | const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&entry.d_name)), 0); | |
| 430 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) | |
| 431 | continue :start_over; | |
| 432 | ||
| 433 | // Solaris dirent doesn't expose d_type, so we have to call stat to get it. | |
| 434 | const stat_info = os.fstatat( | |
| 435 | self.dir.fd, | |
| 436 | name, | |
| 437 | os.AT.SYMLINK_NOFOLLOW, | |
| 438 | ) catch |err| switch (err) { | |
| 439 | error.NameTooLong => unreachable, | |
| 440 | error.SymLinkLoop => unreachable, | |
| 441 | error.FileNotFound => unreachable, // lost the race | |
| 442 | else => |e| return e, | |
| 443 | }; | |
| 444 | const entry_kind: Entry.Kind = switch (stat_info.mode & os.S.IFMT) { | |
| 445 | os.S.IFIFO => .named_pipe, | |
| 446 | os.S.IFCHR => .character_device, | |
| 447 | os.S.IFDIR => .directory, | |
| 448 | os.S.IFBLK => .block_device, | |
| 449 | os.S.IFREG => .file, | |
| 450 | os.S.IFLNK => .sym_link, | |
| 451 | os.S.IFSOCK => .unix_domain_socket, | |
| 452 | os.S.IFDOOR => .door, | |
| 453 | os.S.IFPORT => .event_port, | |
| 454 | else => .unknown, | |
| 455 | }; | |
| 456 | return Entry{ | |
| 457 | .name = name, | |
| 458 | .kind = entry_kind, | |
| 459 | }; | |
| 460 | } | |
| 461 | } | |
| 462 | ||
| 463 | fn nextBsd(self: *Self) !?Entry { | |
| 464 | start_over: while (true) { | |
| 465 | if (self.index >= self.end_index) { | |
| 466 | if (self.first_iter) { | |
| 467 | std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 468 | self.first_iter = false; | |
| 469 | } | |
| 470 | const rc = if (builtin.os.tag == .netbsd) | |
| 471 | os.system.__getdents30(self.dir.fd, &self.buf, self.buf.len) | |
| 472 | else | |
| 473 | os.system.getdents(self.dir.fd, &self.buf, self.buf.len); | |
| 474 | switch (os.errno(rc)) { | |
| 475 | .SUCCESS => {}, | |
| 476 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 477 | .FAULT => unreachable, | |
| 478 | .NOTDIR => unreachable, | |
| 479 | .INVAL => unreachable, | |
| 480 | // Introduced in freebsd 13.2: directory unlinked but still open. | |
| 481 | // To be consistent, iteration ends if the directory being iterated is deleted during iteration. | |
| 482 | .NOENT => return null, | |
| 483 | else => |err| return os.unexpectedErrno(err), | |
| 484 | } | |
| 485 | if (rc == 0) return null; | |
| 486 | self.index = 0; | |
| 487 | self.end_index = @as(usize, @intCast(rc)); | |
| 488 | } | |
| 489 | const bsd_entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 490 | const next_index = self.index + bsd_entry.reclen(); | |
| 491 | self.index = next_index; | |
| 492 | ||
| 493 | const name = @as([*]u8, @ptrCast(&bsd_entry.d_name))[0..bsd_entry.d_namlen]; | |
| 494 | ||
| 495 | const skip_zero_fileno = switch (builtin.os.tag) { | |
| 496 | // d_fileno=0 is used to mark invalid entries or deleted files. | |
| 497 | .openbsd, .netbsd => true, | |
| 498 | else => false, | |
| 499 | }; | |
| 500 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or | |
| 501 | (skip_zero_fileno and bsd_entry.d_fileno == 0)) | |
| 502 | { | |
| 503 | continue :start_over; | |
| 504 | } | |
| 505 | ||
| 506 | const entry_kind: Entry.Kind = switch (bsd_entry.d_type) { | |
| 507 | os.DT.BLK => .block_device, | |
| 508 | os.DT.CHR => .character_device, | |
| 509 | os.DT.DIR => .directory, | |
| 510 | os.DT.FIFO => .named_pipe, | |
| 511 | os.DT.LNK => .sym_link, | |
| 512 | os.DT.REG => .file, | |
| 513 | os.DT.SOCK => .unix_domain_socket, | |
| 514 | os.DT.WHT => .whiteout, | |
| 515 | else => .unknown, | |
| 516 | }; | |
| 517 | return Entry{ | |
| 518 | .name = name, | |
| 519 | .kind = entry_kind, | |
| 520 | }; | |
| 521 | } | |
| 522 | } | |
| 523 | ||
| 524 | pub fn reset(self: *Self) void { | |
| 525 | self.index = 0; | |
| 526 | self.end_index = 0; | |
| 527 | self.first_iter = true; | |
| 528 | } | |
| 529 | }, | |
| 530 | .haiku => struct { | |
| 531 | dir: Dir, | |
| 532 | buf: [1024]u8, // TODO align(@alignOf(os.dirent64)), | |
| 533 | index: usize, | |
| 534 | end_index: usize, | |
| 535 | first_iter: bool, | |
| 536 | ||
| 537 | const Self = @This(); | |
| 538 | ||
| 539 | pub const Error = IteratorError; | |
| 540 | ||
| 541 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 542 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 543 | pub fn next(self: *Self) Error!?Entry { | |
| 544 | start_over: while (true) { | |
| 545 | // TODO: find a better max | |
| 546 | const HAIKU_MAX_COUNT = 10000; | |
| 547 | if (self.index >= self.end_index) { | |
| 548 | if (self.first_iter) { | |
| 549 | std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 550 | self.first_iter = false; | |
| 551 | } | |
| 552 | const rc = os.system._kern_read_dir( | |
| 553 | self.dir.fd, | |
| 554 | &self.buf, | |
| 555 | self.buf.len, | |
| 556 | HAIKU_MAX_COUNT, | |
| 557 | ); | |
| 558 | if (rc == 0) return null; | |
| 559 | if (rc < 0) { | |
| 560 | switch (os.errno(rc)) { | |
| 561 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 562 | .FAULT => unreachable, | |
| 563 | .NOTDIR => unreachable, | |
| 564 | .INVAL => unreachable, | |
| 565 | else => |err| return os.unexpectedErrno(err), | |
| 566 | } | |
| 567 | } | |
| 568 | self.index = 0; | |
| 569 | self.end_index = @as(usize, @intCast(rc)); | |
| 570 | } | |
| 571 | const haiku_entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 572 | const next_index = self.index + haiku_entry.reclen(); | |
| 573 | self.index = next_index; | |
| 574 | const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&haiku_entry.d_name)), 0); | |
| 575 | ||
| 576 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (haiku_entry.d_ino == 0)) { | |
| 577 | continue :start_over; | |
| 578 | } | |
| 579 | ||
| 580 | var stat_info: os.Stat = undefined; | |
| 581 | const rc = os.system._kern_read_stat( | |
| 582 | self.dir.fd, | |
| 583 | &haiku_entry.d_name, | |
| 584 | false, | |
| 585 | &stat_info, | |
| 586 | 0, | |
| 587 | ); | |
| 588 | if (rc != 0) { | |
| 589 | switch (os.errno(rc)) { | |
| 590 | .SUCCESS => {}, | |
| 591 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 592 | .FAULT => unreachable, | |
| 593 | .NOTDIR => unreachable, | |
| 594 | .INVAL => unreachable, | |
| 595 | else => |err| return os.unexpectedErrno(err), | |
| 596 | } | |
| 597 | } | |
| 598 | const statmode = stat_info.mode & os.S.IFMT; | |
| 599 | ||
| 600 | const entry_kind: Entry.Kind = switch (statmode) { | |
| 601 | os.S.IFDIR => .directory, | |
| 602 | os.S.IFBLK => .block_device, | |
| 603 | os.S.IFCHR => .character_device, | |
| 604 | os.S.IFLNK => .sym_link, | |
| 605 | os.S.IFREG => .file, | |
| 606 | os.S.IFIFO => .named_pipe, | |
| 607 | else => .unknown, | |
| 608 | }; | |
| 609 | ||
| 610 | return Entry{ | |
| 611 | .name = name, | |
| 612 | .kind = entry_kind, | |
| 613 | }; | |
| 614 | } | |
| 615 | } | |
| 616 | ||
| 617 | pub fn reset(self: *Self) void { | |
| 618 | self.index = 0; | |
| 619 | self.end_index = 0; | |
| 620 | self.first_iter = true; | |
| 621 | } | |
| 622 | }, | |
| 623 | .linux => struct { | |
| 624 | dir: Dir, | |
| 625 | // The if guard is solely there to prevent compile errors from missing `linux.dirent64` | |
| 626 | // definition when compiling for other OSes. It doesn't do anything when compiling for Linux. | |
| 627 | buf: [1024]u8 align(if (builtin.os.tag != .linux) 1 else @alignOf(linux.dirent64)), | |
| 628 | index: usize, | |
| 629 | end_index: usize, | |
| 630 | first_iter: bool, | |
| 631 | ||
| 632 | const Self = @This(); | |
| 633 | const linux = os.linux; | |
| 634 | ||
| 635 | pub const Error = IteratorError; | |
| 636 | ||
| 637 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 638 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 639 | pub fn next(self: *Self) Error!?Entry { | |
| 640 | return self.nextLinux() catch |err| switch (err) { | |
| 641 | // To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration. | |
| 642 | // This matches the behavior of non-Linux UNIX platforms. | |
| 643 | error.DirNotFound => null, | |
| 644 | else => |e| return e, | |
| 645 | }; | |
| 646 | } | |
| 647 | ||
| 648 | pub const ErrorLinux = error{DirNotFound} || IteratorError; | |
| 649 | ||
| 650 | /// Implementation of `next` that can return `error.DirNotFound` if the directory being | |
| 651 | /// iterated was deleted during iteration (this error is Linux specific). | |
| 652 | pub fn nextLinux(self: *Self) ErrorLinux!?Entry { | |
| 653 | start_over: while (true) { | |
| 654 | if (self.index >= self.end_index) { | |
| 655 | if (self.first_iter) { | |
| 656 | std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 657 | self.first_iter = false; | |
| 658 | } | |
| 659 | const rc = linux.getdents64(self.dir.fd, &self.buf, self.buf.len); | |
| 660 | switch (linux.getErrno(rc)) { | |
| 661 | .SUCCESS => {}, | |
| 662 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 663 | .FAULT => unreachable, | |
| 664 | .NOTDIR => unreachable, | |
| 665 | .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. | |
| 666 | .INVAL => return error.Unexpected, // Linux may in some cases return EINVAL when reading /proc/$PID/net. | |
| 667 | .ACCES => return error.AccessDenied, // Do not have permission to iterate this directory. | |
| 668 | else => |err| return os.unexpectedErrno(err), | |
| 669 | } | |
| 670 | if (rc == 0) return null; | |
| 671 | self.index = 0; | |
| 672 | self.end_index = rc; | |
| 673 | } | |
| 674 | const linux_entry = @as(*align(1) linux.dirent64, @ptrCast(&self.buf[self.index])); | |
| 675 | const next_index = self.index + linux_entry.reclen(); | |
| 676 | self.index = next_index; | |
| 677 | ||
| 678 | const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&linux_entry.d_name)), 0); | |
| 679 | ||
| 680 | // skip . and .. entries | |
| 681 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { | |
| 682 | continue :start_over; | |
| 683 | } | |
| 684 | ||
| 685 | const entry_kind: Entry.Kind = switch (linux_entry.d_type) { | |
| 686 | linux.DT.BLK => .block_device, | |
| 687 | linux.DT.CHR => .character_device, | |
| 688 | linux.DT.DIR => .directory, | |
| 689 | linux.DT.FIFO => .named_pipe, | |
| 690 | linux.DT.LNK => .sym_link, | |
| 691 | linux.DT.REG => .file, | |
| 692 | linux.DT.SOCK => .unix_domain_socket, | |
| 693 | else => .unknown, | |
| 694 | }; | |
| 695 | return Entry{ | |
| 696 | .name = name, | |
| 697 | .kind = entry_kind, | |
| 698 | }; | |
| 699 | } | |
| 700 | } | |
| 701 | ||
| 702 | pub fn reset(self: *Self) void { | |
| 703 | self.index = 0; | |
| 704 | self.end_index = 0; | |
| 705 | self.first_iter = true; | |
| 706 | } | |
| 707 | }, | |
| 708 | .windows => struct { | |
| 709 | dir: Dir, | |
| 710 | buf: [1024]u8 align(@alignOf(os.windows.FILE_BOTH_DIR_INFORMATION)), | |
| 711 | index: usize, | |
| 712 | end_index: usize, | |
| 713 | first_iter: bool, | |
| 714 | name_data: [MAX_NAME_BYTES]u8, | |
| 715 | ||
| 716 | const Self = @This(); | |
| 717 | ||
| 718 | pub const Error = IteratorError; | |
| 719 | ||
| 720 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 721 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 722 | pub fn next(self: *Self) Error!?Entry { | |
| 723 | while (true) { | |
| 724 | const w = os.windows; | |
| 725 | if (self.index >= self.end_index) { | |
| 726 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 727 | const rc = w.ntdll.NtQueryDirectoryFile( | |
| 728 | self.dir.fd, | |
| 729 | null, | |
| 730 | null, | |
| 731 | null, | |
| 732 | &io, | |
| 733 | &self.buf, | |
| 734 | self.buf.len, | |
| 735 | .FileBothDirectoryInformation, | |
| 736 | w.FALSE, | |
| 737 | null, | |
| 738 | if (self.first_iter) @as(w.BOOLEAN, w.TRUE) else @as(w.BOOLEAN, w.FALSE), | |
| 739 | ); | |
| 740 | self.first_iter = false; | |
| 741 | if (io.Information == 0) return null; | |
| 742 | self.index = 0; | |
| 743 | self.end_index = io.Information; | |
| 744 | switch (rc) { | |
| 745 | .SUCCESS => {}, | |
| 746 | .ACCESS_DENIED => return error.AccessDenied, // Double-check that the Dir was opened with iteration ability | |
| 747 | ||
| 748 | else => return w.unexpectedStatus(rc), | |
| 749 | } | |
| 750 | } | |
| 751 | ||
| 752 | // While the official api docs guarantee FILE_BOTH_DIR_INFORMATION to be aligned properly | |
| 753 | // this may not always be the case (e.g. due to faulty VM/Sandboxing tools) | |
| 754 | const dir_info: *align(2) w.FILE_BOTH_DIR_INFORMATION = @ptrCast(@alignCast(&self.buf[self.index])); | |
| 755 | if (dir_info.NextEntryOffset != 0) { | |
| 756 | self.index += dir_info.NextEntryOffset; | |
| 757 | } else { | |
| 758 | self.index = self.buf.len; | |
| 759 | } | |
| 760 | ||
| 761 | const name_utf16le = @as([*]u16, @ptrCast(&dir_info.FileName))[0 .. dir_info.FileNameLength / 2]; | |
| 762 | ||
| 763 | if (mem.eql(u16, name_utf16le, &[_]u16{'.'}) or mem.eql(u16, name_utf16le, &[_]u16{ '.', '.' })) | |
| 764 | continue; | |
| 765 | // Trust that Windows gives us valid UTF-16LE | |
| 766 | const name_utf8_len = std.unicode.utf16leToUtf8(self.name_data[0..], name_utf16le) catch unreachable; | |
| 767 | const name_utf8 = self.name_data[0..name_utf8_len]; | |
| 768 | const kind: Entry.Kind = blk: { | |
| 769 | const attrs = dir_info.FileAttributes; | |
| 770 | if (attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk .directory; | |
| 771 | if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk .sym_link; | |
| 772 | break :blk .file; | |
| 773 | }; | |
| 774 | return Entry{ | |
| 775 | .name = name_utf8, | |
| 776 | .kind = kind, | |
| 777 | }; | |
| 778 | } | |
| 779 | } | |
| 780 | ||
| 781 | pub fn reset(self: *Self) void { | |
| 782 | self.index = 0; | |
| 783 | self.end_index = 0; | |
| 784 | self.first_iter = true; | |
| 785 | } | |
| 786 | }, | |
| 787 | .wasi => struct { | |
| 788 | dir: Dir, | |
| 789 | buf: [1024]u8, // TODO align(@alignOf(os.wasi.dirent_t)), | |
| 790 | cookie: u64, | |
| 791 | index: usize, | |
| 792 | end_index: usize, | |
| 793 | ||
| 794 | const Self = @This(); | |
| 795 | ||
| 796 | pub const Error = IteratorError; | |
| 797 | ||
| 798 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 799 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 800 | pub fn next(self: *Self) Error!?Entry { | |
| 801 | return self.nextWasi() catch |err| switch (err) { | |
| 802 | // To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration. | |
| 803 | // This matches the behavior of non-Linux UNIX platforms. | |
| 804 | error.DirNotFound => null, | |
| 805 | else => |e| return e, | |
| 806 | }; | |
| 807 | } | |
| 808 | ||
| 809 | pub const ErrorWasi = error{DirNotFound} || IteratorError; | |
| 810 | ||
| 811 | /// Implementation of `next` that can return platform-dependent errors depending on the host platform. | |
| 812 | /// When the host platform is Linux, `error.DirNotFound` can be returned if the directory being | |
| 813 | /// iterated was deleted during iteration. | |
| 814 | pub fn nextWasi(self: *Self) ErrorWasi!?Entry { | |
| 815 | // We intentinally use fd_readdir even when linked with libc, | |
| 816 | // since its implementation is exactly the same as below, | |
| 817 | // and we avoid the code complexity here. | |
| 818 | const w = os.wasi; | |
| 819 | start_over: while (true) { | |
| 820 | // According to the WASI spec, the last entry might be truncated, | |
| 821 | // so we need to check if the left buffer contains the whole dirent. | |
| 822 | if (self.end_index - self.index < @sizeOf(w.dirent_t)) { | |
| 823 | var bufused: usize = undefined; | |
| 824 | switch (w.fd_readdir(self.dir.fd, &self.buf, self.buf.len, self.cookie, &bufused)) { | |
| 825 | .SUCCESS => {}, | |
| 826 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 827 | .FAULT => unreachable, | |
| 828 | .NOTDIR => unreachable, | |
| 829 | .INVAL => unreachable, | |
| 830 | .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. | |
| 831 | .NOTCAPABLE => return error.AccessDenied, | |
| 832 | else => |err| return os.unexpectedErrno(err), | |
| 833 | } | |
| 834 | if (bufused == 0) return null; | |
| 835 | self.index = 0; | |
| 836 | self.end_index = bufused; | |
| 837 | } | |
| 838 | const entry = @as(*align(1) w.dirent_t, @ptrCast(&self.buf[self.index])); | |
| 839 | const entry_size = @sizeOf(w.dirent_t); | |
| 840 | const name_index = self.index + entry_size; | |
| 841 | if (name_index + entry.d_namlen > self.end_index) { | |
| 842 | // This case, the name is truncated, so we need to call readdir to store the entire name. | |
| 843 | self.end_index = self.index; // Force fd_readdir in the next loop. | |
| 844 | continue :start_over; | |
| 845 | } | |
| 846 | const name = self.buf[name_index .. name_index + entry.d_namlen]; | |
| 847 | ||
| 848 | const next_index = name_index + entry.d_namlen; | |
| 849 | self.index = next_index; | |
| 850 | self.cookie = entry.d_next; | |
| 851 | ||
| 852 | // skip . and .. entries | |
| 853 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { | |
| 854 | continue :start_over; | |
| 855 | } | |
| 856 | ||
| 857 | const entry_kind: Entry.Kind = switch (entry.d_type) { | |
| 858 | .BLOCK_DEVICE => .block_device, | |
| 859 | .CHARACTER_DEVICE => .character_device, | |
| 860 | .DIRECTORY => .directory, | |
| 861 | .SYMBOLIC_LINK => .sym_link, | |
| 862 | .REGULAR_FILE => .file, | |
| 863 | .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, | |
| 864 | else => .unknown, | |
| 865 | }; | |
| 866 | return Entry{ | |
| 867 | .name = name, | |
| 868 | .kind = entry_kind, | |
| 869 | }; | |
| 870 | } | |
| 871 | } | |
| 872 | ||
| 873 | pub fn reset(self: *Self) void { | |
| 874 | self.index = 0; | |
| 875 | self.end_index = 0; | |
| 876 | self.cookie = os.wasi.DIRCOOKIE_START; | |
| 877 | } | |
| 878 | }, | |
| 879 | else => @compileError("unimplemented"), | |
| 880 | }; | |
| 881 | ||
| 882 | pub fn iterate(self: IterableDir) Iterator { | |
| 883 | return self.iterateImpl(true); | |
| 884 | } | |
| 885 | ||
| 886 | /// Like `iterate`, but will not reset the directory cursor before the first | |
| 887 | /// iteration. This should only be used in cases where it is known that the | |
| 888 | /// `IterableDir` has not had its cursor modified yet (e.g. it was just opened). | |
| 889 | pub fn iterateAssumeFirstIteration(self: IterableDir) Iterator { | |
| 890 | return self.iterateImpl(false); | |
| 891 | } | |
| 892 | ||
| 893 | fn iterateImpl(self: IterableDir, first_iter_start_value: bool) Iterator { | |
| 894 | switch (builtin.os.tag) { | |
| 895 | .macos, | |
| 896 | .ios, | |
| 897 | .freebsd, | |
| 898 | .netbsd, | |
| 899 | .dragonfly, | |
| 900 | .openbsd, | |
| 901 | .solaris, | |
| 902 | .illumos, | |
| 903 | => return Iterator{ | |
| 904 | .dir = self.dir, | |
| 905 | .seek = 0, | |
| 906 | .index = 0, | |
| 907 | .end_index = 0, | |
| 908 | .buf = undefined, | |
| 909 | .first_iter = first_iter_start_value, | |
| 910 | }, | |
| 911 | .linux, .haiku => return Iterator{ | |
| 912 | .dir = self.dir, | |
| 913 | .index = 0, | |
| 914 | .end_index = 0, | |
| 915 | .buf = undefined, | |
| 916 | .first_iter = first_iter_start_value, | |
| 917 | }, | |
| 918 | .windows => return Iterator{ | |
| 919 | .dir = self.dir, | |
| 920 | .index = 0, | |
| 921 | .end_index = 0, | |
| 922 | .first_iter = first_iter_start_value, | |
| 923 | .buf = undefined, | |
| 924 | .name_data = undefined, | |
| 925 | }, | |
| 926 | .wasi => return Iterator{ | |
| 927 | .dir = self.dir, | |
| 928 | .cookie = os.wasi.DIRCOOKIE_START, | |
| 929 | .index = 0, | |
| 930 | .end_index = 0, | |
| 931 | .buf = undefined, | |
| 932 | }, | |
| 933 | else => @compileError("unimplemented"), | |
| 934 | } | |
| 935 | } | |
| 936 | ||
| 937 | pub const Walker = struct { | |
| 938 | stack: std.ArrayList(StackItem), | |
| 939 | name_buffer: std.ArrayList(u8), | |
| 940 | ||
| 941 | pub const WalkerEntry = struct { | |
| 942 | /// The containing directory. This can be used to operate directly on `basename` | |
| 943 | /// rather than `path`, avoiding `error.NameTooLong` for deeply nested paths. | |
| 944 | /// The directory remains open until `next` or `deinit` is called. | |
| 945 | dir: Dir, | |
| 946 | basename: []const u8, | |
| 947 | path: []const u8, | |
| 948 | kind: IterableDir.Entry.Kind, | |
| 949 | }; | |
| 950 | ||
| 951 | const StackItem = struct { | |
| 952 | iter: IterableDir.Iterator, | |
| 953 | dirname_len: usize, | |
| 954 | }; | |
| 955 | ||
| 956 | /// After each call to this function, and on deinit(), the memory returned | |
| 957 | /// from this function becomes invalid. A copy must be made in order to keep | |
| 958 | /// a reference to the path. | |
| 959 | pub fn next(self: *Walker) !?WalkerEntry { | |
| 960 | while (self.stack.items.len != 0) { | |
| 961 | // `top` and `containing` become invalid after appending to `self.stack` | |
| 962 | var top = &self.stack.items[self.stack.items.len - 1]; | |
| 963 | var containing = top; | |
| 964 | var dirname_len = top.dirname_len; | |
| 965 | if (top.iter.next() catch |err| { | |
| 966 | // If we get an error, then we want the user to be able to continue | |
| 967 | // walking if they want, which means that we need to pop the directory | |
| 968 | // that errored from the stack. Otherwise, all future `next` calls would | |
| 969 | // likely just fail with the same error. | |
| 970 | var item = self.stack.pop(); | |
| 971 | if (self.stack.items.len != 0) { | |
| 972 | item.iter.dir.close(); | |
| 973 | } | |
| 974 | return err; | |
| 975 | }) |base| { | |
| 976 | self.name_buffer.shrinkRetainingCapacity(dirname_len); | |
| 977 | if (self.name_buffer.items.len != 0) { | |
| 978 | try self.name_buffer.append(path.sep); | |
| 979 | dirname_len += 1; | |
| 980 | } | |
| 981 | try self.name_buffer.appendSlice(base.name); | |
| 982 | if (base.kind == .directory) { | |
| 983 | var new_dir = top.iter.dir.openIterableDir(base.name, .{}) catch |err| switch (err) { | |
| 984 | error.NameTooLong => unreachable, // no path sep in base.name | |
| 985 | else => |e| return e, | |
| 986 | }; | |
| 987 | { | |
| 988 | errdefer new_dir.close(); | |
| 989 | try self.stack.append(StackItem{ | |
| 990 | .iter = new_dir.iterateAssumeFirstIteration(), | |
| 991 | .dirname_len = self.name_buffer.items.len, | |
| 992 | }); | |
| 993 | top = &self.stack.items[self.stack.items.len - 1]; | |
| 994 | containing = &self.stack.items[self.stack.items.len - 2]; | |
| 995 | } | |
| 996 | } | |
| 997 | return WalkerEntry{ | |
| 998 | .dir = containing.iter.dir, | |
| 999 | .basename = self.name_buffer.items[dirname_len..], | |
| 1000 | .path = self.name_buffer.items, | |
| 1001 | .kind = base.kind, | |
| 1002 | }; | |
| 1003 | } else { | |
| 1004 | var item = self.stack.pop(); | |
| 1005 | if (self.stack.items.len != 0) { | |
| 1006 | item.iter.dir.close(); | |
| 1007 | } | |
| 1008 | } | |
| 1009 | } | |
| 1010 | return null; | |
| 1011 | } | |
| 1012 | ||
| 1013 | pub fn deinit(self: *Walker) void { | |
| 1014 | // Close any remaining directories except the initial one (which is always at index 0) | |
| 1015 | if (self.stack.items.len > 1) { | |
| 1016 | for (self.stack.items[1..]) |*item| { | |
| 1017 | item.iter.dir.close(); | |
| 1018 | } | |
| 1019 | } | |
| 1020 | self.stack.deinit(); | |
| 1021 | self.name_buffer.deinit(); | |
| 1022 | } | |
| 1023 | }; | |
| 1024 | ||
| 1025 | /// Recursively iterates over a directory. | |
| 1026 | /// Must call `Walker.deinit` when done. | |
| 1027 | /// The order of returned file system entries is undefined. | |
| 1028 | /// `self` will not be closed after walking it. | |
| 1029 | pub fn walk(self: IterableDir, allocator: Allocator) !Walker { | |
| 1030 | var name_buffer = std.ArrayList(u8).init(allocator); | |
| 1031 | errdefer name_buffer.deinit(); | |
| 1032 | ||
| 1033 | var stack = std.ArrayList(Walker.StackItem).init(allocator); | |
| 1034 | errdefer stack.deinit(); | |
| 1035 | ||
| 1036 | try stack.append(Walker.StackItem{ | |
| 1037 | .iter = self.iterate(), | |
| 1038 | .dirname_len = 0, | |
| 1039 | }); | |
| 1040 | ||
| 1041 | return Walker{ | |
| 1042 | .stack = stack, | |
| 1043 | .name_buffer = name_buffer, | |
| 1044 | }; | |
| 1045 | } | |
| 1046 | ||
| 1047 | pub fn close(self: *IterableDir) void { | |
| 1048 | self.dir.close(); | |
| 1049 | self.* = undefined; | |
| 1050 | } | |
| 1051 | ||
| 1052 | pub const ChmodError = File.ChmodError; | |
| 1053 | ||
| 1054 | /// Changes the mode of the directory. | |
| 1055 | /// The process must have the correct privileges in order to do this | |
| 1056 | /// successfully, or must have the effective user ID matching the owner | |
| 1057 | /// of the directory. | |
| 1058 | pub fn chmod(self: IterableDir, new_mode: File.Mode) ChmodError!void { | |
| 1059 | const file: File = .{ | |
| 1060 | .handle = self.dir.fd, | |
| 1061 | .capable_io_mode = .blocking, | |
| 1062 | }; | |
| 1063 | try file.chmod(new_mode); | |
| 1064 | } | |
| 1065 | ||
| 1066 | /// Changes the owner and group of the directory. | |
| 1067 | /// The process must have the correct privileges in order to do this | |
| 1068 | /// successfully. The group may be changed by the owner of the directory to | |
| 1069 | /// any group of which the owner is a member. If the | |
| 1070 | /// owner or group is specified as `null`, the ID is not changed. | |
| 1071 | pub fn chown(self: IterableDir, owner: ?File.Uid, group: ?File.Gid) ChownError!void { | |
| 1072 | const file: File = .{ | |
| 1073 | .handle = self.dir.fd, | |
| 1074 | .capable_io_mode = .blocking, | |
| 1075 | }; | |
| 1076 | try file.chown(owner, group); | |
| 1077 | } | |
| 1078 | ||
| 1079 | pub const ChownError = File.ChownError; | |
| 1080 | }; | |
| 1081 | ||
| 1082 | pub const Dir = struct { | |
| 1083 | fd: os.fd_t, | |
| 1084 | ||
| 1085 | pub const iterate = @compileError("only 'IterableDir' can be iterated; 'IterableDir' can be obtained with 'openIterableDir'"); | |
| 1086 | pub const walk = @compileError("only 'IterableDir' can be walked; 'IterableDir' can be obtained with 'openIterableDir'"); | |
| 1087 | pub const chmod = @compileError("only 'IterableDir' can have its mode changed; 'IterableDir' can be obtained with 'openIterableDir'"); | |
| 1088 | pub const chown = @compileError("only 'IterableDir' can have its owner changed; 'IterableDir' can be obtained with 'openIterableDir'"); | |
| 1089 | ||
| 1090 | pub const OpenError = error{ | |
| 1091 | FileNotFound, | |
| 1092 | NotDir, | |
| 1093 | InvalidHandle, | |
| 1094 | AccessDenied, | |
| 1095 | SymLinkLoop, | |
| 1096 | ProcessFdQuotaExceeded, | |
| 1097 | NameTooLong, | |
| 1098 | SystemFdQuotaExceeded, | |
| 1099 | NoDevice, | |
| 1100 | SystemResources, | |
| 1101 | InvalidUtf8, | |
| 1102 | BadPathName, | |
| 1103 | DeviceBusy, | |
| 1104 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 1105 | NetworkNotFound, | |
| 1106 | } || os.UnexpectedError; | |
| 1107 | ||
| 1108 | pub fn close(self: *Dir) void { | |
| 1109 | if (need_async_thread) { | |
| 1110 | std.event.Loop.instance.?.close(self.fd); | |
| 1111 | } else { | |
| 1112 | os.close(self.fd); | |
| 1113 | } | |
| 1114 | self.* = undefined; | |
| 1115 | } | |
| 1116 | ||
| 1117 | /// Opens a file for reading or writing, without attempting to create a new file. | |
| 1118 | /// To create a new file, see `createFile`. | |
| 1119 | /// Call `File.close` to release the resource. | |
| 1120 | /// Asserts that the path parameter has no null bytes. | |
| 1121 | pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { | |
| 1122 | if (builtin.os.tag == .windows) { | |
| 1123 | const path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1124 | return self.openFileW(path_w.span(), flags); | |
| 1125 | } | |
| 1126 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1127 | return self.openFileWasi(sub_path, flags); | |
| 1128 | } | |
| 1129 | const path_c = try os.toPosixPath(sub_path); | |
| 1130 | return self.openFileZ(&path_c, flags); | |
| 1131 | } | |
| 1132 | ||
| 1133 | /// Same as `openFile` but WASI only. | |
| 1134 | pub fn openFileWasi(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { | |
| 1135 | const w = os.wasi; | |
| 1136 | var fdflags: w.fdflags_t = 0x0; | |
| 1137 | var base: w.rights_t = 0x0; | |
| 1138 | if (flags.isRead()) { | |
| 1139 | base |= w.RIGHT.FD_READ | w.RIGHT.FD_TELL | w.RIGHT.FD_SEEK | w.RIGHT.FD_FILESTAT_GET; | |
| 1140 | } | |
| 1141 | if (flags.isWrite()) { | |
| 1142 | fdflags |= w.FDFLAG.APPEND; | |
| 1143 | base |= w.RIGHT.FD_WRITE | | |
| 1144 | w.RIGHT.FD_TELL | | |
| 1145 | w.RIGHT.FD_SEEK | | |
| 1146 | w.RIGHT.FD_DATASYNC | | |
| 1147 | w.RIGHT.FD_FDSTAT_SET_FLAGS | | |
| 1148 | w.RIGHT.FD_SYNC | | |
| 1149 | w.RIGHT.FD_ALLOCATE | | |
| 1150 | w.RIGHT.FD_ADVISE | | |
| 1151 | w.RIGHT.FD_FILESTAT_SET_TIMES | | |
| 1152 | w.RIGHT.FD_FILESTAT_SET_SIZE; | |
| 1153 | } | |
| 1154 | const fd = try os.openatWasi(self.fd, sub_path, 0x0, 0x0, fdflags, base, 0x0); | |
| 1155 | return File{ .handle = fd }; | |
| 1156 | } | |
| 1157 | ||
| 1158 | /// Same as `openFile` but the path parameter is null-terminated. | |
| 1159 | pub fn openFileZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File { | |
| 1160 | if (builtin.os.tag == .windows) { | |
| 1161 | const path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path); | |
| 1162 | return self.openFileW(path_w.span(), flags); | |
| 1163 | } | |
| 1164 | ||
| 1165 | var os_flags: u32 = 0; | |
| 1166 | if (@hasDecl(os.O, "CLOEXEC")) os_flags = os.O.CLOEXEC; | |
| 1167 | ||
| 1168 | // Use the O locking flags if the os supports them to acquire the lock | |
| 1169 | // atomically. | |
| 1170 | const has_flock_open_flags = @hasDecl(os.O, "EXLOCK"); | |
| 1171 | if (has_flock_open_flags) { | |
| 1172 | // Note that the O.NONBLOCK flag is removed after the openat() call | |
| 1173 | // is successful. | |
| 1174 | const nonblocking_lock_flag: u32 = if (flags.lock_nonblocking) | |
| 1175 | os.O.NONBLOCK | |
| 1176 | else | |
| 1177 | 0; | |
| 1178 | os_flags |= switch (flags.lock) { | |
| 1179 | .none => @as(u32, 0), | |
| 1180 | .shared => os.O.SHLOCK | nonblocking_lock_flag, | |
| 1181 | .exclusive => os.O.EXLOCK | nonblocking_lock_flag, | |
| 1182 | }; | |
| 1183 | } | |
| 1184 | if (@hasDecl(os.O, "LARGEFILE")) { | |
| 1185 | os_flags |= os.O.LARGEFILE; | |
| 1186 | } | |
| 1187 | if (@hasDecl(os.O, "NOCTTY") and !flags.allow_ctty) { | |
| 1188 | os_flags |= os.O.NOCTTY; | |
| 1189 | } | |
| 1190 | os_flags |= switch (flags.mode) { | |
| 1191 | .read_only => @as(u32, os.O.RDONLY), | |
| 1192 | .write_only => @as(u32, os.O.WRONLY), | |
| 1193 | .read_write => @as(u32, os.O.RDWR), | |
| 1194 | }; | |
| 1195 | const fd = if (flags.intended_io_mode != .blocking) | |
| 1196 | try std.event.Loop.instance.?.openatZ(self.fd, sub_path, os_flags, 0) | |
| 1197 | else | |
| 1198 | try os.openatZ(self.fd, sub_path, os_flags, 0); | |
| 1199 | errdefer os.close(fd); | |
| 1200 | ||
| 1201 | // WASI doesn't have os.flock so we intetinally check OS prior to the inner if block | |
| 1202 | // since it is not compiltime-known and we need to avoid undefined symbol in Wasm. | |
| 1203 | if (@hasDecl(os.system, "LOCK") and builtin.target.os.tag != .wasi) { | |
| 1204 | if (!has_flock_open_flags and flags.lock != .none) { | |
| 1205 | // TODO: integrate async I/O | |
| 1206 | const lock_nonblocking = if (flags.lock_nonblocking) os.LOCK.NB else @as(i32, 0); | |
| 1207 | try os.flock(fd, switch (flags.lock) { | |
| 1208 | .none => unreachable, | |
| 1209 | .shared => os.LOCK.SH | lock_nonblocking, | |
| 1210 | .exclusive => os.LOCK.EX | lock_nonblocking, | |
| 1211 | }); | |
| 1212 | } | |
| 1213 | } | |
| 1214 | ||
| 1215 | if (has_flock_open_flags and flags.lock_nonblocking) { | |
| 1216 | var fl_flags = os.fcntl(fd, os.F.GETFL, 0) catch |err| switch (err) { | |
| 1217 | error.FileBusy => unreachable, | |
| 1218 | error.Locked => unreachable, | |
| 1219 | error.PermissionDenied => unreachable, | |
| 1220 | error.DeadLock => unreachable, | |
| 1221 | error.LockedRegionLimitExceeded => unreachable, | |
| 1222 | else => |e| return e, | |
| 1223 | }; | |
| 1224 | fl_flags &= ~@as(usize, os.O.NONBLOCK); | |
| 1225 | _ = os.fcntl(fd, os.F.SETFL, fl_flags) catch |err| switch (err) { | |
| 1226 | error.FileBusy => unreachable, | |
| 1227 | error.Locked => unreachable, | |
| 1228 | error.PermissionDenied => unreachable, | |
| 1229 | error.DeadLock => unreachable, | |
| 1230 | error.LockedRegionLimitExceeded => unreachable, | |
| 1231 | else => |e| return e, | |
| 1232 | }; | |
| 1233 | } | |
| 1234 | ||
| 1235 | return File{ | |
| 1236 | .handle = fd, | |
| 1237 | .capable_io_mode = .blocking, | |
| 1238 | .intended_io_mode = flags.intended_io_mode, | |
| 1239 | }; | |
| 1240 | } | |
| 1241 | ||
| 1242 | /// Same as `openFile` but Windows-only and the path parameter is | |
| 1243 | /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. | |
| 1244 | pub fn openFileW(self: Dir, sub_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { | |
| 1245 | const w = os.windows; | |
| 1246 | const file: File = .{ | |
| 1247 | .handle = try w.OpenFile(sub_path_w, .{ | |
| 1248 | .dir = self.fd, | |
| 1249 | .access_mask = w.SYNCHRONIZE | | |
| 1250 | (if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) | | |
| 1251 | (if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0), | |
| 1252 | .creation = w.FILE_OPEN, | |
| 1253 | .io_mode = flags.intended_io_mode, | |
| 1254 | }), | |
| 1255 | .capable_io_mode = std.io.default_mode, | |
| 1256 | .intended_io_mode = flags.intended_io_mode, | |
| 1257 | }; | |
| 1258 | errdefer file.close(); | |
| 1259 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 1260 | const range_off: w.LARGE_INTEGER = 0; | |
| 1261 | const range_len: w.LARGE_INTEGER = 1; | |
| 1262 | const exclusive = switch (flags.lock) { | |
| 1263 | .none => return file, | |
| 1264 | .shared => false, | |
| 1265 | .exclusive => true, | |
| 1266 | }; | |
| 1267 | try w.LockFile( | |
| 1268 | file.handle, | |
| 1269 | null, | |
| 1270 | null, | |
| 1271 | null, | |
| 1272 | &io, | |
| 1273 | &range_off, | |
| 1274 | &range_len, | |
| 1275 | null, | |
| 1276 | @intFromBool(flags.lock_nonblocking), | |
| 1277 | @intFromBool(exclusive), | |
| 1278 | ); | |
| 1279 | return file; | |
| 1280 | } | |
| 1281 | ||
| 1282 | /// Creates, opens, or overwrites a file with write access. | |
| 1283 | /// Call `File.close` on the result when done. | |
| 1284 | /// Asserts that the path parameter has no null bytes. | |
| 1285 | pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { | |
| 1286 | if (builtin.os.tag == .windows) { | |
| 1287 | const path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1288 | return self.createFileW(path_w.span(), flags); | |
| 1289 | } | |
| 1290 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1291 | return self.createFileWasi(sub_path, flags); | |
| 1292 | } | |
| 1293 | const path_c = try os.toPosixPath(sub_path); | |
| 1294 | return self.createFileZ(&path_c, flags); | |
| 1295 | } | |
| 1296 | ||
| 1297 | /// Same as `createFile` but WASI only. | |
| 1298 | pub fn createFileWasi(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { | |
| 1299 | const w = os.wasi; | |
| 1300 | var oflags = w.O.CREAT; | |
| 1301 | var base: w.rights_t = w.RIGHT.FD_WRITE | | |
| 1302 | w.RIGHT.FD_DATASYNC | | |
| 1303 | w.RIGHT.FD_SEEK | | |
| 1304 | w.RIGHT.FD_TELL | | |
| 1305 | w.RIGHT.FD_FDSTAT_SET_FLAGS | | |
| 1306 | w.RIGHT.FD_SYNC | | |
| 1307 | w.RIGHT.FD_ALLOCATE | | |
| 1308 | w.RIGHT.FD_ADVISE | | |
| 1309 | w.RIGHT.FD_FILESTAT_SET_TIMES | | |
| 1310 | w.RIGHT.FD_FILESTAT_SET_SIZE | | |
| 1311 | w.RIGHT.FD_FILESTAT_GET; | |
| 1312 | if (flags.read) { | |
| 1313 | base |= w.RIGHT.FD_READ; | |
| 1314 | } | |
| 1315 | if (flags.truncate) { | |
| 1316 | oflags |= w.O.TRUNC; | |
| 1317 | } | |
| 1318 | if (flags.exclusive) { | |
| 1319 | oflags |= w.O.EXCL; | |
| 1320 | } | |
| 1321 | const fd = try os.openatWasi(self.fd, sub_path, 0x0, oflags, 0x0, base, 0x0); | |
| 1322 | return File{ .handle = fd }; | |
| 1323 | } | |
| 1324 | ||
| 1325 | /// Same as `createFile` but the path parameter is null-terminated. | |
| 1326 | pub fn createFileZ(self: Dir, sub_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File { | |
| 1327 | if (builtin.os.tag == .windows) { | |
| 1328 | const path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); | |
| 1329 | return self.createFileW(path_w.span(), flags); | |
| 1330 | } | |
| 1331 | ||
| 1332 | // Use the O locking flags if the os supports them to acquire the lock | |
| 1333 | // atomically. | |
| 1334 | const has_flock_open_flags = @hasDecl(os.O, "EXLOCK"); | |
| 1335 | // Note that the O.NONBLOCK flag is removed after the openat() call | |
| 1336 | // is successful. | |
| 1337 | const nonblocking_lock_flag: u32 = if (has_flock_open_flags and flags.lock_nonblocking) | |
| 1338 | os.O.NONBLOCK | |
| 1339 | else | |
| 1340 | 0; | |
| 1341 | const lock_flag: u32 = if (has_flock_open_flags) switch (flags.lock) { | |
| 1342 | .none => @as(u32, 0), | |
| 1343 | .shared => os.O.SHLOCK | nonblocking_lock_flag, | |
| 1344 | .exclusive => os.O.EXLOCK | nonblocking_lock_flag, | |
| 1345 | } else 0; | |
| 1346 | ||
| 1347 | const O_LARGEFILE = if (@hasDecl(os.O, "LARGEFILE")) os.O.LARGEFILE else 0; | |
| 1348 | const os_flags = lock_flag | O_LARGEFILE | os.O.CREAT | os.O.CLOEXEC | | |
| 1349 | (if (flags.truncate) @as(u32, os.O.TRUNC) else 0) | | |
| 1350 | (if (flags.read) @as(u32, os.O.RDWR) else os.O.WRONLY) | | |
| 1351 | (if (flags.exclusive) @as(u32, os.O.EXCL) else 0); | |
| 1352 | const fd = if (flags.intended_io_mode != .blocking) | |
| 1353 | try std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, os_flags, flags.mode) | |
| 1354 | else | |
| 1355 | try os.openatZ(self.fd, sub_path_c, os_flags, flags.mode); | |
| 1356 | errdefer os.close(fd); | |
| 1357 | ||
| 1358 | // WASI doesn't have os.flock so we intetinally check OS prior to the inner if block | |
| 1359 | // since it is not compiltime-known and we need to avoid undefined symbol in Wasm. | |
| 1360 | if (builtin.target.os.tag != .wasi) { | |
| 1361 | if (!has_flock_open_flags and flags.lock != .none) { | |
| 1362 | // TODO: integrate async I/O | |
| 1363 | const lock_nonblocking = if (flags.lock_nonblocking) os.LOCK.NB else @as(i32, 0); | |
| 1364 | try os.flock(fd, switch (flags.lock) { | |
| 1365 | .none => unreachable, | |
| 1366 | .shared => os.LOCK.SH | lock_nonblocking, | |
| 1367 | .exclusive => os.LOCK.EX | lock_nonblocking, | |
| 1368 | }); | |
| 1369 | } | |
| 1370 | } | |
| 1371 | ||
| 1372 | if (has_flock_open_flags and flags.lock_nonblocking) { | |
| 1373 | var fl_flags = os.fcntl(fd, os.F.GETFL, 0) catch |err| switch (err) { | |
| 1374 | error.FileBusy => unreachable, | |
| 1375 | error.Locked => unreachable, | |
| 1376 | error.PermissionDenied => unreachable, | |
| 1377 | error.DeadLock => unreachable, | |
| 1378 | error.LockedRegionLimitExceeded => unreachable, | |
| 1379 | else => |e| return e, | |
| 1380 | }; | |
| 1381 | fl_flags &= ~@as(usize, os.O.NONBLOCK); | |
| 1382 | _ = os.fcntl(fd, os.F.SETFL, fl_flags) catch |err| switch (err) { | |
| 1383 | error.FileBusy => unreachable, | |
| 1384 | error.Locked => unreachable, | |
| 1385 | error.PermissionDenied => unreachable, | |
| 1386 | error.DeadLock => unreachable, | |
| 1387 | error.LockedRegionLimitExceeded => unreachable, | |
| 1388 | else => |e| return e, | |
| 1389 | }; | |
| 1390 | } | |
| 1391 | ||
| 1392 | return File{ | |
| 1393 | .handle = fd, | |
| 1394 | .capable_io_mode = .blocking, | |
| 1395 | .intended_io_mode = flags.intended_io_mode, | |
| 1396 | }; | |
| 1397 | } | |
| 1398 | ||
| 1399 | /// Same as `createFile` but Windows-only and the path parameter is | |
| 1400 | /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. | |
| 1401 | pub fn createFileW(self: Dir, sub_path_w: []const u16, flags: File.CreateFlags) File.OpenError!File { | |
| 1402 | const w = os.windows; | |
| 1403 | const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0; | |
| 1404 | const file: File = .{ | |
| 1405 | .handle = try os.windows.OpenFile(sub_path_w, .{ | |
| 1406 | .dir = self.fd, | |
| 1407 | .access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag, | |
| 1408 | .creation = if (flags.exclusive) | |
| 1409 | @as(u32, w.FILE_CREATE) | |
| 1410 | else if (flags.truncate) | |
| 1411 | @as(u32, w.FILE_OVERWRITE_IF) | |
| 1412 | else | |
| 1413 | @as(u32, w.FILE_OPEN_IF), | |
| 1414 | .io_mode = flags.intended_io_mode, | |
| 1415 | }), | |
| 1416 | .capable_io_mode = std.io.default_mode, | |
| 1417 | .intended_io_mode = flags.intended_io_mode, | |
| 1418 | }; | |
| 1419 | errdefer file.close(); | |
| 1420 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 1421 | const range_off: w.LARGE_INTEGER = 0; | |
| 1422 | const range_len: w.LARGE_INTEGER = 1; | |
| 1423 | const exclusive = switch (flags.lock) { | |
| 1424 | .none => return file, | |
| 1425 | .shared => false, | |
| 1426 | .exclusive => true, | |
| 1427 | }; | |
| 1428 | try w.LockFile( | |
| 1429 | file.handle, | |
| 1430 | null, | |
| 1431 | null, | |
| 1432 | null, | |
| 1433 | &io, | |
| 1434 | &range_off, | |
| 1435 | &range_len, | |
| 1436 | null, | |
| 1437 | @intFromBool(flags.lock_nonblocking), | |
| 1438 | @intFromBool(exclusive), | |
| 1439 | ); | |
| 1440 | return file; | |
| 1441 | } | |
| 1442 | ||
| 1443 | /// Creates a single directory with a relative or absolute path. | |
| 1444 | /// To create multiple directories to make an entire path, see `makePath`. | |
| 1445 | /// To operate on only absolute paths, see `makeDirAbsolute`. | |
| 1446 | pub fn makeDir(self: Dir, sub_path: []const u8) !void { | |
| 1447 | try os.mkdirat(self.fd, sub_path, default_new_dir_mode); | |
| 1448 | } | |
| 1449 | ||
| 1450 | /// Creates a single directory with a relative or absolute null-terminated UTF-8-encoded path. | |
| 1451 | /// To create multiple directories to make an entire path, see `makePath`. | |
| 1452 | /// To operate on only absolute paths, see `makeDirAbsoluteZ`. | |
| 1453 | pub fn makeDirZ(self: Dir, sub_path: [*:0]const u8) !void { | |
| 1454 | try os.mkdiratZ(self.fd, sub_path, default_new_dir_mode); | |
| 1455 | } | |
| 1456 | ||
| 1457 | /// Creates a single directory with a relative or absolute null-terminated WTF-16-encoded path. | |
| 1458 | /// To create multiple directories to make an entire path, see `makePath`. | |
| 1459 | /// To operate on only absolute paths, see `makeDirAbsoluteW`. | |
| 1460 | pub fn makeDirW(self: Dir, sub_path: [*:0]const u16) !void { | |
| 1461 | try os.mkdiratW(self.fd, sub_path, default_new_dir_mode); | |
| 1462 | } | |
| 1463 | ||
| 1464 | /// Calls makeDir iteratively to make an entire path | |
| 1465 | /// (i.e. creating any parent directories that do not exist). | |
| 1466 | /// Returns success if the path already exists and is a directory. | |
| 1467 | /// This function is not atomic, and if it returns an error, the file system may | |
| 1468 | /// have been modified regardless. | |
| 1469 | pub fn makePath(self: Dir, sub_path: []const u8) !void { | |
| 1470 | var it = try path.componentIterator(sub_path); | |
| 1471 | var component = it.last() orelse return; | |
| 1472 | while (true) { | |
| 1473 | self.makeDir(component.path) catch |err| switch (err) { | |
| 1474 | error.PathAlreadyExists => { | |
| 1475 | // TODO stat the file and return an error if it's not a directory | |
| 1476 | // this is important because otherwise a dangling symlink | |
| 1477 | // could cause an infinite loop | |
| 1478 | }, | |
| 1479 | error.FileNotFound => |e| { | |
| 1480 | component = it.previous() orelse return e; | |
| 1481 | continue; | |
| 1482 | }, | |
| 1483 | else => |e| return e, | |
| 1484 | }; | |
| 1485 | component = it.next() orelse return; | |
| 1486 | } | |
| 1487 | } | |
| 1488 | ||
| 1489 | /// Calls makeOpenDirAccessMaskW iteratively to make an entire path | |
| 1490 | /// (i.e. creating any parent directories that do not exist). | |
| 1491 | /// Opens the dir if the path already exists and is a directory. | |
| 1492 | /// This function is not atomic, and if it returns an error, the file system may | |
| 1493 | /// have been modified regardless. | |
| 1494 | fn makeOpenPathAccessMaskW(self: Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) OpenError!Dir { | |
| 1495 | const w = os.windows; | |
| 1496 | var it = try path.componentIterator(sub_path); | |
| 1497 | // If there are no components in the path, then create a dummy component with the full path. | |
| 1498 | var component = it.last() orelse path.NativeUtf8ComponentIterator.Component{ | |
| 1499 | .name = "", | |
| 1500 | .path = sub_path, | |
| 1501 | }; | |
| 1502 | ||
| 1503 | while (true) { | |
| 1504 | const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path); | |
| 1505 | const is_last = it.peekNext() == null; | |
| 1506 | var result = self.makeOpenDirAccessMaskW(sub_path_w.span().ptr, access_mask, .{ | |
| 1507 | .no_follow = no_follow, | |
| 1508 | .create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE, | |
| 1509 | }) catch |err| switch (err) { | |
| 1510 | error.FileNotFound => |e| { | |
| 1511 | component = it.previous() orelse return e; | |
| 1512 | continue; | |
| 1513 | }, | |
| 1514 | else => |e| return e, | |
| 1515 | }; | |
| 1516 | ||
| 1517 | component = it.next() orelse return result; | |
| 1518 | // Don't leak the intermediate file handles | |
| 1519 | result.close(); | |
| 1520 | } | |
| 1521 | } | |
| 1522 | ||
| 1523 | /// This function performs `makePath`, followed by `openDir`. | |
| 1524 | /// If supported by the OS, this operation is atomic. It is not atomic on | |
| 1525 | /// all operating systems. | |
| 1526 | /// On Windows, this function performs `makeOpenPathAccessMaskW`. | |
| 1527 | pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !Dir { | |
| 1528 | return switch (builtin.os.tag) { | |
| 1529 | .windows => { | |
| 1530 | const w = os.windows; | |
| 1531 | const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | | |
| 1532 | w.SYNCHRONIZE | w.FILE_TRAVERSE; | |
| 1533 | ||
| 1534 | return self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow); | |
| 1535 | }, | |
| 1536 | else => { | |
| 1537 | return self.openDir(sub_path, open_dir_options) catch |err| switch (err) { | |
| 1538 | error.FileNotFound => { | |
| 1539 | try self.makePath(sub_path); | |
| 1540 | return self.openDir(sub_path, open_dir_options); | |
| 1541 | }, | |
| 1542 | else => |e| return e, | |
| 1543 | }; | |
| 1544 | }, | |
| 1545 | }; | |
| 1546 | } | |
| 1547 | ||
| 1548 | /// This function performs `makePath`, followed by `openIterableDir`. | |
| 1549 | /// If supported by the OS, this operation is atomic. It is not atomic on | |
| 1550 | /// all operating systems. | |
| 1551 | pub fn makeOpenPathIterable(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !IterableDir { | |
| 1552 | return switch (builtin.os.tag) { | |
| 1553 | .windows => { | |
| 1554 | const w = os.windows; | |
| 1555 | const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | | |
| 1556 | w.SYNCHRONIZE | w.FILE_TRAVERSE | w.FILE_LIST_DIRECTORY; | |
| 1557 | ||
| 1558 | return IterableDir{ | |
| 1559 | .dir = try self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow), | |
| 1560 | }; | |
| 1561 | }, | |
| 1562 | else => { | |
| 1563 | return self.openIterableDir(sub_path, open_dir_options) catch |err| switch (err) { | |
| 1564 | error.FileNotFound => { | |
| 1565 | try self.makePath(sub_path); | |
| 1566 | return self.openIterableDir(sub_path, open_dir_options); | |
| 1567 | }, | |
| 1568 | else => |e| return e, | |
| 1569 | }; | |
| 1570 | }, | |
| 1571 | }; | |
| 1572 | } | |
| 1573 | ||
| 1574 | /// This function returns the canonicalized absolute pathname of | |
| 1575 | /// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this | |
| 1576 | /// `Dir` handle and returns the canonicalized absolute pathname of `pathname` | |
| 1577 | /// argument. | |
| 1578 | /// This function is not universally supported by all platforms. | |
| 1579 | /// Currently supported hosts are: Linux, macOS, and Windows. | |
| 1580 | /// See also `Dir.realpathZ`, `Dir.realpathW`, and `Dir.realpathAlloc`. | |
| 1581 | pub fn realpath(self: Dir, pathname: []const u8, out_buffer: []u8) ![]u8 { | |
| 1582 | if (builtin.os.tag == .wasi) { | |
| 1583 | @compileError("realpath is not available on WASI"); | |
| 1584 | } | |
| 1585 | if (builtin.os.tag == .windows) { | |
| 1586 | const pathname_w = try os.windows.sliceToPrefixedFileW(self.fd, pathname); | |
| 1587 | return self.realpathW(pathname_w.span(), out_buffer); | |
| 1588 | } | |
| 1589 | const pathname_c = try os.toPosixPath(pathname); | |
| 1590 | return self.realpathZ(&pathname_c, out_buffer); | |
| 1591 | } | |
| 1592 | ||
| 1593 | /// Same as `Dir.realpath` except `pathname` is null-terminated. | |
| 1594 | /// See also `Dir.realpath`, `realpathZ`. | |
| 1595 | pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) ![]u8 { | |
| 1596 | if (builtin.os.tag == .windows) { | |
| 1597 | const pathname_w = try os.windows.cStrToPrefixedFileW(self.fd, pathname); | |
| 1598 | return self.realpathW(pathname_w.span(), out_buffer); | |
| 1599 | } | |
| 1600 | ||
| 1601 | const flags = if (builtin.os.tag == .linux) os.O.PATH | os.O.NONBLOCK | os.O.CLOEXEC else os.O.NONBLOCK | os.O.CLOEXEC; | |
| 1602 | const fd = os.openatZ(self.fd, pathname, flags, 0) catch |err| switch (err) { | |
| 1603 | error.FileLocksNotSupported => unreachable, | |
| 1604 | else => |e| return e, | |
| 1605 | }; | |
| 1606 | defer os.close(fd); | |
| 1607 | ||
| 1608 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1609 | // have a variant that takes an arbitrary-size buffer. | |
| 1610 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1611 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1612 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1613 | // anyway. | |
| 1614 | var buffer: [MAX_PATH_BYTES]u8 = undefined; | |
| 1615 | const out_path = try os.getFdPath(fd, &buffer); | |
| 1616 | ||
| 1617 | if (out_path.len > out_buffer.len) { | |
| 1618 | return error.NameTooLong; | |
| 1619 | } | |
| 1620 | ||
| 1621 | const result = out_buffer[0..out_path.len]; | |
| 1622 | @memcpy(result, out_path); | |
| 1623 | return result; | |
| 1624 | } | |
| 1625 | ||
| 1626 | /// Windows-only. Same as `Dir.realpath` except `pathname` is WTF16 encoded. | |
| 1627 | /// See also `Dir.realpath`, `realpathW`. | |
| 1628 | pub fn realpathW(self: Dir, pathname: []const u16, out_buffer: []u8) ![]u8 { | |
| 1629 | const w = os.windows; | |
| 1630 | ||
| 1631 | const access_mask = w.GENERIC_READ | w.SYNCHRONIZE; | |
| 1632 | const share_access = w.FILE_SHARE_READ; | |
| 1633 | const creation = w.FILE_OPEN; | |
| 1634 | const h_file = blk: { | |
| 1635 | const res = w.OpenFile(pathname, .{ | |
| 1636 | .dir = self.fd, | |
| 1637 | .access_mask = access_mask, | |
| 1638 | .share_access = share_access, | |
| 1639 | .creation = creation, | |
| 1640 | .io_mode = .blocking, | |
| 1641 | .filter = .any, | |
| 1642 | }) catch |err| switch (err) { | |
| 1643 | error.WouldBlock => unreachable, | |
| 1644 | else => |e| return e, | |
| 1645 | }; | |
| 1646 | break :blk res; | |
| 1647 | }; | |
| 1648 | defer w.CloseHandle(h_file); | |
| 1649 | ||
| 1650 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1651 | // have a variant that takes an arbitrary-size buffer. | |
| 1652 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1653 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1654 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1655 | // anyway. | |
| 1656 | var buffer: [MAX_PATH_BYTES]u8 = undefined; | |
| 1657 | const out_path = try os.getFdPath(h_file, &buffer); | |
| 1658 | ||
| 1659 | if (out_path.len > out_buffer.len) { | |
| 1660 | return error.NameTooLong; | |
| 1661 | } | |
| 1662 | ||
| 1663 | const result = out_buffer[0..out_path.len]; | |
| 1664 | @memcpy(result, out_path); | |
| 1665 | return result; | |
| 1666 | } | |
| 1667 | ||
| 1668 | /// Same as `Dir.realpath` except caller must free the returned memory. | |
| 1669 | /// See also `Dir.realpath`. | |
| 1670 | pub fn realpathAlloc(self: Dir, allocator: Allocator, pathname: []const u8) ![]u8 { | |
| 1671 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1672 | // have a variant that takes an arbitrary-size buffer. | |
| 1673 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1674 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1675 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1676 | // anyway. | |
| 1677 | var buf: [MAX_PATH_BYTES]u8 = undefined; | |
| 1678 | return allocator.dupe(u8, try self.realpath(pathname, buf[0..])); | |
| 1679 | } | |
| 1680 | ||
| 1681 | /// Changes the current working directory to the open directory handle. | |
| 1682 | /// This modifies global state and can have surprising effects in multi- | |
| 1683 | /// threaded applications. Most applications and especially libraries should | |
| 1684 | /// not call this function as a general rule, however it can have use cases | |
| 1685 | /// in, for example, implementing a shell, or child process execution. | |
| 1686 | /// Not all targets support this. For example, WASI does not have the concept | |
| 1687 | /// of a current working directory. | |
| 1688 | pub fn setAsCwd(self: Dir) !void { | |
| 1689 | if (builtin.os.tag == .wasi) { | |
| 1690 | @compileError("changing cwd is not currently possible in WASI"); | |
| 1691 | } | |
| 1692 | if (builtin.os.tag == .windows) { | |
| 1693 | var dir_path_buffer: [os.windows.PATH_MAX_WIDE]u16 = undefined; | |
| 1694 | const dir_path = try os.windows.GetFinalPathNameByHandle(self.fd, .{}, &dir_path_buffer); | |
| 1695 | if (builtin.link_libc) { | |
| 1696 | return os.chdirW(dir_path); | |
| 1697 | } | |
| 1698 | return os.windows.SetCurrentDirectory(dir_path); | |
| 1699 | } | |
| 1700 | try os.fchdir(self.fd); | |
| 1701 | } | |
| 1702 | ||
| 1703 | pub const OpenDirOptions = struct { | |
| 1704 | /// `true` means the opened directory can be used as the `Dir` parameter | |
| 1705 | /// for functions which operate based on an open directory handle. When `false`, | |
| 1706 | /// such operations are Illegal Behavior. | |
| 1707 | access_sub_paths: bool = true, | |
| 1708 | ||
| 1709 | /// `true` means it won't dereference the symlinks. | |
| 1710 | no_follow: bool = false, | |
| 1711 | }; | |
| 1712 | ||
| 1713 | /// Opens a directory at the given path. The directory is a system resource that remains | |
| 1714 | /// open until `close` is called on the result. | |
| 1715 | /// | |
| 1716 | /// Asserts that the path parameter has no null bytes. | |
| 1717 | pub fn openDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { | |
| 1718 | if (builtin.os.tag == .windows) { | |
| 1719 | const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1720 | return self.openDirW(sub_path_w.span().ptr, args, false); | |
| 1721 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1722 | return self.openDirWasi(sub_path, args); | |
| 1723 | } else { | |
| 1724 | const sub_path_c = try os.toPosixPath(sub_path); | |
| 1725 | return self.openDirZ(&sub_path_c, args, false); | |
| 1726 | } | |
| 1727 | } | |
| 1728 | ||
| 1729 | /// Opens an iterable directory at the given path. The directory is a system resource that remains | |
| 1730 | /// open until `close` is called on the result. | |
| 1731 | /// | |
| 1732 | /// Asserts that the path parameter has no null bytes. | |
| 1733 | pub fn openIterableDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!IterableDir { | |
| 1734 | if (builtin.os.tag == .windows) { | |
| 1735 | const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1736 | return IterableDir{ .dir = try self.openDirW(sub_path_w.span().ptr, args, true) }; | |
| 1737 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1738 | return IterableDir{ .dir = try self.openDirWasi(sub_path, args) }; | |
| 1739 | } else { | |
| 1740 | const sub_path_c = try os.toPosixPath(sub_path); | |
| 1741 | return IterableDir{ .dir = try self.openDirZ(&sub_path_c, args, true) }; | |
| 1742 | } | |
| 1743 | } | |
| 1744 | ||
| 1745 | /// Same as `openDir` except only WASI. | |
| 1746 | pub fn openDirWasi(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { | |
| 1747 | const w = os.wasi; | |
| 1748 | var base: w.rights_t = w.RIGHT.FD_FILESTAT_GET | w.RIGHT.FD_FDSTAT_SET_FLAGS | w.RIGHT.FD_FILESTAT_SET_TIMES; | |
| 1749 | if (args.access_sub_paths) { | |
| 1750 | base |= w.RIGHT.FD_READDIR | | |
| 1751 | w.RIGHT.PATH_CREATE_DIRECTORY | | |
| 1752 | w.RIGHT.PATH_CREATE_FILE | | |
| 1753 | w.RIGHT.PATH_LINK_SOURCE | | |
| 1754 | w.RIGHT.PATH_LINK_TARGET | | |
| 1755 | w.RIGHT.PATH_OPEN | | |
| 1756 | w.RIGHT.PATH_READLINK | | |
| 1757 | w.RIGHT.PATH_RENAME_SOURCE | | |
| 1758 | w.RIGHT.PATH_RENAME_TARGET | | |
| 1759 | w.RIGHT.PATH_FILESTAT_GET | | |
| 1760 | w.RIGHT.PATH_FILESTAT_SET_SIZE | | |
| 1761 | w.RIGHT.PATH_FILESTAT_SET_TIMES | | |
| 1762 | w.RIGHT.PATH_SYMLINK | | |
| 1763 | w.RIGHT.PATH_REMOVE_DIRECTORY | | |
| 1764 | w.RIGHT.PATH_UNLINK_FILE; | |
| 1765 | } | |
| 1766 | const symlink_flags: w.lookupflags_t = if (args.no_follow) 0x0 else w.LOOKUP_SYMLINK_FOLLOW; | |
| 1767 | // TODO do we really need all the rights here? | |
| 1768 | const inheriting: w.rights_t = w.RIGHT.ALL ^ w.RIGHT.SOCK_SHUTDOWN; | |
| 1769 | ||
| 1770 | const result = os.openatWasi( | |
| 1771 | self.fd, | |
| 1772 | sub_path, | |
| 1773 | symlink_flags, | |
| 1774 | w.O.DIRECTORY, | |
| 1775 | 0x0, | |
| 1776 | base, | |
| 1777 | inheriting, | |
| 1778 | ); | |
| 1779 | const fd = result catch |err| switch (err) { | |
| 1780 | error.FileTooBig => unreachable, // can't happen for directories | |
| 1781 | error.IsDir => unreachable, // we're providing O.DIRECTORY | |
| 1782 | error.NoSpaceLeft => unreachable, // not providing O.CREAT | |
| 1783 | error.PathAlreadyExists => unreachable, // not providing O.CREAT | |
| 1784 | error.FileLocksNotSupported => unreachable, // locking folders is not supported | |
| 1785 | error.WouldBlock => unreachable, // can't happen for directories | |
| 1786 | error.FileBusy => unreachable, // can't happen for directories | |
| 1787 | else => |e| return e, | |
| 1788 | }; | |
| 1789 | return Dir{ .fd = fd }; | |
| 1790 | } | |
| 1791 | ||
| 1792 | /// Same as `openDir` except the parameter is null-terminated. | |
| 1793 | pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenDirOptions, iterable: bool) OpenError!Dir { | |
| 1794 | if (builtin.os.tag == .windows) { | |
| 1795 | const sub_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); | |
| 1796 | return self.openDirW(sub_path_w.span().ptr, args, iterable); | |
| 1797 | } | |
| 1798 | const symlink_flags: u32 = if (args.no_follow) os.O.NOFOLLOW else 0x0; | |
| 1799 | if (!iterable) { | |
| 1800 | const O_PATH = if (@hasDecl(os.O, "PATH")) os.O.PATH else 0; | |
| 1801 | return self.openDirFlagsZ(sub_path_c, os.O.DIRECTORY | os.O.RDONLY | os.O.CLOEXEC | O_PATH | symlink_flags); | |
| 1802 | } else { | |
| 1803 | return self.openDirFlagsZ(sub_path_c, os.O.DIRECTORY | os.O.RDONLY | os.O.CLOEXEC | symlink_flags); | |
| 1804 | } | |
| 1805 | } | |
| 1806 | ||
| 1807 | /// Same as `openDir` except the path parameter is WTF-16 encoded, NT-prefixed. | |
| 1808 | /// This function asserts the target OS is Windows. | |
| 1809 | pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenDirOptions, iterable: bool) OpenError!Dir { | |
| 1810 | const w = os.windows; | |
| 1811 | // TODO remove some of these flags if args.access_sub_paths is false | |
| 1812 | const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | | |
| 1813 | w.SYNCHRONIZE | w.FILE_TRAVERSE; | |
| 1814 | const flags: u32 = if (iterable) base_flags | w.FILE_LIST_DIRECTORY else base_flags; | |
| 1815 | const dir = try self.makeOpenDirAccessMaskW(sub_path_w, flags, .{ | |
| 1816 | .no_follow = args.no_follow, | |
| 1817 | .create_disposition = w.FILE_OPEN, | |
| 1818 | }); | |
| 1819 | return dir; | |
| 1820 | } | |
| 1821 | ||
| 1822 | /// `flags` must contain `os.O.DIRECTORY`. | |
| 1823 | fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: u32) OpenError!Dir { | |
| 1824 | const result = if (need_async_thread) | |
| 1825 | std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, flags, 0) | |
| 1826 | else | |
| 1827 | os.openatZ(self.fd, sub_path_c, flags, 0); | |
| 1828 | const fd = result catch |err| switch (err) { | |
| 1829 | error.FileTooBig => unreachable, // can't happen for directories | |
| 1830 | error.IsDir => unreachable, // we're providing O.DIRECTORY | |
| 1831 | error.NoSpaceLeft => unreachable, // not providing O.CREAT | |
| 1832 | error.PathAlreadyExists => unreachable, // not providing O.CREAT | |
| 1833 | error.FileLocksNotSupported => unreachable, // locking folders is not supported | |
| 1834 | error.WouldBlock => unreachable, // can't happen for directories | |
| 1835 | error.FileBusy => unreachable, // can't happen for directories | |
| 1836 | else => |e| return e, | |
| 1837 | }; | |
| 1838 | return Dir{ .fd = fd }; | |
| 1839 | } | |
| 1840 | ||
| 1841 | const MakeOpenDirAccessMaskWOptions = struct { | |
| 1842 | no_follow: bool, | |
| 1843 | create_disposition: u32, | |
| 1844 | }; | |
| 1845 | ||
| 1846 | fn makeOpenDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) OpenError!Dir { | |
| 1847 | const w = os.windows; | |
| 1848 | ||
| 1849 | var result = Dir{ | |
| 1850 | .fd = undefined, | |
| 1851 | }; | |
| 1852 | ||
| 1853 | const path_len_bytes = @as(u16, @intCast(mem.sliceTo(sub_path_w, 0).len * 2)); | |
| 1854 | var nt_name = w.UNICODE_STRING{ | |
| 1855 | .Length = path_len_bytes, | |
| 1856 | .MaximumLength = path_len_bytes, | |
| 1857 | .Buffer = @constCast(sub_path_w), | |
| 1858 | }; | |
| 1859 | var attr = w.OBJECT_ATTRIBUTES{ | |
| 1860 | .Length = @sizeOf(w.OBJECT_ATTRIBUTES), | |
| 1861 | .RootDirectory = if (path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, | |
| 1862 | .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. | |
| 1863 | .ObjectName = &nt_name, | |
| 1864 | .SecurityDescriptor = null, | |
| 1865 | .SecurityQualityOfService = null, | |
| 1866 | }; | |
| 1867 | const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; | |
| 1868 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 1869 | const rc = w.ntdll.NtCreateFile( | |
| 1870 | &result.fd, | |
| 1871 | access_mask, | |
| 1872 | &attr, | |
| 1873 | &io, | |
| 1874 | null, | |
| 1875 | w.FILE_ATTRIBUTE_NORMAL, | |
| 1876 | w.FILE_SHARE_READ | w.FILE_SHARE_WRITE, | |
| 1877 | flags.create_disposition, | |
| 1878 | w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, | |
| 1879 | null, | |
| 1880 | 0, | |
| 1881 | ); | |
| 1882 | ||
| 1883 | switch (rc) { | |
| 1884 | .SUCCESS => return result, | |
| 1885 | .OBJECT_NAME_INVALID => return error.BadPathName, | |
| 1886 | .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, | |
| 1887 | .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, | |
| 1888 | .NOT_A_DIRECTORY => return error.NotDir, | |
| 1889 | // This can happen if the directory has 'List folder contents' permission set to 'Deny' | |
| 1890 | // and the directory is trying to be opened for iteration. | |
| 1891 | .ACCESS_DENIED => return error.AccessDenied, | |
| 1892 | .INVALID_PARAMETER => unreachable, | |
| 1893 | else => return w.unexpectedStatus(rc), | |
| 1894 | } | |
| 1895 | } | |
| 1896 | ||
| 1897 | pub const DeleteFileError = os.UnlinkError; | |
| 1898 | ||
| 1899 | /// Delete a file name and possibly the file it refers to, based on an open directory handle. | |
| 1900 | /// Asserts that the path parameter has no null bytes. | |
| 1901 | pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void { | |
| 1902 | if (builtin.os.tag == .windows) { | |
| 1903 | const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1904 | return self.deleteFileW(sub_path_w.span()); | |
| 1905 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1906 | os.unlinkat(self.fd, sub_path, 0) catch |err| switch (err) { | |
| 1907 | error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR | |
| 1908 | else => |e| return e, | |
| 1909 | }; | |
| 1910 | } else { | |
| 1911 | const sub_path_c = try os.toPosixPath(sub_path); | |
| 1912 | return self.deleteFileZ(&sub_path_c); | |
| 1913 | } | |
| 1914 | } | |
| 1915 | ||
| 1916 | /// Same as `deleteFile` except the parameter is null-terminated. | |
| 1917 | pub fn deleteFileZ(self: Dir, sub_path_c: [*:0]const u8) DeleteFileError!void { | |
| 1918 | os.unlinkatZ(self.fd, sub_path_c, 0) catch |err| switch (err) { | |
| 1919 | error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR | |
| 1920 | error.AccessDenied => |e| switch (builtin.os.tag) { | |
| 1921 | // non-Linux POSIX systems return EPERM when trying to delete a directory, so | |
| 1922 | // we need to handle that case specifically and translate the error | |
| 1923 | .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => { | |
| 1924 | // Don't follow symlinks to match unlinkat (which acts on symlinks rather than follows them) | |
| 1925 | const fstat = os.fstatatZ(self.fd, sub_path_c, os.AT.SYMLINK_NOFOLLOW) catch return e; | |
| 1926 | const is_dir = fstat.mode & os.S.IFMT == os.S.IFDIR; | |
| 1927 | return if (is_dir) error.IsDir else e; | |
| 1928 | }, | |
| 1929 | else => return e, | |
| 1930 | }, | |
| 1931 | else => |e| return e, | |
| 1932 | }; | |
| 1933 | } | |
| 1934 | ||
| 1935 | /// Same as `deleteFile` except the parameter is WTF-16 encoded. | |
| 1936 | pub fn deleteFileW(self: Dir, sub_path_w: []const u16) DeleteFileError!void { | |
| 1937 | os.unlinkatW(self.fd, sub_path_w, 0) catch |err| switch (err) { | |
| 1938 | error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR | |
| 1939 | else => |e| return e, | |
| 1940 | }; | |
| 1941 | } | |
| 1942 | ||
| 1943 | pub const DeleteDirError = error{ | |
| 1944 | DirNotEmpty, | |
| 1945 | FileNotFound, | |
| 1946 | AccessDenied, | |
| 1947 | FileBusy, | |
| 1948 | FileSystem, | |
| 1949 | SymLinkLoop, | |
| 1950 | NameTooLong, | |
| 1951 | NotDir, | |
| 1952 | SystemResources, | |
| 1953 | ReadOnlyFileSystem, | |
| 1954 | InvalidUtf8, | |
| 1955 | BadPathName, | |
| 1956 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 1957 | NetworkNotFound, | |
| 1958 | Unexpected, | |
| 1959 | }; | |
| 1960 | ||
| 1961 | /// Returns `error.DirNotEmpty` if the directory is not empty. | |
| 1962 | /// To delete a directory recursively, see `deleteTree`. | |
| 1963 | /// Asserts that the path parameter has no null bytes. | |
| 1964 | pub fn deleteDir(self: Dir, sub_path: []const u8) DeleteDirError!void { | |
| 1965 | if (builtin.os.tag == .windows) { | |
| 1966 | const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1967 | return self.deleteDirW(sub_path_w.span()); | |
| 1968 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1969 | os.unlinkat(self.fd, sub_path, os.AT.REMOVEDIR) catch |err| switch (err) { | |
| 1970 | error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR | |
| 1971 | else => |e| return e, | |
| 1972 | }; | |
| 1973 | } else { | |
| 1974 | const sub_path_c = try os.toPosixPath(sub_path); | |
| 1975 | return self.deleteDirZ(&sub_path_c); | |
| 1976 | } | |
| 1977 | } | |
| 1978 | ||
| 1979 | /// Same as `deleteDir` except the parameter is null-terminated. | |
| 1980 | pub fn deleteDirZ(self: Dir, sub_path_c: [*:0]const u8) DeleteDirError!void { | |
| 1981 | os.unlinkatZ(self.fd, sub_path_c, os.AT.REMOVEDIR) catch |err| switch (err) { | |
| 1982 | error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR | |
| 1983 | else => |e| return e, | |
| 1984 | }; | |
| 1985 | } | |
| 1986 | ||
| 1987 | /// Same as `deleteDir` except the parameter is UTF16LE, NT prefixed. | |
| 1988 | /// This function is Windows-only. | |
| 1989 | pub fn deleteDirW(self: Dir, sub_path_w: []const u16) DeleteDirError!void { | |
| 1990 | os.unlinkatW(self.fd, sub_path_w, os.AT.REMOVEDIR) catch |err| switch (err) { | |
| 1991 | error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR | |
| 1992 | else => |e| return e, | |
| 1993 | }; | |
| 1994 | } | |
| 1995 | ||
| 1996 | pub const RenameError = os.RenameError; | |
| 1997 | ||
| 1998 | /// Change the name or location of a file or directory. | |
| 1999 | /// If new_sub_path already exists, it will be replaced. | |
| 2000 | /// Renaming a file over an existing directory or a directory | |
| 2001 | /// over an existing file will fail with `error.IsDir` or `error.NotDir` | |
| 2002 | pub fn rename(self: Dir, old_sub_path: []const u8, new_sub_path: []const u8) RenameError!void { | |
| 2003 | return os.renameat(self.fd, old_sub_path, self.fd, new_sub_path); | |
| 2004 | } | |
| 2005 | ||
| 2006 | /// Same as `rename` except the parameters are null-terminated. | |
| 2007 | pub fn renameZ(self: Dir, old_sub_path_z: [*:0]const u8, new_sub_path_z: [*:0]const u8) RenameError!void { | |
| 2008 | return os.renameatZ(self.fd, old_sub_path_z, self.fd, new_sub_path_z); | |
| 2009 | } | |
| 2010 | ||
| 2011 | /// Same as `rename` except the parameters are UTF16LE, NT prefixed. | |
| 2012 | /// This function is Windows-only. | |
| 2013 | pub fn renameW(self: Dir, old_sub_path_w: []const u16, new_sub_path_w: []const u16) RenameError!void { | |
| 2014 | return os.renameatW(self.fd, old_sub_path_w, self.fd, new_sub_path_w); | |
| 2015 | } | |
| 2016 | ||
| 2017 | /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. | |
| 2018 | /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent | |
| 2019 | /// one; the latter case is known as a dangling link. | |
| 2020 | /// If `sym_link_path` exists, it will not be overwritten. | |
| 2021 | pub fn symLink( | |
| 2022 | self: Dir, | |
| 2023 | target_path: []const u8, | |
| 2024 | sym_link_path: []const u8, | |
| 2025 | flags: SymLinkFlags, | |
| 2026 | ) !void { | |
| 2027 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 2028 | return self.symLinkWasi(target_path, sym_link_path, flags); | |
| 2029 | } | |
| 2030 | if (builtin.os.tag == .windows) { | |
| 2031 | // Target path does not use sliceToPrefixedFileW because certain paths | |
| 2032 | // are handled differently when creating a symlink than they would be | |
| 2033 | // when converting to an NT namespaced path. CreateSymbolicLink in | |
| 2034 | // symLinkW will handle the necessary conversion. | |
| 2035 | var target_path_w: os.windows.PathSpace = undefined; | |
| 2036 | target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path); | |
| 2037 | target_path_w.data[target_path_w.len] = 0; | |
| 2038 | const sym_link_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sym_link_path); | |
| 2039 | return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); | |
| 2040 | } | |
| 2041 | const target_path_c = try os.toPosixPath(target_path); | |
| 2042 | const sym_link_path_c = try os.toPosixPath(sym_link_path); | |
| 2043 | return self.symLinkZ(&target_path_c, &sym_link_path_c, flags); | |
| 2044 | } | |
| 2045 | ||
| 2046 | /// WASI-only. Same as `symLink` except targeting WASI. | |
| 2047 | pub fn symLinkWasi( | |
| 2048 | self: Dir, | |
| 2049 | target_path: []const u8, | |
| 2050 | sym_link_path: []const u8, | |
| 2051 | _: SymLinkFlags, | |
| 2052 | ) !void { | |
| 2053 | return os.symlinkat(target_path, self.fd, sym_link_path); | |
| 2054 | } | |
| 2055 | ||
| 2056 | /// Same as `symLink`, except the pathname parameters are null-terminated. | |
| 2057 | pub fn symLinkZ( | |
| 2058 | self: Dir, | |
| 2059 | target_path_c: [*:0]const u8, | |
| 2060 | sym_link_path_c: [*:0]const u8, | |
| 2061 | flags: SymLinkFlags, | |
| 2062 | ) !void { | |
| 2063 | if (builtin.os.tag == .windows) { | |
| 2064 | const target_path_w = try os.windows.cStrToPrefixedFileW(self.fd, target_path_c); | |
| 2065 | const sym_link_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sym_link_path_c); | |
| 2066 | return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); | |
| 2067 | } | |
| 2068 | return os.symlinkatZ(target_path_c, self.fd, sym_link_path_c); | |
| 2069 | } | |
| 2070 | ||
| 2071 | /// Windows-only. Same as `symLink` except the pathname parameters | |
| 2072 | /// are null-terminated, WTF16 encoded. | |
| 2073 | pub fn symLinkW( | |
| 2074 | self: Dir, | |
| 2075 | /// WTF-16, does not need to be NT-prefixed. The NT-prefixing | |
| 2076 | /// of this path is handled by CreateSymbolicLink. | |
| 2077 | target_path_w: [:0]const u16, | |
| 2078 | /// WTF-16, must be NT-prefixed or relative | |
| 2079 | sym_link_path_w: []const u16, | |
| 2080 | flags: SymLinkFlags, | |
| 2081 | ) !void { | |
| 2082 | return os.windows.CreateSymbolicLink(self.fd, sym_link_path_w, target_path_w, flags.is_directory); | |
| 2083 | } | |
| 2084 | ||
| 2085 | pub const ReadLinkError = os.ReadLinkError; | |
| 2086 | ||
| 2087 | /// Read value of a symbolic link. | |
| 2088 | /// The return value is a slice of `buffer`, from index `0`. | |
| 2089 | /// Asserts that the path parameter has no null bytes. | |
| 2090 | pub fn readLink(self: Dir, sub_path: []const u8, buffer: []u8) ReadLinkError![]u8 { | |
| 2091 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 2092 | return self.readLinkWasi(sub_path, buffer); | |
| 2093 | } | |
| 2094 | if (builtin.os.tag == .windows) { | |
| 2095 | const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 2096 | return self.readLinkW(sub_path_w.span(), buffer); | |
| 2097 | } | |
| 2098 | const sub_path_c = try os.toPosixPath(sub_path); | |
| 2099 | return self.readLinkZ(&sub_path_c, buffer); | |
| 2100 | } | |
| 2101 | ||
| 2102 | /// WASI-only. Same as `readLink` except targeting WASI. | |
| 2103 | pub fn readLinkWasi(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 { | |
| 2104 | return os.readlinkat(self.fd, sub_path, buffer); | |
| 2105 | } | |
| 2106 | ||
| 2107 | /// Same as `readLink`, except the `pathname` parameter is null-terminated. | |
| 2108 | pub fn readLinkZ(self: Dir, sub_path_c: [*:0]const u8, buffer: []u8) ![]u8 { | |
| 2109 | if (builtin.os.tag == .windows) { | |
| 2110 | const sub_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); | |
| 2111 | return self.readLinkW(sub_path_w.span(), buffer); | |
| 2112 | } | |
| 2113 | return os.readlinkatZ(self.fd, sub_path_c, buffer); | |
| 2114 | } | |
| 2115 | ||
| 2116 | /// Windows-only. Same as `readLink` except the pathname parameter | |
| 2117 | /// is null-terminated, WTF16 encoded. | |
| 2118 | pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 { | |
| 2119 | return os.windows.ReadLink(self.fd, sub_path_w, buffer); | |
| 2120 | } | |
| 2121 | ||
| 2122 | /// Read all of file contents using a preallocated buffer. | |
| 2123 | /// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len` | |
| 2124 | /// the situation is ambiguous. It could either mean that the entire file was read, and | |
| 2125 | /// it exactly fits the buffer, or it could mean the buffer was not big enough for the | |
| 2126 | /// entire file. | |
| 2127 | pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 { | |
| 2128 | var file = try self.openFile(file_path, .{}); | |
| 2129 | defer file.close(); | |
| 2130 | ||
| 2131 | const end_index = try file.readAll(buffer); | |
| 2132 | return buffer[0..end_index]; | |
| 2133 | } | |
| 2134 | ||
| 2135 | /// On success, caller owns returned buffer. | |
| 2136 | /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. | |
| 2137 | pub fn readFileAlloc(self: Dir, allocator: mem.Allocator, file_path: []const u8, max_bytes: usize) ![]u8 { | |
| 2138 | return self.readFileAllocOptions(allocator, file_path, max_bytes, null, @alignOf(u8), null); | |
| 2139 | } | |
| 2140 | ||
| 2141 | /// On success, caller owns returned buffer. | |
| 2142 | /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. | |
| 2143 | /// If `size_hint` is specified the initial buffer size is calculated using | |
| 2144 | /// that value, otherwise the effective file size is used instead. | |
| 2145 | /// Allows specifying alignment and a sentinel value. | |
| 2146 | pub fn readFileAllocOptions( | |
| 2147 | self: Dir, | |
| 2148 | allocator: mem.Allocator, | |
| 2149 | file_path: []const u8, | |
| 2150 | max_bytes: usize, | |
| 2151 | size_hint: ?usize, | |
| 2152 | comptime alignment: u29, | |
| 2153 | comptime optional_sentinel: ?u8, | |
| 2154 | ) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) { | |
| 2155 | var file = try self.openFile(file_path, .{}); | |
| 2156 | defer file.close(); | |
| 2157 | ||
| 2158 | // If the file size doesn't fit a usize it'll be certainly greater than | |
| 2159 | // `max_bytes` | |
| 2160 | const stat_size = size_hint orelse math.cast(usize, try file.getEndPos()) orelse | |
| 2161 | return error.FileTooBig; | |
| 2162 | ||
| 2163 | return file.readToEndAllocOptions(allocator, max_bytes, stat_size, alignment, optional_sentinel); | |
| 2164 | } | |
| 2165 | ||
| 2166 | pub const DeleteTreeError = error{ | |
| 2167 | InvalidHandle, | |
| 2168 | AccessDenied, | |
| 2169 | FileTooBig, | |
| 2170 | SymLinkLoop, | |
| 2171 | ProcessFdQuotaExceeded, | |
| 2172 | NameTooLong, | |
| 2173 | SystemFdQuotaExceeded, | |
| 2174 | NoDevice, | |
| 2175 | SystemResources, | |
| 2176 | ReadOnlyFileSystem, | |
| 2177 | FileSystem, | |
| 2178 | FileBusy, | |
| 2179 | DeviceBusy, | |
| 2180 | ||
| 2181 | /// One of the path components was not a directory. | |
| 2182 | /// This error is unreachable if `sub_path` does not contain a path separator. | |
| 2183 | NotDir, | |
| 2184 | ||
| 2185 | /// On Windows, file paths must be valid Unicode. | |
| 2186 | InvalidUtf8, | |
| 2187 | ||
| 2188 | /// On Windows, file paths cannot contain these characters: | |
| 2189 | /// '/', '*', '?', '"', '<', '>', '|' | |
| 2190 | BadPathName, | |
| 2191 | ||
| 2192 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 2193 | NetworkNotFound, | |
| 2194 | } || os.UnexpectedError; | |
| 2195 | ||
| 2196 | /// Whether `full_path` describes a symlink, file, or directory, this function | |
| 2197 | /// removes it. If it cannot be removed because it is a non-empty directory, | |
| 2198 | /// this function recursively removes its entries and then tries again. | |
| 2199 | /// This operation is not atomic on most file systems. | |
| 2200 | pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { | |
| 2201 | var initial_iterable_dir = (try self.deleteTreeOpenInitialSubpath(sub_path, .file)) orelse return; | |
| 2202 | ||
| 2203 | const StackItem = struct { | |
| 2204 | name: []const u8, | |
| 2205 | parent_dir: Dir, | |
| 2206 | iter: IterableDir.Iterator, | |
| 2207 | ||
| 2208 | fn closeAll(items: []@This()) void { | |
| 2209 | for (items) |*item| item.iter.dir.close(); | |
| 2210 | } | |
| 2211 | }; | |
| 2212 | ||
| 2213 | var stack_buffer: [16]StackItem = undefined; | |
| 2214 | var stack = std.ArrayListUnmanaged(StackItem).initBuffer(&stack_buffer); | |
| 2215 | defer StackItem.closeAll(stack.items); | |
| 2216 | ||
| 2217 | stack.appendAssumeCapacity(.{ | |
| 2218 | .name = sub_path, | |
| 2219 | .parent_dir = self, | |
| 2220 | .iter = initial_iterable_dir.iterateAssumeFirstIteration(), | |
| 2221 | }); | |
| 2222 | ||
| 2223 | process_stack: while (stack.items.len != 0) { | |
| 2224 | var top = &stack.items[stack.items.len - 1]; | |
| 2225 | while (try top.iter.next()) |entry| { | |
| 2226 | var treat_as_dir = entry.kind == .directory; | |
| 2227 | handle_entry: while (true) { | |
| 2228 | if (treat_as_dir) { | |
| 2229 | if (stack.unusedCapacitySlice().len >= 1) { | |
| 2230 | var iterable_dir = top.iter.dir.openIterableDir(entry.name, .{ .no_follow = true }) catch |err| switch (err) { | |
| 2231 | error.NotDir => { | |
| 2232 | treat_as_dir = false; | |
| 2233 | continue :handle_entry; | |
| 2234 | }, | |
| 2235 | error.FileNotFound => { | |
| 2236 | // That's fine, we were trying to remove this directory anyway. | |
| 2237 | break :handle_entry; | |
| 2238 | }, | |
| 2239 | ||
| 2240 | error.InvalidHandle, | |
| 2241 | error.AccessDenied, | |
| 2242 | error.SymLinkLoop, | |
| 2243 | error.ProcessFdQuotaExceeded, | |
| 2244 | error.NameTooLong, | |
| 2245 | error.SystemFdQuotaExceeded, | |
| 2246 | error.NoDevice, | |
| 2247 | error.SystemResources, | |
| 2248 | error.Unexpected, | |
| 2249 | error.InvalidUtf8, | |
| 2250 | error.BadPathName, | |
| 2251 | error.NetworkNotFound, | |
| 2252 | error.DeviceBusy, | |
| 2253 | => |e| return e, | |
| 2254 | }; | |
| 2255 | stack.appendAssumeCapacity(.{ | |
| 2256 | .name = entry.name, | |
| 2257 | .parent_dir = top.iter.dir, | |
| 2258 | .iter = iterable_dir.iterateAssumeFirstIteration(), | |
| 2259 | }); | |
| 2260 | continue :process_stack; | |
| 2261 | } else { | |
| 2262 | try top.iter.dir.deleteTreeMinStackSizeWithKindHint(entry.name, entry.kind); | |
| 2263 | break :handle_entry; | |
| 2264 | } | |
| 2265 | } else { | |
| 2266 | if (top.iter.dir.deleteFile(entry.name)) { | |
| 2267 | break :handle_entry; | |
| 2268 | } else |err| switch (err) { | |
| 2269 | error.FileNotFound => break :handle_entry, | |
| 2270 | ||
| 2271 | // Impossible because we do not pass any path separators. | |
| 2272 | error.NotDir => unreachable, | |
| 2273 | ||
| 2274 | error.IsDir => { | |
| 2275 | treat_as_dir = true; | |
| 2276 | continue :handle_entry; | |
| 2277 | }, | |
| 2278 | ||
| 2279 | error.AccessDenied, | |
| 2280 | error.InvalidUtf8, | |
| 2281 | error.SymLinkLoop, | |
| 2282 | error.NameTooLong, | |
| 2283 | error.SystemResources, | |
| 2284 | error.ReadOnlyFileSystem, | |
| 2285 | error.FileSystem, | |
| 2286 | error.FileBusy, | |
| 2287 | error.BadPathName, | |
| 2288 | error.NetworkNotFound, | |
| 2289 | error.Unexpected, | |
| 2290 | => |e| return e, | |
| 2291 | } | |
| 2292 | } | |
| 2293 | } | |
| 2294 | } | |
| 2295 | ||
| 2296 | // On Windows, we can't delete until the dir's handle has been closed, so | |
| 2297 | // close it before we try to delete. | |
| 2298 | top.iter.dir.close(); | |
| 2299 | ||
| 2300 | // In order to avoid double-closing the directory when cleaning up | |
| 2301 | // the stack in the case of an error, we save the relevant portions and | |
| 2302 | // pop the value from the stack. | |
| 2303 | const parent_dir = top.parent_dir; | |
| 2304 | const name = top.name; | |
| 2305 | stack.items.len -= 1; | |
| 2306 | ||
| 2307 | var need_to_retry: bool = false; | |
| 2308 | parent_dir.deleteDir(name) catch |err| switch (err) { | |
| 2309 | error.FileNotFound => {}, | |
| 2310 | error.DirNotEmpty => need_to_retry = true, | |
| 2311 | else => |e| return e, | |
| 2312 | }; | |
| 2313 | ||
| 2314 | if (need_to_retry) { | |
| 2315 | // Since we closed the handle that the previous iterator used, we | |
| 2316 | // need to re-open the dir and re-create the iterator. | |
| 2317 | var iterable_dir = iterable_dir: { | |
| 2318 | var treat_as_dir = true; | |
| 2319 | handle_entry: while (true) { | |
| 2320 | if (treat_as_dir) { | |
| 2321 | break :iterable_dir parent_dir.openIterableDir(name, .{ .no_follow = true }) catch |err| switch (err) { | |
| 2322 | error.NotDir => { | |
| 2323 | treat_as_dir = false; | |
| 2324 | continue :handle_entry; | |
| 2325 | }, | |
| 2326 | error.FileNotFound => { | |
| 2327 | // That's fine, we were trying to remove this directory anyway. | |
| 2328 | continue :process_stack; | |
| 2329 | }, | |
| 2330 | ||
| 2331 | error.InvalidHandle, | |
| 2332 | error.AccessDenied, | |
| 2333 | error.SymLinkLoop, | |
| 2334 | error.ProcessFdQuotaExceeded, | |
| 2335 | error.NameTooLong, | |
| 2336 | error.SystemFdQuotaExceeded, | |
| 2337 | error.NoDevice, | |
| 2338 | error.SystemResources, | |
| 2339 | error.Unexpected, | |
| 2340 | error.InvalidUtf8, | |
| 2341 | error.BadPathName, | |
| 2342 | error.NetworkNotFound, | |
| 2343 | error.DeviceBusy, | |
| 2344 | => |e| return e, | |
| 2345 | }; | |
| 2346 | } else { | |
| 2347 | if (parent_dir.deleteFile(name)) { | |
| 2348 | continue :process_stack; | |
| 2349 | } else |err| switch (err) { | |
| 2350 | error.FileNotFound => continue :process_stack, | |
| 2351 | ||
| 2352 | // Impossible because we do not pass any path separators. | |
| 2353 | error.NotDir => unreachable, | |
| 2354 | ||
| 2355 | error.IsDir => { | |
| 2356 | treat_as_dir = true; | |
| 2357 | continue :handle_entry; | |
| 2358 | }, | |
| 2359 | ||
| 2360 | error.AccessDenied, | |
| 2361 | error.InvalidUtf8, | |
| 2362 | error.SymLinkLoop, | |
| 2363 | error.NameTooLong, | |
| 2364 | error.SystemResources, | |
| 2365 | error.ReadOnlyFileSystem, | |
| 2366 | error.FileSystem, | |
| 2367 | error.FileBusy, | |
| 2368 | error.BadPathName, | |
| 2369 | error.NetworkNotFound, | |
| 2370 | error.Unexpected, | |
| 2371 | => |e| return e, | |
| 2372 | } | |
| 2373 | } | |
| 2374 | } | |
| 2375 | }; | |
| 2376 | // We know there is room on the stack since we are just re-adding | |
| 2377 | // the StackItem that we previously popped. | |
| 2378 | stack.appendAssumeCapacity(.{ | |
| 2379 | .name = name, | |
| 2380 | .parent_dir = parent_dir, | |
| 2381 | .iter = iterable_dir.iterateAssumeFirstIteration(), | |
| 2382 | }); | |
| 2383 | continue :process_stack; | |
| 2384 | } | |
| 2385 | } | |
| 2386 | } | |
| 2387 | ||
| 2388 | /// Like `deleteTree`, but only keeps one `Iterator` active at a time to minimize the function's stack size. | |
| 2389 | /// This is slower than `deleteTree` but uses less stack space. | |
| 2390 | pub fn deleteTreeMinStackSize(self: Dir, sub_path: []const u8) DeleteTreeError!void { | |
| 2391 | return self.deleteTreeMinStackSizeWithKindHint(sub_path, .file); | |
| 2392 | } | |
| 2393 | ||
| 2394 | fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint: File.Kind) DeleteTreeError!void { | |
| 2395 | start_over: while (true) { | |
| 2396 | var iterable_dir = (try self.deleteTreeOpenInitialSubpath(sub_path, kind_hint)) orelse return; | |
| 2397 | var cleanup_dir_parent: ?IterableDir = null; | |
| 2398 | defer if (cleanup_dir_parent) |*d| d.close(); | |
| 2399 | ||
| 2400 | var cleanup_dir = true; | |
| 2401 | defer if (cleanup_dir) iterable_dir.close(); | |
| 2402 | ||
| 2403 | // Valid use of MAX_PATH_BYTES because dir_name_buf will only | |
| 2404 | // ever store a single path component that was returned from the | |
| 2405 | // filesystem. | |
| 2406 | var dir_name_buf: [MAX_PATH_BYTES]u8 = undefined; | |
| 2407 | var dir_name: []const u8 = sub_path; | |
| 2408 | ||
| 2409 | // Here we must avoid recursion, in order to provide O(1) memory guarantee of this function. | |
| 2410 | // Go through each entry and if it is not a directory, delete it. If it is a directory, | |
| 2411 | // open it, and close the original directory. Repeat. Then start the entire operation over. | |
| 2412 | ||
| 2413 | scan_dir: while (true) { | |
| 2414 | var dir_it = iterable_dir.iterateAssumeFirstIteration(); | |
| 2415 | dir_it: while (try dir_it.next()) |entry| { | |
| 2416 | var treat_as_dir = entry.kind == .directory; | |
| 2417 | handle_entry: while (true) { | |
| 2418 | if (treat_as_dir) { | |
| 2419 | const new_dir = iterable_dir.dir.openIterableDir(entry.name, .{ .no_follow = true }) catch |err| switch (err) { | |
| 2420 | error.NotDir => { | |
| 2421 | treat_as_dir = false; | |
| 2422 | continue :handle_entry; | |
| 2423 | }, | |
| 2424 | error.FileNotFound => { | |
| 2425 | // That's fine, we were trying to remove this directory anyway. | |
| 2426 | continue :dir_it; | |
| 2427 | }, | |
| 2428 | ||
| 2429 | error.InvalidHandle, | |
| 2430 | error.AccessDenied, | |
| 2431 | error.SymLinkLoop, | |
| 2432 | error.ProcessFdQuotaExceeded, | |
| 2433 | error.NameTooLong, | |
| 2434 | error.SystemFdQuotaExceeded, | |
| 2435 | error.NoDevice, | |
| 2436 | error.SystemResources, | |
| 2437 | error.Unexpected, | |
| 2438 | error.InvalidUtf8, | |
| 2439 | error.BadPathName, | |
| 2440 | error.NetworkNotFound, | |
| 2441 | error.DeviceBusy, | |
| 2442 | => |e| return e, | |
| 2443 | }; | |
| 2444 | if (cleanup_dir_parent) |*d| d.close(); | |
| 2445 | cleanup_dir_parent = iterable_dir; | |
| 2446 | iterable_dir = new_dir; | |
| 2447 | const result = dir_name_buf[0..entry.name.len]; | |
| 2448 | @memcpy(result, entry.name); | |
| 2449 | dir_name = result; | |
| 2450 | continue :scan_dir; | |
| 2451 | } else { | |
| 2452 | if (iterable_dir.dir.deleteFile(entry.name)) { | |
| 2453 | continue :dir_it; | |
| 2454 | } else |err| switch (err) { | |
| 2455 | error.FileNotFound => continue :dir_it, | |
| 2456 | ||
| 2457 | // Impossible because we do not pass any path separators. | |
| 2458 | error.NotDir => unreachable, | |
| 2459 | ||
| 2460 | error.IsDir => { | |
| 2461 | treat_as_dir = true; | |
| 2462 | continue :handle_entry; | |
| 2463 | }, | |
| 2464 | ||
| 2465 | error.AccessDenied, | |
| 2466 | error.InvalidUtf8, | |
| 2467 | error.SymLinkLoop, | |
| 2468 | error.NameTooLong, | |
| 2469 | error.SystemResources, | |
| 2470 | error.ReadOnlyFileSystem, | |
| 2471 | error.FileSystem, | |
| 2472 | error.FileBusy, | |
| 2473 | error.BadPathName, | |
| 2474 | error.NetworkNotFound, | |
| 2475 | error.Unexpected, | |
| 2476 | => |e| return e, | |
| 2477 | } | |
| 2478 | } | |
| 2479 | } | |
| 2480 | } | |
| 2481 | // Reached the end of the directory entries, which means we successfully deleted all of them. | |
| 2482 | // Now to remove the directory itself. | |
| 2483 | iterable_dir.close(); | |
| 2484 | cleanup_dir = false; | |
| 2485 | ||
| 2486 | if (cleanup_dir_parent) |d| { | |
| 2487 | d.dir.deleteDir(dir_name) catch |err| switch (err) { | |
| 2488 | // These two things can happen due to file system race conditions. | |
| 2489 | error.FileNotFound, error.DirNotEmpty => continue :start_over, | |
| 2490 | else => |e| return e, | |
| 2491 | }; | |
| 2492 | continue :start_over; | |
| 2493 | } else { | |
| 2494 | self.deleteDir(sub_path) catch |err| switch (err) { | |
| 2495 | error.FileNotFound => return, | |
| 2496 | error.DirNotEmpty => continue :start_over, | |
| 2497 | else => |e| return e, | |
| 2498 | }; | |
| 2499 | return; | |
| 2500 | } | |
| 2501 | } | |
| 2502 | } | |
| 2503 | } | |
| 2504 | ||
| 2505 | /// On successful delete, returns null. | |
| 2506 | fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File.Kind) !?IterableDir { | |
| 2507 | return iterable_dir: { | |
| 2508 | // Treat as a file by default | |
| 2509 | var treat_as_dir = kind_hint == .directory; | |
| 2510 | ||
| 2511 | handle_entry: while (true) { | |
| 2512 | if (treat_as_dir) { | |
| 2513 | break :iterable_dir self.openIterableDir(sub_path, .{ .no_follow = true }) catch |err| switch (err) { | |
| 2514 | error.NotDir => { | |
| 2515 | treat_as_dir = false; | |
| 2516 | continue :handle_entry; | |
| 2517 | }, | |
| 2518 | error.FileNotFound => { | |
| 2519 | // That's fine, we were trying to remove this directory anyway. | |
| 2520 | return null; | |
| 2521 | }, | |
| 2522 | ||
| 2523 | error.InvalidHandle, | |
| 2524 | error.AccessDenied, | |
| 2525 | error.SymLinkLoop, | |
| 2526 | error.ProcessFdQuotaExceeded, | |
| 2527 | error.NameTooLong, | |
| 2528 | error.SystemFdQuotaExceeded, | |
| 2529 | error.NoDevice, | |
| 2530 | error.SystemResources, | |
| 2531 | error.Unexpected, | |
| 2532 | error.InvalidUtf8, | |
| 2533 | error.BadPathName, | |
| 2534 | error.DeviceBusy, | |
| 2535 | error.NetworkNotFound, | |
| 2536 | => |e| return e, | |
| 2537 | }; | |
| 2538 | } else { | |
| 2539 | if (self.deleteFile(sub_path)) { | |
| 2540 | return null; | |
| 2541 | } else |err| switch (err) { | |
| 2542 | error.FileNotFound => return null, | |
| 2543 | ||
| 2544 | error.IsDir => { | |
| 2545 | treat_as_dir = true; | |
| 2546 | continue :handle_entry; | |
| 2547 | }, | |
| 2548 | ||
| 2549 | error.AccessDenied, | |
| 2550 | error.InvalidUtf8, | |
| 2551 | error.SymLinkLoop, | |
| 2552 | error.NameTooLong, | |
| 2553 | error.SystemResources, | |
| 2554 | error.ReadOnlyFileSystem, | |
| 2555 | error.NotDir, | |
| 2556 | error.FileSystem, | |
| 2557 | error.FileBusy, | |
| 2558 | error.BadPathName, | |
| 2559 | error.NetworkNotFound, | |
| 2560 | error.Unexpected, | |
| 2561 | => |e| return e, | |
| 2562 | } | |
| 2563 | } | |
| 2564 | } | |
| 2565 | }; | |
| 2566 | } | |
| 2567 | ||
| 2568 | pub const WriteFileError = File.WriteError || File.OpenError; | |
| 2569 | ||
| 2570 | /// Deprecated: use `writeFile2`. | |
| 2571 | pub fn writeFile(self: Dir, sub_path: []const u8, data: []const u8) WriteFileError!void { | |
| 2572 | return writeFile2(self, .{ | |
| 2573 | .sub_path = sub_path, | |
| 2574 | .data = data, | |
| 2575 | .flags = .{}, | |
| 2576 | }); | |
| 2577 | } | |
| 2578 | ||
| 2579 | pub const WriteFileOptions = struct { | |
| 2580 | sub_path: []const u8, | |
| 2581 | data: []const u8, | |
| 2582 | flags: File.CreateFlags = .{}, | |
| 2583 | }; | |
| 2584 | ||
| 2585 | /// Writes content to the file system, using the file creation flags provided. | |
| 2586 | pub fn writeFile2(self: Dir, options: WriteFileOptions) WriteFileError!void { | |
| 2587 | var file = try self.createFile(options.sub_path, options.flags); | |
| 2588 | defer file.close(); | |
| 2589 | try file.writeAll(options.data); | |
| 2590 | } | |
| 2591 | ||
| 2592 | pub const AccessError = os.AccessError; | |
| 2593 | ||
| 2594 | /// Test accessing `path`. | |
| 2595 | /// `path` is UTF-8-encoded. | |
| 2596 | /// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function. | |
| 2597 | /// For example, instead of testing if a file exists and then opening it, just | |
| 2598 | /// open it and handle the error for file not found. | |
| 2599 | pub fn access(self: Dir, sub_path: []const u8, flags: File.OpenFlags) AccessError!void { | |
| 2600 | if (builtin.os.tag == .windows) { | |
| 2601 | const sub_path_w = os.windows.sliceToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) { | |
| 2602 | error.AccessDenied => return error.PermissionDenied, | |
| 2603 | else => |e| return e, | |
| 2604 | }; | |
| 2605 | return self.accessW(sub_path_w.span().ptr, flags); | |
| 2606 | } | |
| 2607 | const path_c = try os.toPosixPath(sub_path); | |
| 2608 | return self.accessZ(&path_c, flags); | |
| 2609 | } | |
| 2610 | ||
| 2611 | /// Same as `access` except the path parameter is null-terminated. | |
| 2612 | pub fn accessZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) AccessError!void { | |
| 2613 | if (builtin.os.tag == .windows) { | |
| 2614 | const sub_path_w = os.windows.cStrToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) { | |
| 2615 | error.AccessDenied => return error.PermissionDenied, | |
| 2616 | else => |e| return e, | |
| 2617 | }; | |
| 2618 | return self.accessW(sub_path_w.span().ptr, flags); | |
| 2619 | } | |
| 2620 | const os_mode = switch (flags.mode) { | |
| 2621 | .read_only => @as(u32, os.F_OK), | |
| 2622 | .write_only => @as(u32, os.W_OK), | |
| 2623 | .read_write => @as(u32, os.R_OK | os.W_OK), | |
| 2624 | }; | |
| 2625 | const result = if (need_async_thread and flags.intended_io_mode != .blocking) | |
| 2626 | std.event.Loop.instance.?.faccessatZ(self.fd, sub_path, os_mode, 0) | |
| 2627 | else | |
| 2628 | os.faccessatZ(self.fd, sub_path, os_mode, 0); | |
| 2629 | return result; | |
| 2630 | } | |
| 2631 | ||
| 2632 | /// Same as `access` except asserts the target OS is Windows and the path parameter is | |
| 2633 | /// * WTF-16 encoded | |
| 2634 | /// * null-terminated | |
| 2635 | /// * NtDll prefixed | |
| 2636 | /// TODO currently this ignores `flags`. | |
| 2637 | pub fn accessW(self: Dir, sub_path_w: [*:0]const u16, flags: File.OpenFlags) AccessError!void { | |
| 2638 | _ = flags; | |
| 2639 | return os.faccessatW(self.fd, sub_path_w, 0, 0); | |
| 2640 | } | |
| 2641 | ||
| 2642 | /// Check the file size, mtime, and mode of `source_path` and `dest_path`. If they are equal, does nothing. | |
| 2643 | /// Otherwise, atomically copies `source_path` to `dest_path`. The destination file gains the mtime, | |
| 2644 | /// atime, and mode of the source file so that the next call to `updateFile` will not need a copy. | |
| 2645 | /// Returns the previous status of the file before updating. | |
| 2646 | /// If any of the directories do not exist for dest_path, they are created. | |
| 2647 | pub fn updateFile( | |
| 2648 | source_dir: Dir, | |
| 2649 | source_path: []const u8, | |
| 2650 | dest_dir: Dir, | |
| 2651 | dest_path: []const u8, | |
| 2652 | options: CopyFileOptions, | |
| 2653 | ) !PrevStatus { | |
| 2654 | var src_file = try source_dir.openFile(source_path, .{}); | |
| 2655 | defer src_file.close(); | |
| 2656 | ||
| 2657 | const src_stat = try src_file.stat(); | |
| 2658 | const actual_mode = options.override_mode orelse src_stat.mode; | |
| 2659 | check_dest_stat: { | |
| 2660 | const dest_stat = blk: { | |
| 2661 | var dest_file = dest_dir.openFile(dest_path, .{}) catch |err| switch (err) { | |
| 2662 | error.FileNotFound => break :check_dest_stat, | |
| 2663 | else => |e| return e, | |
| 2664 | }; | |
| 2665 | defer dest_file.close(); | |
| 2666 | ||
| 2667 | break :blk try dest_file.stat(); | |
| 2668 | }; | |
| 2669 | ||
| 2670 | if (src_stat.size == dest_stat.size and | |
| 2671 | src_stat.mtime == dest_stat.mtime and | |
| 2672 | actual_mode == dest_stat.mode) | |
| 2673 | { | |
| 2674 | return PrevStatus.fresh; | |
| 2675 | } | |
| 2676 | } | |
| 2677 | ||
| 2678 | if (path.dirname(dest_path)) |dirname| { | |
| 2679 | try dest_dir.makePath(dirname); | |
| 2680 | } | |
| 2681 | ||
| 2682 | var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = actual_mode }); | |
| 2683 | defer atomic_file.deinit(); | |
| 2684 | ||
| 2685 | try atomic_file.file.writeFileAll(src_file, .{ .in_len = src_stat.size }); | |
| 2686 | try atomic_file.file.updateTimes(src_stat.atime, src_stat.mtime); | |
| 2687 | try atomic_file.finish(); | |
| 2688 | return PrevStatus.stale; | |
| 2689 | } | |
| 2690 | ||
| 2691 | pub const CopyFileError = File.OpenError || File.StatError || AtomicFile.InitError || CopyFileRawError || AtomicFile.FinishError; | |
| 2692 | ||
| 2693 | /// Guaranteed to be atomic. | |
| 2694 | /// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and readily available, | |
| 2695 | /// there is a possibility of power loss or application termination leaving temporary files present | |
| 2696 | /// in the same directory as dest_path. | |
| 2697 | pub fn copyFile(source_dir: Dir, source_path: []const u8, dest_dir: Dir, dest_path: []const u8, options: CopyFileOptions) CopyFileError!void { | |
| 2698 | var in_file = try source_dir.openFile(source_path, .{}); | |
| 2699 | defer in_file.close(); | |
| 2700 | ||
| 2701 | var size: ?u64 = null; | |
| 2702 | const mode = options.override_mode orelse blk: { | |
| 2703 | const st = try in_file.stat(); | |
| 2704 | size = st.size; | |
| 2705 | break :blk st.mode; | |
| 2706 | }; | |
| 2707 | ||
| 2708 | var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = mode }); | |
| 2709 | defer atomic_file.deinit(); | |
| 2710 | ||
| 2711 | try copy_file(in_file.handle, atomic_file.file.handle, size); | |
| 2712 | try atomic_file.finish(); | |
| 2713 | } | |
| 2714 | ||
| 2715 | pub const AtomicFileOptions = struct { | |
| 2716 | mode: File.Mode = File.default_mode, | |
| 2717 | }; | |
| 2718 | ||
| 2719 | /// Directly access the `.file` field, and then call `AtomicFile.finish` | |
| 2720 | /// to atomically replace `dest_path` with contents. | |
| 2721 | /// Always call `AtomicFile.deinit` to clean up, regardless of whether `AtomicFile.finish` succeeded. | |
| 2722 | /// `dest_path` must remain valid until `AtomicFile.deinit` is called. | |
| 2723 | pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) !AtomicFile { | |
| 2724 | if (path.dirname(dest_path)) |dirname| { | |
| 2725 | const dir = try self.openDir(dirname, .{}); | |
| 2726 | return AtomicFile.init(path.basename(dest_path), options.mode, dir, true); | |
| 2727 | } else { | |
| 2728 | return AtomicFile.init(dest_path, options.mode, self, false); | |
| 2729 | } | |
| 2730 | } | |
| 2731 | ||
| 2732 | pub const Stat = File.Stat; | |
| 2733 | pub const StatError = File.StatError; | |
| 2734 | ||
| 2735 | pub fn stat(self: Dir) StatError!Stat { | |
| 2736 | const file: File = .{ | |
| 2737 | .handle = self.fd, | |
| 2738 | .capable_io_mode = .blocking, | |
| 2739 | }; | |
| 2740 | return file.stat(); | |
| 2741 | } | |
| 2742 | ||
| 2743 | pub const StatFileError = File.OpenError || File.StatError || os.FStatAtError; | |
| 2744 | ||
| 2745 | /// Returns metadata for a file inside the directory. | |
| 2746 | /// | |
| 2747 | /// On Windows, this requires three syscalls. On other operating systems, it | |
| 2748 | /// only takes one. | |
| 2749 | /// | |
| 2750 | /// Symlinks are followed. | |
| 2751 | /// | |
| 2752 | /// `sub_path` may be absolute, in which case `self` is ignored. | |
| 2753 | pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { | |
| 2754 | if (builtin.os.tag == .windows) { | |
| 2755 | var file = try self.openFile(sub_path, .{}); | |
| 2756 | defer file.close(); | |
| 2757 | return file.stat(); | |
| 2758 | } | |
| 2759 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 2760 | const st = try os.fstatatWasi(self.fd, sub_path, os.wasi.LOOKUP_SYMLINK_FOLLOW); | |
| 2761 | return Stat.fromSystem(st); | |
| 2762 | } | |
| 2763 | const st = try os.fstatat(self.fd, sub_path, 0); | |
| 2764 | return Stat.fromSystem(st); | |
| 2765 | } | |
| 2766 | ||
| 2767 | const Permissions = File.Permissions; | |
| 2768 | pub const SetPermissionsError = File.SetPermissionsError; | |
| 2769 | ||
| 2770 | /// Sets permissions according to the provided `Permissions` struct. | |
| 2771 | /// This method is *NOT* available on WASI | |
| 2772 | pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!void { | |
| 2773 | const file: File = .{ | |
| 2774 | .handle = self.fd, | |
| 2775 | .capable_io_mode = .blocking, | |
| 2776 | }; | |
| 2777 | try file.setPermissions(permissions); | |
| 2778 | } | |
| 2779 | ||
| 2780 | const Metadata = File.Metadata; | |
| 2781 | pub const MetadataError = File.MetadataError; | |
| 2782 | ||
| 2783 | /// Returns a `Metadata` struct, representing the permissions on the directory | |
| 2784 | pub fn metadata(self: Dir) MetadataError!Metadata { | |
| 2785 | const file: File = .{ | |
| 2786 | .handle = self.fd, | |
| 2787 | .capable_io_mode = .blocking, | |
| 2788 | }; | |
| 2789 | return try file.metadata(); | |
| 2790 | } | |
| 2791 | }; | |
| 2792 | ||
| 2793 | 229 | /// Returns a handle to the current working directory. It is not opened with iteration capability. |
| 2794 | 230 | /// Closing the returned `Dir` is checked illegal behavior. Iterating over the result is illegal behavior. |
| 2795 | 231 | /// On POSIX targets, this function is comptime-callable. |
| ... | ... | @@ -2821,33 +257,12 @@ pub fn openDirAbsolute(absolute_path: []const u8, flags: Dir.OpenDirOptions) Fil |
| 2821 | 257 | /// Same as `openDirAbsolute` but the path parameter is null-terminated. |
| 2822 | 258 | pub fn openDirAbsoluteZ(absolute_path_c: [*:0]const u8, flags: Dir.OpenDirOptions) File.OpenError!Dir { |
| 2823 | 259 | assert(path.isAbsoluteZ(absolute_path_c)); |
| 2824 | return cwd().openDirZ(absolute_path_c, flags, false); | |
| 260 | return cwd().openDirZ(absolute_path_c, flags); | |
| 2825 | 261 | } |
| 2826 | 262 | /// Same as `openDirAbsolute` but the path parameter is null-terminated. |
| 2827 | 263 | pub fn openDirAbsoluteW(absolute_path_c: [*:0]const u16, flags: Dir.OpenDirOptions) File.OpenError!Dir { |
| 2828 | 264 | assert(path.isAbsoluteWindowsW(absolute_path_c)); |
| 2829 | return cwd().openDirW(absolute_path_c, flags, false); | |
| 2830 | } | |
| 2831 | ||
| 2832 | /// Opens a directory at the given path. The directory is a system resource that remains | |
| 2833 | /// open until `close` is called on the result. | |
| 2834 | /// See `openIterableDirAbsoluteZ` for a function that accepts a null-terminated path. | |
| 2835 | /// | |
| 2836 | /// Asserts that the path parameter has no null bytes. | |
| 2837 | pub fn openIterableDirAbsolute(absolute_path: []const u8, flags: Dir.OpenDirOptions) File.OpenError!IterableDir { | |
| 2838 | assert(path.isAbsolute(absolute_path)); | |
| 2839 | return cwd().openIterableDir(absolute_path, flags); | |
| 2840 | } | |
| 2841 | ||
| 2842 | /// Same as `openIterableDirAbsolute` but the path parameter is null-terminated. | |
| 2843 | pub fn openIterableDirAbsoluteZ(absolute_path_c: [*:0]const u8, flags: Dir.OpenDirOptions) File.OpenError!IterableDir { | |
| 2844 | assert(path.isAbsoluteZ(absolute_path_c)); | |
| 2845 | return IterableDir{ .dir = try cwd().openDirZ(absolute_path_c, flags, true) }; | |
| 2846 | } | |
| 2847 | /// Same as `openIterableDirAbsolute` but the path parameter is null-terminated. | |
| 2848 | pub fn openIterableDirAbsoluteW(absolute_path_c: [*:0]const u16, flags: Dir.OpenDirOptions) File.OpenError!IterableDir { | |
| 2849 | assert(path.isAbsoluteWindowsW(absolute_path_c)); | |
| 2850 | return IterableDir{ .dir = try cwd().openDirW(absolute_path_c, flags, true) }; | |
| 265 | return cwd().openDirW(absolute_path_c, flags); | |
| 2851 | 266 | } |
| 2852 | 267 | |
| 2853 | 268 | /// Opens a file for reading or writing, without attempting to create a new file, based on an absolute path. |
| ... | ... | @@ -2976,20 +391,16 @@ pub fn readLinkAbsoluteZ(pathname_c: [*:0]const u8, buffer: *[MAX_PATH_BYTES]u8) |
| 2976 | 391 | return os.readlinkZ(pathname_c, buffer); |
| 2977 | 392 | } |
| 2978 | 393 | |
| 2979 | /// Use with `Dir.symLink` and `symLinkAbsolute` to specify whether the symlink | |
| 2980 | /// will point to a file or a directory. This value is ignored on all hosts | |
| 2981 | /// except Windows where creating symlinks to different resource types, requires | |
| 2982 | /// different flags. By default, `symLinkAbsolute` is assumed to point to a file. | |
| 2983 | pub const SymLinkFlags = struct { | |
| 2984 | is_directory: bool = false, | |
| 2985 | }; | |
| 2986 | ||
| 2987 | 394 | /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. |
| 2988 | 395 | /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent |
| 2989 | 396 | /// one; the latter case is known as a dangling link. |
| 2990 | 397 | /// If `sym_link_path` exists, it will not be overwritten. |
| 2991 | 398 | /// See also `symLinkAbsoluteZ` and `symLinkAbsoluteW`. |
| 2992 | pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags: SymLinkFlags) !void { | |
| 399 | pub fn symLinkAbsolute( | |
| 400 | target_path: []const u8, | |
| 401 | sym_link_path: []const u8, | |
| 402 | flags: Dir.SymLinkFlags, | |
| 403 | ) !void { | |
| 2993 | 404 | assert(path.isAbsolute(target_path)); |
| 2994 | 405 | assert(path.isAbsolute(sym_link_path)); |
| 2995 | 406 | if (builtin.os.tag == .windows) { |
| ... | ... | @@ -3004,7 +415,11 @@ pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags |
| 3004 | 415 | /// Note that this function will by default try creating a symbolic link to a file. If you would |
| 3005 | 416 | /// like to create a symbolic link to a directory, specify this with `SymLinkFlags{ .is_directory = true }`. |
| 3006 | 417 | /// See also `symLinkAbsolute`, `symLinkAbsoluteZ`. |
| 3007 | pub fn symLinkAbsoluteW(target_path_w: []const u16, sym_link_path_w: []const u16, flags: SymLinkFlags) !void { | |
| 418 | pub fn symLinkAbsoluteW( | |
| 419 | target_path_w: []const u16, | |
| 420 | sym_link_path_w: []const u16, | |
| 421 | flags: Dir.SymLinkFlags, | |
| 422 | ) !void { | |
| 3008 | 423 | assert(path.isAbsoluteWindowsWTF16(target_path_w)); |
| 3009 | 424 | assert(path.isAbsoluteWindowsWTF16(sym_link_path_w)); |
| 3010 | 425 | return os.windows.CreateSymbolicLink(null, sym_link_path_w, target_path_w, flags.is_directory); |
| ... | ... | @@ -3012,7 +427,11 @@ pub fn symLinkAbsoluteW(target_path_w: []const u16, sym_link_path_w: []const u16 |
| 3012 | 427 | |
| 3013 | 428 | /// Same as `symLinkAbsolute` except the parameters are null-terminated pointers. |
| 3014 | 429 | /// See also `symLinkAbsolute`. |
| 3015 | pub fn symLinkAbsoluteZ(target_path_c: [*:0]const u8, sym_link_path_c: [*:0]const u8, flags: SymLinkFlags) !void { | |
| 430 | pub fn symLinkAbsoluteZ( | |
| 431 | target_path_c: [*:0]const u8, | |
| 432 | sym_link_path_c: [*:0]const u8, | |
| 433 | flags: Dir.SymLinkFlags, | |
| 434 | ) !void { | |
| 3016 | 435 | assert(path.isAbsoluteZ(target_path_c)); |
| 3017 | 436 | assert(path.isAbsoluteZ(sym_link_path_c)); |
| 3018 | 437 | if (builtin.os.tag == .windows) { |
| ... | ... | @@ -3209,59 +628,6 @@ pub fn realpathAlloc(allocator: Allocator, pathname: []const u8) ![]u8 { |
| 3209 | 628 | return allocator.dupe(u8, try os.realpath(pathname, &buf)); |
| 3210 | 629 | } |
| 3211 | 630 | |
| 3212 | const CopyFileRawError = error{SystemResources} || os.CopyFileRangeError || os.SendFileError; | |
| 3213 | ||
| 3214 | // Transfer all the data between two file descriptors in the most efficient way. | |
| 3215 | // The copy starts at offset 0, the initial offsets are preserved. | |
| 3216 | // No metadata is transferred over. | |
| 3217 | fn copy_file(fd_in: os.fd_t, fd_out: os.fd_t, maybe_size: ?u64) CopyFileRawError!void { | |
| 3218 | if (comptime builtin.target.isDarwin()) { | |
| 3219 | const rc = os.system.fcopyfile(fd_in, fd_out, null, os.system.COPYFILE_DATA); | |
| 3220 | switch (os.errno(rc)) { | |
| 3221 | .SUCCESS => return, | |
| 3222 | .INVAL => unreachable, | |
| 3223 | .NOMEM => return error.SystemResources, | |
| 3224 | // The source file is not a directory, symbolic link, or regular file. | |
| 3225 | // Try with the fallback path before giving up. | |
| 3226 | .OPNOTSUPP => {}, | |
| 3227 | else => |err| return os.unexpectedErrno(err), | |
| 3228 | } | |
| 3229 | } | |
| 3230 | ||
| 3231 | if (builtin.os.tag == .linux) { | |
| 3232 | // Try copy_file_range first as that works at the FS level and is the | |
| 3233 | // most efficient method (if available). | |
| 3234 | var offset: u64 = 0; | |
| 3235 | cfr_loop: while (true) { | |
| 3236 | // The kernel checks the u64 value `offset+count` for overflow, use | |
| 3237 | // a 32 bit value so that the syscall won't return EINVAL except for | |
| 3238 | // impossibly large files (> 2^64-1 - 2^32-1). | |
| 3239 | const amt = try os.copy_file_range(fd_in, offset, fd_out, offset, math.maxInt(u32), 0); | |
| 3240 | // Terminate as soon as we have copied size bytes or no bytes | |
| 3241 | if (maybe_size) |s| { | |
| 3242 | if (s == amt) break :cfr_loop; | |
| 3243 | } | |
| 3244 | if (amt == 0) break :cfr_loop; | |
| 3245 | offset += amt; | |
| 3246 | } | |
| 3247 | return; | |
| 3248 | } | |
| 3249 | ||
| 3250 | // Sendfile is a zero-copy mechanism iff the OS supports it, otherwise the | |
| 3251 | // fallback code will copy the contents chunk by chunk. | |
| 3252 | const empty_iovec = [0]os.iovec_const{}; | |
| 3253 | var offset: u64 = 0; | |
| 3254 | sendfile_loop: while (true) { | |
| 3255 | const amt = try os.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0); | |
| 3256 | // Terminate as soon as we have copied size bytes or no bytes | |
| 3257 | if (maybe_size) |s| { | |
| 3258 | if (s == amt) break :sendfile_loop; | |
| 3259 | } | |
| 3260 | if (amt == 0) break :sendfile_loop; | |
| 3261 | offset += amt; | |
| 3262 | } | |
| 3263 | } | |
| 3264 | ||
| 3265 | 631 | test { |
| 3266 | 632 | if (builtin.os.tag != .wasi) { |
| 3267 | 633 | _ = &makeDirAbsolute; |
| ... | ... | @@ -3269,10 +635,11 @@ test { |
| 3269 | 635 | _ = &copyFileAbsolute; |
| 3270 | 636 | _ = &updateFileAbsolute; |
| 3271 | 637 | } |
| 3272 | _ = &Dir.copyFile; | |
| 638 | _ = &AtomicFile; | |
| 639 | _ = &Dir; | |
| 640 | _ = &File; | |
| 641 | _ = &path; | |
| 3273 | 642 | _ = @import("fs/test.zig"); |
| 3274 | _ = @import("fs/path.zig"); | |
| 3275 | _ = @import("fs/file.zig"); | |
| 3276 | 643 | _ = @import("fs/get_app_data_dir.zig"); |
| 3277 | 644 | _ = @import("fs/watch.zig"); |
| 3278 | 645 | } |
lib/std/fs/AtomicFile.zig created+85| ... | ... | @@ -0,0 +1,85 @@ |
| 1 | file: File, | |
| 2 | // TODO either replace this with rand_buf or use []u16 on Windows | |
| 3 | tmp_path_buf: [tmp_path_len:0]u8, | |
| 4 | dest_basename: []const u8, | |
| 5 | file_open: bool, | |
| 6 | file_exists: bool, | |
| 7 | close_dir_on_deinit: bool, | |
| 8 | dir: Dir, | |
| 9 | ||
| 10 | pub const InitError = File.OpenError; | |
| 11 | ||
| 12 | pub const random_bytes_len = 12; | |
| 13 | const tmp_path_len = fs.base64_encoder.calcSize(random_bytes_len); | |
| 14 | ||
| 15 | /// Note that the `Dir.atomicFile` API may be more handy than this lower-level function. | |
| 16 | pub fn init( | |
| 17 | dest_basename: []const u8, | |
| 18 | mode: File.Mode, | |
| 19 | dir: Dir, | |
| 20 | close_dir_on_deinit: bool, | |
| 21 | ) InitError!AtomicFile { | |
| 22 | var rand_buf: [random_bytes_len]u8 = undefined; | |
| 23 | var tmp_path_buf: [tmp_path_len:0]u8 = undefined; | |
| 24 | ||
| 25 | while (true) { | |
| 26 | std.crypto.random.bytes(rand_buf[0..]); | |
| 27 | const tmp_path = fs.base64_encoder.encode(&tmp_path_buf, &rand_buf); | |
| 28 | tmp_path_buf[tmp_path.len] = 0; | |
| 29 | ||
| 30 | const file = dir.createFile( | |
| 31 | tmp_path, | |
| 32 | .{ .mode = mode, .exclusive = true }, | |
| 33 | ) catch |err| switch (err) { | |
| 34 | error.PathAlreadyExists => continue, | |
| 35 | else => |e| return e, | |
| 36 | }; | |
| 37 | ||
| 38 | return AtomicFile{ | |
| 39 | .file = file, | |
| 40 | .tmp_path_buf = tmp_path_buf, | |
| 41 | .dest_basename = dest_basename, | |
| 42 | .file_open = true, | |
| 43 | .file_exists = true, | |
| 44 | .close_dir_on_deinit = close_dir_on_deinit, | |
| 45 | .dir = dir, | |
| 46 | }; | |
| 47 | } | |
| 48 | } | |
| 49 | ||
| 50 | /// Always call deinit, even after a successful finish(). | |
| 51 | pub fn deinit(self: *AtomicFile) void { | |
| 52 | if (self.file_open) { | |
| 53 | self.file.close(); | |
| 54 | self.file_open = false; | |
| 55 | } | |
| 56 | if (self.file_exists) { | |
| 57 | self.dir.deleteFile(&self.tmp_path_buf) catch {}; | |
| 58 | self.file_exists = false; | |
| 59 | } | |
| 60 | if (self.close_dir_on_deinit) { | |
| 61 | self.dir.close(); | |
| 62 | } | |
| 63 | self.* = undefined; | |
| 64 | } | |
| 65 | ||
| 66 | pub const FinishError = posix.RenameError; | |
| 67 | ||
| 68 | pub fn finish(self: *AtomicFile) FinishError!void { | |
| 69 | assert(self.file_exists); | |
| 70 | if (self.file_open) { | |
| 71 | self.file.close(); | |
| 72 | self.file_open = false; | |
| 73 | } | |
| 74 | try posix.renameat(self.dir.fd, self.tmp_path_buf[0..], self.dir.fd, self.dest_basename); | |
| 75 | self.file_exists = false; | |
| 76 | } | |
| 77 | ||
| 78 | const AtomicFile = @This(); | |
| 79 | const std = @import("../std.zig"); | |
| 80 | const File = std.fs.File; | |
| 81 | const Dir = std.fs.Dir; | |
| 82 | const fs = std.fs; | |
| 83 | const assert = std.debug.assert; | |
| 84 | // https://github.com/ziglang/zig/issues/5019 | |
| 85 | const posix = std.os; |
lib/std/fs/Dir.zig created+2534| ... | ... | @@ -0,0 +1,2534 @@ |
| 1 | fd: posix.fd_t, | |
| 2 | ||
| 3 | pub const default_mode = 0o755; | |
| 4 | ||
| 5 | pub const Entry = struct { | |
| 6 | name: []const u8, | |
| 7 | kind: Kind, | |
| 8 | ||
| 9 | pub const Kind = File.Kind; | |
| 10 | }; | |
| 11 | ||
| 12 | const IteratorError = error{ AccessDenied, SystemResources } || posix.UnexpectedError; | |
| 13 | ||
| 14 | pub const Iterator = switch (builtin.os.tag) { | |
| 15 | .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => struct { | |
| 16 | dir: Dir, | |
| 17 | seek: i64, | |
| 18 | buf: [1024]u8, // TODO align(@alignOf(posix.system.dirent)), | |
| 19 | index: usize, | |
| 20 | end_index: usize, | |
| 21 | first_iter: bool, | |
| 22 | ||
| 23 | const Self = @This(); | |
| 24 | ||
| 25 | pub const Error = IteratorError; | |
| 26 | ||
| 27 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 28 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 29 | pub fn next(self: *Self) Error!?Entry { | |
| 30 | switch (builtin.os.tag) { | |
| 31 | .macos, .ios => return self.nextDarwin(), | |
| 32 | .freebsd, .netbsd, .dragonfly, .openbsd => return self.nextBsd(), | |
| 33 | .solaris, .illumos => return self.nextSolaris(), | |
| 34 | else => @compileError("unimplemented"), | |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | fn nextDarwin(self: *Self) !?Entry { | |
| 39 | start_over: while (true) { | |
| 40 | if (self.index >= self.end_index) { | |
| 41 | if (self.first_iter) { | |
| 42 | posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 43 | self.first_iter = false; | |
| 44 | } | |
| 45 | const rc = posix.system.__getdirentries64( | |
| 46 | self.dir.fd, | |
| 47 | &self.buf, | |
| 48 | self.buf.len, | |
| 49 | &self.seek, | |
| 50 | ); | |
| 51 | if (rc == 0) return null; | |
| 52 | if (rc < 0) { | |
| 53 | switch (posix.errno(rc)) { | |
| 54 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 55 | .FAULT => unreachable, | |
| 56 | .NOTDIR => unreachable, | |
| 57 | .INVAL => unreachable, | |
| 58 | else => |err| return posix.unexpectedErrno(err), | |
| 59 | } | |
| 60 | } | |
| 61 | self.index = 0; | |
| 62 | self.end_index = @as(usize, @intCast(rc)); | |
| 63 | } | |
| 64 | const darwin_entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 65 | const next_index = self.index + darwin_entry.reclen(); | |
| 66 | self.index = next_index; | |
| 67 | ||
| 68 | const name = @as([*]u8, @ptrCast(&darwin_entry.d_name))[0..darwin_entry.d_namlen]; | |
| 69 | ||
| 70 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (darwin_entry.d_ino == 0)) { | |
| 71 | continue :start_over; | |
| 72 | } | |
| 73 | ||
| 74 | const entry_kind: Entry.Kind = switch (darwin_entry.d_type) { | |
| 75 | posix.DT.BLK => .block_device, | |
| 76 | posix.DT.CHR => .character_device, | |
| 77 | posix.DT.DIR => .directory, | |
| 78 | posix.DT.FIFO => .named_pipe, | |
| 79 | posix.DT.LNK => .sym_link, | |
| 80 | posix.DT.REG => .file, | |
| 81 | posix.DT.SOCK => .unix_domain_socket, | |
| 82 | posix.DT.WHT => .whiteout, | |
| 83 | else => .unknown, | |
| 84 | }; | |
| 85 | return Entry{ | |
| 86 | .name = name, | |
| 87 | .kind = entry_kind, | |
| 88 | }; | |
| 89 | } | |
| 90 | } | |
| 91 | ||
| 92 | fn nextSolaris(self: *Self) !?Entry { | |
| 93 | start_over: while (true) { | |
| 94 | if (self.index >= self.end_index) { | |
| 95 | if (self.first_iter) { | |
| 96 | posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 97 | self.first_iter = false; | |
| 98 | } | |
| 99 | const rc = posix.system.getdents(self.dir.fd, &self.buf, self.buf.len); | |
| 100 | switch (posix.errno(rc)) { | |
| 101 | .SUCCESS => {}, | |
| 102 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 103 | .FAULT => unreachable, | |
| 104 | .NOTDIR => unreachable, | |
| 105 | .INVAL => unreachable, | |
| 106 | else => |err| return posix.unexpectedErrno(err), | |
| 107 | } | |
| 108 | if (rc == 0) return null; | |
| 109 | self.index = 0; | |
| 110 | self.end_index = @as(usize, @intCast(rc)); | |
| 111 | } | |
| 112 | const entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 113 | const next_index = self.index + entry.reclen(); | |
| 114 | self.index = next_index; | |
| 115 | ||
| 116 | const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&entry.d_name)), 0); | |
| 117 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) | |
| 118 | continue :start_over; | |
| 119 | ||
| 120 | // Solaris dirent doesn't expose d_type, so we have to call stat to get it. | |
| 121 | const stat_info = posix.fstatat( | |
| 122 | self.dir.fd, | |
| 123 | name, | |
| 124 | posix.AT.SYMLINK_NOFOLLOW, | |
| 125 | ) catch |err| switch (err) { | |
| 126 | error.NameTooLong => unreachable, | |
| 127 | error.SymLinkLoop => unreachable, | |
| 128 | error.FileNotFound => unreachable, // lost the race | |
| 129 | else => |e| return e, | |
| 130 | }; | |
| 131 | const entry_kind: Entry.Kind = switch (stat_info.mode & posix.S.IFMT) { | |
| 132 | posix.S.IFIFO => .named_pipe, | |
| 133 | posix.S.IFCHR => .character_device, | |
| 134 | posix.S.IFDIR => .directory, | |
| 135 | posix.S.IFBLK => .block_device, | |
| 136 | posix.S.IFREG => .file, | |
| 137 | posix.S.IFLNK => .sym_link, | |
| 138 | posix.S.IFSOCK => .unix_domain_socket, | |
| 139 | posix.S.IFDOOR => .door, | |
| 140 | posix.S.IFPORT => .event_port, | |
| 141 | else => .unknown, | |
| 142 | }; | |
| 143 | return Entry{ | |
| 144 | .name = name, | |
| 145 | .kind = entry_kind, | |
| 146 | }; | |
| 147 | } | |
| 148 | } | |
| 149 | ||
| 150 | fn nextBsd(self: *Self) !?Entry { | |
| 151 | start_over: while (true) { | |
| 152 | if (self.index >= self.end_index) { | |
| 153 | if (self.first_iter) { | |
| 154 | posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 155 | self.first_iter = false; | |
| 156 | } | |
| 157 | const rc = if (builtin.os.tag == .netbsd) | |
| 158 | posix.system.__getdents30(self.dir.fd, &self.buf, self.buf.len) | |
| 159 | else | |
| 160 | posix.system.getdents(self.dir.fd, &self.buf, self.buf.len); | |
| 161 | switch (posix.errno(rc)) { | |
| 162 | .SUCCESS => {}, | |
| 163 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 164 | .FAULT => unreachable, | |
| 165 | .NOTDIR => unreachable, | |
| 166 | .INVAL => unreachable, | |
| 167 | // Introduced in freebsd 13.2: directory unlinked but still open. | |
| 168 | // To be consistent, iteration ends if the directory being iterated is deleted during iteration. | |
| 169 | .NOENT => return null, | |
| 170 | else => |err| return posix.unexpectedErrno(err), | |
| 171 | } | |
| 172 | if (rc == 0) return null; | |
| 173 | self.index = 0; | |
| 174 | self.end_index = @as(usize, @intCast(rc)); | |
| 175 | } | |
| 176 | const bsd_entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 177 | const next_index = self.index + bsd_entry.reclen(); | |
| 178 | self.index = next_index; | |
| 179 | ||
| 180 | const name = @as([*]u8, @ptrCast(&bsd_entry.d_name))[0..bsd_entry.d_namlen]; | |
| 181 | ||
| 182 | const skip_zero_fileno = switch (builtin.os.tag) { | |
| 183 | // d_fileno=0 is used to mark invalid entries or deleted files. | |
| 184 | .openbsd, .netbsd => true, | |
| 185 | else => false, | |
| 186 | }; | |
| 187 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or | |
| 188 | (skip_zero_fileno and bsd_entry.d_fileno == 0)) | |
| 189 | { | |
| 190 | continue :start_over; | |
| 191 | } | |
| 192 | ||
| 193 | const entry_kind: Entry.Kind = switch (bsd_entry.d_type) { | |
| 194 | posix.DT.BLK => .block_device, | |
| 195 | posix.DT.CHR => .character_device, | |
| 196 | posix.DT.DIR => .directory, | |
| 197 | posix.DT.FIFO => .named_pipe, | |
| 198 | posix.DT.LNK => .sym_link, | |
| 199 | posix.DT.REG => .file, | |
| 200 | posix.DT.SOCK => .unix_domain_socket, | |
| 201 | posix.DT.WHT => .whiteout, | |
| 202 | else => .unknown, | |
| 203 | }; | |
| 204 | return Entry{ | |
| 205 | .name = name, | |
| 206 | .kind = entry_kind, | |
| 207 | }; | |
| 208 | } | |
| 209 | } | |
| 210 | ||
| 211 | pub fn reset(self: *Self) void { | |
| 212 | self.index = 0; | |
| 213 | self.end_index = 0; | |
| 214 | self.first_iter = true; | |
| 215 | } | |
| 216 | }, | |
| 217 | .haiku => struct { | |
| 218 | dir: Dir, | |
| 219 | buf: [1024]u8, // TODO align(@alignOf(posix.dirent64)), | |
| 220 | index: usize, | |
| 221 | end_index: usize, | |
| 222 | first_iter: bool, | |
| 223 | ||
| 224 | const Self = @This(); | |
| 225 | ||
| 226 | pub const Error = IteratorError; | |
| 227 | ||
| 228 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 229 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 230 | pub fn next(self: *Self) Error!?Entry { | |
| 231 | start_over: while (true) { | |
| 232 | // TODO: find a better max | |
| 233 | const HAIKU_MAX_COUNT = 10000; | |
| 234 | if (self.index >= self.end_index) { | |
| 235 | if (self.first_iter) { | |
| 236 | posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 237 | self.first_iter = false; | |
| 238 | } | |
| 239 | const rc = posix.system._kern_read_dir( | |
| 240 | self.dir.fd, | |
| 241 | &self.buf, | |
| 242 | self.buf.len, | |
| 243 | HAIKU_MAX_COUNT, | |
| 244 | ); | |
| 245 | if (rc == 0) return null; | |
| 246 | if (rc < 0) { | |
| 247 | switch (posix.errno(rc)) { | |
| 248 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 249 | .FAULT => unreachable, | |
| 250 | .NOTDIR => unreachable, | |
| 251 | .INVAL => unreachable, | |
| 252 | else => |err| return posix.unexpectedErrno(err), | |
| 253 | } | |
| 254 | } | |
| 255 | self.index = 0; | |
| 256 | self.end_index = @as(usize, @intCast(rc)); | |
| 257 | } | |
| 258 | const haiku_entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 259 | const next_index = self.index + haiku_entry.reclen(); | |
| 260 | self.index = next_index; | |
| 261 | const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&haiku_entry.d_name)), 0); | |
| 262 | ||
| 263 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (haiku_entry.d_ino == 0)) { | |
| 264 | continue :start_over; | |
| 265 | } | |
| 266 | ||
| 267 | var stat_info: posix.Stat = undefined; | |
| 268 | const rc = posix.system._kern_read_stat( | |
| 269 | self.dir.fd, | |
| 270 | &haiku_entry.d_name, | |
| 271 | false, | |
| 272 | &stat_info, | |
| 273 | 0, | |
| 274 | ); | |
| 275 | if (rc != 0) { | |
| 276 | switch (posix.errno(rc)) { | |
| 277 | .SUCCESS => {}, | |
| 278 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 279 | .FAULT => unreachable, | |
| 280 | .NOTDIR => unreachable, | |
| 281 | .INVAL => unreachable, | |
| 282 | else => |err| return posix.unexpectedErrno(err), | |
| 283 | } | |
| 284 | } | |
| 285 | const statmode = stat_info.mode & posix.S.IFMT; | |
| 286 | ||
| 287 | const entry_kind: Entry.Kind = switch (statmode) { | |
| 288 | posix.S.IFDIR => .directory, | |
| 289 | posix.S.IFBLK => .block_device, | |
| 290 | posix.S.IFCHR => .character_device, | |
| 291 | posix.S.IFLNK => .sym_link, | |
| 292 | posix.S.IFREG => .file, | |
| 293 | posix.S.IFIFO => .named_pipe, | |
| 294 | else => .unknown, | |
| 295 | }; | |
| 296 | ||
| 297 | return Entry{ | |
| 298 | .name = name, | |
| 299 | .kind = entry_kind, | |
| 300 | }; | |
| 301 | } | |
| 302 | } | |
| 303 | ||
| 304 | pub fn reset(self: *Self) void { | |
| 305 | self.index = 0; | |
| 306 | self.end_index = 0; | |
| 307 | self.first_iter = true; | |
| 308 | } | |
| 309 | }, | |
| 310 | .linux => struct { | |
| 311 | dir: Dir, | |
| 312 | // The if guard is solely there to prevent compile errors from missing `linux.dirent64` | |
| 313 | // definition when compiling for other OSes. It doesn't do anything when compiling for Linux. | |
| 314 | buf: [1024]u8 align(if (builtin.os.tag != .linux) 1 else @alignOf(linux.dirent64)), | |
| 315 | index: usize, | |
| 316 | end_index: usize, | |
| 317 | first_iter: bool, | |
| 318 | ||
| 319 | const Self = @This(); | |
| 320 | const linux = std.os.linux; | |
| 321 | ||
| 322 | pub const Error = IteratorError; | |
| 323 | ||
| 324 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 325 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 326 | pub fn next(self: *Self) Error!?Entry { | |
| 327 | return self.nextLinux() catch |err| switch (err) { | |
| 328 | // To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration. | |
| 329 | // This matches the behavior of non-Linux UNIX platforms. | |
| 330 | error.DirNotFound => null, | |
| 331 | else => |e| return e, | |
| 332 | }; | |
| 333 | } | |
| 334 | ||
| 335 | pub const ErrorLinux = error{DirNotFound} || IteratorError; | |
| 336 | ||
| 337 | /// Implementation of `next` that can return `error.DirNotFound` if the directory being | |
| 338 | /// iterated was deleted during iteration (this error is Linux specific). | |
| 339 | pub fn nextLinux(self: *Self) ErrorLinux!?Entry { | |
| 340 | start_over: while (true) { | |
| 341 | if (self.index >= self.end_index) { | |
| 342 | if (self.first_iter) { | |
| 343 | posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 344 | self.first_iter = false; | |
| 345 | } | |
| 346 | const rc = linux.getdents64(self.dir.fd, &self.buf, self.buf.len); | |
| 347 | switch (linux.getErrno(rc)) { | |
| 348 | .SUCCESS => {}, | |
| 349 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 350 | .FAULT => unreachable, | |
| 351 | .NOTDIR => unreachable, | |
| 352 | .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. | |
| 353 | .INVAL => return error.Unexpected, // Linux may in some cases return EINVAL when reading /proc/$PID/net. | |
| 354 | .ACCES => return error.AccessDenied, // Do not have permission to iterate this directory. | |
| 355 | else => |err| return posix.unexpectedErrno(err), | |
| 356 | } | |
| 357 | if (rc == 0) return null; | |
| 358 | self.index = 0; | |
| 359 | self.end_index = rc; | |
| 360 | } | |
| 361 | const linux_entry = @as(*align(1) linux.dirent64, @ptrCast(&self.buf[self.index])); | |
| 362 | const next_index = self.index + linux_entry.reclen(); | |
| 363 | self.index = next_index; | |
| 364 | ||
| 365 | const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&linux_entry.d_name)), 0); | |
| 366 | ||
| 367 | // skip . and .. entries | |
| 368 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { | |
| 369 | continue :start_over; | |
| 370 | } | |
| 371 | ||
| 372 | const entry_kind: Entry.Kind = switch (linux_entry.d_type) { | |
| 373 | linux.DT.BLK => .block_device, | |
| 374 | linux.DT.CHR => .character_device, | |
| 375 | linux.DT.DIR => .directory, | |
| 376 | linux.DT.FIFO => .named_pipe, | |
| 377 | linux.DT.LNK => .sym_link, | |
| 378 | linux.DT.REG => .file, | |
| 379 | linux.DT.SOCK => .unix_domain_socket, | |
| 380 | else => .unknown, | |
| 381 | }; | |
| 382 | return Entry{ | |
| 383 | .name = name, | |
| 384 | .kind = entry_kind, | |
| 385 | }; | |
| 386 | } | |
| 387 | } | |
| 388 | ||
| 389 | pub fn reset(self: *Self) void { | |
| 390 | self.index = 0; | |
| 391 | self.end_index = 0; | |
| 392 | self.first_iter = true; | |
| 393 | } | |
| 394 | }, | |
| 395 | .windows => struct { | |
| 396 | dir: Dir, | |
| 397 | buf: [1024]u8 align(@alignOf(std.os.windows.FILE_BOTH_DIR_INFORMATION)), | |
| 398 | index: usize, | |
| 399 | end_index: usize, | |
| 400 | first_iter: bool, | |
| 401 | name_data: [fs.MAX_NAME_BYTES]u8, | |
| 402 | ||
| 403 | const Self = @This(); | |
| 404 | ||
| 405 | pub const Error = IteratorError; | |
| 406 | ||
| 407 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 408 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 409 | pub fn next(self: *Self) Error!?Entry { | |
| 410 | while (true) { | |
| 411 | const w = std.os.windows; | |
| 412 | if (self.index >= self.end_index) { | |
| 413 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 414 | const rc = w.ntdll.NtQueryDirectoryFile( | |
| 415 | self.dir.fd, | |
| 416 | null, | |
| 417 | null, | |
| 418 | null, | |
| 419 | &io, | |
| 420 | &self.buf, | |
| 421 | self.buf.len, | |
| 422 | .FileBothDirectoryInformation, | |
| 423 | w.FALSE, | |
| 424 | null, | |
| 425 | if (self.first_iter) @as(w.BOOLEAN, w.TRUE) else @as(w.BOOLEAN, w.FALSE), | |
| 426 | ); | |
| 427 | self.first_iter = false; | |
| 428 | if (io.Information == 0) return null; | |
| 429 | self.index = 0; | |
| 430 | self.end_index = io.Information; | |
| 431 | switch (rc) { | |
| 432 | .SUCCESS => {}, | |
| 433 | .ACCESS_DENIED => return error.AccessDenied, // Double-check that the Dir was opened with iteration ability | |
| 434 | ||
| 435 | else => return w.unexpectedStatus(rc), | |
| 436 | } | |
| 437 | } | |
| 438 | ||
| 439 | // While the official api docs guarantee FILE_BOTH_DIR_INFORMATION to be aligned properly | |
| 440 | // this may not always be the case (e.g. due to faulty VM/Sandboxing tools) | |
| 441 | const dir_info: *align(2) w.FILE_BOTH_DIR_INFORMATION = @ptrCast(@alignCast(&self.buf[self.index])); | |
| 442 | if (dir_info.NextEntryOffset != 0) { | |
| 443 | self.index += dir_info.NextEntryOffset; | |
| 444 | } else { | |
| 445 | self.index = self.buf.len; | |
| 446 | } | |
| 447 | ||
| 448 | const name_utf16le = @as([*]u16, @ptrCast(&dir_info.FileName))[0 .. dir_info.FileNameLength / 2]; | |
| 449 | ||
| 450 | if (mem.eql(u16, name_utf16le, &[_]u16{'.'}) or mem.eql(u16, name_utf16le, &[_]u16{ '.', '.' })) | |
| 451 | continue; | |
| 452 | // Trust that Windows gives us valid UTF-16LE | |
| 453 | const name_utf8_len = std.unicode.utf16leToUtf8(self.name_data[0..], name_utf16le) catch unreachable; | |
| 454 | const name_utf8 = self.name_data[0..name_utf8_len]; | |
| 455 | const kind: Entry.Kind = blk: { | |
| 456 | const attrs = dir_info.FileAttributes; | |
| 457 | if (attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk .directory; | |
| 458 | if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk .sym_link; | |
| 459 | break :blk .file; | |
| 460 | }; | |
| 461 | return Entry{ | |
| 462 | .name = name_utf8, | |
| 463 | .kind = kind, | |
| 464 | }; | |
| 465 | } | |
| 466 | } | |
| 467 | ||
| 468 | pub fn reset(self: *Self) void { | |
| 469 | self.index = 0; | |
| 470 | self.end_index = 0; | |
| 471 | self.first_iter = true; | |
| 472 | } | |
| 473 | }, | |
| 474 | .wasi => struct { | |
| 475 | dir: Dir, | |
| 476 | buf: [1024]u8, // TODO align(@alignOf(posix.wasi.dirent_t)), | |
| 477 | cookie: u64, | |
| 478 | index: usize, | |
| 479 | end_index: usize, | |
| 480 | ||
| 481 | const Self = @This(); | |
| 482 | ||
| 483 | pub const Error = IteratorError; | |
| 484 | ||
| 485 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 486 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 487 | pub fn next(self: *Self) Error!?Entry { | |
| 488 | return self.nextWasi() catch |err| switch (err) { | |
| 489 | // To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration. | |
| 490 | // This matches the behavior of non-Linux UNIX platforms. | |
| 491 | error.DirNotFound => null, | |
| 492 | else => |e| return e, | |
| 493 | }; | |
| 494 | } | |
| 495 | ||
| 496 | pub const ErrorWasi = error{DirNotFound} || IteratorError; | |
| 497 | ||
| 498 | /// Implementation of `next` that can return platform-dependent errors depending on the host platform. | |
| 499 | /// When the host platform is Linux, `error.DirNotFound` can be returned if the directory being | |
| 500 | /// iterated was deleted during iteration. | |
| 501 | pub fn nextWasi(self: *Self) ErrorWasi!?Entry { | |
| 502 | // We intentinally use fd_readdir even when linked with libc, | |
| 503 | // since its implementation is exactly the same as below, | |
| 504 | // and we avoid the code complexity here. | |
| 505 | const w = std.os.wasi; | |
| 506 | start_over: while (true) { | |
| 507 | // According to the WASI spec, the last entry might be truncated, | |
| 508 | // so we need to check if the left buffer contains the whole dirent. | |
| 509 | if (self.end_index - self.index < @sizeOf(w.dirent_t)) { | |
| 510 | var bufused: usize = undefined; | |
| 511 | switch (w.fd_readdir(self.dir.fd, &self.buf, self.buf.len, self.cookie, &bufused)) { | |
| 512 | .SUCCESS => {}, | |
| 513 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 514 | .FAULT => unreachable, | |
| 515 | .NOTDIR => unreachable, | |
| 516 | .INVAL => unreachable, | |
| 517 | .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. | |
| 518 | .NOTCAPABLE => return error.AccessDenied, | |
| 519 | else => |err| return posix.unexpectedErrno(err), | |
| 520 | } | |
| 521 | if (bufused == 0) return null; | |
| 522 | self.index = 0; | |
| 523 | self.end_index = bufused; | |
| 524 | } | |
| 525 | const entry = @as(*align(1) w.dirent_t, @ptrCast(&self.buf[self.index])); | |
| 526 | const entry_size = @sizeOf(w.dirent_t); | |
| 527 | const name_index = self.index + entry_size; | |
| 528 | if (name_index + entry.d_namlen > self.end_index) { | |
| 529 | // This case, the name is truncated, so we need to call readdir to store the entire name. | |
| 530 | self.end_index = self.index; // Force fd_readdir in the next loop. | |
| 531 | continue :start_over; | |
| 532 | } | |
| 533 | const name = self.buf[name_index .. name_index + entry.d_namlen]; | |
| 534 | ||
| 535 | const next_index = name_index + entry.d_namlen; | |
| 536 | self.index = next_index; | |
| 537 | self.cookie = entry.d_next; | |
| 538 | ||
| 539 | // skip . and .. entries | |
| 540 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { | |
| 541 | continue :start_over; | |
| 542 | } | |
| 543 | ||
| 544 | const entry_kind: Entry.Kind = switch (entry.d_type) { | |
| 545 | .BLOCK_DEVICE => .block_device, | |
| 546 | .CHARACTER_DEVICE => .character_device, | |
| 547 | .DIRECTORY => .directory, | |
| 548 | .SYMBOLIC_LINK => .sym_link, | |
| 549 | .REGULAR_FILE => .file, | |
| 550 | .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, | |
| 551 | else => .unknown, | |
| 552 | }; | |
| 553 | return Entry{ | |
| 554 | .name = name, | |
| 555 | .kind = entry_kind, | |
| 556 | }; | |
| 557 | } | |
| 558 | } | |
| 559 | ||
| 560 | pub fn reset(self: *Self) void { | |
| 561 | self.index = 0; | |
| 562 | self.end_index = 0; | |
| 563 | self.cookie = std.os.wasi.DIRCOOKIE_START; | |
| 564 | } | |
| 565 | }, | |
| 566 | else => @compileError("unimplemented"), | |
| 567 | }; | |
| 568 | ||
| 569 | pub fn iterate(self: Dir) Iterator { | |
| 570 | return self.iterateImpl(true); | |
| 571 | } | |
| 572 | ||
| 573 | /// Like `iterate`, but will not reset the directory cursor before the first | |
| 574 | /// iteration. This should only be used in cases where it is known that the | |
| 575 | /// `Dir` has not had its cursor modified yet (e.g. it was just opened). | |
| 576 | pub fn iterateAssumeFirstIteration(self: Dir) Iterator { | |
| 577 | return self.iterateImpl(false); | |
| 578 | } | |
| 579 | ||
| 580 | fn iterateImpl(self: Dir, first_iter_start_value: bool) Iterator { | |
| 581 | switch (builtin.os.tag) { | |
| 582 | .macos, | |
| 583 | .ios, | |
| 584 | .freebsd, | |
| 585 | .netbsd, | |
| 586 | .dragonfly, | |
| 587 | .openbsd, | |
| 588 | .solaris, | |
| 589 | .illumos, | |
| 590 | => return Iterator{ | |
| 591 | .dir = self, | |
| 592 | .seek = 0, | |
| 593 | .index = 0, | |
| 594 | .end_index = 0, | |
| 595 | .buf = undefined, | |
| 596 | .first_iter = first_iter_start_value, | |
| 597 | }, | |
| 598 | .linux, .haiku => return Iterator{ | |
| 599 | .dir = self, | |
| 600 | .index = 0, | |
| 601 | .end_index = 0, | |
| 602 | .buf = undefined, | |
| 603 | .first_iter = first_iter_start_value, | |
| 604 | }, | |
| 605 | .windows => return Iterator{ | |
| 606 | .dir = self, | |
| 607 | .index = 0, | |
| 608 | .end_index = 0, | |
| 609 | .first_iter = first_iter_start_value, | |
| 610 | .buf = undefined, | |
| 611 | .name_data = undefined, | |
| 612 | }, | |
| 613 | .wasi => return Iterator{ | |
| 614 | .dir = self, | |
| 615 | .cookie = std.os.wasi.DIRCOOKIE_START, | |
| 616 | .index = 0, | |
| 617 | .end_index = 0, | |
| 618 | .buf = undefined, | |
| 619 | }, | |
| 620 | else => @compileError("unimplemented"), | |
| 621 | } | |
| 622 | } | |
| 623 | ||
| 624 | pub const Walker = struct { | |
| 625 | stack: std.ArrayList(StackItem), | |
| 626 | name_buffer: std.ArrayList(u8), | |
| 627 | ||
| 628 | pub const WalkerEntry = struct { | |
| 629 | /// The containing directory. This can be used to operate directly on `basename` | |
| 630 | /// rather than `path`, avoiding `error.NameTooLong` for deeply nested paths. | |
| 631 | /// The directory remains open until `next` or `deinit` is called. | |
| 632 | dir: Dir, | |
| 633 | basename: []const u8, | |
| 634 | path: []const u8, | |
| 635 | kind: Dir.Entry.Kind, | |
| 636 | }; | |
| 637 | ||
| 638 | const StackItem = struct { | |
| 639 | iter: Dir.Iterator, | |
| 640 | dirname_len: usize, | |
| 641 | }; | |
| 642 | ||
| 643 | /// After each call to this function, and on deinit(), the memory returned | |
| 644 | /// from this function becomes invalid. A copy must be made in order to keep | |
| 645 | /// a reference to the path. | |
| 646 | pub fn next(self: *Walker) !?WalkerEntry { | |
| 647 | while (self.stack.items.len != 0) { | |
| 648 | // `top` and `containing` become invalid after appending to `self.stack` | |
| 649 | var top = &self.stack.items[self.stack.items.len - 1]; | |
| 650 | var containing = top; | |
| 651 | var dirname_len = top.dirname_len; | |
| 652 | if (top.iter.next() catch |err| { | |
| 653 | // If we get an error, then we want the user to be able to continue | |
| 654 | // walking if they want, which means that we need to pop the directory | |
| 655 | // that errored from the stack. Otherwise, all future `next` calls would | |
| 656 | // likely just fail with the same error. | |
| 657 | var item = self.stack.pop(); | |
| 658 | if (self.stack.items.len != 0) { | |
| 659 | item.iter.dir.close(); | |
| 660 | } | |
| 661 | return err; | |
| 662 | }) |base| { | |
| 663 | self.name_buffer.shrinkRetainingCapacity(dirname_len); | |
| 664 | if (self.name_buffer.items.len != 0) { | |
| 665 | try self.name_buffer.append(fs.path.sep); | |
| 666 | dirname_len += 1; | |
| 667 | } | |
| 668 | try self.name_buffer.appendSlice(base.name); | |
| 669 | if (base.kind == .directory) { | |
| 670 | var new_dir = top.iter.dir.openDir(base.name, .{ .iterate = true }) catch |err| switch (err) { | |
| 671 | error.NameTooLong => unreachable, // no path sep in base.name | |
| 672 | else => |e| return e, | |
| 673 | }; | |
| 674 | { | |
| 675 | errdefer new_dir.close(); | |
| 676 | try self.stack.append(StackItem{ | |
| 677 | .iter = new_dir.iterateAssumeFirstIteration(), | |
| 678 | .dirname_len = self.name_buffer.items.len, | |
| 679 | }); | |
| 680 | top = &self.stack.items[self.stack.items.len - 1]; | |
| 681 | containing = &self.stack.items[self.stack.items.len - 2]; | |
| 682 | } | |
| 683 | } | |
| 684 | return WalkerEntry{ | |
| 685 | .dir = containing.iter.dir, | |
| 686 | .basename = self.name_buffer.items[dirname_len..], | |
| 687 | .path = self.name_buffer.items, | |
| 688 | .kind = base.kind, | |
| 689 | }; | |
| 690 | } else { | |
| 691 | var item = self.stack.pop(); | |
| 692 | if (self.stack.items.len != 0) { | |
| 693 | item.iter.dir.close(); | |
| 694 | } | |
| 695 | } | |
| 696 | } | |
| 697 | return null; | |
| 698 | } | |
| 699 | ||
| 700 | pub fn deinit(self: *Walker) void { | |
| 701 | // Close any remaining directories except the initial one (which is always at index 0) | |
| 702 | if (self.stack.items.len > 1) { | |
| 703 | for (self.stack.items[1..]) |*item| { | |
| 704 | item.iter.dir.close(); | |
| 705 | } | |
| 706 | } | |
| 707 | self.stack.deinit(); | |
| 708 | self.name_buffer.deinit(); | |
| 709 | } | |
| 710 | }; | |
| 711 | ||
| 712 | /// Recursively iterates over a directory. | |
| 713 | /// `self` must have been opened with `OpenDirOptions{.iterate = true}`. | |
| 714 | /// Must call `Walker.deinit` when done. | |
| 715 | /// The order of returned file system entries is undefined. | |
| 716 | /// `self` will not be closed after walking it. | |
| 717 | pub fn walk(self: Dir, allocator: Allocator) !Walker { | |
| 718 | var name_buffer = std.ArrayList(u8).init(allocator); | |
| 719 | errdefer name_buffer.deinit(); | |
| 720 | ||
| 721 | var stack = std.ArrayList(Walker.StackItem).init(allocator); | |
| 722 | errdefer stack.deinit(); | |
| 723 | ||
| 724 | try stack.append(Walker.StackItem{ | |
| 725 | .iter = self.iterate(), | |
| 726 | .dirname_len = 0, | |
| 727 | }); | |
| 728 | ||
| 729 | return Walker{ | |
| 730 | .stack = stack, | |
| 731 | .name_buffer = name_buffer, | |
| 732 | }; | |
| 733 | } | |
| 734 | ||
| 735 | pub const OpenError = error{ | |
| 736 | FileNotFound, | |
| 737 | NotDir, | |
| 738 | InvalidHandle, | |
| 739 | AccessDenied, | |
| 740 | SymLinkLoop, | |
| 741 | ProcessFdQuotaExceeded, | |
| 742 | NameTooLong, | |
| 743 | SystemFdQuotaExceeded, | |
| 744 | NoDevice, | |
| 745 | SystemResources, | |
| 746 | InvalidUtf8, | |
| 747 | BadPathName, | |
| 748 | DeviceBusy, | |
| 749 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 750 | NetworkNotFound, | |
| 751 | } || posix.UnexpectedError; | |
| 752 | ||
| 753 | pub fn close(self: *Dir) void { | |
| 754 | if (fs.need_async_thread) { | |
| 755 | std.event.Loop.instance.?.close(self.fd); | |
| 756 | } else { | |
| 757 | posix.close(self.fd); | |
| 758 | } | |
| 759 | self.* = undefined; | |
| 760 | } | |
| 761 | ||
| 762 | /// Opens a file for reading or writing, without attempting to create a new file. | |
| 763 | /// To create a new file, see `createFile`. | |
| 764 | /// Call `File.close` to release the resource. | |
| 765 | /// Asserts that the path parameter has no null bytes. | |
| 766 | pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { | |
| 767 | if (builtin.os.tag == .windows) { | |
| 768 | const path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 769 | return self.openFileW(path_w.span(), flags); | |
| 770 | } | |
| 771 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 772 | return self.openFileWasi(sub_path, flags); | |
| 773 | } | |
| 774 | const path_c = try posix.toPosixPath(sub_path); | |
| 775 | return self.openFileZ(&path_c, flags); | |
| 776 | } | |
| 777 | ||
| 778 | /// Same as `openFile` but WASI only. | |
| 779 | pub fn openFileWasi(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { | |
| 780 | const w = std.os.wasi; | |
| 781 | var fdflags: w.fdflags_t = 0x0; | |
| 782 | var base: w.rights_t = 0x0; | |
| 783 | if (flags.isRead()) { | |
| 784 | base |= w.RIGHT.FD_READ | w.RIGHT.FD_TELL | w.RIGHT.FD_SEEK | w.RIGHT.FD_FILESTAT_GET; | |
| 785 | } | |
| 786 | if (flags.isWrite()) { | |
| 787 | fdflags |= w.FDFLAG.APPEND; | |
| 788 | base |= w.RIGHT.FD_WRITE | | |
| 789 | w.RIGHT.FD_TELL | | |
| 790 | w.RIGHT.FD_SEEK | | |
| 791 | w.RIGHT.FD_DATASYNC | | |
| 792 | w.RIGHT.FD_FDSTAT_SET_FLAGS | | |
| 793 | w.RIGHT.FD_SYNC | | |
| 794 | w.RIGHT.FD_ALLOCATE | | |
| 795 | w.RIGHT.FD_ADVISE | | |
| 796 | w.RIGHT.FD_FILESTAT_SET_TIMES | | |
| 797 | w.RIGHT.FD_FILESTAT_SET_SIZE; | |
| 798 | } | |
| 799 | const fd = try posix.openatWasi(self.fd, sub_path, 0x0, 0x0, fdflags, base, 0x0); | |
| 800 | return File{ .handle = fd }; | |
| 801 | } | |
| 802 | ||
| 803 | /// Same as `openFile` but the path parameter is null-terminated. | |
| 804 | pub fn openFileZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File { | |
| 805 | if (builtin.os.tag == .windows) { | |
| 806 | const path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path); | |
| 807 | return self.openFileW(path_w.span(), flags); | |
| 808 | } | |
| 809 | ||
| 810 | var os_flags: u32 = 0; | |
| 811 | if (@hasDecl(posix.O, "CLOEXEC")) os_flags = posix.O.CLOEXEC; | |
| 812 | ||
| 813 | // Use the O locking flags if the os supports them to acquire the lock | |
| 814 | // atomically. | |
| 815 | const has_flock_open_flags = @hasDecl(posix.O, "EXLOCK"); | |
| 816 | if (has_flock_open_flags) { | |
| 817 | // Note that the O.NONBLOCK flag is removed after the openat() call | |
| 818 | // is successful. | |
| 819 | const nonblocking_lock_flag: u32 = if (flags.lock_nonblocking) | |
| 820 | posix.O.NONBLOCK | |
| 821 | else | |
| 822 | 0; | |
| 823 | os_flags |= switch (flags.lock) { | |
| 824 | .none => @as(u32, 0), | |
| 825 | .shared => posix.O.SHLOCK | nonblocking_lock_flag, | |
| 826 | .exclusive => posix.O.EXLOCK | nonblocking_lock_flag, | |
| 827 | }; | |
| 828 | } | |
| 829 | if (@hasDecl(posix.O, "LARGEFILE")) { | |
| 830 | os_flags |= posix.O.LARGEFILE; | |
| 831 | } | |
| 832 | if (@hasDecl(posix.O, "NOCTTY") and !flags.allow_ctty) { | |
| 833 | os_flags |= posix.O.NOCTTY; | |
| 834 | } | |
| 835 | os_flags |= switch (flags.mode) { | |
| 836 | .read_only => @as(u32, posix.O.RDONLY), | |
| 837 | .write_only => @as(u32, posix.O.WRONLY), | |
| 838 | .read_write => @as(u32, posix.O.RDWR), | |
| 839 | }; | |
| 840 | const fd = if (flags.intended_io_mode != .blocking) | |
| 841 | try std.event.Loop.instance.?.openatZ(self.fd, sub_path, os_flags, 0) | |
| 842 | else | |
| 843 | try posix.openatZ(self.fd, sub_path, os_flags, 0); | |
| 844 | errdefer posix.close(fd); | |
| 845 | ||
| 846 | // WASI doesn't have posix.flock so we intetinally check OS prior to the inner if block | |
| 847 | // since it is not compiltime-known and we need to avoid undefined symbol in Wasm. | |
| 848 | if (@hasDecl(posix.system, "LOCK") and builtin.target.os.tag != .wasi) { | |
| 849 | if (!has_flock_open_flags and flags.lock != .none) { | |
| 850 | // TODO: integrate async I/O | |
| 851 | const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; | |
| 852 | try posix.flock(fd, switch (flags.lock) { | |
| 853 | .none => unreachable, | |
| 854 | .shared => posix.LOCK.SH | lock_nonblocking, | |
| 855 | .exclusive => posix.LOCK.EX | lock_nonblocking, | |
| 856 | }); | |
| 857 | } | |
| 858 | } | |
| 859 | ||
| 860 | if (has_flock_open_flags and flags.lock_nonblocking) { | |
| 861 | var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) { | |
| 862 | error.FileBusy => unreachable, | |
| 863 | error.Locked => unreachable, | |
| 864 | error.PermissionDenied => unreachable, | |
| 865 | error.DeadLock => unreachable, | |
| 866 | error.LockedRegionLimitExceeded => unreachable, | |
| 867 | else => |e| return e, | |
| 868 | }; | |
| 869 | fl_flags &= ~@as(usize, posix.O.NONBLOCK); | |
| 870 | _ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) { | |
| 871 | error.FileBusy => unreachable, | |
| 872 | error.Locked => unreachable, | |
| 873 | error.PermissionDenied => unreachable, | |
| 874 | error.DeadLock => unreachable, | |
| 875 | error.LockedRegionLimitExceeded => unreachable, | |
| 876 | else => |e| return e, | |
| 877 | }; | |
| 878 | } | |
| 879 | ||
| 880 | return File{ | |
| 881 | .handle = fd, | |
| 882 | .capable_io_mode = .blocking, | |
| 883 | .intended_io_mode = flags.intended_io_mode, | |
| 884 | }; | |
| 885 | } | |
| 886 | ||
| 887 | /// Same as `openFile` but Windows-only and the path parameter is | |
| 888 | /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. | |
| 889 | pub fn openFileW(self: Dir, sub_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { | |
| 890 | const w = std.os.windows; | |
| 891 | const file: File = .{ | |
| 892 | .handle = try w.OpenFile(sub_path_w, .{ | |
| 893 | .dir = self.fd, | |
| 894 | .access_mask = w.SYNCHRONIZE | | |
| 895 | (if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) | | |
| 896 | (if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0), | |
| 897 | .creation = w.FILE_OPEN, | |
| 898 | .io_mode = flags.intended_io_mode, | |
| 899 | }), | |
| 900 | .capable_io_mode = std.io.default_mode, | |
| 901 | .intended_io_mode = flags.intended_io_mode, | |
| 902 | }; | |
| 903 | errdefer file.close(); | |
| 904 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 905 | const range_off: w.LARGE_INTEGER = 0; | |
| 906 | const range_len: w.LARGE_INTEGER = 1; | |
| 907 | const exclusive = switch (flags.lock) { | |
| 908 | .none => return file, | |
| 909 | .shared => false, | |
| 910 | .exclusive => true, | |
| 911 | }; | |
| 912 | try w.LockFile( | |
| 913 | file.handle, | |
| 914 | null, | |
| 915 | null, | |
| 916 | null, | |
| 917 | &io, | |
| 918 | &range_off, | |
| 919 | &range_len, | |
| 920 | null, | |
| 921 | @intFromBool(flags.lock_nonblocking), | |
| 922 | @intFromBool(exclusive), | |
| 923 | ); | |
| 924 | return file; | |
| 925 | } | |
| 926 | ||
| 927 | /// Creates, opens, or overwrites a file with write access. | |
| 928 | /// Call `File.close` on the result when done. | |
| 929 | /// Asserts that the path parameter has no null bytes. | |
| 930 | pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { | |
| 931 | if (builtin.os.tag == .windows) { | |
| 932 | const path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 933 | return self.createFileW(path_w.span(), flags); | |
| 934 | } | |
| 935 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 936 | return self.createFileWasi(sub_path, flags); | |
| 937 | } | |
| 938 | const path_c = try posix.toPosixPath(sub_path); | |
| 939 | return self.createFileZ(&path_c, flags); | |
| 940 | } | |
| 941 | ||
| 942 | /// Same as `createFile` but WASI only. | |
| 943 | pub fn createFileWasi(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { | |
| 944 | const w = std.os.wasi; | |
| 945 | var oflags = w.O.CREAT; | |
| 946 | var base: w.rights_t = w.RIGHT.FD_WRITE | | |
| 947 | w.RIGHT.FD_DATASYNC | | |
| 948 | w.RIGHT.FD_SEEK | | |
| 949 | w.RIGHT.FD_TELL | | |
| 950 | w.RIGHT.FD_FDSTAT_SET_FLAGS | | |
| 951 | w.RIGHT.FD_SYNC | | |
| 952 | w.RIGHT.FD_ALLOCATE | | |
| 953 | w.RIGHT.FD_ADVISE | | |
| 954 | w.RIGHT.FD_FILESTAT_SET_TIMES | | |
| 955 | w.RIGHT.FD_FILESTAT_SET_SIZE | | |
| 956 | w.RIGHT.FD_FILESTAT_GET; | |
| 957 | if (flags.read) { | |
| 958 | base |= w.RIGHT.FD_READ; | |
| 959 | } | |
| 960 | if (flags.truncate) { | |
| 961 | oflags |= w.O.TRUNC; | |
| 962 | } | |
| 963 | if (flags.exclusive) { | |
| 964 | oflags |= w.O.EXCL; | |
| 965 | } | |
| 966 | const fd = try posix.openatWasi(self.fd, sub_path, 0x0, oflags, 0x0, base, 0x0); | |
| 967 | return File{ .handle = fd }; | |
| 968 | } | |
| 969 | ||
| 970 | /// Same as `createFile` but the path parameter is null-terminated. | |
| 971 | pub fn createFileZ(self: Dir, sub_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File { | |
| 972 | if (builtin.os.tag == .windows) { | |
| 973 | const path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); | |
| 974 | return self.createFileW(path_w.span(), flags); | |
| 975 | } | |
| 976 | ||
| 977 | // Use the O locking flags if the os supports them to acquire the lock | |
| 978 | // atomically. | |
| 979 | const has_flock_open_flags = @hasDecl(posix.O, "EXLOCK"); | |
| 980 | // Note that the O.NONBLOCK flag is removed after the openat() call | |
| 981 | // is successful. | |
| 982 | const nonblocking_lock_flag: u32 = if (has_flock_open_flags and flags.lock_nonblocking) | |
| 983 | posix.O.NONBLOCK | |
| 984 | else | |
| 985 | 0; | |
| 986 | const lock_flag: u32 = if (has_flock_open_flags) switch (flags.lock) { | |
| 987 | .none => @as(u32, 0), | |
| 988 | .shared => posix.O.SHLOCK | nonblocking_lock_flag, | |
| 989 | .exclusive => posix.O.EXLOCK | nonblocking_lock_flag, | |
| 990 | } else 0; | |
| 991 | ||
| 992 | const O_LARGEFILE = if (@hasDecl(posix.O, "LARGEFILE")) posix.O.LARGEFILE else 0; | |
| 993 | const os_flags = lock_flag | O_LARGEFILE | posix.O.CREAT | posix.O.CLOEXEC | | |
| 994 | (if (flags.truncate) @as(u32, posix.O.TRUNC) else 0) | | |
| 995 | (if (flags.read) @as(u32, posix.O.RDWR) else posix.O.WRONLY) | | |
| 996 | (if (flags.exclusive) @as(u32, posix.O.EXCL) else 0); | |
| 997 | const fd = if (flags.intended_io_mode != .blocking) | |
| 998 | try std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, os_flags, flags.mode) | |
| 999 | else | |
| 1000 | try posix.openatZ(self.fd, sub_path_c, os_flags, flags.mode); | |
| 1001 | errdefer posix.close(fd); | |
| 1002 | ||
| 1003 | // WASI doesn't have posix.flock so we intetinally check OS prior to the inner if block | |
| 1004 | // since it is not compiltime-known and we need to avoid undefined symbol in Wasm. | |
| 1005 | if (builtin.target.os.tag != .wasi) { | |
| 1006 | if (!has_flock_open_flags and flags.lock != .none) { | |
| 1007 | // TODO: integrate async I/O | |
| 1008 | const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; | |
| 1009 | try posix.flock(fd, switch (flags.lock) { | |
| 1010 | .none => unreachable, | |
| 1011 | .shared => posix.LOCK.SH | lock_nonblocking, | |
| 1012 | .exclusive => posix.LOCK.EX | lock_nonblocking, | |
| 1013 | }); | |
| 1014 | } | |
| 1015 | } | |
| 1016 | ||
| 1017 | if (has_flock_open_flags and flags.lock_nonblocking) { | |
| 1018 | var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) { | |
| 1019 | error.FileBusy => unreachable, | |
| 1020 | error.Locked => unreachable, | |
| 1021 | error.PermissionDenied => unreachable, | |
| 1022 | error.DeadLock => unreachable, | |
| 1023 | error.LockedRegionLimitExceeded => unreachable, | |
| 1024 | else => |e| return e, | |
| 1025 | }; | |
| 1026 | fl_flags &= ~@as(usize, posix.O.NONBLOCK); | |
| 1027 | _ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) { | |
| 1028 | error.FileBusy => unreachable, | |
| 1029 | error.Locked => unreachable, | |
| 1030 | error.PermissionDenied => unreachable, | |
| 1031 | error.DeadLock => unreachable, | |
| 1032 | error.LockedRegionLimitExceeded => unreachable, | |
| 1033 | else => |e| return e, | |
| 1034 | }; | |
| 1035 | } | |
| 1036 | ||
| 1037 | return File{ | |
| 1038 | .handle = fd, | |
| 1039 | .capable_io_mode = .blocking, | |
| 1040 | .intended_io_mode = flags.intended_io_mode, | |
| 1041 | }; | |
| 1042 | } | |
| 1043 | ||
| 1044 | /// Same as `createFile` but Windows-only and the path parameter is | |
| 1045 | /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. | |
| 1046 | pub fn createFileW(self: Dir, sub_path_w: []const u16, flags: File.CreateFlags) File.OpenError!File { | |
| 1047 | const w = std.os.windows; | |
| 1048 | const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0; | |
| 1049 | const file: File = .{ | |
| 1050 | .handle = try w.OpenFile(sub_path_w, .{ | |
| 1051 | .dir = self.fd, | |
| 1052 | .access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag, | |
| 1053 | .creation = if (flags.exclusive) | |
| 1054 | @as(u32, w.FILE_CREATE) | |
| 1055 | else if (flags.truncate) | |
| 1056 | @as(u32, w.FILE_OVERWRITE_IF) | |
| 1057 | else | |
| 1058 | @as(u32, w.FILE_OPEN_IF), | |
| 1059 | .io_mode = flags.intended_io_mode, | |
| 1060 | }), | |
| 1061 | .capable_io_mode = std.io.default_mode, | |
| 1062 | .intended_io_mode = flags.intended_io_mode, | |
| 1063 | }; | |
| 1064 | errdefer file.close(); | |
| 1065 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 1066 | const range_off: w.LARGE_INTEGER = 0; | |
| 1067 | const range_len: w.LARGE_INTEGER = 1; | |
| 1068 | const exclusive = switch (flags.lock) { | |
| 1069 | .none => return file, | |
| 1070 | .shared => false, | |
| 1071 | .exclusive => true, | |
| 1072 | }; | |
| 1073 | try w.LockFile( | |
| 1074 | file.handle, | |
| 1075 | null, | |
| 1076 | null, | |
| 1077 | null, | |
| 1078 | &io, | |
| 1079 | &range_off, | |
| 1080 | &range_len, | |
| 1081 | null, | |
| 1082 | @intFromBool(flags.lock_nonblocking), | |
| 1083 | @intFromBool(exclusive), | |
| 1084 | ); | |
| 1085 | return file; | |
| 1086 | } | |
| 1087 | ||
| 1088 | /// Creates a single directory with a relative or absolute path. | |
| 1089 | /// To create multiple directories to make an entire path, see `makePath`. | |
| 1090 | /// To operate on only absolute paths, see `makeDirAbsolute`. | |
| 1091 | pub fn makeDir(self: Dir, sub_path: []const u8) !void { | |
| 1092 | try posix.mkdirat(self.fd, sub_path, default_mode); | |
| 1093 | } | |
| 1094 | ||
| 1095 | /// Creates a single directory with a relative or absolute null-terminated UTF-8-encoded path. | |
| 1096 | /// To create multiple directories to make an entire path, see `makePath`. | |
| 1097 | /// To operate on only absolute paths, see `makeDirAbsoluteZ`. | |
| 1098 | pub fn makeDirZ(self: Dir, sub_path: [*:0]const u8) !void { | |
| 1099 | try posix.mkdiratZ(self.fd, sub_path, default_mode); | |
| 1100 | } | |
| 1101 | ||
| 1102 | /// Creates a single directory with a relative or absolute null-terminated WTF-16-encoded path. | |
| 1103 | /// To create multiple directories to make an entire path, see `makePath`. | |
| 1104 | /// To operate on only absolute paths, see `makeDirAbsoluteW`. | |
| 1105 | pub fn makeDirW(self: Dir, sub_path: [*:0]const u16) !void { | |
| 1106 | try posix.mkdiratW(self.fd, sub_path, default_mode); | |
| 1107 | } | |
| 1108 | ||
| 1109 | /// Calls makeDir iteratively to make an entire path | |
| 1110 | /// (i.e. creating any parent directories that do not exist). | |
| 1111 | /// Returns success if the path already exists and is a directory. | |
| 1112 | /// This function is not atomic, and if it returns an error, the file system may | |
| 1113 | /// have been modified regardless. | |
| 1114 | pub fn makePath(self: Dir, sub_path: []const u8) !void { | |
| 1115 | var it = try fs.path.componentIterator(sub_path); | |
| 1116 | var component = it.last() orelse return; | |
| 1117 | while (true) { | |
| 1118 | self.makeDir(component.path) catch |err| switch (err) { | |
| 1119 | error.PathAlreadyExists => { | |
| 1120 | // TODO stat the file and return an error if it's not a directory | |
| 1121 | // this is important because otherwise a dangling symlink | |
| 1122 | // could cause an infinite loop | |
| 1123 | }, | |
| 1124 | error.FileNotFound => |e| { | |
| 1125 | component = it.previous() orelse return e; | |
| 1126 | continue; | |
| 1127 | }, | |
| 1128 | else => |e| return e, | |
| 1129 | }; | |
| 1130 | component = it.next() orelse return; | |
| 1131 | } | |
| 1132 | } | |
| 1133 | ||
| 1134 | /// Calls makeOpenDirAccessMaskW iteratively to make an entire path | |
| 1135 | /// (i.e. creating any parent directories that do not exist). | |
| 1136 | /// Opens the dir if the path already exists and is a directory. | |
| 1137 | /// This function is not atomic, and if it returns an error, the file system may | |
| 1138 | /// have been modified regardless. | |
| 1139 | fn makeOpenPathAccessMaskW(self: Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) OpenError!Dir { | |
| 1140 | const w = std.os.windows; | |
| 1141 | var it = try fs.path.componentIterator(sub_path); | |
| 1142 | // If there are no components in the path, then create a dummy component with the full path. | |
| 1143 | var component = it.last() orelse fs.path.NativeUtf8ComponentIterator.Component{ | |
| 1144 | .name = "", | |
| 1145 | .path = sub_path, | |
| 1146 | }; | |
| 1147 | ||
| 1148 | while (true) { | |
| 1149 | const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path); | |
| 1150 | const is_last = it.peekNext() == null; | |
| 1151 | var result = self.makeOpenDirAccessMaskW(sub_path_w.span().ptr, access_mask, .{ | |
| 1152 | .no_follow = no_follow, | |
| 1153 | .create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE, | |
| 1154 | }) catch |err| switch (err) { | |
| 1155 | error.FileNotFound => |e| { | |
| 1156 | component = it.previous() orelse return e; | |
| 1157 | continue; | |
| 1158 | }, | |
| 1159 | else => |e| return e, | |
| 1160 | }; | |
| 1161 | ||
| 1162 | component = it.next() orelse return result; | |
| 1163 | // Don't leak the intermediate file handles | |
| 1164 | result.close(); | |
| 1165 | } | |
| 1166 | } | |
| 1167 | ||
| 1168 | /// This function performs `makePath`, followed by `openDir`. | |
| 1169 | /// If supported by the OS, this operation is atomic. It is not atomic on | |
| 1170 | /// all operating systems. | |
| 1171 | /// On Windows, this function performs `makeOpenPathAccessMaskW`. | |
| 1172 | pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !Dir { | |
| 1173 | return switch (builtin.os.tag) { | |
| 1174 | .windows => { | |
| 1175 | const w = std.os.windows; | |
| 1176 | const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | | |
| 1177 | w.SYNCHRONIZE | w.FILE_TRAVERSE | | |
| 1178 | (if (open_dir_options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0)); | |
| 1179 | ||
| 1180 | return self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow); | |
| 1181 | }, | |
| 1182 | else => { | |
| 1183 | return self.openDir(sub_path, open_dir_options) catch |err| switch (err) { | |
| 1184 | error.FileNotFound => { | |
| 1185 | try self.makePath(sub_path); | |
| 1186 | return self.openDir(sub_path, open_dir_options); | |
| 1187 | }, | |
| 1188 | else => |e| return e, | |
| 1189 | }; | |
| 1190 | }, | |
| 1191 | }; | |
| 1192 | } | |
| 1193 | ||
| 1194 | /// This function returns the canonicalized absolute pathname of | |
| 1195 | /// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this | |
| 1196 | /// `Dir` handle and returns the canonicalized absolute pathname of `pathname` | |
| 1197 | /// argument. | |
| 1198 | /// This function is not universally supported by all platforms. | |
| 1199 | /// Currently supported hosts are: Linux, macOS, and Windows. | |
| 1200 | /// See also `Dir.realpathZ`, `Dir.realpathW`, and `Dir.realpathAlloc`. | |
| 1201 | pub fn realpath(self: Dir, pathname: []const u8, out_buffer: []u8) ![]u8 { | |
| 1202 | if (builtin.os.tag == .wasi) { | |
| 1203 | @compileError("realpath is not available on WASI"); | |
| 1204 | } | |
| 1205 | if (builtin.os.tag == .windows) { | |
| 1206 | const pathname_w = try std.os.windows.sliceToPrefixedFileW(self.fd, pathname); | |
| 1207 | return self.realpathW(pathname_w.span(), out_buffer); | |
| 1208 | } | |
| 1209 | const pathname_c = try posix.toPosixPath(pathname); | |
| 1210 | return self.realpathZ(&pathname_c, out_buffer); | |
| 1211 | } | |
| 1212 | ||
| 1213 | /// Same as `Dir.realpath` except `pathname` is null-terminated. | |
| 1214 | /// See also `Dir.realpath`, `realpathZ`. | |
| 1215 | pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) ![]u8 { | |
| 1216 | if (builtin.os.tag == .windows) { | |
| 1217 | const pathname_w = try posix.windows.cStrToPrefixedFileW(self.fd, pathname); | |
| 1218 | return self.realpathW(pathname_w.span(), out_buffer); | |
| 1219 | } | |
| 1220 | ||
| 1221 | const flags = if (builtin.os.tag == .linux) | |
| 1222 | posix.O.PATH | posix.O.NONBLOCK | posix.O.CLOEXEC | |
| 1223 | else | |
| 1224 | posix.O.NONBLOCK | posix.O.CLOEXEC; | |
| 1225 | const fd = posix.openatZ(self.fd, pathname, flags, 0) catch |err| switch (err) { | |
| 1226 | error.FileLocksNotSupported => unreachable, | |
| 1227 | else => |e| return e, | |
| 1228 | }; | |
| 1229 | defer posix.close(fd); | |
| 1230 | ||
| 1231 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1232 | // have a variant that takes an arbitrary-size buffer. | |
| 1233 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1234 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1235 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1236 | // anyway. | |
| 1237 | var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; | |
| 1238 | const out_path = try posix.getFdPath(fd, &buffer); | |
| 1239 | ||
| 1240 | if (out_path.len > out_buffer.len) { | |
| 1241 | return error.NameTooLong; | |
| 1242 | } | |
| 1243 | ||
| 1244 | const result = out_buffer[0..out_path.len]; | |
| 1245 | @memcpy(result, out_path); | |
| 1246 | return result; | |
| 1247 | } | |
| 1248 | ||
| 1249 | /// Windows-only. Same as `Dir.realpath` except `pathname` is WTF16 encoded. | |
| 1250 | /// See also `Dir.realpath`, `realpathW`. | |
| 1251 | pub fn realpathW(self: Dir, pathname: []const u16, out_buffer: []u8) ![]u8 { | |
| 1252 | const w = std.os.windows; | |
| 1253 | ||
| 1254 | const access_mask = w.GENERIC_READ | w.SYNCHRONIZE; | |
| 1255 | const share_access = w.FILE_SHARE_READ; | |
| 1256 | const creation = w.FILE_OPEN; | |
| 1257 | const h_file = blk: { | |
| 1258 | const res = w.OpenFile(pathname, .{ | |
| 1259 | .dir = self.fd, | |
| 1260 | .access_mask = access_mask, | |
| 1261 | .share_access = share_access, | |
| 1262 | .creation = creation, | |
| 1263 | .io_mode = .blocking, | |
| 1264 | .filter = .any, | |
| 1265 | }) catch |err| switch (err) { | |
| 1266 | error.WouldBlock => unreachable, | |
| 1267 | else => |e| return e, | |
| 1268 | }; | |
| 1269 | break :blk res; | |
| 1270 | }; | |
| 1271 | defer w.CloseHandle(h_file); | |
| 1272 | ||
| 1273 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1274 | // have a variant that takes an arbitrary-size buffer. | |
| 1275 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1276 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1277 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1278 | // anyway. | |
| 1279 | var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; | |
| 1280 | const out_path = try posix.getFdPath(h_file, &buffer); | |
| 1281 | ||
| 1282 | if (out_path.len > out_buffer.len) { | |
| 1283 | return error.NameTooLong; | |
| 1284 | } | |
| 1285 | ||
| 1286 | const result = out_buffer[0..out_path.len]; | |
| 1287 | @memcpy(result, out_path); | |
| 1288 | return result; | |
| 1289 | } | |
| 1290 | ||
| 1291 | /// Same as `Dir.realpath` except caller must free the returned memory. | |
| 1292 | /// See also `Dir.realpath`. | |
| 1293 | pub fn realpathAlloc(self: Dir, allocator: Allocator, pathname: []const u8) ![]u8 { | |
| 1294 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1295 | // have a variant that takes an arbitrary-size buffer. | |
| 1296 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1297 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1298 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1299 | // anyway. | |
| 1300 | var buf: [fs.MAX_PATH_BYTES]u8 = undefined; | |
| 1301 | return allocator.dupe(u8, try self.realpath(pathname, buf[0..])); | |
| 1302 | } | |
| 1303 | ||
| 1304 | /// Changes the current working directory to the open directory handle. | |
| 1305 | /// This modifies global state and can have surprising effects in multi- | |
| 1306 | /// threaded applications. Most applications and especially libraries should | |
| 1307 | /// not call this function as a general rule, however it can have use cases | |
| 1308 | /// in, for example, implementing a shell, or child process execution. | |
| 1309 | /// Not all targets support this. For example, WASI does not have the concept | |
| 1310 | /// of a current working directory. | |
| 1311 | pub fn setAsCwd(self: Dir) !void { | |
| 1312 | if (builtin.os.tag == .wasi) { | |
| 1313 | @compileError("changing cwd is not currently possible in WASI"); | |
| 1314 | } | |
| 1315 | if (builtin.os.tag == .windows) { | |
| 1316 | var dir_path_buffer: [std.os.windows.PATH_MAX_WIDE]u16 = undefined; | |
| 1317 | const dir_path = try std.os.windows.GetFinalPathNameByHandle(self.fd, .{}, &dir_path_buffer); | |
| 1318 | if (builtin.link_libc) { | |
| 1319 | return posix.chdirW(dir_path); | |
| 1320 | } | |
| 1321 | return std.os.windows.SetCurrentDirectory(dir_path); | |
| 1322 | } | |
| 1323 | try posix.fchdir(self.fd); | |
| 1324 | } | |
| 1325 | ||
| 1326 | pub const OpenDirOptions = struct { | |
| 1327 | /// `true` means the opened directory can be used as the `Dir` parameter | |
| 1328 | /// for functions which operate based on an open directory handle. When `false`, | |
| 1329 | /// such operations are Illegal Behavior. | |
| 1330 | access_sub_paths: bool = true, | |
| 1331 | ||
| 1332 | /// `true` means the opened directory can be scanned for the files and sub-directories | |
| 1333 | /// of the result. It means the `iterate` function can be called. | |
| 1334 | iterate: bool = false, | |
| 1335 | ||
| 1336 | /// `true` means it won't dereference the symlinks. | |
| 1337 | no_follow: bool = false, | |
| 1338 | }; | |
| 1339 | ||
| 1340 | /// Opens a directory at the given path. The directory is a system resource that remains | |
| 1341 | /// open until `close` is called on the result. | |
| 1342 | /// The directory cannot be iterated unless the `iterate` option is set to `true`. | |
| 1343 | /// | |
| 1344 | /// Asserts that the path parameter has no null bytes. | |
| 1345 | pub fn openDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { | |
| 1346 | if (builtin.os.tag == .windows) { | |
| 1347 | const sub_path_w = try posix.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1348 | return self.openDirW(sub_path_w.span().ptr, args); | |
| 1349 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1350 | return self.openDirWasi(sub_path, args); | |
| 1351 | } else { | |
| 1352 | const sub_path_c = try posix.toPosixPath(sub_path); | |
| 1353 | return self.openDirZ(&sub_path_c, args); | |
| 1354 | } | |
| 1355 | } | |
| 1356 | ||
| 1357 | /// Same as `openDir` except only WASI. | |
| 1358 | pub fn openDirWasi(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { | |
| 1359 | const w = std.os.wasi; | |
| 1360 | var base: w.rights_t = w.RIGHT.FD_FILESTAT_GET | w.RIGHT.FD_FDSTAT_SET_FLAGS | w.RIGHT.FD_FILESTAT_SET_TIMES; | |
| 1361 | if (args.access_sub_paths) { | |
| 1362 | base |= w.RIGHT.FD_READDIR | | |
| 1363 | w.RIGHT.PATH_CREATE_DIRECTORY | | |
| 1364 | w.RIGHT.PATH_CREATE_FILE | | |
| 1365 | w.RIGHT.PATH_LINK_SOURCE | | |
| 1366 | w.RIGHT.PATH_LINK_TARGET | | |
| 1367 | w.RIGHT.PATH_OPEN | | |
| 1368 | w.RIGHT.PATH_READLINK | | |
| 1369 | w.RIGHT.PATH_RENAME_SOURCE | | |
| 1370 | w.RIGHT.PATH_RENAME_TARGET | | |
| 1371 | w.RIGHT.PATH_FILESTAT_GET | | |
| 1372 | w.RIGHT.PATH_FILESTAT_SET_SIZE | | |
| 1373 | w.RIGHT.PATH_FILESTAT_SET_TIMES | | |
| 1374 | w.RIGHT.PATH_SYMLINK | | |
| 1375 | w.RIGHT.PATH_REMOVE_DIRECTORY | | |
| 1376 | w.RIGHT.PATH_UNLINK_FILE; | |
| 1377 | } | |
| 1378 | const symlink_flags: w.lookupflags_t = if (args.no_follow) 0x0 else w.LOOKUP_SYMLINK_FOLLOW; | |
| 1379 | // TODO do we really need all the rights here? | |
| 1380 | const inheriting: w.rights_t = w.RIGHT.ALL ^ w.RIGHT.SOCK_SHUTDOWN; | |
| 1381 | ||
| 1382 | const result = posix.openatWasi( | |
| 1383 | self.fd, | |
| 1384 | sub_path, | |
| 1385 | symlink_flags, | |
| 1386 | w.O.DIRECTORY, | |
| 1387 | 0x0, | |
| 1388 | base, | |
| 1389 | inheriting, | |
| 1390 | ); | |
| 1391 | const fd = result catch |err| switch (err) { | |
| 1392 | error.FileTooBig => unreachable, // can't happen for directories | |
| 1393 | error.IsDir => unreachable, // we're providing O.DIRECTORY | |
| 1394 | error.NoSpaceLeft => unreachable, // not providing O.CREAT | |
| 1395 | error.PathAlreadyExists => unreachable, // not providing O.CREAT | |
| 1396 | error.FileLocksNotSupported => unreachable, // locking folders is not supported | |
| 1397 | error.WouldBlock => unreachable, // can't happen for directories | |
| 1398 | error.FileBusy => unreachable, // can't happen for directories | |
| 1399 | else => |e| return e, | |
| 1400 | }; | |
| 1401 | return Dir{ .fd = fd }; | |
| 1402 | } | |
| 1403 | ||
| 1404 | /// Same as `openDir` except the parameter is null-terminated. | |
| 1405 | pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenDirOptions) OpenError!Dir { | |
| 1406 | if (builtin.os.tag == .windows) { | |
| 1407 | const sub_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); | |
| 1408 | return self.openDirW(sub_path_w.span().ptr, args); | |
| 1409 | } | |
| 1410 | const symlink_flags: u32 = if (args.no_follow) posix.O.NOFOLLOW else 0x0; | |
| 1411 | if (!args.iterate) { | |
| 1412 | const O_PATH = if (@hasDecl(posix.O, "PATH")) posix.O.PATH else 0; | |
| 1413 | return self.openDirFlagsZ(sub_path_c, posix.O.DIRECTORY | posix.O.RDONLY | posix.O.CLOEXEC | O_PATH | symlink_flags); | |
| 1414 | } else { | |
| 1415 | return self.openDirFlagsZ(sub_path_c, posix.O.DIRECTORY | posix.O.RDONLY | posix.O.CLOEXEC | symlink_flags); | |
| 1416 | } | |
| 1417 | } | |
| 1418 | ||
| 1419 | /// Same as `openDir` except the path parameter is WTF-16 encoded, NT-prefixed. | |
| 1420 | /// This function asserts the target OS is Windows. | |
| 1421 | pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenDirOptions) OpenError!Dir { | |
| 1422 | const w = std.os.windows; | |
| 1423 | // TODO remove some of these flags if args.access_sub_paths is false | |
| 1424 | const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | | |
| 1425 | w.SYNCHRONIZE | w.FILE_TRAVERSE; | |
| 1426 | const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; | |
| 1427 | const dir = try self.makeOpenDirAccessMaskW(sub_path_w, flags, .{ | |
| 1428 | .no_follow = args.no_follow, | |
| 1429 | .create_disposition = w.FILE_OPEN, | |
| 1430 | }); | |
| 1431 | return dir; | |
| 1432 | } | |
| 1433 | ||
| 1434 | /// `flags` must contain `posix.O.DIRECTORY`. | |
| 1435 | fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: u32) OpenError!Dir { | |
| 1436 | const result = if (fs.need_async_thread) | |
| 1437 | std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, flags, 0) | |
| 1438 | else | |
| 1439 | posix.openatZ(self.fd, sub_path_c, flags, 0); | |
| 1440 | const fd = result catch |err| switch (err) { | |
| 1441 | error.FileTooBig => unreachable, // can't happen for directories | |
| 1442 | error.IsDir => unreachable, // we're providing O.DIRECTORY | |
| 1443 | error.NoSpaceLeft => unreachable, // not providing O.CREAT | |
| 1444 | error.PathAlreadyExists => unreachable, // not providing O.CREAT | |
| 1445 | error.FileLocksNotSupported => unreachable, // locking folders is not supported | |
| 1446 | error.WouldBlock => unreachable, // can't happen for directories | |
| 1447 | error.FileBusy => unreachable, // can't happen for directories | |
| 1448 | else => |e| return e, | |
| 1449 | }; | |
| 1450 | return Dir{ .fd = fd }; | |
| 1451 | } | |
| 1452 | ||
| 1453 | const MakeOpenDirAccessMaskWOptions = struct { | |
| 1454 | no_follow: bool, | |
| 1455 | create_disposition: u32, | |
| 1456 | }; | |
| 1457 | ||
| 1458 | fn makeOpenDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) OpenError!Dir { | |
| 1459 | const w = std.os.windows; | |
| 1460 | ||
| 1461 | var result = Dir{ | |
| 1462 | .fd = undefined, | |
| 1463 | }; | |
| 1464 | ||
| 1465 | const path_len_bytes = @as(u16, @intCast(mem.sliceTo(sub_path_w, 0).len * 2)); | |
| 1466 | var nt_name = w.UNICODE_STRING{ | |
| 1467 | .Length = path_len_bytes, | |
| 1468 | .MaximumLength = path_len_bytes, | |
| 1469 | .Buffer = @constCast(sub_path_w), | |
| 1470 | }; | |
| 1471 | var attr = w.OBJECT_ATTRIBUTES{ | |
| 1472 | .Length = @sizeOf(w.OBJECT_ATTRIBUTES), | |
| 1473 | .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, | |
| 1474 | .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. | |
| 1475 | .ObjectName = &nt_name, | |
| 1476 | .SecurityDescriptor = null, | |
| 1477 | .SecurityQualityOfService = null, | |
| 1478 | }; | |
| 1479 | const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; | |
| 1480 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 1481 | const rc = w.ntdll.NtCreateFile( | |
| 1482 | &result.fd, | |
| 1483 | access_mask, | |
| 1484 | &attr, | |
| 1485 | &io, | |
| 1486 | null, | |
| 1487 | w.FILE_ATTRIBUTE_NORMAL, | |
| 1488 | w.FILE_SHARE_READ | w.FILE_SHARE_WRITE, | |
| 1489 | flags.create_disposition, | |
| 1490 | w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, | |
| 1491 | null, | |
| 1492 | 0, | |
| 1493 | ); | |
| 1494 | ||
| 1495 | switch (rc) { | |
| 1496 | .SUCCESS => return result, | |
| 1497 | .OBJECT_NAME_INVALID => return error.BadPathName, | |
| 1498 | .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, | |
| 1499 | .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, | |
| 1500 | .NOT_A_DIRECTORY => return error.NotDir, | |
| 1501 | // This can happen if the directory has 'List folder contents' permission set to 'Deny' | |
| 1502 | // and the directory is trying to be opened for iteration. | |
| 1503 | .ACCESS_DENIED => return error.AccessDenied, | |
| 1504 | .INVALID_PARAMETER => unreachable, | |
| 1505 | else => return w.unexpectedStatus(rc), | |
| 1506 | } | |
| 1507 | } | |
| 1508 | ||
| 1509 | pub const DeleteFileError = posix.UnlinkError; | |
| 1510 | ||
| 1511 | /// Delete a file name and possibly the file it refers to, based on an open directory handle. | |
| 1512 | /// Asserts that the path parameter has no null bytes. | |
| 1513 | pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void { | |
| 1514 | if (builtin.os.tag == .windows) { | |
| 1515 | const sub_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1516 | return self.deleteFileW(sub_path_w.span()); | |
| 1517 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1518 | posix.unlinkat(self.fd, sub_path, 0) catch |err| switch (err) { | |
| 1519 | error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR | |
| 1520 | else => |e| return e, | |
| 1521 | }; | |
| 1522 | } else { | |
| 1523 | const sub_path_c = try posix.toPosixPath(sub_path); | |
| 1524 | return self.deleteFileZ(&sub_path_c); | |
| 1525 | } | |
| 1526 | } | |
| 1527 | ||
| 1528 | /// Same as `deleteFile` except the parameter is null-terminated. | |
| 1529 | pub fn deleteFileZ(self: Dir, sub_path_c: [*:0]const u8) DeleteFileError!void { | |
| 1530 | posix.unlinkatZ(self.fd, sub_path_c, 0) catch |err| switch (err) { | |
| 1531 | error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR | |
| 1532 | error.AccessDenied => |e| switch (builtin.os.tag) { | |
| 1533 | // non-Linux POSIX systems return EPERM when trying to delete a directory, so | |
| 1534 | // we need to handle that case specifically and translate the error | |
| 1535 | .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => { | |
| 1536 | // Don't follow symlinks to match unlinkat (which acts on symlinks rather than follows them) | |
| 1537 | const fstat = posix.fstatatZ(self.fd, sub_path_c, posix.AT.SYMLINK_NOFOLLOW) catch return e; | |
| 1538 | const is_dir = fstat.mode & posix.S.IFMT == posix.S.IFDIR; | |
| 1539 | return if (is_dir) error.IsDir else e; | |
| 1540 | }, | |
| 1541 | else => return e, | |
| 1542 | }, | |
| 1543 | else => |e| return e, | |
| 1544 | }; | |
| 1545 | } | |
| 1546 | ||
| 1547 | /// Same as `deleteFile` except the parameter is WTF-16 encoded. | |
| 1548 | pub fn deleteFileW(self: Dir, sub_path_w: []const u16) DeleteFileError!void { | |
| 1549 | posix.unlinkatW(self.fd, sub_path_w, 0) catch |err| switch (err) { | |
| 1550 | error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR | |
| 1551 | else => |e| return e, | |
| 1552 | }; | |
| 1553 | } | |
| 1554 | ||
| 1555 | pub const DeleteDirError = error{ | |
| 1556 | DirNotEmpty, | |
| 1557 | FileNotFound, | |
| 1558 | AccessDenied, | |
| 1559 | FileBusy, | |
| 1560 | FileSystem, | |
| 1561 | SymLinkLoop, | |
| 1562 | NameTooLong, | |
| 1563 | NotDir, | |
| 1564 | SystemResources, | |
| 1565 | ReadOnlyFileSystem, | |
| 1566 | InvalidUtf8, | |
| 1567 | BadPathName, | |
| 1568 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 1569 | NetworkNotFound, | |
| 1570 | Unexpected, | |
| 1571 | }; | |
| 1572 | ||
| 1573 | /// Returns `error.DirNotEmpty` if the directory is not empty. | |
| 1574 | /// To delete a directory recursively, see `deleteTree`. | |
| 1575 | /// Asserts that the path parameter has no null bytes. | |
| 1576 | pub fn deleteDir(self: Dir, sub_path: []const u8) DeleteDirError!void { | |
| 1577 | if (builtin.os.tag == .windows) { | |
| 1578 | const sub_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1579 | return self.deleteDirW(sub_path_w.span()); | |
| 1580 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1581 | posix.unlinkat(self.fd, sub_path, posix.AT.REMOVEDIR) catch |err| switch (err) { | |
| 1582 | error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR | |
| 1583 | else => |e| return e, | |
| 1584 | }; | |
| 1585 | } else { | |
| 1586 | const sub_path_c = try posix.toPosixPath(sub_path); | |
| 1587 | return self.deleteDirZ(&sub_path_c); | |
| 1588 | } | |
| 1589 | } | |
| 1590 | ||
| 1591 | /// Same as `deleteDir` except the parameter is null-terminated. | |
| 1592 | pub fn deleteDirZ(self: Dir, sub_path_c: [*:0]const u8) DeleteDirError!void { | |
| 1593 | posix.unlinkatZ(self.fd, sub_path_c, posix.AT.REMOVEDIR) catch |err| switch (err) { | |
| 1594 | error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR | |
| 1595 | else => |e| return e, | |
| 1596 | }; | |
| 1597 | } | |
| 1598 | ||
| 1599 | /// Same as `deleteDir` except the parameter is UTF16LE, NT prefixed. | |
| 1600 | /// This function is Windows-only. | |
| 1601 | pub fn deleteDirW(self: Dir, sub_path_w: []const u16) DeleteDirError!void { | |
| 1602 | posix.unlinkatW(self.fd, sub_path_w, posix.AT.REMOVEDIR) catch |err| switch (err) { | |
| 1603 | error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR | |
| 1604 | else => |e| return e, | |
| 1605 | }; | |
| 1606 | } | |
| 1607 | ||
| 1608 | pub const RenameError = posix.RenameError; | |
| 1609 | ||
| 1610 | /// Change the name or location of a file or directory. | |
| 1611 | /// If new_sub_path already exists, it will be replaced. | |
| 1612 | /// Renaming a file over an existing directory or a directory | |
| 1613 | /// over an existing file will fail with `error.IsDir` or `error.NotDir` | |
| 1614 | pub fn rename(self: Dir, old_sub_path: []const u8, new_sub_path: []const u8) RenameError!void { | |
| 1615 | return posix.renameat(self.fd, old_sub_path, self.fd, new_sub_path); | |
| 1616 | } | |
| 1617 | ||
| 1618 | /// Same as `rename` except the parameters are null-terminated. | |
| 1619 | pub fn renameZ(self: Dir, old_sub_path_z: [*:0]const u8, new_sub_path_z: [*:0]const u8) RenameError!void { | |
| 1620 | return posix.renameatZ(self.fd, old_sub_path_z, self.fd, new_sub_path_z); | |
| 1621 | } | |
| 1622 | ||
| 1623 | /// Same as `rename` except the parameters are UTF16LE, NT prefixed. | |
| 1624 | /// This function is Windows-only. | |
| 1625 | pub fn renameW(self: Dir, old_sub_path_w: []const u16, new_sub_path_w: []const u16) RenameError!void { | |
| 1626 | return posix.renameatW(self.fd, old_sub_path_w, self.fd, new_sub_path_w); | |
| 1627 | } | |
| 1628 | ||
| 1629 | /// Use with `Dir.symLink` and `symLinkAbsolute` to specify whether the symlink | |
| 1630 | /// will point to a file or a directory. This value is ignored on all hosts | |
| 1631 | /// except Windows where creating symlinks to different resource types, requires | |
| 1632 | /// different flags. By default, `symLinkAbsolute` is assumed to point to a file. | |
| 1633 | pub const SymLinkFlags = struct { | |
| 1634 | is_directory: bool = false, | |
| 1635 | }; | |
| 1636 | ||
| 1637 | /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. | |
| 1638 | /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent | |
| 1639 | /// one; the latter case is known as a dangling link. | |
| 1640 | /// If `sym_link_path` exists, it will not be overwritten. | |
| 1641 | pub fn symLink( | |
| 1642 | self: Dir, | |
| 1643 | target_path: []const u8, | |
| 1644 | sym_link_path: []const u8, | |
| 1645 | flags: SymLinkFlags, | |
| 1646 | ) !void { | |
| 1647 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1648 | return self.symLinkWasi(target_path, sym_link_path, flags); | |
| 1649 | } | |
| 1650 | if (builtin.os.tag == .windows) { | |
| 1651 | // Target path does not use sliceToPrefixedFileW because certain paths | |
| 1652 | // are handled differently when creating a symlink than they would be | |
| 1653 | // when converting to an NT namespaced path. CreateSymbolicLink in | |
| 1654 | // symLinkW will handle the necessary conversion. | |
| 1655 | var target_path_w: std.os.windows.PathSpace = undefined; | |
| 1656 | target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path); | |
| 1657 | target_path_w.data[target_path_w.len] = 0; | |
| 1658 | const sym_link_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sym_link_path); | |
| 1659 | return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); | |
| 1660 | } | |
| 1661 | const target_path_c = try posix.toPosixPath(target_path); | |
| 1662 | const sym_link_path_c = try posix.toPosixPath(sym_link_path); | |
| 1663 | return self.symLinkZ(&target_path_c, &sym_link_path_c, flags); | |
| 1664 | } | |
| 1665 | ||
| 1666 | /// WASI-only. Same as `symLink` except targeting WASI. | |
| 1667 | pub fn symLinkWasi( | |
| 1668 | self: Dir, | |
| 1669 | target_path: []const u8, | |
| 1670 | sym_link_path: []const u8, | |
| 1671 | _: SymLinkFlags, | |
| 1672 | ) !void { | |
| 1673 | return posix.symlinkat(target_path, self.fd, sym_link_path); | |
| 1674 | } | |
| 1675 | ||
| 1676 | /// Same as `symLink`, except the pathname parameters are null-terminated. | |
| 1677 | pub fn symLinkZ( | |
| 1678 | self: Dir, | |
| 1679 | target_path_c: [*:0]const u8, | |
| 1680 | sym_link_path_c: [*:0]const u8, | |
| 1681 | flags: SymLinkFlags, | |
| 1682 | ) !void { | |
| 1683 | if (builtin.os.tag == .windows) { | |
| 1684 | const target_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, target_path_c); | |
| 1685 | const sym_link_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sym_link_path_c); | |
| 1686 | return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); | |
| 1687 | } | |
| 1688 | return posix.symlinkatZ(target_path_c, self.fd, sym_link_path_c); | |
| 1689 | } | |
| 1690 | ||
| 1691 | /// Windows-only. Same as `symLink` except the pathname parameters | |
| 1692 | /// are null-terminated, WTF16 encoded. | |
| 1693 | pub fn symLinkW( | |
| 1694 | self: Dir, | |
| 1695 | /// WTF-16, does not need to be NT-prefixed. The NT-prefixing | |
| 1696 | /// of this path is handled by CreateSymbolicLink. | |
| 1697 | target_path_w: [:0]const u16, | |
| 1698 | /// WTF-16, must be NT-prefixed or relative | |
| 1699 | sym_link_path_w: []const u16, | |
| 1700 | flags: SymLinkFlags, | |
| 1701 | ) !void { | |
| 1702 | return std.os.windows.CreateSymbolicLink(self.fd, sym_link_path_w, target_path_w, flags.is_directory); | |
| 1703 | } | |
| 1704 | ||
| 1705 | pub const ReadLinkError = posix.ReadLinkError; | |
| 1706 | ||
| 1707 | /// Read value of a symbolic link. | |
| 1708 | /// The return value is a slice of `buffer`, from index `0`. | |
| 1709 | /// Asserts that the path parameter has no null bytes. | |
| 1710 | pub fn readLink(self: Dir, sub_path: []const u8, buffer: []u8) ReadLinkError![]u8 { | |
| 1711 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1712 | return self.readLinkWasi(sub_path, buffer); | |
| 1713 | } | |
| 1714 | if (builtin.os.tag == .windows) { | |
| 1715 | const sub_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1716 | return self.readLinkW(sub_path_w.span(), buffer); | |
| 1717 | } | |
| 1718 | const sub_path_c = try posix.toPosixPath(sub_path); | |
| 1719 | return self.readLinkZ(&sub_path_c, buffer); | |
| 1720 | } | |
| 1721 | ||
| 1722 | /// WASI-only. Same as `readLink` except targeting WASI. | |
| 1723 | pub fn readLinkWasi(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 { | |
| 1724 | return posix.readlinkat(self.fd, sub_path, buffer); | |
| 1725 | } | |
| 1726 | ||
| 1727 | /// Same as `readLink`, except the `pathname` parameter is null-terminated. | |
| 1728 | pub fn readLinkZ(self: Dir, sub_path_c: [*:0]const u8, buffer: []u8) ![]u8 { | |
| 1729 | if (builtin.os.tag == .windows) { | |
| 1730 | const sub_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); | |
| 1731 | return self.readLinkW(sub_path_w.span(), buffer); | |
| 1732 | } | |
| 1733 | return posix.readlinkatZ(self.fd, sub_path_c, buffer); | |
| 1734 | } | |
| 1735 | ||
| 1736 | /// Windows-only. Same as `readLink` except the pathname parameter | |
| 1737 | /// is null-terminated, WTF16 encoded. | |
| 1738 | pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 { | |
| 1739 | return std.os.windows.ReadLink(self.fd, sub_path_w, buffer); | |
| 1740 | } | |
| 1741 | ||
| 1742 | /// Read all of file contents using a preallocated buffer. | |
| 1743 | /// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len` | |
| 1744 | /// the situation is ambiguous. It could either mean that the entire file was read, and | |
| 1745 | /// it exactly fits the buffer, or it could mean the buffer was not big enough for the | |
| 1746 | /// entire file. | |
| 1747 | pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 { | |
| 1748 | var file = try self.openFile(file_path, .{}); | |
| 1749 | defer file.close(); | |
| 1750 | ||
| 1751 | const end_index = try file.readAll(buffer); | |
| 1752 | return buffer[0..end_index]; | |
| 1753 | } | |
| 1754 | ||
| 1755 | /// On success, caller owns returned buffer. | |
| 1756 | /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. | |
| 1757 | pub fn readFileAlloc(self: Dir, allocator: mem.Allocator, file_path: []const u8, max_bytes: usize) ![]u8 { | |
| 1758 | return self.readFileAllocOptions(allocator, file_path, max_bytes, null, @alignOf(u8), null); | |
| 1759 | } | |
| 1760 | ||
| 1761 | /// On success, caller owns returned buffer. | |
| 1762 | /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. | |
| 1763 | /// If `size_hint` is specified the initial buffer size is calculated using | |
| 1764 | /// that value, otherwise the effective file size is used instead. | |
| 1765 | /// Allows specifying alignment and a sentinel value. | |
| 1766 | pub fn readFileAllocOptions( | |
| 1767 | self: Dir, | |
| 1768 | allocator: mem.Allocator, | |
| 1769 | file_path: []const u8, | |
| 1770 | max_bytes: usize, | |
| 1771 | size_hint: ?usize, | |
| 1772 | comptime alignment: u29, | |
| 1773 | comptime optional_sentinel: ?u8, | |
| 1774 | ) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) { | |
| 1775 | var file = try self.openFile(file_path, .{}); | |
| 1776 | defer file.close(); | |
| 1777 | ||
| 1778 | // If the file size doesn't fit a usize it'll be certainly greater than | |
| 1779 | // `max_bytes` | |
| 1780 | const stat_size = size_hint orelse std.math.cast(usize, try file.getEndPos()) orelse | |
| 1781 | return error.FileTooBig; | |
| 1782 | ||
| 1783 | return file.readToEndAllocOptions(allocator, max_bytes, stat_size, alignment, optional_sentinel); | |
| 1784 | } | |
| 1785 | ||
| 1786 | pub const DeleteTreeError = error{ | |
| 1787 | InvalidHandle, | |
| 1788 | AccessDenied, | |
| 1789 | FileTooBig, | |
| 1790 | SymLinkLoop, | |
| 1791 | ProcessFdQuotaExceeded, | |
| 1792 | NameTooLong, | |
| 1793 | SystemFdQuotaExceeded, | |
| 1794 | NoDevice, | |
| 1795 | SystemResources, | |
| 1796 | ReadOnlyFileSystem, | |
| 1797 | FileSystem, | |
| 1798 | FileBusy, | |
| 1799 | DeviceBusy, | |
| 1800 | ||
| 1801 | /// One of the path components was not a directory. | |
| 1802 | /// This error is unreachable if `sub_path` does not contain a path separator. | |
| 1803 | NotDir, | |
| 1804 | ||
| 1805 | /// On Windows, file paths must be valid Unicode. | |
| 1806 | InvalidUtf8, | |
| 1807 | ||
| 1808 | /// On Windows, file paths cannot contain these characters: | |
| 1809 | /// '/', '*', '?', '"', '<', '>', '|' | |
| 1810 | BadPathName, | |
| 1811 | ||
| 1812 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 1813 | NetworkNotFound, | |
| 1814 | } || posix.UnexpectedError; | |
| 1815 | ||
| 1816 | /// Whether `full_path` describes a symlink, file, or directory, this function | |
| 1817 | /// removes it. If it cannot be removed because it is a non-empty directory, | |
| 1818 | /// this function recursively removes its entries and then tries again. | |
| 1819 | /// This operation is not atomic on most file systems. | |
| 1820 | pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { | |
| 1821 | var initial_iterable_dir = (try self.deleteTreeOpenInitialSubpath(sub_path, .file)) orelse return; | |
| 1822 | ||
| 1823 | const StackItem = struct { | |
| 1824 | name: []const u8, | |
| 1825 | parent_dir: Dir, | |
| 1826 | iter: Dir.Iterator, | |
| 1827 | ||
| 1828 | fn closeAll(items: []@This()) void { | |
| 1829 | for (items) |*item| item.iter.dir.close(); | |
| 1830 | } | |
| 1831 | }; | |
| 1832 | ||
| 1833 | var stack_buffer: [16]StackItem = undefined; | |
| 1834 | var stack = std.ArrayListUnmanaged(StackItem).initBuffer(&stack_buffer); | |
| 1835 | defer StackItem.closeAll(stack.items); | |
| 1836 | ||
| 1837 | stack.appendAssumeCapacity(.{ | |
| 1838 | .name = sub_path, | |
| 1839 | .parent_dir = self, | |
| 1840 | .iter = initial_iterable_dir.iterateAssumeFirstIteration(), | |
| 1841 | }); | |
| 1842 | ||
| 1843 | process_stack: while (stack.items.len != 0) { | |
| 1844 | var top = &stack.items[stack.items.len - 1]; | |
| 1845 | while (try top.iter.next()) |entry| { | |
| 1846 | var treat_as_dir = entry.kind == .directory; | |
| 1847 | handle_entry: while (true) { | |
| 1848 | if (treat_as_dir) { | |
| 1849 | if (stack.unusedCapacitySlice().len >= 1) { | |
| 1850 | var iterable_dir = top.iter.dir.openDir(entry.name, .{ | |
| 1851 | .no_follow = true, | |
| 1852 | .iterate = true, | |
| 1853 | }) catch |err| switch (err) { | |
| 1854 | error.NotDir => { | |
| 1855 | treat_as_dir = false; | |
| 1856 | continue :handle_entry; | |
| 1857 | }, | |
| 1858 | error.FileNotFound => { | |
| 1859 | // That's fine, we were trying to remove this directory anyway. | |
| 1860 | break :handle_entry; | |
| 1861 | }, | |
| 1862 | ||
| 1863 | error.InvalidHandle, | |
| 1864 | error.AccessDenied, | |
| 1865 | error.SymLinkLoop, | |
| 1866 | error.ProcessFdQuotaExceeded, | |
| 1867 | error.NameTooLong, | |
| 1868 | error.SystemFdQuotaExceeded, | |
| 1869 | error.NoDevice, | |
| 1870 | error.SystemResources, | |
| 1871 | error.Unexpected, | |
| 1872 | error.InvalidUtf8, | |
| 1873 | error.BadPathName, | |
| 1874 | error.NetworkNotFound, | |
| 1875 | error.DeviceBusy, | |
| 1876 | => |e| return e, | |
| 1877 | }; | |
| 1878 | stack.appendAssumeCapacity(.{ | |
| 1879 | .name = entry.name, | |
| 1880 | .parent_dir = top.iter.dir, | |
| 1881 | .iter = iterable_dir.iterateAssumeFirstIteration(), | |
| 1882 | }); | |
| 1883 | continue :process_stack; | |
| 1884 | } else { | |
| 1885 | try top.iter.dir.deleteTreeMinStackSizeWithKindHint(entry.name, entry.kind); | |
| 1886 | break :handle_entry; | |
| 1887 | } | |
| 1888 | } else { | |
| 1889 | if (top.iter.dir.deleteFile(entry.name)) { | |
| 1890 | break :handle_entry; | |
| 1891 | } else |err| switch (err) { | |
| 1892 | error.FileNotFound => break :handle_entry, | |
| 1893 | ||
| 1894 | // Impossible because we do not pass any path separators. | |
| 1895 | error.NotDir => unreachable, | |
| 1896 | ||
| 1897 | error.IsDir => { | |
| 1898 | treat_as_dir = true; | |
| 1899 | continue :handle_entry; | |
| 1900 | }, | |
| 1901 | ||
| 1902 | error.AccessDenied, | |
| 1903 | error.InvalidUtf8, | |
| 1904 | error.SymLinkLoop, | |
| 1905 | error.NameTooLong, | |
| 1906 | error.SystemResources, | |
| 1907 | error.ReadOnlyFileSystem, | |
| 1908 | error.FileSystem, | |
| 1909 | error.FileBusy, | |
| 1910 | error.BadPathName, | |
| 1911 | error.NetworkNotFound, | |
| 1912 | error.Unexpected, | |
| 1913 | => |e| return e, | |
| 1914 | } | |
| 1915 | } | |
| 1916 | } | |
| 1917 | } | |
| 1918 | ||
| 1919 | // On Windows, we can't delete until the dir's handle has been closed, so | |
| 1920 | // close it before we try to delete. | |
| 1921 | top.iter.dir.close(); | |
| 1922 | ||
| 1923 | // In order to avoid double-closing the directory when cleaning up | |
| 1924 | // the stack in the case of an error, we save the relevant portions and | |
| 1925 | // pop the value from the stack. | |
| 1926 | const parent_dir = top.parent_dir; | |
| 1927 | const name = top.name; | |
| 1928 | stack.items.len -= 1; | |
| 1929 | ||
| 1930 | var need_to_retry: bool = false; | |
| 1931 | parent_dir.deleteDir(name) catch |err| switch (err) { | |
| 1932 | error.FileNotFound => {}, | |
| 1933 | error.DirNotEmpty => need_to_retry = true, | |
| 1934 | else => |e| return e, | |
| 1935 | }; | |
| 1936 | ||
| 1937 | if (need_to_retry) { | |
| 1938 | // Since we closed the handle that the previous iterator used, we | |
| 1939 | // need to re-open the dir and re-create the iterator. | |
| 1940 | var iterable_dir = iterable_dir: { | |
| 1941 | var treat_as_dir = true; | |
| 1942 | handle_entry: while (true) { | |
| 1943 | if (treat_as_dir) { | |
| 1944 | break :iterable_dir parent_dir.openDir(name, .{ | |
| 1945 | .no_follow = true, | |
| 1946 | .iterate = true, | |
| 1947 | }) catch |err| switch (err) { | |
| 1948 | error.NotDir => { | |
| 1949 | treat_as_dir = false; | |
| 1950 | continue :handle_entry; | |
| 1951 | }, | |
| 1952 | error.FileNotFound => { | |
| 1953 | // That's fine, we were trying to remove this directory anyway. | |
| 1954 | continue :process_stack; | |
| 1955 | }, | |
| 1956 | ||
| 1957 | error.InvalidHandle, | |
| 1958 | error.AccessDenied, | |
| 1959 | error.SymLinkLoop, | |
| 1960 | error.ProcessFdQuotaExceeded, | |
| 1961 | error.NameTooLong, | |
| 1962 | error.SystemFdQuotaExceeded, | |
| 1963 | error.NoDevice, | |
| 1964 | error.SystemResources, | |
| 1965 | error.Unexpected, | |
| 1966 | error.InvalidUtf8, | |
| 1967 | error.BadPathName, | |
| 1968 | error.NetworkNotFound, | |
| 1969 | error.DeviceBusy, | |
| 1970 | => |e| return e, | |
| 1971 | }; | |
| 1972 | } else { | |
| 1973 | if (parent_dir.deleteFile(name)) { | |
| 1974 | continue :process_stack; | |
| 1975 | } else |err| switch (err) { | |
| 1976 | error.FileNotFound => continue :process_stack, | |
| 1977 | ||
| 1978 | // Impossible because we do not pass any path separators. | |
| 1979 | error.NotDir => unreachable, | |
| 1980 | ||
| 1981 | error.IsDir => { | |
| 1982 | treat_as_dir = true; | |
| 1983 | continue :handle_entry; | |
| 1984 | }, | |
| 1985 | ||
| 1986 | error.AccessDenied, | |
| 1987 | error.InvalidUtf8, | |
| 1988 | error.SymLinkLoop, | |
| 1989 | error.NameTooLong, | |
| 1990 | error.SystemResources, | |
| 1991 | error.ReadOnlyFileSystem, | |
| 1992 | error.FileSystem, | |
| 1993 | error.FileBusy, | |
| 1994 | error.BadPathName, | |
| 1995 | error.NetworkNotFound, | |
| 1996 | error.Unexpected, | |
| 1997 | => |e| return e, | |
| 1998 | } | |
| 1999 | } | |
| 2000 | } | |
| 2001 | }; | |
| 2002 | // We know there is room on the stack since we are just re-adding | |
| 2003 | // the StackItem that we previously popped. | |
| 2004 | stack.appendAssumeCapacity(.{ | |
| 2005 | .name = name, | |
| 2006 | .parent_dir = parent_dir, | |
| 2007 | .iter = iterable_dir.iterateAssumeFirstIteration(), | |
| 2008 | }); | |
| 2009 | continue :process_stack; | |
| 2010 | } | |
| 2011 | } | |
| 2012 | } | |
| 2013 | ||
| 2014 | /// Like `deleteTree`, but only keeps one `Iterator` active at a time to minimize the function's stack size. | |
| 2015 | /// This is slower than `deleteTree` but uses less stack space. | |
| 2016 | pub fn deleteTreeMinStackSize(self: Dir, sub_path: []const u8) DeleteTreeError!void { | |
| 2017 | return self.deleteTreeMinStackSizeWithKindHint(sub_path, .file); | |
| 2018 | } | |
| 2019 | ||
| 2020 | fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint: File.Kind) DeleteTreeError!void { | |
| 2021 | start_over: while (true) { | |
| 2022 | var dir = (try self.deleteTreeOpenInitialSubpath(sub_path, kind_hint)) orelse return; | |
| 2023 | var cleanup_dir_parent: ?Dir = null; | |
| 2024 | defer if (cleanup_dir_parent) |*d| d.close(); | |
| 2025 | ||
| 2026 | var cleanup_dir = true; | |
| 2027 | defer if (cleanup_dir) dir.close(); | |
| 2028 | ||
| 2029 | // Valid use of MAX_PATH_BYTES because dir_name_buf will only | |
| 2030 | // ever store a single path component that was returned from the | |
| 2031 | // filesystem. | |
| 2032 | var dir_name_buf: [fs.MAX_PATH_BYTES]u8 = undefined; | |
| 2033 | var dir_name: []const u8 = sub_path; | |
| 2034 | ||
| 2035 | // Here we must avoid recursion, in order to provide O(1) memory guarantee of this function. | |
| 2036 | // Go through each entry and if it is not a directory, delete it. If it is a directory, | |
| 2037 | // open it, and close the original directory. Repeat. Then start the entire operation over. | |
| 2038 | ||
| 2039 | scan_dir: while (true) { | |
| 2040 | var dir_it = dir.iterateAssumeFirstIteration(); | |
| 2041 | dir_it: while (try dir_it.next()) |entry| { | |
| 2042 | var treat_as_dir = entry.kind == .directory; | |
| 2043 | handle_entry: while (true) { | |
| 2044 | if (treat_as_dir) { | |
| 2045 | const new_dir = dir.openDir(entry.name, .{ | |
| 2046 | .no_follow = true, | |
| 2047 | .iterate = true, | |
| 2048 | }) catch |err| switch (err) { | |
| 2049 | error.NotDir => { | |
| 2050 | treat_as_dir = false; | |
| 2051 | continue :handle_entry; | |
| 2052 | }, | |
| 2053 | error.FileNotFound => { | |
| 2054 | // That's fine, we were trying to remove this directory anyway. | |
| 2055 | continue :dir_it; | |
| 2056 | }, | |
| 2057 | ||
| 2058 | error.InvalidHandle, | |
| 2059 | error.AccessDenied, | |
| 2060 | error.SymLinkLoop, | |
| 2061 | error.ProcessFdQuotaExceeded, | |
| 2062 | error.NameTooLong, | |
| 2063 | error.SystemFdQuotaExceeded, | |
| 2064 | error.NoDevice, | |
| 2065 | error.SystemResources, | |
| 2066 | error.Unexpected, | |
| 2067 | error.InvalidUtf8, | |
| 2068 | error.BadPathName, | |
| 2069 | error.NetworkNotFound, | |
| 2070 | error.DeviceBusy, | |
| 2071 | => |e| return e, | |
| 2072 | }; | |
| 2073 | if (cleanup_dir_parent) |*d| d.close(); | |
| 2074 | cleanup_dir_parent = dir; | |
| 2075 | dir = new_dir; | |
| 2076 | const result = dir_name_buf[0..entry.name.len]; | |
| 2077 | @memcpy(result, entry.name); | |
| 2078 | dir_name = result; | |
| 2079 | continue :scan_dir; | |
| 2080 | } else { | |
| 2081 | if (dir.deleteFile(entry.name)) { | |
| 2082 | continue :dir_it; | |
| 2083 | } else |err| switch (err) { | |
| 2084 | error.FileNotFound => continue :dir_it, | |
| 2085 | ||
| 2086 | // Impossible because we do not pass any path separators. | |
| 2087 | error.NotDir => unreachable, | |
| 2088 | ||
| 2089 | error.IsDir => { | |
| 2090 | treat_as_dir = true; | |
| 2091 | continue :handle_entry; | |
| 2092 | }, | |
| 2093 | ||
| 2094 | error.AccessDenied, | |
| 2095 | error.InvalidUtf8, | |
| 2096 | error.SymLinkLoop, | |
| 2097 | error.NameTooLong, | |
| 2098 | error.SystemResources, | |
| 2099 | error.ReadOnlyFileSystem, | |
| 2100 | error.FileSystem, | |
| 2101 | error.FileBusy, | |
| 2102 | error.BadPathName, | |
| 2103 | error.NetworkNotFound, | |
| 2104 | error.Unexpected, | |
| 2105 | => |e| return e, | |
| 2106 | } | |
| 2107 | } | |
| 2108 | } | |
| 2109 | } | |
| 2110 | // Reached the end of the directory entries, which means we successfully deleted all of them. | |
| 2111 | // Now to remove the directory itself. | |
| 2112 | dir.close(); | |
| 2113 | cleanup_dir = false; | |
| 2114 | ||
| 2115 | if (cleanup_dir_parent) |d| { | |
| 2116 | d.deleteDir(dir_name) catch |err| switch (err) { | |
| 2117 | // These two things can happen due to file system race conditions. | |
| 2118 | error.FileNotFound, error.DirNotEmpty => continue :start_over, | |
| 2119 | else => |e| return e, | |
| 2120 | }; | |
| 2121 | continue :start_over; | |
| 2122 | } else { | |
| 2123 | self.deleteDir(sub_path) catch |err| switch (err) { | |
| 2124 | error.FileNotFound => return, | |
| 2125 | error.DirNotEmpty => continue :start_over, | |
| 2126 | else => |e| return e, | |
| 2127 | }; | |
| 2128 | return; | |
| 2129 | } | |
| 2130 | } | |
| 2131 | } | |
| 2132 | } | |
| 2133 | ||
| 2134 | /// On successful delete, returns null. | |
| 2135 | fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File.Kind) !?Dir { | |
| 2136 | return iterable_dir: { | |
| 2137 | // Treat as a file by default | |
| 2138 | var treat_as_dir = kind_hint == .directory; | |
| 2139 | ||
| 2140 | handle_entry: while (true) { | |
| 2141 | if (treat_as_dir) { | |
| 2142 | break :iterable_dir self.openDir(sub_path, .{ | |
| 2143 | .no_follow = true, | |
| 2144 | .iterate = true, | |
| 2145 | }) catch |err| switch (err) { | |
| 2146 | error.NotDir => { | |
| 2147 | treat_as_dir = false; | |
| 2148 | continue :handle_entry; | |
| 2149 | }, | |
| 2150 | error.FileNotFound => { | |
| 2151 | // That's fine, we were trying to remove this directory anyway. | |
| 2152 | return null; | |
| 2153 | }, | |
| 2154 | ||
| 2155 | error.InvalidHandle, | |
| 2156 | error.AccessDenied, | |
| 2157 | error.SymLinkLoop, | |
| 2158 | error.ProcessFdQuotaExceeded, | |
| 2159 | error.NameTooLong, | |
| 2160 | error.SystemFdQuotaExceeded, | |
| 2161 | error.NoDevice, | |
| 2162 | error.SystemResources, | |
| 2163 | error.Unexpected, | |
| 2164 | error.InvalidUtf8, | |
| 2165 | error.BadPathName, | |
| 2166 | error.DeviceBusy, | |
| 2167 | error.NetworkNotFound, | |
| 2168 | => |e| return e, | |
| 2169 | }; | |
| 2170 | } else { | |
| 2171 | if (self.deleteFile(sub_path)) { | |
| 2172 | return null; | |
| 2173 | } else |err| switch (err) { | |
| 2174 | error.FileNotFound => return null, | |
| 2175 | ||
| 2176 | error.IsDir => { | |
| 2177 | treat_as_dir = true; | |
| 2178 | continue :handle_entry; | |
| 2179 | }, | |
| 2180 | ||
| 2181 | error.AccessDenied, | |
| 2182 | error.InvalidUtf8, | |
| 2183 | error.SymLinkLoop, | |
| 2184 | error.NameTooLong, | |
| 2185 | error.SystemResources, | |
| 2186 | error.ReadOnlyFileSystem, | |
| 2187 | error.NotDir, | |
| 2188 | error.FileSystem, | |
| 2189 | error.FileBusy, | |
| 2190 | error.BadPathName, | |
| 2191 | error.NetworkNotFound, | |
| 2192 | error.Unexpected, | |
| 2193 | => |e| return e, | |
| 2194 | } | |
| 2195 | } | |
| 2196 | } | |
| 2197 | }; | |
| 2198 | } | |
| 2199 | ||
| 2200 | pub const WriteFileError = File.WriteError || File.OpenError; | |
| 2201 | ||
| 2202 | /// Deprecated: use `writeFile2`. | |
| 2203 | pub fn writeFile(self: Dir, sub_path: []const u8, data: []const u8) WriteFileError!void { | |
| 2204 | return writeFile2(self, .{ | |
| 2205 | .sub_path = sub_path, | |
| 2206 | .data = data, | |
| 2207 | .flags = .{}, | |
| 2208 | }); | |
| 2209 | } | |
| 2210 | ||
| 2211 | pub const WriteFileOptions = struct { | |
| 2212 | sub_path: []const u8, | |
| 2213 | data: []const u8, | |
| 2214 | flags: File.CreateFlags = .{}, | |
| 2215 | }; | |
| 2216 | ||
| 2217 | /// Writes content to the file system, using the file creation flags provided. | |
| 2218 | pub fn writeFile2(self: Dir, options: WriteFileOptions) WriteFileError!void { | |
| 2219 | var file = try self.createFile(options.sub_path, options.flags); | |
| 2220 | defer file.close(); | |
| 2221 | try file.writeAll(options.data); | |
| 2222 | } | |
| 2223 | ||
| 2224 | pub const AccessError = posix.AccessError; | |
| 2225 | ||
| 2226 | /// Test accessing `path`. | |
| 2227 | /// `path` is UTF-8-encoded. | |
| 2228 | /// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function. | |
| 2229 | /// For example, instead of testing if a file exists and then opening it, just | |
| 2230 | /// open it and handle the error for file not found. | |
| 2231 | pub fn access(self: Dir, sub_path: []const u8, flags: File.OpenFlags) AccessError!void { | |
| 2232 | if (builtin.os.tag == .windows) { | |
| 2233 | const sub_path_w = std.os.windows.sliceToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) { | |
| 2234 | error.AccessDenied => return error.PermissionDenied, | |
| 2235 | else => |e| return e, | |
| 2236 | }; | |
| 2237 | return self.accessW(sub_path_w.span().ptr, flags); | |
| 2238 | } | |
| 2239 | const path_c = try posix.toPosixPath(sub_path); | |
| 2240 | return self.accessZ(&path_c, flags); | |
| 2241 | } | |
| 2242 | ||
| 2243 | /// Same as `access` except the path parameter is null-terminated. | |
| 2244 | pub fn accessZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) AccessError!void { | |
| 2245 | if (builtin.os.tag == .windows) { | |
| 2246 | const sub_path_w = std.os.windows.cStrToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) { | |
| 2247 | error.AccessDenied => return error.PermissionDenied, | |
| 2248 | else => |e| return e, | |
| 2249 | }; | |
| 2250 | return self.accessW(sub_path_w.span().ptr, flags); | |
| 2251 | } | |
| 2252 | const os_mode = switch (flags.mode) { | |
| 2253 | .read_only => @as(u32, posix.F_OK), | |
| 2254 | .write_only => @as(u32, posix.W_OK), | |
| 2255 | .read_write => @as(u32, posix.R_OK | posix.W_OK), | |
| 2256 | }; | |
| 2257 | const result = if (fs.need_async_thread and flags.intended_io_mode != .blocking) | |
| 2258 | std.event.Loop.instance.?.faccessatZ(self.fd, sub_path, os_mode, 0) | |
| 2259 | else | |
| 2260 | posix.faccessatZ(self.fd, sub_path, os_mode, 0); | |
| 2261 | return result; | |
| 2262 | } | |
| 2263 | ||
| 2264 | /// Same as `access` except asserts the target OS is Windows and the path parameter is | |
| 2265 | /// * WTF-16 encoded | |
| 2266 | /// * null-terminated | |
| 2267 | /// * NtDll prefixed | |
| 2268 | /// TODO currently this ignores `flags`. | |
| 2269 | pub fn accessW(self: Dir, sub_path_w: [*:0]const u16, flags: File.OpenFlags) AccessError!void { | |
| 2270 | _ = flags; | |
| 2271 | return posix.faccessatW(self.fd, sub_path_w, 0, 0); | |
| 2272 | } | |
| 2273 | ||
| 2274 | pub const CopyFileOptions = struct { | |
| 2275 | /// When this is `null` the mode is copied from the source file. | |
| 2276 | override_mode: ?File.Mode = null, | |
| 2277 | }; | |
| 2278 | ||
| 2279 | pub const PrevStatus = enum { | |
| 2280 | stale, | |
| 2281 | fresh, | |
| 2282 | }; | |
| 2283 | ||
| 2284 | /// Check the file size, mtime, and mode of `source_path` and `dest_path`. If they are equal, does nothing. | |
| 2285 | /// Otherwise, atomically copies `source_path` to `dest_path`. The destination file gains the mtime, | |
| 2286 | /// atime, and mode of the source file so that the next call to `updateFile` will not need a copy. | |
| 2287 | /// Returns the previous status of the file before updating. | |
| 2288 | /// If any of the directories do not exist for dest_path, they are created. | |
| 2289 | pub fn updateFile( | |
| 2290 | source_dir: Dir, | |
| 2291 | source_path: []const u8, | |
| 2292 | dest_dir: Dir, | |
| 2293 | dest_path: []const u8, | |
| 2294 | options: CopyFileOptions, | |
| 2295 | ) !PrevStatus { | |
| 2296 | var src_file = try source_dir.openFile(source_path, .{}); | |
| 2297 | defer src_file.close(); | |
| 2298 | ||
| 2299 | const src_stat = try src_file.stat(); | |
| 2300 | const actual_mode = options.override_mode orelse src_stat.mode; | |
| 2301 | check_dest_stat: { | |
| 2302 | const dest_stat = blk: { | |
| 2303 | var dest_file = dest_dir.openFile(dest_path, .{}) catch |err| switch (err) { | |
| 2304 | error.FileNotFound => break :check_dest_stat, | |
| 2305 | else => |e| return e, | |
| 2306 | }; | |
| 2307 | defer dest_file.close(); | |
| 2308 | ||
| 2309 | break :blk try dest_file.stat(); | |
| 2310 | }; | |
| 2311 | ||
| 2312 | if (src_stat.size == dest_stat.size and | |
| 2313 | src_stat.mtime == dest_stat.mtime and | |
| 2314 | actual_mode == dest_stat.mode) | |
| 2315 | { | |
| 2316 | return PrevStatus.fresh; | |
| 2317 | } | |
| 2318 | } | |
| 2319 | ||
| 2320 | if (fs.path.dirname(dest_path)) |dirname| { | |
| 2321 | try dest_dir.makePath(dirname); | |
| 2322 | } | |
| 2323 | ||
| 2324 | var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = actual_mode }); | |
| 2325 | defer atomic_file.deinit(); | |
| 2326 | ||
| 2327 | try atomic_file.file.writeFileAll(src_file, .{ .in_len = src_stat.size }); | |
| 2328 | try atomic_file.file.updateTimes(src_stat.atime, src_stat.mtime); | |
| 2329 | try atomic_file.finish(); | |
| 2330 | return PrevStatus.stale; | |
| 2331 | } | |
| 2332 | ||
| 2333 | pub const CopyFileError = File.OpenError || File.StatError || | |
| 2334 | AtomicFile.InitError || CopyFileRawError || AtomicFile.FinishError; | |
| 2335 | ||
| 2336 | /// Guaranteed to be atomic. | |
| 2337 | /// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and readily available, | |
| 2338 | /// there is a possibility of power loss or application termination leaving temporary files present | |
| 2339 | /// in the same directory as dest_path. | |
| 2340 | pub fn copyFile( | |
| 2341 | source_dir: Dir, | |
| 2342 | source_path: []const u8, | |
| 2343 | dest_dir: Dir, | |
| 2344 | dest_path: []const u8, | |
| 2345 | options: CopyFileOptions, | |
| 2346 | ) CopyFileError!void { | |
| 2347 | var in_file = try source_dir.openFile(source_path, .{}); | |
| 2348 | defer in_file.close(); | |
| 2349 | ||
| 2350 | var size: ?u64 = null; | |
| 2351 | const mode = options.override_mode orelse blk: { | |
| 2352 | const st = try in_file.stat(); | |
| 2353 | size = st.size; | |
| 2354 | break :blk st.mode; | |
| 2355 | }; | |
| 2356 | ||
| 2357 | var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = mode }); | |
| 2358 | defer atomic_file.deinit(); | |
| 2359 | ||
| 2360 | try copy_file(in_file.handle, atomic_file.file.handle, size); | |
| 2361 | try atomic_file.finish(); | |
| 2362 | } | |
| 2363 | ||
| 2364 | const CopyFileRawError = error{SystemResources} || posix.CopyFileRangeError || posix.SendFileError; | |
| 2365 | ||
| 2366 | // Transfer all the data between two file descriptors in the most efficient way. | |
| 2367 | // The copy starts at offset 0, the initial offsets are preserved. | |
| 2368 | // No metadata is transferred over. | |
| 2369 | fn copy_file(fd_in: posix.fd_t, fd_out: posix.fd_t, maybe_size: ?u64) CopyFileRawError!void { | |
| 2370 | if (comptime builtin.target.isDarwin()) { | |
| 2371 | const rc = posix.system.fcopyfile(fd_in, fd_out, null, posix.system.COPYFILE_DATA); | |
| 2372 | switch (posix.errno(rc)) { | |
| 2373 | .SUCCESS => return, | |
| 2374 | .INVAL => unreachable, | |
| 2375 | .NOMEM => return error.SystemResources, | |
| 2376 | // The source file is not a directory, symbolic link, or regular file. | |
| 2377 | // Try with the fallback path before giving up. | |
| 2378 | .OPNOTSUPP => {}, | |
| 2379 | else => |err| return posix.unexpectedErrno(err), | |
| 2380 | } | |
| 2381 | } | |
| 2382 | ||
| 2383 | if (builtin.os.tag == .linux) { | |
| 2384 | // Try copy_file_range first as that works at the FS level and is the | |
| 2385 | // most efficient method (if available). | |
| 2386 | var offset: u64 = 0; | |
| 2387 | cfr_loop: while (true) { | |
| 2388 | // The kernel checks the u64 value `offset+count` for overflow, use | |
| 2389 | // a 32 bit value so that the syscall won't return EINVAL except for | |
| 2390 | // impossibly large files (> 2^64-1 - 2^32-1). | |
| 2391 | const amt = try posix.copy_file_range(fd_in, offset, fd_out, offset, std.math.maxInt(u32), 0); | |
| 2392 | // Terminate as soon as we have copied size bytes or no bytes | |
| 2393 | if (maybe_size) |s| { | |
| 2394 | if (s == amt) break :cfr_loop; | |
| 2395 | } | |
| 2396 | if (amt == 0) break :cfr_loop; | |
| 2397 | offset += amt; | |
| 2398 | } | |
| 2399 | return; | |
| 2400 | } | |
| 2401 | ||
| 2402 | // Sendfile is a zero-copy mechanism iff the OS supports it, otherwise the | |
| 2403 | // fallback code will copy the contents chunk by chunk. | |
| 2404 | const empty_iovec = [0]posix.iovec_const{}; | |
| 2405 | var offset: u64 = 0; | |
| 2406 | sendfile_loop: while (true) { | |
| 2407 | const amt = try posix.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0); | |
| 2408 | // Terminate as soon as we have copied size bytes or no bytes | |
| 2409 | if (maybe_size) |s| { | |
| 2410 | if (s == amt) break :sendfile_loop; | |
| 2411 | } | |
| 2412 | if (amt == 0) break :sendfile_loop; | |
| 2413 | offset += amt; | |
| 2414 | } | |
| 2415 | } | |
| 2416 | ||
| 2417 | pub const AtomicFileOptions = struct { | |
| 2418 | mode: File.Mode = File.default_mode, | |
| 2419 | }; | |
| 2420 | ||
| 2421 | /// Directly access the `.file` field, and then call `AtomicFile.finish` | |
| 2422 | /// to atomically replace `dest_path` with contents. | |
| 2423 | /// Always call `AtomicFile.deinit` to clean up, regardless of whether `AtomicFile.finish` succeeded. | |
| 2424 | /// `dest_path` must remain valid until `AtomicFile.deinit` is called. | |
| 2425 | pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) !AtomicFile { | |
| 2426 | if (fs.path.dirname(dest_path)) |dirname| { | |
| 2427 | const dir = try self.openDir(dirname, .{}); | |
| 2428 | return AtomicFile.init(fs.path.basename(dest_path), options.mode, dir, true); | |
| 2429 | } else { | |
| 2430 | return AtomicFile.init(dest_path, options.mode, self, false); | |
| 2431 | } | |
| 2432 | } | |
| 2433 | ||
| 2434 | pub const Stat = File.Stat; | |
| 2435 | pub const StatError = File.StatError; | |
| 2436 | ||
| 2437 | pub fn stat(self: Dir) StatError!Stat { | |
| 2438 | const file: File = .{ | |
| 2439 | .handle = self.fd, | |
| 2440 | .capable_io_mode = .blocking, | |
| 2441 | }; | |
| 2442 | return file.stat(); | |
| 2443 | } | |
| 2444 | ||
| 2445 | pub const StatFileError = File.OpenError || File.StatError || posix.FStatAtError; | |
| 2446 | ||
| 2447 | /// Returns metadata for a file inside the directory. | |
| 2448 | /// | |
| 2449 | /// On Windows, this requires three syscalls. On other operating systems, it | |
| 2450 | /// only takes one. | |
| 2451 | /// | |
| 2452 | /// Symlinks are followed. | |
| 2453 | /// | |
| 2454 | /// `sub_path` may be absolute, in which case `self` is ignored. | |
| 2455 | pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { | |
| 2456 | if (builtin.os.tag == .windows) { | |
| 2457 | var file = try self.openFile(sub_path, .{}); | |
| 2458 | defer file.close(); | |
| 2459 | return file.stat(); | |
| 2460 | } | |
| 2461 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 2462 | const st = try posix.fstatatWasi(self.fd, sub_path, posix.wasi.LOOKUP_SYMLINK_FOLLOW); | |
| 2463 | return Stat.fromSystem(st); | |
| 2464 | } | |
| 2465 | const st = try posix.fstatat(self.fd, sub_path, 0); | |
| 2466 | return Stat.fromSystem(st); | |
| 2467 | } | |
| 2468 | ||
| 2469 | pub const ChmodError = File.ChmodError; | |
| 2470 | ||
| 2471 | /// Changes the mode of the directory. | |
| 2472 | /// The process must have the correct privileges in order to do this | |
| 2473 | /// successfully, or must have the effective user ID matching the owner | |
| 2474 | /// of the directory. Additionally, the directory must have been opened | |
| 2475 | /// with `OpenDirOptions{ .iterate = true }`. | |
| 2476 | pub fn chmod(self: Dir, new_mode: File.Mode) ChmodError!void { | |
| 2477 | const file: File = .{ | |
| 2478 | .handle = self.fd, | |
| 2479 | .capable_io_mode = .blocking, | |
| 2480 | }; | |
| 2481 | try file.chmod(new_mode); | |
| 2482 | } | |
| 2483 | ||
| 2484 | /// Changes the owner and group of the directory. | |
| 2485 | /// The process must have the correct privileges in order to do this | |
| 2486 | /// successfully. The group may be changed by the owner of the directory to | |
| 2487 | /// any group of which the owner is a member. Additionally, the directory | |
| 2488 | /// must have been opened with `OpenDirOptions{ .iterate = true }`. If the | |
| 2489 | /// owner or group is specified as `null`, the ID is not changed. | |
| 2490 | pub fn chown(self: Dir, owner: ?File.Uid, group: ?File.Gid) ChownError!void { | |
| 2491 | const file: File = .{ | |
| 2492 | .handle = self.fd, | |
| 2493 | .capable_io_mode = .blocking, | |
| 2494 | }; | |
| 2495 | try file.chown(owner, group); | |
| 2496 | } | |
| 2497 | ||
| 2498 | pub const ChownError = File.ChownError; | |
| 2499 | ||
| 2500 | const Permissions = File.Permissions; | |
| 2501 | pub const SetPermissionsError = File.SetPermissionsError; | |
| 2502 | ||
| 2503 | /// Sets permissions according to the provided `Permissions` struct. | |
| 2504 | /// This method is *NOT* available on WASI | |
| 2505 | pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!void { | |
| 2506 | const file: File = .{ | |
| 2507 | .handle = self.fd, | |
| 2508 | .capable_io_mode = .blocking, | |
| 2509 | }; | |
| 2510 | try file.setPermissions(permissions); | |
| 2511 | } | |
| 2512 | ||
| 2513 | const Metadata = File.Metadata; | |
| 2514 | pub const MetadataError = File.MetadataError; | |
| 2515 | ||
| 2516 | /// Returns a `Metadata` struct, representing the permissions on the directory | |
| 2517 | pub fn metadata(self: Dir) MetadataError!Metadata { | |
| 2518 | const file: File = .{ | |
| 2519 | .handle = self.fd, | |
| 2520 | .capable_io_mode = .blocking, | |
| 2521 | }; | |
| 2522 | return try file.metadata(); | |
| 2523 | } | |
| 2524 | ||
| 2525 | const Dir = @This(); | |
| 2526 | const builtin = @import("builtin"); | |
| 2527 | const std = @import("../std.zig"); | |
| 2528 | const File = std.fs.File; | |
| 2529 | const AtomicFile = std.fs.AtomicFile; | |
| 2530 | // https://github.com/ziglang/zig/issues/5019 | |
| 2531 | const posix = std.os; | |
| 2532 | const mem = std.mem; | |
| 2533 | const fs = std.fs; | |
| 2534 | const Allocator = std.mem.Allocator; |
lib/std/fs/File.zig created+1624| ... | ... | @@ -0,0 +1,1624 @@ |
| 1 | /// The OS-specific file descriptor or file handle. | |
| 2 | handle: Handle, | |
| 3 | ||
| 4 | /// On some systems, such as Linux, file system file descriptors are incapable | |
| 5 | /// of non-blocking I/O. This forces us to perform asynchronous I/O on a dedicated thread, | |
| 6 | /// to achieve non-blocking file-system I/O. To do this, `File` must be aware of whether | |
| 7 | /// it is a file system file descriptor, or, more specifically, whether the I/O is always | |
| 8 | /// blocking. | |
| 9 | capable_io_mode: io.ModeOverride = io.default_mode, | |
| 10 | ||
| 11 | /// Furthermore, even when `std.options.io_mode` is async, it is still sometimes desirable | |
| 12 | /// to perform blocking I/O, although not by default. For example, when printing a | |
| 13 | /// stack trace to stderr. This field tracks both by acting as an overriding I/O mode. | |
| 14 | /// When not building in async I/O mode, the type only has the `.blocking` tag, making | |
| 15 | /// it a zero-bit type. | |
| 16 | intended_io_mode: io.ModeOverride = io.default_mode, | |
| 17 | ||
| 18 | pub const Handle = posix.fd_t; | |
| 19 | pub const Mode = posix.mode_t; | |
| 20 | pub const INode = posix.ino_t; | |
| 21 | pub const Uid = posix.uid_t; | |
| 22 | pub const Gid = posix.gid_t; | |
| 23 | ||
| 24 | pub const Kind = enum { | |
| 25 | block_device, | |
| 26 | character_device, | |
| 27 | directory, | |
| 28 | named_pipe, | |
| 29 | sym_link, | |
| 30 | file, | |
| 31 | unix_domain_socket, | |
| 32 | whiteout, | |
| 33 | door, | |
| 34 | event_port, | |
| 35 | unknown, | |
| 36 | }; | |
| 37 | ||
| 38 | /// This is the default mode given to POSIX operating systems for creating | |
| 39 | /// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first, | |
| 40 | /// since most people would expect "-rw-r--r--", for example, when using | |
| 41 | /// the `touch` command, which would correspond to `0o644`. However, POSIX | |
| 42 | /// libc implementations use `0o666` inside `fopen` and then rely on the | |
| 43 | /// process-scoped "umask" setting to adjust this number for file creation. | |
| 44 | pub const default_mode = switch (builtin.os.tag) { | |
| 45 | .windows => 0, | |
| 46 | .wasi => 0, | |
| 47 | else => 0o666, | |
| 48 | }; | |
| 49 | ||
| 50 | pub const OpenError = error{ | |
| 51 | SharingViolation, | |
| 52 | PathAlreadyExists, | |
| 53 | FileNotFound, | |
| 54 | AccessDenied, | |
| 55 | PipeBusy, | |
| 56 | NameTooLong, | |
| 57 | /// On Windows, file paths must be valid Unicode. | |
| 58 | InvalidUtf8, | |
| 59 | /// On Windows, file paths cannot contain these characters: | |
| 60 | /// '/', '*', '?', '"', '<', '>', '|' | |
| 61 | BadPathName, | |
| 62 | Unexpected, | |
| 63 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 64 | NetworkNotFound, | |
| 65 | } || posix.OpenError || posix.FlockError; | |
| 66 | ||
| 67 | pub const OpenMode = enum { | |
| 68 | read_only, | |
| 69 | write_only, | |
| 70 | read_write, | |
| 71 | }; | |
| 72 | ||
| 73 | pub const Lock = enum { | |
| 74 | none, | |
| 75 | shared, | |
| 76 | exclusive, | |
| 77 | }; | |
| 78 | ||
| 79 | pub const OpenFlags = struct { | |
| 80 | mode: OpenMode = .read_only, | |
| 81 | ||
| 82 | /// Open the file with an advisory lock to coordinate with other processes | |
| 83 | /// accessing it at the same time. An exclusive lock will prevent other | |
| 84 | /// processes from acquiring a lock. A shared lock will prevent other | |
| 85 | /// processes from acquiring a exclusive lock, but does not prevent | |
| 86 | /// other process from getting their own shared locks. | |
| 87 | /// | |
| 88 | /// The lock is advisory, except on Linux in very specific circumstances[1]. | |
| 89 | /// This means that a process that does not respect the locking API can still get access | |
| 90 | /// to the file, despite the lock. | |
| 91 | /// | |
| 92 | /// On these operating systems, the lock is acquired atomically with | |
| 93 | /// opening the file: | |
| 94 | /// * Darwin | |
| 95 | /// * DragonFlyBSD | |
| 96 | /// * FreeBSD | |
| 97 | /// * Haiku | |
| 98 | /// * NetBSD | |
| 99 | /// * OpenBSD | |
| 100 | /// On these operating systems, the lock is acquired via a separate syscall | |
| 101 | /// after opening the file: | |
| 102 | /// * Linux | |
| 103 | /// * Windows | |
| 104 | /// | |
| 105 | /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt | |
| 106 | lock: Lock = .none, | |
| 107 | ||
| 108 | /// Sets whether or not to wait until the file is locked to return. If set to true, | |
| 109 | /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file | |
| 110 | /// is available to proceed. | |
| 111 | /// In async I/O mode, non-blocking at the OS level is | |
| 112 | /// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned, | |
| 113 | /// and `false` means `error.WouldBlock` is handled by the event loop. | |
| 114 | lock_nonblocking: bool = false, | |
| 115 | ||
| 116 | /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even | |
| 117 | /// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions | |
| 118 | /// related to opening the file, reading, writing, and locking. | |
| 119 | intended_io_mode: io.ModeOverride = io.default_mode, | |
| 120 | ||
| 121 | /// Set this to allow the opened file to automatically become the | |
| 122 | /// controlling TTY for the current process. | |
| 123 | allow_ctty: bool = false, | |
| 124 | ||
| 125 | pub fn isRead(self: OpenFlags) bool { | |
| 126 | return self.mode != .write_only; | |
| 127 | } | |
| 128 | ||
| 129 | pub fn isWrite(self: OpenFlags) bool { | |
| 130 | return self.mode != .read_only; | |
| 131 | } | |
| 132 | }; | |
| 133 | ||
| 134 | pub const CreateFlags = struct { | |
| 135 | /// Whether the file will be created with read access. | |
| 136 | read: bool = false, | |
| 137 | ||
| 138 | /// If the file already exists, and is a regular file, and the access | |
| 139 | /// mode allows writing, it will be truncated to length 0. | |
| 140 | truncate: bool = true, | |
| 141 | ||
| 142 | /// Ensures that this open call creates the file, otherwise causes | |
| 143 | /// `error.PathAlreadyExists` to be returned. | |
| 144 | exclusive: bool = false, | |
| 145 | ||
| 146 | /// Open the file with an advisory lock to coordinate with other processes | |
| 147 | /// accessing it at the same time. An exclusive lock will prevent other | |
| 148 | /// processes from acquiring a lock. A shared lock will prevent other | |
| 149 | /// processes from acquiring a exclusive lock, but does not prevent | |
| 150 | /// other process from getting their own shared locks. | |
| 151 | /// | |
| 152 | /// The lock is advisory, except on Linux in very specific circumstances[1]. | |
| 153 | /// This means that a process that does not respect the locking API can still get access | |
| 154 | /// to the file, despite the lock. | |
| 155 | /// | |
| 156 | /// On these operating systems, the lock is acquired atomically with | |
| 157 | /// opening the file: | |
| 158 | /// * Darwin | |
| 159 | /// * DragonFlyBSD | |
| 160 | /// * FreeBSD | |
| 161 | /// * Haiku | |
| 162 | /// * NetBSD | |
| 163 | /// * OpenBSD | |
| 164 | /// On these operating systems, the lock is acquired via a separate syscall | |
| 165 | /// after opening the file: | |
| 166 | /// * Linux | |
| 167 | /// * Windows | |
| 168 | /// | |
| 169 | /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt | |
| 170 | lock: Lock = .none, | |
| 171 | ||
| 172 | /// Sets whether or not to wait until the file is locked to return. If set to true, | |
| 173 | /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file | |
| 174 | /// is available to proceed. | |
| 175 | /// In async I/O mode, non-blocking at the OS level is | |
| 176 | /// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned, | |
| 177 | /// and `false` means `error.WouldBlock` is handled by the event loop. | |
| 178 | lock_nonblocking: bool = false, | |
| 179 | ||
| 180 | /// For POSIX systems this is the file system mode the file will | |
| 181 | /// be created with. On other systems this is always 0. | |
| 182 | mode: Mode = default_mode, | |
| 183 | ||
| 184 | /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even | |
| 185 | /// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions | |
| 186 | /// related to opening the file, reading, writing, and locking. | |
| 187 | intended_io_mode: io.ModeOverride = io.default_mode, | |
| 188 | }; | |
| 189 | ||
| 190 | /// Upon success, the stream is in an uninitialized state. To continue using it, | |
| 191 | /// you must use the open() function. | |
| 192 | pub fn close(self: File) void { | |
| 193 | if (is_windows) { | |
| 194 | windows.CloseHandle(self.handle); | |
| 195 | } else if (self.capable_io_mode != self.intended_io_mode) { | |
| 196 | std.event.Loop.instance.?.close(self.handle); | |
| 197 | } else { | |
| 198 | posix.close(self.handle); | |
| 199 | } | |
| 200 | } | |
| 201 | ||
| 202 | pub const SyncError = posix.SyncError; | |
| 203 | ||
| 204 | /// Blocks until all pending file contents and metadata modifications | |
| 205 | /// for the file have been synchronized with the underlying filesystem. | |
| 206 | /// | |
| 207 | /// Note that this does not ensure that metadata for the | |
| 208 | /// directory containing the file has also reached disk. | |
| 209 | pub fn sync(self: File) SyncError!void { | |
| 210 | return posix.fsync(self.handle); | |
| 211 | } | |
| 212 | ||
| 213 | /// Test whether the file refers to a terminal. | |
| 214 | /// See also `supportsAnsiEscapeCodes`. | |
| 215 | pub fn isTty(self: File) bool { | |
| 216 | return posix.isatty(self.handle); | |
| 217 | } | |
| 218 | ||
| 219 | /// Test whether ANSI escape codes will be treated as such. | |
| 220 | pub fn supportsAnsiEscapeCodes(self: File) bool { | |
| 221 | if (builtin.os.tag == .windows) { | |
| 222 | var console_mode: windows.DWORD = 0; | |
| 223 | if (windows.kernel32.GetConsoleMode(self.handle, &console_mode) != 0) { | |
| 224 | if (console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true; | |
| 225 | } | |
| 226 | ||
| 227 | return posix.isCygwinPty(self.handle); | |
| 228 | } | |
| 229 | if (builtin.os.tag == .wasi) { | |
| 230 | // WASI sanitizes stdout when fd is a tty so ANSI escape codes | |
| 231 | // will not be interpreted as actual cursor commands, and | |
| 232 | // stderr is always sanitized. | |
| 233 | return false; | |
| 234 | } | |
| 235 | if (self.isTty()) { | |
| 236 | if (self.handle == posix.STDOUT_FILENO or self.handle == posix.STDERR_FILENO) { | |
| 237 | if (posix.getenvZ("TERM")) |term| { | |
| 238 | if (std.mem.eql(u8, term, "dumb")) | |
| 239 | return false; | |
| 240 | } | |
| 241 | } | |
| 242 | return true; | |
| 243 | } | |
| 244 | return false; | |
| 245 | } | |
| 246 | ||
| 247 | pub const SetEndPosError = posix.TruncateError; | |
| 248 | ||
| 249 | /// Shrinks or expands the file. | |
| 250 | /// The file offset after this call is left unchanged. | |
| 251 | pub fn setEndPos(self: File, length: u64) SetEndPosError!void { | |
| 252 | try posix.ftruncate(self.handle, length); | |
| 253 | } | |
| 254 | ||
| 255 | pub const SeekError = posix.SeekError; | |
| 256 | ||
| 257 | /// Repositions read/write file offset relative to the current offset. | |
| 258 | /// TODO: integrate with async I/O | |
| 259 | pub fn seekBy(self: File, offset: i64) SeekError!void { | |
| 260 | return posix.lseek_CUR(self.handle, offset); | |
| 261 | } | |
| 262 | ||
| 263 | /// Repositions read/write file offset relative to the end. | |
| 264 | /// TODO: integrate with async I/O | |
| 265 | pub fn seekFromEnd(self: File, offset: i64) SeekError!void { | |
| 266 | return posix.lseek_END(self.handle, offset); | |
| 267 | } | |
| 268 | ||
| 269 | /// Repositions read/write file offset relative to the beginning. | |
| 270 | /// TODO: integrate with async I/O | |
| 271 | pub fn seekTo(self: File, offset: u64) SeekError!void { | |
| 272 | return posix.lseek_SET(self.handle, offset); | |
| 273 | } | |
| 274 | ||
| 275 | pub const GetSeekPosError = posix.SeekError || posix.FStatError; | |
| 276 | ||
| 277 | /// TODO: integrate with async I/O | |
| 278 | pub fn getPos(self: File) GetSeekPosError!u64 { | |
| 279 | return posix.lseek_CUR_get(self.handle); | |
| 280 | } | |
| 281 | ||
| 282 | /// TODO: integrate with async I/O | |
| 283 | pub fn getEndPos(self: File) GetSeekPosError!u64 { | |
| 284 | if (builtin.os.tag == .windows) { | |
| 285 | return windows.GetFileSizeEx(self.handle); | |
| 286 | } | |
| 287 | return (try self.stat()).size; | |
| 288 | } | |
| 289 | ||
| 290 | pub const ModeError = posix.FStatError; | |
| 291 | ||
| 292 | /// TODO: integrate with async I/O | |
| 293 | pub fn mode(self: File) ModeError!Mode { | |
| 294 | if (builtin.os.tag == .windows) { | |
| 295 | return 0; | |
| 296 | } | |
| 297 | return (try self.stat()).mode; | |
| 298 | } | |
| 299 | ||
| 300 | pub const Stat = struct { | |
| 301 | /// A number that the system uses to point to the file metadata. This | |
| 302 | /// number is not guaranteed to be unique across time, as some file | |
| 303 | /// systems may reuse an inode after its file has been deleted. Some | |
| 304 | /// systems may change the inode of a file over time. | |
| 305 | /// | |
| 306 | /// On Linux, the inode is a structure that stores the metadata, and | |
| 307 | /// the inode _number_ is what you see here: the index number of the | |
| 308 | /// inode. | |
| 309 | /// | |
| 310 | /// The FileIndex on Windows is similar. It is a number for a file that | |
| 311 | /// is unique to each filesystem. | |
| 312 | inode: INode, | |
| 313 | size: u64, | |
| 314 | /// This is available on POSIX systems and is always 0 otherwise. | |
| 315 | mode: Mode, | |
| 316 | kind: Kind, | |
| 317 | ||
| 318 | /// Access time in nanoseconds, relative to UTC 1970-01-01. | |
| 319 | atime: i128, | |
| 320 | /// Last modification time in nanoseconds, relative to UTC 1970-01-01. | |
| 321 | mtime: i128, | |
| 322 | /// Creation time in nanoseconds, relative to UTC 1970-01-01. | |
| 323 | ctime: i128, | |
| 324 | ||
| 325 | pub fn fromSystem(st: posix.system.Stat) Stat { | |
| 326 | const atime = st.atime(); | |
| 327 | const mtime = st.mtime(); | |
| 328 | const ctime = st.ctime(); | |
| 329 | const kind: Kind = if (builtin.os.tag == .wasi and !builtin.link_libc) switch (st.filetype) { | |
| 330 | .BLOCK_DEVICE => .block_device, | |
| 331 | .CHARACTER_DEVICE => .character_device, | |
| 332 | .DIRECTORY => .directory, | |
| 333 | .SYMBOLIC_LINK => .sym_link, | |
| 334 | .REGULAR_FILE => .file, | |
| 335 | .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, | |
| 336 | else => .unknown, | |
| 337 | } else blk: { | |
| 338 | const m = st.mode & posix.S.IFMT; | |
| 339 | switch (m) { | |
| 340 | posix.S.IFBLK => break :blk .block_device, | |
| 341 | posix.S.IFCHR => break :blk .character_device, | |
| 342 | posix.S.IFDIR => break :blk .directory, | |
| 343 | posix.S.IFIFO => break :blk .named_pipe, | |
| 344 | posix.S.IFLNK => break :blk .sym_link, | |
| 345 | posix.S.IFREG => break :blk .file, | |
| 346 | posix.S.IFSOCK => break :blk .unix_domain_socket, | |
| 347 | else => {}, | |
| 348 | } | |
| 349 | if (builtin.os.tag.isSolarish()) switch (m) { | |
| 350 | posix.S.IFDOOR => break :blk .door, | |
| 351 | posix.S.IFPORT => break :blk .event_port, | |
| 352 | else => {}, | |
| 353 | }; | |
| 354 | ||
| 355 | break :blk .unknown; | |
| 356 | }; | |
| 357 | ||
| 358 | return Stat{ | |
| 359 | .inode = st.ino, | |
| 360 | .size = @as(u64, @bitCast(st.size)), | |
| 361 | .mode = st.mode, | |
| 362 | .kind = kind, | |
| 363 | .atime = @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec, | |
| 364 | .mtime = @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec, | |
| 365 | .ctime = @as(i128, ctime.tv_sec) * std.time.ns_per_s + ctime.tv_nsec, | |
| 366 | }; | |
| 367 | } | |
| 368 | }; | |
| 369 | ||
| 370 | pub const StatError = posix.FStatError; | |
| 371 | ||
| 372 | /// TODO: integrate with async I/O | |
| 373 | pub fn stat(self: File) StatError!Stat { | |
| 374 | if (builtin.os.tag == .windows) { | |
| 375 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 376 | var info: windows.FILE_ALL_INFORMATION = undefined; | |
| 377 | const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); | |
| 378 | switch (rc) { | |
| 379 | .SUCCESS => {}, | |
| 380 | // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer | |
| 381 | // size provided. This is treated as success because the type of variable-length information that this would be relevant for | |
| 382 | // (name, volume name, etc) we don't care about. | |
| 383 | .BUFFER_OVERFLOW => {}, | |
| 384 | .INVALID_PARAMETER => unreachable, | |
| 385 | .ACCESS_DENIED => return error.AccessDenied, | |
| 386 | else => return windows.unexpectedStatus(rc), | |
| 387 | } | |
| 388 | return Stat{ | |
| 389 | .inode = info.InternalInformation.IndexNumber, | |
| 390 | .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), | |
| 391 | .mode = 0, | |
| 392 | .kind = if (info.StandardInformation.Directory == 0) .file else .directory, | |
| 393 | .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), | |
| 394 | .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), | |
| 395 | .ctime = windows.fromSysTime(info.BasicInformation.CreationTime), | |
| 396 | }; | |
| 397 | } | |
| 398 | ||
| 399 | const st = try posix.fstat(self.handle); | |
| 400 | return Stat.fromSystem(st); | |
| 401 | } | |
| 402 | ||
| 403 | pub const ChmodError = posix.FChmodError; | |
| 404 | ||
| 405 | /// Changes the mode of the file. | |
| 406 | /// The process must have the correct privileges in order to do this | |
| 407 | /// successfully, or must have the effective user ID matching the owner | |
| 408 | /// of the file. | |
| 409 | pub fn chmod(self: File, new_mode: Mode) ChmodError!void { | |
| 410 | try posix.fchmod(self.handle, new_mode); | |
| 411 | } | |
| 412 | ||
| 413 | pub const ChownError = posix.FChownError; | |
| 414 | ||
| 415 | /// Changes the owner and group of the file. | |
| 416 | /// The process must have the correct privileges in order to do this | |
| 417 | /// successfully. The group may be changed by the owner of the file to | |
| 418 | /// any group of which the owner is a member. If the owner or group is | |
| 419 | /// specified as `null`, the ID is not changed. | |
| 420 | pub fn chown(self: File, owner: ?Uid, group: ?Gid) ChownError!void { | |
| 421 | try posix.fchown(self.handle, owner, group); | |
| 422 | } | |
| 423 | ||
| 424 | /// Cross-platform representation of permissions on a file. | |
| 425 | /// The `readonly` and `setReadonly` are the only methods available across all platforms. | |
| 426 | /// Platform-specific functionality is available through the `inner` field. | |
| 427 | pub const Permissions = struct { | |
| 428 | /// You may use the `inner` field to use platform-specific functionality | |
| 429 | inner: switch (builtin.os.tag) { | |
| 430 | .windows => PermissionsWindows, | |
| 431 | else => PermissionsUnix, | |
| 432 | }, | |
| 433 | ||
| 434 | const Self = @This(); | |
| 435 | ||
| 436 | /// Returns `true` if permissions represent an unwritable file. | |
| 437 | /// On Unix, `true` is returned only if no class has write permissions. | |
| 438 | pub fn readOnly(self: Self) bool { | |
| 439 | return self.inner.readOnly(); | |
| 440 | } | |
| 441 | ||
| 442 | /// Sets whether write permissions are provided. | |
| 443 | /// On Unix, this affects *all* classes. If this is undesired, use `unixSet`. | |
| 444 | /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` | |
| 445 | pub fn setReadOnly(self: *Self, read_only: bool) void { | |
| 446 | self.inner.setReadOnly(read_only); | |
| 447 | } | |
| 448 | }; | |
| 449 | ||
| 450 | pub const PermissionsWindows = struct { | |
| 451 | attributes: windows.DWORD, | |
| 452 | ||
| 453 | const Self = @This(); | |
| 454 | ||
| 455 | /// Returns `true` if permissions represent an unwritable file. | |
| 456 | pub fn readOnly(self: Self) bool { | |
| 457 | return self.attributes & windows.FILE_ATTRIBUTE_READONLY != 0; | |
| 458 | } | |
| 459 | ||
| 460 | /// Sets whether write permissions are provided. | |
| 461 | /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` | |
| 462 | pub fn setReadOnly(self: *Self, read_only: bool) void { | |
| 463 | if (read_only) { | |
| 464 | self.attributes |= windows.FILE_ATTRIBUTE_READONLY; | |
| 465 | } else { | |
| 466 | self.attributes &= ~@as(windows.DWORD, windows.FILE_ATTRIBUTE_READONLY); | |
| 467 | } | |
| 468 | } | |
| 469 | }; | |
| 470 | ||
| 471 | pub const PermissionsUnix = struct { | |
| 472 | mode: Mode, | |
| 473 | ||
| 474 | const Self = @This(); | |
| 475 | ||
| 476 | /// Returns `true` if permissions represent an unwritable file. | |
| 477 | /// `true` is returned only if no class has write permissions. | |
| 478 | pub fn readOnly(self: Self) bool { | |
| 479 | return self.mode & 0o222 == 0; | |
| 480 | } | |
| 481 | ||
| 482 | /// Sets whether write permissions are provided. | |
| 483 | /// This affects *all* classes. If this is undesired, use `unixSet`. | |
| 484 | /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` | |
| 485 | pub fn setReadOnly(self: *Self, read_only: bool) void { | |
| 486 | if (read_only) { | |
| 487 | self.mode &= ~@as(Mode, 0o222); | |
| 488 | } else { | |
| 489 | self.mode |= @as(Mode, 0o222); | |
| 490 | } | |
| 491 | } | |
| 492 | ||
| 493 | pub const Class = enum(u2) { | |
| 494 | user = 2, | |
| 495 | group = 1, | |
| 496 | other = 0, | |
| 497 | }; | |
| 498 | ||
| 499 | pub const Permission = enum(u3) { | |
| 500 | read = 0o4, | |
| 501 | write = 0o2, | |
| 502 | execute = 0o1, | |
| 503 | }; | |
| 504 | ||
| 505 | /// Returns `true` if the chosen class has the selected permission. | |
| 506 | /// This method is only available on Unix platforms. | |
| 507 | pub fn unixHas(self: Self, class: Class, permission: Permission) bool { | |
| 508 | const mask = @as(Mode, @intFromEnum(permission)) << @as(u3, @intFromEnum(class)) * 3; | |
| 509 | return self.mode & mask != 0; | |
| 510 | } | |
| 511 | ||
| 512 | /// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged. | |
| 513 | /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` | |
| 514 | pub fn unixSet(self: *Self, class: Class, permissions: struct { | |
| 515 | read: ?bool = null, | |
| 516 | write: ?bool = null, | |
| 517 | execute: ?bool = null, | |
| 518 | }) void { | |
| 519 | const shift = @as(u3, @intFromEnum(class)) * 3; | |
| 520 | if (permissions.read) |r| { | |
| 521 | if (r) { | |
| 522 | self.mode |= @as(Mode, 0o4) << shift; | |
| 523 | } else { | |
| 524 | self.mode &= ~(@as(Mode, 0o4) << shift); | |
| 525 | } | |
| 526 | } | |
| 527 | if (permissions.write) |w| { | |
| 528 | if (w) { | |
| 529 | self.mode |= @as(Mode, 0o2) << shift; | |
| 530 | } else { | |
| 531 | self.mode &= ~(@as(Mode, 0o2) << shift); | |
| 532 | } | |
| 533 | } | |
| 534 | if (permissions.execute) |x| { | |
| 535 | if (x) { | |
| 536 | self.mode |= @as(Mode, 0o1) << shift; | |
| 537 | } else { | |
| 538 | self.mode &= ~(@as(Mode, 0o1) << shift); | |
| 539 | } | |
| 540 | } | |
| 541 | } | |
| 542 | ||
| 543 | /// Returns a `Permissions` struct representing the permissions from the passed mode. | |
| 544 | pub fn unixNew(new_mode: Mode) Self { | |
| 545 | return Self{ | |
| 546 | .mode = new_mode, | |
| 547 | }; | |
| 548 | } | |
| 549 | }; | |
| 550 | ||
| 551 | pub const SetPermissionsError = ChmodError; | |
| 552 | ||
| 553 | /// Sets permissions according to the provided `Permissions` struct. | |
| 554 | /// This method is *NOT* available on WASI | |
| 555 | pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void { | |
| 556 | switch (builtin.os.tag) { | |
| 557 | .windows => { | |
| 558 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 559 | var info = windows.FILE_BASIC_INFORMATION{ | |
| 560 | .CreationTime = 0, | |
| 561 | .LastAccessTime = 0, | |
| 562 | .LastWriteTime = 0, | |
| 563 | .ChangeTime = 0, | |
| 564 | .FileAttributes = permissions.inner.attributes, | |
| 565 | }; | |
| 566 | const rc = windows.ntdll.NtSetInformationFile( | |
| 567 | self.handle, | |
| 568 | &io_status_block, | |
| 569 | &info, | |
| 570 | @sizeOf(windows.FILE_BASIC_INFORMATION), | |
| 571 | .FileBasicInformation, | |
| 572 | ); | |
| 573 | switch (rc) { | |
| 574 | .SUCCESS => return, | |
| 575 | .INVALID_HANDLE => unreachable, | |
| 576 | .ACCESS_DENIED => return error.AccessDenied, | |
| 577 | else => return windows.unexpectedStatus(rc), | |
| 578 | } | |
| 579 | }, | |
| 580 | .wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod | |
| 581 | else => { | |
| 582 | try self.chmod(permissions.inner.mode); | |
| 583 | }, | |
| 584 | } | |
| 585 | } | |
| 586 | ||
| 587 | /// Cross-platform representation of file metadata. | |
| 588 | /// Platform-specific functionality is available through the `inner` field. | |
| 589 | pub const Metadata = struct { | |
| 590 | /// You may use the `inner` field to use platform-specific functionality | |
| 591 | inner: switch (builtin.os.tag) { | |
| 592 | .windows => MetadataWindows, | |
| 593 | .linux => MetadataLinux, | |
| 594 | else => MetadataUnix, | |
| 595 | }, | |
| 596 | ||
| 597 | const Self = @This(); | |
| 598 | ||
| 599 | /// Returns the size of the file | |
| 600 | pub fn size(self: Self) u64 { | |
| 601 | return self.inner.size(); | |
| 602 | } | |
| 603 | ||
| 604 | /// Returns a `Permissions` struct, representing the permissions on the file | |
| 605 | pub fn permissions(self: Self) Permissions { | |
| 606 | return self.inner.permissions(); | |
| 607 | } | |
| 608 | ||
| 609 | /// Returns the `Kind` of file. | |
| 610 | /// On Windows, can only return: `.file`, `.directory`, `.sym_link` or `.unknown` | |
| 611 | pub fn kind(self: Self) Kind { | |
| 612 | return self.inner.kind(); | |
| 613 | } | |
| 614 | ||
| 615 | /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 | |
| 616 | pub fn accessed(self: Self) i128 { | |
| 617 | return self.inner.accessed(); | |
| 618 | } | |
| 619 | ||
| 620 | /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01 | |
| 621 | pub fn modified(self: Self) i128 { | |
| 622 | return self.inner.modified(); | |
| 623 | } | |
| 624 | ||
| 625 | /// Returns the time the file was created in nanoseconds since UTC 1970-01-01 | |
| 626 | /// On Windows, this cannot return null | |
| 627 | /// On Linux, this returns null if the filesystem does not support creation times, or if the kernel is older than 4.11 | |
| 628 | /// On Unices, this returns null if the filesystem or OS does not support creation times | |
| 629 | /// On MacOS, this returns the ctime if the filesystem does not support creation times; this is insanity, and yet another reason to hate on Apple | |
| 630 | pub fn created(self: Self) ?i128 { | |
| 631 | return self.inner.created(); | |
| 632 | } | |
| 633 | }; | |
| 634 | ||
| 635 | pub const MetadataUnix = struct { | |
| 636 | stat: posix.Stat, | |
| 637 | ||
| 638 | const Self = @This(); | |
| 639 | ||
| 640 | /// Returns the size of the file | |
| 641 | pub fn size(self: Self) u64 { | |
| 642 | return @as(u64, @intCast(self.stat.size)); | |
| 643 | } | |
| 644 | ||
| 645 | /// Returns a `Permissions` struct, representing the permissions on the file | |
| 646 | pub fn permissions(self: Self) Permissions { | |
| 647 | return Permissions{ .inner = PermissionsUnix{ .mode = self.stat.mode } }; | |
| 648 | } | |
| 649 | ||
| 650 | /// Returns the `Kind` of the file | |
| 651 | pub fn kind(self: Self) Kind { | |
| 652 | if (builtin.os.tag == .wasi and !builtin.link_libc) return switch (self.stat.filetype) { | |
| 653 | .BLOCK_DEVICE => .block_device, | |
| 654 | .CHARACTER_DEVICE => .character_device, | |
| 655 | .DIRECTORY => .directory, | |
| 656 | .SYMBOLIC_LINK => .sym_link, | |
| 657 | .REGULAR_FILE => .file, | |
| 658 | .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, | |
| 659 | else => .unknown, | |
| 660 | }; | |
| 661 | ||
| 662 | const m = self.stat.mode & posix.S.IFMT; | |
| 663 | ||
| 664 | switch (m) { | |
| 665 | posix.S.IFBLK => return .block_device, | |
| 666 | posix.S.IFCHR => return .character_device, | |
| 667 | posix.S.IFDIR => return .directory, | |
| 668 | posix.S.IFIFO => return .named_pipe, | |
| 669 | posix.S.IFLNK => return .sym_link, | |
| 670 | posix.S.IFREG => return .file, | |
| 671 | posix.S.IFSOCK => return .unix_domain_socket, | |
| 672 | else => {}, | |
| 673 | } | |
| 674 | ||
| 675 | if (builtin.os.tag.isSolarish()) switch (m) { | |
| 676 | posix.S.IFDOOR => return .door, | |
| 677 | posix.S.IFPORT => return .event_port, | |
| 678 | else => {}, | |
| 679 | }; | |
| 680 | ||
| 681 | return .unknown; | |
| 682 | } | |
| 683 | ||
| 684 | /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 | |
| 685 | pub fn accessed(self: Self) i128 { | |
| 686 | const atime = self.stat.atime(); | |
| 687 | return @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec; | |
| 688 | } | |
| 689 | ||
| 690 | /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01 | |
| 691 | pub fn modified(self: Self) i128 { | |
| 692 | const mtime = self.stat.mtime(); | |
| 693 | return @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec; | |
| 694 | } | |
| 695 | ||
| 696 | /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. | |
| 697 | /// Returns null if this is not supported by the OS or filesystem | |
| 698 | pub fn created(self: Self) ?i128 { | |
| 699 | if (!@hasDecl(@TypeOf(self.stat), "birthtime")) return null; | |
| 700 | const birthtime = self.stat.birthtime(); | |
| 701 | ||
| 702 | // If the filesystem doesn't support this the value *should* be: | |
| 703 | // On FreeBSD: tv_nsec = 0, tv_sec = -1 | |
| 704 | // On NetBSD and OpenBSD: tv_nsec = 0, tv_sec = 0 | |
| 705 | // On MacOS, it is set to ctime -- we cannot detect this!! | |
| 706 | switch (builtin.os.tag) { | |
| 707 | .freebsd => if (birthtime.tv_sec == -1 and birthtime.tv_nsec == 0) return null, | |
| 708 | .netbsd, .openbsd => if (birthtime.tv_sec == 0 and birthtime.tv_nsec == 0) return null, | |
| 709 | .macos => {}, | |
| 710 | else => @compileError("Creation time detection not implemented for OS"), | |
| 711 | } | |
| 712 | ||
| 713 | return @as(i128, birthtime.tv_sec) * std.time.ns_per_s + birthtime.tv_nsec; | |
| 714 | } | |
| 715 | }; | |
| 716 | ||
| 717 | /// `MetadataUnix`, but using Linux's `statx` syscall. | |
| 718 | /// On Linux versions below 4.11, `statx` will be filled with data from stat. | |
| 719 | pub const MetadataLinux = struct { | |
| 720 | statx: std.os.linux.Statx, | |
| 721 | ||
| 722 | const Self = @This(); | |
| 723 | ||
| 724 | /// Returns the size of the file | |
| 725 | pub fn size(self: Self) u64 { | |
| 726 | return self.statx.size; | |
| 727 | } | |
| 728 | ||
| 729 | /// Returns a `Permissions` struct, representing the permissions on the file | |
| 730 | pub fn permissions(self: Self) Permissions { | |
| 731 | return Permissions{ .inner = PermissionsUnix{ .mode = self.statx.mode } }; | |
| 732 | } | |
| 733 | ||
| 734 | /// Returns the `Kind` of the file | |
| 735 | pub fn kind(self: Self) Kind { | |
| 736 | const m = self.statx.mode & posix.S.IFMT; | |
| 737 | ||
| 738 | switch (m) { | |
| 739 | posix.S.IFBLK => return .block_device, | |
| 740 | posix.S.IFCHR => return .character_device, | |
| 741 | posix.S.IFDIR => return .directory, | |
| 742 | posix.S.IFIFO => return .named_pipe, | |
| 743 | posix.S.IFLNK => return .sym_link, | |
| 744 | posix.S.IFREG => return .file, | |
| 745 | posix.S.IFSOCK => return .unix_domain_socket, | |
| 746 | else => {}, | |
| 747 | } | |
| 748 | ||
| 749 | return .unknown; | |
| 750 | } | |
| 751 | ||
| 752 | /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 | |
| 753 | pub fn accessed(self: Self) i128 { | |
| 754 | return @as(i128, self.statx.atime.tv_sec) * std.time.ns_per_s + self.statx.atime.tv_nsec; | |
| 755 | } | |
| 756 | ||
| 757 | /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01 | |
| 758 | pub fn modified(self: Self) i128 { | |
| 759 | return @as(i128, self.statx.mtime.tv_sec) * std.time.ns_per_s + self.statx.mtime.tv_nsec; | |
| 760 | } | |
| 761 | ||
| 762 | /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. | |
| 763 | /// Returns null if this is not supported by the filesystem, or on kernels before than version 4.11 | |
| 764 | pub fn created(self: Self) ?i128 { | |
| 765 | if (self.statx.mask & std.os.linux.STATX_BTIME == 0) return null; | |
| 766 | return @as(i128, self.statx.btime.tv_sec) * std.time.ns_per_s + self.statx.btime.tv_nsec; | |
| 767 | } | |
| 768 | }; | |
| 769 | ||
| 770 | pub const MetadataWindows = struct { | |
| 771 | attributes: windows.DWORD, | |
| 772 | reparse_tag: windows.DWORD, | |
| 773 | _size: u64, | |
| 774 | access_time: i128, | |
| 775 | modified_time: i128, | |
| 776 | creation_time: i128, | |
| 777 | ||
| 778 | const Self = @This(); | |
| 779 | ||
| 780 | /// Returns the size of the file | |
| 781 | pub fn size(self: Self) u64 { | |
| 782 | return self._size; | |
| 783 | } | |
| 784 | ||
| 785 | /// Returns a `Permissions` struct, representing the permissions on the file | |
| 786 | pub fn permissions(self: Self) Permissions { | |
| 787 | return Permissions{ .inner = PermissionsWindows{ .attributes = self.attributes } }; | |
| 788 | } | |
| 789 | ||
| 790 | /// Returns the `Kind` of the file. | |
| 791 | /// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown` | |
| 792 | pub fn kind(self: Self) Kind { | |
| 793 | if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { | |
| 794 | if (self.reparse_tag & 0x20000000 != 0) { | |
| 795 | return .sym_link; | |
| 796 | } | |
| 797 | } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) { | |
| 798 | return .directory; | |
| 799 | } else { | |
| 800 | return .file; | |
| 801 | } | |
| 802 | return .unknown; | |
| 803 | } | |
| 804 | ||
| 805 | /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 | |
| 806 | pub fn accessed(self: Self) i128 { | |
| 807 | return self.access_time; | |
| 808 | } | |
| 809 | ||
| 810 | /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01 | |
| 811 | pub fn modified(self: Self) i128 { | |
| 812 | return self.modified_time; | |
| 813 | } | |
| 814 | ||
| 815 | /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. | |
| 816 | /// This never returns null, only returning an optional for compatibility with other OSes | |
| 817 | pub fn created(self: Self) ?i128 { | |
| 818 | return self.creation_time; | |
| 819 | } | |
| 820 | }; | |
| 821 | ||
| 822 | pub const MetadataError = posix.FStatError; | |
| 823 | ||
| 824 | pub fn metadata(self: File) MetadataError!Metadata { | |
| 825 | return Metadata{ | |
| 826 | .inner = switch (builtin.os.tag) { | |
| 827 | .windows => blk: { | |
| 828 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 829 | var info: windows.FILE_ALL_INFORMATION = undefined; | |
| 830 | ||
| 831 | const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); | |
| 832 | switch (rc) { | |
| 833 | .SUCCESS => {}, | |
| 834 | // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer | |
| 835 | // size provided. This is treated as success because the type of variable-length information that this would be relevant for | |
| 836 | // (name, volume name, etc) we don't care about. | |
| 837 | .BUFFER_OVERFLOW => {}, | |
| 838 | .INVALID_PARAMETER => unreachable, | |
| 839 | .ACCESS_DENIED => return error.AccessDenied, | |
| 840 | else => return windows.unexpectedStatus(rc), | |
| 841 | } | |
| 842 | ||
| 843 | const reparse_tag: windows.DWORD = reparse_blk: { | |
| 844 | if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { | |
| 845 | var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; | |
| 846 | try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]); | |
| 847 | const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0])); | |
| 848 | break :reparse_blk reparse_struct.ReparseTag; | |
| 849 | } | |
| 850 | break :reparse_blk 0; | |
| 851 | }; | |
| 852 | ||
| 853 | break :blk MetadataWindows{ | |
| 854 | .attributes = info.BasicInformation.FileAttributes, | |
| 855 | .reparse_tag = reparse_tag, | |
| 856 | ._size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), | |
| 857 | .access_time = windows.fromSysTime(info.BasicInformation.LastAccessTime), | |
| 858 | .modified_time = windows.fromSysTime(info.BasicInformation.LastWriteTime), | |
| 859 | .creation_time = windows.fromSysTime(info.BasicInformation.CreationTime), | |
| 860 | }; | |
| 861 | }, | |
| 862 | .linux => blk: { | |
| 863 | const l = std.os.linux; | |
| 864 | var stx = std.mem.zeroes(l.Statx); | |
| 865 | const rcx = l.statx(self.handle, "\x00", l.AT.EMPTY_PATH, l.STATX_TYPE | | |
| 866 | l.STATX_MODE | l.STATX_ATIME | l.STATX_MTIME | l.STATX_BTIME, &stx); | |
| 867 | ||
| 868 | switch (posix.errno(rcx)) { | |
| 869 | .SUCCESS => {}, | |
| 870 | // NOSYS happens when `statx` is unsupported, which is the case on kernel versions before 4.11 | |
| 871 | // Here, we call `fstat` and fill `stx` with the data we need | |
| 872 | .NOSYS => { | |
| 873 | const st = try posix.fstat(self.handle); | |
| 874 | ||
| 875 | stx.mode = @as(u16, @intCast(st.mode)); | |
| 876 | ||
| 877 | // Hacky conversion from timespec to statx_timestamp | |
| 878 | stx.atime = std.mem.zeroes(l.statx_timestamp); | |
| 879 | stx.atime.tv_sec = st.atim.tv_sec; | |
| 880 | stx.atime.tv_nsec = @as(u32, @intCast(st.atim.tv_nsec)); // Guaranteed to succeed (tv_nsec is always below 10^9) | |
| 881 | ||
| 882 | stx.mtime = std.mem.zeroes(l.statx_timestamp); | |
| 883 | stx.mtime.tv_sec = st.mtim.tv_sec; | |
| 884 | stx.mtime.tv_nsec = @as(u32, @intCast(st.mtim.tv_nsec)); | |
| 885 | ||
| 886 | stx.mask = l.STATX_BASIC_STATS | l.STATX_MTIME; | |
| 887 | }, | |
| 888 | .BADF => unreachable, | |
| 889 | .FAULT => unreachable, | |
| 890 | .NOMEM => return error.SystemResources, | |
| 891 | else => |err| return posix.unexpectedErrno(err), | |
| 892 | } | |
| 893 | ||
| 894 | break :blk MetadataLinux{ | |
| 895 | .statx = stx, | |
| 896 | }; | |
| 897 | }, | |
| 898 | else => blk: { | |
| 899 | const st = try posix.fstat(self.handle); | |
| 900 | break :blk MetadataUnix{ | |
| 901 | .stat = st, | |
| 902 | }; | |
| 903 | }, | |
| 904 | }, | |
| 905 | }; | |
| 906 | } | |
| 907 | ||
| 908 | pub const UpdateTimesError = posix.FutimensError || windows.SetFileTimeError; | |
| 909 | ||
| 910 | /// The underlying file system may have a different granularity than nanoseconds, | |
| 911 | /// and therefore this function cannot guarantee any precision will be stored. | |
| 912 | /// Further, the maximum value is limited by the system ABI. When a value is provided | |
| 913 | /// that exceeds this range, the value is clamped to the maximum. | |
| 914 | /// TODO: integrate with async I/O | |
| 915 | pub fn updateTimes( | |
| 916 | self: File, | |
| 917 | /// access timestamp in nanoseconds | |
| 918 | atime: i128, | |
| 919 | /// last modification timestamp in nanoseconds | |
| 920 | mtime: i128, | |
| 921 | ) UpdateTimesError!void { | |
| 922 | if (builtin.os.tag == .windows) { | |
| 923 | const atime_ft = windows.nanoSecondsToFileTime(atime); | |
| 924 | const mtime_ft = windows.nanoSecondsToFileTime(mtime); | |
| 925 | return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft); | |
| 926 | } | |
| 927 | const times = [2]posix.timespec{ | |
| 928 | posix.timespec{ | |
| 929 | .tv_sec = math.cast(isize, @divFloor(atime, std.time.ns_per_s)) orelse maxInt(isize), | |
| 930 | .tv_nsec = math.cast(isize, @mod(atime, std.time.ns_per_s)) orelse maxInt(isize), | |
| 931 | }, | |
| 932 | posix.timespec{ | |
| 933 | .tv_sec = math.cast(isize, @divFloor(mtime, std.time.ns_per_s)) orelse maxInt(isize), | |
| 934 | .tv_nsec = math.cast(isize, @mod(mtime, std.time.ns_per_s)) orelse maxInt(isize), | |
| 935 | }, | |
| 936 | }; | |
| 937 | try posix.futimens(self.handle, &times); | |
| 938 | } | |
| 939 | ||
| 940 | /// Reads all the bytes from the current position to the end of the file. | |
| 941 | /// On success, caller owns returned buffer. | |
| 942 | /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. | |
| 943 | pub fn readToEndAlloc(self: File, allocator: Allocator, max_bytes: usize) ![]u8 { | |
| 944 | return self.readToEndAllocOptions(allocator, max_bytes, null, @alignOf(u8), null); | |
| 945 | } | |
| 946 | ||
| 947 | /// Reads all the bytes from the current position to the end of the file. | |
| 948 | /// On success, caller owns returned buffer. | |
| 949 | /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. | |
| 950 | /// If `size_hint` is specified the initial buffer size is calculated using | |
| 951 | /// that value, otherwise an arbitrary value is used instead. | |
| 952 | /// Allows specifying alignment and a sentinel value. | |
| 953 | pub fn readToEndAllocOptions( | |
| 954 | self: File, | |
| 955 | allocator: Allocator, | |
| 956 | max_bytes: usize, | |
| 957 | size_hint: ?usize, | |
| 958 | comptime alignment: u29, | |
| 959 | comptime optional_sentinel: ?u8, | |
| 960 | ) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) { | |
| 961 | // If no size hint is provided fall back to the size=0 code path | |
| 962 | const size = size_hint orelse 0; | |
| 963 | ||
| 964 | // The file size returned by stat is used as hint to set the buffer | |
| 965 | // size. If the reported size is zero, as it happens on Linux for files | |
| 966 | // in /proc, a small buffer is allocated instead. | |
| 967 | const initial_cap = (if (size > 0) size else 1024) + @intFromBool(optional_sentinel != null); | |
| 968 | var array_list = try std.ArrayListAligned(u8, alignment).initCapacity(allocator, initial_cap); | |
| 969 | defer array_list.deinit(); | |
| 970 | ||
| 971 | self.reader().readAllArrayListAligned(alignment, &array_list, max_bytes) catch |err| switch (err) { | |
| 972 | error.StreamTooLong => return error.FileTooBig, | |
| 973 | else => |e| return e, | |
| 974 | }; | |
| 975 | ||
| 976 | if (optional_sentinel) |sentinel| { | |
| 977 | return try array_list.toOwnedSliceSentinel(sentinel); | |
| 978 | } else { | |
| 979 | return try array_list.toOwnedSlice(); | |
| 980 | } | |
| 981 | } | |
| 982 | ||
| 983 | pub const ReadError = posix.ReadError; | |
| 984 | pub const PReadError = posix.PReadError; | |
| 985 | ||
| 986 | pub fn read(self: File, buffer: []u8) ReadError!usize { | |
| 987 | if (is_windows) { | |
| 988 | return windows.ReadFile(self.handle, buffer, null, self.intended_io_mode); | |
| 989 | } | |
| 990 | ||
| 991 | if (self.intended_io_mode == .blocking) { | |
| 992 | return posix.read(self.handle, buffer); | |
| 993 | } else { | |
| 994 | return std.event.Loop.instance.?.read(self.handle, buffer, self.capable_io_mode != self.intended_io_mode); | |
| 995 | } | |
| 996 | } | |
| 997 | ||
| 998 | /// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it | |
| 999 | /// means the file reached the end. Reaching the end of a file is not an error condition. | |
| 1000 | pub fn readAll(self: File, buffer: []u8) ReadError!usize { | |
| 1001 | var index: usize = 0; | |
| 1002 | while (index != buffer.len) { | |
| 1003 | const amt = try self.read(buffer[index..]); | |
| 1004 | if (amt == 0) break; | |
| 1005 | index += amt; | |
| 1006 | } | |
| 1007 | return index; | |
| 1008 | } | |
| 1009 | ||
| 1010 | /// On Windows, this function currently does alter the file pointer. | |
| 1011 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1012 | pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize { | |
| 1013 | if (is_windows) { | |
| 1014 | return windows.ReadFile(self.handle, buffer, offset, self.intended_io_mode); | |
| 1015 | } | |
| 1016 | ||
| 1017 | if (self.intended_io_mode == .blocking) { | |
| 1018 | return posix.pread(self.handle, buffer, offset); | |
| 1019 | } else { | |
| 1020 | return std.event.Loop.instance.?.pread(self.handle, buffer, offset, self.capable_io_mode != self.intended_io_mode); | |
| 1021 | } | |
| 1022 | } | |
| 1023 | ||
| 1024 | /// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it | |
| 1025 | /// means the file reached the end. Reaching the end of a file is not an error condition. | |
| 1026 | /// On Windows, this function currently does alter the file pointer. | |
| 1027 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1028 | pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize { | |
| 1029 | var index: usize = 0; | |
| 1030 | while (index != buffer.len) { | |
| 1031 | const amt = try self.pread(buffer[index..], offset + index); | |
| 1032 | if (amt == 0) break; | |
| 1033 | index += amt; | |
| 1034 | } | |
| 1035 | return index; | |
| 1036 | } | |
| 1037 | ||
| 1038 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1039 | pub fn readv(self: File, iovecs: []const posix.iovec) ReadError!usize { | |
| 1040 | if (is_windows) { | |
| 1041 | // TODO improve this to use ReadFileScatter | |
| 1042 | if (iovecs.len == 0) return @as(usize, 0); | |
| 1043 | const first = iovecs[0]; | |
| 1044 | return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode); | |
| 1045 | } | |
| 1046 | ||
| 1047 | if (self.intended_io_mode == .blocking) { | |
| 1048 | return posix.readv(self.handle, iovecs); | |
| 1049 | } else { | |
| 1050 | return std.event.Loop.instance.?.readv(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode); | |
| 1051 | } | |
| 1052 | } | |
| 1053 | ||
| 1054 | /// Returns the number of bytes read. If the number read is smaller than the total bytes | |
| 1055 | /// from all the buffers, it means the file reached the end. Reaching the end of a file | |
| 1056 | /// is not an error condition. | |
| 1057 | /// | |
| 1058 | /// The `iovecs` parameter is mutable because: | |
| 1059 | /// * This function needs to mutate the fields in order to handle partial | |
| 1060 | /// reads from the underlying OS layer. | |
| 1061 | /// * The OS layer expects pointer addresses to be inside the application's address space | |
| 1062 | /// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer | |
| 1063 | /// addresses when the length is zero. So this function modifies the iov_base fields | |
| 1064 | /// when the length is zero. | |
| 1065 | /// | |
| 1066 | /// Related open issue: https://github.com/ziglang/zig/issues/7699 | |
| 1067 | pub fn readvAll(self: File, iovecs: []posix.iovec) ReadError!usize { | |
| 1068 | if (iovecs.len == 0) return 0; | |
| 1069 | ||
| 1070 | // We use the address of this local variable for all zero-length | |
| 1071 | // vectors so that the OS does not complain that we are giving it | |
| 1072 | // addresses outside the application's address space. | |
| 1073 | var garbage: [1]u8 = undefined; | |
| 1074 | for (iovecs) |*v| { | |
| 1075 | if (v.iov_len == 0) v.iov_base = &garbage; | |
| 1076 | } | |
| 1077 | ||
| 1078 | var i: usize = 0; | |
| 1079 | var off: usize = 0; | |
| 1080 | while (true) { | |
| 1081 | var amt = try self.readv(iovecs[i..]); | |
| 1082 | var eof = amt == 0; | |
| 1083 | off += amt; | |
| 1084 | while (amt >= iovecs[i].iov_len) { | |
| 1085 | amt -= iovecs[i].iov_len; | |
| 1086 | i += 1; | |
| 1087 | if (i >= iovecs.len) return off; | |
| 1088 | eof = false; | |
| 1089 | } | |
| 1090 | if (eof) return off; | |
| 1091 | iovecs[i].iov_base += amt; | |
| 1092 | iovecs[i].iov_len -= amt; | |
| 1093 | } | |
| 1094 | } | |
| 1095 | ||
| 1096 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1097 | /// On Windows, this function currently does alter the file pointer. | |
| 1098 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1099 | pub fn preadv(self: File, iovecs: []const posix.iovec, offset: u64) PReadError!usize { | |
| 1100 | if (is_windows) { | |
| 1101 | // TODO improve this to use ReadFileScatter | |
| 1102 | if (iovecs.len == 0) return @as(usize, 0); | |
| 1103 | const first = iovecs[0]; | |
| 1104 | return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode); | |
| 1105 | } | |
| 1106 | ||
| 1107 | if (self.intended_io_mode == .blocking) { | |
| 1108 | return posix.preadv(self.handle, iovecs, offset); | |
| 1109 | } else { | |
| 1110 | return std.event.Loop.instance.?.preadv(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode); | |
| 1111 | } | |
| 1112 | } | |
| 1113 | ||
| 1114 | /// Returns the number of bytes read. If the number read is smaller than the total bytes | |
| 1115 | /// from all the buffers, it means the file reached the end. Reaching the end of a file | |
| 1116 | /// is not an error condition. | |
| 1117 | /// The `iovecs` parameter is mutable because this function needs to mutate the fields in | |
| 1118 | /// order to handle partial reads from the underlying OS layer. | |
| 1119 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1120 | /// On Windows, this function currently does alter the file pointer. | |
| 1121 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1122 | pub fn preadvAll(self: File, iovecs: []posix.iovec, offset: u64) PReadError!usize { | |
| 1123 | if (iovecs.len == 0) return 0; | |
| 1124 | ||
| 1125 | var i: usize = 0; | |
| 1126 | var off: usize = 0; | |
| 1127 | while (true) { | |
| 1128 | var amt = try self.preadv(iovecs[i..], offset + off); | |
| 1129 | var eof = amt == 0; | |
| 1130 | off += amt; | |
| 1131 | while (amt >= iovecs[i].iov_len) { | |
| 1132 | amt -= iovecs[i].iov_len; | |
| 1133 | i += 1; | |
| 1134 | if (i >= iovecs.len) return off; | |
| 1135 | eof = false; | |
| 1136 | } | |
| 1137 | if (eof) return off; | |
| 1138 | iovecs[i].iov_base += amt; | |
| 1139 | iovecs[i].iov_len -= amt; | |
| 1140 | } | |
| 1141 | } | |
| 1142 | ||
| 1143 | pub const WriteError = posix.WriteError; | |
| 1144 | pub const PWriteError = posix.PWriteError; | |
| 1145 | ||
| 1146 | pub fn write(self: File, bytes: []const u8) WriteError!usize { | |
| 1147 | if (is_windows) { | |
| 1148 | return windows.WriteFile(self.handle, bytes, null, self.intended_io_mode); | |
| 1149 | } | |
| 1150 | ||
| 1151 | if (self.intended_io_mode == .blocking) { | |
| 1152 | return posix.write(self.handle, bytes); | |
| 1153 | } else { | |
| 1154 | return std.event.Loop.instance.?.write(self.handle, bytes, self.capable_io_mode != self.intended_io_mode); | |
| 1155 | } | |
| 1156 | } | |
| 1157 | ||
| 1158 | pub fn writeAll(self: File, bytes: []const u8) WriteError!void { | |
| 1159 | var index: usize = 0; | |
| 1160 | while (index < bytes.len) { | |
| 1161 | index += try self.write(bytes[index..]); | |
| 1162 | } | |
| 1163 | } | |
| 1164 | ||
| 1165 | /// On Windows, this function currently does alter the file pointer. | |
| 1166 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1167 | pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize { | |
| 1168 | if (is_windows) { | |
| 1169 | return windows.WriteFile(self.handle, bytes, offset, self.intended_io_mode); | |
| 1170 | } | |
| 1171 | ||
| 1172 | if (self.intended_io_mode == .blocking) { | |
| 1173 | return posix.pwrite(self.handle, bytes, offset); | |
| 1174 | } else { | |
| 1175 | return std.event.Loop.instance.?.pwrite(self.handle, bytes, offset, self.capable_io_mode != self.intended_io_mode); | |
| 1176 | } | |
| 1177 | } | |
| 1178 | ||
| 1179 | /// On Windows, this function currently does alter the file pointer. | |
| 1180 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1181 | pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void { | |
| 1182 | var index: usize = 0; | |
| 1183 | while (index < bytes.len) { | |
| 1184 | index += try self.pwrite(bytes[index..], offset + index); | |
| 1185 | } | |
| 1186 | } | |
| 1187 | ||
| 1188 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1189 | /// See equivalent function: `std.net.Stream.writev`. | |
| 1190 | pub fn writev(self: File, iovecs: []const posix.iovec_const) WriteError!usize { | |
| 1191 | if (is_windows) { | |
| 1192 | // TODO improve this to use WriteFileScatter | |
| 1193 | if (iovecs.len == 0) return @as(usize, 0); | |
| 1194 | const first = iovecs[0]; | |
| 1195 | return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode); | |
| 1196 | } | |
| 1197 | ||
| 1198 | if (self.intended_io_mode == .blocking) { | |
| 1199 | return posix.writev(self.handle, iovecs); | |
| 1200 | } else { | |
| 1201 | return std.event.Loop.instance.?.writev(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode); | |
| 1202 | } | |
| 1203 | } | |
| 1204 | ||
| 1205 | /// The `iovecs` parameter is mutable because: | |
| 1206 | /// * This function needs to mutate the fields in order to handle partial | |
| 1207 | /// writes from the underlying OS layer. | |
| 1208 | /// * The OS layer expects pointer addresses to be inside the application's address space | |
| 1209 | /// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer | |
| 1210 | /// addresses when the length is zero. So this function modifies the iov_base fields | |
| 1211 | /// when the length is zero. | |
| 1212 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1213 | /// See equivalent function: `std.net.Stream.writevAll`. | |
| 1214 | pub fn writevAll(self: File, iovecs: []posix.iovec_const) WriteError!void { | |
| 1215 | if (iovecs.len == 0) return; | |
| 1216 | ||
| 1217 | // We use the address of this local variable for all zero-length | |
| 1218 | // vectors so that the OS does not complain that we are giving it | |
| 1219 | // addresses outside the application's address space. | |
| 1220 | var garbage: [1]u8 = undefined; | |
| 1221 | for (iovecs) |*v| { | |
| 1222 | if (v.iov_len == 0) v.iov_base = &garbage; | |
| 1223 | } | |
| 1224 | ||
| 1225 | var i: usize = 0; | |
| 1226 | while (true) { | |
| 1227 | var amt = try self.writev(iovecs[i..]); | |
| 1228 | while (amt >= iovecs[i].iov_len) { | |
| 1229 | amt -= iovecs[i].iov_len; | |
| 1230 | i += 1; | |
| 1231 | if (i >= iovecs.len) return; | |
| 1232 | } | |
| 1233 | iovecs[i].iov_base += amt; | |
| 1234 | iovecs[i].iov_len -= amt; | |
| 1235 | } | |
| 1236 | } | |
| 1237 | ||
| 1238 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1239 | /// On Windows, this function currently does alter the file pointer. | |
| 1240 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1241 | pub fn pwritev(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError!usize { | |
| 1242 | if (is_windows) { | |
| 1243 | // TODO improve this to use WriteFileScatter | |
| 1244 | if (iovecs.len == 0) return @as(usize, 0); | |
| 1245 | const first = iovecs[0]; | |
| 1246 | return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode); | |
| 1247 | } | |
| 1248 | ||
| 1249 | if (self.intended_io_mode == .blocking) { | |
| 1250 | return posix.pwritev(self.handle, iovecs, offset); | |
| 1251 | } else { | |
| 1252 | return std.event.Loop.instance.?.pwritev(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode); | |
| 1253 | } | |
| 1254 | } | |
| 1255 | ||
| 1256 | /// The `iovecs` parameter is mutable because this function needs to mutate the fields in | |
| 1257 | /// order to handle partial writes from the underlying OS layer. | |
| 1258 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1259 | /// On Windows, this function currently does alter the file pointer. | |
| 1260 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1261 | pub fn pwritevAll(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError!void { | |
| 1262 | if (iovecs.len == 0) return; | |
| 1263 | ||
| 1264 | var i: usize = 0; | |
| 1265 | var off: u64 = 0; | |
| 1266 | while (true) { | |
| 1267 | var amt = try self.pwritev(iovecs[i..], offset + off); | |
| 1268 | off += amt; | |
| 1269 | while (amt >= iovecs[i].iov_len) { | |
| 1270 | amt -= iovecs[i].iov_len; | |
| 1271 | i += 1; | |
| 1272 | if (i >= iovecs.len) return; | |
| 1273 | } | |
| 1274 | iovecs[i].iov_base += amt; | |
| 1275 | iovecs[i].iov_len -= amt; | |
| 1276 | } | |
| 1277 | } | |
| 1278 | ||
| 1279 | pub const CopyRangeError = posix.CopyFileRangeError; | |
| 1280 | ||
| 1281 | pub fn copyRange(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { | |
| 1282 | const adjusted_len = math.cast(usize, len) orelse maxInt(usize); | |
| 1283 | const result = try posix.copy_file_range(in.handle, in_offset, out.handle, out_offset, adjusted_len, 0); | |
| 1284 | return result; | |
| 1285 | } | |
| 1286 | ||
| 1287 | /// Returns the number of bytes copied. If the number read is smaller than `buffer.len`, it | |
| 1288 | /// means the in file reached the end. Reaching the end of a file is not an error condition. | |
| 1289 | pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { | |
| 1290 | var total_bytes_copied: u64 = 0; | |
| 1291 | var in_off = in_offset; | |
| 1292 | var out_off = out_offset; | |
| 1293 | while (total_bytes_copied < len) { | |
| 1294 | const amt_copied = try copyRange(in, in_off, out, out_off, len - total_bytes_copied); | |
| 1295 | if (amt_copied == 0) return total_bytes_copied; | |
| 1296 | total_bytes_copied += amt_copied; | |
| 1297 | in_off += amt_copied; | |
| 1298 | out_off += amt_copied; | |
| 1299 | } | |
| 1300 | return total_bytes_copied; | |
| 1301 | } | |
| 1302 | ||
| 1303 | pub const WriteFileOptions = struct { | |
| 1304 | in_offset: u64 = 0, | |
| 1305 | ||
| 1306 | /// `null` means the entire file. `0` means no bytes from the file. | |
| 1307 | /// When this is `null`, trailers must be sent in a separate writev() call | |
| 1308 | /// due to a flaw in the BSD sendfile API. Other operating systems, such as | |
| 1309 | /// Linux, already do this anyway due to API limitations. | |
| 1310 | /// If the size of the source file is known, passing the size here will save one syscall. | |
| 1311 | in_len: ?u64 = null, | |
| 1312 | ||
| 1313 | headers_and_trailers: []posix.iovec_const = &[0]posix.iovec_const{}, | |
| 1314 | ||
| 1315 | /// The trailer count is inferred from `headers_and_trailers.len - header_count` | |
| 1316 | header_count: usize = 0, | |
| 1317 | }; | |
| 1318 | ||
| 1319 | pub const WriteFileError = ReadError || error{EndOfStream} || WriteError; | |
| 1320 | ||
| 1321 | pub fn writeFileAll(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void { | |
| 1322 | return self.writeFileAllSendfile(in_file, args) catch |err| switch (err) { | |
| 1323 | error.Unseekable, | |
| 1324 | error.FastOpenAlreadyInProgress, | |
| 1325 | error.MessageTooBig, | |
| 1326 | error.FileDescriptorNotASocket, | |
| 1327 | error.NetworkUnreachable, | |
| 1328 | error.NetworkSubsystemFailed, | |
| 1329 | => return self.writeFileAllUnseekable(in_file, args), | |
| 1330 | ||
| 1331 | else => |e| return e, | |
| 1332 | }; | |
| 1333 | } | |
| 1334 | ||
| 1335 | /// Does not try seeking in either of the File parameters. | |
| 1336 | /// See `writeFileAll` as an alternative to calling this. | |
| 1337 | pub fn writeFileAllUnseekable(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void { | |
| 1338 | const headers = args.headers_and_trailers[0..args.header_count]; | |
| 1339 | const trailers = args.headers_and_trailers[args.header_count..]; | |
| 1340 | ||
| 1341 | try self.writevAll(headers); | |
| 1342 | ||
| 1343 | try in_file.reader().skipBytes(args.in_offset, .{ .buf_size = 4096 }); | |
| 1344 | ||
| 1345 | var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init(); | |
| 1346 | if (args.in_len) |len| { | |
| 1347 | var stream = std.io.limitedReader(in_file.reader(), len); | |
| 1348 | try fifo.pump(stream.reader(), self.writer()); | |
| 1349 | } else { | |
| 1350 | try fifo.pump(in_file.reader(), self.writer()); | |
| 1351 | } | |
| 1352 | ||
| 1353 | try self.writevAll(trailers); | |
| 1354 | } | |
| 1355 | ||
| 1356 | /// Low level function which can fail for OS-specific reasons. | |
| 1357 | /// See `writeFileAll` as an alternative to calling this. | |
| 1358 | /// TODO integrate with async I/O | |
| 1359 | fn writeFileAllSendfile(self: File, in_file: File, args: WriteFileOptions) posix.SendFileError!void { | |
| 1360 | const count = blk: { | |
| 1361 | if (args.in_len) |l| { | |
| 1362 | if (l == 0) { | |
| 1363 | return self.writevAll(args.headers_and_trailers); | |
| 1364 | } else { | |
| 1365 | break :blk l; | |
| 1366 | } | |
| 1367 | } else { | |
| 1368 | break :blk 0; | |
| 1369 | } | |
| 1370 | }; | |
| 1371 | const headers = args.headers_and_trailers[0..args.header_count]; | |
| 1372 | const trailers = args.headers_and_trailers[args.header_count..]; | |
| 1373 | const zero_iovec = &[0]posix.iovec_const{}; | |
| 1374 | // When reading the whole file, we cannot put the trailers in the sendfile() syscall, | |
| 1375 | // because we have no way to determine whether a partial write is past the end of the file or not. | |
| 1376 | const trls = if (count == 0) zero_iovec else trailers; | |
| 1377 | const offset = args.in_offset; | |
| 1378 | const out_fd = self.handle; | |
| 1379 | const in_fd = in_file.handle; | |
| 1380 | const flags = 0; | |
| 1381 | var amt: usize = 0; | |
| 1382 | hdrs: { | |
| 1383 | var i: usize = 0; | |
| 1384 | while (i < headers.len) { | |
| 1385 | amt = try posix.sendfile(out_fd, in_fd, offset, count, headers[i..], trls, flags); | |
| 1386 | while (amt >= headers[i].iov_len) { | |
| 1387 | amt -= headers[i].iov_len; | |
| 1388 | i += 1; | |
| 1389 | if (i >= headers.len) break :hdrs; | |
| 1390 | } | |
| 1391 | headers[i].iov_base += amt; | |
| 1392 | headers[i].iov_len -= amt; | |
| 1393 | } | |
| 1394 | } | |
| 1395 | if (count == 0) { | |
| 1396 | var off: u64 = amt; | |
| 1397 | while (true) { | |
| 1398 | amt = try posix.sendfile(out_fd, in_fd, offset + off, 0, zero_iovec, zero_iovec, flags); | |
| 1399 | if (amt == 0) break; | |
| 1400 | off += amt; | |
| 1401 | } | |
| 1402 | } else { | |
| 1403 | var off: u64 = amt; | |
| 1404 | while (off < count) { | |
| 1405 | amt = try posix.sendfile(out_fd, in_fd, offset + off, count - off, zero_iovec, trailers, flags); | |
| 1406 | off += amt; | |
| 1407 | } | |
| 1408 | amt = @as(usize, @intCast(off - count)); | |
| 1409 | } | |
| 1410 | var i: usize = 0; | |
| 1411 | while (i < trailers.len) { | |
| 1412 | while (amt >= trailers[i].iov_len) { | |
| 1413 | amt -= trailers[i].iov_len; | |
| 1414 | i += 1; | |
| 1415 | if (i >= trailers.len) return; | |
| 1416 | } | |
| 1417 | trailers[i].iov_base += amt; | |
| 1418 | trailers[i].iov_len -= amt; | |
| 1419 | amt = try posix.writev(self.handle, trailers[i..]); | |
| 1420 | } | |
| 1421 | } | |
| 1422 | ||
| 1423 | pub const Reader = io.Reader(File, ReadError, read); | |
| 1424 | ||
| 1425 | pub fn reader(file: File) Reader { | |
| 1426 | return .{ .context = file }; | |
| 1427 | } | |
| 1428 | ||
| 1429 | pub const Writer = io.Writer(File, WriteError, write); | |
| 1430 | ||
| 1431 | pub fn writer(file: File) Writer { | |
| 1432 | return .{ .context = file }; | |
| 1433 | } | |
| 1434 | ||
| 1435 | pub const SeekableStream = io.SeekableStream( | |
| 1436 | File, | |
| 1437 | SeekError, | |
| 1438 | GetSeekPosError, | |
| 1439 | seekTo, | |
| 1440 | seekBy, | |
| 1441 | getPos, | |
| 1442 | getEndPos, | |
| 1443 | ); | |
| 1444 | ||
| 1445 | pub fn seekableStream(file: File) SeekableStream { | |
| 1446 | return .{ .context = file }; | |
| 1447 | } | |
| 1448 | ||
| 1449 | const range_off: windows.LARGE_INTEGER = 0; | |
| 1450 | const range_len: windows.LARGE_INTEGER = 1; | |
| 1451 | ||
| 1452 | pub const LockError = error{ | |
| 1453 | SystemResources, | |
| 1454 | FileLocksNotSupported, | |
| 1455 | } || posix.UnexpectedError; | |
| 1456 | ||
| 1457 | /// Blocks when an incompatible lock is held by another process. | |
| 1458 | /// A process may hold only one type of lock (shared or exclusive) on | |
| 1459 | /// a file. When a process terminates in any way, the lock is released. | |
| 1460 | /// | |
| 1461 | /// Assumes the file is unlocked. | |
| 1462 | /// | |
| 1463 | /// TODO: integrate with async I/O | |
| 1464 | pub fn lock(file: File, l: Lock) LockError!void { | |
| 1465 | if (is_windows) { | |
| 1466 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 1467 | const exclusive = switch (l) { | |
| 1468 | .none => return, | |
| 1469 | .shared => false, | |
| 1470 | .exclusive => true, | |
| 1471 | }; | |
| 1472 | return windows.LockFile( | |
| 1473 | file.handle, | |
| 1474 | null, | |
| 1475 | null, | |
| 1476 | null, | |
| 1477 | &io_status_block, | |
| 1478 | &range_off, | |
| 1479 | &range_len, | |
| 1480 | null, | |
| 1481 | windows.FALSE, // non-blocking=false | |
| 1482 | @intFromBool(exclusive), | |
| 1483 | ) catch |err| switch (err) { | |
| 1484 | error.WouldBlock => unreachable, // non-blocking=false | |
| 1485 | else => |e| return e, | |
| 1486 | }; | |
| 1487 | } else { | |
| 1488 | return posix.flock(file.handle, switch (l) { | |
| 1489 | .none => posix.LOCK.UN, | |
| 1490 | .shared => posix.LOCK.SH, | |
| 1491 | .exclusive => posix.LOCK.EX, | |
| 1492 | }) catch |err| switch (err) { | |
| 1493 | error.WouldBlock => unreachable, // non-blocking=false | |
| 1494 | else => |e| return e, | |
| 1495 | }; | |
| 1496 | } | |
| 1497 | } | |
| 1498 | ||
| 1499 | /// Assumes the file is locked. | |
| 1500 | pub fn unlock(file: File) void { | |
| 1501 | if (is_windows) { | |
| 1502 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 1503 | return windows.UnlockFile( | |
| 1504 | file.handle, | |
| 1505 | &io_status_block, | |
| 1506 | &range_off, | |
| 1507 | &range_len, | |
| 1508 | null, | |
| 1509 | ) catch |err| switch (err) { | |
| 1510 | error.RangeNotLocked => unreachable, // Function assumes unlocked. | |
| 1511 | error.Unexpected => unreachable, // Resource deallocation must succeed. | |
| 1512 | }; | |
| 1513 | } else { | |
| 1514 | return posix.flock(file.handle, posix.LOCK.UN) catch |err| switch (err) { | |
| 1515 | error.WouldBlock => unreachable, // unlocking can't block | |
| 1516 | error.SystemResources => unreachable, // We are deallocating resources. | |
| 1517 | error.FileLocksNotSupported => unreachable, // We already got the lock. | |
| 1518 | error.Unexpected => unreachable, // Resource deallocation must succeed. | |
| 1519 | }; | |
| 1520 | } | |
| 1521 | } | |
| 1522 | ||
| 1523 | /// Attempts to obtain a lock, returning `true` if the lock is | |
| 1524 | /// obtained, and `false` if there was an existing incompatible lock held. | |
| 1525 | /// A process may hold only one type of lock (shared or exclusive) on | |
| 1526 | /// a file. When a process terminates in any way, the lock is released. | |
| 1527 | /// | |
| 1528 | /// Assumes the file is unlocked. | |
| 1529 | /// | |
| 1530 | /// TODO: integrate with async I/O | |
| 1531 | pub fn tryLock(file: File, l: Lock) LockError!bool { | |
| 1532 | if (is_windows) { | |
| 1533 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 1534 | const exclusive = switch (l) { | |
| 1535 | .none => return, | |
| 1536 | .shared => false, | |
| 1537 | .exclusive => true, | |
| 1538 | }; | |
| 1539 | windows.LockFile( | |
| 1540 | file.handle, | |
| 1541 | null, | |
| 1542 | null, | |
| 1543 | null, | |
| 1544 | &io_status_block, | |
| 1545 | &range_off, | |
| 1546 | &range_len, | |
| 1547 | null, | |
| 1548 | windows.TRUE, // non-blocking=true | |
| 1549 | @intFromBool(exclusive), | |
| 1550 | ) catch |err| switch (err) { | |
| 1551 | error.WouldBlock => return false, | |
| 1552 | else => |e| return e, | |
| 1553 | }; | |
| 1554 | } else { | |
| 1555 | posix.flock(file.handle, switch (l) { | |
| 1556 | .none => posix.LOCK.UN, | |
| 1557 | .shared => posix.LOCK.SH | posix.LOCK.NB, | |
| 1558 | .exclusive => posix.LOCK.EX | posix.LOCK.NB, | |
| 1559 | }) catch |err| switch (err) { | |
| 1560 | error.WouldBlock => return false, | |
| 1561 | else => |e| return e, | |
| 1562 | }; | |
| 1563 | } | |
| 1564 | return true; | |
| 1565 | } | |
| 1566 | ||
| 1567 | /// Assumes the file is already locked in exclusive mode. | |
| 1568 | /// Atomically modifies the lock to be in shared mode, without releasing it. | |
| 1569 | /// | |
| 1570 | /// TODO: integrate with async I/O | |
| 1571 | pub fn downgradeLock(file: File) LockError!void { | |
| 1572 | if (is_windows) { | |
| 1573 | // On Windows it works like a semaphore + exclusivity flag. To implement this | |
| 1574 | // function, we first obtain another lock in shared mode. This changes the | |
| 1575 | // exclusivity flag, but increments the semaphore to 2. So we follow up with | |
| 1576 | // an NtUnlockFile which decrements the semaphore but does not modify the | |
| 1577 | // exclusivity flag. | |
| 1578 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 1579 | windows.LockFile( | |
| 1580 | file.handle, | |
| 1581 | null, | |
| 1582 | null, | |
| 1583 | null, | |
| 1584 | &io_status_block, | |
| 1585 | &range_off, | |
| 1586 | &range_len, | |
| 1587 | null, | |
| 1588 | windows.TRUE, // non-blocking=true | |
| 1589 | windows.FALSE, // exclusive=false | |
| 1590 | ) catch |err| switch (err) { | |
| 1591 | error.WouldBlock => unreachable, // File was not locked in exclusive mode. | |
| 1592 | else => |e| return e, | |
| 1593 | }; | |
| 1594 | return windows.UnlockFile( | |
| 1595 | file.handle, | |
| 1596 | &io_status_block, | |
| 1597 | &range_off, | |
| 1598 | &range_len, | |
| 1599 | null, | |
| 1600 | ) catch |err| switch (err) { | |
| 1601 | error.RangeNotLocked => unreachable, // File was not locked. | |
| 1602 | error.Unexpected => unreachable, // Resource deallocation must succeed. | |
| 1603 | }; | |
| 1604 | } else { | |
| 1605 | return posix.flock(file.handle, posix.LOCK.SH | posix.LOCK.NB) catch |err| switch (err) { | |
| 1606 | error.WouldBlock => unreachable, // File was not locked in exclusive mode. | |
| 1607 | else => |e| return e, | |
| 1608 | }; | |
| 1609 | } | |
| 1610 | } | |
| 1611 | ||
| 1612 | const File = @This(); | |
| 1613 | const std = @import("../std.zig"); | |
| 1614 | const builtin = @import("builtin"); | |
| 1615 | const Allocator = std.mem.Allocator; | |
| 1616 | // https://github.com/ziglang/zig/issues/5019 | |
| 1617 | const posix = std.os; | |
| 1618 | const io = std.io; | |
| 1619 | const math = std.math; | |
| 1620 | const assert = std.debug.assert; | |
| 1621 | const windows = std.os.windows; | |
| 1622 | const Os = std.builtin.Os; | |
| 1623 | const maxInt = std.math.maxInt; | |
| 1624 | const is_windows = builtin.os.tag == .windows; |
lib/std/fs/file.zig deleted-1622| ... | ... | @@ -1,1622 +0,0 @@ |
| 1 | const std = @import("../std.zig"); | |
| 2 | const builtin = @import("builtin"); | |
| 3 | const os = std.os; | |
| 4 | const io = std.io; | |
| 5 | const mem = std.mem; | |
| 6 | const math = std.math; | |
| 7 | const assert = std.debug.assert; | |
| 8 | const windows = os.windows; | |
| 9 | const Os = std.builtin.Os; | |
| 10 | const maxInt = std.math.maxInt; | |
| 11 | const is_windows = builtin.os.tag == .windows; | |
| 12 | ||
| 13 | pub const File = struct { | |
| 14 | /// The OS-specific file descriptor or file handle. | |
| 15 | handle: Handle, | |
| 16 | ||
| 17 | /// On some systems, such as Linux, file system file descriptors are incapable | |
| 18 | /// of non-blocking I/O. This forces us to perform asynchronous I/O on a dedicated thread, | |
| 19 | /// to achieve non-blocking file-system I/O. To do this, `File` must be aware of whether | |
| 20 | /// it is a file system file descriptor, or, more specifically, whether the I/O is always | |
| 21 | /// blocking. | |
| 22 | capable_io_mode: io.ModeOverride = io.default_mode, | |
| 23 | ||
| 24 | /// Furthermore, even when `std.options.io_mode` is async, it is still sometimes desirable | |
| 25 | /// to perform blocking I/O, although not by default. For example, when printing a | |
| 26 | /// stack trace to stderr. This field tracks both by acting as an overriding I/O mode. | |
| 27 | /// When not building in async I/O mode, the type only has the `.blocking` tag, making | |
| 28 | /// it a zero-bit type. | |
| 29 | intended_io_mode: io.ModeOverride = io.default_mode, | |
| 30 | ||
| 31 | pub const Handle = os.fd_t; | |
| 32 | pub const Mode = os.mode_t; | |
| 33 | pub const INode = os.ino_t; | |
| 34 | pub const Uid = os.uid_t; | |
| 35 | pub const Gid = os.gid_t; | |
| 36 | ||
| 37 | pub const Kind = enum { | |
| 38 | block_device, | |
| 39 | character_device, | |
| 40 | directory, | |
| 41 | named_pipe, | |
| 42 | sym_link, | |
| 43 | file, | |
| 44 | unix_domain_socket, | |
| 45 | whiteout, | |
| 46 | door, | |
| 47 | event_port, | |
| 48 | unknown, | |
| 49 | }; | |
| 50 | ||
| 51 | /// This is the default mode given to POSIX operating systems for creating | |
| 52 | /// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first, | |
| 53 | /// since most people would expect "-rw-r--r--", for example, when using | |
| 54 | /// the `touch` command, which would correspond to `0o644`. However, POSIX | |
| 55 | /// libc implementations use `0o666` inside `fopen` and then rely on the | |
| 56 | /// process-scoped "umask" setting to adjust this number for file creation. | |
| 57 | pub const default_mode = switch (builtin.os.tag) { | |
| 58 | .windows => 0, | |
| 59 | .wasi => 0, | |
| 60 | else => 0o666, | |
| 61 | }; | |
| 62 | ||
| 63 | pub const OpenError = error{ | |
| 64 | SharingViolation, | |
| 65 | PathAlreadyExists, | |
| 66 | FileNotFound, | |
| 67 | AccessDenied, | |
| 68 | PipeBusy, | |
| 69 | NameTooLong, | |
| 70 | /// On Windows, file paths must be valid Unicode. | |
| 71 | InvalidUtf8, | |
| 72 | /// On Windows, file paths cannot contain these characters: | |
| 73 | /// '/', '*', '?', '"', '<', '>', '|' | |
| 74 | BadPathName, | |
| 75 | Unexpected, | |
| 76 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 77 | NetworkNotFound, | |
| 78 | } || os.OpenError || os.FlockError; | |
| 79 | ||
| 80 | pub const OpenMode = enum { | |
| 81 | read_only, | |
| 82 | write_only, | |
| 83 | read_write, | |
| 84 | }; | |
| 85 | ||
| 86 | pub const Lock = enum { | |
| 87 | none, | |
| 88 | shared, | |
| 89 | exclusive, | |
| 90 | }; | |
| 91 | ||
| 92 | pub const OpenFlags = struct { | |
| 93 | mode: OpenMode = .read_only, | |
| 94 | ||
| 95 | /// Open the file with an advisory lock to coordinate with other processes | |
| 96 | /// accessing it at the same time. An exclusive lock will prevent other | |
| 97 | /// processes from acquiring a lock. A shared lock will prevent other | |
| 98 | /// processes from acquiring a exclusive lock, but does not prevent | |
| 99 | /// other process from getting their own shared locks. | |
| 100 | /// | |
| 101 | /// The lock is advisory, except on Linux in very specific circumstances[1]. | |
| 102 | /// This means that a process that does not respect the locking API can still get access | |
| 103 | /// to the file, despite the lock. | |
| 104 | /// | |
| 105 | /// On these operating systems, the lock is acquired atomically with | |
| 106 | /// opening the file: | |
| 107 | /// * Darwin | |
| 108 | /// * DragonFlyBSD | |
| 109 | /// * FreeBSD | |
| 110 | /// * Haiku | |
| 111 | /// * NetBSD | |
| 112 | /// * OpenBSD | |
| 113 | /// On these operating systems, the lock is acquired via a separate syscall | |
| 114 | /// after opening the file: | |
| 115 | /// * Linux | |
| 116 | /// * Windows | |
| 117 | /// | |
| 118 | /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt | |
| 119 | lock: Lock = .none, | |
| 120 | ||
| 121 | /// Sets whether or not to wait until the file is locked to return. If set to true, | |
| 122 | /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file | |
| 123 | /// is available to proceed. | |
| 124 | /// In async I/O mode, non-blocking at the OS level is | |
| 125 | /// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned, | |
| 126 | /// and `false` means `error.WouldBlock` is handled by the event loop. | |
| 127 | lock_nonblocking: bool = false, | |
| 128 | ||
| 129 | /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even | |
| 130 | /// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions | |
| 131 | /// related to opening the file, reading, writing, and locking. | |
| 132 | intended_io_mode: io.ModeOverride = io.default_mode, | |
| 133 | ||
| 134 | /// Set this to allow the opened file to automatically become the | |
| 135 | /// controlling TTY for the current process. | |
| 136 | allow_ctty: bool = false, | |
| 137 | ||
| 138 | pub fn isRead(self: OpenFlags) bool { | |
| 139 | return self.mode != .write_only; | |
| 140 | } | |
| 141 | ||
| 142 | pub fn isWrite(self: OpenFlags) bool { | |
| 143 | return self.mode != .read_only; | |
| 144 | } | |
| 145 | }; | |
| 146 | ||
| 147 | pub const CreateFlags = struct { | |
| 148 | /// Whether the file will be created with read access. | |
| 149 | read: bool = false, | |
| 150 | ||
| 151 | /// If the file already exists, and is a regular file, and the access | |
| 152 | /// mode allows writing, it will be truncated to length 0. | |
| 153 | truncate: bool = true, | |
| 154 | ||
| 155 | /// Ensures that this open call creates the file, otherwise causes | |
| 156 | /// `error.PathAlreadyExists` to be returned. | |
| 157 | exclusive: bool = false, | |
| 158 | ||
| 159 | /// Open the file with an advisory lock to coordinate with other processes | |
| 160 | /// accessing it at the same time. An exclusive lock will prevent other | |
| 161 | /// processes from acquiring a lock. A shared lock will prevent other | |
| 162 | /// processes from acquiring a exclusive lock, but does not prevent | |
| 163 | /// other process from getting their own shared locks. | |
| 164 | /// | |
| 165 | /// The lock is advisory, except on Linux in very specific circumstances[1]. | |
| 166 | /// This means that a process that does not respect the locking API can still get access | |
| 167 | /// to the file, despite the lock. | |
| 168 | /// | |
| 169 | /// On these operating systems, the lock is acquired atomically with | |
| 170 | /// opening the file: | |
| 171 | /// * Darwin | |
| 172 | /// * DragonFlyBSD | |
| 173 | /// * FreeBSD | |
| 174 | /// * Haiku | |
| 175 | /// * NetBSD | |
| 176 | /// * OpenBSD | |
| 177 | /// On these operating systems, the lock is acquired via a separate syscall | |
| 178 | /// after opening the file: | |
| 179 | /// * Linux | |
| 180 | /// * Windows | |
| 181 | /// | |
| 182 | /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt | |
| 183 | lock: Lock = .none, | |
| 184 | ||
| 185 | /// Sets whether or not to wait until the file is locked to return. If set to true, | |
| 186 | /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file | |
| 187 | /// is available to proceed. | |
| 188 | /// In async I/O mode, non-blocking at the OS level is | |
| 189 | /// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned, | |
| 190 | /// and `false` means `error.WouldBlock` is handled by the event loop. | |
| 191 | lock_nonblocking: bool = false, | |
| 192 | ||
| 193 | /// For POSIX systems this is the file system mode the file will | |
| 194 | /// be created with. On other systems this is always 0. | |
| 195 | mode: Mode = default_mode, | |
| 196 | ||
| 197 | /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even | |
| 198 | /// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions | |
| 199 | /// related to opening the file, reading, writing, and locking. | |
| 200 | intended_io_mode: io.ModeOverride = io.default_mode, | |
| 201 | }; | |
| 202 | ||
| 203 | /// Upon success, the stream is in an uninitialized state. To continue using it, | |
| 204 | /// you must use the open() function. | |
| 205 | pub fn close(self: File) void { | |
| 206 | if (is_windows) { | |
| 207 | windows.CloseHandle(self.handle); | |
| 208 | } else if (self.capable_io_mode != self.intended_io_mode) { | |
| 209 | std.event.Loop.instance.?.close(self.handle); | |
| 210 | } else { | |
| 211 | os.close(self.handle); | |
| 212 | } | |
| 213 | } | |
| 214 | ||
| 215 | pub const SyncError = os.SyncError; | |
| 216 | ||
| 217 | /// Blocks until all pending file contents and metadata modifications | |
| 218 | /// for the file have been synchronized with the underlying filesystem. | |
| 219 | /// | |
| 220 | /// Note that this does not ensure that metadata for the | |
| 221 | /// directory containing the file has also reached disk. | |
| 222 | pub fn sync(self: File) SyncError!void { | |
| 223 | return os.fsync(self.handle); | |
| 224 | } | |
| 225 | ||
| 226 | /// Test whether the file refers to a terminal. | |
| 227 | /// See also `supportsAnsiEscapeCodes`. | |
| 228 | pub fn isTty(self: File) bool { | |
| 229 | return os.isatty(self.handle); | |
| 230 | } | |
| 231 | ||
| 232 | /// Test whether ANSI escape codes will be treated as such. | |
| 233 | pub fn supportsAnsiEscapeCodes(self: File) bool { | |
| 234 | if (builtin.os.tag == .windows) { | |
| 235 | var console_mode: os.windows.DWORD = 0; | |
| 236 | if (os.windows.kernel32.GetConsoleMode(self.handle, &console_mode) != 0) { | |
| 237 | if (console_mode & os.windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true; | |
| 238 | } | |
| 239 | ||
| 240 | return os.isCygwinPty(self.handle); | |
| 241 | } | |
| 242 | if (builtin.os.tag == .wasi) { | |
| 243 | // WASI sanitizes stdout when fd is a tty so ANSI escape codes | |
| 244 | // will not be interpreted as actual cursor commands, and | |
| 245 | // stderr is always sanitized. | |
| 246 | return false; | |
| 247 | } | |
| 248 | if (self.isTty()) { | |
| 249 | if (self.handle == os.STDOUT_FILENO or self.handle == os.STDERR_FILENO) { | |
| 250 | if (os.getenvZ("TERM")) |term| { | |
| 251 | if (std.mem.eql(u8, term, "dumb")) | |
| 252 | return false; | |
| 253 | } | |
| 254 | } | |
| 255 | return true; | |
| 256 | } | |
| 257 | return false; | |
| 258 | } | |
| 259 | ||
| 260 | pub const SetEndPosError = os.TruncateError; | |
| 261 | ||
| 262 | /// Shrinks or expands the file. | |
| 263 | /// The file offset after this call is left unchanged. | |
| 264 | pub fn setEndPos(self: File, length: u64) SetEndPosError!void { | |
| 265 | try os.ftruncate(self.handle, length); | |
| 266 | } | |
| 267 | ||
| 268 | pub const SeekError = os.SeekError; | |
| 269 | ||
| 270 | /// Repositions read/write file offset relative to the current offset. | |
| 271 | /// TODO: integrate with async I/O | |
| 272 | pub fn seekBy(self: File, offset: i64) SeekError!void { | |
| 273 | return os.lseek_CUR(self.handle, offset); | |
| 274 | } | |
| 275 | ||
| 276 | /// Repositions read/write file offset relative to the end. | |
| 277 | /// TODO: integrate with async I/O | |
| 278 | pub fn seekFromEnd(self: File, offset: i64) SeekError!void { | |
| 279 | return os.lseek_END(self.handle, offset); | |
| 280 | } | |
| 281 | ||
| 282 | /// Repositions read/write file offset relative to the beginning. | |
| 283 | /// TODO: integrate with async I/O | |
| 284 | pub fn seekTo(self: File, offset: u64) SeekError!void { | |
| 285 | return os.lseek_SET(self.handle, offset); | |
| 286 | } | |
| 287 | ||
| 288 | pub const GetSeekPosError = os.SeekError || os.FStatError; | |
| 289 | ||
| 290 | /// TODO: integrate with async I/O | |
| 291 | pub fn getPos(self: File) GetSeekPosError!u64 { | |
| 292 | return os.lseek_CUR_get(self.handle); | |
| 293 | } | |
| 294 | ||
| 295 | /// TODO: integrate with async I/O | |
| 296 | pub fn getEndPos(self: File) GetSeekPosError!u64 { | |
| 297 | if (builtin.os.tag == .windows) { | |
| 298 | return windows.GetFileSizeEx(self.handle); | |
| 299 | } | |
| 300 | return (try self.stat()).size; | |
| 301 | } | |
| 302 | ||
| 303 | pub const ModeError = os.FStatError; | |
| 304 | ||
| 305 | /// TODO: integrate with async I/O | |
| 306 | pub fn mode(self: File) ModeError!Mode { | |
| 307 | if (builtin.os.tag == .windows) { | |
| 308 | return 0; | |
| 309 | } | |
| 310 | return (try self.stat()).mode; | |
| 311 | } | |
| 312 | ||
| 313 | pub const Stat = struct { | |
| 314 | /// A number that the system uses to point to the file metadata. This | |
| 315 | /// number is not guaranteed to be unique across time, as some file | |
| 316 | /// systems may reuse an inode after its file has been deleted. Some | |
| 317 | /// systems may change the inode of a file over time. | |
| 318 | /// | |
| 319 | /// On Linux, the inode is a structure that stores the metadata, and | |
| 320 | /// the inode _number_ is what you see here: the index number of the | |
| 321 | /// inode. | |
| 322 | /// | |
| 323 | /// The FileIndex on Windows is similar. It is a number for a file that | |
| 324 | /// is unique to each filesystem. | |
| 325 | inode: INode, | |
| 326 | size: u64, | |
| 327 | /// This is available on POSIX systems and is always 0 otherwise. | |
| 328 | mode: Mode, | |
| 329 | kind: Kind, | |
| 330 | ||
| 331 | /// Access time in nanoseconds, relative to UTC 1970-01-01. | |
| 332 | atime: i128, | |
| 333 | /// Last modification time in nanoseconds, relative to UTC 1970-01-01. | |
| 334 | mtime: i128, | |
| 335 | /// Creation time in nanoseconds, relative to UTC 1970-01-01. | |
| 336 | ctime: i128, | |
| 337 | ||
| 338 | pub fn fromSystem(st: os.system.Stat) Stat { | |
| 339 | const atime = st.atime(); | |
| 340 | const mtime = st.mtime(); | |
| 341 | const ctime = st.ctime(); | |
| 342 | const kind: Kind = if (builtin.os.tag == .wasi and !builtin.link_libc) switch (st.filetype) { | |
| 343 | .BLOCK_DEVICE => .block_device, | |
| 344 | .CHARACTER_DEVICE => .character_device, | |
| 345 | .DIRECTORY => .directory, | |
| 346 | .SYMBOLIC_LINK => .sym_link, | |
| 347 | .REGULAR_FILE => .file, | |
| 348 | .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, | |
| 349 | else => .unknown, | |
| 350 | } else blk: { | |
| 351 | const m = st.mode & os.S.IFMT; | |
| 352 | switch (m) { | |
| 353 | os.S.IFBLK => break :blk .block_device, | |
| 354 | os.S.IFCHR => break :blk .character_device, | |
| 355 | os.S.IFDIR => break :blk .directory, | |
| 356 | os.S.IFIFO => break :blk .named_pipe, | |
| 357 | os.S.IFLNK => break :blk .sym_link, | |
| 358 | os.S.IFREG => break :blk .file, | |
| 359 | os.S.IFSOCK => break :blk .unix_domain_socket, | |
| 360 | else => {}, | |
| 361 | } | |
| 362 | if (builtin.os.tag.isSolarish()) switch (m) { | |
| 363 | os.S.IFDOOR => break :blk .door, | |
| 364 | os.S.IFPORT => break :blk .event_port, | |
| 365 | else => {}, | |
| 366 | }; | |
| 367 | ||
| 368 | break :blk .unknown; | |
| 369 | }; | |
| 370 | ||
| 371 | return Stat{ | |
| 372 | .inode = st.ino, | |
| 373 | .size = @as(u64, @bitCast(st.size)), | |
| 374 | .mode = st.mode, | |
| 375 | .kind = kind, | |
| 376 | .atime = @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec, | |
| 377 | .mtime = @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec, | |
| 378 | .ctime = @as(i128, ctime.tv_sec) * std.time.ns_per_s + ctime.tv_nsec, | |
| 379 | }; | |
| 380 | } | |
| 381 | }; | |
| 382 | ||
| 383 | pub const StatError = os.FStatError; | |
| 384 | ||
| 385 | /// TODO: integrate with async I/O | |
| 386 | pub fn stat(self: File) StatError!Stat { | |
| 387 | if (builtin.os.tag == .windows) { | |
| 388 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 389 | var info: windows.FILE_ALL_INFORMATION = undefined; | |
| 390 | const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); | |
| 391 | switch (rc) { | |
| 392 | .SUCCESS => {}, | |
| 393 | // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer | |
| 394 | // size provided. This is treated as success because the type of variable-length information that this would be relevant for | |
| 395 | // (name, volume name, etc) we don't care about. | |
| 396 | .BUFFER_OVERFLOW => {}, | |
| 397 | .INVALID_PARAMETER => unreachable, | |
| 398 | .ACCESS_DENIED => return error.AccessDenied, | |
| 399 | else => return windows.unexpectedStatus(rc), | |
| 400 | } | |
| 401 | return Stat{ | |
| 402 | .inode = info.InternalInformation.IndexNumber, | |
| 403 | .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), | |
| 404 | .mode = 0, | |
| 405 | .kind = if (info.StandardInformation.Directory == 0) .file else .directory, | |
| 406 | .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), | |
| 407 | .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), | |
| 408 | .ctime = windows.fromSysTime(info.BasicInformation.CreationTime), | |
| 409 | }; | |
| 410 | } | |
| 411 | ||
| 412 | const st = try os.fstat(self.handle); | |
| 413 | return Stat.fromSystem(st); | |
| 414 | } | |
| 415 | ||
| 416 | pub const ChmodError = std.os.FChmodError; | |
| 417 | ||
| 418 | /// Changes the mode of the file. | |
| 419 | /// The process must have the correct privileges in order to do this | |
| 420 | /// successfully, or must have the effective user ID matching the owner | |
| 421 | /// of the file. | |
| 422 | pub fn chmod(self: File, new_mode: Mode) ChmodError!void { | |
| 423 | try os.fchmod(self.handle, new_mode); | |
| 424 | } | |
| 425 | ||
| 426 | pub const ChownError = std.os.FChownError; | |
| 427 | ||
| 428 | /// Changes the owner and group of the file. | |
| 429 | /// The process must have the correct privileges in order to do this | |
| 430 | /// successfully. The group may be changed by the owner of the file to | |
| 431 | /// any group of which the owner is a member. If the owner or group is | |
| 432 | /// specified as `null`, the ID is not changed. | |
| 433 | pub fn chown(self: File, owner: ?Uid, group: ?Gid) ChownError!void { | |
| 434 | try os.fchown(self.handle, owner, group); | |
| 435 | } | |
| 436 | ||
| 437 | /// Cross-platform representation of permissions on a file. | |
| 438 | /// The `readonly` and `setReadonly` are the only methods available across all platforms. | |
| 439 | /// Platform-specific functionality is available through the `inner` field. | |
| 440 | pub const Permissions = struct { | |
| 441 | /// You may use the `inner` field to use platform-specific functionality | |
| 442 | inner: switch (builtin.os.tag) { | |
| 443 | .windows => PermissionsWindows, | |
| 444 | else => PermissionsUnix, | |
| 445 | }, | |
| 446 | ||
| 447 | const Self = @This(); | |
| 448 | ||
| 449 | /// Returns `true` if permissions represent an unwritable file. | |
| 450 | /// On Unix, `true` is returned only if no class has write permissions. | |
| 451 | pub fn readOnly(self: Self) bool { | |
| 452 | return self.inner.readOnly(); | |
| 453 | } | |
| 454 | ||
| 455 | /// Sets whether write permissions are provided. | |
| 456 | /// On Unix, this affects *all* classes. If this is undesired, use `unixSet`. | |
| 457 | /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` | |
| 458 | pub fn setReadOnly(self: *Self, read_only: bool) void { | |
| 459 | self.inner.setReadOnly(read_only); | |
| 460 | } | |
| 461 | }; | |
| 462 | ||
| 463 | pub const PermissionsWindows = struct { | |
| 464 | attributes: os.windows.DWORD, | |
| 465 | ||
| 466 | const Self = @This(); | |
| 467 | ||
| 468 | /// Returns `true` if permissions represent an unwritable file. | |
| 469 | pub fn readOnly(self: Self) bool { | |
| 470 | return self.attributes & os.windows.FILE_ATTRIBUTE_READONLY != 0; | |
| 471 | } | |
| 472 | ||
| 473 | /// Sets whether write permissions are provided. | |
| 474 | /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` | |
| 475 | pub fn setReadOnly(self: *Self, read_only: bool) void { | |
| 476 | if (read_only) { | |
| 477 | self.attributes |= os.windows.FILE_ATTRIBUTE_READONLY; | |
| 478 | } else { | |
| 479 | self.attributes &= ~@as(os.windows.DWORD, os.windows.FILE_ATTRIBUTE_READONLY); | |
| 480 | } | |
| 481 | } | |
| 482 | }; | |
| 483 | ||
| 484 | pub const PermissionsUnix = struct { | |
| 485 | mode: Mode, | |
| 486 | ||
| 487 | const Self = @This(); | |
| 488 | ||
| 489 | /// Returns `true` if permissions represent an unwritable file. | |
| 490 | /// `true` is returned only if no class has write permissions. | |
| 491 | pub fn readOnly(self: Self) bool { | |
| 492 | return self.mode & 0o222 == 0; | |
| 493 | } | |
| 494 | ||
| 495 | /// Sets whether write permissions are provided. | |
| 496 | /// This affects *all* classes. If this is undesired, use `unixSet`. | |
| 497 | /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` | |
| 498 | pub fn setReadOnly(self: *Self, read_only: bool) void { | |
| 499 | if (read_only) { | |
| 500 | self.mode &= ~@as(Mode, 0o222); | |
| 501 | } else { | |
| 502 | self.mode |= @as(Mode, 0o222); | |
| 503 | } | |
| 504 | } | |
| 505 | ||
| 506 | pub const Class = enum(u2) { | |
| 507 | user = 2, | |
| 508 | group = 1, | |
| 509 | other = 0, | |
| 510 | }; | |
| 511 | ||
| 512 | pub const Permission = enum(u3) { | |
| 513 | read = 0o4, | |
| 514 | write = 0o2, | |
| 515 | execute = 0o1, | |
| 516 | }; | |
| 517 | ||
| 518 | /// Returns `true` if the chosen class has the selected permission. | |
| 519 | /// This method is only available on Unix platforms. | |
| 520 | pub fn unixHas(self: Self, class: Class, permission: Permission) bool { | |
| 521 | const mask = @as(Mode, @intFromEnum(permission)) << @as(u3, @intFromEnum(class)) * 3; | |
| 522 | return self.mode & mask != 0; | |
| 523 | } | |
| 524 | ||
| 525 | /// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged. | |
| 526 | /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` | |
| 527 | pub fn unixSet(self: *Self, class: Class, permissions: struct { | |
| 528 | read: ?bool = null, | |
| 529 | write: ?bool = null, | |
| 530 | execute: ?bool = null, | |
| 531 | }) void { | |
| 532 | const shift = @as(u3, @intFromEnum(class)) * 3; | |
| 533 | if (permissions.read) |r| { | |
| 534 | if (r) { | |
| 535 | self.mode |= @as(Mode, 0o4) << shift; | |
| 536 | } else { | |
| 537 | self.mode &= ~(@as(Mode, 0o4) << shift); | |
| 538 | } | |
| 539 | } | |
| 540 | if (permissions.write) |w| { | |
| 541 | if (w) { | |
| 542 | self.mode |= @as(Mode, 0o2) << shift; | |
| 543 | } else { | |
| 544 | self.mode &= ~(@as(Mode, 0o2) << shift); | |
| 545 | } | |
| 546 | } | |
| 547 | if (permissions.execute) |x| { | |
| 548 | if (x) { | |
| 549 | self.mode |= @as(Mode, 0o1) << shift; | |
| 550 | } else { | |
| 551 | self.mode &= ~(@as(Mode, 0o1) << shift); | |
| 552 | } | |
| 553 | } | |
| 554 | } | |
| 555 | ||
| 556 | /// Returns a `Permissions` struct representing the permissions from the passed mode. | |
| 557 | pub fn unixNew(new_mode: Mode) Self { | |
| 558 | return Self{ | |
| 559 | .mode = new_mode, | |
| 560 | }; | |
| 561 | } | |
| 562 | }; | |
| 563 | ||
| 564 | pub const SetPermissionsError = ChmodError; | |
| 565 | ||
| 566 | /// Sets permissions according to the provided `Permissions` struct. | |
| 567 | /// This method is *NOT* available on WASI | |
| 568 | pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void { | |
| 569 | switch (builtin.os.tag) { | |
| 570 | .windows => { | |
| 571 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 572 | var info = windows.FILE_BASIC_INFORMATION{ | |
| 573 | .CreationTime = 0, | |
| 574 | .LastAccessTime = 0, | |
| 575 | .LastWriteTime = 0, | |
| 576 | .ChangeTime = 0, | |
| 577 | .FileAttributes = permissions.inner.attributes, | |
| 578 | }; | |
| 579 | const rc = windows.ntdll.NtSetInformationFile( | |
| 580 | self.handle, | |
| 581 | &io_status_block, | |
| 582 | &info, | |
| 583 | @sizeOf(windows.FILE_BASIC_INFORMATION), | |
| 584 | .FileBasicInformation, | |
| 585 | ); | |
| 586 | switch (rc) { | |
| 587 | .SUCCESS => return, | |
| 588 | .INVALID_HANDLE => unreachable, | |
| 589 | .ACCESS_DENIED => return error.AccessDenied, | |
| 590 | else => return windows.unexpectedStatus(rc), | |
| 591 | } | |
| 592 | }, | |
| 593 | .wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod | |
| 594 | else => { | |
| 595 | try self.chmod(permissions.inner.mode); | |
| 596 | }, | |
| 597 | } | |
| 598 | } | |
| 599 | ||
| 600 | /// Cross-platform representation of file metadata. | |
| 601 | /// Platform-specific functionality is available through the `inner` field. | |
| 602 | pub const Metadata = struct { | |
| 603 | /// You may use the `inner` field to use platform-specific functionality | |
| 604 | inner: switch (builtin.os.tag) { | |
| 605 | .windows => MetadataWindows, | |
| 606 | .linux => MetadataLinux, | |
| 607 | else => MetadataUnix, | |
| 608 | }, | |
| 609 | ||
| 610 | const Self = @This(); | |
| 611 | ||
| 612 | /// Returns the size of the file | |
| 613 | pub fn size(self: Self) u64 { | |
| 614 | return self.inner.size(); | |
| 615 | } | |
| 616 | ||
| 617 | /// Returns a `Permissions` struct, representing the permissions on the file | |
| 618 | pub fn permissions(self: Self) Permissions { | |
| 619 | return self.inner.permissions(); | |
| 620 | } | |
| 621 | ||
| 622 | /// Returns the `Kind` of file. | |
| 623 | /// On Windows, can only return: `.file`, `.directory`, `.sym_link` or `.unknown` | |
| 624 | pub fn kind(self: Self) Kind { | |
| 625 | return self.inner.kind(); | |
| 626 | } | |
| 627 | ||
| 628 | /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 | |
| 629 | pub fn accessed(self: Self) i128 { | |
| 630 | return self.inner.accessed(); | |
| 631 | } | |
| 632 | ||
| 633 | /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01 | |
| 634 | pub fn modified(self: Self) i128 { | |
| 635 | return self.inner.modified(); | |
| 636 | } | |
| 637 | ||
| 638 | /// Returns the time the file was created in nanoseconds since UTC 1970-01-01 | |
| 639 | /// On Windows, this cannot return null | |
| 640 | /// On Linux, this returns null if the filesystem does not support creation times, or if the kernel is older than 4.11 | |
| 641 | /// On Unices, this returns null if the filesystem or OS does not support creation times | |
| 642 | /// On MacOS, this returns the ctime if the filesystem does not support creation times; this is insanity, and yet another reason to hate on Apple | |
| 643 | pub fn created(self: Self) ?i128 { | |
| 644 | return self.inner.created(); | |
| 645 | } | |
| 646 | }; | |
| 647 | ||
| 648 | pub const MetadataUnix = struct { | |
| 649 | stat: os.Stat, | |
| 650 | ||
| 651 | const Self = @This(); | |
| 652 | ||
| 653 | /// Returns the size of the file | |
| 654 | pub fn size(self: Self) u64 { | |
| 655 | return @as(u64, @intCast(self.stat.size)); | |
| 656 | } | |
| 657 | ||
| 658 | /// Returns a `Permissions` struct, representing the permissions on the file | |
| 659 | pub fn permissions(self: Self) Permissions { | |
| 660 | return Permissions{ .inner = PermissionsUnix{ .mode = self.stat.mode } }; | |
| 661 | } | |
| 662 | ||
| 663 | /// Returns the `Kind` of the file | |
| 664 | pub fn kind(self: Self) Kind { | |
| 665 | if (builtin.os.tag == .wasi and !builtin.link_libc) return switch (self.stat.filetype) { | |
| 666 | .BLOCK_DEVICE => .block_device, | |
| 667 | .CHARACTER_DEVICE => .character_device, | |
| 668 | .DIRECTORY => .directory, | |
| 669 | .SYMBOLIC_LINK => .sym_link, | |
| 670 | .REGULAR_FILE => .file, | |
| 671 | .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, | |
| 672 | else => .unknown, | |
| 673 | }; | |
| 674 | ||
| 675 | const m = self.stat.mode & os.S.IFMT; | |
| 676 | ||
| 677 | switch (m) { | |
| 678 | os.S.IFBLK => return .block_device, | |
| 679 | os.S.IFCHR => return .character_device, | |
| 680 | os.S.IFDIR => return .directory, | |
| 681 | os.S.IFIFO => return .named_pipe, | |
| 682 | os.S.IFLNK => return .sym_link, | |
| 683 | os.S.IFREG => return .file, | |
| 684 | os.S.IFSOCK => return .unix_domain_socket, | |
| 685 | else => {}, | |
| 686 | } | |
| 687 | ||
| 688 | if (builtin.os.tag.isSolarish()) switch (m) { | |
| 689 | os.S.IFDOOR => return .door, | |
| 690 | os.S.IFPORT => return .event_port, | |
| 691 | else => {}, | |
| 692 | }; | |
| 693 | ||
| 694 | return .unknown; | |
| 695 | } | |
| 696 | ||
| 697 | /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 | |
| 698 | pub fn accessed(self: Self) i128 { | |
| 699 | const atime = self.stat.atime(); | |
| 700 | return @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec; | |
| 701 | } | |
| 702 | ||
| 703 | /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01 | |
| 704 | pub fn modified(self: Self) i128 { | |
| 705 | const mtime = self.stat.mtime(); | |
| 706 | return @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec; | |
| 707 | } | |
| 708 | ||
| 709 | /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. | |
| 710 | /// Returns null if this is not supported by the OS or filesystem | |
| 711 | pub fn created(self: Self) ?i128 { | |
| 712 | if (!@hasDecl(@TypeOf(self.stat), "birthtime")) return null; | |
| 713 | const birthtime = self.stat.birthtime(); | |
| 714 | ||
| 715 | // If the filesystem doesn't support this the value *should* be: | |
| 716 | // On FreeBSD: tv_nsec = 0, tv_sec = -1 | |
| 717 | // On NetBSD and OpenBSD: tv_nsec = 0, tv_sec = 0 | |
| 718 | // On MacOS, it is set to ctime -- we cannot detect this!! | |
| 719 | switch (builtin.os.tag) { | |
| 720 | .freebsd => if (birthtime.tv_sec == -1 and birthtime.tv_nsec == 0) return null, | |
| 721 | .netbsd, .openbsd => if (birthtime.tv_sec == 0 and birthtime.tv_nsec == 0) return null, | |
| 722 | .macos => {}, | |
| 723 | else => @compileError("Creation time detection not implemented for OS"), | |
| 724 | } | |
| 725 | ||
| 726 | return @as(i128, birthtime.tv_sec) * std.time.ns_per_s + birthtime.tv_nsec; | |
| 727 | } | |
| 728 | }; | |
| 729 | ||
| 730 | /// `MetadataUnix`, but using Linux's `statx` syscall. | |
| 731 | /// On Linux versions below 4.11, `statx` will be filled with data from stat. | |
| 732 | pub const MetadataLinux = struct { | |
| 733 | statx: os.linux.Statx, | |
| 734 | ||
| 735 | const Self = @This(); | |
| 736 | ||
| 737 | /// Returns the size of the file | |
| 738 | pub fn size(self: Self) u64 { | |
| 739 | return self.statx.size; | |
| 740 | } | |
| 741 | ||
| 742 | /// Returns a `Permissions` struct, representing the permissions on the file | |
| 743 | pub fn permissions(self: Self) Permissions { | |
| 744 | return Permissions{ .inner = PermissionsUnix{ .mode = self.statx.mode } }; | |
| 745 | } | |
| 746 | ||
| 747 | /// Returns the `Kind` of the file | |
| 748 | pub fn kind(self: Self) Kind { | |
| 749 | const m = self.statx.mode & os.S.IFMT; | |
| 750 | ||
| 751 | switch (m) { | |
| 752 | os.S.IFBLK => return .block_device, | |
| 753 | os.S.IFCHR => return .character_device, | |
| 754 | os.S.IFDIR => return .directory, | |
| 755 | os.S.IFIFO => return .named_pipe, | |
| 756 | os.S.IFLNK => return .sym_link, | |
| 757 | os.S.IFREG => return .file, | |
| 758 | os.S.IFSOCK => return .unix_domain_socket, | |
| 759 | else => {}, | |
| 760 | } | |
| 761 | ||
| 762 | return .unknown; | |
| 763 | } | |
| 764 | ||
| 765 | /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 | |
| 766 | pub fn accessed(self: Self) i128 { | |
| 767 | return @as(i128, self.statx.atime.tv_sec) * std.time.ns_per_s + self.statx.atime.tv_nsec; | |
| 768 | } | |
| 769 | ||
| 770 | /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01 | |
| 771 | pub fn modified(self: Self) i128 { | |
| 772 | return @as(i128, self.statx.mtime.tv_sec) * std.time.ns_per_s + self.statx.mtime.tv_nsec; | |
| 773 | } | |
| 774 | ||
| 775 | /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. | |
| 776 | /// Returns null if this is not supported by the filesystem, or on kernels before than version 4.11 | |
| 777 | pub fn created(self: Self) ?i128 { | |
| 778 | if (self.statx.mask & os.linux.STATX_BTIME == 0) return null; | |
| 779 | return @as(i128, self.statx.btime.tv_sec) * std.time.ns_per_s + self.statx.btime.tv_nsec; | |
| 780 | } | |
| 781 | }; | |
| 782 | ||
| 783 | pub const MetadataWindows = struct { | |
| 784 | attributes: windows.DWORD, | |
| 785 | reparse_tag: windows.DWORD, | |
| 786 | _size: u64, | |
| 787 | access_time: i128, | |
| 788 | modified_time: i128, | |
| 789 | creation_time: i128, | |
| 790 | ||
| 791 | const Self = @This(); | |
| 792 | ||
| 793 | /// Returns the size of the file | |
| 794 | pub fn size(self: Self) u64 { | |
| 795 | return self._size; | |
| 796 | } | |
| 797 | ||
| 798 | /// Returns a `Permissions` struct, representing the permissions on the file | |
| 799 | pub fn permissions(self: Self) Permissions { | |
| 800 | return Permissions{ .inner = PermissionsWindows{ .attributes = self.attributes } }; | |
| 801 | } | |
| 802 | ||
| 803 | /// Returns the `Kind` of the file. | |
| 804 | /// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown` | |
| 805 | pub fn kind(self: Self) Kind { | |
| 806 | if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { | |
| 807 | if (self.reparse_tag & 0x20000000 != 0) { | |
| 808 | return .sym_link; | |
| 809 | } | |
| 810 | } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) { | |
| 811 | return .directory; | |
| 812 | } else { | |
| 813 | return .file; | |
| 814 | } | |
| 815 | return .unknown; | |
| 816 | } | |
| 817 | ||
| 818 | /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 | |
| 819 | pub fn accessed(self: Self) i128 { | |
| 820 | return self.access_time; | |
| 821 | } | |
| 822 | ||
| 823 | /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01 | |
| 824 | pub fn modified(self: Self) i128 { | |
| 825 | return self.modified_time; | |
| 826 | } | |
| 827 | ||
| 828 | /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. | |
| 829 | /// This never returns null, only returning an optional for compatibility with other OSes | |
| 830 | pub fn created(self: Self) ?i128 { | |
| 831 | return self.creation_time; | |
| 832 | } | |
| 833 | }; | |
| 834 | ||
| 835 | pub const MetadataError = os.FStatError; | |
| 836 | ||
| 837 | pub fn metadata(self: File) MetadataError!Metadata { | |
| 838 | return Metadata{ | |
| 839 | .inner = switch (builtin.os.tag) { | |
| 840 | .windows => blk: { | |
| 841 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 842 | var info: windows.FILE_ALL_INFORMATION = undefined; | |
| 843 | ||
| 844 | const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); | |
| 845 | switch (rc) { | |
| 846 | .SUCCESS => {}, | |
| 847 | // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer | |
| 848 | // size provided. This is treated as success because the type of variable-length information that this would be relevant for | |
| 849 | // (name, volume name, etc) we don't care about. | |
| 850 | .BUFFER_OVERFLOW => {}, | |
| 851 | .INVALID_PARAMETER => unreachable, | |
| 852 | .ACCESS_DENIED => return error.AccessDenied, | |
| 853 | else => return windows.unexpectedStatus(rc), | |
| 854 | } | |
| 855 | ||
| 856 | const reparse_tag: windows.DWORD = reparse_blk: { | |
| 857 | if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { | |
| 858 | var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; | |
| 859 | try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]); | |
| 860 | const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0])); | |
| 861 | break :reparse_blk reparse_struct.ReparseTag; | |
| 862 | } | |
| 863 | break :reparse_blk 0; | |
| 864 | }; | |
| 865 | ||
| 866 | break :blk MetadataWindows{ | |
| 867 | .attributes = info.BasicInformation.FileAttributes, | |
| 868 | .reparse_tag = reparse_tag, | |
| 869 | ._size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), | |
| 870 | .access_time = windows.fromSysTime(info.BasicInformation.LastAccessTime), | |
| 871 | .modified_time = windows.fromSysTime(info.BasicInformation.LastWriteTime), | |
| 872 | .creation_time = windows.fromSysTime(info.BasicInformation.CreationTime), | |
| 873 | }; | |
| 874 | }, | |
| 875 | .linux => blk: { | |
| 876 | var stx = mem.zeroes(os.linux.Statx); | |
| 877 | const rcx = os.linux.statx(self.handle, "\x00", os.linux.AT.EMPTY_PATH, os.linux.STATX_TYPE | os.linux.STATX_MODE | os.linux.STATX_ATIME | os.linux.STATX_MTIME | os.linux.STATX_BTIME, &stx); | |
| 878 | ||
| 879 | switch (os.errno(rcx)) { | |
| 880 | .SUCCESS => {}, | |
| 881 | // NOSYS happens when `statx` is unsupported, which is the case on kernel versions before 4.11 | |
| 882 | // Here, we call `fstat` and fill `stx` with the data we need | |
| 883 | .NOSYS => { | |
| 884 | const st = try os.fstat(self.handle); | |
| 885 | ||
| 886 | stx.mode = @as(u16, @intCast(st.mode)); | |
| 887 | ||
| 888 | // Hacky conversion from timespec to statx_timestamp | |
| 889 | stx.atime = std.mem.zeroes(os.linux.statx_timestamp); | |
| 890 | stx.atime.tv_sec = st.atim.tv_sec; | |
| 891 | stx.atime.tv_nsec = @as(u32, @intCast(st.atim.tv_nsec)); // Guaranteed to succeed (tv_nsec is always below 10^9) | |
| 892 | ||
| 893 | stx.mtime = std.mem.zeroes(os.linux.statx_timestamp); | |
| 894 | stx.mtime.tv_sec = st.mtim.tv_sec; | |
| 895 | stx.mtime.tv_nsec = @as(u32, @intCast(st.mtim.tv_nsec)); | |
| 896 | ||
| 897 | stx.mask = os.linux.STATX_BASIC_STATS | os.linux.STATX_MTIME; | |
| 898 | }, | |
| 899 | .BADF => unreachable, | |
| 900 | .FAULT => unreachable, | |
| 901 | .NOMEM => return error.SystemResources, | |
| 902 | else => |err| return os.unexpectedErrno(err), | |
| 903 | } | |
| 904 | ||
| 905 | break :blk MetadataLinux{ | |
| 906 | .statx = stx, | |
| 907 | }; | |
| 908 | }, | |
| 909 | else => blk: { | |
| 910 | const st = try os.fstat(self.handle); | |
| 911 | break :blk MetadataUnix{ | |
| 912 | .stat = st, | |
| 913 | }; | |
| 914 | }, | |
| 915 | }, | |
| 916 | }; | |
| 917 | } | |
| 918 | ||
| 919 | pub const UpdateTimesError = os.FutimensError || windows.SetFileTimeError; | |
| 920 | ||
| 921 | /// The underlying file system may have a different granularity than nanoseconds, | |
| 922 | /// and therefore this function cannot guarantee any precision will be stored. | |
| 923 | /// Further, the maximum value is limited by the system ABI. When a value is provided | |
| 924 | /// that exceeds this range, the value is clamped to the maximum. | |
| 925 | /// TODO: integrate with async I/O | |
| 926 | pub fn updateTimes( | |
| 927 | self: File, | |
| 928 | /// access timestamp in nanoseconds | |
| 929 | atime: i128, | |
| 930 | /// last modification timestamp in nanoseconds | |
| 931 | mtime: i128, | |
| 932 | ) UpdateTimesError!void { | |
| 933 | if (builtin.os.tag == .windows) { | |
| 934 | const atime_ft = windows.nanoSecondsToFileTime(atime); | |
| 935 | const mtime_ft = windows.nanoSecondsToFileTime(mtime); | |
| 936 | return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft); | |
| 937 | } | |
| 938 | const times = [2]os.timespec{ | |
| 939 | os.timespec{ | |
| 940 | .tv_sec = math.cast(isize, @divFloor(atime, std.time.ns_per_s)) orelse maxInt(isize), | |
| 941 | .tv_nsec = math.cast(isize, @mod(atime, std.time.ns_per_s)) orelse maxInt(isize), | |
| 942 | }, | |
| 943 | os.timespec{ | |
| 944 | .tv_sec = math.cast(isize, @divFloor(mtime, std.time.ns_per_s)) orelse maxInt(isize), | |
| 945 | .tv_nsec = math.cast(isize, @mod(mtime, std.time.ns_per_s)) orelse maxInt(isize), | |
| 946 | }, | |
| 947 | }; | |
| 948 | try os.futimens(self.handle, &times); | |
| 949 | } | |
| 950 | ||
| 951 | /// Reads all the bytes from the current position to the end of the file. | |
| 952 | /// On success, caller owns returned buffer. | |
| 953 | /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. | |
| 954 | pub fn readToEndAlloc(self: File, allocator: mem.Allocator, max_bytes: usize) ![]u8 { | |
| 955 | return self.readToEndAllocOptions(allocator, max_bytes, null, @alignOf(u8), null); | |
| 956 | } | |
| 957 | ||
| 958 | /// Reads all the bytes from the current position to the end of the file. | |
| 959 | /// On success, caller owns returned buffer. | |
| 960 | /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. | |
| 961 | /// If `size_hint` is specified the initial buffer size is calculated using | |
| 962 | /// that value, otherwise an arbitrary value is used instead. | |
| 963 | /// Allows specifying alignment and a sentinel value. | |
| 964 | pub fn readToEndAllocOptions( | |
| 965 | self: File, | |
| 966 | allocator: mem.Allocator, | |
| 967 | max_bytes: usize, | |
| 968 | size_hint: ?usize, | |
| 969 | comptime alignment: u29, | |
| 970 | comptime optional_sentinel: ?u8, | |
| 971 | ) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) { | |
| 972 | // If no size hint is provided fall back to the size=0 code path | |
| 973 | const size = size_hint orelse 0; | |
| 974 | ||
| 975 | // The file size returned by stat is used as hint to set the buffer | |
| 976 | // size. If the reported size is zero, as it happens on Linux for files | |
| 977 | // in /proc, a small buffer is allocated instead. | |
| 978 | const initial_cap = (if (size > 0) size else 1024) + @intFromBool(optional_sentinel != null); | |
| 979 | var array_list = try std.ArrayListAligned(u8, alignment).initCapacity(allocator, initial_cap); | |
| 980 | defer array_list.deinit(); | |
| 981 | ||
| 982 | self.reader().readAllArrayListAligned(alignment, &array_list, max_bytes) catch |err| switch (err) { | |
| 983 | error.StreamTooLong => return error.FileTooBig, | |
| 984 | else => |e| return e, | |
| 985 | }; | |
| 986 | ||
| 987 | if (optional_sentinel) |sentinel| { | |
| 988 | return try array_list.toOwnedSliceSentinel(sentinel); | |
| 989 | } else { | |
| 990 | return try array_list.toOwnedSlice(); | |
| 991 | } | |
| 992 | } | |
| 993 | ||
| 994 | pub const ReadError = os.ReadError; | |
| 995 | pub const PReadError = os.PReadError; | |
| 996 | ||
| 997 | pub fn read(self: File, buffer: []u8) ReadError!usize { | |
| 998 | if (is_windows) { | |
| 999 | return windows.ReadFile(self.handle, buffer, null, self.intended_io_mode); | |
| 1000 | } | |
| 1001 | ||
| 1002 | if (self.intended_io_mode == .blocking) { | |
| 1003 | return os.read(self.handle, buffer); | |
| 1004 | } else { | |
| 1005 | return std.event.Loop.instance.?.read(self.handle, buffer, self.capable_io_mode != self.intended_io_mode); | |
| 1006 | } | |
| 1007 | } | |
| 1008 | ||
| 1009 | /// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it | |
| 1010 | /// means the file reached the end. Reaching the end of a file is not an error condition. | |
| 1011 | pub fn readAll(self: File, buffer: []u8) ReadError!usize { | |
| 1012 | var index: usize = 0; | |
| 1013 | while (index != buffer.len) { | |
| 1014 | const amt = try self.read(buffer[index..]); | |
| 1015 | if (amt == 0) break; | |
| 1016 | index += amt; | |
| 1017 | } | |
| 1018 | return index; | |
| 1019 | } | |
| 1020 | ||
| 1021 | /// On Windows, this function currently does alter the file pointer. | |
| 1022 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1023 | pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize { | |
| 1024 | if (is_windows) { | |
| 1025 | return windows.ReadFile(self.handle, buffer, offset, self.intended_io_mode); | |
| 1026 | } | |
| 1027 | ||
| 1028 | if (self.intended_io_mode == .blocking) { | |
| 1029 | return os.pread(self.handle, buffer, offset); | |
| 1030 | } else { | |
| 1031 | return std.event.Loop.instance.?.pread(self.handle, buffer, offset, self.capable_io_mode != self.intended_io_mode); | |
| 1032 | } | |
| 1033 | } | |
| 1034 | ||
| 1035 | /// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it | |
| 1036 | /// means the file reached the end. Reaching the end of a file is not an error condition. | |
| 1037 | /// On Windows, this function currently does alter the file pointer. | |
| 1038 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1039 | pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize { | |
| 1040 | var index: usize = 0; | |
| 1041 | while (index != buffer.len) { | |
| 1042 | const amt = try self.pread(buffer[index..], offset + index); | |
| 1043 | if (amt == 0) break; | |
| 1044 | index += amt; | |
| 1045 | } | |
| 1046 | return index; | |
| 1047 | } | |
| 1048 | ||
| 1049 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1050 | pub fn readv(self: File, iovecs: []const os.iovec) ReadError!usize { | |
| 1051 | if (is_windows) { | |
| 1052 | // TODO improve this to use ReadFileScatter | |
| 1053 | if (iovecs.len == 0) return @as(usize, 0); | |
| 1054 | const first = iovecs[0]; | |
| 1055 | return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode); | |
| 1056 | } | |
| 1057 | ||
| 1058 | if (self.intended_io_mode == .blocking) { | |
| 1059 | return os.readv(self.handle, iovecs); | |
| 1060 | } else { | |
| 1061 | return std.event.Loop.instance.?.readv(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode); | |
| 1062 | } | |
| 1063 | } | |
| 1064 | ||
| 1065 | /// Returns the number of bytes read. If the number read is smaller than the total bytes | |
| 1066 | /// from all the buffers, it means the file reached the end. Reaching the end of a file | |
| 1067 | /// is not an error condition. | |
| 1068 | /// | |
| 1069 | /// The `iovecs` parameter is mutable because: | |
| 1070 | /// * This function needs to mutate the fields in order to handle partial | |
| 1071 | /// reads from the underlying OS layer. | |
| 1072 | /// * The OS layer expects pointer addresses to be inside the application's address space | |
| 1073 | /// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer | |
| 1074 | /// addresses when the length is zero. So this function modifies the iov_base fields | |
| 1075 | /// when the length is zero. | |
| 1076 | /// | |
| 1077 | /// Related open issue: https://github.com/ziglang/zig/issues/7699 | |
| 1078 | pub fn readvAll(self: File, iovecs: []os.iovec) ReadError!usize { | |
| 1079 | if (iovecs.len == 0) return 0; | |
| 1080 | ||
| 1081 | // We use the address of this local variable for all zero-length | |
| 1082 | // vectors so that the OS does not complain that we are giving it | |
| 1083 | // addresses outside the application's address space. | |
| 1084 | var garbage: [1]u8 = undefined; | |
| 1085 | for (iovecs) |*v| { | |
| 1086 | if (v.iov_len == 0) v.iov_base = &garbage; | |
| 1087 | } | |
| 1088 | ||
| 1089 | var i: usize = 0; | |
| 1090 | var off: usize = 0; | |
| 1091 | while (true) { | |
| 1092 | var amt = try self.readv(iovecs[i..]); | |
| 1093 | var eof = amt == 0; | |
| 1094 | off += amt; | |
| 1095 | while (amt >= iovecs[i].iov_len) { | |
| 1096 | amt -= iovecs[i].iov_len; | |
| 1097 | i += 1; | |
| 1098 | if (i >= iovecs.len) return off; | |
| 1099 | eof = false; | |
| 1100 | } | |
| 1101 | if (eof) return off; | |
| 1102 | iovecs[i].iov_base += amt; | |
| 1103 | iovecs[i].iov_len -= amt; | |
| 1104 | } | |
| 1105 | } | |
| 1106 | ||
| 1107 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1108 | /// On Windows, this function currently does alter the file pointer. | |
| 1109 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1110 | pub fn preadv(self: File, iovecs: []const os.iovec, offset: u64) PReadError!usize { | |
| 1111 | if (is_windows) { | |
| 1112 | // TODO improve this to use ReadFileScatter | |
| 1113 | if (iovecs.len == 0) return @as(usize, 0); | |
| 1114 | const first = iovecs[0]; | |
| 1115 | return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode); | |
| 1116 | } | |
| 1117 | ||
| 1118 | if (self.intended_io_mode == .blocking) { | |
| 1119 | return os.preadv(self.handle, iovecs, offset); | |
| 1120 | } else { | |
| 1121 | return std.event.Loop.instance.?.preadv(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode); | |
| 1122 | } | |
| 1123 | } | |
| 1124 | ||
| 1125 | /// Returns the number of bytes read. If the number read is smaller than the total bytes | |
| 1126 | /// from all the buffers, it means the file reached the end. Reaching the end of a file | |
| 1127 | /// is not an error condition. | |
| 1128 | /// The `iovecs` parameter is mutable because this function needs to mutate the fields in | |
| 1129 | /// order to handle partial reads from the underlying OS layer. | |
| 1130 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1131 | /// On Windows, this function currently does alter the file pointer. | |
| 1132 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1133 | pub fn preadvAll(self: File, iovecs: []os.iovec, offset: u64) PReadError!usize { | |
| 1134 | if (iovecs.len == 0) return 0; | |
| 1135 | ||
| 1136 | var i: usize = 0; | |
| 1137 | var off: usize = 0; | |
| 1138 | while (true) { | |
| 1139 | var amt = try self.preadv(iovecs[i..], offset + off); | |
| 1140 | var eof = amt == 0; | |
| 1141 | off += amt; | |
| 1142 | while (amt >= iovecs[i].iov_len) { | |
| 1143 | amt -= iovecs[i].iov_len; | |
| 1144 | i += 1; | |
| 1145 | if (i >= iovecs.len) return off; | |
| 1146 | eof = false; | |
| 1147 | } | |
| 1148 | if (eof) return off; | |
| 1149 | iovecs[i].iov_base += amt; | |
| 1150 | iovecs[i].iov_len -= amt; | |
| 1151 | } | |
| 1152 | } | |
| 1153 | ||
| 1154 | pub const WriteError = os.WriteError; | |
| 1155 | pub const PWriteError = os.PWriteError; | |
| 1156 | ||
| 1157 | pub fn write(self: File, bytes: []const u8) WriteError!usize { | |
| 1158 | if (is_windows) { | |
| 1159 | return windows.WriteFile(self.handle, bytes, null, self.intended_io_mode); | |
| 1160 | } | |
| 1161 | ||
| 1162 | if (self.intended_io_mode == .blocking) { | |
| 1163 | return os.write(self.handle, bytes); | |
| 1164 | } else { | |
| 1165 | return std.event.Loop.instance.?.write(self.handle, bytes, self.capable_io_mode != self.intended_io_mode); | |
| 1166 | } | |
| 1167 | } | |
| 1168 | ||
| 1169 | pub fn writeAll(self: File, bytes: []const u8) WriteError!void { | |
| 1170 | var index: usize = 0; | |
| 1171 | while (index < bytes.len) { | |
| 1172 | index += try self.write(bytes[index..]); | |
| 1173 | } | |
| 1174 | } | |
| 1175 | ||
| 1176 | /// On Windows, this function currently does alter the file pointer. | |
| 1177 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1178 | pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize { | |
| 1179 | if (is_windows) { | |
| 1180 | return windows.WriteFile(self.handle, bytes, offset, self.intended_io_mode); | |
| 1181 | } | |
| 1182 | ||
| 1183 | if (self.intended_io_mode == .blocking) { | |
| 1184 | return os.pwrite(self.handle, bytes, offset); | |
| 1185 | } else { | |
| 1186 | return std.event.Loop.instance.?.pwrite(self.handle, bytes, offset, self.capable_io_mode != self.intended_io_mode); | |
| 1187 | } | |
| 1188 | } | |
| 1189 | ||
| 1190 | /// On Windows, this function currently does alter the file pointer. | |
| 1191 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1192 | pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void { | |
| 1193 | var index: usize = 0; | |
| 1194 | while (index < bytes.len) { | |
| 1195 | index += try self.pwrite(bytes[index..], offset + index); | |
| 1196 | } | |
| 1197 | } | |
| 1198 | ||
| 1199 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1200 | /// See equivalent function: `std.net.Stream.writev`. | |
| 1201 | pub fn writev(self: File, iovecs: []const os.iovec_const) WriteError!usize { | |
| 1202 | if (is_windows) { | |
| 1203 | // TODO improve this to use WriteFileScatter | |
| 1204 | if (iovecs.len == 0) return @as(usize, 0); | |
| 1205 | const first = iovecs[0]; | |
| 1206 | return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode); | |
| 1207 | } | |
| 1208 | ||
| 1209 | if (self.intended_io_mode == .blocking) { | |
| 1210 | return os.writev(self.handle, iovecs); | |
| 1211 | } else { | |
| 1212 | return std.event.Loop.instance.?.writev(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode); | |
| 1213 | } | |
| 1214 | } | |
| 1215 | ||
| 1216 | /// The `iovecs` parameter is mutable because: | |
| 1217 | /// * This function needs to mutate the fields in order to handle partial | |
| 1218 | /// writes from the underlying OS layer. | |
| 1219 | /// * The OS layer expects pointer addresses to be inside the application's address space | |
| 1220 | /// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer | |
| 1221 | /// addresses when the length is zero. So this function modifies the iov_base fields | |
| 1222 | /// when the length is zero. | |
| 1223 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1224 | /// See equivalent function: `std.net.Stream.writevAll`. | |
| 1225 | pub fn writevAll(self: File, iovecs: []os.iovec_const) WriteError!void { | |
| 1226 | if (iovecs.len == 0) return; | |
| 1227 | ||
| 1228 | // We use the address of this local variable for all zero-length | |
| 1229 | // vectors so that the OS does not complain that we are giving it | |
| 1230 | // addresses outside the application's address space. | |
| 1231 | var garbage: [1]u8 = undefined; | |
| 1232 | for (iovecs) |*v| { | |
| 1233 | if (v.iov_len == 0) v.iov_base = &garbage; | |
| 1234 | } | |
| 1235 | ||
| 1236 | var i: usize = 0; | |
| 1237 | while (true) { | |
| 1238 | var amt = try self.writev(iovecs[i..]); | |
| 1239 | while (amt >= iovecs[i].iov_len) { | |
| 1240 | amt -= iovecs[i].iov_len; | |
| 1241 | i += 1; | |
| 1242 | if (i >= iovecs.len) return; | |
| 1243 | } | |
| 1244 | iovecs[i].iov_base += amt; | |
| 1245 | iovecs[i].iov_len -= amt; | |
| 1246 | } | |
| 1247 | } | |
| 1248 | ||
| 1249 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1250 | /// On Windows, this function currently does alter the file pointer. | |
| 1251 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1252 | pub fn pwritev(self: File, iovecs: []os.iovec_const, offset: u64) PWriteError!usize { | |
| 1253 | if (is_windows) { | |
| 1254 | // TODO improve this to use WriteFileScatter | |
| 1255 | if (iovecs.len == 0) return @as(usize, 0); | |
| 1256 | const first = iovecs[0]; | |
| 1257 | return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode); | |
| 1258 | } | |
| 1259 | ||
| 1260 | if (self.intended_io_mode == .blocking) { | |
| 1261 | return os.pwritev(self.handle, iovecs, offset); | |
| 1262 | } else { | |
| 1263 | return std.event.Loop.instance.?.pwritev(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode); | |
| 1264 | } | |
| 1265 | } | |
| 1266 | ||
| 1267 | /// The `iovecs` parameter is mutable because this function needs to mutate the fields in | |
| 1268 | /// order to handle partial writes from the underlying OS layer. | |
| 1269 | /// See https://github.com/ziglang/zig/issues/7699 | |
| 1270 | /// On Windows, this function currently does alter the file pointer. | |
| 1271 | /// https://github.com/ziglang/zig/issues/12783 | |
| 1272 | pub fn pwritevAll(self: File, iovecs: []os.iovec_const, offset: u64) PWriteError!void { | |
| 1273 | if (iovecs.len == 0) return; | |
| 1274 | ||
| 1275 | var i: usize = 0; | |
| 1276 | var off: u64 = 0; | |
| 1277 | while (true) { | |
| 1278 | var amt = try self.pwritev(iovecs[i..], offset + off); | |
| 1279 | off += amt; | |
| 1280 | while (amt >= iovecs[i].iov_len) { | |
| 1281 | amt -= iovecs[i].iov_len; | |
| 1282 | i += 1; | |
| 1283 | if (i >= iovecs.len) return; | |
| 1284 | } | |
| 1285 | iovecs[i].iov_base += amt; | |
| 1286 | iovecs[i].iov_len -= amt; | |
| 1287 | } | |
| 1288 | } | |
| 1289 | ||
| 1290 | pub const CopyRangeError = os.CopyFileRangeError; | |
| 1291 | ||
| 1292 | pub fn copyRange(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { | |
| 1293 | const adjusted_len = math.cast(usize, len) orelse math.maxInt(usize); | |
| 1294 | const result = try os.copy_file_range(in.handle, in_offset, out.handle, out_offset, adjusted_len, 0); | |
| 1295 | return result; | |
| 1296 | } | |
| 1297 | ||
| 1298 | /// Returns the number of bytes copied. If the number read is smaller than `buffer.len`, it | |
| 1299 | /// means the in file reached the end. Reaching the end of a file is not an error condition. | |
| 1300 | pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { | |
| 1301 | var total_bytes_copied: u64 = 0; | |
| 1302 | var in_off = in_offset; | |
| 1303 | var out_off = out_offset; | |
| 1304 | while (total_bytes_copied < len) { | |
| 1305 | const amt_copied = try copyRange(in, in_off, out, out_off, len - total_bytes_copied); | |
| 1306 | if (amt_copied == 0) return total_bytes_copied; | |
| 1307 | total_bytes_copied += amt_copied; | |
| 1308 | in_off += amt_copied; | |
| 1309 | out_off += amt_copied; | |
| 1310 | } | |
| 1311 | return total_bytes_copied; | |
| 1312 | } | |
| 1313 | ||
| 1314 | pub const WriteFileOptions = struct { | |
| 1315 | in_offset: u64 = 0, | |
| 1316 | ||
| 1317 | /// `null` means the entire file. `0` means no bytes from the file. | |
| 1318 | /// When this is `null`, trailers must be sent in a separate writev() call | |
| 1319 | /// due to a flaw in the BSD sendfile API. Other operating systems, such as | |
| 1320 | /// Linux, already do this anyway due to API limitations. | |
| 1321 | /// If the size of the source file is known, passing the size here will save one syscall. | |
| 1322 | in_len: ?u64 = null, | |
| 1323 | ||
| 1324 | headers_and_trailers: []os.iovec_const = &[0]os.iovec_const{}, | |
| 1325 | ||
| 1326 | /// The trailer count is inferred from `headers_and_trailers.len - header_count` | |
| 1327 | header_count: usize = 0, | |
| 1328 | }; | |
| 1329 | ||
| 1330 | pub const WriteFileError = ReadError || error{EndOfStream} || WriteError; | |
| 1331 | ||
| 1332 | pub fn writeFileAll(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void { | |
| 1333 | return self.writeFileAllSendfile(in_file, args) catch |err| switch (err) { | |
| 1334 | error.Unseekable, | |
| 1335 | error.FastOpenAlreadyInProgress, | |
| 1336 | error.MessageTooBig, | |
| 1337 | error.FileDescriptorNotASocket, | |
| 1338 | error.NetworkUnreachable, | |
| 1339 | error.NetworkSubsystemFailed, | |
| 1340 | => return self.writeFileAllUnseekable(in_file, args), | |
| 1341 | ||
| 1342 | else => |e| return e, | |
| 1343 | }; | |
| 1344 | } | |
| 1345 | ||
| 1346 | /// Does not try seeking in either of the File parameters. | |
| 1347 | /// See `writeFileAll` as an alternative to calling this. | |
| 1348 | pub fn writeFileAllUnseekable(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void { | |
| 1349 | const headers = args.headers_and_trailers[0..args.header_count]; | |
| 1350 | const trailers = args.headers_and_trailers[args.header_count..]; | |
| 1351 | ||
| 1352 | try self.writevAll(headers); | |
| 1353 | ||
| 1354 | try in_file.reader().skipBytes(args.in_offset, .{ .buf_size = 4096 }); | |
| 1355 | ||
| 1356 | var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init(); | |
| 1357 | if (args.in_len) |len| { | |
| 1358 | var stream = std.io.limitedReader(in_file.reader(), len); | |
| 1359 | try fifo.pump(stream.reader(), self.writer()); | |
| 1360 | } else { | |
| 1361 | try fifo.pump(in_file.reader(), self.writer()); | |
| 1362 | } | |
| 1363 | ||
| 1364 | try self.writevAll(trailers); | |
| 1365 | } | |
| 1366 | ||
| 1367 | /// Low level function which can fail for OS-specific reasons. | |
| 1368 | /// See `writeFileAll` as an alternative to calling this. | |
| 1369 | /// TODO integrate with async I/O | |
| 1370 | fn writeFileAllSendfile(self: File, in_file: File, args: WriteFileOptions) os.SendFileError!void { | |
| 1371 | const count = blk: { | |
| 1372 | if (args.in_len) |l| { | |
| 1373 | if (l == 0) { | |
| 1374 | return self.writevAll(args.headers_and_trailers); | |
| 1375 | } else { | |
| 1376 | break :blk l; | |
| 1377 | } | |
| 1378 | } else { | |
| 1379 | break :blk 0; | |
| 1380 | } | |
| 1381 | }; | |
| 1382 | const headers = args.headers_and_trailers[0..args.header_count]; | |
| 1383 | const trailers = args.headers_and_trailers[args.header_count..]; | |
| 1384 | const zero_iovec = &[0]os.iovec_const{}; | |
| 1385 | // When reading the whole file, we cannot put the trailers in the sendfile() syscall, | |
| 1386 | // because we have no way to determine whether a partial write is past the end of the file or not. | |
| 1387 | const trls = if (count == 0) zero_iovec else trailers; | |
| 1388 | const offset = args.in_offset; | |
| 1389 | const out_fd = self.handle; | |
| 1390 | const in_fd = in_file.handle; | |
| 1391 | const flags = 0; | |
| 1392 | var amt: usize = 0; | |
| 1393 | hdrs: { | |
| 1394 | var i: usize = 0; | |
| 1395 | while (i < headers.len) { | |
| 1396 | amt = try os.sendfile(out_fd, in_fd, offset, count, headers[i..], trls, flags); | |
| 1397 | while (amt >= headers[i].iov_len) { | |
| 1398 | amt -= headers[i].iov_len; | |
| 1399 | i += 1; | |
| 1400 | if (i >= headers.len) break :hdrs; | |
| 1401 | } | |
| 1402 | headers[i].iov_base += amt; | |
| 1403 | headers[i].iov_len -= amt; | |
| 1404 | } | |
| 1405 | } | |
| 1406 | if (count == 0) { | |
| 1407 | var off: u64 = amt; | |
| 1408 | while (true) { | |
| 1409 | amt = try os.sendfile(out_fd, in_fd, offset + off, 0, zero_iovec, zero_iovec, flags); | |
| 1410 | if (amt == 0) break; | |
| 1411 | off += amt; | |
| 1412 | } | |
| 1413 | } else { | |
| 1414 | var off: u64 = amt; | |
| 1415 | while (off < count) { | |
| 1416 | amt = try os.sendfile(out_fd, in_fd, offset + off, count - off, zero_iovec, trailers, flags); | |
| 1417 | off += amt; | |
| 1418 | } | |
| 1419 | amt = @as(usize, @intCast(off - count)); | |
| 1420 | } | |
| 1421 | var i: usize = 0; | |
| 1422 | while (i < trailers.len) { | |
| 1423 | while (amt >= trailers[i].iov_len) { | |
| 1424 | amt -= trailers[i].iov_len; | |
| 1425 | i += 1; | |
| 1426 | if (i >= trailers.len) return; | |
| 1427 | } | |
| 1428 | trailers[i].iov_base += amt; | |
| 1429 | trailers[i].iov_len -= amt; | |
| 1430 | amt = try os.writev(self.handle, trailers[i..]); | |
| 1431 | } | |
| 1432 | } | |
| 1433 | ||
| 1434 | pub const Reader = io.Reader(File, ReadError, read); | |
| 1435 | ||
| 1436 | pub fn reader(file: File) Reader { | |
| 1437 | return .{ .context = file }; | |
| 1438 | } | |
| 1439 | ||
| 1440 | pub const Writer = io.Writer(File, WriteError, write); | |
| 1441 | ||
| 1442 | pub fn writer(file: File) Writer { | |
| 1443 | return .{ .context = file }; | |
| 1444 | } | |
| 1445 | ||
| 1446 | pub const SeekableStream = io.SeekableStream( | |
| 1447 | File, | |
| 1448 | SeekError, | |
| 1449 | GetSeekPosError, | |
| 1450 | seekTo, | |
| 1451 | seekBy, | |
| 1452 | getPos, | |
| 1453 | getEndPos, | |
| 1454 | ); | |
| 1455 | ||
| 1456 | pub fn seekableStream(file: File) SeekableStream { | |
| 1457 | return .{ .context = file }; | |
| 1458 | } | |
| 1459 | ||
| 1460 | const range_off: windows.LARGE_INTEGER = 0; | |
| 1461 | const range_len: windows.LARGE_INTEGER = 1; | |
| 1462 | ||
| 1463 | pub const LockError = error{ | |
| 1464 | SystemResources, | |
| 1465 | FileLocksNotSupported, | |
| 1466 | } || os.UnexpectedError; | |
| 1467 | ||
| 1468 | /// Blocks when an incompatible lock is held by another process. | |
| 1469 | /// A process may hold only one type of lock (shared or exclusive) on | |
| 1470 | /// a file. When a process terminates in any way, the lock is released. | |
| 1471 | /// | |
| 1472 | /// Assumes the file is unlocked. | |
| 1473 | /// | |
| 1474 | /// TODO: integrate with async I/O | |
| 1475 | pub fn lock(file: File, l: Lock) LockError!void { | |
| 1476 | if (is_windows) { | |
| 1477 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 1478 | const exclusive = switch (l) { | |
| 1479 | .none => return, | |
| 1480 | .shared => false, | |
| 1481 | .exclusive => true, | |
| 1482 | }; | |
| 1483 | return windows.LockFile( | |
| 1484 | file.handle, | |
| 1485 | null, | |
| 1486 | null, | |
| 1487 | null, | |
| 1488 | &io_status_block, | |
| 1489 | &range_off, | |
| 1490 | &range_len, | |
| 1491 | null, | |
| 1492 | windows.FALSE, // non-blocking=false | |
| 1493 | @intFromBool(exclusive), | |
| 1494 | ) catch |err| switch (err) { | |
| 1495 | error.WouldBlock => unreachable, // non-blocking=false | |
| 1496 | else => |e| return e, | |
| 1497 | }; | |
| 1498 | } else { | |
| 1499 | return os.flock(file.handle, switch (l) { | |
| 1500 | .none => os.LOCK.UN, | |
| 1501 | .shared => os.LOCK.SH, | |
| 1502 | .exclusive => os.LOCK.EX, | |
| 1503 | }) catch |err| switch (err) { | |
| 1504 | error.WouldBlock => unreachable, // non-blocking=false | |
| 1505 | else => |e| return e, | |
| 1506 | }; | |
| 1507 | } | |
| 1508 | } | |
| 1509 | ||
| 1510 | /// Assumes the file is locked. | |
| 1511 | pub fn unlock(file: File) void { | |
| 1512 | if (is_windows) { | |
| 1513 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 1514 | return windows.UnlockFile( | |
| 1515 | file.handle, | |
| 1516 | &io_status_block, | |
| 1517 | &range_off, | |
| 1518 | &range_len, | |
| 1519 | null, | |
| 1520 | ) catch |err| switch (err) { | |
| 1521 | error.RangeNotLocked => unreachable, // Function assumes unlocked. | |
| 1522 | error.Unexpected => unreachable, // Resource deallocation must succeed. | |
| 1523 | }; | |
| 1524 | } else { | |
| 1525 | return os.flock(file.handle, os.LOCK.UN) catch |err| switch (err) { | |
| 1526 | error.WouldBlock => unreachable, // unlocking can't block | |
| 1527 | error.SystemResources => unreachable, // We are deallocating resources. | |
| 1528 | error.FileLocksNotSupported => unreachable, // We already got the lock. | |
| 1529 | error.Unexpected => unreachable, // Resource deallocation must succeed. | |
| 1530 | }; | |
| 1531 | } | |
| 1532 | } | |
| 1533 | ||
| 1534 | /// Attempts to obtain a lock, returning `true` if the lock is | |
| 1535 | /// obtained, and `false` if there was an existing incompatible lock held. | |
| 1536 | /// A process may hold only one type of lock (shared or exclusive) on | |
| 1537 | /// a file. When a process terminates in any way, the lock is released. | |
| 1538 | /// | |
| 1539 | /// Assumes the file is unlocked. | |
| 1540 | /// | |
| 1541 | /// TODO: integrate with async I/O | |
| 1542 | pub fn tryLock(file: File, l: Lock) LockError!bool { | |
| 1543 | if (is_windows) { | |
| 1544 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 1545 | const exclusive = switch (l) { | |
| 1546 | .none => return, | |
| 1547 | .shared => false, | |
| 1548 | .exclusive => true, | |
| 1549 | }; | |
| 1550 | windows.LockFile( | |
| 1551 | file.handle, | |
| 1552 | null, | |
| 1553 | null, | |
| 1554 | null, | |
| 1555 | &io_status_block, | |
| 1556 | &range_off, | |
| 1557 | &range_len, | |
| 1558 | null, | |
| 1559 | windows.TRUE, // non-blocking=true | |
| 1560 | @intFromBool(exclusive), | |
| 1561 | ) catch |err| switch (err) { | |
| 1562 | error.WouldBlock => return false, | |
| 1563 | else => |e| return e, | |
| 1564 | }; | |
| 1565 | } else { | |
| 1566 | os.flock(file.handle, switch (l) { | |
| 1567 | .none => os.LOCK.UN, | |
| 1568 | .shared => os.LOCK.SH | os.LOCK.NB, | |
| 1569 | .exclusive => os.LOCK.EX | os.LOCK.NB, | |
| 1570 | }) catch |err| switch (err) { | |
| 1571 | error.WouldBlock => return false, | |
| 1572 | else => |e| return e, | |
| 1573 | }; | |
| 1574 | } | |
| 1575 | return true; | |
| 1576 | } | |
| 1577 | ||
| 1578 | /// Assumes the file is already locked in exclusive mode. | |
| 1579 | /// Atomically modifies the lock to be in shared mode, without releasing it. | |
| 1580 | /// | |
| 1581 | /// TODO: integrate with async I/O | |
| 1582 | pub fn downgradeLock(file: File) LockError!void { | |
| 1583 | if (is_windows) { | |
| 1584 | // On Windows it works like a semaphore + exclusivity flag. To implement this | |
| 1585 | // function, we first obtain another lock in shared mode. This changes the | |
| 1586 | // exclusivity flag, but increments the semaphore to 2. So we follow up with | |
| 1587 | // an NtUnlockFile which decrements the semaphore but does not modify the | |
| 1588 | // exclusivity flag. | |
| 1589 | var io_status_block: windows.IO_STATUS_BLOCK = undefined; | |
| 1590 | windows.LockFile( | |
| 1591 | file.handle, | |
| 1592 | null, | |
| 1593 | null, | |
| 1594 | null, | |
| 1595 | &io_status_block, | |
| 1596 | &range_off, | |
| 1597 | &range_len, | |
| 1598 | null, | |
| 1599 | windows.TRUE, // non-blocking=true | |
| 1600 | windows.FALSE, // exclusive=false | |
| 1601 | ) catch |err| switch (err) { | |
| 1602 | error.WouldBlock => unreachable, // File was not locked in exclusive mode. | |
| 1603 | else => |e| return e, | |
| 1604 | }; | |
| 1605 | return windows.UnlockFile( | |
| 1606 | file.handle, | |
| 1607 | &io_status_block, | |
| 1608 | &range_off, | |
| 1609 | &range_len, | |
| 1610 | null, | |
| 1611 | ) catch |err| switch (err) { | |
| 1612 | error.RangeNotLocked => unreachable, // File was not locked. | |
| 1613 | error.Unexpected => unreachable, // Resource deallocation must succeed. | |
| 1614 | }; | |
| 1615 | } else { | |
| 1616 | return os.flock(file.handle, os.LOCK.SH | os.LOCK.NB) catch |err| switch (err) { | |
| 1617 | error.WouldBlock => unreachable, // File was not locked in exclusive mode. | |
| 1618 | else => |e| return e, | |
| 1619 | }; | |
| 1620 | } | |
| 1621 | } | |
| 1622 | }; |
lib/std/fs/test.zig+52-56| ... | ... | @@ -8,10 +8,8 @@ const wasi = std.os.wasi; |
| 8 | 8 | |
| 9 | 9 | const ArenaAllocator = std.heap.ArenaAllocator; |
| 10 | 10 | const Dir = std.fs.Dir; |
| 11 | const IterableDir = std.fs.IterableDir; | |
| 12 | 11 | const File = std.fs.File; |
| 13 | 12 | const tmpDir = testing.tmpDir; |
| 14 | const tmpIterableDir = testing.tmpIterableDir; | |
| 15 | 13 | |
| 16 | 14 | const PathType = enum { |
| 17 | 15 | relative, |
| ... | ... | @@ -74,19 +72,17 @@ const PathType = enum { |
| 74 | 72 | const TestContext = struct { |
| 75 | 73 | path_type: PathType, |
| 76 | 74 | arena: ArenaAllocator, |
| 77 | tmp: testing.TmpIterableDir, | |
| 75 | tmp: testing.TmpDir, | |
| 78 | 76 | dir: std.fs.Dir, |
| 79 | iterable_dir: std.fs.IterableDir, | |
| 80 | 77 | transform_fn: *const PathType.TransformFn, |
| 81 | 78 | |
| 82 | 79 | pub fn init(path_type: PathType, allocator: mem.Allocator, transform_fn: *const PathType.TransformFn) TestContext { |
| 83 | const tmp = tmpIterableDir(.{}); | |
| 80 | const tmp = tmpDir(.{ .iterate = true }); | |
| 84 | 81 | return .{ |
| 85 | 82 | .path_type = path_type, |
| 86 | 83 | .arena = ArenaAllocator.init(allocator), |
| 87 | 84 | .tmp = tmp, |
| 88 | .dir = tmp.iterable_dir.dir, | |
| 89 | .iterable_dir = tmp.iterable_dir, | |
| 85 | .dir = tmp.dir, | |
| 90 | 86 | .transform_fn = transform_fn, |
| 91 | 87 | }; |
| 92 | 88 | } |
| ... | ... | @@ -323,28 +319,28 @@ fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void |
| 323 | 319 | } |
| 324 | 320 | |
| 325 | 321 | test "Dir.Iterator" { |
| 326 | var tmp_dir = tmpIterableDir(.{}); | |
| 322 | var tmp_dir = tmpDir(.{ .iterate = true }); | |
| 327 | 323 | defer tmp_dir.cleanup(); |
| 328 | 324 | |
| 329 | 325 | // First, create a couple of entries to iterate over. |
| 330 | const file = try tmp_dir.iterable_dir.dir.createFile("some_file", .{}); | |
| 326 | const file = try tmp_dir.dir.createFile("some_file", .{}); | |
| 331 | 327 | file.close(); |
| 332 | 328 | |
| 333 | try tmp_dir.iterable_dir.dir.makeDir("some_dir"); | |
| 329 | try tmp_dir.dir.makeDir("some_dir"); | |
| 334 | 330 | |
| 335 | 331 | var arena = ArenaAllocator.init(testing.allocator); |
| 336 | 332 | defer arena.deinit(); |
| 337 | 333 | const allocator = arena.allocator(); |
| 338 | 334 | |
| 339 | var entries = std.ArrayList(IterableDir.Entry).init(allocator); | |
| 335 | var entries = std.ArrayList(Dir.Entry).init(allocator); | |
| 340 | 336 | |
| 341 | 337 | // Create iterator. |
| 342 | var iter = tmp_dir.iterable_dir.iterate(); | |
| 338 | var iter = tmp_dir.dir.iterate(); | |
| 343 | 339 | while (try iter.next()) |entry| { |
| 344 | 340 | // We cannot just store `entry` as on Windows, we're re-using the name buffer |
| 345 | 341 | // which means we'll actually share the `name` pointer between entries! |
| 346 | 342 | const name = try allocator.dupe(u8, entry.name); |
| 347 | try entries.append(.{ .name = name, .kind = entry.kind }); | |
| 343 | try entries.append(Dir.Entry{ .name = name, .kind = entry.kind }); | |
| 348 | 344 | } |
| 349 | 345 | |
| 350 | 346 | try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..' |
| ... | ... | @@ -353,7 +349,7 @@ test "Dir.Iterator" { |
| 353 | 349 | } |
| 354 | 350 | |
| 355 | 351 | test "Dir.Iterator many entries" { |
| 356 | var tmp_dir = tmpIterableDir(.{}); | |
| 352 | var tmp_dir = tmpDir(.{ .iterate = true }); | |
| 357 | 353 | defer tmp_dir.cleanup(); |
| 358 | 354 | |
| 359 | 355 | const num = 1024; |
| ... | ... | @@ -361,7 +357,7 @@ test "Dir.Iterator many entries" { |
| 361 | 357 | var buf: [4]u8 = undefined; // Enough to store "1024". |
| 362 | 358 | while (i < num) : (i += 1) { |
| 363 | 359 | const name = try std.fmt.bufPrint(&buf, "{}", .{i}); |
| 364 | const file = try tmp_dir.iterable_dir.dir.createFile(name, .{}); | |
| 360 | const file = try tmp_dir.dir.createFile(name, .{}); | |
| 365 | 361 | file.close(); |
| 366 | 362 | } |
| 367 | 363 | |
| ... | ... | @@ -369,10 +365,10 @@ test "Dir.Iterator many entries" { |
| 369 | 365 | defer arena.deinit(); |
| 370 | 366 | const allocator = arena.allocator(); |
| 371 | 367 | |
| 372 | var entries = std.ArrayList(IterableDir.Entry).init(allocator); | |
| 368 | var entries = std.ArrayList(Dir.Entry).init(allocator); | |
| 373 | 369 | |
| 374 | 370 | // Create iterator. |
| 375 | var iter = tmp_dir.iterable_dir.iterate(); | |
| 371 | var iter = tmp_dir.dir.iterate(); | |
| 376 | 372 | while (try iter.next()) |entry| { |
| 377 | 373 | // We cannot just store `entry` as on Windows, we're re-using the name buffer |
| 378 | 374 | // which means we'll actually share the `name` pointer between entries! |
| ... | ... | @@ -388,14 +384,14 @@ test "Dir.Iterator many entries" { |
| 388 | 384 | } |
| 389 | 385 | |
| 390 | 386 | test "Dir.Iterator twice" { |
| 391 | var tmp_dir = tmpIterableDir(.{}); | |
| 387 | var tmp_dir = tmpDir(.{ .iterate = true }); | |
| 392 | 388 | defer tmp_dir.cleanup(); |
| 393 | 389 | |
| 394 | 390 | // First, create a couple of entries to iterate over. |
| 395 | const file = try tmp_dir.iterable_dir.dir.createFile("some_file", .{}); | |
| 391 | const file = try tmp_dir.dir.createFile("some_file", .{}); | |
| 396 | 392 | file.close(); |
| 397 | 393 | |
| 398 | try tmp_dir.iterable_dir.dir.makeDir("some_dir"); | |
| 394 | try tmp_dir.dir.makeDir("some_dir"); | |
| 399 | 395 | |
| 400 | 396 | var arena = ArenaAllocator.init(testing.allocator); |
| 401 | 397 | defer arena.deinit(); |
| ... | ... | @@ -403,15 +399,15 @@ test "Dir.Iterator twice" { |
| 403 | 399 | |
| 404 | 400 | var i: u8 = 0; |
| 405 | 401 | while (i < 2) : (i += 1) { |
| 406 | var entries = std.ArrayList(IterableDir.Entry).init(allocator); | |
| 402 | var entries = std.ArrayList(Dir.Entry).init(allocator); | |
| 407 | 403 | |
| 408 | 404 | // Create iterator. |
| 409 | var iter = tmp_dir.iterable_dir.iterate(); | |
| 405 | var iter = tmp_dir.dir.iterate(); | |
| 410 | 406 | while (try iter.next()) |entry| { |
| 411 | 407 | // We cannot just store `entry` as on Windows, we're re-using the name buffer |
| 412 | 408 | // which means we'll actually share the `name` pointer between entries! |
| 413 | 409 | const name = try allocator.dupe(u8, entry.name); |
| 414 | try entries.append(.{ .name = name, .kind = entry.kind }); | |
| 410 | try entries.append(Dir.Entry{ .name = name, .kind = entry.kind }); | |
| 415 | 411 | } |
| 416 | 412 | |
| 417 | 413 | try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..' |
| ... | ... | @@ -421,25 +417,25 @@ test "Dir.Iterator twice" { |
| 421 | 417 | } |
| 422 | 418 | |
| 423 | 419 | test "Dir.Iterator reset" { |
| 424 | var tmp_dir = tmpIterableDir(.{}); | |
| 420 | var tmp_dir = tmpDir(.{ .iterate = true }); | |
| 425 | 421 | defer tmp_dir.cleanup(); |
| 426 | 422 | |
| 427 | 423 | // First, create a couple of entries to iterate over. |
| 428 | const file = try tmp_dir.iterable_dir.dir.createFile("some_file", .{}); | |
| 424 | const file = try tmp_dir.dir.createFile("some_file", .{}); | |
| 429 | 425 | file.close(); |
| 430 | 426 | |
| 431 | try tmp_dir.iterable_dir.dir.makeDir("some_dir"); | |
| 427 | try tmp_dir.dir.makeDir("some_dir"); | |
| 432 | 428 | |
| 433 | 429 | var arena = ArenaAllocator.init(testing.allocator); |
| 434 | 430 | defer arena.deinit(); |
| 435 | 431 | const allocator = arena.allocator(); |
| 436 | 432 | |
| 437 | 433 | // Create iterator. |
| 438 | var iter = tmp_dir.iterable_dir.iterate(); | |
| 434 | var iter = tmp_dir.dir.iterate(); | |
| 439 | 435 | |
| 440 | 436 | var i: u8 = 0; |
| 441 | 437 | while (i < 2) : (i += 1) { |
| 442 | var entries = std.ArrayList(IterableDir.Entry).init(allocator); | |
| 438 | var entries = std.ArrayList(Dir.Entry).init(allocator); | |
| 443 | 439 | |
| 444 | 440 | while (try iter.next()) |entry| { |
| 445 | 441 | // We cannot just store `entry` as on Windows, we're re-using the name buffer |
| ... | ... | @@ -461,10 +457,10 @@ test "Dir.Iterator but dir is deleted during iteration" { |
| 461 | 457 | defer tmp.cleanup(); |
| 462 | 458 | |
| 463 | 459 | // Create directory and setup an iterator for it |
| 464 | var iterable_subdir = try tmp.dir.makeOpenPathIterable("subdir", .{}); | |
| 465 | defer iterable_subdir.close(); | |
| 460 | var subdir = try tmp.dir.makeOpenPath("subdir", .{ .iterate = true }); | |
| 461 | defer subdir.close(); | |
| 466 | 462 | |
| 467 | var iterator = iterable_subdir.iterate(); | |
| 463 | var iterator = subdir.iterate(); | |
| 468 | 464 | |
| 469 | 465 | // Create something to iterate over within the subdir |
| 470 | 466 | try tmp.dir.makePath("subdir/b"); |
| ... | ... | @@ -485,11 +481,11 @@ test "Dir.Iterator but dir is deleted during iteration" { |
| 485 | 481 | } |
| 486 | 482 | } |
| 487 | 483 | |
| 488 | fn entryEql(lhs: IterableDir.Entry, rhs: IterableDir.Entry) bool { | |
| 484 | fn entryEql(lhs: Dir.Entry, rhs: Dir.Entry) bool { | |
| 489 | 485 | return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind; |
| 490 | 486 | } |
| 491 | 487 | |
| 492 | fn contains(entries: *const std.ArrayList(IterableDir.Entry), el: IterableDir.Entry) bool { | |
| 488 | fn contains(entries: *const std.ArrayList(Dir.Entry), el: Dir.Entry) bool { | |
| 493 | 489 | for (entries.items) |entry| { |
| 494 | 490 | if (entryEql(entry, el)) return true; |
| 495 | 491 | } |
| ... | ... | @@ -963,10 +959,10 @@ test "makePath in a directory that no longer exists" { |
| 963 | 959 | try testing.expectError(error.FileNotFound, tmp.dir.makePath("sub-path")); |
| 964 | 960 | } |
| 965 | 961 | |
| 966 | fn testFilenameLimits(iterable_dir: IterableDir, maxed_filename: []const u8) !void { | |
| 962 | fn testFilenameLimits(iterable_dir: Dir, maxed_filename: []const u8) !void { | |
| 967 | 963 | // setup, create a dir and a nested file both with maxed filenames, and walk the dir |
| 968 | 964 | { |
| 969 | var maxed_dir = try iterable_dir.dir.makeOpenPath(maxed_filename, .{}); | |
| 965 | var maxed_dir = try iterable_dir.makeOpenPath(maxed_filename, .{}); | |
| 970 | 966 | defer maxed_dir.close(); |
| 971 | 967 | |
| 972 | 968 | try maxed_dir.writeFile(maxed_filename, ""); |
| ... | ... | @@ -983,27 +979,27 @@ fn testFilenameLimits(iterable_dir: IterableDir, maxed_filename: []const u8) !vo |
| 983 | 979 | } |
| 984 | 980 | |
| 985 | 981 | // ensure that we can delete the tree |
| 986 | try iterable_dir.dir.deleteTree(maxed_filename); | |
| 982 | try iterable_dir.deleteTree(maxed_filename); | |
| 987 | 983 | } |
| 988 | 984 | |
| 989 | 985 | test "max file name component lengths" { |
| 990 | var tmp = tmpIterableDir(.{}); | |
| 986 | var tmp = tmpDir(.{ .iterate = true }); | |
| 991 | 987 | defer tmp.cleanup(); |
| 992 | 988 | |
| 993 | 989 | if (builtin.os.tag == .windows) { |
| 994 | 990 | // U+FFFF is the character with the largest code point that is encoded as a single |
| 995 | 991 | // UTF-16 code unit, so Windows allows for NAME_MAX of them. |
| 996 | 992 | const maxed_windows_filename = ("\u{FFFF}".*) ** std.os.windows.NAME_MAX; |
| 997 | try testFilenameLimits(tmp.iterable_dir, &maxed_windows_filename); | |
| 993 | try testFilenameLimits(tmp.dir, &maxed_windows_filename); | |
| 998 | 994 | } else if (builtin.os.tag == .wasi) { |
| 999 | 995 | // On WASI, the maxed filename depends on the host OS, so in order for this test to |
| 1000 | 996 | // work on any host, we need to use a length that will work for all platforms |
| 1001 | 997 | // (i.e. the minimum MAX_NAME_BYTES of all supported platforms). |
| 1002 | 998 | const maxed_wasi_filename = [_]u8{'1'} ** 255; |
| 1003 | try testFilenameLimits(tmp.iterable_dir, &maxed_wasi_filename); | |
| 999 | try testFilenameLimits(tmp.dir, &maxed_wasi_filename); | |
| 1004 | 1000 | } else { |
| 1005 | 1001 | const maxed_ascii_filename = [_]u8{'1'} ** std.fs.MAX_NAME_BYTES; |
| 1006 | try testFilenameLimits(tmp.iterable_dir, &maxed_ascii_filename); | |
| 1002 | try testFilenameLimits(tmp.dir, &maxed_ascii_filename); | |
| 1007 | 1003 | } |
| 1008 | 1004 | } |
| 1009 | 1005 | |
| ... | ... | @@ -1384,7 +1380,7 @@ test "open file with exclusive nonblocking lock twice (absolute paths)" { |
| 1384 | 1380 | test "walker" { |
| 1385 | 1381 | if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; |
| 1386 | 1382 | |
| 1387 | var tmp = tmpIterableDir(.{}); | |
| 1383 | var tmp = tmpDir(.{ .iterate = true }); | |
| 1388 | 1384 | defer tmp.cleanup(); |
| 1389 | 1385 | |
| 1390 | 1386 | // iteration order of walker is undefined, so need lookup maps to check against |
| ... | ... | @@ -1410,10 +1406,10 @@ test "walker" { |
| 1410 | 1406 | }); |
| 1411 | 1407 | |
| 1412 | 1408 | for (expected_paths.kvs) |kv| { |
| 1413 | try tmp.iterable_dir.dir.makePath(kv.key); | |
| 1409 | try tmp.dir.makePath(kv.key); | |
| 1414 | 1410 | } |
| 1415 | 1411 | |
| 1416 | var walker = try tmp.iterable_dir.walk(testing.allocator); | |
| 1412 | var walker = try tmp.dir.walk(testing.allocator); | |
| 1417 | 1413 | defer walker.deinit(); |
| 1418 | 1414 | |
| 1419 | 1415 | var num_walked: usize = 0; |
| ... | ... | @@ -1437,17 +1433,17 @@ test "walker" { |
| 1437 | 1433 | test "walker without fully iterating" { |
| 1438 | 1434 | if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; |
| 1439 | 1435 | |
| 1440 | var tmp = tmpIterableDir(.{}); | |
| 1436 | var tmp = tmpDir(.{ .iterate = true }); | |
| 1441 | 1437 | defer tmp.cleanup(); |
| 1442 | 1438 | |
| 1443 | var walker = try tmp.iterable_dir.walk(testing.allocator); | |
| 1439 | var walker = try tmp.dir.walk(testing.allocator); | |
| 1444 | 1440 | defer walker.deinit(); |
| 1445 | 1441 | |
| 1446 | 1442 | // Create 2 directories inside the tmp directory, but then only iterate once before breaking. |
| 1447 | 1443 | // This ensures that walker doesn't try to close the initial directory when not fully iterating. |
| 1448 | 1444 | |
| 1449 | try tmp.iterable_dir.dir.makePath("a"); | |
| 1450 | try tmp.iterable_dir.dir.makePath("b"); | |
| 1445 | try tmp.dir.makePath("a"); | |
| 1446 | try tmp.dir.makePath("b"); | |
| 1451 | 1447 | |
| 1452 | 1448 | var num_walked: usize = 0; |
| 1453 | 1449 | while (try walker.next()) |_| { |
| ... | ... | @@ -1490,7 +1486,7 @@ test ". and .. in fs.Dir functions" { |
| 1490 | 1486 | |
| 1491 | 1487 | try ctx.dir.writeFile(update_path, "something"); |
| 1492 | 1488 | const prev_status = try ctx.dir.updateFile(file_path, ctx.dir, update_path, .{}); |
| 1493 | try testing.expectEqual(fs.PrevStatus.stale, prev_status); | |
| 1489 | try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status); | |
| 1494 | 1490 | |
| 1495 | 1491 | try ctx.dir.deleteDir(subdir_path); |
| 1496 | 1492 | } |
| ... | ... | @@ -1536,7 +1532,7 @@ test ". and .. in absolute functions" { |
| 1536 | 1532 | try update_file.writeAll("something"); |
| 1537 | 1533 | update_file.close(); |
| 1538 | 1534 | const prev_status = try fs.updateFileAbsolute(created_file_path, update_file_path, .{}); |
| 1539 | try testing.expectEqual(fs.PrevStatus.stale, prev_status); | |
| 1535 | try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status); | |
| 1540 | 1536 | |
| 1541 | 1537 | try fs.deleteDirAbsolute(subdir_path); |
| 1542 | 1538 | } |
| ... | ... | @@ -1556,11 +1552,11 @@ test "chmod" { |
| 1556 | 1552 | try testing.expectEqual(@as(File.Mode, 0o644), (try file.stat()).mode & 0o7777); |
| 1557 | 1553 | |
| 1558 | 1554 | try tmp.dir.makeDir("test_dir"); |
| 1559 | var iterable_dir = try tmp.dir.openIterableDir("test_dir", .{}); | |
| 1560 | defer iterable_dir.close(); | |
| 1555 | var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true }); | |
| 1556 | defer dir.close(); | |
| 1561 | 1557 | |
| 1562 | try iterable_dir.chmod(0o700); | |
| 1563 | try testing.expectEqual(@as(File.Mode, 0o700), (try iterable_dir.dir.stat()).mode & 0o7777); | |
| 1558 | try dir.chmod(0o700); | |
| 1559 | try testing.expectEqual(@as(File.Mode, 0o700), (try dir.stat()).mode & 0o7777); | |
| 1564 | 1560 | } |
| 1565 | 1561 | |
| 1566 | 1562 | test "chown" { |
| ... | ... | @@ -1576,9 +1572,9 @@ test "chown" { |
| 1576 | 1572 | |
| 1577 | 1573 | try tmp.dir.makeDir("test_dir"); |
| 1578 | 1574 | |
| 1579 | var iterable_dir = try tmp.dir.openIterableDir("test_dir", .{}); | |
| 1580 | defer iterable_dir.close(); | |
| 1581 | try iterable_dir.chown(null, null); | |
| 1575 | var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true }); | |
| 1576 | defer dir.close(); | |
| 1577 | try dir.chown(null, null); | |
| 1582 | 1578 | } |
| 1583 | 1579 | |
| 1584 | 1580 | test "File.Metadata" { |
lib/std/os.zig+1-1| ... | ... | @@ -402,7 +402,7 @@ pub fn fchown(fd: fd_t, owner: ?uid_t, group: ?gid_t) FChownError!void { |
| 402 | 402 | switch (system.getErrno(res)) { |
| 403 | 403 | .SUCCESS => return, |
| 404 | 404 | .INTR => continue, |
| 405 | .BADF => unreachable, // Can be reached if the fd refers to a non-iterable directory. | |
| 405 | .BADF => unreachable, // Can be reached if the fd refers to a directory opened without `OpenDirOptions{ .iterate = true }` | |
| 406 | 406 | |
| 407 | 407 | .FAULT => unreachable, |
| 408 | 408 | .INVAL => unreachable, |
lib/std/testing.zig-38| ... | ... | @@ -543,22 +543,6 @@ pub const TmpDir = struct { |
| 543 | 543 | } |
| 544 | 544 | }; |
| 545 | 545 | |
| 546 | pub const TmpIterableDir = struct { | |
| 547 | iterable_dir: std.fs.IterableDir, | |
| 548 | parent_dir: std.fs.Dir, | |
| 549 | sub_path: [sub_path_len]u8, | |
| 550 | ||
| 551 | const random_bytes_count = 12; | |
| 552 | const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count); | |
| 553 | ||
| 554 | pub fn cleanup(self: *TmpIterableDir) void { | |
| 555 | self.iterable_dir.close(); | |
| 556 | self.parent_dir.deleteTree(&self.sub_path) catch {}; | |
| 557 | self.parent_dir.close(); | |
| 558 | self.* = undefined; | |
| 559 | } | |
| 560 | }; | |
| 561 | ||
| 562 | 546 | pub fn tmpDir(opts: std.fs.Dir.OpenDirOptions) TmpDir { |
| 563 | 547 | var random_bytes: [TmpDir.random_bytes_count]u8 = undefined; |
| 564 | 548 | std.crypto.random.bytes(&random_bytes); |
| ... | ... | @@ -581,28 +565,6 @@ pub fn tmpDir(opts: std.fs.Dir.OpenDirOptions) TmpDir { |
| 581 | 565 | }; |
| 582 | 566 | } |
| 583 | 567 | |
| 584 | pub fn tmpIterableDir(opts: std.fs.Dir.OpenDirOptions) TmpIterableDir { | |
| 585 | var random_bytes: [TmpIterableDir.random_bytes_count]u8 = undefined; | |
| 586 | std.crypto.random.bytes(&random_bytes); | |
| 587 | var sub_path: [TmpIterableDir.sub_path_len]u8 = undefined; | |
| 588 | _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); | |
| 589 | ||
| 590 | const cwd = std.fs.cwd(); | |
| 591 | var cache_dir = cwd.makeOpenPath("zig-cache", .{}) catch | |
| 592 | @panic("unable to make tmp dir for testing: unable to make and open zig-cache dir"); | |
| 593 | defer cache_dir.close(); | |
| 594 | const parent_dir = cache_dir.makeOpenPath("tmp", .{}) catch | |
| 595 | @panic("unable to make tmp dir for testing: unable to make and open zig-cache/tmp dir"); | |
| 596 | const dir = parent_dir.makeOpenPathIterable(&sub_path, opts) catch | |
| 597 | @panic("unable to make tmp dir for testing: unable to make and open the tmp dir"); | |
| 598 | ||
| 599 | return .{ | |
| 600 | .iterable_dir = dir, | |
| 601 | .parent_dir = parent_dir, | |
| 602 | .sub_path = sub_path, | |
| 603 | }; | |
| 604 | } | |
| 605 | ||
| 606 | 568 | test "expectEqual nested array" { |
| 607 | 569 | const a = [2][2]f32{ |
| 608 | 570 | [_]f32{ 1.0, 0.0 }, |
src/Package/Fetch.zig+14-12| ... | ... | @@ -280,7 +280,7 @@ pub fn run(f: *Fetch) RunError!void { |
| 280 | 280 | }, |
| 281 | 281 | .remote => |remote| remote, |
| 282 | 282 | .path_or_url => |path_or_url| { |
| 283 | if (fs.cwd().openIterableDir(path_or_url, .{})) |dir| { | |
| 283 | if (fs.cwd().openDir(path_or_url, .{ .iterate = true })) |dir| { | |
| 284 | 284 | var resource: Resource = .{ .dir = dir }; |
| 285 | 285 | return runResource(f, path_or_url, &resource, null); |
| 286 | 286 | } else |dir_err| { |
| ... | ... | @@ -363,7 +363,9 @@ fn runResource( |
| 363 | 363 | var tmp_directory: Cache.Directory = .{ |
| 364 | 364 | .path = tmp_directory_path, |
| 365 | 365 | .handle = handle: { |
| 366 | const dir = cache_root.handle.makeOpenPathIterable(tmp_dir_sub_path, .{}) catch |err| { | |
| 366 | const dir = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{ | |
| 367 | .iterate = true, | |
| 368 | }) catch |err| { | |
| 367 | 369 | try eb.addRootErrorMessage(.{ |
| 368 | 370 | .msg = try eb.printString("unable to create temporary directory '{s}': {s}", .{ |
| 369 | 371 | tmp_directory_path, @errorName(err), |
| ... | ... | @@ -371,7 +373,7 @@ fn runResource( |
| 371 | 373 | }); |
| 372 | 374 | return error.FetchFailed; |
| 373 | 375 | }; |
| 374 | break :handle dir.dir; | |
| 376 | break :handle dir; | |
| 375 | 377 | }, |
| 376 | 378 | }; |
| 377 | 379 | defer tmp_directory.handle.close(); |
| ... | ... | @@ -400,9 +402,9 @@ fn runResource( |
| 400 | 402 | if (builtin.os.tag == .linux and f.job_queue.work_around_btrfs_bug) { |
| 401 | 403 | // https://github.com/ziglang/zig/issues/17095 |
| 402 | 404 | tmp_directory.handle.close(); |
| 403 | const iterable_dir = cache_root.handle.makeOpenPathIterable(tmp_dir_sub_path, .{}) catch | |
| 404 | @panic("btrfs workaround failed"); | |
| 405 | tmp_directory.handle = iterable_dir.dir; | |
| 405 | tmp_directory.handle = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{ | |
| 406 | .iterate = true, | |
| 407 | }) catch @panic("btrfs workaround failed"); | |
| 406 | 408 | } |
| 407 | 409 | |
| 408 | 410 | f.actual_hash = try computeHash(f, tmp_directory, filter); |
| ... | ... | @@ -717,7 +719,7 @@ const Resource = union(enum) { |
| 717 | 719 | file: fs.File, |
| 718 | 720 | http_request: std.http.Client.Request, |
| 719 | 721 | git: Git, |
| 720 | dir: fs.IterableDir, | |
| 722 | dir: fs.Dir, | |
| 721 | 723 | |
| 722 | 724 | const Git = struct { |
| 723 | 725 | fetch_stream: git.Session.FetchStream, |
| ... | ... | @@ -1198,7 +1200,7 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource) anyerror!void |
| 1198 | 1200 | try out_dir.deleteTree(".git"); |
| 1199 | 1201 | } |
| 1200 | 1202 | |
| 1201 | fn recursiveDirectoryCopy(f: *Fetch, dir: fs.IterableDir, tmp_dir: fs.Dir) anyerror!void { | |
| 1203 | fn recursiveDirectoryCopy(f: *Fetch, dir: fs.Dir, tmp_dir: fs.Dir) anyerror!void { | |
| 1202 | 1204 | const gpa = f.arena.child_allocator; |
| 1203 | 1205 | // Recursive directory copy. |
| 1204 | 1206 | var it = try dir.walk(gpa); |
| ... | ... | @@ -1207,7 +1209,7 @@ fn recursiveDirectoryCopy(f: *Fetch, dir: fs.IterableDir, tmp_dir: fs.Dir) anyer |
| 1207 | 1209 | switch (entry.kind) { |
| 1208 | 1210 | .directory => {}, // omit empty directories |
| 1209 | 1211 | .file => { |
| 1210 | dir.dir.copyFile( | |
| 1212 | dir.copyFile( | |
| 1211 | 1213 | entry.path, |
| 1212 | 1214 | tmp_dir, |
| 1213 | 1215 | entry.path, |
| ... | ... | @@ -1215,14 +1217,14 @@ fn recursiveDirectoryCopy(f: *Fetch, dir: fs.IterableDir, tmp_dir: fs.Dir) anyer |
| 1215 | 1217 | ) catch |err| switch (err) { |
| 1216 | 1218 | error.FileNotFound => { |
| 1217 | 1219 | if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(dirname); |
| 1218 | try dir.dir.copyFile(entry.path, tmp_dir, entry.path, .{}); | |
| 1220 | try dir.copyFile(entry.path, tmp_dir, entry.path, .{}); | |
| 1219 | 1221 | }, |
| 1220 | 1222 | else => |e| return e, |
| 1221 | 1223 | }; |
| 1222 | 1224 | }, |
| 1223 | 1225 | .sym_link => { |
| 1224 | 1226 | var buf: [fs.MAX_PATH_BYTES]u8 = undefined; |
| 1225 | const link_name = try dir.dir.readLink(entry.path, &buf); | |
| 1227 | const link_name = try dir.readLink(entry.path, &buf); | |
| 1226 | 1228 | // TODO: if this would create a symlink to outside |
| 1227 | 1229 | // the destination directory, fail with an error instead. |
| 1228 | 1230 | tmp_dir.symLink(link_name, entry.path, .{}) catch |err| switch (err) { |
| ... | ... | @@ -1296,7 +1298,7 @@ fn computeHash( |
| 1296 | 1298 | var sus_dirs: std.StringArrayHashMapUnmanaged(void) = .{}; |
| 1297 | 1299 | defer sus_dirs.deinit(gpa); |
| 1298 | 1300 | |
| 1299 | var walker = try @as(fs.IterableDir, .{ .dir = tmp_directory.handle }).walk(gpa); | |
| 1301 | var walker = try tmp_directory.handle.walk(gpa); | |
| 1300 | 1302 | defer walker.deinit(); |
| 1301 | 1303 | |
| 1302 | 1304 | { |
src/Package/Fetch/git.zig+4-4| ... | ... | @@ -1384,11 +1384,11 @@ test "packfile indexing and checkout" { |
| 1384 | 1384 | var repository = try Repository.init(testing.allocator, pack_file, index_file); |
| 1385 | 1385 | defer repository.deinit(); |
| 1386 | 1386 | |
| 1387 | var worktree = testing.tmpIterableDir(.{}); | |
| 1387 | var worktree = testing.tmpDir(.{ .iterate = true }); | |
| 1388 | 1388 | defer worktree.cleanup(); |
| 1389 | 1389 | |
| 1390 | 1390 | const commit_id = try parseOid("dd582c0720819ab7130b103635bd7271b9fd4feb"); |
| 1391 | try repository.checkout(worktree.iterable_dir.dir, commit_id); | |
| 1391 | try repository.checkout(worktree.dir, commit_id); | |
| 1392 | 1392 | |
| 1393 | 1393 | const expected_files: []const []const u8 = &.{ |
| 1394 | 1394 | "dir/file", |
| ... | ... | @@ -1410,7 +1410,7 @@ test "packfile indexing and checkout" { |
| 1410 | 1410 | var actual_files: std.ArrayListUnmanaged([]u8) = .{}; |
| 1411 | 1411 | defer actual_files.deinit(testing.allocator); |
| 1412 | 1412 | defer for (actual_files.items) |file| testing.allocator.free(file); |
| 1413 | var walker = try worktree.iterable_dir.walk(testing.allocator); | |
| 1413 | var walker = try worktree.dir.walk(testing.allocator); | |
| 1414 | 1414 | defer walker.deinit(); |
| 1415 | 1415 | while (try walker.next()) |entry| { |
| 1416 | 1416 | if (entry.kind != .file) continue; |
| ... | ... | @@ -1442,7 +1442,7 @@ test "packfile indexing and checkout" { |
| 1442 | 1442 | \\revision 19 |
| 1443 | 1443 | \\ |
| 1444 | 1444 | ; |
| 1445 | const actual_file_contents = try worktree.iterable_dir.dir.readFileAlloc(testing.allocator, "file", max_file_size); | |
| 1445 | const actual_file_contents = try worktree.dir.readFileAlloc(testing.allocator, "file", max_file_size); | |
| 1446 | 1446 | defer testing.allocator.free(actual_file_contents); |
| 1447 | 1447 | try testing.expectEqualStrings(expected_file_contents, actual_file_contents); |
| 1448 | 1448 | } |
src/main.zig+6-6| ... | ... | @@ -5698,13 +5698,13 @@ fn fmtPathDir( |
| 5698 | 5698 | parent_dir: fs.Dir, |
| 5699 | 5699 | parent_sub_path: []const u8, |
| 5700 | 5700 | ) FmtError!void { |
| 5701 | var iterable_dir = try parent_dir.openIterableDir(parent_sub_path, .{}); | |
| 5702 | defer iterable_dir.close(); | |
| 5701 | var dir = try parent_dir.openDir(parent_sub_path, .{ .iterate = true }); | |
| 5702 | defer dir.close(); | |
| 5703 | 5703 | |
| 5704 | const stat = try iterable_dir.dir.stat(); | |
| 5704 | const stat = try dir.stat(); | |
| 5705 | 5705 | if (try fmt.seen.fetchPut(stat.inode, {})) |_| return; |
| 5706 | 5706 | |
| 5707 | var dir_it = iterable_dir.iterate(); | |
| 5707 | var dir_it = dir.iterate(); | |
| 5708 | 5708 | while (try dir_it.next()) |entry| { |
| 5709 | 5709 | const is_dir = entry.kind == .directory; |
| 5710 | 5710 | |
| ... | ... | @@ -5715,9 +5715,9 @@ fn fmtPathDir( |
| 5715 | 5715 | defer fmt.gpa.free(full_path); |
| 5716 | 5716 | |
| 5717 | 5717 | if (is_dir) { |
| 5718 | try fmtPathDir(fmt, full_path, check_mode, iterable_dir.dir, entry.name); | |
| 5718 | try fmtPathDir(fmt, full_path, check_mode, dir, entry.name); | |
| 5719 | 5719 | } else { |
| 5720 | fmtPathFile(fmt, full_path, check_mode, iterable_dir.dir, entry.name) catch |err| { | |
| 5720 | fmtPathFile(fmt, full_path, check_mode, dir, entry.name) catch |err| { | |
| 5721 | 5721 | warn("unable to format '{s}': {s}", .{ full_path, @errorName(err) }); |
| 5722 | 5722 | fmt.any_error = true; |
| 5723 | 5723 | return; |
src/windows_sdk.zig+11-3| ... | ... | @@ -14,7 +14,11 @@ const product_version_max_length = version_major_minor_max_length + ".65535".len |
| 14 | 14 | /// Iterates via `iterator` and collects all folders with names starting with `optional_prefix` |
| 15 | 15 | /// and similar to SemVer. Returns slice of folder names sorted in descending order. |
| 16 | 16 | /// Caller owns result. |
| 17 | fn iterateAndFilterBySemVer(iterator: *std.fs.IterableDir.Iterator, allocator: std.mem.Allocator, comptime optional_prefix: ?[]const u8) error{ OutOfMemory, VersionNotFound }![][]const u8 { | |
| 17 | fn iterateAndFilterBySemVer( | |
| 18 | iterator: *std.fs.Dir.Iterator, | |
| 19 | allocator: std.mem.Allocator, | |
| 20 | comptime optional_prefix: ?[]const u8, | |
| 21 | ) error{ OutOfMemory, VersionNotFound }![][]const u8 { | |
| 18 | 22 | var dirs_filtered_list = std.ArrayList([]const u8).init(allocator); |
| 19 | 23 | errdefer { |
| 20 | 24 | for (dirs_filtered_list.items) |filtered_dir| allocator.free(filtered_dir); |
| ... | ... | @@ -476,7 +480,9 @@ pub const Windows81Sdk = struct { |
| 476 | 480 | if (!std.fs.path.isAbsolute(sdk_lib_dir_path)) return error.Windows81SdkNotFound; |
| 477 | 481 | |
| 478 | 482 | // enumerate files in sdk path looking for latest version |
| 479 | var sdk_lib_dir = std.fs.openIterableDirAbsolute(sdk_lib_dir_path, .{}) catch |err| switch (err) { | |
| 483 | var sdk_lib_dir = std.fs.openDirAbsolute(sdk_lib_dir_path, .{ | |
| 484 | .iterate = true, | |
| 485 | }) catch |err| switch (err) { | |
| 480 | 486 | error.NameTooLong => return error.PathTooLong, |
| 481 | 487 | else => return error.Windows81SdkNotFound, |
| 482 | 488 | }; |
| ... | ... | @@ -727,7 +733,9 @@ const MsvcLibDir = struct { |
| 727 | 733 | if (!std.fs.path.isAbsolute(visualstudio_folder_path)) return error.PathNotFound; |
| 728 | 734 | // enumerate folders that contain `privateregistry.bin`, looking for all versions |
| 729 | 735 | // f.i. %localappdata%\Microsoft\VisualStudio\17.0_9e9cbb98\ |
| 730 | var visualstudio_folder = std.fs.openIterableDirAbsolute(visualstudio_folder_path, .{}) catch return error.PathNotFound; | |
| 736 | var visualstudio_folder = std.fs.openDirAbsolute(visualstudio_folder_path, .{ | |
| 737 | .iterate = true, | |
| 738 | }) catch return error.PathNotFound; | |
| 731 | 739 | defer visualstudio_folder.close(); |
| 732 | 740 | |
| 733 | 741 | var iterator = visualstudio_folder.iterate(); |
test/src/Cases.zig+5-5| ... | ... | @@ -368,7 +368,7 @@ pub fn addCompile( |
| 368 | 368 | /// Each file should include a test manifest as a contiguous block of comments at |
| 369 | 369 | /// the end of the file. The first line should be the test type, followed by a set of |
| 370 | 370 | /// key-value config values, followed by a blank line, then the expected output. |
| 371 | pub fn addFromDir(ctx: *Cases, dir: std.fs.IterableDir) void { | |
| 371 | pub fn addFromDir(ctx: *Cases, dir: std.fs.Dir) void { | |
| 372 | 372 | var current_file: []const u8 = "none"; |
| 373 | 373 | ctx.addFromDirInner(dir, &current_file) catch |err| { |
| 374 | 374 | std.debug.panicExtra( |
| ... | ... | @@ -382,7 +382,7 @@ pub fn addFromDir(ctx: *Cases, dir: std.fs.IterableDir) void { |
| 382 | 382 | |
| 383 | 383 | fn addFromDirInner( |
| 384 | 384 | ctx: *Cases, |
| 385 | iterable_dir: std.fs.IterableDir, | |
| 385 | iterable_dir: std.fs.Dir, | |
| 386 | 386 | /// This is kept up to date with the currently being processed file so |
| 387 | 387 | /// that if any errors occur the caller knows it happened during this file. |
| 388 | 388 | current_file: *[]const u8, |
| ... | ... | @@ -416,7 +416,7 @@ fn addFromDirInner( |
| 416 | 416 | } |
| 417 | 417 | |
| 418 | 418 | const max_file_size = 10 * 1024 * 1024; |
| 419 | const src = try iterable_dir.dir.readFileAllocOptions(ctx.arena, filename, max_file_size, null, 1, 0); | |
| 419 | const src = try iterable_dir.readFileAllocOptions(ctx.arena, filename, max_file_size, null, 1, 0); | |
| 420 | 420 | |
| 421 | 421 | // Parse the manifest |
| 422 | 422 | var manifest = try TestManifest.parse(ctx.arena, src); |
| ... | ... | @@ -1246,7 +1246,7 @@ pub fn main() !void { |
| 1246 | 1246 | var filenames = std.ArrayList([]const u8).init(arena); |
| 1247 | 1247 | |
| 1248 | 1248 | const case_dirname = std.fs.path.dirname(case_file_path).?; |
| 1249 | var iterable_dir = try std.fs.cwd().openIterableDir(case_dirname, .{}); | |
| 1249 | var iterable_dir = try std.fs.cwd().openDir(case_dirname, .{ .iterate = true }); | |
| 1250 | 1250 | defer iterable_dir.close(); |
| 1251 | 1251 | |
| 1252 | 1252 | if (std.mem.endsWith(u8, case_file_path, ".0.zig")) { |
| ... | ... | @@ -1280,7 +1280,7 @@ pub fn main() !void { |
| 1280 | 1280 | |
| 1281 | 1281 | for (batch) |filename| { |
| 1282 | 1282 | const max_file_size = 10 * 1024 * 1024; |
| 1283 | const src = try iterable_dir.dir.readFileAllocOptions(arena, filename, max_file_size, null, 1, 0); | |
| 1283 | const src = try iterable_dir.readFileAllocOptions(arena, filename, max_file_size, null, 1, 0); | |
| 1284 | 1284 | |
| 1285 | 1285 | // Parse the manifest |
| 1286 | 1286 | var manifest = try TestManifest.parse(arena, src); |
test/tests.zig+1-1| ... | ... | @@ -1288,7 +1288,7 @@ pub fn addCases( |
| 1288 | 1288 | |
| 1289 | 1289 | var cases = @import("src/Cases.zig").init(gpa, arena); |
| 1290 | 1290 | |
| 1291 | var dir = try b.build_root.handle.openIterableDir("test/cases", .{}); | |
| 1291 | var dir = try b.build_root.handle.openDir("test/cases", .{ .iterate = true }); | |
| 1292 | 1292 | defer dir.close(); |
| 1293 | 1293 | |
| 1294 | 1294 | cases.addFromDir(dir); |
tools/generate_JSONTestSuite.zig+1-1| ... | ... | @@ -18,7 +18,7 @@ pub fn main() !void { |
| 18 | 18 | ); |
| 19 | 19 | |
| 20 | 20 | var names = std.ArrayList([]const u8).init(allocator); |
| 21 | var cwd = try std.fs.cwd().openIterableDir(".", .{}); | |
| 21 | var cwd = try std.fs.cwd().openDir(".", .{ .iterate = true }); | |
| 22 | 22 | var it = cwd.iterate(); |
| 23 | 23 | while (try it.next()) |entry| { |
| 24 | 24 | try names.append(try allocator.dupe(u8, entry.name)); |
tools/process_headers.zig+3-3| ... | ... | @@ -382,14 +382,14 @@ pub fn main() !void { |
| 382 | 382 | try dir_stack.append(target_include_dir); |
| 383 | 383 | |
| 384 | 384 | while (dir_stack.popOrNull()) |full_dir_name| { |
| 385 | var iterable_dir = std.fs.cwd().openIterableDir(full_dir_name, .{}) catch |err| switch (err) { | |
| 385 | var dir = std.fs.cwd().openDir(full_dir_name, .{ .iterate = true }) catch |err| switch (err) { | |
| 386 | 386 | error.FileNotFound => continue :search, |
| 387 | 387 | error.AccessDenied => continue :search, |
| 388 | 388 | else => return err, |
| 389 | 389 | }; |
| 390 | defer iterable_dir.close(); | |
| 390 | defer dir.close(); | |
| 391 | 391 | |
| 392 | var dir_it = iterable_dir.iterate(); | |
| 392 | var dir_it = dir.iterate(); | |
| 393 | 393 | |
| 394 | 394 | while (try dir_it.next()) |entry| { |
| 395 | 395 | const full_path = try std.fs.path.join(allocator, &[_][]const u8{ full_dir_name, entry.name }); |
tools/update-license-headers.zig+4-4| ... | ... | @@ -14,9 +14,9 @@ pub fn main() !void { |
| 14 | 14 | |
| 15 | 15 | const args = try std.process.argsAlloc(arena); |
| 16 | 16 | const path_to_walk = args[1]; |
| 17 | const iterable_dir = try std.fs.cwd().openIterableDir(path_to_walk, .{}); | |
| 17 | const dir = try std.fs.cwd().openDir(path_to_walk, .{ .iterate = true }); | |
| 18 | 18 | |
| 19 | var walker = try iterable_dir.walk(arena); | |
| 19 | var walker = try dir.walk(arena); | |
| 20 | 20 | defer walker.deinit(); |
| 21 | 21 | |
| 22 | 22 | var buffer: [500]u8 = undefined; |
| ... | ... | @@ -30,7 +30,7 @@ pub fn main() !void { |
| 30 | 30 | node.activate(); |
| 31 | 31 | defer node.end(); |
| 32 | 32 | |
| 33 | const source = try iterable_dir.dir.readFileAlloc(arena, entry.path, 20 * 1024 * 1024); | |
| 33 | const source = try dir.readFileAlloc(arena, entry.path, 20 * 1024 * 1024); | |
| 34 | 34 | if (!std.mem.startsWith(u8, source, expected_header)) { |
| 35 | 35 | std.debug.print("no match: {s}\n", .{entry.path}); |
| 36 | 36 | continue; |
| ... | ... | @@ -42,6 +42,6 @@ pub fn main() !void { |
| 42 | 42 | std.mem.copy(u8, new_source, new_header); |
| 43 | 43 | std.mem.copy(u8, new_source[new_header.len..], truncated_source); |
| 44 | 44 | |
| 45 | try iterable_dir.dir.writeFile(entry.path, new_source); | |
| 45 | try dir.writeFile(entry.path, new_source); | |
| 46 | 46 | } |
| 47 | 47 | } |
tools/update-linux-headers.zig+3-3| ... | ... | @@ -190,14 +190,14 @@ pub fn main() !void { |
| 190 | 190 | try dir_stack.append(target_include_dir); |
| 191 | 191 | |
| 192 | 192 | while (dir_stack.popOrNull()) |full_dir_name| { |
| 193 | var iterable_dir = std.fs.cwd().openIterableDir(full_dir_name, .{}) catch |err| switch (err) { | |
| 193 | var dir = std.fs.cwd().openDir(full_dir_name, .{ .iterate = true }) catch |err| switch (err) { | |
| 194 | 194 | error.FileNotFound => continue :search, |
| 195 | 195 | error.AccessDenied => continue :search, |
| 196 | 196 | else => return err, |
| 197 | 197 | }; |
| 198 | defer iterable_dir.close(); | |
| 198 | defer dir.close(); | |
| 199 | 199 | |
| 200 | var dir_it = iterable_dir.iterate(); | |
| 200 | var dir_it = dir.iterate(); | |
| 201 | 201 | |
| 202 | 202 | while (try dir_it.next()) |entry| { |
| 203 | 203 | const full_path = try std.fs.path.join(arena, &[_][]const u8{ full_dir_name, entry.name }); |
tools/update_glibc.zig+5-5| ... | ... | @@ -47,7 +47,7 @@ pub fn main() !void { |
| 47 | 47 | |
| 48 | 48 | const dest_dir_path = try std.fmt.allocPrint(arena, "{s}/lib/libc/glibc", .{zig_src_path}); |
| 49 | 49 | |
| 50 | var dest_dir = fs.cwd().openIterableDir(dest_dir_path, .{}) catch |err| { | |
| 50 | var dest_dir = fs.cwd().openDir(dest_dir_path, .{ .iterate = true }) catch |err| { | |
| 51 | 51 | fatal("unable to open destination directory '{s}': {s}", .{ |
| 52 | 52 | dest_dir_path, @errorName(err), |
| 53 | 53 | }); |
| ... | ... | @@ -72,14 +72,14 @@ pub fn main() !void { |
| 72 | 72 | if (mem.endsWith(u8, entry.path, ext)) continue :walk; |
| 73 | 73 | } |
| 74 | 74 | |
| 75 | glibc_src_dir.copyFile(entry.path, dest_dir.dir, entry.path, .{}) catch |err| { | |
| 75 | glibc_src_dir.copyFile(entry.path, dest_dir, entry.path, .{}) catch |err| { | |
| 76 | 76 | log.warn("unable to copy '{s}/{s}' to '{s}/{s}': {s}", .{ |
| 77 | 77 | glibc_src_path, entry.path, |
| 78 | 78 | dest_dir_path, entry.path, |
| 79 | 79 | @errorName(err), |
| 80 | 80 | }); |
| 81 | 81 | if (err == error.FileNotFound) { |
| 82 | try dest_dir.dir.deleteFile(entry.path); | |
| 82 | try dest_dir.deleteFile(entry.path); | |
| 83 | 83 | } |
| 84 | 84 | }; |
| 85 | 85 | } |
| ... | ... | @@ -88,7 +88,7 @@ pub fn main() !void { |
| 88 | 88 | // Warn about duplicated files inside glibc/include/* that can be omitted |
| 89 | 89 | // because they are already in generic-glibc/*. |
| 90 | 90 | |
| 91 | var include_dir = dest_dir.dir.openIterableDir("include", .{}) catch |err| { | |
| 91 | var include_dir = dest_dir.openDir("include", .{ .iterate = true }) catch |err| { | |
| 92 | 92 | fatal("unable to open directory '{s}/include': {s}", .{ |
| 93 | 93 | dest_dir_path, @errorName(err), |
| 94 | 94 | }); |
| ... | ... | @@ -125,7 +125,7 @@ pub fn main() !void { |
| 125 | 125 | generic_glibc_path, entry.path, @errorName(e), |
| 126 | 126 | }), |
| 127 | 127 | }; |
| 128 | const glibc_include_contents = include_dir.dir.readFileAlloc( | |
| 128 | const glibc_include_contents = include_dir.readFileAlloc( | |
| 129 | 129 | arena, |
| 130 | 130 | entry.path, |
| 131 | 131 | max_file_size, |
tools/update_spirv_features.zig+3-3| ... | ... | @@ -226,7 +226,7 @@ pub fn main() !void { |
| 226 | 226 | /// TODO: Unfortunately, neither repository contains a machine-readable list of extension dependencies. |
| 227 | 227 | fn gather_extensions(allocator: Allocator, spirv_registry_root: []const u8) ![]const []const u8 { |
| 228 | 228 | const extensions_path = try fs.path.join(allocator, &.{ spirv_registry_root, "extensions" }); |
| 229 | var extensions_dir = try fs.cwd().openIterableDir(extensions_path, .{}); | |
| 229 | var extensions_dir = try fs.cwd().openDir(extensions_path, .{ .iterate = true }); | |
| 230 | 230 | defer extensions_dir.close(); |
| 231 | 231 | |
| 232 | 232 | var extensions = std.ArrayList([]const u8).init(allocator); |
| ... | ... | @@ -235,7 +235,7 @@ fn gather_extensions(allocator: Allocator, spirv_registry_root: []const u8) ![]c |
| 235 | 235 | while (try vendor_it.next()) |vendor_entry| { |
| 236 | 236 | std.debug.assert(vendor_entry.kind == .directory); // If this fails, the structure of SPIRV-Registry has changed. |
| 237 | 237 | |
| 238 | const vendor_dir = try extensions_dir.dir.openIterableDir(vendor_entry.name, .{}); | |
| 238 | const vendor_dir = try extensions_dir.openDir(vendor_entry.name, .{ .iterate = true }); | |
| 239 | 239 | var ext_it = vendor_dir.iterate(); |
| 240 | 240 | while (try ext_it.next()) |ext_entry| { |
| 241 | 241 | // There is both a HTML and asciidoc version of every spec (as well as some other directories), |
| ... | ... | @@ -258,7 +258,7 @@ fn gather_extensions(allocator: Allocator, spirv_registry_root: []const u8) ![]c |
| 258 | 258 | // SPV_EXT_name |
| 259 | 259 | // ``` |
| 260 | 260 | |
| 261 | const ext_spec = try vendor_dir.dir.readFileAlloc(allocator, ext_entry.name, std.math.maxInt(usize)); | |
| 261 | const ext_spec = try vendor_dir.readFileAlloc(allocator, ext_entry.name, std.math.maxInt(usize)); | |
| 262 | 262 | const name_strings = "Name Strings"; |
| 263 | 263 | |
| 264 | 264 | const name_strings_offset = std.mem.indexOf(u8, ext_spec, name_strings) orelse return error.InvalidRegistry; |