aboutsummaryrefslogtreecommitdiff
path: root/lib/std/fs.zig
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2019-09-26 01:54:45 -0400
committerGitHub <noreply@github.com>2019-09-26 01:54:45 -0400
commit68bb3945708c43109c48bda3664176307d45b62c (patch)
treeafb9731e10cef9d192560b52cd9ae2cf179775c4 /lib/std/fs.zig
parent6128bc728d1e1024a178c16c2149f5b1a167a013 (diff)
parent4637e8f9699af9c3c6cf4df50ef5bb67c7a318a4 (diff)
downloadzig-68bb3945708c43109c48bda3664176307d45b62c.tar.gz
zig-68bb3945708c43109c48bda3664176307d45b62c.zip
Merge pull request #3315 from ziglang/mv-std-lib
Move std/ to lib/std/
Diffstat (limited to 'lib/std/fs.zig')
-rw-r--r--lib/std/fs.zig988
1 files changed, 988 insertions, 0 deletions
diff --git a/lib/std/fs.zig b/lib/std/fs.zig
new file mode 100644
index 0000000000..6301c8a26c
--- /dev/null
+++ b/lib/std/fs.zig
@@ -0,0 +1,988 @@
+const builtin = @import("builtin");
+const std = @import("std.zig");
+const os = std.os;
+const mem = std.mem;
+const base64 = std.base64;
+const crypto = std.crypto;
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+
+pub const path = @import("fs/path.zig");
+pub const File = @import("fs/file.zig").File;
+
+pub const symLink = os.symlink;
+pub const symLinkC = os.symlinkC;
+pub const deleteFile = os.unlink;
+pub const deleteFileC = os.unlinkC;
+pub const rename = os.rename;
+pub const renameC = os.renameC;
+pub const renameW = os.renameW;
+pub const realpath = os.realpath;
+pub const realpathC = os.realpathC;
+pub const realpathW = os.realpathW;
+
+pub const getAppDataDir = @import("fs/get_app_data_dir.zig").getAppDataDir;
+pub const GetAppDataDirError = @import("fs/get_app_data_dir.zig").GetAppDataDirError;
+
+/// This represents the maximum size of a UTF-8 encoded file path.
+/// All file system operations which return a path are guaranteed to
+/// fit into a UTF-8 encoded array of this length.
+/// path being too long if it is this 0long
+pub const MAX_PATH_BYTES = switch (builtin.os) {
+ .linux, .macosx, .ios, .freebsd, .netbsd => os.PATH_MAX,
+ // Each UTF-16LE character may be expanded to 3 UTF-8 bytes.
+ // If it would require 4 UTF-8 bytes, then there would be a surrogate
+ // pair in the UTF-16LE, and we (over)account 3 bytes for it that way.
+ // +1 for the null byte at the end, which can be encoded in 1 byte.
+ .windows => os.windows.PATH_MAX_WIDE * 3 + 1,
+ else => @compileError("Unsupported OS"),
+};
+
+// here we replace the standard +/ with -_ so that it can be used in a file name
+const b64_fs_encoder = base64.Base64Encoder.init("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", base64.standard_pad_char);
+
+/// TODO remove the allocator requirement from this API
+pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: []const u8) !void {
+ if (symLink(existing_path, new_path)) {
+ return;
+ } else |err| switch (err) {
+ error.PathAlreadyExists => {},
+ else => return err, // TODO zig should know this set does not include PathAlreadyExists
+ }
+
+ const dirname = path.dirname(new_path) orelse ".";
+
+ var rand_buf: [12]u8 = undefined;
+ const tmp_path = try allocator.alloc(u8, dirname.len + 1 + base64.Base64Encoder.calcSize(rand_buf.len));
+ defer allocator.free(tmp_path);
+ mem.copy(u8, tmp_path[0..], dirname);
+ tmp_path[dirname.len] = path.sep;
+ while (true) {
+ try crypto.randomBytes(rand_buf[0..]);
+ b64_fs_encoder.encode(tmp_path[dirname.len + 1 ..], rand_buf);
+
+ if (symLink(existing_path, tmp_path)) {
+ return rename(tmp_path, new_path);
+ } else |err| switch (err) {
+ error.PathAlreadyExists => continue,
+ else => return err, // TODO zig should know this set does not include PathAlreadyExists
+ }
+ }
+}
+
+// TODO fix enum literal not casting to error union
+const PrevStatus = enum {
+ stale,
+ fresh,
+};
+
+pub fn updateFile(source_path: []const u8, dest_path: []const u8) !PrevStatus {
+ return updateFileMode(source_path, dest_path, null);
+}
+
+/// 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.
+/// TODO https://github.com/ziglang/zig/issues/2885
+pub fn updateFileMode(source_path: []const u8, dest_path: []const u8, mode: ?File.Mode) !PrevStatus {
+ var src_file = try File.openRead(source_path);
+ defer src_file.close();
+
+ const src_stat = try src_file.stat();
+ check_dest_stat: {
+ const dest_stat = blk: {
+ var dest_file = File.openRead(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
+ src_stat.mode == dest_stat.mode)
+ {
+ return PrevStatus.fresh;
+ }
+ }
+ const actual_mode = mode orelse src_stat.mode;
+
+ // TODO this logic could be made more efficient by calling makePath, once
+ // that API does not require an allocator
+ var atomic_file = make_atomic_file: while (true) {
+ const af = AtomicFile.init(dest_path, actual_mode) catch |err| switch (err) {
+ error.FileNotFound => {
+ var p = dest_path;
+ while (path.dirname(p)) |dirname| {
+ makeDir(dirname) catch |e| switch (e) {
+ error.FileNotFound => {
+ p = dirname;
+ continue;
+ },
+ else => return e,
+ };
+ continue :make_atomic_file;
+ } else {
+ return err;
+ }
+ },
+ else => |e| return e,
+ };
+ break af;
+ } else unreachable;
+ defer atomic_file.deinit();
+
+ const in_stream = &src_file.inStream().stream;
+
+ var buf: [mem.page_size * 6]u8 = undefined;
+ while (true) {
+ const amt = try in_stream.readFull(buf[0..]);
+ try atomic_file.file.write(buf[0..amt]);
+ if (amt != buf.len) {
+ try atomic_file.file.updateTimes(src_stat.atime, src_stat.mtime);
+ try atomic_file.finish();
+ return PrevStatus.stale;
+ }
+ }
+}
+
+/// Guaranteed to be atomic. However 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.
+/// Destination file will have the same mode as the source file.
+pub fn copyFile(source_path: []const u8, dest_path: []const u8) !void {
+ var in_file = try File.openRead(source_path);
+ defer in_file.close();
+
+ const mode = try in_file.mode();
+ const in_stream = &in_file.inStream().stream;
+
+ var atomic_file = try AtomicFile.init(dest_path, mode);
+ defer atomic_file.deinit();
+
+ var buf: [mem.page_size]u8 = undefined;
+ while (true) {
+ const amt = try in_stream.readFull(buf[0..]);
+ try atomic_file.file.write(buf[0..amt]);
+ if (amt != buf.len) {
+ return atomic_file.finish();
+ }
+ }
+}
+
+/// Guaranteed to be atomic. However 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
+pub fn copyFileMode(source_path: []const u8, dest_path: []const u8, mode: File.Mode) !void {
+ var in_file = try File.openRead(source_path);
+ defer in_file.close();
+
+ var atomic_file = try AtomicFile.init(dest_path, mode);
+ defer atomic_file.deinit();
+
+ var buf: [mem.page_size * 6]u8 = undefined;
+ while (true) {
+ const amt = try in_file.read(buf[0..]);
+ try atomic_file.file.write(buf[0..amt]);
+ if (amt != buf.len) {
+ return atomic_file.finish();
+ }
+ }
+}
+
+pub const AtomicFile = struct {
+ file: File,
+ tmp_path_buf: [MAX_PATH_BYTES]u8,
+ dest_path: []const u8,
+ finished: bool,
+
+ const InitError = File.OpenError;
+
+ /// dest_path must remain valid for the lifetime of AtomicFile
+ /// call finish to atomically replace dest_path with contents
+ /// TODO once we have null terminated pointers, use the
+ /// openWriteNoClobberN function
+ pub fn init(dest_path: []const u8, mode: File.Mode) InitError!AtomicFile {
+ const dirname = path.dirname(dest_path);
+ var rand_buf: [12]u8 = undefined;
+ const dirname_component_len = if (dirname) |d| d.len + 1 else 0;
+ const encoded_rand_len = comptime base64.Base64Encoder.calcSize(rand_buf.len);
+ const tmp_path_len = dirname_component_len + encoded_rand_len;
+ var tmp_path_buf: [MAX_PATH_BYTES]u8 = undefined;
+ if (tmp_path_len >= tmp_path_buf.len) return error.NameTooLong;
+
+ if (dirname) |dir| {
+ mem.copy(u8, tmp_path_buf[0..], dir);
+ tmp_path_buf[dir.len] = path.sep;
+ }
+
+ tmp_path_buf[tmp_path_len] = 0;
+
+ while (true) {
+ try crypto.randomBytes(rand_buf[0..]);
+ b64_fs_encoder.encode(tmp_path_buf[dirname_component_len..tmp_path_len], rand_buf);
+
+ const file = File.openWriteNoClobberC(&tmp_path_buf, mode) catch |err| switch (err) {
+ error.PathAlreadyExists => continue,
+ // TODO zig should figure out that this error set does not include PathAlreadyExists since
+ // it is handled in the above switch
+ else => return err,
+ };
+
+ return AtomicFile{
+ .file = file,
+ .tmp_path_buf = tmp_path_buf,
+ .dest_path = dest_path,
+ .finished = false,
+ };
+ }
+ }
+
+ /// always call deinit, even after successful finish()
+ pub fn deinit(self: *AtomicFile) void {
+ if (!self.finished) {
+ self.file.close();
+ deleteFileC(&self.tmp_path_buf) catch {};
+ self.finished = true;
+ }
+ }
+
+ pub fn finish(self: *AtomicFile) !void {
+ assert(!self.finished);
+ self.file.close();
+ self.finished = true;
+ if (os.windows.is_the_target) {
+ const dest_path_w = try os.windows.sliceToPrefixedFileW(self.dest_path);
+ const tmp_path_w = try os.windows.cStrToPrefixedFileW(&self.tmp_path_buf);
+ return os.renameW(&tmp_path_w, &dest_path_w);
+ }
+ const dest_path_c = try os.toPosixPath(self.dest_path);
+ return os.renameC(&self.tmp_path_buf, &dest_path_c);
+ }
+};
+
+const default_new_dir_mode = 0o755;
+
+/// Create a new directory.
+pub fn makeDir(dir_path: []const u8) !void {
+ return os.mkdir(dir_path, default_new_dir_mode);
+}
+
+/// Same as `makeDir` except the parameter is a null-terminated UTF8-encoded string.
+pub fn makeDirC(dir_path: [*]const u8) !void {
+ return os.mkdirC(dir_path, default_new_dir_mode);
+}
+
+/// Same as `makeDir` except the parameter is a null-terminated UTF16LE-encoded string.
+pub fn makeDirW(dir_path: [*]const u16) !void {
+ return os.mkdirW(dir_path, default_new_dir_mode);
+}
+
+/// Calls makeDir recursively to make an entire path. 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.
+/// TODO determine if we can remove the allocator requirement from this function
+pub fn makePath(allocator: *Allocator, full_path: []const u8) !void {
+ const resolved_path = try path.resolve(allocator, [_][]const u8{full_path});
+ defer allocator.free(resolved_path);
+
+ var end_index: usize = resolved_path.len;
+ while (true) {
+ makeDir(resolved_path[0..end_index]) 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
+ if (end_index == resolved_path.len) return;
+ },
+ error.FileNotFound => {
+ // march end_index backward until next path component
+ while (true) {
+ end_index -= 1;
+ if (path.isSep(resolved_path[end_index])) break;
+ }
+ continue;
+ },
+ else => return err,
+ };
+ if (end_index == resolved_path.len) return;
+ // march end_index forward until next path component
+ while (true) {
+ end_index += 1;
+ if (end_index == resolved_path.len or path.isSep(resolved_path[end_index])) break;
+ }
+ }
+}
+
+/// Returns `error.DirNotEmpty` if the directory is not empty.
+/// To delete a directory recursively, see `deleteTree`.
+pub fn deleteDir(dir_path: []const u8) !void {
+ return os.rmdir(dir_path);
+}
+
+/// Same as `deleteDir` except the parameter is a null-terminated UTF8-encoded string.
+pub fn deleteDirC(dir_path: [*]const u8) !void {
+ return os.rmdirC(dir_path);
+}
+
+/// Same as `deleteDir` except the parameter is a null-terminated UTF16LE-encoded string.
+pub fn deleteDirW(dir_path: [*]const u16) !void {
+ return os.rmdirW(dir_path);
+}
+
+const DeleteTreeError = error{
+ OutOfMemory,
+ AccessDenied,
+ FileTooBig,
+ IsDir,
+ SymLinkLoop,
+ ProcessFdQuotaExceeded,
+ NameTooLong,
+ SystemFdQuotaExceeded,
+ NoDevice,
+ SystemResources,
+ NoSpaceLeft,
+ PathAlreadyExists,
+ ReadOnlyFileSystem,
+ NotDir,
+ FileNotFound,
+ FileSystem,
+ FileBusy,
+ DirNotEmpty,
+ DeviceBusy,
+
+ /// On Windows, file paths must be valid Unicode.
+ InvalidUtf8,
+
+ /// On Windows, file paths cannot contain these characters:
+ /// '/', '*', '?', '"', '<', '>', '|'
+ BadPathName,
+
+ Unexpected,
+};
+
+/// 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.
+/// TODO determine if we can remove the allocator requirement
+/// https://github.com/ziglang/zig/issues/2886
+pub fn deleteTree(allocator: *Allocator, full_path: []const u8) DeleteTreeError!void {
+ start_over: while (true) {
+ var got_access_denied = false;
+ // First, try deleting the item as a file. This way we don't follow sym links.
+ if (deleteFile(full_path)) {
+ return;
+ } else |err| switch (err) {
+ error.FileNotFound => return,
+ error.IsDir => {},
+ error.AccessDenied => got_access_denied = true,
+
+ error.InvalidUtf8,
+ error.SymLinkLoop,
+ error.NameTooLong,
+ error.SystemResources,
+ error.ReadOnlyFileSystem,
+ error.NotDir,
+ error.FileSystem,
+ error.FileBusy,
+ error.BadPathName,
+ error.Unexpected,
+ => return err,
+ }
+ {
+ var dir = Dir.open(allocator, full_path) catch |err| switch (err) {
+ error.NotDir => {
+ if (got_access_denied) {
+ return error.AccessDenied;
+ }
+ continue :start_over;
+ },
+
+ error.OutOfMemory,
+ error.AccessDenied,
+ error.FileTooBig,
+ error.IsDir,
+ error.SymLinkLoop,
+ error.ProcessFdQuotaExceeded,
+ error.NameTooLong,
+ error.SystemFdQuotaExceeded,
+ error.NoDevice,
+ error.FileNotFound,
+ error.SystemResources,
+ error.NoSpaceLeft,
+ error.PathAlreadyExists,
+ error.Unexpected,
+ error.InvalidUtf8,
+ error.BadPathName,
+ error.DeviceBusy,
+ => return err,
+ };
+ defer dir.close();
+
+ var full_entry_buf = std.ArrayList(u8).init(allocator);
+ defer full_entry_buf.deinit();
+
+ while (try dir.next()) |entry| {
+ try full_entry_buf.resize(full_path.len + entry.name.len + 1);
+ const full_entry_path = full_entry_buf.toSlice();
+ mem.copy(u8, full_entry_path, full_path);
+ full_entry_path[full_path.len] = path.sep;
+ mem.copy(u8, full_entry_path[full_path.len + 1 ..], entry.name);
+
+ try deleteTree(allocator, full_entry_path);
+ }
+ }
+ return deleteDir(full_path);
+ }
+}
+
+/// TODO: separate this API into the one that opens directory handles to then subsequently open
+/// files, and into the one that reads files from an open directory handle.
+pub const Dir = struct {
+ handle: Handle,
+ allocator: *Allocator,
+
+ pub const Handle = switch (builtin.os) {
+ .macosx, .ios, .freebsd, .netbsd => struct {
+ fd: i32,
+ seek: i64,
+ buf: []u8,
+ index: usize,
+ end_index: usize,
+ },
+ .linux => struct {
+ fd: i32,
+ buf: []u8,
+ index: usize,
+ end_index: usize,
+ },
+ .windows => struct {
+ handle: os.windows.HANDLE,
+ find_file_data: os.windows.WIN32_FIND_DATAW,
+ first: bool,
+ name_data: [256]u8,
+ },
+ else => @compileError("unimplemented"),
+ };
+
+ pub const Entry = struct {
+ name: []const u8,
+ kind: Kind,
+
+ pub const Kind = enum {
+ BlockDevice,
+ CharacterDevice,
+ Directory,
+ NamedPipe,
+ SymLink,
+ File,
+ UnixDomainSocket,
+ Whiteout,
+ Unknown,
+ };
+ };
+
+ pub const OpenError = error{
+ FileNotFound,
+ NotDir,
+ AccessDenied,
+ FileTooBig,
+ IsDir,
+ SymLinkLoop,
+ ProcessFdQuotaExceeded,
+ NameTooLong,
+ SystemFdQuotaExceeded,
+ NoDevice,
+ SystemResources,
+ NoSpaceLeft,
+ PathAlreadyExists,
+ OutOfMemory,
+ InvalidUtf8,
+ BadPathName,
+ DeviceBusy,
+
+ Unexpected,
+ };
+
+ /// Call close when done.
+ /// TODO remove the allocator requirement from this API
+ /// https://github.com/ziglang/zig/issues/2885
+ pub fn open(allocator: *Allocator, dir_path: []const u8) OpenError!Dir {
+ return Dir{
+ .allocator = allocator,
+ .handle = switch (builtin.os) {
+ .windows => blk: {
+ var find_file_data: os.windows.WIN32_FIND_DATAW = undefined;
+ const handle = try os.windows.FindFirstFile(dir_path, &find_file_data);
+ break :blk Handle{
+ .handle = handle,
+ .find_file_data = find_file_data, // TODO guaranteed copy elision
+ .first = true,
+ .name_data = undefined,
+ };
+ },
+ .macosx, .ios, .freebsd, .netbsd => Handle{
+ .fd = try os.open(dir_path, os.O_RDONLY | os.O_NONBLOCK | os.O_DIRECTORY | os.O_CLOEXEC, 0),
+ .seek = 0,
+ .index = 0,
+ .end_index = 0,
+ .buf = [_]u8{},
+ },
+ .linux => Handle{
+ .fd = try os.open(dir_path, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC, 0),
+ .index = 0,
+ .end_index = 0,
+ .buf = [_]u8{},
+ },
+ else => @compileError("unimplemented"),
+ },
+ };
+ }
+
+ pub fn close(self: *Dir) void {
+ if (os.windows.is_the_target) {
+ return os.windows.FindClose(self.handle.handle);
+ }
+ self.allocator.free(self.handle.buf);
+ os.close(self.handle.fd);
+ }
+
+ /// 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: *Dir) !?Entry {
+ switch (builtin.os) {
+ .linux => return self.nextLinux(),
+ .macosx, .ios => return self.nextDarwin(),
+ .windows => return self.nextWindows(),
+ .freebsd => return self.nextBsd(),
+ .netbsd => return self.nextBsd(),
+ else => @compileError("unimplemented"),
+ }
+ }
+
+ pub fn openRead(self: Dir, file_path: []const u8) os.OpenError!File {
+ const path_c = try os.toPosixPath(file_path);
+ return self.openReadC(&path_c);
+ }
+
+ pub fn openReadC(self: Dir, file_path: [*]const u8) OpenError!File {
+ const flags = os.O_LARGEFILE | os.O_RDONLY;
+ const fd = try os.openatC(self.handle.fd, file_path, flags, 0);
+ return File.openHandle(fd);
+ }
+
+ fn nextDarwin(self: *Dir) !?Entry {
+ start_over: while (true) {
+ if (self.handle.index >= self.handle.end_index) {
+ if (self.handle.buf.len == 0) {
+ self.handle.buf = try self.allocator.alloc(u8, mem.page_size);
+ }
+
+ while (true) {
+ const rc = os.system.__getdirentries64(
+ self.handle.fd,
+ self.handle.buf.ptr,
+ self.handle.buf.len,
+ &self.handle.seek,
+ );
+ if (rc == 0) return null;
+ if (rc < 0) {
+ switch (os.errno(rc)) {
+ os.EBADF => unreachable,
+ os.EFAULT => unreachable,
+ os.ENOTDIR => unreachable,
+ os.EINVAL => {
+ self.handle.buf = try self.allocator.realloc(self.handle.buf, self.handle.buf.len * 2);
+ continue;
+ },
+ else => |err| return os.unexpectedErrno(err),
+ }
+ }
+ self.handle.index = 0;
+ self.handle.end_index = @intCast(usize, rc);
+ break;
+ }
+ }
+ const darwin_entry = @ptrCast(*align(1) os.dirent, &self.handle.buf[self.handle.index]);
+ const next_index = self.handle.index + darwin_entry.d_reclen;
+ self.handle.index = next_index;
+
+ const name = @ptrCast([*]u8, &darwin_entry.d_name)[0..darwin_entry.d_namlen];
+
+ if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) {
+ continue :start_over;
+ }
+
+ const entry_kind = switch (darwin_entry.d_type) {
+ os.DT_BLK => Entry.Kind.BlockDevice,
+ os.DT_CHR => Entry.Kind.CharacterDevice,
+ os.DT_DIR => Entry.Kind.Directory,
+ os.DT_FIFO => Entry.Kind.NamedPipe,
+ os.DT_LNK => Entry.Kind.SymLink,
+ os.DT_REG => Entry.Kind.File,
+ os.DT_SOCK => Entry.Kind.UnixDomainSocket,
+ os.DT_WHT => Entry.Kind.Whiteout,
+ else => Entry.Kind.Unknown,
+ };
+ return Entry{
+ .name = name,
+ .kind = entry_kind,
+ };
+ }
+ }
+
+ fn nextWindows(self: *Dir) !?Entry {
+ while (true) {
+ if (self.handle.first) {
+ self.handle.first = false;
+ } else {
+ if (!try os.windows.FindNextFile(self.handle.handle, &self.handle.find_file_data))
+ return null;
+ }
+ const name_utf16le = mem.toSlice(u16, self.handle.find_file_data.cFileName[0..].ptr);
+ 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.handle.name_data[0..], name_utf16le) catch unreachable;
+ const name_utf8 = self.handle.name_data[0..name_utf8_len];
+ const kind = blk: {
+ const attrs = self.handle.find_file_data.dwFileAttributes;
+ if (attrs & os.windows.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk Entry.Kind.Directory;
+ if (attrs & os.windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk Entry.Kind.SymLink;
+ break :blk Entry.Kind.File;
+ };
+ return Entry{
+ .name = name_utf8,
+ .kind = kind,
+ };
+ }
+ }
+
+ fn nextLinux(self: *Dir) !?Entry {
+ start_over: while (true) {
+ if (self.handle.index >= self.handle.end_index) {
+ if (self.handle.buf.len == 0) {
+ self.handle.buf = try self.allocator.alloc(u8, mem.page_size);
+ }
+
+ while (true) {
+ const rc = os.linux.getdents64(self.handle.fd, self.handle.buf.ptr, self.handle.buf.len);
+ switch (os.linux.getErrno(rc)) {
+ 0 => {},
+ os.EBADF => unreachable,
+ os.EFAULT => unreachable,
+ os.ENOTDIR => unreachable,
+ os.EINVAL => {
+ self.handle.buf = try self.allocator.realloc(self.handle.buf, self.handle.buf.len * 2);
+ continue;
+ },
+ else => |err| return os.unexpectedErrno(err),
+ }
+ if (rc == 0) return null;
+ self.handle.index = 0;
+ self.handle.end_index = rc;
+ break;
+ }
+ }
+ const linux_entry = @ptrCast(*align(1) os.dirent64, &self.handle.buf[self.handle.index]);
+ const next_index = self.handle.index + linux_entry.d_reclen;
+ self.handle.index = next_index;
+
+ const name = mem.toSlice(u8, @ptrCast([*]u8, &linux_entry.d_name));
+
+ // skip . and .. entries
+ if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) {
+ continue :start_over;
+ }
+
+ const entry_kind = switch (linux_entry.d_type) {
+ os.DT_BLK => Entry.Kind.BlockDevice,
+ os.DT_CHR => Entry.Kind.CharacterDevice,
+ os.DT_DIR => Entry.Kind.Directory,
+ os.DT_FIFO => Entry.Kind.NamedPipe,
+ os.DT_LNK => Entry.Kind.SymLink,
+ os.DT_REG => Entry.Kind.File,
+ os.DT_SOCK => Entry.Kind.UnixDomainSocket,
+ else => Entry.Kind.Unknown,
+ };
+ return Entry{
+ .name = name,
+ .kind = entry_kind,
+ };
+ }
+ }
+
+ fn nextBsd(self: *Dir) !?Entry {
+ start_over: while (true) {
+ if (self.handle.index >= self.handle.end_index) {
+ if (self.handle.buf.len == 0) {
+ self.handle.buf = try self.allocator.alloc(u8, mem.page_size);
+ }
+
+ while (true) {
+ const rc = os.system.getdirentries(
+ self.handle.fd,
+ self.handle.buf.ptr,
+ self.handle.buf.len,
+ &self.handle.seek,
+ );
+ switch (os.errno(rc)) {
+ 0 => {},
+ os.EBADF => unreachable,
+ os.EFAULT => unreachable,
+ os.ENOTDIR => unreachable,
+ os.EINVAL => {
+ self.handle.buf = try self.allocator.realloc(self.handle.buf, self.handle.buf.len * 2);
+ continue;
+ },
+ else => |err| return os.unexpectedErrno(err),
+ }
+ if (rc == 0) return null;
+ self.handle.index = 0;
+ self.handle.end_index = @intCast(usize, rc);
+ break;
+ }
+ }
+ const freebsd_entry = @ptrCast(*align(1) os.dirent, &self.handle.buf[self.handle.index]);
+ const next_index = self.handle.index + freebsd_entry.d_reclen;
+ self.handle.index = next_index;
+
+ const name = @ptrCast([*]u8, &freebsd_entry.d_name)[0..freebsd_entry.d_namlen];
+
+ if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) {
+ continue :start_over;
+ }
+
+ const entry_kind = switch (freebsd_entry.d_type) {
+ os.DT_BLK => Entry.Kind.BlockDevice,
+ os.DT_CHR => Entry.Kind.CharacterDevice,
+ os.DT_DIR => Entry.Kind.Directory,
+ os.DT_FIFO => Entry.Kind.NamedPipe,
+ os.DT_LNK => Entry.Kind.SymLink,
+ os.DT_REG => Entry.Kind.File,
+ os.DT_SOCK => Entry.Kind.UnixDomainSocket,
+ os.DT_WHT => Entry.Kind.Whiteout,
+ else => Entry.Kind.Unknown,
+ };
+ return Entry{
+ .name = name,
+ .kind = entry_kind,
+ };
+ }
+ }
+};
+
+pub const Walker = struct {
+ stack: std.ArrayList(StackItem),
+ name_buffer: std.Buffer,
+
+ pub const Entry = struct {
+ path: []const u8,
+ basename: []const u8,
+ kind: Dir.Entry.Kind,
+ };
+
+ const StackItem = struct {
+ dir_it: Dir,
+ 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) !?Entry {
+ while (true) {
+ if (self.stack.len == 0) return null;
+ // `top` becomes invalid after appending to `self.stack`.
+ const top = &self.stack.toSlice()[self.stack.len - 1];
+ const dirname_len = top.dirname_len;
+ if (try top.dir_it.next()) |base| {
+ self.name_buffer.shrink(dirname_len);
+ try self.name_buffer.appendByte(path.sep);
+ try self.name_buffer.append(base.name);
+ if (base.kind == .Directory) {
+ // TODO https://github.com/ziglang/zig/issues/2888
+ var new_dir = try Dir.open(self.stack.allocator, self.name_buffer.toSliceConst());
+ {
+ errdefer new_dir.close();
+ try self.stack.append(StackItem{
+ .dir_it = new_dir,
+ .dirname_len = self.name_buffer.len(),
+ });
+ }
+ }
+ return Entry{
+ .basename = self.name_buffer.toSliceConst()[dirname_len + 1 ..],
+ .path = self.name_buffer.toSliceConst(),
+ .kind = base.kind,
+ };
+ } else {
+ self.stack.pop().dir_it.close();
+ }
+ }
+ }
+
+ pub fn deinit(self: *Walker) void {
+ while (self.stack.popOrNull()) |*item| item.dir_it.close();
+ self.stack.deinit();
+ self.name_buffer.deinit();
+ }
+};
+
+/// Recursively iterates over a directory.
+/// Must call `Walker.deinit` when done.
+/// `dir_path` must not end in a path separator.
+/// TODO: https://github.com/ziglang/zig/issues/2888
+pub fn walkPath(allocator: *Allocator, dir_path: []const u8) !Walker {
+ assert(!mem.endsWith(u8, dir_path, path.sep_str));
+
+ var dir_it = try Dir.open(allocator, dir_path);
+ errdefer dir_it.close();
+
+ var name_buffer = try std.Buffer.init(allocator, dir_path);
+ errdefer name_buffer.deinit();
+
+ var walker = Walker{
+ .stack = std.ArrayList(Walker.StackItem).init(allocator),
+ .name_buffer = name_buffer,
+ };
+
+ try walker.stack.append(Walker.StackItem{
+ .dir_it = dir_it,
+ .dirname_len = dir_path.len,
+ });
+
+ return walker;
+}
+
+/// Read value of a symbolic link.
+/// The return value is a slice of buffer, from index `0`.
+/// TODO https://github.com/ziglang/zig/issues/2888
+pub fn readLink(pathname: []const u8, buffer: *[os.PATH_MAX]u8) ![]u8 {
+ return os.readlink(pathname, buffer);
+}
+
+/// Same as `readLink`, except the `pathname` parameter is null-terminated.
+/// TODO https://github.com/ziglang/zig/issues/2888
+pub fn readLinkC(pathname: [*]const u8, buffer: *[os.PATH_MAX]u8) ![]u8 {
+ return os.readlinkC(pathname, buffer);
+}
+
+pub const OpenSelfExeError = os.OpenError || os.windows.CreateFileError || SelfExePathError;
+
+pub fn openSelfExe() OpenSelfExeError!File {
+ if (os.linux.is_the_target) {
+ return File.openReadC(c"/proc/self/exe");
+ }
+ if (os.windows.is_the_target) {
+ var buf: [os.windows.PATH_MAX_WIDE]u16 = undefined;
+ const wide_slice = try selfExePathW(&buf);
+ return File.openReadW(wide_slice.ptr);
+ }
+ var buf: [MAX_PATH_BYTES]u8 = undefined;
+ const self_exe_path = try selfExePath(&buf);
+ buf[self_exe_path.len] = 0;
+ return File.openReadC(self_exe_path.ptr);
+}
+
+test "openSelfExe" {
+ switch (builtin.os) {
+ .linux, .macosx, .ios, .windows, .freebsd => (try openSelfExe()).close(),
+ else => return error.SkipZigTest, // Unsupported OS.
+ }
+}
+
+pub const SelfExePathError = os.ReadLinkError || os.SysCtlError;
+
+/// Get the path to the current executable.
+/// If you only need the directory, use selfExeDirPath.
+/// If you only want an open file handle, use openSelfExe.
+/// This function may return an error if the current executable
+/// was deleted after spawning.
+/// Returned value is a slice of out_buffer.
+///
+/// On Linux, depends on procfs being mounted. If the currently executing binary has
+/// been deleted, the file path looks something like `/a/b/c/exe (deleted)`.
+/// TODO make the return type of this a null terminated pointer
+pub fn selfExePath(out_buffer: *[MAX_PATH_BYTES]u8) SelfExePathError![]u8 {
+ if (os.darwin.is_the_target) {
+ var u32_len: u32 = out_buffer.len;
+ const rc = std.c._NSGetExecutablePath(out_buffer, &u32_len);
+ if (rc != 0) return error.NameTooLong;
+ return mem.toSlice(u8, out_buffer);
+ }
+ switch (builtin.os) {
+ .linux => return os.readlinkC(c"/proc/self/exe", out_buffer),
+ .freebsd => {
+ var mib = [4]c_int{ os.CTL_KERN, os.KERN_PROC, os.KERN_PROC_PATHNAME, -1 };
+ var out_len: usize = out_buffer.len;
+ try os.sysctl(&mib, out_buffer, &out_len, null, 0);
+ // TODO could this slice from 0 to out_len instead?
+ return mem.toSlice(u8, out_buffer);
+ },
+ .netbsd => {
+ var mib = [4]c_int{ os.CTL_KERN, os.KERN_PROC_ARGS, -1, os.KERN_PROC_PATHNAME };
+ var out_len: usize = out_buffer.len;
+ try os.sysctl(&mib, out_buffer, &out_len, null, 0);
+ // TODO could this slice from 0 to out_len instead?
+ return mem.toSlice(u8, out_buffer);
+ },
+ .windows => {
+ var utf16le_buf: [os.windows.PATH_MAX_WIDE]u16 = undefined;
+ const utf16le_slice = try selfExePathW(&utf16le_buf);
+ // Trust that Windows gives us valid UTF-16LE.
+ const end_index = std.unicode.utf16leToUtf8(out_buffer, utf16le_slice) catch unreachable;
+ return out_buffer[0..end_index];
+ },
+ else => @compileError("std.fs.selfExePath not supported for this target"),
+ }
+}
+
+/// Same as `selfExePath` except the result is UTF16LE-encoded.
+pub fn selfExePathW(out_buffer: *[os.windows.PATH_MAX_WIDE]u16) SelfExePathError![]u16 {
+ return os.windows.GetModuleFileNameW(null, out_buffer, out_buffer.len);
+}
+
+/// `selfExeDirPath` except allocates the result on the heap.
+/// Caller owns returned memory.
+pub fn selfExeDirPathAlloc(allocator: *Allocator) ![]u8 {
+ var buf: [MAX_PATH_BYTES]u8 = undefined;
+ return mem.dupe(allocator, u8, try selfExeDirPath(&buf));
+}
+
+/// Get the directory path that contains the current executable.
+/// Returned value is a slice of out_buffer.
+pub fn selfExeDirPath(out_buffer: *[MAX_PATH_BYTES]u8) SelfExePathError![]const u8 {
+ if (os.linux.is_the_target) {
+ // If the currently executing binary has been deleted,
+ // the file path looks something like `/a/b/c/exe (deleted)`
+ // This path cannot be opened, but it's valid for determining the directory
+ // the executable was in when it was run.
+ const full_exe_path = try os.readlinkC(c"/proc/self/exe", out_buffer);
+ // Assume that /proc/self/exe has an absolute path, and therefore dirname
+ // will not return null.
+ return path.dirname(full_exe_path).?;
+ }
+ const self_exe_path = try selfExePath(out_buffer);
+ // Assume that the OS APIs return absolute paths, and therefore dirname
+ // will not return null.
+ return path.dirname(self_exe_path).?;
+}
+
+/// `realpath`, except caller must free the returned memory.
+pub fn realpathAlloc(allocator: *Allocator, pathname: []const u8) ![]u8 {
+ var buf: [MAX_PATH_BYTES]u8 = undefined;
+ return mem.dupe(allocator, u8, try os.realpath(pathname, &buf));
+}
+
+test "" {
+ _ = @import("fs/path.zig");
+ _ = @import("fs/file.zig");
+ _ = @import("fs/get_app_data_dir.zig");
+}