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.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.zig')
| -rw-r--r-- | lib/std/fs.zig | 2560 |
1 files changed, 31 insertions, 2529 deletions
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"); } |
