From 519ba9bb654a4d5caf7440160f018b7a3ae1e95a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Nov 2023 12:35:33 -0700 Subject: [PATCH 1/5] Revert "Merge pull request #12060 from Vexu/IterableDir" This reverts commit da94227f783ec3c92859c4713b80a668f1183f96, reversing changes made to 8f943b3d33432a26b7e242c1181e4220ed400501. I was against this change originally, but decided to approve it to keep an open mind. After a year of trying it in practice, I firmly believe that the previous way of doing it was better. --- lib/std/fs.zig | 234 ++++++++++++------------------- lib/std/fs/test.zig | 68 +++++---- lib/std/os.zig | 2 +- lib/std/testing.zig | 38 ----- src/main.zig | 12 +- tools/process_headers.zig | 6 +- tools/update-license-headers.zig | 8 +- tools/update-linux-headers.zig | 6 +- tools/update_glibc.zig | 10 +- tools/update_spirv_features.zig | 6 +- 10 files changed, 146 insertions(+), 244 deletions(-) diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 2bcf4ee309..12e1ac34ab 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -310,10 +310,8 @@ pub fn renameW(old_dir: Dir, old_sub_path_w: []const u16, new_dir: Dir, new_sub_ return os.renameatW(old_dir.fd, old_sub_path_w, new_dir.fd, new_sub_path_w); } -/// A directory that can be iterated. It is *NOT* legal to initialize this with a regular `Dir` -/// that has been opened without iteration permission. -pub const IterableDir = struct { - dir: Dir, +pub const Dir = struct { + fd: os.fd_t, pub const Entry = struct { name: []const u8, @@ -879,18 +877,18 @@ pub const IterableDir = struct { else => @compileError("unimplemented"), }; - pub fn iterate(self: IterableDir) Iterator { + pub fn iterate(self: Dir) Iterator { return self.iterateImpl(true); } /// Like `iterate`, but will not reset the directory cursor before the first /// iteration. This should only be used in cases where it is known that the - /// `IterableDir` has not had its cursor modified yet (e.g. it was just opened). - pub fn iterateAssumeFirstIteration(self: IterableDir) Iterator { + /// `Dir` has not had its cursor modified yet (e.g. it was just opened). + pub fn iterateAssumeFirstIteration(self: Dir) Iterator { return self.iterateImpl(false); } - fn iterateImpl(self: IterableDir, first_iter_start_value: bool) Iterator { + fn iterateImpl(self: Dir, first_iter_start_value: bool) Iterator { switch (builtin.os.tag) { .macos, .ios, @@ -901,7 +899,7 @@ pub const IterableDir = struct { .solaris, .illumos, => return Iterator{ - .dir = self.dir, + .dir = self, .seek = 0, .index = 0, .end_index = 0, @@ -909,14 +907,14 @@ pub const IterableDir = struct { .first_iter = first_iter_start_value, }, .linux, .haiku => return Iterator{ - .dir = self.dir, + .dir = self, .index = 0, .end_index = 0, .buf = undefined, .first_iter = first_iter_start_value, }, .windows => return Iterator{ - .dir = self.dir, + .dir = self, .index = 0, .end_index = 0, .first_iter = first_iter_start_value, @@ -924,7 +922,7 @@ pub const IterableDir = struct { .name_data = undefined, }, .wasi => return Iterator{ - .dir = self.dir, + .dir = self, .cookie = os.wasi.DIRCOOKIE_START, .index = 0, .end_index = 0, @@ -945,11 +943,11 @@ pub const IterableDir = struct { dir: Dir, basename: []const u8, path: []const u8, - kind: IterableDir.Entry.Kind, + kind: Dir.Entry.Kind, }; const StackItem = struct { - iter: IterableDir.Iterator, + iter: Dir.Iterator, dirname_len: usize, }; @@ -980,7 +978,7 @@ pub const IterableDir = struct { } try self.name_buffer.appendSlice(base.name); if (base.kind == .directory) { - var new_dir = top.iter.dir.openIterableDir(base.name, .{}) catch |err| switch (err) { + var new_dir = top.iter.dir.openDir(base.name, .{ .iterate = true }) catch |err| switch (err) { error.NameTooLong => unreachable, // no path sep in base.name else => |e| return e, }; @@ -1023,10 +1021,11 @@ pub const IterableDir = struct { }; /// Recursively iterates over a directory. + /// `self` must have been opened with `OpenDirOptions{.iterate = true}`. /// Must call `Walker.deinit` when done. /// The order of returned file system entries is undefined. /// `self` will not be closed after walking it. - pub fn walk(self: IterableDir, allocator: Allocator) !Walker { + pub fn walk(self: Dir, allocator: Allocator) !Walker { var name_buffer = std.ArrayList(u8).init(allocator); errdefer name_buffer.deinit(); @@ -1044,49 +1043,6 @@ pub const IterableDir = struct { }; } - pub fn close(self: *IterableDir) void { - self.dir.close(); - self.* = undefined; - } - - pub const ChmodError = File.ChmodError; - - /// Changes the mode of the directory. - /// The process must have the correct privileges in order to do this - /// successfully, or must have the effective user ID matching the owner - /// of the directory. - pub fn chmod(self: IterableDir, new_mode: File.Mode) ChmodError!void { - const file: File = .{ - .handle = self.dir.fd, - .capable_io_mode = .blocking, - }; - try file.chmod(new_mode); - } - - /// Changes the owner and group of the directory. - /// The process must have the correct privileges in order to do this - /// successfully. The group may be changed by the owner of the directory to - /// any group of which the owner is a member. If the - /// owner or group is specified as `null`, the ID is not changed. - pub fn chown(self: IterableDir, owner: ?File.Uid, group: ?File.Gid) ChownError!void { - const file: File = .{ - .handle = self.dir.fd, - .capable_io_mode = .blocking, - }; - try file.chown(owner, group); - } - - pub const ChownError = File.ChownError; -}; - -pub const Dir = struct { - fd: os.fd_t, - - pub const iterate = @compileError("only 'IterableDir' can be iterated; 'IterableDir' can be obtained with 'openIterableDir'"); - pub const walk = @compileError("only 'IterableDir' can be walked; 'IterableDir' can be obtained with 'openIterableDir'"); - pub const chmod = @compileError("only 'IterableDir' can have its mode changed; 'IterableDir' can be obtained with 'openIterableDir'"); - pub const chown = @compileError("only 'IterableDir' can have its owner changed; 'IterableDir' can be obtained with 'openIterableDir'"); - pub const OpenError = error{ FileNotFound, NotDir, @@ -1529,7 +1485,8 @@ pub const Dir = struct { .windows => { const w = os.windows; const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | - w.SYNCHRONIZE | w.FILE_TRAVERSE; + w.SYNCHRONIZE | w.FILE_TRAVERSE | + (if (open_dir_options.iterate) w.FILE_LIST_DIRECTORY else 0); return self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow); }, @@ -1545,32 +1502,6 @@ pub const Dir = struct { }; } - /// This function performs `makePath`, followed by `openIterableDir`. - /// If supported by the OS, this operation is atomic. It is not atomic on - /// all operating systems. - pub fn makeOpenPathIterable(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !IterableDir { - return switch (builtin.os.tag) { - .windows => { - const w = os.windows; - const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | - w.SYNCHRONIZE | w.FILE_TRAVERSE | w.FILE_LIST_DIRECTORY; - - return IterableDir{ - .dir = try self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow), - }; - }, - else => { - return self.openIterableDir(sub_path, open_dir_options) catch |err| switch (err) { - error.FileNotFound => { - try self.makePath(sub_path); - return self.openIterableDir(sub_path, open_dir_options); - }, - else => |e| return e, - }; - }, - }; - } - /// This function returns the canonicalized absolute pathname of /// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this /// `Dir` handle and returns the canonicalized absolute pathname of `pathname` @@ -1706,39 +1637,28 @@ pub const Dir = struct { /// such operations are Illegal Behavior. access_sub_paths: bool = true, + /// `true` means the opened directory can be scanned for the files and sub-directories + /// of the result. It means the `iterate` function can be called. + iterate: bool = false, + /// `true` means it won't dereference the symlinks. no_follow: bool = false, }; /// Opens a directory at the given path. The directory is a system resource that remains /// open until `close` is called on the result. + /// The directory cannot be iterated unless the `iterate` option is set to `true`. /// /// Asserts that the path parameter has no null bytes. pub fn openDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { if (builtin.os.tag == .windows) { const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.openDirW(sub_path_w.span().ptr, args, false); + return .{ .dir = try self.openDirW(sub_path_w.span().ptr, args) }; } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - return self.openDirWasi(sub_path, args); + return .{ .dir = try self.openDirWasi(sub_path, args) }; } else { const sub_path_c = try os.toPosixPath(sub_path); - return self.openDirZ(&sub_path_c, args, false); - } - } - - /// Opens an iterable directory at the given path. The directory is a system resource that remains - /// open until `close` is called on the result. - /// - /// Asserts that the path parameter has no null bytes. - pub fn openIterableDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!IterableDir { - if (builtin.os.tag == .windows) { - const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); - return IterableDir{ .dir = try self.openDirW(sub_path_w.span().ptr, args, true) }; - } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - return IterableDir{ .dir = try self.openDirWasi(sub_path, args) }; - } else { - const sub_path_c = try os.toPosixPath(sub_path); - return IterableDir{ .dir = try self.openDirZ(&sub_path_c, args, true) }; + return .{ .dir = try self.openDirZ(&sub_path_c, args) }; } } @@ -1790,13 +1710,13 @@ pub const Dir = struct { } /// Same as `openDir` except the parameter is null-terminated. - pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenDirOptions, iterable: bool) OpenError!Dir { + pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenDirOptions) OpenError!Dir { if (builtin.os.tag == .windows) { const sub_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); - return self.openDirW(sub_path_w.span().ptr, args, iterable); + return self.openDirW(sub_path_w.span().ptr, args); } const symlink_flags: u32 = if (args.no_follow) os.O.NOFOLLOW else 0x0; - if (!iterable) { + if (!args.iterate) { const O_PATH = if (@hasDecl(os.O, "PATH")) os.O.PATH else 0; return self.openDirFlagsZ(sub_path_c, os.O.DIRECTORY | os.O.RDONLY | os.O.CLOEXEC | O_PATH | symlink_flags); } else { @@ -1806,12 +1726,12 @@ pub const Dir = struct { /// Same as `openDir` except the path parameter is WTF-16 encoded, NT-prefixed. /// This function asserts the target OS is Windows. - pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenDirOptions, iterable: bool) OpenError!Dir { + pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenDirOptions) OpenError!Dir { const w = os.windows; // TODO remove some of these flags if args.access_sub_paths is false const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | w.SYNCHRONIZE | w.FILE_TRAVERSE; - const flags: u32 = if (iterable) base_flags | w.FILE_LIST_DIRECTORY else base_flags; + const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; const dir = try self.makeOpenDirAccessMaskW(sub_path_w, flags, .{ .no_follow = args.no_follow, .create_disposition = w.FILE_OPEN, @@ -2203,7 +2123,7 @@ pub const Dir = struct { const StackItem = struct { name: []const u8, parent_dir: Dir, - iter: IterableDir.Iterator, + iter: Dir.Iterator, fn closeAll(items: []@This()) void { for (items) |*item| item.iter.dir.close(); @@ -2227,7 +2147,10 @@ pub const Dir = struct { handle_entry: while (true) { if (treat_as_dir) { if (stack.unusedCapacitySlice().len >= 1) { - var iterable_dir = top.iter.dir.openIterableDir(entry.name, .{ .no_follow = true }) catch |err| switch (err) { + var iterable_dir = top.iter.dir.openDir(entry.name, .{ + .no_follow = true, + .iterate = true, + }) catch |err| switch (err) { error.NotDir => { treat_as_dir = false; continue :handle_entry; @@ -2318,7 +2241,10 @@ pub const Dir = struct { var treat_as_dir = true; handle_entry: while (true) { if (treat_as_dir) { - break :iterable_dir parent_dir.openIterableDir(name, .{ .no_follow = true }) catch |err| switch (err) { + break :iterable_dir parent_dir.openDir(name, .{ + .no_follow = true, + .iterate = true, + }) catch |err| switch (err) { error.NotDir => { treat_as_dir = false; continue :handle_entry; @@ -2393,12 +2319,12 @@ pub const Dir = struct { fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint: File.Kind) DeleteTreeError!void { start_over: while (true) { - var iterable_dir = (try self.deleteTreeOpenInitialSubpath(sub_path, kind_hint)) orelse return; - var cleanup_dir_parent: ?IterableDir = null; + var dir = (try self.deleteTreeOpenInitialSubpath(sub_path, kind_hint)) orelse return; + var cleanup_dir_parent: ?Dir = null; defer if (cleanup_dir_parent) |*d| d.close(); var cleanup_dir = true; - defer if (cleanup_dir) iterable_dir.close(); + defer if (cleanup_dir) dir.close(); // Valid use of MAX_PATH_BYTES because dir_name_buf will only // ever store a single path component that was returned from the @@ -2411,12 +2337,15 @@ pub const Dir = struct { // open it, and close the original directory. Repeat. Then start the entire operation over. scan_dir: while (true) { - var dir_it = iterable_dir.iterateAssumeFirstIteration(); + var dir_it = dir.iterateAssumeFirstIteration(); dir_it: while (try dir_it.next()) |entry| { var treat_as_dir = entry.kind == .directory; handle_entry: while (true) { if (treat_as_dir) { - const new_dir = iterable_dir.dir.openIterableDir(entry.name, .{ .no_follow = true }) catch |err| switch (err) { + const new_dir = dir.openDir(entry.name, .{ + .no_follow = true, + .iterate = true, + }) catch |err| switch (err) { error.NotDir => { treat_as_dir = false; continue :handle_entry; @@ -2442,14 +2371,14 @@ pub const Dir = struct { => |e| return e, }; if (cleanup_dir_parent) |*d| d.close(); - cleanup_dir_parent = iterable_dir; - iterable_dir = new_dir; + cleanup_dir_parent = dir; + dir = new_dir; const result = dir_name_buf[0..entry.name.len]; @memcpy(result, entry.name); dir_name = result; continue :scan_dir; } else { - if (iterable_dir.dir.deleteFile(entry.name)) { + if (dir.deleteFile(entry.name)) { continue :dir_it; } else |err| switch (err) { error.FileNotFound => continue :dir_it, @@ -2480,11 +2409,11 @@ pub const Dir = struct { } // Reached the end of the directory entries, which means we successfully deleted all of them. // Now to remove the directory itself. - iterable_dir.close(); + dir.close(); cleanup_dir = false; if (cleanup_dir_parent) |d| { - d.dir.deleteDir(dir_name) catch |err| switch (err) { + d.deleteDir(dir_name) catch |err| switch (err) { // These two things can happen due to file system race conditions. error.FileNotFound, error.DirNotEmpty => continue :start_over, else => |e| return e, @@ -2503,14 +2432,17 @@ pub const Dir = struct { } /// On successful delete, returns null. - fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File.Kind) !?IterableDir { + fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File.Kind) !?Dir { return iterable_dir: { // Treat as a file by default var treat_as_dir = kind_hint == .directory; handle_entry: while (true) { if (treat_as_dir) { - break :iterable_dir self.openIterableDir(sub_path, .{ .no_follow = true }) catch |err| switch (err) { + break :iterable_dir self.openDir(sub_path, .{ + .no_follow = true, + .iterate = true, + }) catch |err| switch (err) { error.NotDir => { treat_as_dir = false; continue :handle_entry; @@ -2764,6 +2696,37 @@ pub const Dir = struct { return Stat.fromSystem(st); } + pub const ChmodError = File.ChmodError; + + /// Changes the mode of the directory. + /// The process must have the correct privileges in order to do this + /// successfully, or must have the effective user ID matching the owner + /// of the directory. Additionally, the directory must have been opened + /// with `OpenDirOptions{ .iterate = true }`. + pub fn chmod(self: Dir, new_mode: File.Mode) ChmodError!void { + const file: File = .{ + .handle = self.fd, + .capable_io_mode = .blocking, + }; + try file.chmod(new_mode); + } + + /// Changes the owner and group of the directory. + /// The process must have the correct privileges in order to do this + /// successfully. The group may be changed by the owner of the directory to + /// any group of which the owner is a member. Additionally, the directory + /// must have been opened with `OpenDirOptions{ .iterate = true }`. If the + /// owner or group is specified as `null`, the ID is not changed. + pub fn chown(self: Dir, owner: ?File.Uid, group: ?File.Gid) ChownError!void { + const file: File = .{ + .handle = self.fd, + .capable_io_mode = .blocking, + }; + try file.chown(owner, group); + } + + pub const ChownError = File.ChownError; + const Permissions = File.Permissions; pub const SetPermissionsError = File.SetPermissionsError; @@ -2829,27 +2792,6 @@ pub fn openDirAbsoluteW(absolute_path_c: [*:0]const u16, flags: Dir.OpenDirOptio return cwd().openDirW(absolute_path_c, flags, false); } -/// Opens a directory at the given path. The directory is a system resource that remains -/// open until `close` is called on the result. -/// See `openIterableDirAbsoluteZ` for a function that accepts a null-terminated path. -/// -/// Asserts that the path parameter has no null bytes. -pub fn openIterableDirAbsolute(absolute_path: []const u8, flags: Dir.OpenDirOptions) File.OpenError!IterableDir { - assert(path.isAbsolute(absolute_path)); - return cwd().openIterableDir(absolute_path, flags); -} - -/// Same as `openIterableDirAbsolute` but the path parameter is null-terminated. -pub fn openIterableDirAbsoluteZ(absolute_path_c: [*:0]const u8, flags: Dir.OpenDirOptions) File.OpenError!IterableDir { - assert(path.isAbsoluteZ(absolute_path_c)); - return IterableDir{ .dir = try cwd().openDirZ(absolute_path_c, flags, true) }; -} -/// Same as `openIterableDirAbsolute` but the path parameter is null-terminated. -pub fn openIterableDirAbsoluteW(absolute_path_c: [*:0]const u16, flags: Dir.OpenDirOptions) File.OpenError!IterableDir { - assert(path.isAbsoluteWindowsW(absolute_path_c)); - return IterableDir{ .dir = try cwd().openDirW(absolute_path_c, flags, true) }; -} - /// Opens a file for reading or writing, without attempting to create a new file, based on an absolute path. /// Call `File.close` to release the resource. /// Asserts that the path is absolute. See `Dir.openFile` for a function that diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 6786aec4b0..fcf978d307 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -8,10 +8,8 @@ const wasi = std.os.wasi; const ArenaAllocator = std.heap.ArenaAllocator; const Dir = std.fs.Dir; -const IterableDir = std.fs.IterableDir; const File = std.fs.File; const tmpDir = testing.tmpDir; -const tmpIterableDir = testing.tmpIterableDir; const PathType = enum { relative, @@ -76,11 +74,11 @@ const TestContext = struct { arena: ArenaAllocator, tmp: testing.TmpIterableDir, dir: std.fs.Dir, - iterable_dir: std.fs.IterableDir, + iterable_dir: std.fs.Dir, transform_fn: *const PathType.TransformFn, pub fn init(path_type: PathType, allocator: mem.Allocator, transform_fn: *const PathType.TransformFn) TestContext { - const tmp = tmpIterableDir(.{}); + const tmp = tmpDir(.{ .iterate = true }); return .{ .path_type = path_type, .arena = ArenaAllocator.init(allocator), @@ -323,28 +321,28 @@ fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void } test "Dir.Iterator" { - var tmp_dir = tmpIterableDir(.{}); + var tmp_dir = tmpDir(.{ .iterate = true }); defer tmp_dir.cleanup(); // First, create a couple of entries to iterate over. - const file = try tmp_dir.iterable_dir.dir.createFile("some_file", .{}); + const file = try tmp_dir.dir.createFile("some_file", .{}); file.close(); - try tmp_dir.iterable_dir.dir.makeDir("some_dir"); + try tmp_dir.dir.makeDir("some_dir"); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); - var entries = std.ArrayList(IterableDir.Entry).init(allocator); + var entries = std.ArrayList(Dir.Entry).init(allocator); // Create iterator. - var iter = tmp_dir.iterable_dir.iterate(); + var iter = tmp_dir.dir.iterate(); while (try iter.next()) |entry| { // We cannot just store `entry` as on Windows, we're re-using the name buffer // which means we'll actually share the `name` pointer between entries! const name = try allocator.dupe(u8, entry.name); - try entries.append(.{ .name = name, .kind = entry.kind }); + try entries.append(Dir.Entry{ .name = name, .kind = entry.kind }); } try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..' @@ -353,7 +351,7 @@ test "Dir.Iterator" { } test "Dir.Iterator many entries" { - var tmp_dir = tmpIterableDir(.{}); + var tmp_dir = tmpDir(.{ .iterate = true }); defer tmp_dir.cleanup(); const num = 1024; @@ -369,7 +367,7 @@ test "Dir.Iterator many entries" { defer arena.deinit(); const allocator = arena.allocator(); - var entries = std.ArrayList(IterableDir.Entry).init(allocator); + var entries = std.ArrayList(Dir.Entry).init(allocator); // Create iterator. var iter = tmp_dir.iterable_dir.iterate(); @@ -388,14 +386,14 @@ test "Dir.Iterator many entries" { } test "Dir.Iterator twice" { - var tmp_dir = tmpIterableDir(.{}); + var tmp_dir = tmpDir(.{ .iterate = true }); defer tmp_dir.cleanup(); // First, create a couple of entries to iterate over. - const file = try tmp_dir.iterable_dir.dir.createFile("some_file", .{}); + const file = try tmp_dir.dir.createFile("some_file", .{}); file.close(); - try tmp_dir.iterable_dir.dir.makeDir("some_dir"); + try tmp_dir.dir.makeDir("some_dir"); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); @@ -403,15 +401,15 @@ test "Dir.Iterator twice" { var i: u8 = 0; while (i < 2) : (i += 1) { - var entries = std.ArrayList(IterableDir.Entry).init(allocator); + var entries = std.ArrayList(Dir.Entry).init(allocator); // Create iterator. - var iter = tmp_dir.iterable_dir.iterate(); + var iter = tmp_dir.dir.iterate(); while (try iter.next()) |entry| { // We cannot just store `entry` as on Windows, we're re-using the name buffer // which means we'll actually share the `name` pointer between entries! const name = try allocator.dupe(u8, entry.name); - try entries.append(.{ .name = name, .kind = entry.kind }); + try entries.append(Dir.Entry{ .name = name, .kind = entry.kind }); } try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..' @@ -421,7 +419,7 @@ test "Dir.Iterator twice" { } test "Dir.Iterator reset" { - var tmp_dir = tmpIterableDir(.{}); + var tmp_dir = tmpDir(.{ .iterate = true }); defer tmp_dir.cleanup(); // First, create a couple of entries to iterate over. @@ -439,7 +437,7 @@ test "Dir.Iterator reset" { var i: u8 = 0; while (i < 2) : (i += 1) { - var entries = std.ArrayList(IterableDir.Entry).init(allocator); + var entries = std.ArrayList(Dir.Entry).init(allocator); while (try iter.next()) |entry| { // We cannot just store `entry` as on Windows, we're re-using the name buffer @@ -485,11 +483,11 @@ test "Dir.Iterator but dir is deleted during iteration" { } } -fn entryEql(lhs: IterableDir.Entry, rhs: IterableDir.Entry) bool { +fn entryEql(lhs: Dir.Entry, rhs: Dir.Entry) bool { return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind; } -fn contains(entries: *const std.ArrayList(IterableDir.Entry), el: IterableDir.Entry) bool { +fn contains(entries: *const std.ArrayList(Dir.Entry), el: Dir.Entry) bool { for (entries.items) |entry| { if (entryEql(entry, el)) return true; } @@ -963,7 +961,7 @@ test "makePath in a directory that no longer exists" { try testing.expectError(error.FileNotFound, tmp.dir.makePath("sub-path")); } -fn testFilenameLimits(iterable_dir: IterableDir, maxed_filename: []const u8) !void { +fn testFilenameLimits(iterable_dir: Dir, maxed_filename: []const u8) !void { // setup, create a dir and a nested file both with maxed filenames, and walk the dir { var maxed_dir = try iterable_dir.dir.makeOpenPath(maxed_filename, .{}); @@ -987,7 +985,7 @@ fn testFilenameLimits(iterable_dir: IterableDir, maxed_filename: []const u8) !vo } test "max file name component lengths" { - var tmp = tmpIterableDir(.{}); + var tmp = tmpDir(.{ .iterate = true }); defer tmp.cleanup(); if (builtin.os.tag == .windows) { @@ -1384,7 +1382,7 @@ test "open file with exclusive nonblocking lock twice (absolute paths)" { test "walker" { if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; - var tmp = tmpIterableDir(.{}); + var tmp = tmpDir(.{ .iterate = true }); defer tmp.cleanup(); // iteration order of walker is undefined, so need lookup maps to check against @@ -1410,10 +1408,10 @@ test "walker" { }); for (expected_paths.kvs) |kv| { - try tmp.iterable_dir.dir.makePath(kv.key); + try tmp.dir.makePath(kv.key); } - var walker = try tmp.iterable_dir.walk(testing.allocator); + var walker = try tmp.dir.walk(testing.allocator); defer walker.deinit(); var num_walked: usize = 0; @@ -1437,7 +1435,7 @@ test "walker" { test "walker without fully iterating" { if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; - var tmp = tmpIterableDir(.{}); + var tmp = tmpDir(.{ .iterate = true }); defer tmp.cleanup(); var walker = try tmp.iterable_dir.walk(testing.allocator); @@ -1556,11 +1554,11 @@ test "chmod" { try testing.expectEqual(@as(File.Mode, 0o644), (try file.stat()).mode & 0o7777); try tmp.dir.makeDir("test_dir"); - var iterable_dir = try tmp.dir.openIterableDir("test_dir", .{}); - defer iterable_dir.close(); + var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true }); + defer dir.close(); - try iterable_dir.chmod(0o700); - try testing.expectEqual(@as(File.Mode, 0o700), (try iterable_dir.dir.stat()).mode & 0o7777); + try dir.chmod(0o700); + try testing.expectEqual(@as(File.Mode, 0o700), (try dir.stat()).mode & 0o7777); } test "chown" { @@ -1576,9 +1574,9 @@ test "chown" { try tmp.dir.makeDir("test_dir"); - var iterable_dir = try tmp.dir.openIterableDir("test_dir", .{}); - defer iterable_dir.close(); - try iterable_dir.chown(null, null); + var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true }); + defer dir.close(); + try dir.chown(null, null); } test "File.Metadata" { diff --git a/lib/std/os.zig b/lib/std/os.zig index cf388f76e9..10697b970f 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -402,7 +402,7 @@ pub fn fchown(fd: fd_t, owner: ?uid_t, group: ?gid_t) FChownError!void { switch (system.getErrno(res)) { .SUCCESS => return, .INTR => continue, - .BADF => unreachable, // Can be reached if the fd refers to a non-iterable directory. + .BADF => unreachable, // Can be reached if the fd refers to a directory opened without `OpenDirOptions{ .iterate = true }` .FAULT => unreachable, .INVAL => unreachable, diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 09507a2392..a26b6ff321 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -543,22 +543,6 @@ pub const TmpDir = struct { } }; -pub const TmpIterableDir = struct { - iterable_dir: std.fs.IterableDir, - parent_dir: std.fs.Dir, - sub_path: [sub_path_len]u8, - - const random_bytes_count = 12; - const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count); - - pub fn cleanup(self: *TmpIterableDir) void { - self.iterable_dir.close(); - self.parent_dir.deleteTree(&self.sub_path) catch {}; - self.parent_dir.close(); - self.* = undefined; - } -}; - pub fn tmpDir(opts: std.fs.Dir.OpenDirOptions) TmpDir { var random_bytes: [TmpDir.random_bytes_count]u8 = undefined; std.crypto.random.bytes(&random_bytes); @@ -581,28 +565,6 @@ pub fn tmpDir(opts: std.fs.Dir.OpenDirOptions) TmpDir { }; } -pub fn tmpIterableDir(opts: std.fs.Dir.OpenDirOptions) TmpIterableDir { - var random_bytes: [TmpIterableDir.random_bytes_count]u8 = undefined; - std.crypto.random.bytes(&random_bytes); - var sub_path: [TmpIterableDir.sub_path_len]u8 = undefined; - _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); - - const cwd = std.fs.cwd(); - var cache_dir = cwd.makeOpenPath("zig-cache", .{}) catch - @panic("unable to make tmp dir for testing: unable to make and open zig-cache dir"); - defer cache_dir.close(); - const parent_dir = cache_dir.makeOpenPath("tmp", .{}) catch - @panic("unable to make tmp dir for testing: unable to make and open zig-cache/tmp dir"); - const dir = parent_dir.makeOpenPathIterable(&sub_path, opts) catch - @panic("unable to make tmp dir for testing: unable to make and open the tmp dir"); - - return .{ - .iterable_dir = dir, - .parent_dir = parent_dir, - .sub_path = sub_path, - }; -} - test "expectEqual nested array" { const a = [2][2]f32{ [_]f32{ 1.0, 0.0 }, diff --git a/src/main.zig b/src/main.zig index 8a01ef4cdb..641dd04164 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5698,13 +5698,13 @@ fn fmtPathDir( parent_dir: fs.Dir, parent_sub_path: []const u8, ) FmtError!void { - var iterable_dir = try parent_dir.openIterableDir(parent_sub_path, .{}); - defer iterable_dir.close(); + var dir = try parent_dir.openDir(parent_sub_path, .{ .iterate = true }); + defer dir.close(); - const stat = try iterable_dir.dir.stat(); + const stat = try dir.stat(); if (try fmt.seen.fetchPut(stat.inode, {})) |_| return; - var dir_it = iterable_dir.iterate(); + var dir_it = dir.iterate(); while (try dir_it.next()) |entry| { const is_dir = entry.kind == .directory; @@ -5715,9 +5715,9 @@ fn fmtPathDir( defer fmt.gpa.free(full_path); if (is_dir) { - try fmtPathDir(fmt, full_path, check_mode, iterable_dir.dir, entry.name); + try fmtPathDir(fmt, full_path, check_mode, dir, entry.name); } else { - fmtPathFile(fmt, full_path, check_mode, iterable_dir.dir, entry.name) catch |err| { + fmtPathFile(fmt, full_path, check_mode, dir, entry.name) catch |err| { warn("unable to format '{s}': {s}", .{ full_path, @errorName(err) }); fmt.any_error = true; return; diff --git a/tools/process_headers.zig b/tools/process_headers.zig index ea08d4de40..576e894f98 100644 --- a/tools/process_headers.zig +++ b/tools/process_headers.zig @@ -382,14 +382,14 @@ pub fn main() !void { try dir_stack.append(target_include_dir); while (dir_stack.popOrNull()) |full_dir_name| { - var iterable_dir = std.fs.cwd().openIterableDir(full_dir_name, .{}) catch |err| switch (err) { + var dir = std.fs.cwd().openDir(full_dir_name, .{ .iterate = true }) catch |err| switch (err) { error.FileNotFound => continue :search, error.AccessDenied => continue :search, else => return err, }; - defer iterable_dir.close(); + defer dir.close(); - var dir_it = iterable_dir.iterate(); + var dir_it = dir.iterate(); while (try dir_it.next()) |entry| { const full_path = try std.fs.path.join(allocator, &[_][]const u8{ full_dir_name, entry.name }); diff --git a/tools/update-license-headers.zig b/tools/update-license-headers.zig index 7756cf898c..b2aed7ccdb 100644 --- a/tools/update-license-headers.zig +++ b/tools/update-license-headers.zig @@ -14,9 +14,9 @@ pub fn main() !void { const args = try std.process.argsAlloc(arena); const path_to_walk = args[1]; - const iterable_dir = try std.fs.cwd().openIterableDir(path_to_walk, .{}); + const dir = try std.fs.cwd().openDir(path_to_walk, .{ .iterate = true }); - var walker = try iterable_dir.walk(arena); + var walker = try dir.walk(arena); defer walker.deinit(); var buffer: [500]u8 = undefined; @@ -30,7 +30,7 @@ pub fn main() !void { node.activate(); defer node.end(); - const source = try iterable_dir.dir.readFileAlloc(arena, entry.path, 20 * 1024 * 1024); + const source = try dir.readFileAlloc(arena, entry.path, 20 * 1024 * 1024); if (!std.mem.startsWith(u8, source, expected_header)) { std.debug.print("no match: {s}\n", .{entry.path}); continue; @@ -42,6 +42,6 @@ pub fn main() !void { std.mem.copy(u8, new_source, new_header); std.mem.copy(u8, new_source[new_header.len..], truncated_source); - try iterable_dir.dir.writeFile(entry.path, new_source); + try dir.writeFile(entry.path, new_source); } } diff --git a/tools/update-linux-headers.zig b/tools/update-linux-headers.zig index ef701fc86d..91032618c9 100644 --- a/tools/update-linux-headers.zig +++ b/tools/update-linux-headers.zig @@ -190,14 +190,14 @@ pub fn main() !void { try dir_stack.append(target_include_dir); while (dir_stack.popOrNull()) |full_dir_name| { - var iterable_dir = std.fs.cwd().openIterableDir(full_dir_name, .{}) catch |err| switch (err) { + var dir = std.fs.cwd().openDir(full_dir_name, .{ .iterate = true }) catch |err| switch (err) { error.FileNotFound => continue :search, error.AccessDenied => continue :search, else => return err, }; - defer iterable_dir.close(); + defer dir.close(); - var dir_it = iterable_dir.iterate(); + var dir_it = dir.iterate(); while (try dir_it.next()) |entry| { const full_path = try std.fs.path.join(arena, &[_][]const u8{ full_dir_name, entry.name }); diff --git a/tools/update_glibc.zig b/tools/update_glibc.zig index cf43736c8a..e86ce13454 100644 --- a/tools/update_glibc.zig +++ b/tools/update_glibc.zig @@ -47,7 +47,7 @@ pub fn main() !void { const dest_dir_path = try std.fmt.allocPrint(arena, "{s}/lib/libc/glibc", .{zig_src_path}); - var dest_dir = fs.cwd().openIterableDir(dest_dir_path, .{}) catch |err| { + var dest_dir = fs.cwd().openDir(dest_dir_path, .{ .iterate = true }) catch |err| { fatal("unable to open destination directory '{s}': {s}", .{ dest_dir_path, @errorName(err), }); @@ -72,14 +72,14 @@ pub fn main() !void { if (mem.endsWith(u8, entry.path, ext)) continue :walk; } - glibc_src_dir.copyFile(entry.path, dest_dir.dir, entry.path, .{}) catch |err| { + glibc_src_dir.copyFile(entry.path, dest_dir, entry.path, .{}) catch |err| { log.warn("unable to copy '{s}/{s}' to '{s}/{s}': {s}", .{ glibc_src_path, entry.path, dest_dir_path, entry.path, @errorName(err), }); if (err == error.FileNotFound) { - try dest_dir.dir.deleteFile(entry.path); + try dest_dir.deleteFile(entry.path); } }; } @@ -88,7 +88,7 @@ pub fn main() !void { // Warn about duplicated files inside glibc/include/* that can be omitted // because they are already in generic-glibc/*. - var include_dir = dest_dir.dir.openIterableDir("include", .{}) catch |err| { + var include_dir = dest_dir.openDir("include", .{ .iterate = true }) catch |err| { fatal("unable to open directory '{s}/include': {s}", .{ dest_dir_path, @errorName(err), }); @@ -125,7 +125,7 @@ pub fn main() !void { generic_glibc_path, entry.path, @errorName(e), }), }; - const glibc_include_contents = include_dir.dir.readFileAlloc( + const glibc_include_contents = include_dir.readFileAlloc( arena, entry.path, max_file_size, diff --git a/tools/update_spirv_features.zig b/tools/update_spirv_features.zig index 0c8acf5aef..0215c24507 100644 --- a/tools/update_spirv_features.zig +++ b/tools/update_spirv_features.zig @@ -226,7 +226,7 @@ pub fn main() !void { /// TODO: Unfortunately, neither repository contains a machine-readable list of extension dependencies. fn gather_extensions(allocator: Allocator, spirv_registry_root: []const u8) ![]const []const u8 { const extensions_path = try fs.path.join(allocator, &.{ spirv_registry_root, "extensions" }); - var extensions_dir = try fs.cwd().openIterableDir(extensions_path, .{}); + var extensions_dir = try fs.cwd().openDir(extensions_path, .{ .iterate = true }); defer extensions_dir.close(); var extensions = std.ArrayList([]const u8).init(allocator); @@ -235,7 +235,7 @@ fn gather_extensions(allocator: Allocator, spirv_registry_root: []const u8) ![]c while (try vendor_it.next()) |vendor_entry| { std.debug.assert(vendor_entry.kind == .directory); // If this fails, the structure of SPIRV-Registry has changed. - const vendor_dir = try extensions_dir.dir.openIterableDir(vendor_entry.name, .{}); + const vendor_dir = try extensions_dir.openDir(vendor_entry.name, .{ .iterate = true }); var ext_it = vendor_dir.iterate(); while (try ext_it.next()) |ext_entry| { // 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 // SPV_EXT_name // ``` - const ext_spec = try vendor_dir.dir.readFileAlloc(allocator, ext_entry.name, std.math.maxInt(usize)); + const ext_spec = try vendor_dir.readFileAlloc(allocator, ext_entry.name, std.math.maxInt(usize)); const name_strings = "Name Strings"; const name_strings_offset = std.mem.indexOf(u8, ext_spec, name_strings) orelse return error.InvalidRegistry; From e357550610aef476390ed7191eeaf7597a8e9d53 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Nov 2023 13:12:22 -0700 Subject: [PATCH 2/5] update for the std.fs.Dir changes --- lib/std/Build/Step/InstallDir.zig | 2 +- lib/std/child_process.zig | 3 ++- lib/std/crypto/Certificate/Bundle.zig | 8 +++--- lib/std/fs.zig | 10 +++---- lib/std/fs/test.zig | 38 +++++++++++++-------------- src/Package/Fetch.zig | 26 +++++++++--------- src/Package/Fetch/git.zig | 8 +++--- src/windows_sdk.zig | 14 +++++++--- test/src/Cases.zig | 10 +++---- test/tests.zig | 2 +- tools/generate_JSONTestSuite.zig | 2 +- 11 files changed, 66 insertions(+), 57 deletions(-) diff --git a/lib/std/Build/Step/InstallDir.zig b/lib/std/Build/Step/InstallDir.zig index b6c842164b..7472dba2a7 100644 --- a/lib/std/Build/Step/InstallDir.zig +++ b/lib/std/Build/Step/InstallDir.zig @@ -69,7 +69,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void { const dest_prefix = dest_builder.getInstallPath(self.options.install_dir, self.options.install_subdir); const src_builder = self.step.owner; const src_dir_path = self.options.source_dir.getPath2(src_builder, step); - var src_dir = src_builder.build_root.handle.openIterableDir(src_dir_path, .{}) catch |err| { + var src_dir = src_builder.build_root.handle.openDir(src_dir_path, .{ .iterate = true }) catch |err| { return step.fail("unable to open source directory '{}{s}': {s}", .{ src_builder.build_root, src_dir_path, @errorName(err), }); diff --git a/lib/std/child_process.zig b/lib/std/child_process.zig index 1d79302860..79b1549d99 100644 --- a/lib/std/child_process.zig +++ b/lib/std/child_process.zig @@ -976,7 +976,8 @@ fn windowsCreateProcessPathExt( defer dir_buf.shrinkRetainingCapacity(dir_path_len); const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); - break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{}, true) catch return error.FileNotFound; + break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{ .iterate = true }) catch + return error.FileNotFound; }; defer dir.close(); diff --git a/lib/std/crypto/Certificate/Bundle.zig b/lib/std/crypto/Certificate/Bundle.zig index b5a3832115..b1c3cfee2f 100644 --- a/lib/std/crypto/Certificate/Bundle.zig +++ b/lib/std/crypto/Certificate/Bundle.zig @@ -160,7 +160,7 @@ pub fn addCertsFromDirPath( dir: fs.Dir, sub_dir_path: []const u8, ) AddCertsFromDirPathError!void { - var iterable_dir = try dir.openIterableDir(sub_dir_path, .{}); + var iterable_dir = try dir.openDir(sub_dir_path, .{ .iterate = true }); defer iterable_dir.close(); return addCertsFromDir(cb, gpa, iterable_dir); } @@ -171,14 +171,14 @@ pub fn addCertsFromDirPathAbsolute( abs_dir_path: []const u8, ) AddCertsFromDirPathError!void { assert(fs.path.isAbsolute(abs_dir_path)); - var iterable_dir = try fs.openIterableDirAbsolute(abs_dir_path, .{}); + var iterable_dir = try fs.openDirAbsolute(abs_dir_path, .{ .iterate = true }); defer iterable_dir.close(); return addCertsFromDir(cb, gpa, iterable_dir); } pub const AddCertsFromDirError = AddCertsFromFilePathError; -pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir) AddCertsFromDirError!void { +pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.Dir) AddCertsFromDirError!void { var it = iterable_dir.iterate(); while (try it.next()) |entry| { switch (entry.kind) { @@ -186,7 +186,7 @@ pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir else => continue, } - try addCertsFromFilePath(cb, gpa, iterable_dir.dir, entry.name); + try addCertsFromFilePath(cb, gpa, iterable_dir, entry.name); } } diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 12e1ac34ab..b73af438b1 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1653,12 +1653,12 @@ pub const Dir = struct { pub fn openDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { if (builtin.os.tag == .windows) { const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); - return .{ .dir = try self.openDirW(sub_path_w.span().ptr, args) }; + return self.openDirW(sub_path_w.span().ptr, args); } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - return .{ .dir = try self.openDirWasi(sub_path, args) }; + return self.openDirWasi(sub_path, args); } else { const sub_path_c = try os.toPosixPath(sub_path); - return .{ .dir = try self.openDirZ(&sub_path_c, args) }; + return self.openDirZ(&sub_path_c, args); } } @@ -2784,12 +2784,12 @@ pub fn openDirAbsolute(absolute_path: []const u8, flags: Dir.OpenDirOptions) Fil /// Same as `openDirAbsolute` but the path parameter is null-terminated. pub fn openDirAbsoluteZ(absolute_path_c: [*:0]const u8, flags: Dir.OpenDirOptions) File.OpenError!Dir { assert(path.isAbsoluteZ(absolute_path_c)); - return cwd().openDirZ(absolute_path_c, flags, false); + return cwd().openDirZ(absolute_path_c, flags); } /// Same as `openDirAbsolute` but the path parameter is null-terminated. pub fn openDirAbsoluteW(absolute_path_c: [*:0]const u16, flags: Dir.OpenDirOptions) File.OpenError!Dir { assert(path.isAbsoluteWindowsW(absolute_path_c)); - return cwd().openDirW(absolute_path_c, flags, false); + return cwd().openDirW(absolute_path_c, flags); } /// Opens a file for reading or writing, without attempting to create a new file, based on an absolute path. diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index fcf978d307..5ad354590b 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -72,9 +72,8 @@ const PathType = enum { const TestContext = struct { path_type: PathType, arena: ArenaAllocator, - tmp: testing.TmpIterableDir, + tmp: testing.TmpDir, dir: std.fs.Dir, - iterable_dir: std.fs.Dir, transform_fn: *const PathType.TransformFn, pub fn init(path_type: PathType, allocator: mem.Allocator, transform_fn: *const PathType.TransformFn) TestContext { @@ -83,8 +82,7 @@ const TestContext = struct { .path_type = path_type, .arena = ArenaAllocator.init(allocator), .tmp = tmp, - .dir = tmp.iterable_dir.dir, - .iterable_dir = tmp.iterable_dir, + .dir = tmp.dir, .transform_fn = transform_fn, }; } @@ -359,7 +357,7 @@ test "Dir.Iterator many entries" { var buf: [4]u8 = undefined; // Enough to store "1024". while (i < num) : (i += 1) { const name = try std.fmt.bufPrint(&buf, "{}", .{i}); - const file = try tmp_dir.iterable_dir.dir.createFile(name, .{}); + const file = try tmp_dir.dir.createFile(name, .{}); file.close(); } @@ -370,7 +368,7 @@ test "Dir.Iterator many entries" { var entries = std.ArrayList(Dir.Entry).init(allocator); // Create iterator. - var iter = tmp_dir.iterable_dir.iterate(); + var iter = tmp_dir.dir.iterate(); while (try iter.next()) |entry| { // We cannot just store `entry` as on Windows, we're re-using the name buffer // which means we'll actually share the `name` pointer between entries! @@ -423,17 +421,17 @@ test "Dir.Iterator reset" { defer tmp_dir.cleanup(); // First, create a couple of entries to iterate over. - const file = try tmp_dir.iterable_dir.dir.createFile("some_file", .{}); + const file = try tmp_dir.dir.createFile("some_file", .{}); file.close(); - try tmp_dir.iterable_dir.dir.makeDir("some_dir"); + try tmp_dir.dir.makeDir("some_dir"); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); // Create iterator. - var iter = tmp_dir.iterable_dir.iterate(); + var iter = tmp_dir.dir.iterate(); var i: u8 = 0; while (i < 2) : (i += 1) { @@ -459,10 +457,10 @@ test "Dir.Iterator but dir is deleted during iteration" { defer tmp.cleanup(); // Create directory and setup an iterator for it - var iterable_subdir = try tmp.dir.makeOpenPathIterable("subdir", .{}); - defer iterable_subdir.close(); + var subdir = try tmp.dir.makeOpenPath("subdir", .{ .iterate = true }); + defer subdir.close(); - var iterator = iterable_subdir.iterate(); + var iterator = subdir.iterate(); // Create something to iterate over within the subdir try tmp.dir.makePath("subdir/b"); @@ -964,7 +962,7 @@ test "makePath in a directory that no longer exists" { fn testFilenameLimits(iterable_dir: Dir, maxed_filename: []const u8) !void { // setup, create a dir and a nested file both with maxed filenames, and walk the dir { - var maxed_dir = try iterable_dir.dir.makeOpenPath(maxed_filename, .{}); + var maxed_dir = try iterable_dir.makeOpenPath(maxed_filename, .{}); defer maxed_dir.close(); try maxed_dir.writeFile(maxed_filename, ""); @@ -981,7 +979,7 @@ fn testFilenameLimits(iterable_dir: Dir, maxed_filename: []const u8) !void { } // ensure that we can delete the tree - try iterable_dir.dir.deleteTree(maxed_filename); + try iterable_dir.deleteTree(maxed_filename); } test "max file name component lengths" { @@ -992,16 +990,16 @@ test "max file name component lengths" { // U+FFFF is the character with the largest code point that is encoded as a single // UTF-16 code unit, so Windows allows for NAME_MAX of them. const maxed_windows_filename = ("\u{FFFF}".*) ** std.os.windows.NAME_MAX; - try testFilenameLimits(tmp.iterable_dir, &maxed_windows_filename); + try testFilenameLimits(tmp.dir, &maxed_windows_filename); } else if (builtin.os.tag == .wasi) { // On WASI, the maxed filename depends on the host OS, so in order for this test to // work on any host, we need to use a length that will work for all platforms // (i.e. the minimum MAX_NAME_BYTES of all supported platforms). const maxed_wasi_filename = [_]u8{'1'} ** 255; - try testFilenameLimits(tmp.iterable_dir, &maxed_wasi_filename); + try testFilenameLimits(tmp.dir, &maxed_wasi_filename); } else { const maxed_ascii_filename = [_]u8{'1'} ** std.fs.MAX_NAME_BYTES; - try testFilenameLimits(tmp.iterable_dir, &maxed_ascii_filename); + try testFilenameLimits(tmp.dir, &maxed_ascii_filename); } } @@ -1438,14 +1436,14 @@ test "walker without fully iterating" { var tmp = tmpDir(.{ .iterate = true }); defer tmp.cleanup(); - var walker = try tmp.iterable_dir.walk(testing.allocator); + var walker = try tmp.dir.walk(testing.allocator); defer walker.deinit(); // Create 2 directories inside the tmp directory, but then only iterate once before breaking. // This ensures that walker doesn't try to close the initial directory when not fully iterating. - try tmp.iterable_dir.dir.makePath("a"); - try tmp.iterable_dir.dir.makePath("b"); + try tmp.dir.makePath("a"); + try tmp.dir.makePath("b"); var num_walked: usize = 0; while (try walker.next()) |_| { diff --git a/src/Package/Fetch.zig b/src/Package/Fetch.zig index a61c420cb2..28b8158453 100644 --- a/src/Package/Fetch.zig +++ b/src/Package/Fetch.zig @@ -280,7 +280,7 @@ pub fn run(f: *Fetch) RunError!void { }, .remote => |remote| remote, .path_or_url => |path_or_url| { - if (fs.cwd().openIterableDir(path_or_url, .{})) |dir| { + if (fs.cwd().openDir(path_or_url, .{ .iterate = true })) |dir| { var resource: Resource = .{ .dir = dir }; return runResource(f, path_or_url, &resource, null); } else |dir_err| { @@ -363,7 +363,9 @@ fn runResource( var tmp_directory: Cache.Directory = .{ .path = tmp_directory_path, .handle = handle: { - const dir = cache_root.handle.makeOpenPathIterable(tmp_dir_sub_path, .{}) catch |err| { + const dir = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{ + .iterate = true, + }) catch |err| { try eb.addRootErrorMessage(.{ .msg = try eb.printString("unable to create temporary directory '{s}': {s}", .{ tmp_directory_path, @errorName(err), @@ -371,7 +373,7 @@ fn runResource( }); return error.FetchFailed; }; - break :handle dir.dir; + break :handle dir; }, }; defer tmp_directory.handle.close(); @@ -400,9 +402,9 @@ fn runResource( if (builtin.os.tag == .linux and f.job_queue.work_around_btrfs_bug) { // https://github.com/ziglang/zig/issues/17095 tmp_directory.handle.close(); - const iterable_dir = cache_root.handle.makeOpenPathIterable(tmp_dir_sub_path, .{}) catch - @panic("btrfs workaround failed"); - tmp_directory.handle = iterable_dir.dir; + tmp_directory.handle = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{ + .iterate = true, + }) catch @panic("btrfs workaround failed"); } f.actual_hash = try computeHash(f, tmp_directory, filter); @@ -717,7 +719,7 @@ const Resource = union(enum) { file: fs.File, http_request: std.http.Client.Request, git: Git, - dir: fs.IterableDir, + dir: fs.Dir, const Git = struct { fetch_stream: git.Session.FetchStream, @@ -1198,7 +1200,7 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource) anyerror!void try out_dir.deleteTree(".git"); } -fn recursiveDirectoryCopy(f: *Fetch, dir: fs.IterableDir, tmp_dir: fs.Dir) anyerror!void { +fn recursiveDirectoryCopy(f: *Fetch, dir: fs.Dir, tmp_dir: fs.Dir) anyerror!void { const gpa = f.arena.child_allocator; // Recursive directory copy. var it = try dir.walk(gpa); @@ -1207,7 +1209,7 @@ fn recursiveDirectoryCopy(f: *Fetch, dir: fs.IterableDir, tmp_dir: fs.Dir) anyer switch (entry.kind) { .directory => {}, // omit empty directories .file => { - dir.dir.copyFile( + dir.copyFile( entry.path, tmp_dir, entry.path, @@ -1215,14 +1217,14 @@ fn recursiveDirectoryCopy(f: *Fetch, dir: fs.IterableDir, tmp_dir: fs.Dir) anyer ) catch |err| switch (err) { error.FileNotFound => { if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(dirname); - try dir.dir.copyFile(entry.path, tmp_dir, entry.path, .{}); + try dir.copyFile(entry.path, tmp_dir, entry.path, .{}); }, else => |e| return e, }; }, .sym_link => { var buf: [fs.MAX_PATH_BYTES]u8 = undefined; - const link_name = try dir.dir.readLink(entry.path, &buf); + const link_name = try dir.readLink(entry.path, &buf); // TODO: if this would create a symlink to outside // the destination directory, fail with an error instead. tmp_dir.symLink(link_name, entry.path, .{}) catch |err| switch (err) { @@ -1296,7 +1298,7 @@ fn computeHash( var sus_dirs: std.StringArrayHashMapUnmanaged(void) = .{}; defer sus_dirs.deinit(gpa); - var walker = try @as(fs.IterableDir, .{ .dir = tmp_directory.handle }).walk(gpa); + var walker = try tmp_directory.handle.walk(gpa); defer walker.deinit(); { diff --git a/src/Package/Fetch/git.zig b/src/Package/Fetch/git.zig index 827b608cc6..df5332d41d 100644 --- a/src/Package/Fetch/git.zig +++ b/src/Package/Fetch/git.zig @@ -1384,11 +1384,11 @@ test "packfile indexing and checkout" { var repository = try Repository.init(testing.allocator, pack_file, index_file); defer repository.deinit(); - var worktree = testing.tmpIterableDir(.{}); + var worktree = testing.tmpDir(.{ .iterate = true }); defer worktree.cleanup(); const commit_id = try parseOid("dd582c0720819ab7130b103635bd7271b9fd4feb"); - try repository.checkout(worktree.iterable_dir.dir, commit_id); + try repository.checkout(worktree.dir, commit_id); const expected_files: []const []const u8 = &.{ "dir/file", @@ -1410,7 +1410,7 @@ test "packfile indexing and checkout" { var actual_files: std.ArrayListUnmanaged([]u8) = .{}; defer actual_files.deinit(testing.allocator); defer for (actual_files.items) |file| testing.allocator.free(file); - var walker = try worktree.iterable_dir.walk(testing.allocator); + var walker = try worktree.dir.walk(testing.allocator); defer walker.deinit(); while (try walker.next()) |entry| { if (entry.kind != .file) continue; @@ -1442,7 +1442,7 @@ test "packfile indexing and checkout" { \\revision 19 \\ ; - const actual_file_contents = try worktree.iterable_dir.dir.readFileAlloc(testing.allocator, "file", max_file_size); + const actual_file_contents = try worktree.dir.readFileAlloc(testing.allocator, "file", max_file_size); defer testing.allocator.free(actual_file_contents); try testing.expectEqualStrings(expected_file_contents, actual_file_contents); } diff --git a/src/windows_sdk.zig b/src/windows_sdk.zig index d6add063d8..3acb70e986 100644 --- a/src/windows_sdk.zig +++ b/src/windows_sdk.zig @@ -14,7 +14,11 @@ const product_version_max_length = version_major_minor_max_length + ".65535".len /// Iterates via `iterator` and collects all folders with names starting with `optional_prefix` /// and similar to SemVer. Returns slice of folder names sorted in descending order. /// Caller owns result. -fn iterateAndFilterBySemVer(iterator: *std.fs.IterableDir.Iterator, allocator: std.mem.Allocator, comptime optional_prefix: ?[]const u8) error{ OutOfMemory, VersionNotFound }![][]const u8 { +fn iterateAndFilterBySemVer( + iterator: *std.fs.Dir.Iterator, + allocator: std.mem.Allocator, + comptime optional_prefix: ?[]const u8, +) error{ OutOfMemory, VersionNotFound }![][]const u8 { var dirs_filtered_list = std.ArrayList([]const u8).init(allocator); errdefer { for (dirs_filtered_list.items) |filtered_dir| allocator.free(filtered_dir); @@ -476,7 +480,9 @@ pub const Windows81Sdk = struct { if (!std.fs.path.isAbsolute(sdk_lib_dir_path)) return error.Windows81SdkNotFound; // enumerate files in sdk path looking for latest version - var sdk_lib_dir = std.fs.openIterableDirAbsolute(sdk_lib_dir_path, .{}) catch |err| switch (err) { + var sdk_lib_dir = std.fs.openDirAbsolute(sdk_lib_dir_path, .{ + .iterate = true, + }) catch |err| switch (err) { error.NameTooLong => return error.PathTooLong, else => return error.Windows81SdkNotFound, }; @@ -727,7 +733,9 @@ const MsvcLibDir = struct { if (!std.fs.path.isAbsolute(visualstudio_folder_path)) return error.PathNotFound; // enumerate folders that contain `privateregistry.bin`, looking for all versions // f.i. %localappdata%\Microsoft\VisualStudio\17.0_9e9cbb98\ - var visualstudio_folder = std.fs.openIterableDirAbsolute(visualstudio_folder_path, .{}) catch return error.PathNotFound; + var visualstudio_folder = std.fs.openDirAbsolute(visualstudio_folder_path, .{ + .iterate = true, + }) catch return error.PathNotFound; defer visualstudio_folder.close(); var iterator = visualstudio_folder.iterate(); diff --git a/test/src/Cases.zig b/test/src/Cases.zig index f533af37d7..12852d388b 100644 --- a/test/src/Cases.zig +++ b/test/src/Cases.zig @@ -368,7 +368,7 @@ pub fn addCompile( /// Each file should include a test manifest as a contiguous block of comments at /// the end of the file. The first line should be the test type, followed by a set of /// key-value config values, followed by a blank line, then the expected output. -pub fn addFromDir(ctx: *Cases, dir: std.fs.IterableDir) void { +pub fn addFromDir(ctx: *Cases, dir: std.fs.Dir) void { var current_file: []const u8 = "none"; ctx.addFromDirInner(dir, ¤t_file) catch |err| { std.debug.panicExtra( @@ -382,7 +382,7 @@ pub fn addFromDir(ctx: *Cases, dir: std.fs.IterableDir) void { fn addFromDirInner( ctx: *Cases, - iterable_dir: std.fs.IterableDir, + iterable_dir: std.fs.Dir, /// This is kept up to date with the currently being processed file so /// that if any errors occur the caller knows it happened during this file. current_file: *[]const u8, @@ -416,7 +416,7 @@ fn addFromDirInner( } const max_file_size = 10 * 1024 * 1024; - const src = try iterable_dir.dir.readFileAllocOptions(ctx.arena, filename, max_file_size, null, 1, 0); + const src = try iterable_dir.readFileAllocOptions(ctx.arena, filename, max_file_size, null, 1, 0); // Parse the manifest var manifest = try TestManifest.parse(ctx.arena, src); @@ -1246,7 +1246,7 @@ pub fn main() !void { var filenames = std.ArrayList([]const u8).init(arena); const case_dirname = std.fs.path.dirname(case_file_path).?; - var iterable_dir = try std.fs.cwd().openIterableDir(case_dirname, .{}); + var iterable_dir = try std.fs.cwd().openDir(case_dirname, .{ .iterate = true }); defer iterable_dir.close(); if (std.mem.endsWith(u8, case_file_path, ".0.zig")) { @@ -1280,7 +1280,7 @@ pub fn main() !void { for (batch) |filename| { const max_file_size = 10 * 1024 * 1024; - const src = try iterable_dir.dir.readFileAllocOptions(arena, filename, max_file_size, null, 1, 0); + const src = try iterable_dir.readFileAllocOptions(arena, filename, max_file_size, null, 1, 0); // Parse the manifest var manifest = try TestManifest.parse(arena, src); diff --git a/test/tests.zig b/test/tests.zig index 0bdc15765a..a25ba4b1d2 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -1288,7 +1288,7 @@ pub fn addCases( var cases = @import("src/Cases.zig").init(gpa, arena); - var dir = try b.build_root.handle.openIterableDir("test/cases", .{}); + var dir = try b.build_root.handle.openDir("test/cases", .{ .iterate = true }); defer dir.close(); cases.addFromDir(dir); diff --git a/tools/generate_JSONTestSuite.zig b/tools/generate_JSONTestSuite.zig index 2229cf4012..ed3e5bd082 100644 --- a/tools/generate_JSONTestSuite.zig +++ b/tools/generate_JSONTestSuite.zig @@ -18,7 +18,7 @@ pub fn main() !void { ); var names = std.ArrayList([]const u8).init(allocator); - var cwd = try std.fs.cwd().openIterableDir(".", .{}); + var cwd = try std.fs.cwd().openDir(".", .{ .iterate = true }); var it = cwd.iterate(); while (try it.next()) |entry| { try names.append(try allocator.dupe(u8, entry.name)); From c95e2e65fac6afec5c57f6716b1f4e8e7a7292fb Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Nov 2023 13:50:31 -0700 Subject: [PATCH 3/5] std.fs: extract Dir into separate file --- CMakeLists.txt | 1 + lib/std/fs.zig | 2560 +------------------------------------------ lib/std/fs/Dir.zig | 2533 ++++++++++++++++++++++++++++++++++++++++++ lib/std/fs/test.zig | 4 +- 4 files changed, 2567 insertions(+), 2531 deletions(-) create mode 100644 lib/std/fs/Dir.zig diff --git a/CMakeLists.txt b/CMakeLists.txt index 18e627b641..8aaeb288ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -247,6 +247,7 @@ set(ZIG_STAGE2_SOURCES "${CMAKE_SOURCE_DIR}/lib/std/fmt/errol/lookup.zig" "${CMAKE_SOURCE_DIR}/lib/std/fmt/parse_float.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs.zig" + "${CMAKE_SOURCE_DIR}/lib/std/fs/Dir.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/file.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/get_app_data_dir.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/path.zig" diff --git a/lib/std/fs.zig b/lib/std/fs.zig index b73af438b1..1a4b42ef1b 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -11,6 +11,8 @@ const math = std.math; const is_darwin = builtin.os.tag.isDarwin(); +pub const Dir = @import("fs/Dir.zig"); + pub const has_executable_bit = switch (builtin.os.tag) { .windows, .wasi => false, else => true, @@ -120,24 +122,14 @@ pub fn atomicSymLink(allocator: Allocator, existing_path: []const u8, new_path: } } -pub const PrevStatus = enum { - stale, - fresh, -}; - -pub const CopyFileOptions = struct { - /// When this is `null` the mode is copied from the source file. - override_mode: ?File.Mode = null, -}; - /// Same as `Dir.updateFile`, except asserts that both `source_path` and `dest_path` /// are absolute. See `Dir.updateFile` for a function that operates on both /// absolute and relative paths. pub fn updateFileAbsolute( source_path: []const u8, dest_path: []const u8, - args: CopyFileOptions, -) !PrevStatus { + args: Dir.CopyFileOptions, +) !Dir.PrevStatus { assert(path.isAbsolute(source_path)); assert(path.isAbsolute(dest_path)); const my_cwd = cwd(); @@ -147,7 +139,11 @@ pub fn updateFileAbsolute( /// Same as `Dir.copyFile`, except asserts that both `source_path` and `dest_path` /// are absolute. See `Dir.copyFile` for a function that operates on both /// absolute and relative paths. -pub fn copyFileAbsolute(source_path: []const u8, dest_path: []const u8, args: CopyFileOptions) !void { +pub fn copyFileAbsolute( + source_path: []const u8, + dest_path: []const u8, + args: Dir.CopyFileOptions, +) !void { assert(path.isAbsolute(source_path)); assert(path.isAbsolute(dest_path)); const my_cwd = cwd(); @@ -164,7 +160,7 @@ pub const AtomicFile = struct { close_dir_on_deinit: bool, dir: Dir, - const InitError = File.OpenError; + pub const InitError = File.OpenError; const RANDOM_BYTES = 12; const TMP_PATH_LEN = base64_encoder.calcSize(RANDOM_BYTES); @@ -233,26 +229,24 @@ pub const AtomicFile = struct { } }; -const default_new_dir_mode = 0o755; - /// Create a new directory, based on an absolute path. /// Asserts that the path is absolute. See `Dir.makeDir` for a function that operates /// on both absolute and relative paths. pub fn makeDirAbsolute(absolute_path: []const u8) !void { assert(path.isAbsolute(absolute_path)); - return os.mkdir(absolute_path, default_new_dir_mode); + return os.mkdir(absolute_path, Dir.default_mode); } /// Same as `makeDirAbsolute` except the parameter is a null-terminated UTF-8-encoded string. pub fn makeDirAbsoluteZ(absolute_path_z: [*:0]const u8) !void { assert(path.isAbsoluteZ(absolute_path_z)); - return os.mkdirZ(absolute_path_z, default_new_dir_mode); + return os.mkdirZ(absolute_path_z, Dir.default_mode); } /// Same as `makeDirAbsolute` except the parameter is a null-terminated WTF-16-encoded string. pub fn makeDirAbsoluteW(absolute_path_w: [*:0]const u16) !void { assert(path.isAbsoluteWindowsW(absolute_path_w)); - return os.mkdirW(absolute_path_w, default_new_dir_mode); + return os.mkdirW(absolute_path_w, Dir.default_mode); } /// Same as `Dir.deleteDir` except the path is absolute. @@ -310,2449 +304,6 @@ pub fn renameW(old_dir: Dir, old_sub_path_w: []const u16, new_dir: Dir, new_sub_ return os.renameatW(old_dir.fd, old_sub_path_w, new_dir.fd, new_sub_path_w); } -pub const Dir = struct { - fd: os.fd_t, - - pub const Entry = struct { - name: []const u8, - kind: Kind, - - pub const Kind = File.Kind; - }; - - const IteratorError = error{ AccessDenied, SystemResources } || os.UnexpectedError; - - pub const Iterator = switch (builtin.os.tag) { - .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => struct { - dir: Dir, - seek: i64, - buf: [1024]u8, // TODO align(@alignOf(os.system.dirent)), - index: usize, - end_index: usize, - first_iter: bool, - - const Self = @This(); - - pub const Error = IteratorError; - - /// Memory such as file names referenced in this returned entry becomes invalid - /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. - pub fn next(self: *Self) Error!?Entry { - switch (builtin.os.tag) { - .macos, .ios => return self.nextDarwin(), - .freebsd, .netbsd, .dragonfly, .openbsd => return self.nextBsd(), - .solaris, .illumos => return self.nextSolaris(), - else => @compileError("unimplemented"), - } - } - - fn nextDarwin(self: *Self) !?Entry { - start_over: while (true) { - if (self.index >= self.end_index) { - if (self.first_iter) { - std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions - self.first_iter = false; - } - const rc = os.system.__getdirentries64( - self.dir.fd, - &self.buf, - self.buf.len, - &self.seek, - ); - if (rc == 0) return null; - if (rc < 0) { - switch (os.errno(rc)) { - .BADF => unreachable, // Dir is invalid or was opened without iteration ability - .FAULT => unreachable, - .NOTDIR => unreachable, - .INVAL => unreachable, - else => |err| return os.unexpectedErrno(err), - } - } - self.index = 0; - self.end_index = @as(usize, @intCast(rc)); - } - const darwin_entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index])); - const next_index = self.index + darwin_entry.reclen(); - self.index = next_index; - - const name = @as([*]u8, @ptrCast(&darwin_entry.d_name))[0..darwin_entry.d_namlen]; - - if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (darwin_entry.d_ino == 0)) { - continue :start_over; - } - - const entry_kind: Entry.Kind = switch (darwin_entry.d_type) { - os.DT.BLK => .block_device, - os.DT.CHR => .character_device, - os.DT.DIR => .directory, - os.DT.FIFO => .named_pipe, - os.DT.LNK => .sym_link, - os.DT.REG => .file, - os.DT.SOCK => .unix_domain_socket, - os.DT.WHT => .whiteout, - else => .unknown, - }; - return Entry{ - .name = name, - .kind = entry_kind, - }; - } - } - - fn nextSolaris(self: *Self) !?Entry { - start_over: while (true) { - if (self.index >= self.end_index) { - if (self.first_iter) { - std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions - self.first_iter = false; - } - const rc = os.system.getdents(self.dir.fd, &self.buf, self.buf.len); - switch (os.errno(rc)) { - .SUCCESS => {}, - .BADF => unreachable, // Dir is invalid or was opened without iteration ability - .FAULT => unreachable, - .NOTDIR => unreachable, - .INVAL => unreachable, - else => |err| return os.unexpectedErrno(err), - } - if (rc == 0) return null; - self.index = 0; - self.end_index = @as(usize, @intCast(rc)); - } - const entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index])); - const next_index = self.index + entry.reclen(); - self.index = next_index; - - const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&entry.d_name)), 0); - if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) - continue :start_over; - - // Solaris dirent doesn't expose d_type, so we have to call stat to get it. - const stat_info = os.fstatat( - self.dir.fd, - name, - os.AT.SYMLINK_NOFOLLOW, - ) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.SymLinkLoop => unreachable, - error.FileNotFound => unreachable, // lost the race - else => |e| return e, - }; - const entry_kind: Entry.Kind = switch (stat_info.mode & os.S.IFMT) { - os.S.IFIFO => .named_pipe, - os.S.IFCHR => .character_device, - os.S.IFDIR => .directory, - os.S.IFBLK => .block_device, - os.S.IFREG => .file, - os.S.IFLNK => .sym_link, - os.S.IFSOCK => .unix_domain_socket, - os.S.IFDOOR => .door, - os.S.IFPORT => .event_port, - else => .unknown, - }; - return Entry{ - .name = name, - .kind = entry_kind, - }; - } - } - - fn nextBsd(self: *Self) !?Entry { - start_over: while (true) { - if (self.index >= self.end_index) { - if (self.first_iter) { - std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions - self.first_iter = false; - } - const rc = if (builtin.os.tag == .netbsd) - os.system.__getdents30(self.dir.fd, &self.buf, self.buf.len) - else - os.system.getdents(self.dir.fd, &self.buf, self.buf.len); - switch (os.errno(rc)) { - .SUCCESS => {}, - .BADF => unreachable, // Dir is invalid or was opened without iteration ability - .FAULT => unreachable, - .NOTDIR => unreachable, - .INVAL => unreachable, - // Introduced in freebsd 13.2: directory unlinked but still open. - // To be consistent, iteration ends if the directory being iterated is deleted during iteration. - .NOENT => return null, - else => |err| return os.unexpectedErrno(err), - } - if (rc == 0) return null; - self.index = 0; - self.end_index = @as(usize, @intCast(rc)); - } - const bsd_entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index])); - const next_index = self.index + bsd_entry.reclen(); - self.index = next_index; - - const name = @as([*]u8, @ptrCast(&bsd_entry.d_name))[0..bsd_entry.d_namlen]; - - const skip_zero_fileno = switch (builtin.os.tag) { - // d_fileno=0 is used to mark invalid entries or deleted files. - .openbsd, .netbsd => true, - else => false, - }; - if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or - (skip_zero_fileno and bsd_entry.d_fileno == 0)) - { - continue :start_over; - } - - const entry_kind: Entry.Kind = switch (bsd_entry.d_type) { - os.DT.BLK => .block_device, - os.DT.CHR => .character_device, - os.DT.DIR => .directory, - os.DT.FIFO => .named_pipe, - os.DT.LNK => .sym_link, - os.DT.REG => .file, - os.DT.SOCK => .unix_domain_socket, - os.DT.WHT => .whiteout, - else => .unknown, - }; - return Entry{ - .name = name, - .kind = entry_kind, - }; - } - } - - pub fn reset(self: *Self) void { - self.index = 0; - self.end_index = 0; - self.first_iter = true; - } - }, - .haiku => struct { - dir: Dir, - buf: [1024]u8, // TODO align(@alignOf(os.dirent64)), - index: usize, - end_index: usize, - first_iter: bool, - - const Self = @This(); - - pub const Error = IteratorError; - - /// Memory such as file names referenced in this returned entry becomes invalid - /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. - pub fn next(self: *Self) Error!?Entry { - start_over: while (true) { - // TODO: find a better max - const HAIKU_MAX_COUNT = 10000; - if (self.index >= self.end_index) { - if (self.first_iter) { - std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions - self.first_iter = false; - } - const rc = os.system._kern_read_dir( - self.dir.fd, - &self.buf, - self.buf.len, - HAIKU_MAX_COUNT, - ); - if (rc == 0) return null; - if (rc < 0) { - switch (os.errno(rc)) { - .BADF => unreachable, // Dir is invalid or was opened without iteration ability - .FAULT => unreachable, - .NOTDIR => unreachable, - .INVAL => unreachable, - else => |err| return os.unexpectedErrno(err), - } - } - self.index = 0; - self.end_index = @as(usize, @intCast(rc)); - } - const haiku_entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index])); - const next_index = self.index + haiku_entry.reclen(); - self.index = next_index; - const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&haiku_entry.d_name)), 0); - - if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (haiku_entry.d_ino == 0)) { - continue :start_over; - } - - var stat_info: os.Stat = undefined; - const rc = os.system._kern_read_stat( - self.dir.fd, - &haiku_entry.d_name, - false, - &stat_info, - 0, - ); - if (rc != 0) { - switch (os.errno(rc)) { - .SUCCESS => {}, - .BADF => unreachable, // Dir is invalid or was opened without iteration ability - .FAULT => unreachable, - .NOTDIR => unreachable, - .INVAL => unreachable, - else => |err| return os.unexpectedErrno(err), - } - } - const statmode = stat_info.mode & os.S.IFMT; - - const entry_kind: Entry.Kind = switch (statmode) { - os.S.IFDIR => .directory, - os.S.IFBLK => .block_device, - os.S.IFCHR => .character_device, - os.S.IFLNK => .sym_link, - os.S.IFREG => .file, - os.S.IFIFO => .named_pipe, - else => .unknown, - }; - - return Entry{ - .name = name, - .kind = entry_kind, - }; - } - } - - pub fn reset(self: *Self) void { - self.index = 0; - self.end_index = 0; - self.first_iter = true; - } - }, - .linux => struct { - dir: Dir, - // The if guard is solely there to prevent compile errors from missing `linux.dirent64` - // definition when compiling for other OSes. It doesn't do anything when compiling for Linux. - buf: [1024]u8 align(if (builtin.os.tag != .linux) 1 else @alignOf(linux.dirent64)), - index: usize, - end_index: usize, - first_iter: bool, - - const Self = @This(); - const linux = os.linux; - - pub const Error = IteratorError; - - /// Memory such as file names referenced in this returned entry becomes invalid - /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. - pub fn next(self: *Self) Error!?Entry { - return self.nextLinux() catch |err| switch (err) { - // To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration. - // This matches the behavior of non-Linux UNIX platforms. - error.DirNotFound => null, - else => |e| return e, - }; - } - - pub const ErrorLinux = error{DirNotFound} || IteratorError; - - /// Implementation of `next` that can return `error.DirNotFound` if the directory being - /// iterated was deleted during iteration (this error is Linux specific). - pub fn nextLinux(self: *Self) ErrorLinux!?Entry { - start_over: while (true) { - if (self.index >= self.end_index) { - if (self.first_iter) { - std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions - self.first_iter = false; - } - const rc = linux.getdents64(self.dir.fd, &self.buf, self.buf.len); - switch (linux.getErrno(rc)) { - .SUCCESS => {}, - .BADF => unreachable, // Dir is invalid or was opened without iteration ability - .FAULT => unreachable, - .NOTDIR => unreachable, - .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. - .INVAL => return error.Unexpected, // Linux may in some cases return EINVAL when reading /proc/$PID/net. - .ACCES => return error.AccessDenied, // Do not have permission to iterate this directory. - else => |err| return os.unexpectedErrno(err), - } - if (rc == 0) return null; - self.index = 0; - self.end_index = rc; - } - const linux_entry = @as(*align(1) linux.dirent64, @ptrCast(&self.buf[self.index])); - const next_index = self.index + linux_entry.reclen(); - self.index = next_index; - - const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&linux_entry.d_name)), 0); - - // skip . and .. entries - if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { - continue :start_over; - } - - const entry_kind: Entry.Kind = switch (linux_entry.d_type) { - linux.DT.BLK => .block_device, - linux.DT.CHR => .character_device, - linux.DT.DIR => .directory, - linux.DT.FIFO => .named_pipe, - linux.DT.LNK => .sym_link, - linux.DT.REG => .file, - linux.DT.SOCK => .unix_domain_socket, - else => .unknown, - }; - return Entry{ - .name = name, - .kind = entry_kind, - }; - } - } - - pub fn reset(self: *Self) void { - self.index = 0; - self.end_index = 0; - self.first_iter = true; - } - }, - .windows => struct { - dir: Dir, - buf: [1024]u8 align(@alignOf(os.windows.FILE_BOTH_DIR_INFORMATION)), - index: usize, - end_index: usize, - first_iter: bool, - name_data: [MAX_NAME_BYTES]u8, - - const Self = @This(); - - pub const Error = IteratorError; - - /// Memory such as file names referenced in this returned entry becomes invalid - /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. - pub fn next(self: *Self) Error!?Entry { - while (true) { - const w = os.windows; - if (self.index >= self.end_index) { - var io: w.IO_STATUS_BLOCK = undefined; - const rc = w.ntdll.NtQueryDirectoryFile( - self.dir.fd, - null, - null, - null, - &io, - &self.buf, - self.buf.len, - .FileBothDirectoryInformation, - w.FALSE, - null, - if (self.first_iter) @as(w.BOOLEAN, w.TRUE) else @as(w.BOOLEAN, w.FALSE), - ); - self.first_iter = false; - if (io.Information == 0) return null; - self.index = 0; - self.end_index = io.Information; - switch (rc) { - .SUCCESS => {}, - .ACCESS_DENIED => return error.AccessDenied, // Double-check that the Dir was opened with iteration ability - - else => return w.unexpectedStatus(rc), - } - } - - // While the official api docs guarantee FILE_BOTH_DIR_INFORMATION to be aligned properly - // this may not always be the case (e.g. due to faulty VM/Sandboxing tools) - const dir_info: *align(2) w.FILE_BOTH_DIR_INFORMATION = @ptrCast(@alignCast(&self.buf[self.index])); - if (dir_info.NextEntryOffset != 0) { - self.index += dir_info.NextEntryOffset; - } else { - self.index = self.buf.len; - } - - const name_utf16le = @as([*]u16, @ptrCast(&dir_info.FileName))[0 .. dir_info.FileNameLength / 2]; - - if (mem.eql(u16, name_utf16le, &[_]u16{'.'}) or mem.eql(u16, name_utf16le, &[_]u16{ '.', '.' })) - continue; - // Trust that Windows gives us valid UTF-16LE - const name_utf8_len = std.unicode.utf16leToUtf8(self.name_data[0..], name_utf16le) catch unreachable; - const name_utf8 = self.name_data[0..name_utf8_len]; - const kind: Entry.Kind = blk: { - const attrs = dir_info.FileAttributes; - if (attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk .directory; - if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk .sym_link; - break :blk .file; - }; - return Entry{ - .name = name_utf8, - .kind = kind, - }; - } - } - - pub fn reset(self: *Self) void { - self.index = 0; - self.end_index = 0; - self.first_iter = true; - } - }, - .wasi => struct { - dir: Dir, - buf: [1024]u8, // TODO align(@alignOf(os.wasi.dirent_t)), - cookie: u64, - index: usize, - end_index: usize, - - const Self = @This(); - - pub const Error = IteratorError; - - /// Memory such as file names referenced in this returned entry becomes invalid - /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. - pub fn next(self: *Self) Error!?Entry { - return self.nextWasi() catch |err| switch (err) { - // To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration. - // This matches the behavior of non-Linux UNIX platforms. - error.DirNotFound => null, - else => |e| return e, - }; - } - - pub const ErrorWasi = error{DirNotFound} || IteratorError; - - /// Implementation of `next` that can return platform-dependent errors depending on the host platform. - /// When the host platform is Linux, `error.DirNotFound` can be returned if the directory being - /// iterated was deleted during iteration. - pub fn nextWasi(self: *Self) ErrorWasi!?Entry { - // We intentinally use fd_readdir even when linked with libc, - // since its implementation is exactly the same as below, - // and we avoid the code complexity here. - const w = os.wasi; - start_over: while (true) { - // According to the WASI spec, the last entry might be truncated, - // so we need to check if the left buffer contains the whole dirent. - if (self.end_index - self.index < @sizeOf(w.dirent_t)) { - var bufused: usize = undefined; - switch (w.fd_readdir(self.dir.fd, &self.buf, self.buf.len, self.cookie, &bufused)) { - .SUCCESS => {}, - .BADF => unreachable, // Dir is invalid or was opened without iteration ability - .FAULT => unreachable, - .NOTDIR => unreachable, - .INVAL => unreachable, - .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. - .NOTCAPABLE => return error.AccessDenied, - else => |err| return os.unexpectedErrno(err), - } - if (bufused == 0) return null; - self.index = 0; - self.end_index = bufused; - } - const entry = @as(*align(1) w.dirent_t, @ptrCast(&self.buf[self.index])); - const entry_size = @sizeOf(w.dirent_t); - const name_index = self.index + entry_size; - if (name_index + entry.d_namlen > self.end_index) { - // This case, the name is truncated, so we need to call readdir to store the entire name. - self.end_index = self.index; // Force fd_readdir in the next loop. - continue :start_over; - } - const name = self.buf[name_index .. name_index + entry.d_namlen]; - - const next_index = name_index + entry.d_namlen; - self.index = next_index; - self.cookie = entry.d_next; - - // skip . and .. entries - if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { - continue :start_over; - } - - const entry_kind: Entry.Kind = switch (entry.d_type) { - .BLOCK_DEVICE => .block_device, - .CHARACTER_DEVICE => .character_device, - .DIRECTORY => .directory, - .SYMBOLIC_LINK => .sym_link, - .REGULAR_FILE => .file, - .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, - else => .unknown, - }; - return Entry{ - .name = name, - .kind = entry_kind, - }; - } - } - - pub fn reset(self: *Self) void { - self.index = 0; - self.end_index = 0; - self.cookie = os.wasi.DIRCOOKIE_START; - } - }, - else => @compileError("unimplemented"), - }; - - pub fn iterate(self: Dir) Iterator { - return self.iterateImpl(true); - } - - /// Like `iterate`, but will not reset the directory cursor before the first - /// iteration. This should only be used in cases where it is known that the - /// `Dir` has not had its cursor modified yet (e.g. it was just opened). - pub fn iterateAssumeFirstIteration(self: Dir) Iterator { - return self.iterateImpl(false); - } - - fn iterateImpl(self: Dir, first_iter_start_value: bool) Iterator { - switch (builtin.os.tag) { - .macos, - .ios, - .freebsd, - .netbsd, - .dragonfly, - .openbsd, - .solaris, - .illumos, - => return Iterator{ - .dir = self, - .seek = 0, - .index = 0, - .end_index = 0, - .buf = undefined, - .first_iter = first_iter_start_value, - }, - .linux, .haiku => return Iterator{ - .dir = self, - .index = 0, - .end_index = 0, - .buf = undefined, - .first_iter = first_iter_start_value, - }, - .windows => return Iterator{ - .dir = self, - .index = 0, - .end_index = 0, - .first_iter = first_iter_start_value, - .buf = undefined, - .name_data = undefined, - }, - .wasi => return Iterator{ - .dir = self, - .cookie = os.wasi.DIRCOOKIE_START, - .index = 0, - .end_index = 0, - .buf = undefined, - }, - else => @compileError("unimplemented"), - } - } - - pub const Walker = struct { - stack: std.ArrayList(StackItem), - name_buffer: std.ArrayList(u8), - - pub const WalkerEntry = struct { - /// The containing directory. This can be used to operate directly on `basename` - /// rather than `path`, avoiding `error.NameTooLong` for deeply nested paths. - /// The directory remains open until `next` or `deinit` is called. - dir: Dir, - basename: []const u8, - path: []const u8, - kind: Dir.Entry.Kind, - }; - - const StackItem = struct { - iter: Dir.Iterator, - dirname_len: usize, - }; - - /// After each call to this function, and on deinit(), the memory returned - /// from this function becomes invalid. A copy must be made in order to keep - /// a reference to the path. - pub fn next(self: *Walker) !?WalkerEntry { - while (self.stack.items.len != 0) { - // `top` and `containing` become invalid after appending to `self.stack` - var top = &self.stack.items[self.stack.items.len - 1]; - var containing = top; - var dirname_len = top.dirname_len; - if (top.iter.next() catch |err| { - // If we get an error, then we want the user to be able to continue - // walking if they want, which means that we need to pop the directory - // that errored from the stack. Otherwise, all future `next` calls would - // likely just fail with the same error. - var item = self.stack.pop(); - if (self.stack.items.len != 0) { - item.iter.dir.close(); - } - return err; - }) |base| { - self.name_buffer.shrinkRetainingCapacity(dirname_len); - if (self.name_buffer.items.len != 0) { - try self.name_buffer.append(path.sep); - dirname_len += 1; - } - try self.name_buffer.appendSlice(base.name); - if (base.kind == .directory) { - var new_dir = top.iter.dir.openDir(base.name, .{ .iterate = true }) catch |err| switch (err) { - error.NameTooLong => unreachable, // no path sep in base.name - else => |e| return e, - }; - { - errdefer new_dir.close(); - try self.stack.append(StackItem{ - .iter = new_dir.iterateAssumeFirstIteration(), - .dirname_len = self.name_buffer.items.len, - }); - top = &self.stack.items[self.stack.items.len - 1]; - containing = &self.stack.items[self.stack.items.len - 2]; - } - } - return WalkerEntry{ - .dir = containing.iter.dir, - .basename = self.name_buffer.items[dirname_len..], - .path = self.name_buffer.items, - .kind = base.kind, - }; - } else { - var item = self.stack.pop(); - if (self.stack.items.len != 0) { - item.iter.dir.close(); - } - } - } - return null; - } - - pub fn deinit(self: *Walker) void { - // Close any remaining directories except the initial one (which is always at index 0) - if (self.stack.items.len > 1) { - for (self.stack.items[1..]) |*item| { - item.iter.dir.close(); - } - } - self.stack.deinit(); - self.name_buffer.deinit(); - } - }; - - /// Recursively iterates over a directory. - /// `self` must have been opened with `OpenDirOptions{.iterate = true}`. - /// Must call `Walker.deinit` when done. - /// The order of returned file system entries is undefined. - /// `self` will not be closed after walking it. - pub fn walk(self: Dir, allocator: Allocator) !Walker { - var name_buffer = std.ArrayList(u8).init(allocator); - errdefer name_buffer.deinit(); - - var stack = std.ArrayList(Walker.StackItem).init(allocator); - errdefer stack.deinit(); - - try stack.append(Walker.StackItem{ - .iter = self.iterate(), - .dirname_len = 0, - }); - - return Walker{ - .stack = stack, - .name_buffer = name_buffer, - }; - } - - pub const OpenError = error{ - FileNotFound, - NotDir, - InvalidHandle, - AccessDenied, - SymLinkLoop, - ProcessFdQuotaExceeded, - NameTooLong, - SystemFdQuotaExceeded, - NoDevice, - SystemResources, - InvalidUtf8, - BadPathName, - DeviceBusy, - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - } || os.UnexpectedError; - - pub fn close(self: *Dir) void { - if (need_async_thread) { - std.event.Loop.instance.?.close(self.fd); - } else { - os.close(self.fd); - } - self.* = undefined; - } - - /// Opens a file for reading or writing, without attempting to create a new file. - /// To create a new file, see `createFile`. - /// Call `File.close` to release the resource. - /// Asserts that the path parameter has no null bytes. - pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { - if (builtin.os.tag == .windows) { - const path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.openFileW(path_w.span(), flags); - } - if (builtin.os.tag == .wasi and !builtin.link_libc) { - return self.openFileWasi(sub_path, flags); - } - const path_c = try os.toPosixPath(sub_path); - return self.openFileZ(&path_c, flags); - } - - /// Same as `openFile` but WASI only. - pub fn openFileWasi(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { - const w = os.wasi; - var fdflags: w.fdflags_t = 0x0; - var base: w.rights_t = 0x0; - if (flags.isRead()) { - base |= w.RIGHT.FD_READ | w.RIGHT.FD_TELL | w.RIGHT.FD_SEEK | w.RIGHT.FD_FILESTAT_GET; - } - if (flags.isWrite()) { - fdflags |= w.FDFLAG.APPEND; - base |= w.RIGHT.FD_WRITE | - w.RIGHT.FD_TELL | - w.RIGHT.FD_SEEK | - w.RIGHT.FD_DATASYNC | - w.RIGHT.FD_FDSTAT_SET_FLAGS | - w.RIGHT.FD_SYNC | - w.RIGHT.FD_ALLOCATE | - w.RIGHT.FD_ADVISE | - w.RIGHT.FD_FILESTAT_SET_TIMES | - w.RIGHT.FD_FILESTAT_SET_SIZE; - } - const fd = try os.openatWasi(self.fd, sub_path, 0x0, 0x0, fdflags, base, 0x0); - return File{ .handle = fd }; - } - - /// Same as `openFile` but the path parameter is null-terminated. - pub fn openFileZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File { - if (builtin.os.tag == .windows) { - const path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path); - return self.openFileW(path_w.span(), flags); - } - - var os_flags: u32 = 0; - if (@hasDecl(os.O, "CLOEXEC")) os_flags = os.O.CLOEXEC; - - // Use the O locking flags if the os supports them to acquire the lock - // atomically. - const has_flock_open_flags = @hasDecl(os.O, "EXLOCK"); - if (has_flock_open_flags) { - // Note that the O.NONBLOCK flag is removed after the openat() call - // is successful. - const nonblocking_lock_flag: u32 = if (flags.lock_nonblocking) - os.O.NONBLOCK - else - 0; - os_flags |= switch (flags.lock) { - .none => @as(u32, 0), - .shared => os.O.SHLOCK | nonblocking_lock_flag, - .exclusive => os.O.EXLOCK | nonblocking_lock_flag, - }; - } - if (@hasDecl(os.O, "LARGEFILE")) { - os_flags |= os.O.LARGEFILE; - } - if (@hasDecl(os.O, "NOCTTY") and !flags.allow_ctty) { - os_flags |= os.O.NOCTTY; - } - os_flags |= switch (flags.mode) { - .read_only => @as(u32, os.O.RDONLY), - .write_only => @as(u32, os.O.WRONLY), - .read_write => @as(u32, os.O.RDWR), - }; - const fd = if (flags.intended_io_mode != .blocking) - try std.event.Loop.instance.?.openatZ(self.fd, sub_path, os_flags, 0) - else - try os.openatZ(self.fd, sub_path, os_flags, 0); - errdefer os.close(fd); - - // WASI doesn't have os.flock so we intetinally check OS prior to the inner if block - // since it is not compiltime-known and we need to avoid undefined symbol in Wasm. - if (@hasDecl(os.system, "LOCK") and builtin.target.os.tag != .wasi) { - if (!has_flock_open_flags and flags.lock != .none) { - // TODO: integrate async I/O - const lock_nonblocking = if (flags.lock_nonblocking) os.LOCK.NB else @as(i32, 0); - try os.flock(fd, switch (flags.lock) { - .none => unreachable, - .shared => os.LOCK.SH | lock_nonblocking, - .exclusive => os.LOCK.EX | lock_nonblocking, - }); - } - } - - if (has_flock_open_flags and flags.lock_nonblocking) { - var fl_flags = os.fcntl(fd, os.F.GETFL, 0) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - fl_flags &= ~@as(usize, os.O.NONBLOCK); - _ = os.fcntl(fd, os.F.SETFL, fl_flags) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - } - - return File{ - .handle = fd, - .capable_io_mode = .blocking, - .intended_io_mode = flags.intended_io_mode, - }; - } - - /// Same as `openFile` but Windows-only and the path parameter is - /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. - pub fn openFileW(self: Dir, sub_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { - const w = os.windows; - const file: File = .{ - .handle = try w.OpenFile(sub_path_w, .{ - .dir = self.fd, - .access_mask = w.SYNCHRONIZE | - (if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) | - (if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0), - .creation = w.FILE_OPEN, - .io_mode = flags.intended_io_mode, - }), - .capable_io_mode = std.io.default_mode, - .intended_io_mode = flags.intended_io_mode, - }; - errdefer file.close(); - var io: w.IO_STATUS_BLOCK = undefined; - const range_off: w.LARGE_INTEGER = 0; - const range_len: w.LARGE_INTEGER = 1; - const exclusive = switch (flags.lock) { - .none => return file, - .shared => false, - .exclusive => true, - }; - try w.LockFile( - file.handle, - null, - null, - null, - &io, - &range_off, - &range_len, - null, - @intFromBool(flags.lock_nonblocking), - @intFromBool(exclusive), - ); - return file; - } - - /// Creates, opens, or overwrites a file with write access. - /// Call `File.close` on the result when done. - /// Asserts that the path parameter has no null bytes. - pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { - if (builtin.os.tag == .windows) { - const path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.createFileW(path_w.span(), flags); - } - if (builtin.os.tag == .wasi and !builtin.link_libc) { - return self.createFileWasi(sub_path, flags); - } - const path_c = try os.toPosixPath(sub_path); - return self.createFileZ(&path_c, flags); - } - - /// Same as `createFile` but WASI only. - pub fn createFileWasi(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { - const w = os.wasi; - var oflags = w.O.CREAT; - var base: w.rights_t = w.RIGHT.FD_WRITE | - w.RIGHT.FD_DATASYNC | - w.RIGHT.FD_SEEK | - w.RIGHT.FD_TELL | - w.RIGHT.FD_FDSTAT_SET_FLAGS | - w.RIGHT.FD_SYNC | - w.RIGHT.FD_ALLOCATE | - w.RIGHT.FD_ADVISE | - w.RIGHT.FD_FILESTAT_SET_TIMES | - w.RIGHT.FD_FILESTAT_SET_SIZE | - w.RIGHT.FD_FILESTAT_GET; - if (flags.read) { - base |= w.RIGHT.FD_READ; - } - if (flags.truncate) { - oflags |= w.O.TRUNC; - } - if (flags.exclusive) { - oflags |= w.O.EXCL; - } - const fd = try os.openatWasi(self.fd, sub_path, 0x0, oflags, 0x0, base, 0x0); - return File{ .handle = fd }; - } - - /// Same as `createFile` but the path parameter is null-terminated. - pub fn createFileZ(self: Dir, sub_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File { - if (builtin.os.tag == .windows) { - const path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); - return self.createFileW(path_w.span(), flags); - } - - // Use the O locking flags if the os supports them to acquire the lock - // atomically. - const has_flock_open_flags = @hasDecl(os.O, "EXLOCK"); - // Note that the O.NONBLOCK flag is removed after the openat() call - // is successful. - const nonblocking_lock_flag: u32 = if (has_flock_open_flags and flags.lock_nonblocking) - os.O.NONBLOCK - else - 0; - const lock_flag: u32 = if (has_flock_open_flags) switch (flags.lock) { - .none => @as(u32, 0), - .shared => os.O.SHLOCK | nonblocking_lock_flag, - .exclusive => os.O.EXLOCK | nonblocking_lock_flag, - } else 0; - - const O_LARGEFILE = if (@hasDecl(os.O, "LARGEFILE")) os.O.LARGEFILE else 0; - const os_flags = lock_flag | O_LARGEFILE | os.O.CREAT | os.O.CLOEXEC | - (if (flags.truncate) @as(u32, os.O.TRUNC) else 0) | - (if (flags.read) @as(u32, os.O.RDWR) else os.O.WRONLY) | - (if (flags.exclusive) @as(u32, os.O.EXCL) else 0); - const fd = if (flags.intended_io_mode != .blocking) - try std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, os_flags, flags.mode) - else - try os.openatZ(self.fd, sub_path_c, os_flags, flags.mode); - errdefer os.close(fd); - - // WASI doesn't have os.flock so we intetinally check OS prior to the inner if block - // since it is not compiltime-known and we need to avoid undefined symbol in Wasm. - if (builtin.target.os.tag != .wasi) { - if (!has_flock_open_flags and flags.lock != .none) { - // TODO: integrate async I/O - const lock_nonblocking = if (flags.lock_nonblocking) os.LOCK.NB else @as(i32, 0); - try os.flock(fd, switch (flags.lock) { - .none => unreachable, - .shared => os.LOCK.SH | lock_nonblocking, - .exclusive => os.LOCK.EX | lock_nonblocking, - }); - } - } - - if (has_flock_open_flags and flags.lock_nonblocking) { - var fl_flags = os.fcntl(fd, os.F.GETFL, 0) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - fl_flags &= ~@as(usize, os.O.NONBLOCK); - _ = os.fcntl(fd, os.F.SETFL, fl_flags) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - } - - return File{ - .handle = fd, - .capable_io_mode = .blocking, - .intended_io_mode = flags.intended_io_mode, - }; - } - - /// Same as `createFile` but Windows-only and the path parameter is - /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. - pub fn createFileW(self: Dir, sub_path_w: []const u16, flags: File.CreateFlags) File.OpenError!File { - const w = os.windows; - const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0; - const file: File = .{ - .handle = try os.windows.OpenFile(sub_path_w, .{ - .dir = self.fd, - .access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag, - .creation = if (flags.exclusive) - @as(u32, w.FILE_CREATE) - else if (flags.truncate) - @as(u32, w.FILE_OVERWRITE_IF) - else - @as(u32, w.FILE_OPEN_IF), - .io_mode = flags.intended_io_mode, - }), - .capable_io_mode = std.io.default_mode, - .intended_io_mode = flags.intended_io_mode, - }; - errdefer file.close(); - var io: w.IO_STATUS_BLOCK = undefined; - const range_off: w.LARGE_INTEGER = 0; - const range_len: w.LARGE_INTEGER = 1; - const exclusive = switch (flags.lock) { - .none => return file, - .shared => false, - .exclusive => true, - }; - try w.LockFile( - file.handle, - null, - null, - null, - &io, - &range_off, - &range_len, - null, - @intFromBool(flags.lock_nonblocking), - @intFromBool(exclusive), - ); - return file; - } - - /// Creates a single directory with a relative or absolute path. - /// To create multiple directories to make an entire path, see `makePath`. - /// To operate on only absolute paths, see `makeDirAbsolute`. - pub fn makeDir(self: Dir, sub_path: []const u8) !void { - try os.mkdirat(self.fd, sub_path, default_new_dir_mode); - } - - /// Creates a single directory with a relative or absolute null-terminated UTF-8-encoded path. - /// To create multiple directories to make an entire path, see `makePath`. - /// To operate on only absolute paths, see `makeDirAbsoluteZ`. - pub fn makeDirZ(self: Dir, sub_path: [*:0]const u8) !void { - try os.mkdiratZ(self.fd, sub_path, default_new_dir_mode); - } - - /// Creates a single directory with a relative or absolute null-terminated WTF-16-encoded path. - /// To create multiple directories to make an entire path, see `makePath`. - /// To operate on only absolute paths, see `makeDirAbsoluteW`. - pub fn makeDirW(self: Dir, sub_path: [*:0]const u16) !void { - try os.mkdiratW(self.fd, sub_path, default_new_dir_mode); - } - - /// Calls makeDir iteratively to make an entire path - /// (i.e. creating any parent directories that do not exist). - /// Returns success if the path already exists and is a directory. - /// This function is not atomic, and if it returns an error, the file system may - /// have been modified regardless. - pub fn makePath(self: Dir, sub_path: []const u8) !void { - var it = try path.componentIterator(sub_path); - var component = it.last() orelse return; - while (true) { - self.makeDir(component.path) catch |err| switch (err) { - error.PathAlreadyExists => { - // TODO stat the file and return an error if it's not a directory - // this is important because otherwise a dangling symlink - // could cause an infinite loop - }, - error.FileNotFound => |e| { - component = it.previous() orelse return e; - continue; - }, - else => |e| return e, - }; - component = it.next() orelse return; - } - } - - /// Calls makeOpenDirAccessMaskW iteratively to make an entire path - /// (i.e. creating any parent directories that do not exist). - /// Opens the dir if the path already exists and is a directory. - /// This function is not atomic, and if it returns an error, the file system may - /// have been modified regardless. - fn makeOpenPathAccessMaskW(self: Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) OpenError!Dir { - const w = os.windows; - var it = try path.componentIterator(sub_path); - // If there are no components in the path, then create a dummy component with the full path. - var component = it.last() orelse path.NativeUtf8ComponentIterator.Component{ - .name = "", - .path = sub_path, - }; - - while (true) { - const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path); - const is_last = it.peekNext() == null; - var result = self.makeOpenDirAccessMaskW(sub_path_w.span().ptr, access_mask, .{ - .no_follow = no_follow, - .create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE, - }) catch |err| switch (err) { - error.FileNotFound => |e| { - component = it.previous() orelse return e; - continue; - }, - else => |e| return e, - }; - - component = it.next() orelse return result; - // Don't leak the intermediate file handles - result.close(); - } - } - - /// This function performs `makePath`, followed by `openDir`. - /// If supported by the OS, this operation is atomic. It is not atomic on - /// all operating systems. - /// On Windows, this function performs `makeOpenPathAccessMaskW`. - pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !Dir { - return switch (builtin.os.tag) { - .windows => { - const w = os.windows; - const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | - w.SYNCHRONIZE | w.FILE_TRAVERSE | - (if (open_dir_options.iterate) w.FILE_LIST_DIRECTORY else 0); - - return self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow); - }, - else => { - return self.openDir(sub_path, open_dir_options) catch |err| switch (err) { - error.FileNotFound => { - try self.makePath(sub_path); - return self.openDir(sub_path, open_dir_options); - }, - else => |e| return e, - }; - }, - }; - } - - /// This function returns the canonicalized absolute pathname of - /// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this - /// `Dir` handle and returns the canonicalized absolute pathname of `pathname` - /// argument. - /// This function is not universally supported by all platforms. - /// Currently supported hosts are: Linux, macOS, and Windows. - /// See also `Dir.realpathZ`, `Dir.realpathW`, and `Dir.realpathAlloc`. - pub fn realpath(self: Dir, pathname: []const u8, out_buffer: []u8) ![]u8 { - if (builtin.os.tag == .wasi) { - @compileError("realpath is not available on WASI"); - } - if (builtin.os.tag == .windows) { - const pathname_w = try os.windows.sliceToPrefixedFileW(self.fd, pathname); - return self.realpathW(pathname_w.span(), out_buffer); - } - const pathname_c = try os.toPosixPath(pathname); - return self.realpathZ(&pathname_c, out_buffer); - } - - /// Same as `Dir.realpath` except `pathname` is null-terminated. - /// See also `Dir.realpath`, `realpathZ`. - pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) ![]u8 { - if (builtin.os.tag == .windows) { - const pathname_w = try os.windows.cStrToPrefixedFileW(self.fd, pathname); - return self.realpathW(pathname_w.span(), out_buffer); - } - - const flags = if (builtin.os.tag == .linux) os.O.PATH | os.O.NONBLOCK | os.O.CLOEXEC else os.O.NONBLOCK | os.O.CLOEXEC; - const fd = os.openatZ(self.fd, pathname, flags, 0) catch |err| switch (err) { - error.FileLocksNotSupported => unreachable, - else => |e| return e, - }; - defer os.close(fd); - - // Use of MAX_PATH_BYTES here is valid as the realpath function does not - // have a variant that takes an arbitrary-size buffer. - // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 - // NULL out parameter (GNU's canonicalize_file_name) to handle overelong - // paths. musl supports passing NULL but restricts the output to PATH_MAX - // anyway. - var buffer: [MAX_PATH_BYTES]u8 = undefined; - const out_path = try os.getFdPath(fd, &buffer); - - if (out_path.len > out_buffer.len) { - return error.NameTooLong; - } - - const result = out_buffer[0..out_path.len]; - @memcpy(result, out_path); - return result; - } - - /// Windows-only. Same as `Dir.realpath` except `pathname` is WTF16 encoded. - /// See also `Dir.realpath`, `realpathW`. - pub fn realpathW(self: Dir, pathname: []const u16, out_buffer: []u8) ![]u8 { - const w = os.windows; - - const access_mask = w.GENERIC_READ | w.SYNCHRONIZE; - const share_access = w.FILE_SHARE_READ; - const creation = w.FILE_OPEN; - const h_file = blk: { - const res = w.OpenFile(pathname, .{ - .dir = self.fd, - .access_mask = access_mask, - .share_access = share_access, - .creation = creation, - .io_mode = .blocking, - .filter = .any, - }) catch |err| switch (err) { - error.WouldBlock => unreachable, - else => |e| return e, - }; - break :blk res; - }; - defer w.CloseHandle(h_file); - - // Use of MAX_PATH_BYTES here is valid as the realpath function does not - // have a variant that takes an arbitrary-size buffer. - // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 - // NULL out parameter (GNU's canonicalize_file_name) to handle overelong - // paths. musl supports passing NULL but restricts the output to PATH_MAX - // anyway. - var buffer: [MAX_PATH_BYTES]u8 = undefined; - const out_path = try os.getFdPath(h_file, &buffer); - - if (out_path.len > out_buffer.len) { - return error.NameTooLong; - } - - const result = out_buffer[0..out_path.len]; - @memcpy(result, out_path); - return result; - } - - /// Same as `Dir.realpath` except caller must free the returned memory. - /// See also `Dir.realpath`. - pub fn realpathAlloc(self: Dir, allocator: Allocator, pathname: []const u8) ![]u8 { - // Use of MAX_PATH_BYTES here is valid as the realpath function does not - // have a variant that takes an arbitrary-size buffer. - // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 - // NULL out parameter (GNU's canonicalize_file_name) to handle overelong - // paths. musl supports passing NULL but restricts the output to PATH_MAX - // anyway. - var buf: [MAX_PATH_BYTES]u8 = undefined; - return allocator.dupe(u8, try self.realpath(pathname, buf[0..])); - } - - /// Changes the current working directory to the open directory handle. - /// This modifies global state and can have surprising effects in multi- - /// threaded applications. Most applications and especially libraries should - /// not call this function as a general rule, however it can have use cases - /// in, for example, implementing a shell, or child process execution. - /// Not all targets support this. For example, WASI does not have the concept - /// of a current working directory. - pub fn setAsCwd(self: Dir) !void { - if (builtin.os.tag == .wasi) { - @compileError("changing cwd is not currently possible in WASI"); - } - if (builtin.os.tag == .windows) { - var dir_path_buffer: [os.windows.PATH_MAX_WIDE]u16 = undefined; - const dir_path = try os.windows.GetFinalPathNameByHandle(self.fd, .{}, &dir_path_buffer); - if (builtin.link_libc) { - return os.chdirW(dir_path); - } - return os.windows.SetCurrentDirectory(dir_path); - } - try os.fchdir(self.fd); - } - - pub const OpenDirOptions = struct { - /// `true` means the opened directory can be used as the `Dir` parameter - /// for functions which operate based on an open directory handle. When `false`, - /// such operations are Illegal Behavior. - access_sub_paths: bool = true, - - /// `true` means the opened directory can be scanned for the files and sub-directories - /// of the result. It means the `iterate` function can be called. - iterate: bool = false, - - /// `true` means it won't dereference the symlinks. - no_follow: bool = false, - }; - - /// Opens a directory at the given path. The directory is a system resource that remains - /// open until `close` is called on the result. - /// The directory cannot be iterated unless the `iterate` option is set to `true`. - /// - /// Asserts that the path parameter has no null bytes. - pub fn openDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { - if (builtin.os.tag == .windows) { - const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.openDirW(sub_path_w.span().ptr, args); - } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - return self.openDirWasi(sub_path, args); - } else { - const sub_path_c = try os.toPosixPath(sub_path); - return self.openDirZ(&sub_path_c, args); - } - } - - /// Same as `openDir` except only WASI. - pub fn openDirWasi(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { - const w = os.wasi; - var base: w.rights_t = w.RIGHT.FD_FILESTAT_GET | w.RIGHT.FD_FDSTAT_SET_FLAGS | w.RIGHT.FD_FILESTAT_SET_TIMES; - if (args.access_sub_paths) { - base |= w.RIGHT.FD_READDIR | - w.RIGHT.PATH_CREATE_DIRECTORY | - w.RIGHT.PATH_CREATE_FILE | - w.RIGHT.PATH_LINK_SOURCE | - w.RIGHT.PATH_LINK_TARGET | - w.RIGHT.PATH_OPEN | - w.RIGHT.PATH_READLINK | - w.RIGHT.PATH_RENAME_SOURCE | - w.RIGHT.PATH_RENAME_TARGET | - w.RIGHT.PATH_FILESTAT_GET | - w.RIGHT.PATH_FILESTAT_SET_SIZE | - w.RIGHT.PATH_FILESTAT_SET_TIMES | - w.RIGHT.PATH_SYMLINK | - w.RIGHT.PATH_REMOVE_DIRECTORY | - w.RIGHT.PATH_UNLINK_FILE; - } - const symlink_flags: w.lookupflags_t = if (args.no_follow) 0x0 else w.LOOKUP_SYMLINK_FOLLOW; - // TODO do we really need all the rights here? - const inheriting: w.rights_t = w.RIGHT.ALL ^ w.RIGHT.SOCK_SHUTDOWN; - - const result = os.openatWasi( - self.fd, - sub_path, - symlink_flags, - w.O.DIRECTORY, - 0x0, - base, - inheriting, - ); - const fd = result catch |err| switch (err) { - error.FileTooBig => unreachable, // can't happen for directories - error.IsDir => unreachable, // we're providing O.DIRECTORY - error.NoSpaceLeft => unreachable, // not providing O.CREAT - error.PathAlreadyExists => unreachable, // not providing O.CREAT - error.FileLocksNotSupported => unreachable, // locking folders is not supported - error.WouldBlock => unreachable, // can't happen for directories - error.FileBusy => unreachable, // can't happen for directories - else => |e| return e, - }; - return Dir{ .fd = fd }; - } - - /// Same as `openDir` except the parameter is null-terminated. - pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenDirOptions) OpenError!Dir { - if (builtin.os.tag == .windows) { - const sub_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); - return self.openDirW(sub_path_w.span().ptr, args); - } - const symlink_flags: u32 = if (args.no_follow) os.O.NOFOLLOW else 0x0; - if (!args.iterate) { - const O_PATH = if (@hasDecl(os.O, "PATH")) os.O.PATH else 0; - return self.openDirFlagsZ(sub_path_c, os.O.DIRECTORY | os.O.RDONLY | os.O.CLOEXEC | O_PATH | symlink_flags); - } else { - return self.openDirFlagsZ(sub_path_c, os.O.DIRECTORY | os.O.RDONLY | os.O.CLOEXEC | symlink_flags); - } - } - - /// Same as `openDir` except the path parameter is WTF-16 encoded, NT-prefixed. - /// This function asserts the target OS is Windows. - pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenDirOptions) OpenError!Dir { - const w = os.windows; - // TODO remove some of these flags if args.access_sub_paths is false - const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | - w.SYNCHRONIZE | w.FILE_TRAVERSE; - const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; - const dir = try self.makeOpenDirAccessMaskW(sub_path_w, flags, .{ - .no_follow = args.no_follow, - .create_disposition = w.FILE_OPEN, - }); - return dir; - } - - /// `flags` must contain `os.O.DIRECTORY`. - fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: u32) OpenError!Dir { - const result = if (need_async_thread) - std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, flags, 0) - else - os.openatZ(self.fd, sub_path_c, flags, 0); - const fd = result catch |err| switch (err) { - error.FileTooBig => unreachable, // can't happen for directories - error.IsDir => unreachable, // we're providing O.DIRECTORY - error.NoSpaceLeft => unreachable, // not providing O.CREAT - error.PathAlreadyExists => unreachable, // not providing O.CREAT - error.FileLocksNotSupported => unreachable, // locking folders is not supported - error.WouldBlock => unreachable, // can't happen for directories - error.FileBusy => unreachable, // can't happen for directories - else => |e| return e, - }; - return Dir{ .fd = fd }; - } - - const MakeOpenDirAccessMaskWOptions = struct { - no_follow: bool, - create_disposition: u32, - }; - - fn makeOpenDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) OpenError!Dir { - const w = os.windows; - - var result = Dir{ - .fd = undefined, - }; - - const path_len_bytes = @as(u16, @intCast(mem.sliceTo(sub_path_w, 0).len * 2)); - var nt_name = w.UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - .Buffer = @constCast(sub_path_w), - }; - var attr = w.OBJECT_ATTRIBUTES{ - .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; - var io: w.IO_STATUS_BLOCK = undefined; - const rc = w.ntdll.NtCreateFile( - &result.fd, - access_mask, - &attr, - &io, - null, - w.FILE_ATTRIBUTE_NORMAL, - w.FILE_SHARE_READ | w.FILE_SHARE_WRITE, - flags.create_disposition, - w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, - null, - 0, - ); - - switch (rc) { - .SUCCESS => return result, - .OBJECT_NAME_INVALID => return error.BadPathName, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .NOT_A_DIRECTORY => return error.NotDir, - // This can happen if the directory has 'List folder contents' permission set to 'Deny' - // and the directory is trying to be opened for iteration. - .ACCESS_DENIED => return error.AccessDenied, - .INVALID_PARAMETER => unreachable, - else => return w.unexpectedStatus(rc), - } - } - - pub const DeleteFileError = os.UnlinkError; - - /// Delete a file name and possibly the file it refers to, based on an open directory handle. - /// Asserts that the path parameter has no null bytes. - pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void { - if (builtin.os.tag == .windows) { - const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.deleteFileW(sub_path_w.span()); - } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - os.unlinkat(self.fd, sub_path, 0) catch |err| switch (err) { - error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR - else => |e| return e, - }; - } else { - const sub_path_c = try os.toPosixPath(sub_path); - return self.deleteFileZ(&sub_path_c); - } - } - - /// Same as `deleteFile` except the parameter is null-terminated. - pub fn deleteFileZ(self: Dir, sub_path_c: [*:0]const u8) DeleteFileError!void { - os.unlinkatZ(self.fd, sub_path_c, 0) catch |err| switch (err) { - error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR - error.AccessDenied => |e| switch (builtin.os.tag) { - // non-Linux POSIX systems return EPERM when trying to delete a directory, so - // we need to handle that case specifically and translate the error - .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => { - // Don't follow symlinks to match unlinkat (which acts on symlinks rather than follows them) - const fstat = os.fstatatZ(self.fd, sub_path_c, os.AT.SYMLINK_NOFOLLOW) catch return e; - const is_dir = fstat.mode & os.S.IFMT == os.S.IFDIR; - return if (is_dir) error.IsDir else e; - }, - else => return e, - }, - else => |e| return e, - }; - } - - /// Same as `deleteFile` except the parameter is WTF-16 encoded. - pub fn deleteFileW(self: Dir, sub_path_w: []const u16) DeleteFileError!void { - os.unlinkatW(self.fd, sub_path_w, 0) catch |err| switch (err) { - error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR - else => |e| return e, - }; - } - - pub const DeleteDirError = error{ - DirNotEmpty, - FileNotFound, - AccessDenied, - FileBusy, - FileSystem, - SymLinkLoop, - NameTooLong, - NotDir, - SystemResources, - ReadOnlyFileSystem, - InvalidUtf8, - BadPathName, - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - Unexpected, - }; - - /// Returns `error.DirNotEmpty` if the directory is not empty. - /// To delete a directory recursively, see `deleteTree`. - /// Asserts that the path parameter has no null bytes. - pub fn deleteDir(self: Dir, sub_path: []const u8) DeleteDirError!void { - if (builtin.os.tag == .windows) { - const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.deleteDirW(sub_path_w.span()); - } else if (builtin.os.tag == .wasi and !builtin.link_libc) { - os.unlinkat(self.fd, sub_path, os.AT.REMOVEDIR) catch |err| switch (err) { - error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR - else => |e| return e, - }; - } else { - const sub_path_c = try os.toPosixPath(sub_path); - return self.deleteDirZ(&sub_path_c); - } - } - - /// Same as `deleteDir` except the parameter is null-terminated. - pub fn deleteDirZ(self: Dir, sub_path_c: [*:0]const u8) DeleteDirError!void { - os.unlinkatZ(self.fd, sub_path_c, os.AT.REMOVEDIR) catch |err| switch (err) { - error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR - else => |e| return e, - }; - } - - /// Same as `deleteDir` except the parameter is UTF16LE, NT prefixed. - /// This function is Windows-only. - pub fn deleteDirW(self: Dir, sub_path_w: []const u16) DeleteDirError!void { - os.unlinkatW(self.fd, sub_path_w, os.AT.REMOVEDIR) catch |err| switch (err) { - error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR - else => |e| return e, - }; - } - - pub const RenameError = os.RenameError; - - /// Change the name or location of a file or directory. - /// If new_sub_path already exists, it will be replaced. - /// Renaming a file over an existing directory or a directory - /// over an existing file will fail with `error.IsDir` or `error.NotDir` - pub fn rename(self: Dir, old_sub_path: []const u8, new_sub_path: []const u8) RenameError!void { - return os.renameat(self.fd, old_sub_path, self.fd, new_sub_path); - } - - /// Same as `rename` except the parameters are null-terminated. - pub fn renameZ(self: Dir, old_sub_path_z: [*:0]const u8, new_sub_path_z: [*:0]const u8) RenameError!void { - return os.renameatZ(self.fd, old_sub_path_z, self.fd, new_sub_path_z); - } - - /// Same as `rename` except the parameters are UTF16LE, NT prefixed. - /// This function is Windows-only. - pub fn renameW(self: Dir, old_sub_path_w: []const u16, new_sub_path_w: []const u16) RenameError!void { - return os.renameatW(self.fd, old_sub_path_w, self.fd, new_sub_path_w); - } - - /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. - /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent - /// one; the latter case is known as a dangling link. - /// If `sym_link_path` exists, it will not be overwritten. - pub fn symLink( - self: Dir, - target_path: []const u8, - sym_link_path: []const u8, - flags: SymLinkFlags, - ) !void { - if (builtin.os.tag == .wasi and !builtin.link_libc) { - return self.symLinkWasi(target_path, sym_link_path, flags); - } - if (builtin.os.tag == .windows) { - // Target path does not use sliceToPrefixedFileW because certain paths - // are handled differently when creating a symlink than they would be - // when converting to an NT namespaced path. CreateSymbolicLink in - // symLinkW will handle the necessary conversion. - var target_path_w: os.windows.PathSpace = undefined; - target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path); - target_path_w.data[target_path_w.len] = 0; - const sym_link_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sym_link_path); - return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); - } - const target_path_c = try os.toPosixPath(target_path); - const sym_link_path_c = try os.toPosixPath(sym_link_path); - return self.symLinkZ(&target_path_c, &sym_link_path_c, flags); - } - - /// WASI-only. Same as `symLink` except targeting WASI. - pub fn symLinkWasi( - self: Dir, - target_path: []const u8, - sym_link_path: []const u8, - _: SymLinkFlags, - ) !void { - return os.symlinkat(target_path, self.fd, sym_link_path); - } - - /// Same as `symLink`, except the pathname parameters are null-terminated. - pub fn symLinkZ( - self: Dir, - target_path_c: [*:0]const u8, - sym_link_path_c: [*:0]const u8, - flags: SymLinkFlags, - ) !void { - if (builtin.os.tag == .windows) { - const target_path_w = try os.windows.cStrToPrefixedFileW(self.fd, target_path_c); - const sym_link_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sym_link_path_c); - return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); - } - return os.symlinkatZ(target_path_c, self.fd, sym_link_path_c); - } - - /// Windows-only. Same as `symLink` except the pathname parameters - /// are null-terminated, WTF16 encoded. - pub fn symLinkW( - self: Dir, - /// WTF-16, does not need to be NT-prefixed. The NT-prefixing - /// of this path is handled by CreateSymbolicLink. - target_path_w: [:0]const u16, - /// WTF-16, must be NT-prefixed or relative - sym_link_path_w: []const u16, - flags: SymLinkFlags, - ) !void { - return os.windows.CreateSymbolicLink(self.fd, sym_link_path_w, target_path_w, flags.is_directory); - } - - pub const ReadLinkError = os.ReadLinkError; - - /// Read value of a symbolic link. - /// The return value is a slice of `buffer`, from index `0`. - /// Asserts that the path parameter has no null bytes. - pub fn readLink(self: Dir, sub_path: []const u8, buffer: []u8) ReadLinkError![]u8 { - if (builtin.os.tag == .wasi and !builtin.link_libc) { - return self.readLinkWasi(sub_path, buffer); - } - if (builtin.os.tag == .windows) { - const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.readLinkW(sub_path_w.span(), buffer); - } - const sub_path_c = try os.toPosixPath(sub_path); - return self.readLinkZ(&sub_path_c, buffer); - } - - /// WASI-only. Same as `readLink` except targeting WASI. - pub fn readLinkWasi(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 { - return os.readlinkat(self.fd, sub_path, buffer); - } - - /// Same as `readLink`, except the `pathname` parameter is null-terminated. - pub fn readLinkZ(self: Dir, sub_path_c: [*:0]const u8, buffer: []u8) ![]u8 { - if (builtin.os.tag == .windows) { - const sub_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); - return self.readLinkW(sub_path_w.span(), buffer); - } - return os.readlinkatZ(self.fd, sub_path_c, buffer); - } - - /// Windows-only. Same as `readLink` except the pathname parameter - /// is null-terminated, WTF16 encoded. - pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 { - return os.windows.ReadLink(self.fd, sub_path_w, buffer); - } - - /// Read all of file contents using a preallocated buffer. - /// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len` - /// the situation is ambiguous. It could either mean that the entire file was read, and - /// it exactly fits the buffer, or it could mean the buffer was not big enough for the - /// entire file. - pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 { - var file = try self.openFile(file_path, .{}); - defer file.close(); - - const end_index = try file.readAll(buffer); - return buffer[0..end_index]; - } - - /// On success, caller owns returned buffer. - /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. - pub fn readFileAlloc(self: Dir, allocator: mem.Allocator, file_path: []const u8, max_bytes: usize) ![]u8 { - return self.readFileAllocOptions(allocator, file_path, max_bytes, null, @alignOf(u8), null); - } - - /// On success, caller owns returned buffer. - /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. - /// If `size_hint` is specified the initial buffer size is calculated using - /// that value, otherwise the effective file size is used instead. - /// Allows specifying alignment and a sentinel value. - pub fn readFileAllocOptions( - self: Dir, - allocator: mem.Allocator, - file_path: []const u8, - max_bytes: usize, - size_hint: ?usize, - comptime alignment: u29, - comptime optional_sentinel: ?u8, - ) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) { - var file = try self.openFile(file_path, .{}); - defer file.close(); - - // If the file size doesn't fit a usize it'll be certainly greater than - // `max_bytes` - const stat_size = size_hint orelse math.cast(usize, try file.getEndPos()) orelse - return error.FileTooBig; - - return file.readToEndAllocOptions(allocator, max_bytes, stat_size, alignment, optional_sentinel); - } - - pub const DeleteTreeError = error{ - InvalidHandle, - AccessDenied, - FileTooBig, - SymLinkLoop, - ProcessFdQuotaExceeded, - NameTooLong, - SystemFdQuotaExceeded, - NoDevice, - SystemResources, - ReadOnlyFileSystem, - FileSystem, - FileBusy, - DeviceBusy, - - /// One of the path components was not a directory. - /// This error is unreachable if `sub_path` does not contain a path separator. - NotDir, - - /// On Windows, file paths must be valid Unicode. - InvalidUtf8, - - /// On Windows, file paths cannot contain these characters: - /// '/', '*', '?', '"', '<', '>', '|' - BadPathName, - - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - } || os.UnexpectedError; - - /// Whether `full_path` describes a symlink, file, or directory, this function - /// removes it. If it cannot be removed because it is a non-empty directory, - /// this function recursively removes its entries and then tries again. - /// This operation is not atomic on most file systems. - pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { - var initial_iterable_dir = (try self.deleteTreeOpenInitialSubpath(sub_path, .file)) orelse return; - - const StackItem = struct { - name: []const u8, - parent_dir: Dir, - iter: Dir.Iterator, - - fn closeAll(items: []@This()) void { - for (items) |*item| item.iter.dir.close(); - } - }; - - var stack_buffer: [16]StackItem = undefined; - var stack = std.ArrayListUnmanaged(StackItem).initBuffer(&stack_buffer); - defer StackItem.closeAll(stack.items); - - stack.appendAssumeCapacity(.{ - .name = sub_path, - .parent_dir = self, - .iter = initial_iterable_dir.iterateAssumeFirstIteration(), - }); - - process_stack: while (stack.items.len != 0) { - var top = &stack.items[stack.items.len - 1]; - while (try top.iter.next()) |entry| { - var treat_as_dir = entry.kind == .directory; - handle_entry: while (true) { - if (treat_as_dir) { - if (stack.unusedCapacitySlice().len >= 1) { - var iterable_dir = top.iter.dir.openDir(entry.name, .{ - .no_follow = true, - .iterate = true, - }) catch |err| switch (err) { - error.NotDir => { - treat_as_dir = false; - continue :handle_entry; - }, - error.FileNotFound => { - // That's fine, we were trying to remove this directory anyway. - break :handle_entry; - }, - - error.InvalidHandle, - error.AccessDenied, - error.SymLinkLoop, - error.ProcessFdQuotaExceeded, - error.NameTooLong, - error.SystemFdQuotaExceeded, - error.NoDevice, - error.SystemResources, - error.Unexpected, - error.InvalidUtf8, - error.BadPathName, - error.NetworkNotFound, - error.DeviceBusy, - => |e| return e, - }; - stack.appendAssumeCapacity(.{ - .name = entry.name, - .parent_dir = top.iter.dir, - .iter = iterable_dir.iterateAssumeFirstIteration(), - }); - continue :process_stack; - } else { - try top.iter.dir.deleteTreeMinStackSizeWithKindHint(entry.name, entry.kind); - break :handle_entry; - } - } else { - if (top.iter.dir.deleteFile(entry.name)) { - break :handle_entry; - } else |err| switch (err) { - error.FileNotFound => break :handle_entry, - - // Impossible because we do not pass any path separators. - error.NotDir => unreachable, - - error.IsDir => { - treat_as_dir = true; - continue :handle_entry; - }, - - error.AccessDenied, - error.InvalidUtf8, - error.SymLinkLoop, - error.NameTooLong, - error.SystemResources, - error.ReadOnlyFileSystem, - error.FileSystem, - error.FileBusy, - error.BadPathName, - error.NetworkNotFound, - error.Unexpected, - => |e| return e, - } - } - } - } - - // On Windows, we can't delete until the dir's handle has been closed, so - // close it before we try to delete. - top.iter.dir.close(); - - // In order to avoid double-closing the directory when cleaning up - // the stack in the case of an error, we save the relevant portions and - // pop the value from the stack. - const parent_dir = top.parent_dir; - const name = top.name; - stack.items.len -= 1; - - var need_to_retry: bool = false; - parent_dir.deleteDir(name) catch |err| switch (err) { - error.FileNotFound => {}, - error.DirNotEmpty => need_to_retry = true, - else => |e| return e, - }; - - if (need_to_retry) { - // Since we closed the handle that the previous iterator used, we - // need to re-open the dir and re-create the iterator. - var iterable_dir = iterable_dir: { - var treat_as_dir = true; - handle_entry: while (true) { - if (treat_as_dir) { - break :iterable_dir parent_dir.openDir(name, .{ - .no_follow = true, - .iterate = true, - }) catch |err| switch (err) { - error.NotDir => { - treat_as_dir = false; - continue :handle_entry; - }, - error.FileNotFound => { - // That's fine, we were trying to remove this directory anyway. - continue :process_stack; - }, - - error.InvalidHandle, - error.AccessDenied, - error.SymLinkLoop, - error.ProcessFdQuotaExceeded, - error.NameTooLong, - error.SystemFdQuotaExceeded, - error.NoDevice, - error.SystemResources, - error.Unexpected, - error.InvalidUtf8, - error.BadPathName, - error.NetworkNotFound, - error.DeviceBusy, - => |e| return e, - }; - } else { - if (parent_dir.deleteFile(name)) { - continue :process_stack; - } else |err| switch (err) { - error.FileNotFound => continue :process_stack, - - // Impossible because we do not pass any path separators. - error.NotDir => unreachable, - - error.IsDir => { - treat_as_dir = true; - continue :handle_entry; - }, - - error.AccessDenied, - error.InvalidUtf8, - error.SymLinkLoop, - error.NameTooLong, - error.SystemResources, - error.ReadOnlyFileSystem, - error.FileSystem, - error.FileBusy, - error.BadPathName, - error.NetworkNotFound, - error.Unexpected, - => |e| return e, - } - } - } - }; - // We know there is room on the stack since we are just re-adding - // the StackItem that we previously popped. - stack.appendAssumeCapacity(.{ - .name = name, - .parent_dir = parent_dir, - .iter = iterable_dir.iterateAssumeFirstIteration(), - }); - continue :process_stack; - } - } - } - - /// Like `deleteTree`, but only keeps one `Iterator` active at a time to minimize the function's stack size. - /// This is slower than `deleteTree` but uses less stack space. - pub fn deleteTreeMinStackSize(self: Dir, sub_path: []const u8) DeleteTreeError!void { - return self.deleteTreeMinStackSizeWithKindHint(sub_path, .file); - } - - fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint: File.Kind) DeleteTreeError!void { - start_over: while (true) { - var dir = (try self.deleteTreeOpenInitialSubpath(sub_path, kind_hint)) orelse return; - var cleanup_dir_parent: ?Dir = null; - defer if (cleanup_dir_parent) |*d| d.close(); - - var cleanup_dir = true; - defer if (cleanup_dir) dir.close(); - - // Valid use of MAX_PATH_BYTES because dir_name_buf will only - // ever store a single path component that was returned from the - // filesystem. - var dir_name_buf: [MAX_PATH_BYTES]u8 = undefined; - var dir_name: []const u8 = sub_path; - - // Here we must avoid recursion, in order to provide O(1) memory guarantee of this function. - // Go through each entry and if it is not a directory, delete it. If it is a directory, - // open it, and close the original directory. Repeat. Then start the entire operation over. - - scan_dir: while (true) { - var dir_it = dir.iterateAssumeFirstIteration(); - dir_it: while (try dir_it.next()) |entry| { - var treat_as_dir = entry.kind == .directory; - handle_entry: while (true) { - if (treat_as_dir) { - const new_dir = dir.openDir(entry.name, .{ - .no_follow = true, - .iterate = true, - }) catch |err| switch (err) { - error.NotDir => { - treat_as_dir = false; - continue :handle_entry; - }, - error.FileNotFound => { - // That's fine, we were trying to remove this directory anyway. - continue :dir_it; - }, - - error.InvalidHandle, - error.AccessDenied, - error.SymLinkLoop, - error.ProcessFdQuotaExceeded, - error.NameTooLong, - error.SystemFdQuotaExceeded, - error.NoDevice, - error.SystemResources, - error.Unexpected, - error.InvalidUtf8, - error.BadPathName, - error.NetworkNotFound, - error.DeviceBusy, - => |e| return e, - }; - if (cleanup_dir_parent) |*d| d.close(); - cleanup_dir_parent = dir; - dir = new_dir; - const result = dir_name_buf[0..entry.name.len]; - @memcpy(result, entry.name); - dir_name = result; - continue :scan_dir; - } else { - if (dir.deleteFile(entry.name)) { - continue :dir_it; - } else |err| switch (err) { - error.FileNotFound => continue :dir_it, - - // Impossible because we do not pass any path separators. - error.NotDir => unreachable, - - error.IsDir => { - treat_as_dir = true; - continue :handle_entry; - }, - - error.AccessDenied, - error.InvalidUtf8, - error.SymLinkLoop, - error.NameTooLong, - error.SystemResources, - error.ReadOnlyFileSystem, - error.FileSystem, - error.FileBusy, - error.BadPathName, - error.NetworkNotFound, - error.Unexpected, - => |e| return e, - } - } - } - } - // Reached the end of the directory entries, which means we successfully deleted all of them. - // Now to remove the directory itself. - dir.close(); - cleanup_dir = false; - - if (cleanup_dir_parent) |d| { - d.deleteDir(dir_name) catch |err| switch (err) { - // These two things can happen due to file system race conditions. - error.FileNotFound, error.DirNotEmpty => continue :start_over, - else => |e| return e, - }; - continue :start_over; - } else { - self.deleteDir(sub_path) catch |err| switch (err) { - error.FileNotFound => return, - error.DirNotEmpty => continue :start_over, - else => |e| return e, - }; - return; - } - } - } - } - - /// On successful delete, returns null. - fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File.Kind) !?Dir { - return iterable_dir: { - // Treat as a file by default - var treat_as_dir = kind_hint == .directory; - - handle_entry: while (true) { - if (treat_as_dir) { - break :iterable_dir self.openDir(sub_path, .{ - .no_follow = true, - .iterate = true, - }) catch |err| switch (err) { - error.NotDir => { - treat_as_dir = false; - continue :handle_entry; - }, - error.FileNotFound => { - // That's fine, we were trying to remove this directory anyway. - return null; - }, - - error.InvalidHandle, - error.AccessDenied, - error.SymLinkLoop, - error.ProcessFdQuotaExceeded, - error.NameTooLong, - error.SystemFdQuotaExceeded, - error.NoDevice, - error.SystemResources, - error.Unexpected, - error.InvalidUtf8, - error.BadPathName, - error.DeviceBusy, - error.NetworkNotFound, - => |e| return e, - }; - } else { - if (self.deleteFile(sub_path)) { - return null; - } else |err| switch (err) { - error.FileNotFound => return null, - - error.IsDir => { - treat_as_dir = true; - continue :handle_entry; - }, - - error.AccessDenied, - error.InvalidUtf8, - error.SymLinkLoop, - error.NameTooLong, - error.SystemResources, - error.ReadOnlyFileSystem, - error.NotDir, - error.FileSystem, - error.FileBusy, - error.BadPathName, - error.NetworkNotFound, - error.Unexpected, - => |e| return e, - } - } - } - }; - } - - pub const WriteFileError = File.WriteError || File.OpenError; - - /// Deprecated: use `writeFile2`. - pub fn writeFile(self: Dir, sub_path: []const u8, data: []const u8) WriteFileError!void { - return writeFile2(self, .{ - .sub_path = sub_path, - .data = data, - .flags = .{}, - }); - } - - pub const WriteFileOptions = struct { - sub_path: []const u8, - data: []const u8, - flags: File.CreateFlags = .{}, - }; - - /// Writes content to the file system, using the file creation flags provided. - pub fn writeFile2(self: Dir, options: WriteFileOptions) WriteFileError!void { - var file = try self.createFile(options.sub_path, options.flags); - defer file.close(); - try file.writeAll(options.data); - } - - pub const AccessError = os.AccessError; - - /// Test accessing `path`. - /// `path` is UTF-8-encoded. - /// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function. - /// For example, instead of testing if a file exists and then opening it, just - /// open it and handle the error for file not found. - pub fn access(self: Dir, sub_path: []const u8, flags: File.OpenFlags) AccessError!void { - if (builtin.os.tag == .windows) { - const sub_path_w = os.windows.sliceToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) { - error.AccessDenied => return error.PermissionDenied, - else => |e| return e, - }; - return self.accessW(sub_path_w.span().ptr, flags); - } - const path_c = try os.toPosixPath(sub_path); - return self.accessZ(&path_c, flags); - } - - /// Same as `access` except the path parameter is null-terminated. - pub fn accessZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) AccessError!void { - if (builtin.os.tag == .windows) { - const sub_path_w = os.windows.cStrToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) { - error.AccessDenied => return error.PermissionDenied, - else => |e| return e, - }; - return self.accessW(sub_path_w.span().ptr, flags); - } - const os_mode = switch (flags.mode) { - .read_only => @as(u32, os.F_OK), - .write_only => @as(u32, os.W_OK), - .read_write => @as(u32, os.R_OK | os.W_OK), - }; - const result = if (need_async_thread and flags.intended_io_mode != .blocking) - std.event.Loop.instance.?.faccessatZ(self.fd, sub_path, os_mode, 0) - else - os.faccessatZ(self.fd, sub_path, os_mode, 0); - return result; - } - - /// Same as `access` except asserts the target OS is Windows and the path parameter is - /// * WTF-16 encoded - /// * null-terminated - /// * NtDll prefixed - /// TODO currently this ignores `flags`. - pub fn accessW(self: Dir, sub_path_w: [*:0]const u16, flags: File.OpenFlags) AccessError!void { - _ = flags; - return os.faccessatW(self.fd, sub_path_w, 0, 0); - } - - /// Check the file size, mtime, and mode of `source_path` and `dest_path`. If they are equal, does nothing. - /// Otherwise, atomically copies `source_path` to `dest_path`. The destination file gains the mtime, - /// atime, and mode of the source file so that the next call to `updateFile` will not need a copy. - /// Returns the previous status of the file before updating. - /// If any of the directories do not exist for dest_path, they are created. - pub fn updateFile( - source_dir: Dir, - source_path: []const u8, - dest_dir: Dir, - dest_path: []const u8, - options: CopyFileOptions, - ) !PrevStatus { - var src_file = try source_dir.openFile(source_path, .{}); - defer src_file.close(); - - const src_stat = try src_file.stat(); - const actual_mode = options.override_mode orelse src_stat.mode; - check_dest_stat: { - const dest_stat = blk: { - var dest_file = dest_dir.openFile(dest_path, .{}) catch |err| switch (err) { - error.FileNotFound => break :check_dest_stat, - else => |e| return e, - }; - defer dest_file.close(); - - break :blk try dest_file.stat(); - }; - - if (src_stat.size == dest_stat.size and - src_stat.mtime == dest_stat.mtime and - actual_mode == dest_stat.mode) - { - return PrevStatus.fresh; - } - } - - if (path.dirname(dest_path)) |dirname| { - try dest_dir.makePath(dirname); - } - - var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = actual_mode }); - defer atomic_file.deinit(); - - try atomic_file.file.writeFileAll(src_file, .{ .in_len = src_stat.size }); - try atomic_file.file.updateTimes(src_stat.atime, src_stat.mtime); - try atomic_file.finish(); - return PrevStatus.stale; - } - - pub const CopyFileError = File.OpenError || File.StatError || AtomicFile.InitError || CopyFileRawError || AtomicFile.FinishError; - - /// Guaranteed to be atomic. - /// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and readily available, - /// there is a possibility of power loss or application termination leaving temporary files present - /// in the same directory as dest_path. - pub fn copyFile(source_dir: Dir, source_path: []const u8, dest_dir: Dir, dest_path: []const u8, options: CopyFileOptions) CopyFileError!void { - var in_file = try source_dir.openFile(source_path, .{}); - defer in_file.close(); - - var size: ?u64 = null; - const mode = options.override_mode orelse blk: { - const st = try in_file.stat(); - size = st.size; - break :blk st.mode; - }; - - var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = mode }); - defer atomic_file.deinit(); - - try copy_file(in_file.handle, atomic_file.file.handle, size); - try atomic_file.finish(); - } - - pub const AtomicFileOptions = struct { - mode: File.Mode = File.default_mode, - }; - - /// Directly access the `.file` field, and then call `AtomicFile.finish` - /// to atomically replace `dest_path` with contents. - /// Always call `AtomicFile.deinit` to clean up, regardless of whether `AtomicFile.finish` succeeded. - /// `dest_path` must remain valid until `AtomicFile.deinit` is called. - pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) !AtomicFile { - if (path.dirname(dest_path)) |dirname| { - const dir = try self.openDir(dirname, .{}); - return AtomicFile.init(path.basename(dest_path), options.mode, dir, true); - } else { - return AtomicFile.init(dest_path, options.mode, self, false); - } - } - - pub const Stat = File.Stat; - pub const StatError = File.StatError; - - pub fn stat(self: Dir) StatError!Stat { - const file: File = .{ - .handle = self.fd, - .capable_io_mode = .blocking, - }; - return file.stat(); - } - - pub const StatFileError = File.OpenError || File.StatError || os.FStatAtError; - - /// Returns metadata for a file inside the directory. - /// - /// On Windows, this requires three syscalls. On other operating systems, it - /// only takes one. - /// - /// Symlinks are followed. - /// - /// `sub_path` may be absolute, in which case `self` is ignored. - pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { - if (builtin.os.tag == .windows) { - var file = try self.openFile(sub_path, .{}); - defer file.close(); - return file.stat(); - } - if (builtin.os.tag == .wasi and !builtin.link_libc) { - const st = try os.fstatatWasi(self.fd, sub_path, os.wasi.LOOKUP_SYMLINK_FOLLOW); - return Stat.fromSystem(st); - } - const st = try os.fstatat(self.fd, sub_path, 0); - return Stat.fromSystem(st); - } - - pub const ChmodError = File.ChmodError; - - /// Changes the mode of the directory. - /// The process must have the correct privileges in order to do this - /// successfully, or must have the effective user ID matching the owner - /// of the directory. Additionally, the directory must have been opened - /// with `OpenDirOptions{ .iterate = true }`. - pub fn chmod(self: Dir, new_mode: File.Mode) ChmodError!void { - const file: File = .{ - .handle = self.fd, - .capable_io_mode = .blocking, - }; - try file.chmod(new_mode); - } - - /// Changes the owner and group of the directory. - /// The process must have the correct privileges in order to do this - /// successfully. The group may be changed by the owner of the directory to - /// any group of which the owner is a member. Additionally, the directory - /// must have been opened with `OpenDirOptions{ .iterate = true }`. If the - /// owner or group is specified as `null`, the ID is not changed. - pub fn chown(self: Dir, owner: ?File.Uid, group: ?File.Gid) ChownError!void { - const file: File = .{ - .handle = self.fd, - .capable_io_mode = .blocking, - }; - try file.chown(owner, group); - } - - pub const ChownError = File.ChownError; - - const Permissions = File.Permissions; - pub const SetPermissionsError = File.SetPermissionsError; - - /// Sets permissions according to the provided `Permissions` struct. - /// This method is *NOT* available on WASI - pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!void { - const file: File = .{ - .handle = self.fd, - .capable_io_mode = .blocking, - }; - try file.setPermissions(permissions); - } - - const Metadata = File.Metadata; - pub const MetadataError = File.MetadataError; - - /// Returns a `Metadata` struct, representing the permissions on the directory - pub fn metadata(self: Dir) MetadataError!Metadata { - const file: File = .{ - .handle = self.fd, - .capable_io_mode = .blocking, - }; - return try file.metadata(); - } -}; - /// Returns a handle to the current working directory. It is not opened with iteration capability. /// Closing the returned `Dir` is checked illegal behavior. Iterating over the result is illegal behavior. /// On POSIX targets, this function is comptime-callable. @@ -2918,20 +469,16 @@ pub fn readLinkAbsoluteZ(pathname_c: [*:0]const u8, buffer: *[MAX_PATH_BYTES]u8) return os.readlinkZ(pathname_c, buffer); } -/// Use with `Dir.symLink` and `symLinkAbsolute` to specify whether the symlink -/// will point to a file or a directory. This value is ignored on all hosts -/// except Windows where creating symlinks to different resource types, requires -/// different flags. By default, `symLinkAbsolute` is assumed to point to a file. -pub const SymLinkFlags = struct { - is_directory: bool = false, -}; - /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent /// one; the latter case is known as a dangling link. /// If `sym_link_path` exists, it will not be overwritten. /// See also `symLinkAbsoluteZ` and `symLinkAbsoluteW`. -pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags: SymLinkFlags) !void { +pub fn symLinkAbsolute( + target_path: []const u8, + sym_link_path: []const u8, + flags: Dir.SymLinkFlags, +) !void { assert(path.isAbsolute(target_path)); assert(path.isAbsolute(sym_link_path)); if (builtin.os.tag == .windows) { @@ -2946,7 +493,11 @@ pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags /// Note that this function will by default try creating a symbolic link to a file. If you would /// like to create a symbolic link to a directory, specify this with `SymLinkFlags{ .is_directory = true }`. /// See also `symLinkAbsolute`, `symLinkAbsoluteZ`. -pub fn symLinkAbsoluteW(target_path_w: []const u16, sym_link_path_w: []const u16, flags: SymLinkFlags) !void { +pub fn symLinkAbsoluteW( + target_path_w: []const u16, + sym_link_path_w: []const u16, + flags: Dir.SymLinkFlags, +) !void { assert(path.isAbsoluteWindowsWTF16(target_path_w)); assert(path.isAbsoluteWindowsWTF16(sym_link_path_w)); return os.windows.CreateSymbolicLink(null, sym_link_path_w, target_path_w, flags.is_directory); @@ -2954,7 +505,11 @@ pub fn symLinkAbsoluteW(target_path_w: []const u16, sym_link_path_w: []const u16 /// Same as `symLinkAbsolute` except the parameters are null-terminated pointers. /// See also `symLinkAbsolute`. -pub fn symLinkAbsoluteZ(target_path_c: [*:0]const u8, sym_link_path_c: [*:0]const u8, flags: SymLinkFlags) !void { +pub fn symLinkAbsoluteZ( + target_path_c: [*:0]const u8, + sym_link_path_c: [*:0]const u8, + flags: Dir.SymLinkFlags, +) !void { assert(path.isAbsoluteZ(target_path_c)); assert(path.isAbsoluteZ(sym_link_path_c)); if (builtin.os.tag == .windows) { @@ -3151,59 +706,6 @@ pub fn realpathAlloc(allocator: Allocator, pathname: []const u8) ![]u8 { return allocator.dupe(u8, try os.realpath(pathname, &buf)); } -const CopyFileRawError = error{SystemResources} || os.CopyFileRangeError || os.SendFileError; - -// Transfer all the data between two file descriptors in the most efficient way. -// The copy starts at offset 0, the initial offsets are preserved. -// No metadata is transferred over. -fn copy_file(fd_in: os.fd_t, fd_out: os.fd_t, maybe_size: ?u64) CopyFileRawError!void { - if (comptime builtin.target.isDarwin()) { - const rc = os.system.fcopyfile(fd_in, fd_out, null, os.system.COPYFILE_DATA); - switch (os.errno(rc)) { - .SUCCESS => return, - .INVAL => unreachable, - .NOMEM => return error.SystemResources, - // The source file is not a directory, symbolic link, or regular file. - // Try with the fallback path before giving up. - .OPNOTSUPP => {}, - else => |err| return os.unexpectedErrno(err), - } - } - - if (builtin.os.tag == .linux) { - // Try copy_file_range first as that works at the FS level and is the - // most efficient method (if available). - var offset: u64 = 0; - cfr_loop: while (true) { - // The kernel checks the u64 value `offset+count` for overflow, use - // a 32 bit value so that the syscall won't return EINVAL except for - // impossibly large files (> 2^64-1 - 2^32-1). - const amt = try os.copy_file_range(fd_in, offset, fd_out, offset, math.maxInt(u32), 0); - // Terminate as soon as we have copied size bytes or no bytes - if (maybe_size) |s| { - if (s == amt) break :cfr_loop; - } - if (amt == 0) break :cfr_loop; - offset += amt; - } - return; - } - - // Sendfile is a zero-copy mechanism iff the OS supports it, otherwise the - // fallback code will copy the contents chunk by chunk. - const empty_iovec = [0]os.iovec_const{}; - var offset: u64 = 0; - sendfile_loop: while (true) { - const amt = try os.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0); - // Terminate as soon as we have copied size bytes or no bytes - if (maybe_size) |s| { - if (s == amt) break :sendfile_loop; - } - if (amt == 0) break :sendfile_loop; - offset += amt; - } -} - test { if (builtin.os.tag != .wasi) { _ = &makeDirAbsolute; @@ -3211,10 +713,10 @@ test { _ = ©FileAbsolute; _ = &updateFileAbsolute; } - _ = &Dir.copyFile; + _ = &File; + _ = &Dir; + _ = &path; _ = @import("fs/test.zig"); - _ = @import("fs/path.zig"); - _ = @import("fs/file.zig"); _ = @import("fs/get_app_data_dir.zig"); _ = @import("fs/watch.zig"); } diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig new file mode 100644 index 0000000000..1d59615c07 --- /dev/null +++ b/lib/std/fs/Dir.zig @@ -0,0 +1,2533 @@ +fd: posix.fd_t, + +pub const default_mode = 0o755; + +pub const Entry = struct { + name: []const u8, + kind: Kind, + + pub const Kind = File.Kind; +}; + +const IteratorError = error{ AccessDenied, SystemResources } || posix.UnexpectedError; + +pub const Iterator = switch (builtin.os.tag) { + .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => struct { + dir: Dir, + seek: i64, + buf: [1024]u8, // TODO align(@alignOf(posix.system.dirent)), + index: usize, + end_index: usize, + first_iter: bool, + + const Self = @This(); + + pub const Error = IteratorError; + + /// Memory such as file names referenced in this returned entry becomes invalid + /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. + pub fn next(self: *Self) Error!?Entry { + switch (builtin.os.tag) { + .macos, .ios => return self.nextDarwin(), + .freebsd, .netbsd, .dragonfly, .openbsd => return self.nextBsd(), + .solaris, .illumos => return self.nextSolaris(), + else => @compileError("unimplemented"), + } + } + + fn nextDarwin(self: *Self) !?Entry { + start_over: while (true) { + if (self.index >= self.end_index) { + if (self.first_iter) { + posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions + self.first_iter = false; + } + const rc = posix.system.__getdirentries64( + self.dir.fd, + &self.buf, + self.buf.len, + &self.seek, + ); + if (rc == 0) return null; + if (rc < 0) { + switch (posix.errno(rc)) { + .BADF => unreachable, // Dir is invalid or was opened without iteration ability + .FAULT => unreachable, + .NOTDIR => unreachable, + .INVAL => unreachable, + else => |err| return posix.unexpectedErrno(err), + } + } + self.index = 0; + self.end_index = @as(usize, @intCast(rc)); + } + const darwin_entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index])); + const next_index = self.index + darwin_entry.reclen(); + self.index = next_index; + + const name = @as([*]u8, @ptrCast(&darwin_entry.d_name))[0..darwin_entry.d_namlen]; + + if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (darwin_entry.d_ino == 0)) { + continue :start_over; + } + + const entry_kind: Entry.Kind = switch (darwin_entry.d_type) { + posix.DT.BLK => .block_device, + posix.DT.CHR => .character_device, + posix.DT.DIR => .directory, + posix.DT.FIFO => .named_pipe, + posix.DT.LNK => .sym_link, + posix.DT.REG => .file, + posix.DT.SOCK => .unix_domain_socket, + posix.DT.WHT => .whiteout, + else => .unknown, + }; + return Entry{ + .name = name, + .kind = entry_kind, + }; + } + } + + fn nextSolaris(self: *Self) !?Entry { + start_over: while (true) { + if (self.index >= self.end_index) { + if (self.first_iter) { + posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions + self.first_iter = false; + } + const rc = posix.system.getdents(self.dir.fd, &self.buf, self.buf.len); + switch (posix.errno(rc)) { + .SUCCESS => {}, + .BADF => unreachable, // Dir is invalid or was opened without iteration ability + .FAULT => unreachable, + .NOTDIR => unreachable, + .INVAL => unreachable, + else => |err| return posix.unexpectedErrno(err), + } + if (rc == 0) return null; + self.index = 0; + self.end_index = @as(usize, @intCast(rc)); + } + const entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index])); + const next_index = self.index + entry.reclen(); + self.index = next_index; + + const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&entry.d_name)), 0); + if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) + continue :start_over; + + // Solaris dirent doesn't expose d_type, so we have to call stat to get it. + const stat_info = posix.fstatat( + self.dir.fd, + name, + posix.AT.SYMLINK_NOFOLLOW, + ) catch |err| switch (err) { + error.NameTooLong => unreachable, + error.SymLinkLoop => unreachable, + error.FileNotFound => unreachable, // lost the race + else => |e| return e, + }; + const entry_kind: Entry.Kind = switch (stat_info.mode & posix.S.IFMT) { + posix.S.IFIFO => .named_pipe, + posix.S.IFCHR => .character_device, + posix.S.IFDIR => .directory, + posix.S.IFBLK => .block_device, + posix.S.IFREG => .file, + posix.S.IFLNK => .sym_link, + posix.S.IFSOCK => .unix_domain_socket, + posix.S.IFDOOR => .door, + posix.S.IFPORT => .event_port, + else => .unknown, + }; + return Entry{ + .name = name, + .kind = entry_kind, + }; + } + } + + fn nextBsd(self: *Self) !?Entry { + start_over: while (true) { + if (self.index >= self.end_index) { + if (self.first_iter) { + posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions + self.first_iter = false; + } + const rc = if (builtin.os.tag == .netbsd) + posix.system.__getdents30(self.dir.fd, &self.buf, self.buf.len) + else + posix.system.getdents(self.dir.fd, &self.buf, self.buf.len); + switch (posix.errno(rc)) { + .SUCCESS => {}, + .BADF => unreachable, // Dir is invalid or was opened without iteration ability + .FAULT => unreachable, + .NOTDIR => unreachable, + .INVAL => unreachable, + // Introduced in freebsd 13.2: directory unlinked but still open. + // To be consistent, iteration ends if the directory being iterated is deleted during iteration. + .NOENT => return null, + else => |err| return posix.unexpectedErrno(err), + } + if (rc == 0) return null; + self.index = 0; + self.end_index = @as(usize, @intCast(rc)); + } + const bsd_entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index])); + const next_index = self.index + bsd_entry.reclen(); + self.index = next_index; + + const name = @as([*]u8, @ptrCast(&bsd_entry.d_name))[0..bsd_entry.d_namlen]; + + const skip_zero_fileno = switch (builtin.os.tag) { + // d_fileno=0 is used to mark invalid entries or deleted files. + .openbsd, .netbsd => true, + else => false, + }; + if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or + (skip_zero_fileno and bsd_entry.d_fileno == 0)) + { + continue :start_over; + } + + const entry_kind: Entry.Kind = switch (bsd_entry.d_type) { + posix.DT.BLK => .block_device, + posix.DT.CHR => .character_device, + posix.DT.DIR => .directory, + posix.DT.FIFO => .named_pipe, + posix.DT.LNK => .sym_link, + posix.DT.REG => .file, + posix.DT.SOCK => .unix_domain_socket, + posix.DT.WHT => .whiteout, + else => .unknown, + }; + return Entry{ + .name = name, + .kind = entry_kind, + }; + } + } + + pub fn reset(self: *Self) void { + self.index = 0; + self.end_index = 0; + self.first_iter = true; + } + }, + .haiku => struct { + dir: Dir, + buf: [1024]u8, // TODO align(@alignOf(posix.dirent64)), + index: usize, + end_index: usize, + first_iter: bool, + + const Self = @This(); + + pub const Error = IteratorError; + + /// Memory such as file names referenced in this returned entry becomes invalid + /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. + pub fn next(self: *Self) Error!?Entry { + start_over: while (true) { + // TODO: find a better max + const HAIKU_MAX_COUNT = 10000; + if (self.index >= self.end_index) { + if (self.first_iter) { + posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions + self.first_iter = false; + } + const rc = posix.system._kern_read_dir( + self.dir.fd, + &self.buf, + self.buf.len, + HAIKU_MAX_COUNT, + ); + if (rc == 0) return null; + if (rc < 0) { + switch (posix.errno(rc)) { + .BADF => unreachable, // Dir is invalid or was opened without iteration ability + .FAULT => unreachable, + .NOTDIR => unreachable, + .INVAL => unreachable, + else => |err| return posix.unexpectedErrno(err), + } + } + self.index = 0; + self.end_index = @as(usize, @intCast(rc)); + } + const haiku_entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index])); + const next_index = self.index + haiku_entry.reclen(); + self.index = next_index; + const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&haiku_entry.d_name)), 0); + + if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (haiku_entry.d_ino == 0)) { + continue :start_over; + } + + var stat_info: posix.Stat = undefined; + const rc = posix.system._kern_read_stat( + self.dir.fd, + &haiku_entry.d_name, + false, + &stat_info, + 0, + ); + if (rc != 0) { + switch (posix.errno(rc)) { + .SUCCESS => {}, + .BADF => unreachable, // Dir is invalid or was opened without iteration ability + .FAULT => unreachable, + .NOTDIR => unreachable, + .INVAL => unreachable, + else => |err| return posix.unexpectedErrno(err), + } + } + const statmode = stat_info.mode & posix.S.IFMT; + + const entry_kind: Entry.Kind = switch (statmode) { + posix.S.IFDIR => .directory, + posix.S.IFBLK => .block_device, + posix.S.IFCHR => .character_device, + posix.S.IFLNK => .sym_link, + posix.S.IFREG => .file, + posix.S.IFIFO => .named_pipe, + else => .unknown, + }; + + return Entry{ + .name = name, + .kind = entry_kind, + }; + } + } + + pub fn reset(self: *Self) void { + self.index = 0; + self.end_index = 0; + self.first_iter = true; + } + }, + .linux => struct { + dir: Dir, + // The if guard is solely there to prevent compile errors from missing `linux.dirent64` + // definition when compiling for other OSes. It doesn't do anything when compiling for Linux. + buf: [1024]u8 align(if (builtin.os.tag != .linux) 1 else @alignOf(linux.dirent64)), + index: usize, + end_index: usize, + first_iter: bool, + + const Self = @This(); + const linux = std.os.linux; + + pub const Error = IteratorError; + + /// Memory such as file names referenced in this returned entry becomes invalid + /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. + pub fn next(self: *Self) Error!?Entry { + return self.nextLinux() catch |err| switch (err) { + // To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration. + // This matches the behavior of non-Linux UNIX platforms. + error.DirNotFound => null, + else => |e| return e, + }; + } + + pub const ErrorLinux = error{DirNotFound} || IteratorError; + + /// Implementation of `next` that can return `error.DirNotFound` if the directory being + /// iterated was deleted during iteration (this error is Linux specific). + pub fn nextLinux(self: *Self) ErrorLinux!?Entry { + start_over: while (true) { + if (self.index >= self.end_index) { + if (self.first_iter) { + posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions + self.first_iter = false; + } + const rc = linux.getdents64(self.dir.fd, &self.buf, self.buf.len); + switch (linux.getErrno(rc)) { + .SUCCESS => {}, + .BADF => unreachable, // Dir is invalid or was opened without iteration ability + .FAULT => unreachable, + .NOTDIR => unreachable, + .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. + .INVAL => return error.Unexpected, // Linux may in some cases return EINVAL when reading /proc/$PID/net. + .ACCES => return error.AccessDenied, // Do not have permission to iterate this directory. + else => |err| return posix.unexpectedErrno(err), + } + if (rc == 0) return null; + self.index = 0; + self.end_index = rc; + } + const linux_entry = @as(*align(1) linux.dirent64, @ptrCast(&self.buf[self.index])); + const next_index = self.index + linux_entry.reclen(); + self.index = next_index; + + const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&linux_entry.d_name)), 0); + + // skip . and .. entries + if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { + continue :start_over; + } + + const entry_kind: Entry.Kind = switch (linux_entry.d_type) { + linux.DT.BLK => .block_device, + linux.DT.CHR => .character_device, + linux.DT.DIR => .directory, + linux.DT.FIFO => .named_pipe, + linux.DT.LNK => .sym_link, + linux.DT.REG => .file, + linux.DT.SOCK => .unix_domain_socket, + else => .unknown, + }; + return Entry{ + .name = name, + .kind = entry_kind, + }; + } + } + + pub fn reset(self: *Self) void { + self.index = 0; + self.end_index = 0; + self.first_iter = true; + } + }, + .windows => struct { + dir: Dir, + buf: [1024]u8 align(@alignOf(std.os.windows.FILE_BOTH_DIR_INFORMATION)), + index: usize, + end_index: usize, + first_iter: bool, + name_data: [fs.MAX_NAME_BYTES]u8, + + const Self = @This(); + + pub const Error = IteratorError; + + /// Memory such as file names referenced in this returned entry becomes invalid + /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. + pub fn next(self: *Self) Error!?Entry { + while (true) { + const w = std.os.windows; + if (self.index >= self.end_index) { + var io: w.IO_STATUS_BLOCK = undefined; + const rc = w.ntdll.NtQueryDirectoryFile( + self.dir.fd, + null, + null, + null, + &io, + &self.buf, + self.buf.len, + .FileBothDirectoryInformation, + w.FALSE, + null, + if (self.first_iter) @as(w.BOOLEAN, w.TRUE) else @as(w.BOOLEAN, w.FALSE), + ); + self.first_iter = false; + if (io.Information == 0) return null; + self.index = 0; + self.end_index = io.Information; + switch (rc) { + .SUCCESS => {}, + .ACCESS_DENIED => return error.AccessDenied, // Double-check that the Dir was opened with iteration ability + + else => return w.unexpectedStatus(rc), + } + } + + // While the official api docs guarantee FILE_BOTH_DIR_INFORMATION to be aligned properly + // this may not always be the case (e.g. due to faulty VM/Sandboxing tools) + const dir_info: *align(2) w.FILE_BOTH_DIR_INFORMATION = @ptrCast(@alignCast(&self.buf[self.index])); + if (dir_info.NextEntryOffset != 0) { + self.index += dir_info.NextEntryOffset; + } else { + self.index = self.buf.len; + } + + const name_utf16le = @as([*]u16, @ptrCast(&dir_info.FileName))[0 .. dir_info.FileNameLength / 2]; + + if (mem.eql(u16, name_utf16le, &[_]u16{'.'}) or mem.eql(u16, name_utf16le, &[_]u16{ '.', '.' })) + continue; + // Trust that Windows gives us valid UTF-16LE + const name_utf8_len = std.unicode.utf16leToUtf8(self.name_data[0..], name_utf16le) catch unreachable; + const name_utf8 = self.name_data[0..name_utf8_len]; + const kind: Entry.Kind = blk: { + const attrs = dir_info.FileAttributes; + if (attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk .directory; + if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk .sym_link; + break :blk .file; + }; + return Entry{ + .name = name_utf8, + .kind = kind, + }; + } + } + + pub fn reset(self: *Self) void { + self.index = 0; + self.end_index = 0; + self.first_iter = true; + } + }, + .wasi => struct { + dir: Dir, + buf: [1024]u8, // TODO align(@alignOf(posix.wasi.dirent_t)), + cookie: u64, + index: usize, + end_index: usize, + + const Self = @This(); + + pub const Error = IteratorError; + + /// Memory such as file names referenced in this returned entry becomes invalid + /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. + pub fn next(self: *Self) Error!?Entry { + return self.nextWasi() catch |err| switch (err) { + // To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration. + // This matches the behavior of non-Linux UNIX platforms. + error.DirNotFound => null, + else => |e| return e, + }; + } + + pub const ErrorWasi = error{DirNotFound} || IteratorError; + + /// Implementation of `next` that can return platform-dependent errors depending on the host platform. + /// When the host platform is Linux, `error.DirNotFound` can be returned if the directory being + /// iterated was deleted during iteration. + pub fn nextWasi(self: *Self) ErrorWasi!?Entry { + // We intentinally use fd_readdir even when linked with libc, + // since its implementation is exactly the same as below, + // and we avoid the code complexity here. + const w = std.os.wasi; + start_over: while (true) { + // According to the WASI spec, the last entry might be truncated, + // so we need to check if the left buffer contains the whole dirent. + if (self.end_index - self.index < @sizeOf(w.dirent_t)) { + var bufused: usize = undefined; + switch (w.fd_readdir(self.dir.fd, &self.buf, self.buf.len, self.cookie, &bufused)) { + .SUCCESS => {}, + .BADF => unreachable, // Dir is invalid or was opened without iteration ability + .FAULT => unreachable, + .NOTDIR => unreachable, + .INVAL => unreachable, + .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. + .NOTCAPABLE => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + if (bufused == 0) return null; + self.index = 0; + self.end_index = bufused; + } + const entry = @as(*align(1) w.dirent_t, @ptrCast(&self.buf[self.index])); + const entry_size = @sizeOf(w.dirent_t); + const name_index = self.index + entry_size; + if (name_index + entry.d_namlen > self.end_index) { + // This case, the name is truncated, so we need to call readdir to store the entire name. + self.end_index = self.index; // Force fd_readdir in the next loop. + continue :start_over; + } + const name = self.buf[name_index .. name_index + entry.d_namlen]; + + const next_index = name_index + entry.d_namlen; + self.index = next_index; + self.cookie = entry.d_next; + + // skip . and .. entries + if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { + continue :start_over; + } + + const entry_kind: Entry.Kind = switch (entry.d_type) { + .BLOCK_DEVICE => .block_device, + .CHARACTER_DEVICE => .character_device, + .DIRECTORY => .directory, + .SYMBOLIC_LINK => .sym_link, + .REGULAR_FILE => .file, + .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, + else => .unknown, + }; + return Entry{ + .name = name, + .kind = entry_kind, + }; + } + } + + pub fn reset(self: *Self) void { + self.index = 0; + self.end_index = 0; + self.cookie = std.os.wasi.DIRCOOKIE_START; + } + }, + else => @compileError("unimplemented"), +}; + +pub fn iterate(self: Dir) Iterator { + return self.iterateImpl(true); +} + +/// Like `iterate`, but will not reset the directory cursor before the first +/// iteration. This should only be used in cases where it is known that the +/// `Dir` has not had its cursor modified yet (e.g. it was just opened). +pub fn iterateAssumeFirstIteration(self: Dir) Iterator { + return self.iterateImpl(false); +} + +fn iterateImpl(self: Dir, first_iter_start_value: bool) Iterator { + switch (builtin.os.tag) { + .macos, + .ios, + .freebsd, + .netbsd, + .dragonfly, + .openbsd, + .solaris, + .illumos, + => return Iterator{ + .dir = self, + .seek = 0, + .index = 0, + .end_index = 0, + .buf = undefined, + .first_iter = first_iter_start_value, + }, + .linux, .haiku => return Iterator{ + .dir = self, + .index = 0, + .end_index = 0, + .buf = undefined, + .first_iter = first_iter_start_value, + }, + .windows => return Iterator{ + .dir = self, + .index = 0, + .end_index = 0, + .first_iter = first_iter_start_value, + .buf = undefined, + .name_data = undefined, + }, + .wasi => return Iterator{ + .dir = self, + .cookie = std.os.wasi.DIRCOOKIE_START, + .index = 0, + .end_index = 0, + .buf = undefined, + }, + else => @compileError("unimplemented"), + } +} + +pub const Walker = struct { + stack: std.ArrayList(StackItem), + name_buffer: std.ArrayList(u8), + + pub const WalkerEntry = struct { + /// The containing directory. This can be used to operate directly on `basename` + /// rather than `path`, avoiding `error.NameTooLong` for deeply nested paths. + /// The directory remains open until `next` or `deinit` is called. + dir: Dir, + basename: []const u8, + path: []const u8, + kind: Dir.Entry.Kind, + }; + + const StackItem = struct { + iter: Dir.Iterator, + dirname_len: usize, + }; + + /// After each call to this function, and on deinit(), the memory returned + /// from this function becomes invalid. A copy must be made in order to keep + /// a reference to the path. + pub fn next(self: *Walker) !?WalkerEntry { + while (self.stack.items.len != 0) { + // `top` and `containing` become invalid after appending to `self.stack` + var top = &self.stack.items[self.stack.items.len - 1]; + var containing = top; + var dirname_len = top.dirname_len; + if (top.iter.next() catch |err| { + // If we get an error, then we want the user to be able to continue + // walking if they want, which means that we need to pop the directory + // that errored from the stack. Otherwise, all future `next` calls would + // likely just fail with the same error. + var item = self.stack.pop(); + if (self.stack.items.len != 0) { + item.iter.dir.close(); + } + return err; + }) |base| { + self.name_buffer.shrinkRetainingCapacity(dirname_len); + if (self.name_buffer.items.len != 0) { + try self.name_buffer.append(fs.path.sep); + dirname_len += 1; + } + try self.name_buffer.appendSlice(base.name); + if (base.kind == .directory) { + var new_dir = top.iter.dir.openDir(base.name, .{ .iterate = true }) catch |err| switch (err) { + error.NameTooLong => unreachable, // no path sep in base.name + else => |e| return e, + }; + { + errdefer new_dir.close(); + try self.stack.append(StackItem{ + .iter = new_dir.iterateAssumeFirstIteration(), + .dirname_len = self.name_buffer.items.len, + }); + top = &self.stack.items[self.stack.items.len - 1]; + containing = &self.stack.items[self.stack.items.len - 2]; + } + } + return WalkerEntry{ + .dir = containing.iter.dir, + .basename = self.name_buffer.items[dirname_len..], + .path = self.name_buffer.items, + .kind = base.kind, + }; + } else { + var item = self.stack.pop(); + if (self.stack.items.len != 0) { + item.iter.dir.close(); + } + } + } + return null; + } + + pub fn deinit(self: *Walker) void { + // Close any remaining directories except the initial one (which is always at index 0) + if (self.stack.items.len > 1) { + for (self.stack.items[1..]) |*item| { + item.iter.dir.close(); + } + } + self.stack.deinit(); + self.name_buffer.deinit(); + } +}; + +/// Recursively iterates over a directory. +/// `self` must have been opened with `OpenDirOptions{.iterate = true}`. +/// Must call `Walker.deinit` when done. +/// The order of returned file system entries is undefined. +/// `self` will not be closed after walking it. +pub fn walk(self: Dir, allocator: Allocator) !Walker { + var name_buffer = std.ArrayList(u8).init(allocator); + errdefer name_buffer.deinit(); + + var stack = std.ArrayList(Walker.StackItem).init(allocator); + errdefer stack.deinit(); + + try stack.append(Walker.StackItem{ + .iter = self.iterate(), + .dirname_len = 0, + }); + + return Walker{ + .stack = stack, + .name_buffer = name_buffer, + }; +} + +pub const OpenError = error{ + FileNotFound, + NotDir, + InvalidHandle, + AccessDenied, + SymLinkLoop, + ProcessFdQuotaExceeded, + NameTooLong, + SystemFdQuotaExceeded, + NoDevice, + SystemResources, + InvalidUtf8, + BadPathName, + DeviceBusy, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, +} || posix.UnexpectedError; + +pub fn close(self: *Dir) void { + if (fs.need_async_thread) { + std.event.Loop.instance.?.close(self.fd); + } else { + posix.close(self.fd); + } + self.* = undefined; +} + +/// Opens a file for reading or writing, without attempting to create a new file. +/// To create a new file, see `createFile`. +/// Call `File.close` to release the resource. +/// Asserts that the path parameter has no null bytes. +pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { + if (builtin.os.tag == .windows) { + const path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); + return self.openFileW(path_w.span(), flags); + } + if (builtin.os.tag == .wasi and !builtin.link_libc) { + return self.openFileWasi(sub_path, flags); + } + const path_c = try posix.toPosixPath(sub_path); + return self.openFileZ(&path_c, flags); +} + +/// Same as `openFile` but WASI only. +pub fn openFileWasi(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { + const w = std.os.wasi; + var fdflags: w.fdflags_t = 0x0; + var base: w.rights_t = 0x0; + if (flags.isRead()) { + base |= w.RIGHT.FD_READ | w.RIGHT.FD_TELL | w.RIGHT.FD_SEEK | w.RIGHT.FD_FILESTAT_GET; + } + if (flags.isWrite()) { + fdflags |= w.FDFLAG.APPEND; + base |= w.RIGHT.FD_WRITE | + w.RIGHT.FD_TELL | + w.RIGHT.FD_SEEK | + w.RIGHT.FD_DATASYNC | + w.RIGHT.FD_FDSTAT_SET_FLAGS | + w.RIGHT.FD_SYNC | + w.RIGHT.FD_ALLOCATE | + w.RIGHT.FD_ADVISE | + w.RIGHT.FD_FILESTAT_SET_TIMES | + w.RIGHT.FD_FILESTAT_SET_SIZE; + } + const fd = try posix.openatWasi(self.fd, sub_path, 0x0, 0x0, fdflags, base, 0x0); + return File{ .handle = fd }; +} + +/// Same as `openFile` but the path parameter is null-terminated. +pub fn openFileZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File { + if (builtin.os.tag == .windows) { + const path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path); + return self.openFileW(path_w.span(), flags); + } + + var os_flags: u32 = 0; + if (@hasDecl(posix.O, "CLOEXEC")) os_flags = posix.O.CLOEXEC; + + // Use the O locking flags if the os supports them to acquire the lock + // atomically. + const has_flock_open_flags = @hasDecl(posix.O, "EXLOCK"); + if (has_flock_open_flags) { + // Note that the O.NONBLOCK flag is removed after the openat() call + // is successful. + const nonblocking_lock_flag: u32 = if (flags.lock_nonblocking) + posix.O.NONBLOCK + else + 0; + os_flags |= switch (flags.lock) { + .none => @as(u32, 0), + .shared => posix.O.SHLOCK | nonblocking_lock_flag, + .exclusive => posix.O.EXLOCK | nonblocking_lock_flag, + }; + } + if (@hasDecl(posix.O, "LARGEFILE")) { + os_flags |= posix.O.LARGEFILE; + } + if (@hasDecl(posix.O, "NOCTTY") and !flags.allow_ctty) { + os_flags |= posix.O.NOCTTY; + } + os_flags |= switch (flags.mode) { + .read_only => @as(u32, posix.O.RDONLY), + .write_only => @as(u32, posix.O.WRONLY), + .read_write => @as(u32, posix.O.RDWR), + }; + const fd = if (flags.intended_io_mode != .blocking) + try std.event.Loop.instance.?.openatZ(self.fd, sub_path, os_flags, 0) + else + try posix.openatZ(self.fd, sub_path, os_flags, 0); + errdefer posix.close(fd); + + // WASI doesn't have posix.flock so we intetinally check OS prior to the inner if block + // since it is not compiltime-known and we need to avoid undefined symbol in Wasm. + if (@hasDecl(posix.system, "LOCK") and builtin.target.os.tag != .wasi) { + if (!has_flock_open_flags and flags.lock != .none) { + // TODO: integrate async I/O + const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; + try posix.flock(fd, switch (flags.lock) { + .none => unreachable, + .shared => posix.LOCK.SH | lock_nonblocking, + .exclusive => posix.LOCK.EX | lock_nonblocking, + }); + } + } + + if (has_flock_open_flags and flags.lock_nonblocking) { + var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) { + error.FileBusy => unreachable, + error.Locked => unreachable, + error.PermissionDenied => unreachable, + error.DeadLock => unreachable, + error.LockedRegionLimitExceeded => unreachable, + else => |e| return e, + }; + fl_flags &= ~@as(usize, posix.O.NONBLOCK); + _ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) { + error.FileBusy => unreachable, + error.Locked => unreachable, + error.PermissionDenied => unreachable, + error.DeadLock => unreachable, + error.LockedRegionLimitExceeded => unreachable, + else => |e| return e, + }; + } + + return File{ + .handle = fd, + .capable_io_mode = .blocking, + .intended_io_mode = flags.intended_io_mode, + }; +} + +/// Same as `openFile` but Windows-only and the path parameter is +/// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. +pub fn openFileW(self: Dir, sub_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { + const w = std.os.windows; + const file: File = .{ + .handle = try w.OpenFile(sub_path_w, .{ + .dir = self.fd, + .access_mask = w.SYNCHRONIZE | + (if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) | + (if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0), + .creation = w.FILE_OPEN, + .io_mode = flags.intended_io_mode, + }), + .capable_io_mode = std.io.default_mode, + .intended_io_mode = flags.intended_io_mode, + }; + errdefer file.close(); + var io: w.IO_STATUS_BLOCK = undefined; + const range_off: w.LARGE_INTEGER = 0; + const range_len: w.LARGE_INTEGER = 1; + const exclusive = switch (flags.lock) { + .none => return file, + .shared => false, + .exclusive => true, + }; + try w.LockFile( + file.handle, + null, + null, + null, + &io, + &range_off, + &range_len, + null, + @intFromBool(flags.lock_nonblocking), + @intFromBool(exclusive), + ); + return file; +} + +/// Creates, opens, or overwrites a file with write access. +/// Call `File.close` on the result when done. +/// Asserts that the path parameter has no null bytes. +pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { + if (builtin.os.tag == .windows) { + const path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); + return self.createFileW(path_w.span(), flags); + } + if (builtin.os.tag == .wasi and !builtin.link_libc) { + return self.createFileWasi(sub_path, flags); + } + const path_c = try posix.toPosixPath(sub_path); + return self.createFileZ(&path_c, flags); +} + +/// Same as `createFile` but WASI only. +pub fn createFileWasi(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { + const w = std.os.wasi; + var oflags = w.O.CREAT; + var base: w.rights_t = w.RIGHT.FD_WRITE | + w.RIGHT.FD_DATASYNC | + w.RIGHT.FD_SEEK | + w.RIGHT.FD_TELL | + w.RIGHT.FD_FDSTAT_SET_FLAGS | + w.RIGHT.FD_SYNC | + w.RIGHT.FD_ALLOCATE | + w.RIGHT.FD_ADVISE | + w.RIGHT.FD_FILESTAT_SET_TIMES | + w.RIGHT.FD_FILESTAT_SET_SIZE | + w.RIGHT.FD_FILESTAT_GET; + if (flags.read) { + base |= w.RIGHT.FD_READ; + } + if (flags.truncate) { + oflags |= w.O.TRUNC; + } + if (flags.exclusive) { + oflags |= w.O.EXCL; + } + const fd = try posix.openatWasi(self.fd, sub_path, 0x0, oflags, 0x0, base, 0x0); + return File{ .handle = fd }; +} + +/// Same as `createFile` but the path parameter is null-terminated. +pub fn createFileZ(self: Dir, sub_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File { + if (builtin.os.tag == .windows) { + const path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); + return self.createFileW(path_w.span(), flags); + } + + // Use the O locking flags if the os supports them to acquire the lock + // atomically. + const has_flock_open_flags = @hasDecl(posix.O, "EXLOCK"); + // Note that the O.NONBLOCK flag is removed after the openat() call + // is successful. + const nonblocking_lock_flag: u32 = if (has_flock_open_flags and flags.lock_nonblocking) + posix.O.NONBLOCK + else + 0; + const lock_flag: u32 = if (has_flock_open_flags) switch (flags.lock) { + .none => @as(u32, 0), + .shared => posix.O.SHLOCK | nonblocking_lock_flag, + .exclusive => posix.O.EXLOCK | nonblocking_lock_flag, + } else 0; + + const O_LARGEFILE = if (@hasDecl(posix.O, "LARGEFILE")) posix.O.LARGEFILE else 0; + const os_flags = lock_flag | O_LARGEFILE | posix.O.CREAT | posix.O.CLOEXEC | + (if (flags.truncate) @as(u32, posix.O.TRUNC) else 0) | + (if (flags.read) @as(u32, posix.O.RDWR) else posix.O.WRONLY) | + (if (flags.exclusive) @as(u32, posix.O.EXCL) else 0); + const fd = if (flags.intended_io_mode != .blocking) + try std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, os_flags, flags.mode) + else + try posix.openatZ(self.fd, sub_path_c, os_flags, flags.mode); + errdefer posix.close(fd); + + // WASI doesn't have posix.flock so we intetinally check OS prior to the inner if block + // since it is not compiltime-known and we need to avoid undefined symbol in Wasm. + if (builtin.target.os.tag != .wasi) { + if (!has_flock_open_flags and flags.lock != .none) { + // TODO: integrate async I/O + const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; + try posix.flock(fd, switch (flags.lock) { + .none => unreachable, + .shared => posix.LOCK.SH | lock_nonblocking, + .exclusive => posix.LOCK.EX | lock_nonblocking, + }); + } + } + + if (has_flock_open_flags and flags.lock_nonblocking) { + var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) { + error.FileBusy => unreachable, + error.Locked => unreachable, + error.PermissionDenied => unreachable, + error.DeadLock => unreachable, + error.LockedRegionLimitExceeded => unreachable, + else => |e| return e, + }; + fl_flags &= ~@as(usize, posix.O.NONBLOCK); + _ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) { + error.FileBusy => unreachable, + error.Locked => unreachable, + error.PermissionDenied => unreachable, + error.DeadLock => unreachable, + error.LockedRegionLimitExceeded => unreachable, + else => |e| return e, + }; + } + + return File{ + .handle = fd, + .capable_io_mode = .blocking, + .intended_io_mode = flags.intended_io_mode, + }; +} + +/// Same as `createFile` but Windows-only and the path parameter is +/// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. +pub fn createFileW(self: Dir, sub_path_w: []const u16, flags: File.CreateFlags) File.OpenError!File { + const w = std.os.windows; + const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0; + const file: File = .{ + .handle = try w.OpenFile(sub_path_w, .{ + .dir = self.fd, + .access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag, + .creation = if (flags.exclusive) + @as(u32, w.FILE_CREATE) + else if (flags.truncate) + @as(u32, w.FILE_OVERWRITE_IF) + else + @as(u32, w.FILE_OPEN_IF), + .io_mode = flags.intended_io_mode, + }), + .capable_io_mode = std.io.default_mode, + .intended_io_mode = flags.intended_io_mode, + }; + errdefer file.close(); + var io: w.IO_STATUS_BLOCK = undefined; + const range_off: w.LARGE_INTEGER = 0; + const range_len: w.LARGE_INTEGER = 1; + const exclusive = switch (flags.lock) { + .none => return file, + .shared => false, + .exclusive => true, + }; + try w.LockFile( + file.handle, + null, + null, + null, + &io, + &range_off, + &range_len, + null, + @intFromBool(flags.lock_nonblocking), + @intFromBool(exclusive), + ); + return file; +} + +/// Creates a single directory with a relative or absolute path. +/// To create multiple directories to make an entire path, see `makePath`. +/// To operate on only absolute paths, see `makeDirAbsolute`. +pub fn makeDir(self: Dir, sub_path: []const u8) !void { + try posix.mkdirat(self.fd, sub_path, default_mode); +} + +/// Creates a single directory with a relative or absolute null-terminated UTF-8-encoded path. +/// To create multiple directories to make an entire path, see `makePath`. +/// To operate on only absolute paths, see `makeDirAbsoluteZ`. +pub fn makeDirZ(self: Dir, sub_path: [*:0]const u8) !void { + try posix.mkdiratZ(self.fd, sub_path, default_mode); +} + +/// Creates a single directory with a relative or absolute null-terminated WTF-16-encoded path. +/// To create multiple directories to make an entire path, see `makePath`. +/// To operate on only absolute paths, see `makeDirAbsoluteW`. +pub fn makeDirW(self: Dir, sub_path: [*:0]const u16) !void { + try posix.mkdiratW(self.fd, sub_path, default_mode); +} + +/// Calls makeDir iteratively to make an entire path +/// (i.e. creating any parent directories that do not exist). +/// Returns success if the path already exists and is a directory. +/// This function is not atomic, and if it returns an error, the file system may +/// have been modified regardless. +pub fn makePath(self: Dir, sub_path: []const u8) !void { + var it = try fs.path.componentIterator(sub_path); + var component = it.last() orelse return; + while (true) { + self.makeDir(component.path) catch |err| switch (err) { + error.PathAlreadyExists => { + // TODO stat the file and return an error if it's not a directory + // this is important because otherwise a dangling symlink + // could cause an infinite loop + }, + error.FileNotFound => |e| { + component = it.previous() orelse return e; + continue; + }, + else => |e| return e, + }; + component = it.next() orelse return; + } +} + +/// Calls makeOpenDirAccessMaskW iteratively to make an entire path +/// (i.e. creating any parent directories that do not exist). +/// Opens the dir if the path already exists and is a directory. +/// This function is not atomic, and if it returns an error, the file system may +/// have been modified regardless. +fn makeOpenPathAccessMaskW(self: Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) OpenError!Dir { + const w = std.os.windows; + var it = try fs.path.componentIterator(sub_path); + // If there are no components in the path, then create a dummy component with the full path. + var component = it.last() orelse fs.path.NativeUtf8ComponentIterator.Component{ + .name = "", + .path = sub_path, + }; + + while (true) { + const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path); + const is_last = it.peekNext() == null; + var result = self.makeOpenDirAccessMaskW(sub_path_w.span().ptr, access_mask, .{ + .no_follow = no_follow, + .create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE, + }) catch |err| switch (err) { + error.FileNotFound => |e| { + component = it.previous() orelse return e; + continue; + }, + else => |e| return e, + }; + + component = it.next() orelse return result; + // Don't leak the intermediate file handles + result.close(); + } +} + +/// This function performs `makePath`, followed by `openDir`. +/// If supported by the OS, this operation is atomic. It is not atomic on +/// all operating systems. +/// On Windows, this function performs `makeOpenPathAccessMaskW`. +pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !Dir { + return switch (builtin.os.tag) { + .windows => { + const w = std.os.windows; + const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | + w.SYNCHRONIZE | w.FILE_TRAVERSE | + (if (open_dir_options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0)); + + return self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow); + }, + else => { + return self.openDir(sub_path, open_dir_options) catch |err| switch (err) { + error.FileNotFound => { + try self.makePath(sub_path); + return self.openDir(sub_path, open_dir_options); + }, + else => |e| return e, + }; + }, + }; +} + +/// This function returns the canonicalized absolute pathname of +/// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this +/// `Dir` handle and returns the canonicalized absolute pathname of `pathname` +/// argument. +/// This function is not universally supported by all platforms. +/// Currently supported hosts are: Linux, macOS, and Windows. +/// See also `Dir.realpathZ`, `Dir.realpathW`, and `Dir.realpathAlloc`. +pub fn realpath(self: Dir, pathname: []const u8, out_buffer: []u8) ![]u8 { + if (builtin.os.tag == .wasi) { + @compileError("realpath is not available on WASI"); + } + if (builtin.os.tag == .windows) { + const pathname_w = try std.os.windows.sliceToPrefixedFileW(self.fd, pathname); + return self.realpathW(pathname_w.span(), out_buffer); + } + const pathname_c = try posix.toPosixPath(pathname); + return self.realpathZ(&pathname_c, out_buffer); +} + +/// Same as `Dir.realpath` except `pathname` is null-terminated. +/// See also `Dir.realpath`, `realpathZ`. +pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) ![]u8 { + if (builtin.os.tag == .windows) { + const pathname_w = try posix.windows.cStrToPrefixedFileW(self.fd, pathname); + return self.realpathW(pathname_w.span(), out_buffer); + } + + const flags = if (builtin.os.tag == .linux) + posix.O.PATH | posix.O.NONBLOCK | posix.O.CLOEXEC + else + posix.O.NONBLOCK | posix.O.CLOEXEC; + const fd = posix.openatZ(self.fd, pathname, flags, 0) catch |err| switch (err) { + error.FileLocksNotSupported => unreachable, + else => |e| return e, + }; + defer posix.close(fd); + + // Use of MAX_PATH_BYTES here is valid as the realpath function does not + // have a variant that takes an arbitrary-size buffer. + // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 + // NULL out parameter (GNU's canonicalize_file_name) to handle overelong + // paths. musl supports passing NULL but restricts the output to PATH_MAX + // anyway. + var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; + const out_path = try posix.getFdPath(fd, &buffer); + + if (out_path.len > out_buffer.len) { + return error.NameTooLong; + } + + const result = out_buffer[0..out_path.len]; + @memcpy(result, out_path); + return result; +} + +/// Windows-only. Same as `Dir.realpath` except `pathname` is WTF16 encoded. +/// See also `Dir.realpath`, `realpathW`. +pub fn realpathW(self: Dir, pathname: []const u16, out_buffer: []u8) ![]u8 { + const w = std.os.windows; + + const access_mask = w.GENERIC_READ | w.SYNCHRONIZE; + const share_access = w.FILE_SHARE_READ; + const creation = w.FILE_OPEN; + const h_file = blk: { + const res = w.OpenFile(pathname, .{ + .dir = self.fd, + .access_mask = access_mask, + .share_access = share_access, + .creation = creation, + .io_mode = .blocking, + .filter = .any, + }) catch |err| switch (err) { + error.WouldBlock => unreachable, + else => |e| return e, + }; + break :blk res; + }; + defer w.CloseHandle(h_file); + + // Use of MAX_PATH_BYTES here is valid as the realpath function does not + // have a variant that takes an arbitrary-size buffer. + // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 + // NULL out parameter (GNU's canonicalize_file_name) to handle overelong + // paths. musl supports passing NULL but restricts the output to PATH_MAX + // anyway. + var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; + const out_path = try posix.getFdPath(h_file, &buffer); + + if (out_path.len > out_buffer.len) { + return error.NameTooLong; + } + + const result = out_buffer[0..out_path.len]; + @memcpy(result, out_path); + return result; +} + +/// Same as `Dir.realpath` except caller must free the returned memory. +/// See also `Dir.realpath`. +pub fn realpathAlloc(self: Dir, allocator: Allocator, pathname: []const u8) ![]u8 { + // Use of MAX_PATH_BYTES here is valid as the realpath function does not + // have a variant that takes an arbitrary-size buffer. + // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 + // NULL out parameter (GNU's canonicalize_file_name) to handle overelong + // paths. musl supports passing NULL but restricts the output to PATH_MAX + // anyway. + var buf: [fs.MAX_PATH_BYTES]u8 = undefined; + return allocator.dupe(u8, try self.realpath(pathname, buf[0..])); +} + +/// Changes the current working directory to the open directory handle. +/// This modifies global state and can have surprising effects in multi- +/// threaded applications. Most applications and especially libraries should +/// not call this function as a general rule, however it can have use cases +/// in, for example, implementing a shell, or child process execution. +/// Not all targets support this. For example, WASI does not have the concept +/// of a current working directory. +pub fn setAsCwd(self: Dir) !void { + if (builtin.os.tag == .wasi) { + @compileError("changing cwd is not currently possible in WASI"); + } + if (builtin.os.tag == .windows) { + var dir_path_buffer: [std.os.windows.PATH_MAX_WIDE]u16 = undefined; + const dir_path = try std.os.windows.GetFinalPathNameByHandle(self.fd, .{}, &dir_path_buffer); + if (builtin.link_libc) { + return posix.chdirW(dir_path); + } + return std.os.windows.SetCurrentDirectory(dir_path); + } + try posix.fchdir(self.fd); +} + +pub const OpenDirOptions = struct { + /// `true` means the opened directory can be used as the `Dir` parameter + /// for functions which operate based on an open directory handle. When `false`, + /// such operations are Illegal Behavior. + access_sub_paths: bool = true, + + /// `true` means the opened directory can be scanned for the files and sub-directories + /// of the result. It means the `iterate` function can be called. + iterate: bool = false, + + /// `true` means it won't dereference the symlinks. + no_follow: bool = false, +}; + +/// Opens a directory at the given path. The directory is a system resource that remains +/// open until `close` is called on the result. +/// The directory cannot be iterated unless the `iterate` option is set to `true`. +/// +/// Asserts that the path parameter has no null bytes. +pub fn openDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { + if (builtin.os.tag == .windows) { + const sub_path_w = try posix.windows.sliceToPrefixedFileW(self.fd, sub_path); + return self.openDirW(sub_path_w.span().ptr, args); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + return self.openDirWasi(sub_path, args); + } else { + const sub_path_c = try posix.toPosixPath(sub_path); + return self.openDirZ(&sub_path_c, args); + } +} + +/// Same as `openDir` except only WASI. +pub fn openDirWasi(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { + const w = std.os.wasi; + var base: w.rights_t = w.RIGHT.FD_FILESTAT_GET | w.RIGHT.FD_FDSTAT_SET_FLAGS | w.RIGHT.FD_FILESTAT_SET_TIMES; + if (args.access_sub_paths) { + base |= w.RIGHT.FD_READDIR | + w.RIGHT.PATH_CREATE_DIRECTORY | + w.RIGHT.PATH_CREATE_FILE | + w.RIGHT.PATH_LINK_SOURCE | + w.RIGHT.PATH_LINK_TARGET | + w.RIGHT.PATH_OPEN | + w.RIGHT.PATH_READLINK | + w.RIGHT.PATH_RENAME_SOURCE | + w.RIGHT.PATH_RENAME_TARGET | + w.RIGHT.PATH_FILESTAT_GET | + w.RIGHT.PATH_FILESTAT_SET_SIZE | + w.RIGHT.PATH_FILESTAT_SET_TIMES | + w.RIGHT.PATH_SYMLINK | + w.RIGHT.PATH_REMOVE_DIRECTORY | + w.RIGHT.PATH_UNLINK_FILE; + } + const symlink_flags: w.lookupflags_t = if (args.no_follow) 0x0 else w.LOOKUP_SYMLINK_FOLLOW; + // TODO do we really need all the rights here? + const inheriting: w.rights_t = w.RIGHT.ALL ^ w.RIGHT.SOCK_SHUTDOWN; + + const result = posix.openatWasi( + self.fd, + sub_path, + symlink_flags, + w.O.DIRECTORY, + 0x0, + base, + inheriting, + ); + const fd = result catch |err| switch (err) { + error.FileTooBig => unreachable, // can't happen for directories + error.IsDir => unreachable, // we're providing O.DIRECTORY + error.NoSpaceLeft => unreachable, // not providing O.CREAT + error.PathAlreadyExists => unreachable, // not providing O.CREAT + error.FileLocksNotSupported => unreachable, // locking folders is not supported + error.WouldBlock => unreachable, // can't happen for directories + error.FileBusy => unreachable, // can't happen for directories + else => |e| return e, + }; + return Dir{ .fd = fd }; +} + +/// Same as `openDir` except the parameter is null-terminated. +pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenDirOptions) OpenError!Dir { + if (builtin.os.tag == .windows) { + const sub_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); + return self.openDirW(sub_path_w.span().ptr, args); + } + const symlink_flags: u32 = if (args.no_follow) posix.O.NOFOLLOW else 0x0; + if (!args.iterate) { + const O_PATH = if (@hasDecl(posix.O, "PATH")) posix.O.PATH else 0; + return self.openDirFlagsZ(sub_path_c, posix.O.DIRECTORY | posix.O.RDONLY | posix.O.CLOEXEC | O_PATH | symlink_flags); + } else { + return self.openDirFlagsZ(sub_path_c, posix.O.DIRECTORY | posix.O.RDONLY | posix.O.CLOEXEC | symlink_flags); + } +} + +/// Same as `openDir` except the path parameter is WTF-16 encoded, NT-prefixed. +/// This function asserts the target OS is Windows. +pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenDirOptions) OpenError!Dir { + const w = std.os.windows; + // TODO remove some of these flags if args.access_sub_paths is false + const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | + w.SYNCHRONIZE | w.FILE_TRAVERSE; + const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; + const dir = try self.makeOpenDirAccessMaskW(sub_path_w, flags, .{ + .no_follow = args.no_follow, + .create_disposition = w.FILE_OPEN, + }); + return dir; +} + +/// `flags` must contain `posix.O.DIRECTORY`. +fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: u32) OpenError!Dir { + const result = if (fs.need_async_thread) + std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, flags, 0) + else + posix.openatZ(self.fd, sub_path_c, flags, 0); + const fd = result catch |err| switch (err) { + error.FileTooBig => unreachable, // can't happen for directories + error.IsDir => unreachable, // we're providing O.DIRECTORY + error.NoSpaceLeft => unreachable, // not providing O.CREAT + error.PathAlreadyExists => unreachable, // not providing O.CREAT + error.FileLocksNotSupported => unreachable, // locking folders is not supported + error.WouldBlock => unreachable, // can't happen for directories + error.FileBusy => unreachable, // can't happen for directories + else => |e| return e, + }; + return Dir{ .fd = fd }; +} + +const MakeOpenDirAccessMaskWOptions = struct { + no_follow: bool, + create_disposition: u32, +}; + +fn makeOpenDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) OpenError!Dir { + const w = std.os.windows; + + var result = Dir{ + .fd = undefined, + }; + + const path_len_bytes = @as(u16, @intCast(mem.sliceTo(sub_path_w, 0).len * 2)); + var nt_name = w.UNICODE_STRING{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w), + }; + var attr = w.OBJECT_ATTRIBUTES{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; + var io: w.IO_STATUS_BLOCK = undefined; + const rc = w.ntdll.NtCreateFile( + &result.fd, + access_mask, + &attr, + &io, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_READ | w.FILE_SHARE_WRITE, + flags.create_disposition, + w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, + null, + 0, + ); + + switch (rc) { + .SUCCESS => return result, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .NOT_A_DIRECTORY => return error.NotDir, + // This can happen if the directory has 'List folder contents' permission set to 'Deny' + // and the directory is trying to be opened for iteration. + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_PARAMETER => unreachable, + else => return w.unexpectedStatus(rc), + } +} + +pub const DeleteFileError = posix.UnlinkError; + +/// Delete a file name and possibly the file it refers to, based on an open directory handle. +/// Asserts that the path parameter has no null bytes. +pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void { + if (builtin.os.tag == .windows) { + const sub_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); + return self.deleteFileW(sub_path_w.span()); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + posix.unlinkat(self.fd, sub_path, 0) catch |err| switch (err) { + error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR + else => |e| return e, + }; + } else { + const sub_path_c = try posix.toPosixPath(sub_path); + return self.deleteFileZ(&sub_path_c); + } +} + +/// Same as `deleteFile` except the parameter is null-terminated. +pub fn deleteFileZ(self: Dir, sub_path_c: [*:0]const u8) DeleteFileError!void { + posix.unlinkatZ(self.fd, sub_path_c, 0) catch |err| switch (err) { + error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR + error.AccessDenied => |e| switch (builtin.os.tag) { + // non-Linux POSIX systems return EPERM when trying to delete a directory, so + // we need to handle that case specifically and translate the error + .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => { + // Don't follow symlinks to match unlinkat (which acts on symlinks rather than follows them) + const fstat = posix.fstatatZ(self.fd, sub_path_c, posix.AT.SYMLINK_NOFOLLOW) catch return e; + const is_dir = fstat.mode & posix.S.IFMT == posix.S.IFDIR; + return if (is_dir) error.IsDir else e; + }, + else => return e, + }, + else => |e| return e, + }; +} + +/// Same as `deleteFile` except the parameter is WTF-16 encoded. +pub fn deleteFileW(self: Dir, sub_path_w: []const u16) DeleteFileError!void { + posix.unlinkatW(self.fd, sub_path_w, 0) catch |err| switch (err) { + error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR + else => |e| return e, + }; +} + +pub const DeleteDirError = error{ + DirNotEmpty, + FileNotFound, + AccessDenied, + FileBusy, + FileSystem, + SymLinkLoop, + NameTooLong, + NotDir, + SystemResources, + ReadOnlyFileSystem, + InvalidUtf8, + BadPathName, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, + Unexpected, +}; + +/// Returns `error.DirNotEmpty` if the directory is not empty. +/// To delete a directory recursively, see `deleteTree`. +/// Asserts that the path parameter has no null bytes. +pub fn deleteDir(self: Dir, sub_path: []const u8) DeleteDirError!void { + if (builtin.os.tag == .windows) { + const sub_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); + return self.deleteDirW(sub_path_w.span()); + } else if (builtin.os.tag == .wasi and !builtin.link_libc) { + posix.unlinkat(self.fd, sub_path, posix.AT.REMOVEDIR) catch |err| switch (err) { + error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR + else => |e| return e, + }; + } else { + const sub_path_c = try posix.toPosixPath(sub_path); + return self.deleteDirZ(&sub_path_c); + } +} + +/// Same as `deleteDir` except the parameter is null-terminated. +pub fn deleteDirZ(self: Dir, sub_path_c: [*:0]const u8) DeleteDirError!void { + posix.unlinkatZ(self.fd, sub_path_c, posix.AT.REMOVEDIR) catch |err| switch (err) { + error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR + else => |e| return e, + }; +} + +/// Same as `deleteDir` except the parameter is UTF16LE, NT prefixed. +/// This function is Windows-only. +pub fn deleteDirW(self: Dir, sub_path_w: []const u16) DeleteDirError!void { + posix.unlinkatW(self.fd, sub_path_w, posix.AT.REMOVEDIR) catch |err| switch (err) { + error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR + else => |e| return e, + }; +} + +pub const RenameError = posix.RenameError; + +/// Change the name or location of a file or directory. +/// If new_sub_path already exists, it will be replaced. +/// Renaming a file over an existing directory or a directory +/// over an existing file will fail with `error.IsDir` or `error.NotDir` +pub fn rename(self: Dir, old_sub_path: []const u8, new_sub_path: []const u8) RenameError!void { + return posix.renameat(self.fd, old_sub_path, self.fd, new_sub_path); +} + +/// Same as `rename` except the parameters are null-terminated. +pub fn renameZ(self: Dir, old_sub_path_z: [*:0]const u8, new_sub_path_z: [*:0]const u8) RenameError!void { + return posix.renameatZ(self.fd, old_sub_path_z, self.fd, new_sub_path_z); +} + +/// Same as `rename` except the parameters are UTF16LE, NT prefixed. +/// This function is Windows-only. +pub fn renameW(self: Dir, old_sub_path_w: []const u16, new_sub_path_w: []const u16) RenameError!void { + return posix.renameatW(self.fd, old_sub_path_w, self.fd, new_sub_path_w); +} + +/// Use with `Dir.symLink` and `symLinkAbsolute` to specify whether the symlink +/// will point to a file or a directory. This value is ignored on all hosts +/// except Windows where creating symlinks to different resource types, requires +/// different flags. By default, `symLinkAbsolute` is assumed to point to a file. +pub const SymLinkFlags = struct { + is_directory: bool = false, +}; + +/// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. +/// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent +/// one; the latter case is known as a dangling link. +/// If `sym_link_path` exists, it will not be overwritten. +pub fn symLink( + self: Dir, + target_path: []const u8, + sym_link_path: []const u8, + flags: SymLinkFlags, +) !void { + if (builtin.os.tag == .wasi and !builtin.link_libc) { + return self.symLinkWasi(target_path, sym_link_path, flags); + } + if (builtin.os.tag == .windows) { + // Target path does not use sliceToPrefixedFileW because certain paths + // are handled differently when creating a symlink than they would be + // when converting to an NT namespaced path. CreateSymbolicLink in + // symLinkW will handle the necessary conversion. + var target_path_w: std.os.windows.PathSpace = undefined; + target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path); + target_path_w.data[target_path_w.len] = 0; + const sym_link_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sym_link_path); + return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); + } + const target_path_c = try posix.toPosixPath(target_path); + const sym_link_path_c = try posix.toPosixPath(sym_link_path); + return self.symLinkZ(&target_path_c, &sym_link_path_c, flags); +} + +/// WASI-only. Same as `symLink` except targeting WASI. +pub fn symLinkWasi( + self: Dir, + target_path: []const u8, + sym_link_path: []const u8, + _: SymLinkFlags, +) !void { + return posix.symlinkat(target_path, self.fd, sym_link_path); +} + +/// Same as `symLink`, except the pathname parameters are null-terminated. +pub fn symLinkZ( + self: Dir, + target_path_c: [*:0]const u8, + sym_link_path_c: [*:0]const u8, + flags: SymLinkFlags, +) !void { + if (builtin.os.tag == .windows) { + const target_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, target_path_c); + const sym_link_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sym_link_path_c); + return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); + } + return posix.symlinkatZ(target_path_c, self.fd, sym_link_path_c); +} + +/// Windows-only. Same as `symLink` except the pathname parameters +/// are null-terminated, WTF16 encoded. +pub fn symLinkW( + self: Dir, + /// WTF-16, does not need to be NT-prefixed. The NT-prefixing + /// of this path is handled by CreateSymbolicLink. + target_path_w: [:0]const u16, + /// WTF-16, must be NT-prefixed or relative + sym_link_path_w: []const u16, + flags: SymLinkFlags, +) !void { + return std.os.windows.CreateSymbolicLink(self.fd, sym_link_path_w, target_path_w, flags.is_directory); +} + +pub const ReadLinkError = posix.ReadLinkError; + +/// Read value of a symbolic link. +/// The return value is a slice of `buffer`, from index `0`. +/// Asserts that the path parameter has no null bytes. +pub fn readLink(self: Dir, sub_path: []const u8, buffer: []u8) ReadLinkError![]u8 { + if (builtin.os.tag == .wasi and !builtin.link_libc) { + return self.readLinkWasi(sub_path, buffer); + } + if (builtin.os.tag == .windows) { + const sub_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); + return self.readLinkW(sub_path_w.span(), buffer); + } + const sub_path_c = try posix.toPosixPath(sub_path); + return self.readLinkZ(&sub_path_c, buffer); +} + +/// WASI-only. Same as `readLink` except targeting WASI. +pub fn readLinkWasi(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 { + return posix.readlinkat(self.fd, sub_path, buffer); +} + +/// Same as `readLink`, except the `pathname` parameter is null-terminated. +pub fn readLinkZ(self: Dir, sub_path_c: [*:0]const u8, buffer: []u8) ![]u8 { + if (builtin.os.tag == .windows) { + const sub_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); + return self.readLinkW(sub_path_w.span(), buffer); + } + return posix.readlinkatZ(self.fd, sub_path_c, buffer); +} + +/// Windows-only. Same as `readLink` except the pathname parameter +/// is null-terminated, WTF16 encoded. +pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 { + return std.os.windows.ReadLink(self.fd, sub_path_w, buffer); +} + +/// Read all of file contents using a preallocated buffer. +/// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len` +/// the situation is ambiguous. It could either mean that the entire file was read, and +/// it exactly fits the buffer, or it could mean the buffer was not big enough for the +/// entire file. +pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 { + var file = try self.openFile(file_path, .{}); + defer file.close(); + + const end_index = try file.readAll(buffer); + return buffer[0..end_index]; +} + +/// On success, caller owns returned buffer. +/// If the file is larger than `max_bytes`, returns `error.FileTooBig`. +pub fn readFileAlloc(self: Dir, allocator: mem.Allocator, file_path: []const u8, max_bytes: usize) ![]u8 { + return self.readFileAllocOptions(allocator, file_path, max_bytes, null, @alignOf(u8), null); +} + +/// On success, caller owns returned buffer. +/// If the file is larger than `max_bytes`, returns `error.FileTooBig`. +/// If `size_hint` is specified the initial buffer size is calculated using +/// that value, otherwise the effective file size is used instead. +/// Allows specifying alignment and a sentinel value. +pub fn readFileAllocOptions( + self: Dir, + allocator: mem.Allocator, + file_path: []const u8, + max_bytes: usize, + size_hint: ?usize, + comptime alignment: u29, + comptime optional_sentinel: ?u8, +) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) { + var file = try self.openFile(file_path, .{}); + defer file.close(); + + // If the file size doesn't fit a usize it'll be certainly greater than + // `max_bytes` + const stat_size = size_hint orelse std.math.cast(usize, try file.getEndPos()) orelse + return error.FileTooBig; + + return file.readToEndAllocOptions(allocator, max_bytes, stat_size, alignment, optional_sentinel); +} + +pub const DeleteTreeError = error{ + InvalidHandle, + AccessDenied, + FileTooBig, + SymLinkLoop, + ProcessFdQuotaExceeded, + NameTooLong, + SystemFdQuotaExceeded, + NoDevice, + SystemResources, + ReadOnlyFileSystem, + FileSystem, + FileBusy, + DeviceBusy, + + /// One of the path components was not a directory. + /// This error is unreachable if `sub_path` does not contain a path separator. + NotDir, + + /// On Windows, file paths must be valid Unicode. + InvalidUtf8, + + /// On Windows, file paths cannot contain these characters: + /// '/', '*', '?', '"', '<', '>', '|' + BadPathName, + + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, +} || posix.UnexpectedError; + +/// Whether `full_path` describes a symlink, file, or directory, this function +/// removes it. If it cannot be removed because it is a non-empty directory, +/// this function recursively removes its entries and then tries again. +/// This operation is not atomic on most file systems. +pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { + var initial_iterable_dir = (try self.deleteTreeOpenInitialSubpath(sub_path, .file)) orelse return; + + const StackItem = struct { + name: []const u8, + parent_dir: Dir, + iter: Dir.Iterator, + + fn closeAll(items: []@This()) void { + for (items) |*item| item.iter.dir.close(); + } + }; + + var stack_buffer: [16]StackItem = undefined; + var stack = std.ArrayListUnmanaged(StackItem).initBuffer(&stack_buffer); + defer StackItem.closeAll(stack.items); + + stack.appendAssumeCapacity(.{ + .name = sub_path, + .parent_dir = self, + .iter = initial_iterable_dir.iterateAssumeFirstIteration(), + }); + + process_stack: while (stack.items.len != 0) { + var top = &stack.items[stack.items.len - 1]; + while (try top.iter.next()) |entry| { + var treat_as_dir = entry.kind == .directory; + handle_entry: while (true) { + if (treat_as_dir) { + if (stack.unusedCapacitySlice().len >= 1) { + var iterable_dir = top.iter.dir.openDir(entry.name, .{ + .no_follow = true, + .iterate = true, + }) catch |err| switch (err) { + error.NotDir => { + treat_as_dir = false; + continue :handle_entry; + }, + error.FileNotFound => { + // That's fine, we were trying to remove this directory anyway. + break :handle_entry; + }, + + error.InvalidHandle, + error.AccessDenied, + error.SymLinkLoop, + error.ProcessFdQuotaExceeded, + error.NameTooLong, + error.SystemFdQuotaExceeded, + error.NoDevice, + error.SystemResources, + error.Unexpected, + error.InvalidUtf8, + error.BadPathName, + error.NetworkNotFound, + error.DeviceBusy, + => |e| return e, + }; + stack.appendAssumeCapacity(.{ + .name = entry.name, + .parent_dir = top.iter.dir, + .iter = iterable_dir.iterateAssumeFirstIteration(), + }); + continue :process_stack; + } else { + try top.iter.dir.deleteTreeMinStackSizeWithKindHint(entry.name, entry.kind); + break :handle_entry; + } + } else { + if (top.iter.dir.deleteFile(entry.name)) { + break :handle_entry; + } else |err| switch (err) { + error.FileNotFound => break :handle_entry, + + // Impossible because we do not pass any path separators. + error.NotDir => unreachable, + + error.IsDir => { + treat_as_dir = true; + continue :handle_entry; + }, + + error.AccessDenied, + error.InvalidUtf8, + error.SymLinkLoop, + error.NameTooLong, + error.SystemResources, + error.ReadOnlyFileSystem, + error.FileSystem, + error.FileBusy, + error.BadPathName, + error.NetworkNotFound, + error.Unexpected, + => |e| return e, + } + } + } + } + + // On Windows, we can't delete until the dir's handle has been closed, so + // close it before we try to delete. + top.iter.dir.close(); + + // In order to avoid double-closing the directory when cleaning up + // the stack in the case of an error, we save the relevant portions and + // pop the value from the stack. + const parent_dir = top.parent_dir; + const name = top.name; + stack.items.len -= 1; + + var need_to_retry: bool = false; + parent_dir.deleteDir(name) catch |err| switch (err) { + error.FileNotFound => {}, + error.DirNotEmpty => need_to_retry = true, + else => |e| return e, + }; + + if (need_to_retry) { + // Since we closed the handle that the previous iterator used, we + // need to re-open the dir and re-create the iterator. + var iterable_dir = iterable_dir: { + var treat_as_dir = true; + handle_entry: while (true) { + if (treat_as_dir) { + break :iterable_dir parent_dir.openDir(name, .{ + .no_follow = true, + .iterate = true, + }) catch |err| switch (err) { + error.NotDir => { + treat_as_dir = false; + continue :handle_entry; + }, + error.FileNotFound => { + // That's fine, we were trying to remove this directory anyway. + continue :process_stack; + }, + + error.InvalidHandle, + error.AccessDenied, + error.SymLinkLoop, + error.ProcessFdQuotaExceeded, + error.NameTooLong, + error.SystemFdQuotaExceeded, + error.NoDevice, + error.SystemResources, + error.Unexpected, + error.InvalidUtf8, + error.BadPathName, + error.NetworkNotFound, + error.DeviceBusy, + => |e| return e, + }; + } else { + if (parent_dir.deleteFile(name)) { + continue :process_stack; + } else |err| switch (err) { + error.FileNotFound => continue :process_stack, + + // Impossible because we do not pass any path separators. + error.NotDir => unreachable, + + error.IsDir => { + treat_as_dir = true; + continue :handle_entry; + }, + + error.AccessDenied, + error.InvalidUtf8, + error.SymLinkLoop, + error.NameTooLong, + error.SystemResources, + error.ReadOnlyFileSystem, + error.FileSystem, + error.FileBusy, + error.BadPathName, + error.NetworkNotFound, + error.Unexpected, + => |e| return e, + } + } + } + }; + // We know there is room on the stack since we are just re-adding + // the StackItem that we previously popped. + stack.appendAssumeCapacity(.{ + .name = name, + .parent_dir = parent_dir, + .iter = iterable_dir.iterateAssumeFirstIteration(), + }); + continue :process_stack; + } + } +} + +/// Like `deleteTree`, but only keeps one `Iterator` active at a time to minimize the function's stack size. +/// This is slower than `deleteTree` but uses less stack space. +pub fn deleteTreeMinStackSize(self: Dir, sub_path: []const u8) DeleteTreeError!void { + return self.deleteTreeMinStackSizeWithKindHint(sub_path, .file); +} + +fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint: File.Kind) DeleteTreeError!void { + start_over: while (true) { + var dir = (try self.deleteTreeOpenInitialSubpath(sub_path, kind_hint)) orelse return; + var cleanup_dir_parent: ?Dir = null; + defer if (cleanup_dir_parent) |*d| d.close(); + + var cleanup_dir = true; + defer if (cleanup_dir) dir.close(); + + // Valid use of MAX_PATH_BYTES because dir_name_buf will only + // ever store a single path component that was returned from the + // filesystem. + var dir_name_buf: [fs.MAX_PATH_BYTES]u8 = undefined; + var dir_name: []const u8 = sub_path; + + // Here we must avoid recursion, in order to provide O(1) memory guarantee of this function. + // Go through each entry and if it is not a directory, delete it. If it is a directory, + // open it, and close the original directory. Repeat. Then start the entire operation over. + + scan_dir: while (true) { + var dir_it = dir.iterateAssumeFirstIteration(); + dir_it: while (try dir_it.next()) |entry| { + var treat_as_dir = entry.kind == .directory; + handle_entry: while (true) { + if (treat_as_dir) { + const new_dir = dir.openDir(entry.name, .{ + .no_follow = true, + .iterate = true, + }) catch |err| switch (err) { + error.NotDir => { + treat_as_dir = false; + continue :handle_entry; + }, + error.FileNotFound => { + // That's fine, we were trying to remove this directory anyway. + continue :dir_it; + }, + + error.InvalidHandle, + error.AccessDenied, + error.SymLinkLoop, + error.ProcessFdQuotaExceeded, + error.NameTooLong, + error.SystemFdQuotaExceeded, + error.NoDevice, + error.SystemResources, + error.Unexpected, + error.InvalidUtf8, + error.BadPathName, + error.NetworkNotFound, + error.DeviceBusy, + => |e| return e, + }; + if (cleanup_dir_parent) |*d| d.close(); + cleanup_dir_parent = dir; + dir = new_dir; + const result = dir_name_buf[0..entry.name.len]; + @memcpy(result, entry.name); + dir_name = result; + continue :scan_dir; + } else { + if (dir.deleteFile(entry.name)) { + continue :dir_it; + } else |err| switch (err) { + error.FileNotFound => continue :dir_it, + + // Impossible because we do not pass any path separators. + error.NotDir => unreachable, + + error.IsDir => { + treat_as_dir = true; + continue :handle_entry; + }, + + error.AccessDenied, + error.InvalidUtf8, + error.SymLinkLoop, + error.NameTooLong, + error.SystemResources, + error.ReadOnlyFileSystem, + error.FileSystem, + error.FileBusy, + error.BadPathName, + error.NetworkNotFound, + error.Unexpected, + => |e| return e, + } + } + } + } + // Reached the end of the directory entries, which means we successfully deleted all of them. + // Now to remove the directory itself. + dir.close(); + cleanup_dir = false; + + if (cleanup_dir_parent) |d| { + d.deleteDir(dir_name) catch |err| switch (err) { + // These two things can happen due to file system race conditions. + error.FileNotFound, error.DirNotEmpty => continue :start_over, + else => |e| return e, + }; + continue :start_over; + } else { + self.deleteDir(sub_path) catch |err| switch (err) { + error.FileNotFound => return, + error.DirNotEmpty => continue :start_over, + else => |e| return e, + }; + return; + } + } + } +} + +/// On successful delete, returns null. +fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File.Kind) !?Dir { + return iterable_dir: { + // Treat as a file by default + var treat_as_dir = kind_hint == .directory; + + handle_entry: while (true) { + if (treat_as_dir) { + break :iterable_dir self.openDir(sub_path, .{ + .no_follow = true, + .iterate = true, + }) catch |err| switch (err) { + error.NotDir => { + treat_as_dir = false; + continue :handle_entry; + }, + error.FileNotFound => { + // That's fine, we were trying to remove this directory anyway. + return null; + }, + + error.InvalidHandle, + error.AccessDenied, + error.SymLinkLoop, + error.ProcessFdQuotaExceeded, + error.NameTooLong, + error.SystemFdQuotaExceeded, + error.NoDevice, + error.SystemResources, + error.Unexpected, + error.InvalidUtf8, + error.BadPathName, + error.DeviceBusy, + error.NetworkNotFound, + => |e| return e, + }; + } else { + if (self.deleteFile(sub_path)) { + return null; + } else |err| switch (err) { + error.FileNotFound => return null, + + error.IsDir => { + treat_as_dir = true; + continue :handle_entry; + }, + + error.AccessDenied, + error.InvalidUtf8, + error.SymLinkLoop, + error.NameTooLong, + error.SystemResources, + error.ReadOnlyFileSystem, + error.NotDir, + error.FileSystem, + error.FileBusy, + error.BadPathName, + error.NetworkNotFound, + error.Unexpected, + => |e| return e, + } + } + } + }; +} + +pub const WriteFileError = File.WriteError || File.OpenError; + +/// Deprecated: use `writeFile2`. +pub fn writeFile(self: Dir, sub_path: []const u8, data: []const u8) WriteFileError!void { + return writeFile2(self, .{ + .sub_path = sub_path, + .data = data, + .flags = .{}, + }); +} + +pub const WriteFileOptions = struct { + sub_path: []const u8, + data: []const u8, + flags: File.CreateFlags = .{}, +}; + +/// Writes content to the file system, using the file creation flags provided. +pub fn writeFile2(self: Dir, options: WriteFileOptions) WriteFileError!void { + var file = try self.createFile(options.sub_path, options.flags); + defer file.close(); + try file.writeAll(options.data); +} + +pub const AccessError = posix.AccessError; + +/// Test accessing `path`. +/// `path` is UTF-8-encoded. +/// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function. +/// For example, instead of testing if a file exists and then opening it, just +/// open it and handle the error for file not found. +pub fn access(self: Dir, sub_path: []const u8, flags: File.OpenFlags) AccessError!void { + if (builtin.os.tag == .windows) { + const sub_path_w = std.os.windows.sliceToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) { + error.AccessDenied => return error.PermissionDenied, + else => |e| return e, + }; + return self.accessW(sub_path_w.span().ptr, flags); + } + const path_c = try posix.toPosixPath(sub_path); + return self.accessZ(&path_c, flags); +} + +/// Same as `access` except the path parameter is null-terminated. +pub fn accessZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) AccessError!void { + if (builtin.os.tag == .windows) { + const sub_path_w = std.os.windows.cStrToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) { + error.AccessDenied => return error.PermissionDenied, + else => |e| return e, + }; + return self.accessW(sub_path_w.span().ptr, flags); + } + const os_mode = switch (flags.mode) { + .read_only => @as(u32, posix.F_OK), + .write_only => @as(u32, posix.W_OK), + .read_write => @as(u32, posix.R_OK | posix.W_OK), + }; + const result = if (fs.need_async_thread and flags.intended_io_mode != .blocking) + std.event.Loop.instance.?.faccessatZ(self.fd, sub_path, os_mode, 0) + else + posix.faccessatZ(self.fd, sub_path, os_mode, 0); + return result; +} + +/// Same as `access` except asserts the target OS is Windows and the path parameter is +/// * WTF-16 encoded +/// * null-terminated +/// * NtDll prefixed +/// TODO currently this ignores `flags`. +pub fn accessW(self: Dir, sub_path_w: [*:0]const u16, flags: File.OpenFlags) AccessError!void { + _ = flags; + return posix.faccessatW(self.fd, sub_path_w, 0, 0); +} + +pub const CopyFileOptions = struct { + /// When this is `null` the mode is copied from the source file. + override_mode: ?File.Mode = null, +}; + +pub const PrevStatus = enum { + stale, + fresh, +}; + +/// Check the file size, mtime, and mode of `source_path` and `dest_path`. If they are equal, does nothing. +/// Otherwise, atomically copies `source_path` to `dest_path`. The destination file gains the mtime, +/// atime, and mode of the source file so that the next call to `updateFile` will not need a copy. +/// Returns the previous status of the file before updating. +/// If any of the directories do not exist for dest_path, they are created. +pub fn updateFile( + source_dir: Dir, + source_path: []const u8, + dest_dir: Dir, + dest_path: []const u8, + options: CopyFileOptions, +) !PrevStatus { + var src_file = try source_dir.openFile(source_path, .{}); + defer src_file.close(); + + const src_stat = try src_file.stat(); + const actual_mode = options.override_mode orelse src_stat.mode; + check_dest_stat: { + const dest_stat = blk: { + var dest_file = dest_dir.openFile(dest_path, .{}) catch |err| switch (err) { + error.FileNotFound => break :check_dest_stat, + else => |e| return e, + }; + defer dest_file.close(); + + break :blk try dest_file.stat(); + }; + + if (src_stat.size == dest_stat.size and + src_stat.mtime == dest_stat.mtime and + actual_mode == dest_stat.mode) + { + return PrevStatus.fresh; + } + } + + if (fs.path.dirname(dest_path)) |dirname| { + try dest_dir.makePath(dirname); + } + + var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = actual_mode }); + defer atomic_file.deinit(); + + try atomic_file.file.writeFileAll(src_file, .{ .in_len = src_stat.size }); + try atomic_file.file.updateTimes(src_stat.atime, src_stat.mtime); + try atomic_file.finish(); + return PrevStatus.stale; +} + +pub const CopyFileError = File.OpenError || File.StatError || + AtomicFile.InitError || CopyFileRawError || AtomicFile.FinishError; + +/// Guaranteed to be atomic. +/// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and readily available, +/// there is a possibility of power loss or application termination leaving temporary files present +/// in the same directory as dest_path. +pub fn copyFile( + source_dir: Dir, + source_path: []const u8, + dest_dir: Dir, + dest_path: []const u8, + options: CopyFileOptions, +) CopyFileError!void { + var in_file = try source_dir.openFile(source_path, .{}); + defer in_file.close(); + + var size: ?u64 = null; + const mode = options.override_mode orelse blk: { + const st = try in_file.stat(); + size = st.size; + break :blk st.mode; + }; + + var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = mode }); + defer atomic_file.deinit(); + + try copy_file(in_file.handle, atomic_file.file.handle, size); + try atomic_file.finish(); +} + +const CopyFileRawError = error{SystemResources} || posix.CopyFileRangeError || posix.SendFileError; + +// Transfer all the data between two file descriptors in the most efficient way. +// The copy starts at offset 0, the initial offsets are preserved. +// No metadata is transferred over. +fn copy_file(fd_in: posix.fd_t, fd_out: posix.fd_t, maybe_size: ?u64) CopyFileRawError!void { + if (comptime builtin.target.isDarwin()) { + const rc = posix.system.fcopyfile(fd_in, fd_out, null, posix.system.COPYFILE_DATA); + switch (posix.errno(rc)) { + .SUCCESS => return, + .INVAL => unreachable, + .NOMEM => return error.SystemResources, + // The source file is not a directory, symbolic link, or regular file. + // Try with the fallback path before giving up. + .OPNOTSUPP => {}, + else => |err| return posix.unexpectedErrno(err), + } + } + + if (builtin.os.tag == .linux) { + // Try copy_file_range first as that works at the FS level and is the + // most efficient method (if available). + var offset: u64 = 0; + cfr_loop: while (true) { + // The kernel checks the u64 value `offset+count` for overflow, use + // a 32 bit value so that the syscall won't return EINVAL except for + // impossibly large files (> 2^64-1 - 2^32-1). + const amt = try posix.copy_file_range(fd_in, offset, fd_out, offset, std.math.maxInt(u32), 0); + // Terminate as soon as we have copied size bytes or no bytes + if (maybe_size) |s| { + if (s == amt) break :cfr_loop; + } + if (amt == 0) break :cfr_loop; + offset += amt; + } + return; + } + + // Sendfile is a zero-copy mechanism iff the OS supports it, otherwise the + // fallback code will copy the contents chunk by chunk. + const empty_iovec = [0]posix.iovec_const{}; + var offset: u64 = 0; + sendfile_loop: while (true) { + const amt = try posix.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0); + // Terminate as soon as we have copied size bytes or no bytes + if (maybe_size) |s| { + if (s == amt) break :sendfile_loop; + } + if (amt == 0) break :sendfile_loop; + offset += amt; + } +} + +pub const AtomicFileOptions = struct { + mode: File.Mode = File.default_mode, +}; + +/// Directly access the `.file` field, and then call `AtomicFile.finish` +/// to atomically replace `dest_path` with contents. +/// Always call `AtomicFile.deinit` to clean up, regardless of whether `AtomicFile.finish` succeeded. +/// `dest_path` must remain valid until `AtomicFile.deinit` is called. +pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) !AtomicFile { + if (fs.path.dirname(dest_path)) |dirname| { + const dir = try self.openDir(dirname, .{}); + return AtomicFile.init(fs.path.basename(dest_path), options.mode, dir, true); + } else { + return AtomicFile.init(dest_path, options.mode, self, false); + } +} + +pub const Stat = File.Stat; +pub const StatError = File.StatError; + +pub fn stat(self: Dir) StatError!Stat { + const file: File = .{ + .handle = self.fd, + .capable_io_mode = .blocking, + }; + return file.stat(); +} + +pub const StatFileError = File.OpenError || File.StatError || posix.FStatAtError; + +/// Returns metadata for a file inside the directory. +/// +/// On Windows, this requires three syscalls. On other operating systems, it +/// only takes one. +/// +/// Symlinks are followed. +/// +/// `sub_path` may be absolute, in which case `self` is ignored. +pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { + if (builtin.os.tag == .windows) { + var file = try self.openFile(sub_path, .{}); + defer file.close(); + return file.stat(); + } + if (builtin.os.tag == .wasi and !builtin.link_libc) { + const st = try posix.fstatatWasi(self.fd, sub_path, posix.wasi.LOOKUP_SYMLINK_FOLLOW); + return Stat.fromSystem(st); + } + const st = try posix.fstatat(self.fd, sub_path, 0); + return Stat.fromSystem(st); +} + +pub const ChmodError = File.ChmodError; + +/// Changes the mode of the directory. +/// The process must have the correct privileges in order to do this +/// successfully, or must have the effective user ID matching the owner +/// of the directory. Additionally, the directory must have been opened +/// with `OpenDirOptions{ .iterate = true }`. +pub fn chmod(self: Dir, new_mode: File.Mode) ChmodError!void { + const file: File = .{ + .handle = self.fd, + .capable_io_mode = .blocking, + }; + try file.chmod(new_mode); +} + +/// Changes the owner and group of the directory. +/// The process must have the correct privileges in order to do this +/// successfully. The group may be changed by the owner of the directory to +/// any group of which the owner is a member. Additionally, the directory +/// must have been opened with `OpenDirOptions{ .iterate = true }`. If the +/// owner or group is specified as `null`, the ID is not changed. +pub fn chown(self: Dir, owner: ?File.Uid, group: ?File.Gid) ChownError!void { + const file: File = .{ + .handle = self.fd, + .capable_io_mode = .blocking, + }; + try file.chown(owner, group); +} + +pub const ChownError = File.ChownError; + +const Permissions = File.Permissions; +pub const SetPermissionsError = File.SetPermissionsError; + +/// Sets permissions according to the provided `Permissions` struct. +/// This method is *NOT* available on WASI +pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!void { + const file: File = .{ + .handle = self.fd, + .capable_io_mode = .blocking, + }; + try file.setPermissions(permissions); +} + +const Metadata = File.Metadata; +pub const MetadataError = File.MetadataError; + +/// Returns a `Metadata` struct, representing the permissions on the directory +pub fn metadata(self: Dir) MetadataError!Metadata { + const file: File = .{ + .handle = self.fd, + .capable_io_mode = .blocking, + }; + return try file.metadata(); +} + +const Dir = @This(); +const builtin = @import("builtin"); +const std = @import("../std.zig"); +const File = std.fs.File; +const AtomicFile = std.fs.AtomicFile; +const posix = std.os; +const mem = std.mem; +const fs = std.fs; +const Allocator = std.mem.Allocator; diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 5ad354590b..bba4bc551d 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -1486,7 +1486,7 @@ test ". and .. in fs.Dir functions" { try ctx.dir.writeFile(update_path, "something"); const prev_status = try ctx.dir.updateFile(file_path, ctx.dir, update_path, .{}); - try testing.expectEqual(fs.PrevStatus.stale, prev_status); + try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status); try ctx.dir.deleteDir(subdir_path); } @@ -1532,7 +1532,7 @@ test ". and .. in absolute functions" { try update_file.writeAll("something"); update_file.close(); const prev_status = try fs.updateFileAbsolute(created_file_path, update_file_path, .{}); - try testing.expectEqual(fs.PrevStatus.stale, prev_status); + try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status); try fs.deleteDirAbsolute(subdir_path); } From e00e9c0fbf0913154f0f0de95e8d28a6d0ace95a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Nov 2023 13:54:16 -0700 Subject: [PATCH 4/5] std.fs: extract AtomicFile to separate file --- CMakeLists.txt | 1 + lib/std/fs.zig | 81 +------------------------------------ lib/std/fs/AtomicFile.zig | 84 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 80 deletions(-) create mode 100644 lib/std/fs/AtomicFile.zig diff --git a/CMakeLists.txt b/CMakeLists.txt index 8aaeb288ed..4481dfb7f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -247,6 +247,7 @@ set(ZIG_STAGE2_SOURCES "${CMAKE_SOURCE_DIR}/lib/std/fmt/errol/lookup.zig" "${CMAKE_SOURCE_DIR}/lib/std/fmt/parse_float.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs.zig" + "${CMAKE_SOURCE_DIR}/lib/std/fs/AtomicFile.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/Dir.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/file.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/get_app_data_dir.zig" diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 1a4b42ef1b..1aad688395 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -7,11 +7,11 @@ const base64 = std.base64; const crypto = std.crypto; const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const math = std.math; const is_darwin = builtin.os.tag.isDarwin(); pub const Dir = @import("fs/Dir.zig"); +pub const AtomicFile = @import("fs/AtomicFile.zig"); pub const has_executable_bit = switch (builtin.os.tag) { .windows, .wasi => false, @@ -150,85 +150,6 @@ pub fn copyFileAbsolute( return Dir.copyFile(my_cwd, source_path, my_cwd, dest_path, args); } -pub const AtomicFile = struct { - file: File, - // TODO either replace this with rand_buf or use []u16 on Windows - tmp_path_buf: [TMP_PATH_LEN:0]u8, - dest_basename: []const u8, - file_open: bool, - file_exists: bool, - close_dir_on_deinit: bool, - dir: Dir, - - pub const InitError = File.OpenError; - - const RANDOM_BYTES = 12; - const TMP_PATH_LEN = base64_encoder.calcSize(RANDOM_BYTES); - - /// Note that the `Dir.atomicFile` API may be more handy than this lower-level function. - pub fn init( - dest_basename: []const u8, - mode: File.Mode, - dir: Dir, - close_dir_on_deinit: bool, - ) InitError!AtomicFile { - var rand_buf: [RANDOM_BYTES]u8 = undefined; - var tmp_path_buf: [TMP_PATH_LEN:0]u8 = undefined; - - while (true) { - crypto.random.bytes(rand_buf[0..]); - const tmp_path = base64_encoder.encode(&tmp_path_buf, &rand_buf); - tmp_path_buf[tmp_path.len] = 0; - - const file = dir.createFile( - tmp_path, - .{ .mode = mode, .exclusive = true }, - ) catch |err| switch (err) { - error.PathAlreadyExists => continue, - else => |e| return e, - }; - - return AtomicFile{ - .file = file, - .tmp_path_buf = tmp_path_buf, - .dest_basename = dest_basename, - .file_open = true, - .file_exists = true, - .close_dir_on_deinit = close_dir_on_deinit, - .dir = dir, - }; - } - } - - /// Always call deinit, even after a successful finish(). - pub fn deinit(self: *AtomicFile) void { - if (self.file_open) { - self.file.close(); - self.file_open = false; - } - if (self.file_exists) { - self.dir.deleteFile(&self.tmp_path_buf) catch {}; - self.file_exists = false; - } - if (self.close_dir_on_deinit) { - self.dir.close(); - } - self.* = undefined; - } - - pub const FinishError = std.os.RenameError; - - pub fn finish(self: *AtomicFile) FinishError!void { - assert(self.file_exists); - if (self.file_open) { - self.file.close(); - self.file_open = false; - } - try os.renameat(self.dir.fd, self.tmp_path_buf[0..], self.dir.fd, self.dest_basename); - self.file_exists = false; - } -}; - /// Create a new directory, based on an absolute path. /// Asserts that the path is absolute. See `Dir.makeDir` for a function that operates /// on both absolute and relative paths. diff --git a/lib/std/fs/AtomicFile.zig b/lib/std/fs/AtomicFile.zig new file mode 100644 index 0000000000..3e3ab92d08 --- /dev/null +++ b/lib/std/fs/AtomicFile.zig @@ -0,0 +1,84 @@ +file: File, +// TODO either replace this with rand_buf or use []u16 on Windows +tmp_path_buf: [TMP_PATH_LEN:0]u8, +dest_basename: []const u8, +file_open: bool, +file_exists: bool, +close_dir_on_deinit: bool, +dir: Dir, + +pub const InitError = File.OpenError; + +const RANDOM_BYTES = 12; +const TMP_PATH_LEN = fs.base64_encoder.calcSize(RANDOM_BYTES); + +/// Note that the `Dir.atomicFile` API may be more handy than this lower-level function. +pub fn init( + dest_basename: []const u8, + mode: File.Mode, + dir: Dir, + close_dir_on_deinit: bool, +) InitError!AtomicFile { + var rand_buf: [RANDOM_BYTES]u8 = undefined; + var tmp_path_buf: [TMP_PATH_LEN:0]u8 = undefined; + + while (true) { + std.crypto.random.bytes(rand_buf[0..]); + const tmp_path = fs.base64_encoder.encode(&tmp_path_buf, &rand_buf); + tmp_path_buf[tmp_path.len] = 0; + + const file = dir.createFile( + tmp_path, + .{ .mode = mode, .exclusive = true }, + ) catch |err| switch (err) { + error.PathAlreadyExists => continue, + else => |e| return e, + }; + + return AtomicFile{ + .file = file, + .tmp_path_buf = tmp_path_buf, + .dest_basename = dest_basename, + .file_open = true, + .file_exists = true, + .close_dir_on_deinit = close_dir_on_deinit, + .dir = dir, + }; + } +} + +/// Always call deinit, even after a successful finish(). +pub fn deinit(self: *AtomicFile) void { + if (self.file_open) { + self.file.close(); + self.file_open = false; + } + if (self.file_exists) { + self.dir.deleteFile(&self.tmp_path_buf) catch {}; + self.file_exists = false; + } + if (self.close_dir_on_deinit) { + self.dir.close(); + } + self.* = undefined; +} + +pub const FinishError = posix.RenameError; + +pub fn finish(self: *AtomicFile) FinishError!void { + assert(self.file_exists); + if (self.file_open) { + self.file.close(); + self.file_open = false; + } + try posix.renameat(self.dir.fd, self.tmp_path_buf[0..], self.dir.fd, self.dest_basename); + self.file_exists = false; +} + +const AtomicFile = @This(); +const std = @import("../std.zig"); +const File = std.fs.File; +const Dir = std.fs.Dir; +const fs = std.fs; +const assert = std.debug.assert; +const posix = std.os; From 0a536a7c9020c891001be053a4c0b354961cc46f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Nov 2023 14:12:53 -0700 Subject: [PATCH 5/5] std.fs.File: flatten struct --- CMakeLists.txt | 2 +- lib/std/fs.zig | 12 +- lib/std/fs/AtomicFile.zig | 11 +- lib/std/fs/Dir.zig | 1 + lib/std/fs/File.zig | 1624 +++++++++++++++++++++++++++++++++++++ lib/std/fs/file.zig | 1622 ------------------------------------ 6 files changed, 1639 insertions(+), 1633 deletions(-) create mode 100644 lib/std/fs/File.zig delete mode 100644 lib/std/fs/file.zig diff --git a/CMakeLists.txt b/CMakeLists.txt index 4481dfb7f3..393113e336 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -249,7 +249,7 @@ set(ZIG_STAGE2_SOURCES "${CMAKE_SOURCE_DIR}/lib/std/fs.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/AtomicFile.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/Dir.zig" - "${CMAKE_SOURCE_DIR}/lib/std/fs/file.zig" + "${CMAKE_SOURCE_DIR}/lib/std/fs/File.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/get_app_data_dir.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/path.zig" "${CMAKE_SOURCE_DIR}/lib/std/hash.zig" diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 1aad688395..93125c2530 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -10,16 +10,16 @@ const assert = std.debug.assert; const is_darwin = builtin.os.tag.isDarwin(); -pub const Dir = @import("fs/Dir.zig"); pub const AtomicFile = @import("fs/AtomicFile.zig"); +pub const Dir = @import("fs/Dir.zig"); +pub const File = @import("fs/File.zig"); +pub const path = @import("fs/path.zig"); pub const has_executable_bit = switch (builtin.os.tag) { .windows, .wasi => false, else => true, }; -pub const path = @import("fs/path.zig"); -pub const File = @import("fs/file.zig").File; pub const wasi = @import("fs/wasi.zig"); // TODO audit these APIs with respect to Dir and absolute paths @@ -94,6 +94,7 @@ pub const need_async_thread = std.io.is_async and switch (builtin.os.tag) { }; /// TODO remove the allocator requirement from this API +/// TODO move to Dir pub fn atomicSymLink(allocator: Allocator, existing_path: []const u8, new_path: []const u8) !void { if (cwd().symLink(existing_path, new_path, .{})) { return; @@ -104,7 +105,7 @@ pub fn atomicSymLink(allocator: Allocator, existing_path: []const u8, new_path: const dirname = path.dirname(new_path) orelse "."; - var rand_buf: [AtomicFile.RANDOM_BYTES]u8 = undefined; + var rand_buf: [AtomicFile.random_bytes_len]u8 = undefined; const tmp_path = try allocator.alloc(u8, dirname.len + 1 + base64_encoder.calcSize(rand_buf.len)); defer allocator.free(tmp_path); @memcpy(tmp_path[0..dirname.len], dirname); @@ -634,8 +635,9 @@ test { _ = ©FileAbsolute; _ = &updateFileAbsolute; } - _ = &File; + _ = &AtomicFile; _ = &Dir; + _ = &File; _ = &path; _ = @import("fs/test.zig"); _ = @import("fs/get_app_data_dir.zig"); diff --git a/lib/std/fs/AtomicFile.zig b/lib/std/fs/AtomicFile.zig index 3e3ab92d08..b090bc9582 100644 --- a/lib/std/fs/AtomicFile.zig +++ b/lib/std/fs/AtomicFile.zig @@ -1,6 +1,6 @@ file: File, // TODO either replace this with rand_buf or use []u16 on Windows -tmp_path_buf: [TMP_PATH_LEN:0]u8, +tmp_path_buf: [tmp_path_len:0]u8, dest_basename: []const u8, file_open: bool, file_exists: bool, @@ -9,8 +9,8 @@ dir: Dir, pub const InitError = File.OpenError; -const RANDOM_BYTES = 12; -const TMP_PATH_LEN = fs.base64_encoder.calcSize(RANDOM_BYTES); +pub const random_bytes_len = 12; +const tmp_path_len = fs.base64_encoder.calcSize(random_bytes_len); /// Note that the `Dir.atomicFile` API may be more handy than this lower-level function. pub fn init( @@ -19,8 +19,8 @@ pub fn init( dir: Dir, close_dir_on_deinit: bool, ) InitError!AtomicFile { - var rand_buf: [RANDOM_BYTES]u8 = undefined; - var tmp_path_buf: [TMP_PATH_LEN:0]u8 = undefined; + var rand_buf: [random_bytes_len]u8 = undefined; + var tmp_path_buf: [tmp_path_len:0]u8 = undefined; while (true) { std.crypto.random.bytes(rand_buf[0..]); @@ -81,4 +81,5 @@ const File = std.fs.File; const Dir = std.fs.Dir; const fs = std.fs; const assert = std.debug.assert; +// https://github.com/ziglang/zig/issues/5019 const posix = std.os; diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 1d59615c07..b637e664f1 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -2527,6 +2527,7 @@ const builtin = @import("builtin"); const std = @import("../std.zig"); const File = std.fs.File; const AtomicFile = std.fs.AtomicFile; +// https://github.com/ziglang/zig/issues/5019 const posix = std.os; const mem = std.mem; const fs = std.fs; diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig new file mode 100644 index 0000000000..464e7207dc --- /dev/null +++ b/lib/std/fs/File.zig @@ -0,0 +1,1624 @@ +/// The OS-specific file descriptor or file handle. +handle: Handle, + +/// On some systems, such as Linux, file system file descriptors are incapable +/// of non-blocking I/O. This forces us to perform asynchronous I/O on a dedicated thread, +/// to achieve non-blocking file-system I/O. To do this, `File` must be aware of whether +/// it is a file system file descriptor, or, more specifically, whether the I/O is always +/// blocking. +capable_io_mode: io.ModeOverride = io.default_mode, + +/// Furthermore, even when `std.options.io_mode` is async, it is still sometimes desirable +/// to perform blocking I/O, although not by default. For example, when printing a +/// stack trace to stderr. This field tracks both by acting as an overriding I/O mode. +/// When not building in async I/O mode, the type only has the `.blocking` tag, making +/// it a zero-bit type. +intended_io_mode: io.ModeOverride = io.default_mode, + +pub const Handle = posix.fd_t; +pub const Mode = posix.mode_t; +pub const INode = posix.ino_t; +pub const Uid = posix.uid_t; +pub const Gid = posix.gid_t; + +pub const Kind = enum { + block_device, + character_device, + directory, + named_pipe, + sym_link, + file, + unix_domain_socket, + whiteout, + door, + event_port, + unknown, +}; + +/// This is the default mode given to POSIX operating systems for creating +/// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first, +/// since most people would expect "-rw-r--r--", for example, when using +/// the `touch` command, which would correspond to `0o644`. However, POSIX +/// libc implementations use `0o666` inside `fopen` and then rely on the +/// process-scoped "umask" setting to adjust this number for file creation. +pub const default_mode = switch (builtin.os.tag) { + .windows => 0, + .wasi => 0, + else => 0o666, +}; + +pub const OpenError = error{ + SharingViolation, + PathAlreadyExists, + FileNotFound, + AccessDenied, + PipeBusy, + NameTooLong, + /// On Windows, file paths must be valid Unicode. + InvalidUtf8, + /// On Windows, file paths cannot contain these characters: + /// '/', '*', '?', '"', '<', '>', '|' + BadPathName, + Unexpected, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, +} || posix.OpenError || posix.FlockError; + +pub const OpenMode = enum { + read_only, + write_only, + read_write, +}; + +pub const Lock = enum { + none, + shared, + exclusive, +}; + +pub const OpenFlags = struct { + mode: OpenMode = .read_only, + + /// Open the file with an advisory lock to coordinate with other processes + /// accessing it at the same time. An exclusive lock will prevent other + /// processes from acquiring a lock. A shared lock will prevent other + /// processes from acquiring a exclusive lock, but does not prevent + /// other process from getting their own shared locks. + /// + /// The lock is advisory, except on Linux in very specific circumstances[1]. + /// This means that a process that does not respect the locking API can still get access + /// to the file, despite the lock. + /// + /// On these operating systems, the lock is acquired atomically with + /// opening the file: + /// * Darwin + /// * DragonFlyBSD + /// * FreeBSD + /// * Haiku + /// * NetBSD + /// * OpenBSD + /// On these operating systems, the lock is acquired via a separate syscall + /// after opening the file: + /// * Linux + /// * Windows + /// + /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt + lock: Lock = .none, + + /// Sets whether or not to wait until the file is locked to return. If set to true, + /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file + /// is available to proceed. + /// In async I/O mode, non-blocking at the OS level is + /// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned, + /// and `false` means `error.WouldBlock` is handled by the event loop. + lock_nonblocking: bool = false, + + /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even + /// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions + /// related to opening the file, reading, writing, and locking. + intended_io_mode: io.ModeOverride = io.default_mode, + + /// Set this to allow the opened file to automatically become the + /// controlling TTY for the current process. + allow_ctty: bool = false, + + pub fn isRead(self: OpenFlags) bool { + return self.mode != .write_only; + } + + pub fn isWrite(self: OpenFlags) bool { + return self.mode != .read_only; + } +}; + +pub const CreateFlags = struct { + /// Whether the file will be created with read access. + read: bool = false, + + /// If the file already exists, and is a regular file, and the access + /// mode allows writing, it will be truncated to length 0. + truncate: bool = true, + + /// Ensures that this open call creates the file, otherwise causes + /// `error.PathAlreadyExists` to be returned. + exclusive: bool = false, + + /// Open the file with an advisory lock to coordinate with other processes + /// accessing it at the same time. An exclusive lock will prevent other + /// processes from acquiring a lock. A shared lock will prevent other + /// processes from acquiring a exclusive lock, but does not prevent + /// other process from getting their own shared locks. + /// + /// The lock is advisory, except on Linux in very specific circumstances[1]. + /// This means that a process that does not respect the locking API can still get access + /// to the file, despite the lock. + /// + /// On these operating systems, the lock is acquired atomically with + /// opening the file: + /// * Darwin + /// * DragonFlyBSD + /// * FreeBSD + /// * Haiku + /// * NetBSD + /// * OpenBSD + /// On these operating systems, the lock is acquired via a separate syscall + /// after opening the file: + /// * Linux + /// * Windows + /// + /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt + lock: Lock = .none, + + /// Sets whether or not to wait until the file is locked to return. If set to true, + /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file + /// is available to proceed. + /// In async I/O mode, non-blocking at the OS level is + /// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned, + /// and `false` means `error.WouldBlock` is handled by the event loop. + lock_nonblocking: bool = false, + + /// For POSIX systems this is the file system mode the file will + /// be created with. On other systems this is always 0. + mode: Mode = default_mode, + + /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even + /// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions + /// related to opening the file, reading, writing, and locking. + intended_io_mode: io.ModeOverride = io.default_mode, +}; + +/// Upon success, the stream is in an uninitialized state. To continue using it, +/// you must use the open() function. +pub fn close(self: File) void { + if (is_windows) { + windows.CloseHandle(self.handle); + } else if (self.capable_io_mode != self.intended_io_mode) { + std.event.Loop.instance.?.close(self.handle); + } else { + posix.close(self.handle); + } +} + +pub const SyncError = posix.SyncError; + +/// Blocks until all pending file contents and metadata modifications +/// for the file have been synchronized with the underlying filesystem. +/// +/// Note that this does not ensure that metadata for the +/// directory containing the file has also reached disk. +pub fn sync(self: File) SyncError!void { + return posix.fsync(self.handle); +} + +/// Test whether the file refers to a terminal. +/// See also `supportsAnsiEscapeCodes`. +pub fn isTty(self: File) bool { + return posix.isatty(self.handle); +} + +/// Test whether ANSI escape codes will be treated as such. +pub fn supportsAnsiEscapeCodes(self: File) bool { + if (builtin.os.tag == .windows) { + var console_mode: windows.DWORD = 0; + if (windows.kernel32.GetConsoleMode(self.handle, &console_mode) != 0) { + if (console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true; + } + + return posix.isCygwinPty(self.handle); + } + if (builtin.os.tag == .wasi) { + // WASI sanitizes stdout when fd is a tty so ANSI escape codes + // will not be interpreted as actual cursor commands, and + // stderr is always sanitized. + return false; + } + if (self.isTty()) { + if (self.handle == posix.STDOUT_FILENO or self.handle == posix.STDERR_FILENO) { + if (posix.getenvZ("TERM")) |term| { + if (std.mem.eql(u8, term, "dumb")) + return false; + } + } + return true; + } + return false; +} + +pub const SetEndPosError = posix.TruncateError; + +/// Shrinks or expands the file. +/// The file offset after this call is left unchanged. +pub fn setEndPos(self: File, length: u64) SetEndPosError!void { + try posix.ftruncate(self.handle, length); +} + +pub const SeekError = posix.SeekError; + +/// Repositions read/write file offset relative to the current offset. +/// TODO: integrate with async I/O +pub fn seekBy(self: File, offset: i64) SeekError!void { + return posix.lseek_CUR(self.handle, offset); +} + +/// Repositions read/write file offset relative to the end. +/// TODO: integrate with async I/O +pub fn seekFromEnd(self: File, offset: i64) SeekError!void { + return posix.lseek_END(self.handle, offset); +} + +/// Repositions read/write file offset relative to the beginning. +/// TODO: integrate with async I/O +pub fn seekTo(self: File, offset: u64) SeekError!void { + return posix.lseek_SET(self.handle, offset); +} + +pub const GetSeekPosError = posix.SeekError || posix.FStatError; + +/// TODO: integrate with async I/O +pub fn getPos(self: File) GetSeekPosError!u64 { + return posix.lseek_CUR_get(self.handle); +} + +/// TODO: integrate with async I/O +pub fn getEndPos(self: File) GetSeekPosError!u64 { + if (builtin.os.tag == .windows) { + return windows.GetFileSizeEx(self.handle); + } + return (try self.stat()).size; +} + +pub const ModeError = posix.FStatError; + +/// TODO: integrate with async I/O +pub fn mode(self: File) ModeError!Mode { + if (builtin.os.tag == .windows) { + return 0; + } + return (try self.stat()).mode; +} + +pub const Stat = struct { + /// A number that the system uses to point to the file metadata. This + /// number is not guaranteed to be unique across time, as some file + /// systems may reuse an inode after its file has been deleted. Some + /// systems may change the inode of a file over time. + /// + /// On Linux, the inode is a structure that stores the metadata, and + /// the inode _number_ is what you see here: the index number of the + /// inode. + /// + /// The FileIndex on Windows is similar. It is a number for a file that + /// is unique to each filesystem. + inode: INode, + size: u64, + /// This is available on POSIX systems and is always 0 otherwise. + mode: Mode, + kind: Kind, + + /// Access time in nanoseconds, relative to UTC 1970-01-01. + atime: i128, + /// Last modification time in nanoseconds, relative to UTC 1970-01-01. + mtime: i128, + /// Creation time in nanoseconds, relative to UTC 1970-01-01. + ctime: i128, + + pub fn fromSystem(st: posix.system.Stat) Stat { + const atime = st.atime(); + const mtime = st.mtime(); + const ctime = st.ctime(); + const kind: Kind = if (builtin.os.tag == .wasi and !builtin.link_libc) switch (st.filetype) { + .BLOCK_DEVICE => .block_device, + .CHARACTER_DEVICE => .character_device, + .DIRECTORY => .directory, + .SYMBOLIC_LINK => .sym_link, + .REGULAR_FILE => .file, + .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, + else => .unknown, + } else blk: { + const m = st.mode & posix.S.IFMT; + switch (m) { + posix.S.IFBLK => break :blk .block_device, + posix.S.IFCHR => break :blk .character_device, + posix.S.IFDIR => break :blk .directory, + posix.S.IFIFO => break :blk .named_pipe, + posix.S.IFLNK => break :blk .sym_link, + posix.S.IFREG => break :blk .file, + posix.S.IFSOCK => break :blk .unix_domain_socket, + else => {}, + } + if (builtin.os.tag.isSolarish()) switch (m) { + posix.S.IFDOOR => break :blk .door, + posix.S.IFPORT => break :blk .event_port, + else => {}, + }; + + break :blk .unknown; + }; + + return Stat{ + .inode = st.ino, + .size = @as(u64, @bitCast(st.size)), + .mode = st.mode, + .kind = kind, + .atime = @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec, + .mtime = @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec, + .ctime = @as(i128, ctime.tv_sec) * std.time.ns_per_s + ctime.tv_nsec, + }; + } +}; + +pub const StatError = posix.FStatError; + +/// TODO: integrate with async I/O +pub fn stat(self: File) StatError!Stat { + if (builtin.os.tag == .windows) { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + var info: windows.FILE_ALL_INFORMATION = undefined; + const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); + switch (rc) { + .SUCCESS => {}, + // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer + // size provided. This is treated as success because the type of variable-length information that this would be relevant for + // (name, volume name, etc) we don't care about. + .BUFFER_OVERFLOW => {}, + .INVALID_PARAMETER => unreachable, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + return Stat{ + .inode = info.InternalInformation.IndexNumber, + .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), + .mode = 0, + .kind = if (info.StandardInformation.Directory == 0) .file else .directory, + .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), + .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), + .ctime = windows.fromSysTime(info.BasicInformation.CreationTime), + }; + } + + const st = try posix.fstat(self.handle); + return Stat.fromSystem(st); +} + +pub const ChmodError = posix.FChmodError; + +/// Changes the mode of the file. +/// The process must have the correct privileges in order to do this +/// successfully, or must have the effective user ID matching the owner +/// of the file. +pub fn chmod(self: File, new_mode: Mode) ChmodError!void { + try posix.fchmod(self.handle, new_mode); +} + +pub const ChownError = posix.FChownError; + +/// Changes the owner and group of the file. +/// The process must have the correct privileges in order to do this +/// successfully. The group may be changed by the owner of the file to +/// any group of which the owner is a member. If the owner or group is +/// specified as `null`, the ID is not changed. +pub fn chown(self: File, owner: ?Uid, group: ?Gid) ChownError!void { + try posix.fchown(self.handle, owner, group); +} + +/// Cross-platform representation of permissions on a file. +/// The `readonly` and `setReadonly` are the only methods available across all platforms. +/// Platform-specific functionality is available through the `inner` field. +pub const Permissions = struct { + /// You may use the `inner` field to use platform-specific functionality + inner: switch (builtin.os.tag) { + .windows => PermissionsWindows, + else => PermissionsUnix, + }, + + const Self = @This(); + + /// Returns `true` if permissions represent an unwritable file. + /// On Unix, `true` is returned only if no class has write permissions. + pub fn readOnly(self: Self) bool { + return self.inner.readOnly(); + } + + /// Sets whether write permissions are provided. + /// On Unix, this affects *all* classes. If this is undesired, use `unixSet`. + /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` + pub fn setReadOnly(self: *Self, read_only: bool) void { + self.inner.setReadOnly(read_only); + } +}; + +pub const PermissionsWindows = struct { + attributes: windows.DWORD, + + const Self = @This(); + + /// Returns `true` if permissions represent an unwritable file. + pub fn readOnly(self: Self) bool { + return self.attributes & windows.FILE_ATTRIBUTE_READONLY != 0; + } + + /// Sets whether write permissions are provided. + /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` + pub fn setReadOnly(self: *Self, read_only: bool) void { + if (read_only) { + self.attributes |= windows.FILE_ATTRIBUTE_READONLY; + } else { + self.attributes &= ~@as(windows.DWORD, windows.FILE_ATTRIBUTE_READONLY); + } + } +}; + +pub const PermissionsUnix = struct { + mode: Mode, + + const Self = @This(); + + /// Returns `true` if permissions represent an unwritable file. + /// `true` is returned only if no class has write permissions. + pub fn readOnly(self: Self) bool { + return self.mode & 0o222 == 0; + } + + /// Sets whether write permissions are provided. + /// This affects *all* classes. If this is undesired, use `unixSet`. + /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` + pub fn setReadOnly(self: *Self, read_only: bool) void { + if (read_only) { + self.mode &= ~@as(Mode, 0o222); + } else { + self.mode |= @as(Mode, 0o222); + } + } + + pub const Class = enum(u2) { + user = 2, + group = 1, + other = 0, + }; + + pub const Permission = enum(u3) { + read = 0o4, + write = 0o2, + execute = 0o1, + }; + + /// Returns `true` if the chosen class has the selected permission. + /// This method is only available on Unix platforms. + pub fn unixHas(self: Self, class: Class, permission: Permission) bool { + const mask = @as(Mode, @intFromEnum(permission)) << @as(u3, @intFromEnum(class)) * 3; + return self.mode & mask != 0; + } + + /// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged. + /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` + pub fn unixSet(self: *Self, class: Class, permissions: struct { + read: ?bool = null, + write: ?bool = null, + execute: ?bool = null, + }) void { + const shift = @as(u3, @intFromEnum(class)) * 3; + if (permissions.read) |r| { + if (r) { + self.mode |= @as(Mode, 0o4) << shift; + } else { + self.mode &= ~(@as(Mode, 0o4) << shift); + } + } + if (permissions.write) |w| { + if (w) { + self.mode |= @as(Mode, 0o2) << shift; + } else { + self.mode &= ~(@as(Mode, 0o2) << shift); + } + } + if (permissions.execute) |x| { + if (x) { + self.mode |= @as(Mode, 0o1) << shift; + } else { + self.mode &= ~(@as(Mode, 0o1) << shift); + } + } + } + + /// Returns a `Permissions` struct representing the permissions from the passed mode. + pub fn unixNew(new_mode: Mode) Self { + return Self{ + .mode = new_mode, + }; + } +}; + +pub const SetPermissionsError = ChmodError; + +/// Sets permissions according to the provided `Permissions` struct. +/// This method is *NOT* available on WASI +pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void { + switch (builtin.os.tag) { + .windows => { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + var info = windows.FILE_BASIC_INFORMATION{ + .CreationTime = 0, + .LastAccessTime = 0, + .LastWriteTime = 0, + .ChangeTime = 0, + .FileAttributes = permissions.inner.attributes, + }; + const rc = windows.ntdll.NtSetInformationFile( + self.handle, + &io_status_block, + &info, + @sizeOf(windows.FILE_BASIC_INFORMATION), + .FileBasicInformation, + ); + switch (rc) { + .SUCCESS => return, + .INVALID_HANDLE => unreachable, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + }, + .wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod + else => { + try self.chmod(permissions.inner.mode); + }, + } +} + +/// Cross-platform representation of file metadata. +/// Platform-specific functionality is available through the `inner` field. +pub const Metadata = struct { + /// You may use the `inner` field to use platform-specific functionality + inner: switch (builtin.os.tag) { + .windows => MetadataWindows, + .linux => MetadataLinux, + else => MetadataUnix, + }, + + const Self = @This(); + + /// Returns the size of the file + pub fn size(self: Self) u64 { + return self.inner.size(); + } + + /// Returns a `Permissions` struct, representing the permissions on the file + pub fn permissions(self: Self) Permissions { + return self.inner.permissions(); + } + + /// Returns the `Kind` of file. + /// On Windows, can only return: `.file`, `.directory`, `.sym_link` or `.unknown` + pub fn kind(self: Self) Kind { + return self.inner.kind(); + } + + /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 + pub fn accessed(self: Self) i128 { + return self.inner.accessed(); + } + + /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01 + pub fn modified(self: Self) i128 { + return self.inner.modified(); + } + + /// Returns the time the file was created in nanoseconds since UTC 1970-01-01 + /// On Windows, this cannot return null + /// On Linux, this returns null if the filesystem does not support creation times, or if the kernel is older than 4.11 + /// On Unices, this returns null if the filesystem or OS does not support creation times + /// 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 + pub fn created(self: Self) ?i128 { + return self.inner.created(); + } +}; + +pub const MetadataUnix = struct { + stat: posix.Stat, + + const Self = @This(); + + /// Returns the size of the file + pub fn size(self: Self) u64 { + return @as(u64, @intCast(self.stat.size)); + } + + /// Returns a `Permissions` struct, representing the permissions on the file + pub fn permissions(self: Self) Permissions { + return Permissions{ .inner = PermissionsUnix{ .mode = self.stat.mode } }; + } + + /// Returns the `Kind` of the file + pub fn kind(self: Self) Kind { + if (builtin.os.tag == .wasi and !builtin.link_libc) return switch (self.stat.filetype) { + .BLOCK_DEVICE => .block_device, + .CHARACTER_DEVICE => .character_device, + .DIRECTORY => .directory, + .SYMBOLIC_LINK => .sym_link, + .REGULAR_FILE => .file, + .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, + else => .unknown, + }; + + const m = self.stat.mode & posix.S.IFMT; + + switch (m) { + posix.S.IFBLK => return .block_device, + posix.S.IFCHR => return .character_device, + posix.S.IFDIR => return .directory, + posix.S.IFIFO => return .named_pipe, + posix.S.IFLNK => return .sym_link, + posix.S.IFREG => return .file, + posix.S.IFSOCK => return .unix_domain_socket, + else => {}, + } + + if (builtin.os.tag.isSolarish()) switch (m) { + posix.S.IFDOOR => return .door, + posix.S.IFPORT => return .event_port, + else => {}, + }; + + return .unknown; + } + + /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 + pub fn accessed(self: Self) i128 { + const atime = self.stat.atime(); + return @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec; + } + + /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01 + pub fn modified(self: Self) i128 { + const mtime = self.stat.mtime(); + return @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec; + } + + /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. + /// Returns null if this is not supported by the OS or filesystem + pub fn created(self: Self) ?i128 { + if (!@hasDecl(@TypeOf(self.stat), "birthtime")) return null; + const birthtime = self.stat.birthtime(); + + // If the filesystem doesn't support this the value *should* be: + // On FreeBSD: tv_nsec = 0, tv_sec = -1 + // On NetBSD and OpenBSD: tv_nsec = 0, tv_sec = 0 + // On MacOS, it is set to ctime -- we cannot detect this!! + switch (builtin.os.tag) { + .freebsd => if (birthtime.tv_sec == -1 and birthtime.tv_nsec == 0) return null, + .netbsd, .openbsd => if (birthtime.tv_sec == 0 and birthtime.tv_nsec == 0) return null, + .macos => {}, + else => @compileError("Creation time detection not implemented for OS"), + } + + return @as(i128, birthtime.tv_sec) * std.time.ns_per_s + birthtime.tv_nsec; + } +}; + +/// `MetadataUnix`, but using Linux's `statx` syscall. +/// On Linux versions below 4.11, `statx` will be filled with data from stat. +pub const MetadataLinux = struct { + statx: std.os.linux.Statx, + + const Self = @This(); + + /// Returns the size of the file + pub fn size(self: Self) u64 { + return self.statx.size; + } + + /// Returns a `Permissions` struct, representing the permissions on the file + pub fn permissions(self: Self) Permissions { + return Permissions{ .inner = PermissionsUnix{ .mode = self.statx.mode } }; + } + + /// Returns the `Kind` of the file + pub fn kind(self: Self) Kind { + const m = self.statx.mode & posix.S.IFMT; + + switch (m) { + posix.S.IFBLK => return .block_device, + posix.S.IFCHR => return .character_device, + posix.S.IFDIR => return .directory, + posix.S.IFIFO => return .named_pipe, + posix.S.IFLNK => return .sym_link, + posix.S.IFREG => return .file, + posix.S.IFSOCK => return .unix_domain_socket, + else => {}, + } + + return .unknown; + } + + /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 + pub fn accessed(self: Self) i128 { + return @as(i128, self.statx.atime.tv_sec) * std.time.ns_per_s + self.statx.atime.tv_nsec; + } + + /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01 + pub fn modified(self: Self) i128 { + return @as(i128, self.statx.mtime.tv_sec) * std.time.ns_per_s + self.statx.mtime.tv_nsec; + } + + /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. + /// Returns null if this is not supported by the filesystem, or on kernels before than version 4.11 + pub fn created(self: Self) ?i128 { + if (self.statx.mask & std.os.linux.STATX_BTIME == 0) return null; + return @as(i128, self.statx.btime.tv_sec) * std.time.ns_per_s + self.statx.btime.tv_nsec; + } +}; + +pub const MetadataWindows = struct { + attributes: windows.DWORD, + reparse_tag: windows.DWORD, + _size: u64, + access_time: i128, + modified_time: i128, + creation_time: i128, + + const Self = @This(); + + /// Returns the size of the file + pub fn size(self: Self) u64 { + return self._size; + } + + /// Returns a `Permissions` struct, representing the permissions on the file + pub fn permissions(self: Self) Permissions { + return Permissions{ .inner = PermissionsWindows{ .attributes = self.attributes } }; + } + + /// Returns the `Kind` of the file. + /// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown` + pub fn kind(self: Self) Kind { + if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { + if (self.reparse_tag & 0x20000000 != 0) { + return .sym_link; + } + } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) { + return .directory; + } else { + return .file; + } + return .unknown; + } + + /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 + pub fn accessed(self: Self) i128 { + return self.access_time; + } + + /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01 + pub fn modified(self: Self) i128 { + return self.modified_time; + } + + /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. + /// This never returns null, only returning an optional for compatibility with other OSes + pub fn created(self: Self) ?i128 { + return self.creation_time; + } +}; + +pub const MetadataError = posix.FStatError; + +pub fn metadata(self: File) MetadataError!Metadata { + return Metadata{ + .inner = switch (builtin.os.tag) { + .windows => blk: { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + var info: windows.FILE_ALL_INFORMATION = undefined; + + const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); + switch (rc) { + .SUCCESS => {}, + // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer + // size provided. This is treated as success because the type of variable-length information that this would be relevant for + // (name, volume name, etc) we don't care about. + .BUFFER_OVERFLOW => {}, + .INVALID_PARAMETER => unreachable, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + + const reparse_tag: windows.DWORD = reparse_blk: { + if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { + var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; + try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]); + const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0])); + break :reparse_blk reparse_struct.ReparseTag; + } + break :reparse_blk 0; + }; + + break :blk MetadataWindows{ + .attributes = info.BasicInformation.FileAttributes, + .reparse_tag = reparse_tag, + ._size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), + .access_time = windows.fromSysTime(info.BasicInformation.LastAccessTime), + .modified_time = windows.fromSysTime(info.BasicInformation.LastWriteTime), + .creation_time = windows.fromSysTime(info.BasicInformation.CreationTime), + }; + }, + .linux => blk: { + const l = std.os.linux; + var stx = std.mem.zeroes(l.Statx); + const rcx = l.statx(self.handle, "\x00", l.AT.EMPTY_PATH, l.STATX_TYPE | + l.STATX_MODE | l.STATX_ATIME | l.STATX_MTIME | l.STATX_BTIME, &stx); + + switch (posix.errno(rcx)) { + .SUCCESS => {}, + // NOSYS happens when `statx` is unsupported, which is the case on kernel versions before 4.11 + // Here, we call `fstat` and fill `stx` with the data we need + .NOSYS => { + const st = try posix.fstat(self.handle); + + stx.mode = @as(u16, @intCast(st.mode)); + + // Hacky conversion from timespec to statx_timestamp + stx.atime = std.mem.zeroes(l.statx_timestamp); + stx.atime.tv_sec = st.atim.tv_sec; + stx.atime.tv_nsec = @as(u32, @intCast(st.atim.tv_nsec)); // Guaranteed to succeed (tv_nsec is always below 10^9) + + stx.mtime = std.mem.zeroes(l.statx_timestamp); + stx.mtime.tv_sec = st.mtim.tv_sec; + stx.mtime.tv_nsec = @as(u32, @intCast(st.mtim.tv_nsec)); + + stx.mask = l.STATX_BASIC_STATS | l.STATX_MTIME; + }, + .BADF => unreachable, + .FAULT => unreachable, + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + + break :blk MetadataLinux{ + .statx = stx, + }; + }, + else => blk: { + const st = try posix.fstat(self.handle); + break :blk MetadataUnix{ + .stat = st, + }; + }, + }, + }; +} + +pub const UpdateTimesError = posix.FutimensError || windows.SetFileTimeError; + +/// The underlying file system may have a different granularity than nanoseconds, +/// and therefore this function cannot guarantee any precision will be stored. +/// Further, the maximum value is limited by the system ABI. When a value is provided +/// that exceeds this range, the value is clamped to the maximum. +/// TODO: integrate with async I/O +pub fn updateTimes( + self: File, + /// access timestamp in nanoseconds + atime: i128, + /// last modification timestamp in nanoseconds + mtime: i128, +) UpdateTimesError!void { + if (builtin.os.tag == .windows) { + const atime_ft = windows.nanoSecondsToFileTime(atime); + const mtime_ft = windows.nanoSecondsToFileTime(mtime); + return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft); + } + const times = [2]posix.timespec{ + posix.timespec{ + .tv_sec = math.cast(isize, @divFloor(atime, std.time.ns_per_s)) orelse maxInt(isize), + .tv_nsec = math.cast(isize, @mod(atime, std.time.ns_per_s)) orelse maxInt(isize), + }, + posix.timespec{ + .tv_sec = math.cast(isize, @divFloor(mtime, std.time.ns_per_s)) orelse maxInt(isize), + .tv_nsec = math.cast(isize, @mod(mtime, std.time.ns_per_s)) orelse maxInt(isize), + }, + }; + try posix.futimens(self.handle, ×); +} + +/// Reads all the bytes from the current position to the end of the file. +/// On success, caller owns returned buffer. +/// If the file is larger than `max_bytes`, returns `error.FileTooBig`. +pub fn readToEndAlloc(self: File, allocator: Allocator, max_bytes: usize) ![]u8 { + return self.readToEndAllocOptions(allocator, max_bytes, null, @alignOf(u8), null); +} + +/// Reads all the bytes from the current position to the end of the file. +/// On success, caller owns returned buffer. +/// If the file is larger than `max_bytes`, returns `error.FileTooBig`. +/// If `size_hint` is specified the initial buffer size is calculated using +/// that value, otherwise an arbitrary value is used instead. +/// Allows specifying alignment and a sentinel value. +pub fn readToEndAllocOptions( + self: File, + allocator: Allocator, + max_bytes: usize, + size_hint: ?usize, + comptime alignment: u29, + comptime optional_sentinel: ?u8, +) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) { + // If no size hint is provided fall back to the size=0 code path + const size = size_hint orelse 0; + + // The file size returned by stat is used as hint to set the buffer + // size. If the reported size is zero, as it happens on Linux for files + // in /proc, a small buffer is allocated instead. + const initial_cap = (if (size > 0) size else 1024) + @intFromBool(optional_sentinel != null); + var array_list = try std.ArrayListAligned(u8, alignment).initCapacity(allocator, initial_cap); + defer array_list.deinit(); + + self.reader().readAllArrayListAligned(alignment, &array_list, max_bytes) catch |err| switch (err) { + error.StreamTooLong => return error.FileTooBig, + else => |e| return e, + }; + + if (optional_sentinel) |sentinel| { + return try array_list.toOwnedSliceSentinel(sentinel); + } else { + return try array_list.toOwnedSlice(); + } +} + +pub const ReadError = posix.ReadError; +pub const PReadError = posix.PReadError; + +pub fn read(self: File, buffer: []u8) ReadError!usize { + if (is_windows) { + return windows.ReadFile(self.handle, buffer, null, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.read(self.handle, buffer); + } else { + return std.event.Loop.instance.?.read(self.handle, buffer, self.capable_io_mode != self.intended_io_mode); + } +} + +/// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it +/// means the file reached the end. Reaching the end of a file is not an error condition. +pub fn readAll(self: File, buffer: []u8) ReadError!usize { + var index: usize = 0; + while (index != buffer.len) { + const amt = try self.read(buffer[index..]); + if (amt == 0) break; + index += amt; + } + return index; +} + +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize { + if (is_windows) { + return windows.ReadFile(self.handle, buffer, offset, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.pread(self.handle, buffer, offset); + } else { + return std.event.Loop.instance.?.pread(self.handle, buffer, offset, self.capable_io_mode != self.intended_io_mode); + } +} + +/// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it +/// means the file reached the end. Reaching the end of a file is not an error condition. +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize { + var index: usize = 0; + while (index != buffer.len) { + const amt = try self.pread(buffer[index..], offset + index); + if (amt == 0) break; + index += amt; + } + return index; +} + +/// See https://github.com/ziglang/zig/issues/7699 +pub fn readv(self: File, iovecs: []const posix.iovec) ReadError!usize { + if (is_windows) { + // TODO improve this to use ReadFileScatter + if (iovecs.len == 0) return @as(usize, 0); + const first = iovecs[0]; + return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.readv(self.handle, iovecs); + } else { + return std.event.Loop.instance.?.readv(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode); + } +} + +/// Returns the number of bytes read. If the number read is smaller than the total bytes +/// from all the buffers, it means the file reached the end. Reaching the end of a file +/// is not an error condition. +/// +/// The `iovecs` parameter is mutable because: +/// * This function needs to mutate the fields in order to handle partial +/// reads from the underlying OS layer. +/// * The OS layer expects pointer addresses to be inside the application's address space +/// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer +/// addresses when the length is zero. So this function modifies the iov_base fields +/// when the length is zero. +/// +/// Related open issue: https://github.com/ziglang/zig/issues/7699 +pub fn readvAll(self: File, iovecs: []posix.iovec) ReadError!usize { + if (iovecs.len == 0) return 0; + + // We use the address of this local variable for all zero-length + // vectors so that the OS does not complain that we are giving it + // addresses outside the application's address space. + var garbage: [1]u8 = undefined; + for (iovecs) |*v| { + if (v.iov_len == 0) v.iov_base = &garbage; + } + + var i: usize = 0; + var off: usize = 0; + while (true) { + var amt = try self.readv(iovecs[i..]); + var eof = amt == 0; + off += amt; + while (amt >= iovecs[i].iov_len) { + amt -= iovecs[i].iov_len; + i += 1; + if (i >= iovecs.len) return off; + eof = false; + } + if (eof) return off; + iovecs[i].iov_base += amt; + iovecs[i].iov_len -= amt; + } +} + +/// See https://github.com/ziglang/zig/issues/7699 +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn preadv(self: File, iovecs: []const posix.iovec, offset: u64) PReadError!usize { + if (is_windows) { + // TODO improve this to use ReadFileScatter + if (iovecs.len == 0) return @as(usize, 0); + const first = iovecs[0]; + return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.preadv(self.handle, iovecs, offset); + } else { + return std.event.Loop.instance.?.preadv(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode); + } +} + +/// Returns the number of bytes read. If the number read is smaller than the total bytes +/// from all the buffers, it means the file reached the end. Reaching the end of a file +/// is not an error condition. +/// The `iovecs` parameter is mutable because this function needs to mutate the fields in +/// order to handle partial reads from the underlying OS layer. +/// See https://github.com/ziglang/zig/issues/7699 +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn preadvAll(self: File, iovecs: []posix.iovec, offset: u64) PReadError!usize { + if (iovecs.len == 0) return 0; + + var i: usize = 0; + var off: usize = 0; + while (true) { + var amt = try self.preadv(iovecs[i..], offset + off); + var eof = amt == 0; + off += amt; + while (amt >= iovecs[i].iov_len) { + amt -= iovecs[i].iov_len; + i += 1; + if (i >= iovecs.len) return off; + eof = false; + } + if (eof) return off; + iovecs[i].iov_base += amt; + iovecs[i].iov_len -= amt; + } +} + +pub const WriteError = posix.WriteError; +pub const PWriteError = posix.PWriteError; + +pub fn write(self: File, bytes: []const u8) WriteError!usize { + if (is_windows) { + return windows.WriteFile(self.handle, bytes, null, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.write(self.handle, bytes); + } else { + return std.event.Loop.instance.?.write(self.handle, bytes, self.capable_io_mode != self.intended_io_mode); + } +} + +pub fn writeAll(self: File, bytes: []const u8) WriteError!void { + var index: usize = 0; + while (index < bytes.len) { + index += try self.write(bytes[index..]); + } +} + +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize { + if (is_windows) { + return windows.WriteFile(self.handle, bytes, offset, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.pwrite(self.handle, bytes, offset); + } else { + return std.event.Loop.instance.?.pwrite(self.handle, bytes, offset, self.capable_io_mode != self.intended_io_mode); + } +} + +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void { + var index: usize = 0; + while (index < bytes.len) { + index += try self.pwrite(bytes[index..], offset + index); + } +} + +/// See https://github.com/ziglang/zig/issues/7699 +/// See equivalent function: `std.net.Stream.writev`. +pub fn writev(self: File, iovecs: []const posix.iovec_const) WriteError!usize { + if (is_windows) { + // TODO improve this to use WriteFileScatter + if (iovecs.len == 0) return @as(usize, 0); + const first = iovecs[0]; + return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.writev(self.handle, iovecs); + } else { + return std.event.Loop.instance.?.writev(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode); + } +} + +/// The `iovecs` parameter is mutable because: +/// * This function needs to mutate the fields in order to handle partial +/// writes from the underlying OS layer. +/// * The OS layer expects pointer addresses to be inside the application's address space +/// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer +/// addresses when the length is zero. So this function modifies the iov_base fields +/// when the length is zero. +/// See https://github.com/ziglang/zig/issues/7699 +/// See equivalent function: `std.net.Stream.writevAll`. +pub fn writevAll(self: File, iovecs: []posix.iovec_const) WriteError!void { + if (iovecs.len == 0) return; + + // We use the address of this local variable for all zero-length + // vectors so that the OS does not complain that we are giving it + // addresses outside the application's address space. + var garbage: [1]u8 = undefined; + for (iovecs) |*v| { + if (v.iov_len == 0) v.iov_base = &garbage; + } + + var i: usize = 0; + while (true) { + var amt = try self.writev(iovecs[i..]); + while (amt >= iovecs[i].iov_len) { + amt -= iovecs[i].iov_len; + i += 1; + if (i >= iovecs.len) return; + } + iovecs[i].iov_base += amt; + iovecs[i].iov_len -= amt; + } +} + +/// See https://github.com/ziglang/zig/issues/7699 +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn pwritev(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError!usize { + if (is_windows) { + // TODO improve this to use WriteFileScatter + if (iovecs.len == 0) return @as(usize, 0); + const first = iovecs[0]; + return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.pwritev(self.handle, iovecs, offset); + } else { + return std.event.Loop.instance.?.pwritev(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode); + } +} + +/// The `iovecs` parameter is mutable because this function needs to mutate the fields in +/// order to handle partial writes from the underlying OS layer. +/// See https://github.com/ziglang/zig/issues/7699 +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn pwritevAll(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError!void { + if (iovecs.len == 0) return; + + var i: usize = 0; + var off: u64 = 0; + while (true) { + var amt = try self.pwritev(iovecs[i..], offset + off); + off += amt; + while (amt >= iovecs[i].iov_len) { + amt -= iovecs[i].iov_len; + i += 1; + if (i >= iovecs.len) return; + } + iovecs[i].iov_base += amt; + iovecs[i].iov_len -= amt; + } +} + +pub const CopyRangeError = posix.CopyFileRangeError; + +pub fn copyRange(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { + const adjusted_len = math.cast(usize, len) orelse maxInt(usize); + const result = try posix.copy_file_range(in.handle, in_offset, out.handle, out_offset, adjusted_len, 0); + return result; +} + +/// Returns the number of bytes copied. If the number read is smaller than `buffer.len`, it +/// means the in file reached the end. Reaching the end of a file is not an error condition. +pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { + var total_bytes_copied: u64 = 0; + var in_off = in_offset; + var out_off = out_offset; + while (total_bytes_copied < len) { + const amt_copied = try copyRange(in, in_off, out, out_off, len - total_bytes_copied); + if (amt_copied == 0) return total_bytes_copied; + total_bytes_copied += amt_copied; + in_off += amt_copied; + out_off += amt_copied; + } + return total_bytes_copied; +} + +pub const WriteFileOptions = struct { + in_offset: u64 = 0, + + /// `null` means the entire file. `0` means no bytes from the file. + /// When this is `null`, trailers must be sent in a separate writev() call + /// due to a flaw in the BSD sendfile API. Other operating systems, such as + /// Linux, already do this anyway due to API limitations. + /// If the size of the source file is known, passing the size here will save one syscall. + in_len: ?u64 = null, + + headers_and_trailers: []posix.iovec_const = &[0]posix.iovec_const{}, + + /// The trailer count is inferred from `headers_and_trailers.len - header_count` + header_count: usize = 0, +}; + +pub const WriteFileError = ReadError || error{EndOfStream} || WriteError; + +pub fn writeFileAll(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void { + return self.writeFileAllSendfile(in_file, args) catch |err| switch (err) { + error.Unseekable, + error.FastOpenAlreadyInProgress, + error.MessageTooBig, + error.FileDescriptorNotASocket, + error.NetworkUnreachable, + error.NetworkSubsystemFailed, + => return self.writeFileAllUnseekable(in_file, args), + + else => |e| return e, + }; +} + +/// Does not try seeking in either of the File parameters. +/// See `writeFileAll` as an alternative to calling this. +pub fn writeFileAllUnseekable(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void { + const headers = args.headers_and_trailers[0..args.header_count]; + const trailers = args.headers_and_trailers[args.header_count..]; + + try self.writevAll(headers); + + try in_file.reader().skipBytes(args.in_offset, .{ .buf_size = 4096 }); + + var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init(); + if (args.in_len) |len| { + var stream = std.io.limitedReader(in_file.reader(), len); + try fifo.pump(stream.reader(), self.writer()); + } else { + try fifo.pump(in_file.reader(), self.writer()); + } + + try self.writevAll(trailers); +} + +/// Low level function which can fail for OS-specific reasons. +/// See `writeFileAll` as an alternative to calling this. +/// TODO integrate with async I/O +fn writeFileAllSendfile(self: File, in_file: File, args: WriteFileOptions) posix.SendFileError!void { + const count = blk: { + if (args.in_len) |l| { + if (l == 0) { + return self.writevAll(args.headers_and_trailers); + } else { + break :blk l; + } + } else { + break :blk 0; + } + }; + const headers = args.headers_and_trailers[0..args.header_count]; + const trailers = args.headers_and_trailers[args.header_count..]; + const zero_iovec = &[0]posix.iovec_const{}; + // When reading the whole file, we cannot put the trailers in the sendfile() syscall, + // because we have no way to determine whether a partial write is past the end of the file or not. + const trls = if (count == 0) zero_iovec else trailers; + const offset = args.in_offset; + const out_fd = self.handle; + const in_fd = in_file.handle; + const flags = 0; + var amt: usize = 0; + hdrs: { + var i: usize = 0; + while (i < headers.len) { + amt = try posix.sendfile(out_fd, in_fd, offset, count, headers[i..], trls, flags); + while (amt >= headers[i].iov_len) { + amt -= headers[i].iov_len; + i += 1; + if (i >= headers.len) break :hdrs; + } + headers[i].iov_base += amt; + headers[i].iov_len -= amt; + } + } + if (count == 0) { + var off: u64 = amt; + while (true) { + amt = try posix.sendfile(out_fd, in_fd, offset + off, 0, zero_iovec, zero_iovec, flags); + if (amt == 0) break; + off += amt; + } + } else { + var off: u64 = amt; + while (off < count) { + amt = try posix.sendfile(out_fd, in_fd, offset + off, count - off, zero_iovec, trailers, flags); + off += amt; + } + amt = @as(usize, @intCast(off - count)); + } + var i: usize = 0; + while (i < trailers.len) { + while (amt >= trailers[i].iov_len) { + amt -= trailers[i].iov_len; + i += 1; + if (i >= trailers.len) return; + } + trailers[i].iov_base += amt; + trailers[i].iov_len -= amt; + amt = try posix.writev(self.handle, trailers[i..]); + } +} + +pub const Reader = io.Reader(File, ReadError, read); + +pub fn reader(file: File) Reader { + return .{ .context = file }; +} + +pub const Writer = io.Writer(File, WriteError, write); + +pub fn writer(file: File) Writer { + return .{ .context = file }; +} + +pub const SeekableStream = io.SeekableStream( + File, + SeekError, + GetSeekPosError, + seekTo, + seekBy, + getPos, + getEndPos, +); + +pub fn seekableStream(file: File) SeekableStream { + return .{ .context = file }; +} + +const range_off: windows.LARGE_INTEGER = 0; +const range_len: windows.LARGE_INTEGER = 1; + +pub const LockError = error{ + SystemResources, + FileLocksNotSupported, +} || posix.UnexpectedError; + +/// Blocks when an incompatible lock is held by another process. +/// A process may hold only one type of lock (shared or exclusive) on +/// a file. When a process terminates in any way, the lock is released. +/// +/// Assumes the file is unlocked. +/// +/// TODO: integrate with async I/O +pub fn lock(file: File, l: Lock) LockError!void { + if (is_windows) { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + const exclusive = switch (l) { + .none => return, + .shared => false, + .exclusive => true, + }; + return windows.LockFile( + file.handle, + null, + null, + null, + &io_status_block, + &range_off, + &range_len, + null, + windows.FALSE, // non-blocking=false + @intFromBool(exclusive), + ) catch |err| switch (err) { + error.WouldBlock => unreachable, // non-blocking=false + else => |e| return e, + }; + } else { + return posix.flock(file.handle, switch (l) { + .none => posix.LOCK.UN, + .shared => posix.LOCK.SH, + .exclusive => posix.LOCK.EX, + }) catch |err| switch (err) { + error.WouldBlock => unreachable, // non-blocking=false + else => |e| return e, + }; + } +} + +/// Assumes the file is locked. +pub fn unlock(file: File) void { + if (is_windows) { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + return windows.UnlockFile( + file.handle, + &io_status_block, + &range_off, + &range_len, + null, + ) catch |err| switch (err) { + error.RangeNotLocked => unreachable, // Function assumes unlocked. + error.Unexpected => unreachable, // Resource deallocation must succeed. + }; + } else { + return posix.flock(file.handle, posix.LOCK.UN) catch |err| switch (err) { + error.WouldBlock => unreachable, // unlocking can't block + error.SystemResources => unreachable, // We are deallocating resources. + error.FileLocksNotSupported => unreachable, // We already got the lock. + error.Unexpected => unreachable, // Resource deallocation must succeed. + }; + } +} + +/// Attempts to obtain a lock, returning `true` if the lock is +/// obtained, and `false` if there was an existing incompatible lock held. +/// A process may hold only one type of lock (shared or exclusive) on +/// a file. When a process terminates in any way, the lock is released. +/// +/// Assumes the file is unlocked. +/// +/// TODO: integrate with async I/O +pub fn tryLock(file: File, l: Lock) LockError!bool { + if (is_windows) { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + const exclusive = switch (l) { + .none => return, + .shared => false, + .exclusive => true, + }; + windows.LockFile( + file.handle, + null, + null, + null, + &io_status_block, + &range_off, + &range_len, + null, + windows.TRUE, // non-blocking=true + @intFromBool(exclusive), + ) catch |err| switch (err) { + error.WouldBlock => return false, + else => |e| return e, + }; + } else { + posix.flock(file.handle, switch (l) { + .none => posix.LOCK.UN, + .shared => posix.LOCK.SH | posix.LOCK.NB, + .exclusive => posix.LOCK.EX | posix.LOCK.NB, + }) catch |err| switch (err) { + error.WouldBlock => return false, + else => |e| return e, + }; + } + return true; +} + +/// Assumes the file is already locked in exclusive mode. +/// Atomically modifies the lock to be in shared mode, without releasing it. +/// +/// TODO: integrate with async I/O +pub fn downgradeLock(file: File) LockError!void { + if (is_windows) { + // On Windows it works like a semaphore + exclusivity flag. To implement this + // function, we first obtain another lock in shared mode. This changes the + // exclusivity flag, but increments the semaphore to 2. So we follow up with + // an NtUnlockFile which decrements the semaphore but does not modify the + // exclusivity flag. + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + windows.LockFile( + file.handle, + null, + null, + null, + &io_status_block, + &range_off, + &range_len, + null, + windows.TRUE, // non-blocking=true + windows.FALSE, // exclusive=false + ) catch |err| switch (err) { + error.WouldBlock => unreachable, // File was not locked in exclusive mode. + else => |e| return e, + }; + return windows.UnlockFile( + file.handle, + &io_status_block, + &range_off, + &range_len, + null, + ) catch |err| switch (err) { + error.RangeNotLocked => unreachable, // File was not locked. + error.Unexpected => unreachable, // Resource deallocation must succeed. + }; + } else { + return posix.flock(file.handle, posix.LOCK.SH | posix.LOCK.NB) catch |err| switch (err) { + error.WouldBlock => unreachable, // File was not locked in exclusive mode. + else => |e| return e, + }; + } +} + +const File = @This(); +const std = @import("../std.zig"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; +// https://github.com/ziglang/zig/issues/5019 +const posix = std.os; +const io = std.io; +const math = std.math; +const assert = std.debug.assert; +const windows = std.os.windows; +const Os = std.builtin.Os; +const maxInt = std.math.maxInt; +const is_windows = builtin.os.tag == .windows; diff --git a/lib/std/fs/file.zig b/lib/std/fs/file.zig deleted file mode 100644 index 3436aa4b33..0000000000 --- a/lib/std/fs/file.zig +++ /dev/null @@ -1,1622 +0,0 @@ -const std = @import("../std.zig"); -const builtin = @import("builtin"); -const os = std.os; -const io = std.io; -const mem = std.mem; -const math = std.math; -const assert = std.debug.assert; -const windows = os.windows; -const Os = std.builtin.Os; -const maxInt = std.math.maxInt; -const is_windows = builtin.os.tag == .windows; - -pub const File = struct { - /// The OS-specific file descriptor or file handle. - handle: Handle, - - /// On some systems, such as Linux, file system file descriptors are incapable - /// of non-blocking I/O. This forces us to perform asynchronous I/O on a dedicated thread, - /// to achieve non-blocking file-system I/O. To do this, `File` must be aware of whether - /// it is a file system file descriptor, or, more specifically, whether the I/O is always - /// blocking. - capable_io_mode: io.ModeOverride = io.default_mode, - - /// Furthermore, even when `std.options.io_mode` is async, it is still sometimes desirable - /// to perform blocking I/O, although not by default. For example, when printing a - /// stack trace to stderr. This field tracks both by acting as an overriding I/O mode. - /// When not building in async I/O mode, the type only has the `.blocking` tag, making - /// it a zero-bit type. - intended_io_mode: io.ModeOverride = io.default_mode, - - pub const Handle = os.fd_t; - pub const Mode = os.mode_t; - pub const INode = os.ino_t; - pub const Uid = os.uid_t; - pub const Gid = os.gid_t; - - pub const Kind = enum { - block_device, - character_device, - directory, - named_pipe, - sym_link, - file, - unix_domain_socket, - whiteout, - door, - event_port, - unknown, - }; - - /// This is the default mode given to POSIX operating systems for creating - /// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first, - /// since most people would expect "-rw-r--r--", for example, when using - /// the `touch` command, which would correspond to `0o644`. However, POSIX - /// libc implementations use `0o666` inside `fopen` and then rely on the - /// process-scoped "umask" setting to adjust this number for file creation. - pub const default_mode = switch (builtin.os.tag) { - .windows => 0, - .wasi => 0, - else => 0o666, - }; - - pub const OpenError = error{ - SharingViolation, - PathAlreadyExists, - FileNotFound, - AccessDenied, - PipeBusy, - NameTooLong, - /// On Windows, file paths must be valid Unicode. - InvalidUtf8, - /// On Windows, file paths cannot contain these characters: - /// '/', '*', '?', '"', '<', '>', '|' - BadPathName, - Unexpected, - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - } || os.OpenError || os.FlockError; - - pub const OpenMode = enum { - read_only, - write_only, - read_write, - }; - - pub const Lock = enum { - none, - shared, - exclusive, - }; - - pub const OpenFlags = struct { - mode: OpenMode = .read_only, - - /// Open the file with an advisory lock to coordinate with other processes - /// accessing it at the same time. An exclusive lock will prevent other - /// processes from acquiring a lock. A shared lock will prevent other - /// processes from acquiring a exclusive lock, but does not prevent - /// other process from getting their own shared locks. - /// - /// The lock is advisory, except on Linux in very specific circumstances[1]. - /// This means that a process that does not respect the locking API can still get access - /// to the file, despite the lock. - /// - /// On these operating systems, the lock is acquired atomically with - /// opening the file: - /// * Darwin - /// * DragonFlyBSD - /// * FreeBSD - /// * Haiku - /// * NetBSD - /// * OpenBSD - /// On these operating systems, the lock is acquired via a separate syscall - /// after opening the file: - /// * Linux - /// * Windows - /// - /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt - lock: Lock = .none, - - /// Sets whether or not to wait until the file is locked to return. If set to true, - /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file - /// is available to proceed. - /// In async I/O mode, non-blocking at the OS level is - /// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned, - /// and `false` means `error.WouldBlock` is handled by the event loop. - lock_nonblocking: bool = false, - - /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even - /// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions - /// related to opening the file, reading, writing, and locking. - intended_io_mode: io.ModeOverride = io.default_mode, - - /// Set this to allow the opened file to automatically become the - /// controlling TTY for the current process. - allow_ctty: bool = false, - - pub fn isRead(self: OpenFlags) bool { - return self.mode != .write_only; - } - - pub fn isWrite(self: OpenFlags) bool { - return self.mode != .read_only; - } - }; - - pub const CreateFlags = struct { - /// Whether the file will be created with read access. - read: bool = false, - - /// If the file already exists, and is a regular file, and the access - /// mode allows writing, it will be truncated to length 0. - truncate: bool = true, - - /// Ensures that this open call creates the file, otherwise causes - /// `error.PathAlreadyExists` to be returned. - exclusive: bool = false, - - /// Open the file with an advisory lock to coordinate with other processes - /// accessing it at the same time. An exclusive lock will prevent other - /// processes from acquiring a lock. A shared lock will prevent other - /// processes from acquiring a exclusive lock, but does not prevent - /// other process from getting their own shared locks. - /// - /// The lock is advisory, except on Linux in very specific circumstances[1]. - /// This means that a process that does not respect the locking API can still get access - /// to the file, despite the lock. - /// - /// On these operating systems, the lock is acquired atomically with - /// opening the file: - /// * Darwin - /// * DragonFlyBSD - /// * FreeBSD - /// * Haiku - /// * NetBSD - /// * OpenBSD - /// On these operating systems, the lock is acquired via a separate syscall - /// after opening the file: - /// * Linux - /// * Windows - /// - /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt - lock: Lock = .none, - - /// Sets whether or not to wait until the file is locked to return. If set to true, - /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file - /// is available to proceed. - /// In async I/O mode, non-blocking at the OS level is - /// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned, - /// and `false` means `error.WouldBlock` is handled by the event loop. - lock_nonblocking: bool = false, - - /// For POSIX systems this is the file system mode the file will - /// be created with. On other systems this is always 0. - mode: Mode = default_mode, - - /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even - /// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions - /// related to opening the file, reading, writing, and locking. - intended_io_mode: io.ModeOverride = io.default_mode, - }; - - /// Upon success, the stream is in an uninitialized state. To continue using it, - /// you must use the open() function. - pub fn close(self: File) void { - if (is_windows) { - windows.CloseHandle(self.handle); - } else if (self.capable_io_mode != self.intended_io_mode) { - std.event.Loop.instance.?.close(self.handle); - } else { - os.close(self.handle); - } - } - - pub const SyncError = os.SyncError; - - /// Blocks until all pending file contents and metadata modifications - /// for the file have been synchronized with the underlying filesystem. - /// - /// Note that this does not ensure that metadata for the - /// directory containing the file has also reached disk. - pub fn sync(self: File) SyncError!void { - return os.fsync(self.handle); - } - - /// Test whether the file refers to a terminal. - /// See also `supportsAnsiEscapeCodes`. - pub fn isTty(self: File) bool { - return os.isatty(self.handle); - } - - /// Test whether ANSI escape codes will be treated as such. - pub fn supportsAnsiEscapeCodes(self: File) bool { - if (builtin.os.tag == .windows) { - var console_mode: os.windows.DWORD = 0; - if (os.windows.kernel32.GetConsoleMode(self.handle, &console_mode) != 0) { - if (console_mode & os.windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true; - } - - return os.isCygwinPty(self.handle); - } - if (builtin.os.tag == .wasi) { - // WASI sanitizes stdout when fd is a tty so ANSI escape codes - // will not be interpreted as actual cursor commands, and - // stderr is always sanitized. - return false; - } - if (self.isTty()) { - if (self.handle == os.STDOUT_FILENO or self.handle == os.STDERR_FILENO) { - if (os.getenvZ("TERM")) |term| { - if (std.mem.eql(u8, term, "dumb")) - return false; - } - } - return true; - } - return false; - } - - pub const SetEndPosError = os.TruncateError; - - /// Shrinks or expands the file. - /// The file offset after this call is left unchanged. - pub fn setEndPos(self: File, length: u64) SetEndPosError!void { - try os.ftruncate(self.handle, length); - } - - pub const SeekError = os.SeekError; - - /// Repositions read/write file offset relative to the current offset. - /// TODO: integrate with async I/O - pub fn seekBy(self: File, offset: i64) SeekError!void { - return os.lseek_CUR(self.handle, offset); - } - - /// Repositions read/write file offset relative to the end. - /// TODO: integrate with async I/O - pub fn seekFromEnd(self: File, offset: i64) SeekError!void { - return os.lseek_END(self.handle, offset); - } - - /// Repositions read/write file offset relative to the beginning. - /// TODO: integrate with async I/O - pub fn seekTo(self: File, offset: u64) SeekError!void { - return os.lseek_SET(self.handle, offset); - } - - pub const GetSeekPosError = os.SeekError || os.FStatError; - - /// TODO: integrate with async I/O - pub fn getPos(self: File) GetSeekPosError!u64 { - return os.lseek_CUR_get(self.handle); - } - - /// TODO: integrate with async I/O - pub fn getEndPos(self: File) GetSeekPosError!u64 { - if (builtin.os.tag == .windows) { - return windows.GetFileSizeEx(self.handle); - } - return (try self.stat()).size; - } - - pub const ModeError = os.FStatError; - - /// TODO: integrate with async I/O - pub fn mode(self: File) ModeError!Mode { - if (builtin.os.tag == .windows) { - return 0; - } - return (try self.stat()).mode; - } - - pub const Stat = struct { - /// A number that the system uses to point to the file metadata. This - /// number is not guaranteed to be unique across time, as some file - /// systems may reuse an inode after its file has been deleted. Some - /// systems may change the inode of a file over time. - /// - /// On Linux, the inode is a structure that stores the metadata, and - /// the inode _number_ is what you see here: the index number of the - /// inode. - /// - /// The FileIndex on Windows is similar. It is a number for a file that - /// is unique to each filesystem. - inode: INode, - size: u64, - /// This is available on POSIX systems and is always 0 otherwise. - mode: Mode, - kind: Kind, - - /// Access time in nanoseconds, relative to UTC 1970-01-01. - atime: i128, - /// Last modification time in nanoseconds, relative to UTC 1970-01-01. - mtime: i128, - /// Creation time in nanoseconds, relative to UTC 1970-01-01. - ctime: i128, - - pub fn fromSystem(st: os.system.Stat) Stat { - const atime = st.atime(); - const mtime = st.mtime(); - const ctime = st.ctime(); - const kind: Kind = if (builtin.os.tag == .wasi and !builtin.link_libc) switch (st.filetype) { - .BLOCK_DEVICE => .block_device, - .CHARACTER_DEVICE => .character_device, - .DIRECTORY => .directory, - .SYMBOLIC_LINK => .sym_link, - .REGULAR_FILE => .file, - .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, - else => .unknown, - } else blk: { - const m = st.mode & os.S.IFMT; - switch (m) { - os.S.IFBLK => break :blk .block_device, - os.S.IFCHR => break :blk .character_device, - os.S.IFDIR => break :blk .directory, - os.S.IFIFO => break :blk .named_pipe, - os.S.IFLNK => break :blk .sym_link, - os.S.IFREG => break :blk .file, - os.S.IFSOCK => break :blk .unix_domain_socket, - else => {}, - } - if (builtin.os.tag.isSolarish()) switch (m) { - os.S.IFDOOR => break :blk .door, - os.S.IFPORT => break :blk .event_port, - else => {}, - }; - - break :blk .unknown; - }; - - return Stat{ - .inode = st.ino, - .size = @as(u64, @bitCast(st.size)), - .mode = st.mode, - .kind = kind, - .atime = @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec, - .mtime = @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec, - .ctime = @as(i128, ctime.tv_sec) * std.time.ns_per_s + ctime.tv_nsec, - }; - } - }; - - pub const StatError = os.FStatError; - - /// TODO: integrate with async I/O - pub fn stat(self: File) StatError!Stat { - if (builtin.os.tag == .windows) { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - var info: windows.FILE_ALL_INFORMATION = undefined; - const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); - switch (rc) { - .SUCCESS => {}, - // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer - // size provided. This is treated as success because the type of variable-length information that this would be relevant for - // (name, volume name, etc) we don't care about. - .BUFFER_OVERFLOW => {}, - .INVALID_PARAMETER => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - return Stat{ - .inode = info.InternalInformation.IndexNumber, - .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), - .mode = 0, - .kind = if (info.StandardInformation.Directory == 0) .file else .directory, - .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), - .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), - .ctime = windows.fromSysTime(info.BasicInformation.CreationTime), - }; - } - - const st = try os.fstat(self.handle); - return Stat.fromSystem(st); - } - - pub const ChmodError = std.os.FChmodError; - - /// Changes the mode of the file. - /// The process must have the correct privileges in order to do this - /// successfully, or must have the effective user ID matching the owner - /// of the file. - pub fn chmod(self: File, new_mode: Mode) ChmodError!void { - try os.fchmod(self.handle, new_mode); - } - - pub const ChownError = std.os.FChownError; - - /// Changes the owner and group of the file. - /// The process must have the correct privileges in order to do this - /// successfully. The group may be changed by the owner of the file to - /// any group of which the owner is a member. If the owner or group is - /// specified as `null`, the ID is not changed. - pub fn chown(self: File, owner: ?Uid, group: ?Gid) ChownError!void { - try os.fchown(self.handle, owner, group); - } - - /// Cross-platform representation of permissions on a file. - /// The `readonly` and `setReadonly` are the only methods available across all platforms. - /// Platform-specific functionality is available through the `inner` field. - pub const Permissions = struct { - /// You may use the `inner` field to use platform-specific functionality - inner: switch (builtin.os.tag) { - .windows => PermissionsWindows, - else => PermissionsUnix, - }, - - const Self = @This(); - - /// Returns `true` if permissions represent an unwritable file. - /// On Unix, `true` is returned only if no class has write permissions. - pub fn readOnly(self: Self) bool { - return self.inner.readOnly(); - } - - /// Sets whether write permissions are provided. - /// On Unix, this affects *all* classes. If this is undesired, use `unixSet`. - /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` - pub fn setReadOnly(self: *Self, read_only: bool) void { - self.inner.setReadOnly(read_only); - } - }; - - pub const PermissionsWindows = struct { - attributes: os.windows.DWORD, - - const Self = @This(); - - /// Returns `true` if permissions represent an unwritable file. - pub fn readOnly(self: Self) bool { - return self.attributes & os.windows.FILE_ATTRIBUTE_READONLY != 0; - } - - /// Sets whether write permissions are provided. - /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` - pub fn setReadOnly(self: *Self, read_only: bool) void { - if (read_only) { - self.attributes |= os.windows.FILE_ATTRIBUTE_READONLY; - } else { - self.attributes &= ~@as(os.windows.DWORD, os.windows.FILE_ATTRIBUTE_READONLY); - } - } - }; - - pub const PermissionsUnix = struct { - mode: Mode, - - const Self = @This(); - - /// Returns `true` if permissions represent an unwritable file. - /// `true` is returned only if no class has write permissions. - pub fn readOnly(self: Self) bool { - return self.mode & 0o222 == 0; - } - - /// Sets whether write permissions are provided. - /// This affects *all* classes. If this is undesired, use `unixSet`. - /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` - pub fn setReadOnly(self: *Self, read_only: bool) void { - if (read_only) { - self.mode &= ~@as(Mode, 0o222); - } else { - self.mode |= @as(Mode, 0o222); - } - } - - pub const Class = enum(u2) { - user = 2, - group = 1, - other = 0, - }; - - pub const Permission = enum(u3) { - read = 0o4, - write = 0o2, - execute = 0o1, - }; - - /// Returns `true` if the chosen class has the selected permission. - /// This method is only available on Unix platforms. - pub fn unixHas(self: Self, class: Class, permission: Permission) bool { - const mask = @as(Mode, @intFromEnum(permission)) << @as(u3, @intFromEnum(class)) * 3; - return self.mode & mask != 0; - } - - /// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged. - /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` - pub fn unixSet(self: *Self, class: Class, permissions: struct { - read: ?bool = null, - write: ?bool = null, - execute: ?bool = null, - }) void { - const shift = @as(u3, @intFromEnum(class)) * 3; - if (permissions.read) |r| { - if (r) { - self.mode |= @as(Mode, 0o4) << shift; - } else { - self.mode &= ~(@as(Mode, 0o4) << shift); - } - } - if (permissions.write) |w| { - if (w) { - self.mode |= @as(Mode, 0o2) << shift; - } else { - self.mode &= ~(@as(Mode, 0o2) << shift); - } - } - if (permissions.execute) |x| { - if (x) { - self.mode |= @as(Mode, 0o1) << shift; - } else { - self.mode &= ~(@as(Mode, 0o1) << shift); - } - } - } - - /// Returns a `Permissions` struct representing the permissions from the passed mode. - pub fn unixNew(new_mode: Mode) Self { - return Self{ - .mode = new_mode, - }; - } - }; - - pub const SetPermissionsError = ChmodError; - - /// Sets permissions according to the provided `Permissions` struct. - /// This method is *NOT* available on WASI - pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void { - switch (builtin.os.tag) { - .windows => { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - var info = windows.FILE_BASIC_INFORMATION{ - .CreationTime = 0, - .LastAccessTime = 0, - .LastWriteTime = 0, - .ChangeTime = 0, - .FileAttributes = permissions.inner.attributes, - }; - const rc = windows.ntdll.NtSetInformationFile( - self.handle, - &io_status_block, - &info, - @sizeOf(windows.FILE_BASIC_INFORMATION), - .FileBasicInformation, - ); - switch (rc) { - .SUCCESS => return, - .INVALID_HANDLE => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - }, - .wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod - else => { - try self.chmod(permissions.inner.mode); - }, - } - } - - /// Cross-platform representation of file metadata. - /// Platform-specific functionality is available through the `inner` field. - pub const Metadata = struct { - /// You may use the `inner` field to use platform-specific functionality - inner: switch (builtin.os.tag) { - .windows => MetadataWindows, - .linux => MetadataLinux, - else => MetadataUnix, - }, - - const Self = @This(); - - /// Returns the size of the file - pub fn size(self: Self) u64 { - return self.inner.size(); - } - - /// Returns a `Permissions` struct, representing the permissions on the file - pub fn permissions(self: Self) Permissions { - return self.inner.permissions(); - } - - /// Returns the `Kind` of file. - /// On Windows, can only return: `.file`, `.directory`, `.sym_link` or `.unknown` - pub fn kind(self: Self) Kind { - return self.inner.kind(); - } - - /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 - pub fn accessed(self: Self) i128 { - return self.inner.accessed(); - } - - /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01 - pub fn modified(self: Self) i128 { - return self.inner.modified(); - } - - /// Returns the time the file was created in nanoseconds since UTC 1970-01-01 - /// On Windows, this cannot return null - /// On Linux, this returns null if the filesystem does not support creation times, or if the kernel is older than 4.11 - /// On Unices, this returns null if the filesystem or OS does not support creation times - /// 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 - pub fn created(self: Self) ?i128 { - return self.inner.created(); - } - }; - - pub const MetadataUnix = struct { - stat: os.Stat, - - const Self = @This(); - - /// Returns the size of the file - pub fn size(self: Self) u64 { - return @as(u64, @intCast(self.stat.size)); - } - - /// Returns a `Permissions` struct, representing the permissions on the file - pub fn permissions(self: Self) Permissions { - return Permissions{ .inner = PermissionsUnix{ .mode = self.stat.mode } }; - } - - /// Returns the `Kind` of the file - pub fn kind(self: Self) Kind { - if (builtin.os.tag == .wasi and !builtin.link_libc) return switch (self.stat.filetype) { - .BLOCK_DEVICE => .block_device, - .CHARACTER_DEVICE => .character_device, - .DIRECTORY => .directory, - .SYMBOLIC_LINK => .sym_link, - .REGULAR_FILE => .file, - .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, - else => .unknown, - }; - - const m = self.stat.mode & os.S.IFMT; - - switch (m) { - os.S.IFBLK => return .block_device, - os.S.IFCHR => return .character_device, - os.S.IFDIR => return .directory, - os.S.IFIFO => return .named_pipe, - os.S.IFLNK => return .sym_link, - os.S.IFREG => return .file, - os.S.IFSOCK => return .unix_domain_socket, - else => {}, - } - - if (builtin.os.tag.isSolarish()) switch (m) { - os.S.IFDOOR => return .door, - os.S.IFPORT => return .event_port, - else => {}, - }; - - return .unknown; - } - - /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 - pub fn accessed(self: Self) i128 { - const atime = self.stat.atime(); - return @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec; - } - - /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01 - pub fn modified(self: Self) i128 { - const mtime = self.stat.mtime(); - return @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec; - } - - /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. - /// Returns null if this is not supported by the OS or filesystem - pub fn created(self: Self) ?i128 { - if (!@hasDecl(@TypeOf(self.stat), "birthtime")) return null; - const birthtime = self.stat.birthtime(); - - // If the filesystem doesn't support this the value *should* be: - // On FreeBSD: tv_nsec = 0, tv_sec = -1 - // On NetBSD and OpenBSD: tv_nsec = 0, tv_sec = 0 - // On MacOS, it is set to ctime -- we cannot detect this!! - switch (builtin.os.tag) { - .freebsd => if (birthtime.tv_sec == -1 and birthtime.tv_nsec == 0) return null, - .netbsd, .openbsd => if (birthtime.tv_sec == 0 and birthtime.tv_nsec == 0) return null, - .macos => {}, - else => @compileError("Creation time detection not implemented for OS"), - } - - return @as(i128, birthtime.tv_sec) * std.time.ns_per_s + birthtime.tv_nsec; - } - }; - - /// `MetadataUnix`, but using Linux's `statx` syscall. - /// On Linux versions below 4.11, `statx` will be filled with data from stat. - pub const MetadataLinux = struct { - statx: os.linux.Statx, - - const Self = @This(); - - /// Returns the size of the file - pub fn size(self: Self) u64 { - return self.statx.size; - } - - /// Returns a `Permissions` struct, representing the permissions on the file - pub fn permissions(self: Self) Permissions { - return Permissions{ .inner = PermissionsUnix{ .mode = self.statx.mode } }; - } - - /// Returns the `Kind` of the file - pub fn kind(self: Self) Kind { - const m = self.statx.mode & os.S.IFMT; - - switch (m) { - os.S.IFBLK => return .block_device, - os.S.IFCHR => return .character_device, - os.S.IFDIR => return .directory, - os.S.IFIFO => return .named_pipe, - os.S.IFLNK => return .sym_link, - os.S.IFREG => return .file, - os.S.IFSOCK => return .unix_domain_socket, - else => {}, - } - - return .unknown; - } - - /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 - pub fn accessed(self: Self) i128 { - return @as(i128, self.statx.atime.tv_sec) * std.time.ns_per_s + self.statx.atime.tv_nsec; - } - - /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01 - pub fn modified(self: Self) i128 { - return @as(i128, self.statx.mtime.tv_sec) * std.time.ns_per_s + self.statx.mtime.tv_nsec; - } - - /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. - /// Returns null if this is not supported by the filesystem, or on kernels before than version 4.11 - pub fn created(self: Self) ?i128 { - if (self.statx.mask & os.linux.STATX_BTIME == 0) return null; - return @as(i128, self.statx.btime.tv_sec) * std.time.ns_per_s + self.statx.btime.tv_nsec; - } - }; - - pub const MetadataWindows = struct { - attributes: windows.DWORD, - reparse_tag: windows.DWORD, - _size: u64, - access_time: i128, - modified_time: i128, - creation_time: i128, - - const Self = @This(); - - /// Returns the size of the file - pub fn size(self: Self) u64 { - return self._size; - } - - /// Returns a `Permissions` struct, representing the permissions on the file - pub fn permissions(self: Self) Permissions { - return Permissions{ .inner = PermissionsWindows{ .attributes = self.attributes } }; - } - - /// Returns the `Kind` of the file. - /// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown` - pub fn kind(self: Self) Kind { - if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { - if (self.reparse_tag & 0x20000000 != 0) { - return .sym_link; - } - } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) { - return .directory; - } else { - return .file; - } - return .unknown; - } - - /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 - pub fn accessed(self: Self) i128 { - return self.access_time; - } - - /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01 - pub fn modified(self: Self) i128 { - return self.modified_time; - } - - /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. - /// This never returns null, only returning an optional for compatibility with other OSes - pub fn created(self: Self) ?i128 { - return self.creation_time; - } - }; - - pub const MetadataError = os.FStatError; - - pub fn metadata(self: File) MetadataError!Metadata { - return Metadata{ - .inner = switch (builtin.os.tag) { - .windows => blk: { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - var info: windows.FILE_ALL_INFORMATION = undefined; - - const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); - switch (rc) { - .SUCCESS => {}, - // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer - // size provided. This is treated as success because the type of variable-length information that this would be relevant for - // (name, volume name, etc) we don't care about. - .BUFFER_OVERFLOW => {}, - .INVALID_PARAMETER => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - - const reparse_tag: windows.DWORD = reparse_blk: { - if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { - var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; - try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]); - const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0])); - break :reparse_blk reparse_struct.ReparseTag; - } - break :reparse_blk 0; - }; - - break :blk MetadataWindows{ - .attributes = info.BasicInformation.FileAttributes, - .reparse_tag = reparse_tag, - ._size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), - .access_time = windows.fromSysTime(info.BasicInformation.LastAccessTime), - .modified_time = windows.fromSysTime(info.BasicInformation.LastWriteTime), - .creation_time = windows.fromSysTime(info.BasicInformation.CreationTime), - }; - }, - .linux => blk: { - var stx = mem.zeroes(os.linux.Statx); - 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); - - switch (os.errno(rcx)) { - .SUCCESS => {}, - // NOSYS happens when `statx` is unsupported, which is the case on kernel versions before 4.11 - // Here, we call `fstat` and fill `stx` with the data we need - .NOSYS => { - const st = try os.fstat(self.handle); - - stx.mode = @as(u16, @intCast(st.mode)); - - // Hacky conversion from timespec to statx_timestamp - stx.atime = std.mem.zeroes(os.linux.statx_timestamp); - stx.atime.tv_sec = st.atim.tv_sec; - stx.atime.tv_nsec = @as(u32, @intCast(st.atim.tv_nsec)); // Guaranteed to succeed (tv_nsec is always below 10^9) - - stx.mtime = std.mem.zeroes(os.linux.statx_timestamp); - stx.mtime.tv_sec = st.mtim.tv_sec; - stx.mtime.tv_nsec = @as(u32, @intCast(st.mtim.tv_nsec)); - - stx.mask = os.linux.STATX_BASIC_STATS | os.linux.STATX_MTIME; - }, - .BADF => unreachable, - .FAULT => unreachable, - .NOMEM => return error.SystemResources, - else => |err| return os.unexpectedErrno(err), - } - - break :blk MetadataLinux{ - .statx = stx, - }; - }, - else => blk: { - const st = try os.fstat(self.handle); - break :blk MetadataUnix{ - .stat = st, - }; - }, - }, - }; - } - - pub const UpdateTimesError = os.FutimensError || windows.SetFileTimeError; - - /// The underlying file system may have a different granularity than nanoseconds, - /// and therefore this function cannot guarantee any precision will be stored. - /// Further, the maximum value is limited by the system ABI. When a value is provided - /// that exceeds this range, the value is clamped to the maximum. - /// TODO: integrate with async I/O - pub fn updateTimes( - self: File, - /// access timestamp in nanoseconds - atime: i128, - /// last modification timestamp in nanoseconds - mtime: i128, - ) UpdateTimesError!void { - if (builtin.os.tag == .windows) { - const atime_ft = windows.nanoSecondsToFileTime(atime); - const mtime_ft = windows.nanoSecondsToFileTime(mtime); - return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft); - } - const times = [2]os.timespec{ - os.timespec{ - .tv_sec = math.cast(isize, @divFloor(atime, std.time.ns_per_s)) orelse maxInt(isize), - .tv_nsec = math.cast(isize, @mod(atime, std.time.ns_per_s)) orelse maxInt(isize), - }, - os.timespec{ - .tv_sec = math.cast(isize, @divFloor(mtime, std.time.ns_per_s)) orelse maxInt(isize), - .tv_nsec = math.cast(isize, @mod(mtime, std.time.ns_per_s)) orelse maxInt(isize), - }, - }; - try os.futimens(self.handle, ×); - } - - /// Reads all the bytes from the current position to the end of the file. - /// On success, caller owns returned buffer. - /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. - pub fn readToEndAlloc(self: File, allocator: mem.Allocator, max_bytes: usize) ![]u8 { - return self.readToEndAllocOptions(allocator, max_bytes, null, @alignOf(u8), null); - } - - /// Reads all the bytes from the current position to the end of the file. - /// On success, caller owns returned buffer. - /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. - /// If `size_hint` is specified the initial buffer size is calculated using - /// that value, otherwise an arbitrary value is used instead. - /// Allows specifying alignment and a sentinel value. - pub fn readToEndAllocOptions( - self: File, - allocator: mem.Allocator, - max_bytes: usize, - size_hint: ?usize, - comptime alignment: u29, - comptime optional_sentinel: ?u8, - ) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) { - // If no size hint is provided fall back to the size=0 code path - const size = size_hint orelse 0; - - // The file size returned by stat is used as hint to set the buffer - // size. If the reported size is zero, as it happens on Linux for files - // in /proc, a small buffer is allocated instead. - const initial_cap = (if (size > 0) size else 1024) + @intFromBool(optional_sentinel != null); - var array_list = try std.ArrayListAligned(u8, alignment).initCapacity(allocator, initial_cap); - defer array_list.deinit(); - - self.reader().readAllArrayListAligned(alignment, &array_list, max_bytes) catch |err| switch (err) { - error.StreamTooLong => return error.FileTooBig, - else => |e| return e, - }; - - if (optional_sentinel) |sentinel| { - return try array_list.toOwnedSliceSentinel(sentinel); - } else { - return try array_list.toOwnedSlice(); - } - } - - pub const ReadError = os.ReadError; - pub const PReadError = os.PReadError; - - pub fn read(self: File, buffer: []u8) ReadError!usize { - if (is_windows) { - return windows.ReadFile(self.handle, buffer, null, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.read(self.handle, buffer); - } else { - return std.event.Loop.instance.?.read(self.handle, buffer, self.capable_io_mode != self.intended_io_mode); - } - } - - /// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it - /// means the file reached the end. Reaching the end of a file is not an error condition. - pub fn readAll(self: File, buffer: []u8) ReadError!usize { - var index: usize = 0; - while (index != buffer.len) { - const amt = try self.read(buffer[index..]); - if (amt == 0) break; - index += amt; - } - return index; - } - - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize { - if (is_windows) { - return windows.ReadFile(self.handle, buffer, offset, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.pread(self.handle, buffer, offset); - } else { - return std.event.Loop.instance.?.pread(self.handle, buffer, offset, self.capable_io_mode != self.intended_io_mode); - } - } - - /// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it - /// means the file reached the end. Reaching the end of a file is not an error condition. - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize { - var index: usize = 0; - while (index != buffer.len) { - const amt = try self.pread(buffer[index..], offset + index); - if (amt == 0) break; - index += amt; - } - return index; - } - - /// See https://github.com/ziglang/zig/issues/7699 - pub fn readv(self: File, iovecs: []const os.iovec) ReadError!usize { - if (is_windows) { - // TODO improve this to use ReadFileScatter - if (iovecs.len == 0) return @as(usize, 0); - const first = iovecs[0]; - return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.readv(self.handle, iovecs); - } else { - return std.event.Loop.instance.?.readv(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode); - } - } - - /// Returns the number of bytes read. If the number read is smaller than the total bytes - /// from all the buffers, it means the file reached the end. Reaching the end of a file - /// is not an error condition. - /// - /// The `iovecs` parameter is mutable because: - /// * This function needs to mutate the fields in order to handle partial - /// reads from the underlying OS layer. - /// * The OS layer expects pointer addresses to be inside the application's address space - /// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer - /// addresses when the length is zero. So this function modifies the iov_base fields - /// when the length is zero. - /// - /// Related open issue: https://github.com/ziglang/zig/issues/7699 - pub fn readvAll(self: File, iovecs: []os.iovec) ReadError!usize { - if (iovecs.len == 0) return 0; - - // We use the address of this local variable for all zero-length - // vectors so that the OS does not complain that we are giving it - // addresses outside the application's address space. - var garbage: [1]u8 = undefined; - for (iovecs) |*v| { - if (v.iov_len == 0) v.iov_base = &garbage; - } - - var i: usize = 0; - var off: usize = 0; - while (true) { - var amt = try self.readv(iovecs[i..]); - var eof = amt == 0; - off += amt; - while (amt >= iovecs[i].iov_len) { - amt -= iovecs[i].iov_len; - i += 1; - if (i >= iovecs.len) return off; - eof = false; - } - if (eof) return off; - iovecs[i].iov_base += amt; - iovecs[i].iov_len -= amt; - } - } - - /// See https://github.com/ziglang/zig/issues/7699 - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn preadv(self: File, iovecs: []const os.iovec, offset: u64) PReadError!usize { - if (is_windows) { - // TODO improve this to use ReadFileScatter - if (iovecs.len == 0) return @as(usize, 0); - const first = iovecs[0]; - return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.preadv(self.handle, iovecs, offset); - } else { - return std.event.Loop.instance.?.preadv(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode); - } - } - - /// Returns the number of bytes read. If the number read is smaller than the total bytes - /// from all the buffers, it means the file reached the end. Reaching the end of a file - /// is not an error condition. - /// The `iovecs` parameter is mutable because this function needs to mutate the fields in - /// order to handle partial reads from the underlying OS layer. - /// See https://github.com/ziglang/zig/issues/7699 - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn preadvAll(self: File, iovecs: []os.iovec, offset: u64) PReadError!usize { - if (iovecs.len == 0) return 0; - - var i: usize = 0; - var off: usize = 0; - while (true) { - var amt = try self.preadv(iovecs[i..], offset + off); - var eof = amt == 0; - off += amt; - while (amt >= iovecs[i].iov_len) { - amt -= iovecs[i].iov_len; - i += 1; - if (i >= iovecs.len) return off; - eof = false; - } - if (eof) return off; - iovecs[i].iov_base += amt; - iovecs[i].iov_len -= amt; - } - } - - pub const WriteError = os.WriteError; - pub const PWriteError = os.PWriteError; - - pub fn write(self: File, bytes: []const u8) WriteError!usize { - if (is_windows) { - return windows.WriteFile(self.handle, bytes, null, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.write(self.handle, bytes); - } else { - return std.event.Loop.instance.?.write(self.handle, bytes, self.capable_io_mode != self.intended_io_mode); - } - } - - pub fn writeAll(self: File, bytes: []const u8) WriteError!void { - var index: usize = 0; - while (index < bytes.len) { - index += try self.write(bytes[index..]); - } - } - - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize { - if (is_windows) { - return windows.WriteFile(self.handle, bytes, offset, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.pwrite(self.handle, bytes, offset); - } else { - return std.event.Loop.instance.?.pwrite(self.handle, bytes, offset, self.capable_io_mode != self.intended_io_mode); - } - } - - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void { - var index: usize = 0; - while (index < bytes.len) { - index += try self.pwrite(bytes[index..], offset + index); - } - } - - /// See https://github.com/ziglang/zig/issues/7699 - /// See equivalent function: `std.net.Stream.writev`. - pub fn writev(self: File, iovecs: []const os.iovec_const) WriteError!usize { - if (is_windows) { - // TODO improve this to use WriteFileScatter - if (iovecs.len == 0) return @as(usize, 0); - const first = iovecs[0]; - return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.writev(self.handle, iovecs); - } else { - return std.event.Loop.instance.?.writev(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode); - } - } - - /// The `iovecs` parameter is mutable because: - /// * This function needs to mutate the fields in order to handle partial - /// writes from the underlying OS layer. - /// * The OS layer expects pointer addresses to be inside the application's address space - /// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer - /// addresses when the length is zero. So this function modifies the iov_base fields - /// when the length is zero. - /// See https://github.com/ziglang/zig/issues/7699 - /// See equivalent function: `std.net.Stream.writevAll`. - pub fn writevAll(self: File, iovecs: []os.iovec_const) WriteError!void { - if (iovecs.len == 0) return; - - // We use the address of this local variable for all zero-length - // vectors so that the OS does not complain that we are giving it - // addresses outside the application's address space. - var garbage: [1]u8 = undefined; - for (iovecs) |*v| { - if (v.iov_len == 0) v.iov_base = &garbage; - } - - var i: usize = 0; - while (true) { - var amt = try self.writev(iovecs[i..]); - while (amt >= iovecs[i].iov_len) { - amt -= iovecs[i].iov_len; - i += 1; - if (i >= iovecs.len) return; - } - iovecs[i].iov_base += amt; - iovecs[i].iov_len -= amt; - } - } - - /// See https://github.com/ziglang/zig/issues/7699 - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn pwritev(self: File, iovecs: []os.iovec_const, offset: u64) PWriteError!usize { - if (is_windows) { - // TODO improve this to use WriteFileScatter - if (iovecs.len == 0) return @as(usize, 0); - const first = iovecs[0]; - return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.pwritev(self.handle, iovecs, offset); - } else { - return std.event.Loop.instance.?.pwritev(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode); - } - } - - /// The `iovecs` parameter is mutable because this function needs to mutate the fields in - /// order to handle partial writes from the underlying OS layer. - /// See https://github.com/ziglang/zig/issues/7699 - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn pwritevAll(self: File, iovecs: []os.iovec_const, offset: u64) PWriteError!void { - if (iovecs.len == 0) return; - - var i: usize = 0; - var off: u64 = 0; - while (true) { - var amt = try self.pwritev(iovecs[i..], offset + off); - off += amt; - while (amt >= iovecs[i].iov_len) { - amt -= iovecs[i].iov_len; - i += 1; - if (i >= iovecs.len) return; - } - iovecs[i].iov_base += amt; - iovecs[i].iov_len -= amt; - } - } - - pub const CopyRangeError = os.CopyFileRangeError; - - pub fn copyRange(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { - const adjusted_len = math.cast(usize, len) orelse math.maxInt(usize); - const result = try os.copy_file_range(in.handle, in_offset, out.handle, out_offset, adjusted_len, 0); - return result; - } - - /// Returns the number of bytes copied. If the number read is smaller than `buffer.len`, it - /// means the in file reached the end. Reaching the end of a file is not an error condition. - pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { - var total_bytes_copied: u64 = 0; - var in_off = in_offset; - var out_off = out_offset; - while (total_bytes_copied < len) { - const amt_copied = try copyRange(in, in_off, out, out_off, len - total_bytes_copied); - if (amt_copied == 0) return total_bytes_copied; - total_bytes_copied += amt_copied; - in_off += amt_copied; - out_off += amt_copied; - } - return total_bytes_copied; - } - - pub const WriteFileOptions = struct { - in_offset: u64 = 0, - - /// `null` means the entire file. `0` means no bytes from the file. - /// When this is `null`, trailers must be sent in a separate writev() call - /// due to a flaw in the BSD sendfile API. Other operating systems, such as - /// Linux, already do this anyway due to API limitations. - /// If the size of the source file is known, passing the size here will save one syscall. - in_len: ?u64 = null, - - headers_and_trailers: []os.iovec_const = &[0]os.iovec_const{}, - - /// The trailer count is inferred from `headers_and_trailers.len - header_count` - header_count: usize = 0, - }; - - pub const WriteFileError = ReadError || error{EndOfStream} || WriteError; - - pub fn writeFileAll(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void { - return self.writeFileAllSendfile(in_file, args) catch |err| switch (err) { - error.Unseekable, - error.FastOpenAlreadyInProgress, - error.MessageTooBig, - error.FileDescriptorNotASocket, - error.NetworkUnreachable, - error.NetworkSubsystemFailed, - => return self.writeFileAllUnseekable(in_file, args), - - else => |e| return e, - }; - } - - /// Does not try seeking in either of the File parameters. - /// See `writeFileAll` as an alternative to calling this. - pub fn writeFileAllUnseekable(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void { - const headers = args.headers_and_trailers[0..args.header_count]; - const trailers = args.headers_and_trailers[args.header_count..]; - - try self.writevAll(headers); - - try in_file.reader().skipBytes(args.in_offset, .{ .buf_size = 4096 }); - - var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init(); - if (args.in_len) |len| { - var stream = std.io.limitedReader(in_file.reader(), len); - try fifo.pump(stream.reader(), self.writer()); - } else { - try fifo.pump(in_file.reader(), self.writer()); - } - - try self.writevAll(trailers); - } - - /// Low level function which can fail for OS-specific reasons. - /// See `writeFileAll` as an alternative to calling this. - /// TODO integrate with async I/O - fn writeFileAllSendfile(self: File, in_file: File, args: WriteFileOptions) os.SendFileError!void { - const count = blk: { - if (args.in_len) |l| { - if (l == 0) { - return self.writevAll(args.headers_and_trailers); - } else { - break :blk l; - } - } else { - break :blk 0; - } - }; - const headers = args.headers_and_trailers[0..args.header_count]; - const trailers = args.headers_and_trailers[args.header_count..]; - const zero_iovec = &[0]os.iovec_const{}; - // When reading the whole file, we cannot put the trailers in the sendfile() syscall, - // because we have no way to determine whether a partial write is past the end of the file or not. - const trls = if (count == 0) zero_iovec else trailers; - const offset = args.in_offset; - const out_fd = self.handle; - const in_fd = in_file.handle; - const flags = 0; - var amt: usize = 0; - hdrs: { - var i: usize = 0; - while (i < headers.len) { - amt = try os.sendfile(out_fd, in_fd, offset, count, headers[i..], trls, flags); - while (amt >= headers[i].iov_len) { - amt -= headers[i].iov_len; - i += 1; - if (i >= headers.len) break :hdrs; - } - headers[i].iov_base += amt; - headers[i].iov_len -= amt; - } - } - if (count == 0) { - var off: u64 = amt; - while (true) { - amt = try os.sendfile(out_fd, in_fd, offset + off, 0, zero_iovec, zero_iovec, flags); - if (amt == 0) break; - off += amt; - } - } else { - var off: u64 = amt; - while (off < count) { - amt = try os.sendfile(out_fd, in_fd, offset + off, count - off, zero_iovec, trailers, flags); - off += amt; - } - amt = @as(usize, @intCast(off - count)); - } - var i: usize = 0; - while (i < trailers.len) { - while (amt >= trailers[i].iov_len) { - amt -= trailers[i].iov_len; - i += 1; - if (i >= trailers.len) return; - } - trailers[i].iov_base += amt; - trailers[i].iov_len -= amt; - amt = try os.writev(self.handle, trailers[i..]); - } - } - - pub const Reader = io.Reader(File, ReadError, read); - - pub fn reader(file: File) Reader { - return .{ .context = file }; - } - - pub const Writer = io.Writer(File, WriteError, write); - - pub fn writer(file: File) Writer { - return .{ .context = file }; - } - - pub const SeekableStream = io.SeekableStream( - File, - SeekError, - GetSeekPosError, - seekTo, - seekBy, - getPos, - getEndPos, - ); - - pub fn seekableStream(file: File) SeekableStream { - return .{ .context = file }; - } - - const range_off: windows.LARGE_INTEGER = 0; - const range_len: windows.LARGE_INTEGER = 1; - - pub const LockError = error{ - SystemResources, - FileLocksNotSupported, - } || os.UnexpectedError; - - /// Blocks when an incompatible lock is held by another process. - /// A process may hold only one type of lock (shared or exclusive) on - /// a file. When a process terminates in any way, the lock is released. - /// - /// Assumes the file is unlocked. - /// - /// TODO: integrate with async I/O - pub fn lock(file: File, l: Lock) LockError!void { - if (is_windows) { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - const exclusive = switch (l) { - .none => return, - .shared => false, - .exclusive => true, - }; - return windows.LockFile( - file.handle, - null, - null, - null, - &io_status_block, - &range_off, - &range_len, - null, - windows.FALSE, // non-blocking=false - @intFromBool(exclusive), - ) catch |err| switch (err) { - error.WouldBlock => unreachable, // non-blocking=false - else => |e| return e, - }; - } else { - return os.flock(file.handle, switch (l) { - .none => os.LOCK.UN, - .shared => os.LOCK.SH, - .exclusive => os.LOCK.EX, - }) catch |err| switch (err) { - error.WouldBlock => unreachable, // non-blocking=false - else => |e| return e, - }; - } - } - - /// Assumes the file is locked. - pub fn unlock(file: File) void { - if (is_windows) { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - return windows.UnlockFile( - file.handle, - &io_status_block, - &range_off, - &range_len, - null, - ) catch |err| switch (err) { - error.RangeNotLocked => unreachable, // Function assumes unlocked. - error.Unexpected => unreachable, // Resource deallocation must succeed. - }; - } else { - return os.flock(file.handle, os.LOCK.UN) catch |err| switch (err) { - error.WouldBlock => unreachable, // unlocking can't block - error.SystemResources => unreachable, // We are deallocating resources. - error.FileLocksNotSupported => unreachable, // We already got the lock. - error.Unexpected => unreachable, // Resource deallocation must succeed. - }; - } - } - - /// Attempts to obtain a lock, returning `true` if the lock is - /// obtained, and `false` if there was an existing incompatible lock held. - /// A process may hold only one type of lock (shared or exclusive) on - /// a file. When a process terminates in any way, the lock is released. - /// - /// Assumes the file is unlocked. - /// - /// TODO: integrate with async I/O - pub fn tryLock(file: File, l: Lock) LockError!bool { - if (is_windows) { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - const exclusive = switch (l) { - .none => return, - .shared => false, - .exclusive => true, - }; - windows.LockFile( - file.handle, - null, - null, - null, - &io_status_block, - &range_off, - &range_len, - null, - windows.TRUE, // non-blocking=true - @intFromBool(exclusive), - ) catch |err| switch (err) { - error.WouldBlock => return false, - else => |e| return e, - }; - } else { - os.flock(file.handle, switch (l) { - .none => os.LOCK.UN, - .shared => os.LOCK.SH | os.LOCK.NB, - .exclusive => os.LOCK.EX | os.LOCK.NB, - }) catch |err| switch (err) { - error.WouldBlock => return false, - else => |e| return e, - }; - } - return true; - } - - /// Assumes the file is already locked in exclusive mode. - /// Atomically modifies the lock to be in shared mode, without releasing it. - /// - /// TODO: integrate with async I/O - pub fn downgradeLock(file: File) LockError!void { - if (is_windows) { - // On Windows it works like a semaphore + exclusivity flag. To implement this - // function, we first obtain another lock in shared mode. This changes the - // exclusivity flag, but increments the semaphore to 2. So we follow up with - // an NtUnlockFile which decrements the semaphore but does not modify the - // exclusivity flag. - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - windows.LockFile( - file.handle, - null, - null, - null, - &io_status_block, - &range_off, - &range_len, - null, - windows.TRUE, // non-blocking=true - windows.FALSE, // exclusive=false - ) catch |err| switch (err) { - error.WouldBlock => unreachable, // File was not locked in exclusive mode. - else => |e| return e, - }; - return windows.UnlockFile( - file.handle, - &io_status_block, - &range_off, - &range_len, - null, - ) catch |err| switch (err) { - error.RangeNotLocked => unreachable, // File was not locked. - error.Unexpected => unreachable, // Resource deallocation must succeed. - }; - } else { - return os.flock(file.handle, os.LOCK.SH | os.LOCK.NB) catch |err| switch (err) { - error.WouldBlock => unreachable, // File was not locked in exclusive mode. - else => |e| return e, - }; - } - } -};