diff options
Diffstat (limited to 'lib/std/fs.zig')
| -rw-r--r-- | lib/std/fs.zig | 988 |
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"); +} |
