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");
}
