diff options
| author | Andrew Kelley <andrew@ziglang.org> | 2023-11-22 13:50:31 -0700 |
|---|---|---|
| committer | Andrew Kelley <andrew@ziglang.org> | 2023-11-22 15:24:57 -0700 |
| commit | c95e2e65fac6afec5c57f6716b1f4e8e7a7292fb (patch) | |
| tree | 818aaacaa27fb754107fb0f5ef5a19cb80c94831 /lib/std/fs/Dir.zig | |
| parent | e357550610aef476390ed7191eeaf7597a8e9d53 (diff) | |
| download | zig-c95e2e65fac6afec5c57f6716b1f4e8e7a7292fb.tar.gz zig-c95e2e65fac6afec5c57f6716b1f4e8e7a7292fb.zip | |
std.fs: extract Dir into separate file
Diffstat (limited to 'lib/std/fs/Dir.zig')
| -rw-r--r-- | lib/std/fs/Dir.zig | 2533 |
1 files changed, 2533 insertions, 0 deletions
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; |
