srctree

Andrew Kelley parent 6fc20b3b 0a536a7c f4e426a0
Merge pull request #18076 from ziglang/revert-iterable-dir

std.fs: Absorb `IterableDir` into `Dir`
CMakeLists.txt added: 4428, removed: 4469, total 0
@@ -247,7 +247,9 @@ set(ZIG_STAGE2_SOURCES
"${CMAKE_SOURCE_DIR}/lib/std/fmt/errol/lookup.zig"
"${CMAKE_SOURCE_DIR}/lib/std/fmt/parse_float.zig"
"${CMAKE_SOURCE_DIR}/lib/std/fs.zig"
"${CMAKE_SOURCE_DIR}/lib/std/fs/file.zig"
"${CMAKE_SOURCE_DIR}/lib/std/fs/AtomicFile.zig"
"${CMAKE_SOURCE_DIR}/lib/std/fs/Dir.zig"
"${CMAKE_SOURCE_DIR}/lib/std/fs/File.zig"
"${CMAKE_SOURCE_DIR}/lib/std/fs/get_app_data_dir.zig"
"${CMAKE_SOURCE_DIR}/lib/std/fs/path.zig"
"${CMAKE_SOURCE_DIR}/lib/std/hash.zig"
 
lib/std/Build/Step/InstallDir.zig added: 4428, removed: 4469, total 0
@@ -69,7 +69,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
const dest_prefix = dest_builder.getInstallPath(self.options.install_dir, self.options.install_subdir);
const src_builder = self.step.owner;
const src_dir_path = self.options.source_dir.getPath2(src_builder, step);
var src_dir = src_builder.build_root.handle.openIterableDir(src_dir_path, .{}) catch |err| {
var src_dir = src_builder.build_root.handle.openDir(src_dir_path, .{ .iterate = true }) catch |err| {
return step.fail("unable to open source directory '{}{s}': {s}", .{
src_builder.build_root, src_dir_path, @errorName(err),
});
 
lib/std/child_process.zig added: 4428, removed: 4469, total 0
@@ -976,7 +976,8 @@ fn windowsCreateProcessPathExt(
defer dir_buf.shrinkRetainingCapacity(dir_path_len);
const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z);
break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{}, true) catch return error.FileNotFound;
break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{ .iterate = true }) catch
return error.FileNotFound;
};
defer dir.close();
 
 
lib/std/crypto/Certificate/Bundle.zig added: 4428, removed: 4469, total 0
@@ -160,7 +160,7 @@ pub fn addCertsFromDirPath(
dir: fs.Dir,
sub_dir_path: []const u8,
) AddCertsFromDirPathError!void {
var iterable_dir = try dir.openIterableDir(sub_dir_path, .{});
var iterable_dir = try dir.openDir(sub_dir_path, .{ .iterate = true });
defer iterable_dir.close();
return addCertsFromDir(cb, gpa, iterable_dir);
}
@@ -171,14 +171,14 @@ pub fn addCertsFromDirPathAbsolute(
abs_dir_path: []const u8,
) AddCertsFromDirPathError!void {
assert(fs.path.isAbsolute(abs_dir_path));
var iterable_dir = try fs.openIterableDirAbsolute(abs_dir_path, .{});
var iterable_dir = try fs.openDirAbsolute(abs_dir_path, .{ .iterate = true });
defer iterable_dir.close();
return addCertsFromDir(cb, gpa, iterable_dir);
}
 
pub const AddCertsFromDirError = AddCertsFromFilePathError;
 
pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir) AddCertsFromDirError!void {
pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.Dir) AddCertsFromDirError!void {
var it = iterable_dir.iterate();
while (try it.next()) |entry| {
switch (entry.kind) {
@@ -186,7 +186,7 @@ pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir
else => continue,
}
 
try addCertsFromFilePath(cb, gpa, iterable_dir.dir, entry.name);
try addCertsFromFilePath(cb, gpa, iterable_dir, entry.name);
}
}
 
 
lib/std/fs.zig added: 4428, removed: 4469, total 0
@@ -7,17 +7,19 @@ const base64 = std.base64;
const crypto = std.crypto;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const math = std.math;
 
const is_darwin = builtin.os.tag.isDarwin();
 
pub const AtomicFile = @import("fs/AtomicFile.zig");
pub const Dir = @import("fs/Dir.zig");
pub const File = @import("fs/File.zig");
pub const path = @import("fs/path.zig");
 
pub const has_executable_bit = switch (builtin.os.tag) {
.windows, .wasi => false,
else => true,
};
 
pub const path = @import("fs/path.zig");
pub const File = @import("fs/file.zig").File;
pub const wasi = @import("fs/wasi.zig");
 
// TODO audit these APIs with respect to Dir and absolute paths
@@ -92,6 +94,7 @@ pub const need_async_thread = std.io.is_async and switch (builtin.os.tag) {
};
 
/// TODO remove the allocator requirement from this API
/// TODO move to Dir
pub fn atomicSymLink(allocator: Allocator, existing_path: []const u8, new_path: []const u8) !void {
if (cwd().symLink(existing_path, new_path, .{})) {
return;
@@ -102,7 +105,7 @@ pub fn atomicSymLink(allocator: Allocator, existing_path: []const u8, new_path:
 
const dirname = path.dirname(new_path) orelse ".";
 
var rand_buf: [AtomicFile.RANDOM_BYTES]u8 = undefined;
var rand_buf: [AtomicFile.random_bytes_len]u8 = undefined;
const tmp_path = try allocator.alloc(u8, dirname.len + 1 + base64_encoder.calcSize(rand_buf.len));
defer allocator.free(tmp_path);
@memcpy(tmp_path[0..dirname.len], dirname);
@@ -120,24 +123,14 @@ pub fn atomicSymLink(allocator: Allocator, existing_path: []const u8, new_path:
}
}
 
pub const PrevStatus = enum {
stale,
fresh,
};
 
pub const CopyFileOptions = struct {
/// When this is `null` the mode is copied from the source file.
override_mode: ?File.Mode = null,
};
 
/// Same as `Dir.updateFile`, except asserts that both `source_path` and `dest_path`
/// are absolute. See `Dir.updateFile` for a function that operates on both
/// absolute and relative paths.
pub fn updateFileAbsolute(
source_path: []const u8,
dest_path: []const u8,
args: CopyFileOptions,
) !PrevStatus {
args: Dir.CopyFileOptions,
) !Dir.PrevStatus {
assert(path.isAbsolute(source_path));
assert(path.isAbsolute(dest_path));
const my_cwd = cwd();
@@ -147,112 +140,35 @@ pub fn updateFileAbsolute(
/// Same as `Dir.copyFile`, except asserts that both `source_path` and `dest_path`
/// are absolute. See `Dir.copyFile` for a function that operates on both
/// absolute and relative paths.
pub fn copyFileAbsolute(source_path: []const u8, dest_path: []const u8, args: CopyFileOptions) !void {
pub fn copyFileAbsolute(
source_path: []const u8,
dest_path: []const u8,
args: Dir.CopyFileOptions,
) !void {
assert(path.isAbsolute(source_path));
assert(path.isAbsolute(dest_path));
const my_cwd = cwd();
return Dir.copyFile(my_cwd, source_path, my_cwd, dest_path, args);
}
 
pub const AtomicFile = struct {
file: File,
// TODO either replace this with rand_buf or use []u16 on Windows
tmp_path_buf: [TMP_PATH_LEN:0]u8,
dest_basename: []const u8,
file_open: bool,
file_exists: bool,
close_dir_on_deinit: bool,
dir: Dir,
 
const InitError = File.OpenError;
 
const RANDOM_BYTES = 12;
const TMP_PATH_LEN = base64_encoder.calcSize(RANDOM_BYTES);
 
/// Note that the `Dir.atomicFile` API may be more handy than this lower-level function.
pub fn init(
dest_basename: []const u8,
mode: File.Mode,
dir: Dir,
close_dir_on_deinit: bool,
) InitError!AtomicFile {
var rand_buf: [RANDOM_BYTES]u8 = undefined;
var tmp_path_buf: [TMP_PATH_LEN:0]u8 = undefined;
 
while (true) {
crypto.random.bytes(rand_buf[0..]);
const tmp_path = base64_encoder.encode(&tmp_path_buf, &rand_buf);
tmp_path_buf[tmp_path.len] = 0;
 
const file = dir.createFile(
tmp_path,
.{ .mode = mode, .exclusive = true },
) catch |err| switch (err) {
error.PathAlreadyExists => continue,
else => |e| return e,
};
 
return AtomicFile{
.file = file,
.tmp_path_buf = tmp_path_buf,
.dest_basename = dest_basename,
.file_open = true,
.file_exists = true,
.close_dir_on_deinit = close_dir_on_deinit,
.dir = dir,
};
}
}
 
/// Always call deinit, even after a successful finish().
pub fn deinit(self: *AtomicFile) void {
if (self.file_open) {
self.file.close();
self.file_open = false;
}
if (self.file_exists) {
self.dir.deleteFile(&self.tmp_path_buf) catch {};
self.file_exists = false;
}
if (self.close_dir_on_deinit) {
self.dir.close();
}
self.* = undefined;
}
 
pub const FinishError = std.os.RenameError;
 
pub fn finish(self: *AtomicFile) FinishError!void {
assert(self.file_exists);
if (self.file_open) {
self.file.close();
self.file_open = false;
}
try os.renameat(self.dir.fd, self.tmp_path_buf[0..], self.dir.fd, self.dest_basename);
self.file_exists = false;
}
};
 
const default_new_dir_mode = 0o755;
 
/// Create a new directory, based on an absolute path.
/// Asserts that the path is absolute. See `Dir.makeDir` for a function that operates
/// on both absolute and relative paths.
pub fn makeDirAbsolute(absolute_path: []const u8) !void {
assert(path.isAbsolute(absolute_path));
return os.mkdir(absolute_path, default_new_dir_mode);
return os.mkdir(absolute_path, Dir.default_mode);
}
 
/// Same as `makeDirAbsolute` except the parameter is a null-terminated UTF-8-encoded string.
pub fn makeDirAbsoluteZ(absolute_path_z: [*:0]const u8) !void {
assert(path.isAbsoluteZ(absolute_path_z));
return os.mkdirZ(absolute_path_z, default_new_dir_mode);
return os.mkdirZ(absolute_path_z, Dir.default_mode);
}
 
/// Same as `makeDirAbsolute` except the parameter is a null-terminated WTF-16-encoded string.
pub fn makeDirAbsoluteW(absolute_path_w: [*:0]const u16) !void {
assert(path.isAbsoluteWindowsW(absolute_path_w));
return os.mkdirW(absolute_path_w, default_new_dir_mode);
return os.mkdirW(absolute_path_w, Dir.default_mode);
}
 
/// Same as `Dir.deleteDir` except the path is absolute.
@@ -310,2486 +226,6 @@ pub fn renameW(old_dir: Dir, old_sub_path_w: []const u16, new_dir: Dir, new_sub_
return os.renameatW(old_dir.fd, old_sub_path_w, new_dir.fd, new_sub_path_w);
}
 
/// A directory that can be iterated. It is *NOT* legal to initialize this with a regular `Dir`
/// that has been opened without iteration permission.
pub const IterableDir = struct {
dir: Dir,
 
pub const Entry = struct {
name: []const u8,
kind: Kind,
 
pub const Kind = File.Kind;
};
 
const IteratorError = error{ AccessDenied, SystemResources } || os.UnexpectedError;
 
pub const Iterator = switch (builtin.os.tag) {
.macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => struct {
dir: Dir,
seek: i64,
buf: [1024]u8, // TODO align(@alignOf(os.system.dirent)),
index: usize,
end_index: usize,
first_iter: bool,
 
const Self = @This();
 
pub const Error = IteratorError;
 
/// Memory such as file names referenced in this returned entry becomes invalid
/// with subsequent calls to `next`, as well as when this `Dir` is deinitialized.
pub fn next(self: *Self) Error!?Entry {
switch (builtin.os.tag) {
.macos, .ios => return self.nextDarwin(),
.freebsd, .netbsd, .dragonfly, .openbsd => return self.nextBsd(),
.solaris, .illumos => return self.nextSolaris(),
else => @compileError("unimplemented"),
}
}
 
fn nextDarwin(self: *Self) !?Entry {
start_over: while (true) {
if (self.index >= self.end_index) {
if (self.first_iter) {
std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions
self.first_iter = false;
}
const rc = os.system.__getdirentries64(
self.dir.fd,
&self.buf,
self.buf.len,
&self.seek,
);
if (rc == 0) return null;
if (rc < 0) {
switch (os.errno(rc)) {
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
else => |err| return os.unexpectedErrno(err),
}
}
self.index = 0;
self.end_index = @as(usize, @intCast(rc));
}
const darwin_entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index]));
const next_index = self.index + darwin_entry.reclen();
self.index = next_index;
 
const name = @as([*]u8, @ptrCast(&darwin_entry.d_name))[0..darwin_entry.d_namlen];
 
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (darwin_entry.d_ino == 0)) {
continue :start_over;
}
 
const entry_kind: Entry.Kind = switch (darwin_entry.d_type) {
os.DT.BLK => .block_device,
os.DT.CHR => .character_device,
os.DT.DIR => .directory,
os.DT.FIFO => .named_pipe,
os.DT.LNK => .sym_link,
os.DT.REG => .file,
os.DT.SOCK => .unix_domain_socket,
os.DT.WHT => .whiteout,
else => .unknown,
};
return Entry{
.name = name,
.kind = entry_kind,
};
}
}
 
fn nextSolaris(self: *Self) !?Entry {
start_over: while (true) {
if (self.index >= self.end_index) {
if (self.first_iter) {
std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions
self.first_iter = false;
}
const rc = os.system.getdents(self.dir.fd, &self.buf, self.buf.len);
switch (os.errno(rc)) {
.SUCCESS => {},
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
else => |err| return os.unexpectedErrno(err),
}
if (rc == 0) return null;
self.index = 0;
self.end_index = @as(usize, @intCast(rc));
}
const entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index]));
const next_index = self.index + entry.reclen();
self.index = next_index;
 
const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&entry.d_name)), 0);
if (mem.eql(u8, name, ".") or mem.eql(u8, name, ".."))
continue :start_over;
 
// Solaris dirent doesn't expose d_type, so we have to call stat to get it.
const stat_info = os.fstatat(
self.dir.fd,
name,
os.AT.SYMLINK_NOFOLLOW,
) catch |err| switch (err) {
error.NameTooLong => unreachable,
error.SymLinkLoop => unreachable,
error.FileNotFound => unreachable, // lost the race
else => |e| return e,
};
const entry_kind: Entry.Kind = switch (stat_info.mode & os.S.IFMT) {
os.S.IFIFO => .named_pipe,
os.S.IFCHR => .character_device,
os.S.IFDIR => .directory,
os.S.IFBLK => .block_device,
os.S.IFREG => .file,
os.S.IFLNK => .sym_link,
os.S.IFSOCK => .unix_domain_socket,
os.S.IFDOOR => .door,
os.S.IFPORT => .event_port,
else => .unknown,
};
return Entry{
.name = name,
.kind = entry_kind,
};
}
}
 
fn nextBsd(self: *Self) !?Entry {
start_over: while (true) {
if (self.index >= self.end_index) {
if (self.first_iter) {
std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions
self.first_iter = false;
}
const rc = if (builtin.os.tag == .netbsd)
os.system.__getdents30(self.dir.fd, &self.buf, self.buf.len)
else
os.system.getdents(self.dir.fd, &self.buf, self.buf.len);
switch (os.errno(rc)) {
.SUCCESS => {},
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
// Introduced in freebsd 13.2: directory unlinked but still open.
// To be consistent, iteration ends if the directory being iterated is deleted during iteration.
.NOENT => return null,
else => |err| return os.unexpectedErrno(err),
}
if (rc == 0) return null;
self.index = 0;
self.end_index = @as(usize, @intCast(rc));
}
const bsd_entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index]));
const next_index = self.index + bsd_entry.reclen();
self.index = next_index;
 
const name = @as([*]u8, @ptrCast(&bsd_entry.d_name))[0..bsd_entry.d_namlen];
 
const skip_zero_fileno = switch (builtin.os.tag) {
// d_fileno=0 is used to mark invalid entries or deleted files.
.openbsd, .netbsd => true,
else => false,
};
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or
(skip_zero_fileno and bsd_entry.d_fileno == 0))
{
continue :start_over;
}
 
const entry_kind: Entry.Kind = switch (bsd_entry.d_type) {
os.DT.BLK => .block_device,
os.DT.CHR => .character_device,
os.DT.DIR => .directory,
os.DT.FIFO => .named_pipe,
os.DT.LNK => .sym_link,
os.DT.REG => .file,
os.DT.SOCK => .unix_domain_socket,
os.DT.WHT => .whiteout,
else => .unknown,
};
return Entry{
.name = name,
.kind = entry_kind,
};
}
}
 
pub fn reset(self: *Self) void {
self.index = 0;
self.end_index = 0;
self.first_iter = true;
}
},
.haiku => struct {
dir: Dir,
buf: [1024]u8, // TODO align(@alignOf(os.dirent64)),
index: usize,
end_index: usize,
first_iter: bool,
 
const Self = @This();
 
pub const Error = IteratorError;
 
/// Memory such as file names referenced in this returned entry becomes invalid
/// with subsequent calls to `next`, as well as when this `Dir` is deinitialized.
pub fn next(self: *Self) Error!?Entry {
start_over: while (true) {
// TODO: find a better max
const HAIKU_MAX_COUNT = 10000;
if (self.index >= self.end_index) {
if (self.first_iter) {
std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions
self.first_iter = false;
}
const rc = os.system._kern_read_dir(
self.dir.fd,
&self.buf,
self.buf.len,
HAIKU_MAX_COUNT,
);
if (rc == 0) return null;
if (rc < 0) {
switch (os.errno(rc)) {
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
else => |err| return os.unexpectedErrno(err),
}
}
self.index = 0;
self.end_index = @as(usize, @intCast(rc));
}
const haiku_entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index]));
const next_index = self.index + haiku_entry.reclen();
self.index = next_index;
const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&haiku_entry.d_name)), 0);
 
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (haiku_entry.d_ino == 0)) {
continue :start_over;
}
 
var stat_info: os.Stat = undefined;
const rc = os.system._kern_read_stat(
self.dir.fd,
&haiku_entry.d_name,
false,
&stat_info,
0,
);
if (rc != 0) {
switch (os.errno(rc)) {
.SUCCESS => {},
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
else => |err| return os.unexpectedErrno(err),
}
}
const statmode = stat_info.mode & os.S.IFMT;
 
const entry_kind: Entry.Kind = switch (statmode) {
os.S.IFDIR => .directory,
os.S.IFBLK => .block_device,
os.S.IFCHR => .character_device,
os.S.IFLNK => .sym_link,
os.S.IFREG => .file,
os.S.IFIFO => .named_pipe,
else => .unknown,
};
 
return Entry{
.name = name,
.kind = entry_kind,
};
}
}
 
pub fn reset(self: *Self) void {
self.index = 0;
self.end_index = 0;
self.first_iter = true;
}
},
.linux => struct {
dir: Dir,
// The if guard is solely there to prevent compile errors from missing `linux.dirent64`
// definition when compiling for other OSes. It doesn't do anything when compiling for Linux.
buf: [1024]u8 align(if (builtin.os.tag != .linux) 1 else @alignOf(linux.dirent64)),
index: usize,
end_index: usize,
first_iter: bool,
 
const Self = @This();
const linux = os.linux;
 
pub const Error = IteratorError;
 
/// Memory such as file names referenced in this returned entry becomes invalid
/// with subsequent calls to `next`, as well as when this `Dir` is deinitialized.
pub fn next(self: *Self) Error!?Entry {
return self.nextLinux() catch |err| switch (err) {
// To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration.
// This matches the behavior of non-Linux UNIX platforms.
error.DirNotFound => null,
else => |e| return e,
};
}
 
pub const ErrorLinux = error{DirNotFound} || IteratorError;
 
/// Implementation of `next` that can return `error.DirNotFound` if the directory being
/// iterated was deleted during iteration (this error is Linux specific).
pub fn nextLinux(self: *Self) ErrorLinux!?Entry {
start_over: while (true) {
if (self.index >= self.end_index) {
if (self.first_iter) {
std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions
self.first_iter = false;
}
const rc = linux.getdents64(self.dir.fd, &self.buf, self.buf.len);
switch (linux.getErrno(rc)) {
.SUCCESS => {},
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration.
.INVAL => return error.Unexpected, // Linux may in some cases return EINVAL when reading /proc/$PID/net.
.ACCES => return error.AccessDenied, // Do not have permission to iterate this directory.
else => |err| return os.unexpectedErrno(err),
}
if (rc == 0) return null;
self.index = 0;
self.end_index = rc;
}
const linux_entry = @as(*align(1) linux.dirent64, @ptrCast(&self.buf[self.index]));
const next_index = self.index + linux_entry.reclen();
self.index = next_index;
 
const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&linux_entry.d_name)), 0);
 
// skip . and .. entries
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) {
continue :start_over;
}
 
const entry_kind: Entry.Kind = switch (linux_entry.d_type) {
linux.DT.BLK => .block_device,
linux.DT.CHR => .character_device,
linux.DT.DIR => .directory,
linux.DT.FIFO => .named_pipe,
linux.DT.LNK => .sym_link,
linux.DT.REG => .file,
linux.DT.SOCK => .unix_domain_socket,
else => .unknown,
};
return Entry{
.name = name,
.kind = entry_kind,
};
}
}
 
pub fn reset(self: *Self) void {
self.index = 0;
self.end_index = 0;
self.first_iter = true;
}
},
.windows => struct {
dir: Dir,
buf: [1024]u8 align(@alignOf(os.windows.FILE_BOTH_DIR_INFORMATION)),
index: usize,
end_index: usize,
first_iter: bool,
name_data: [MAX_NAME_BYTES]u8,
 
const Self = @This();
 
pub const Error = IteratorError;
 
/// Memory such as file names referenced in this returned entry becomes invalid
/// with subsequent calls to `next`, as well as when this `Dir` is deinitialized.
pub fn next(self: *Self) Error!?Entry {
while (true) {
const w = os.windows;
if (self.index >= self.end_index) {
var io: w.IO_STATUS_BLOCK = undefined;
const rc = w.ntdll.NtQueryDirectoryFile(
self.dir.fd,
null,
null,
null,
&io,
&self.buf,
self.buf.len,
.FileBothDirectoryInformation,
w.FALSE,
null,
if (self.first_iter) @as(w.BOOLEAN, w.TRUE) else @as(w.BOOLEAN, w.FALSE),
);
self.first_iter = false;
if (io.Information == 0) return null;
self.index = 0;
self.end_index = io.Information;
switch (rc) {
.SUCCESS => {},
.ACCESS_DENIED => return error.AccessDenied, // Double-check that the Dir was opened with iteration ability
 
else => return w.unexpectedStatus(rc),
}
}
 
// While the official api docs guarantee FILE_BOTH_DIR_INFORMATION to be aligned properly
// this may not always be the case (e.g. due to faulty VM/Sandboxing tools)
const dir_info: *align(2) w.FILE_BOTH_DIR_INFORMATION = @ptrCast(@alignCast(&self.buf[self.index]));
if (dir_info.NextEntryOffset != 0) {
self.index += dir_info.NextEntryOffset;
} else {
self.index = self.buf.len;
}
 
const name_utf16le = @as([*]u16, @ptrCast(&dir_info.FileName))[0 .. dir_info.FileNameLength / 2];
 
if (mem.eql(u16, name_utf16le, &[_]u16{'.'}) or mem.eql(u16, name_utf16le, &[_]u16{ '.', '.' }))
continue;
// Trust that Windows gives us valid UTF-16LE
const name_utf8_len = std.unicode.utf16leToUtf8(self.name_data[0..], name_utf16le) catch unreachable;
const name_utf8 = self.name_data[0..name_utf8_len];
const kind: Entry.Kind = blk: {
const attrs = dir_info.FileAttributes;
if (attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk .directory;
if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk .sym_link;
break :blk .file;
};
return Entry{
.name = name_utf8,
.kind = kind,
};
}
}
 
pub fn reset(self: *Self) void {
self.index = 0;
self.end_index = 0;
self.first_iter = true;
}
},
.wasi => struct {
dir: Dir,
buf: [1024]u8, // TODO align(@alignOf(os.wasi.dirent_t)),
cookie: u64,
index: usize,
end_index: usize,
 
const Self = @This();
 
pub const Error = IteratorError;
 
/// Memory such as file names referenced in this returned entry becomes invalid
/// with subsequent calls to `next`, as well as when this `Dir` is deinitialized.
pub fn next(self: *Self) Error!?Entry {
return self.nextWasi() catch |err| switch (err) {
// To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration.
// This matches the behavior of non-Linux UNIX platforms.
error.DirNotFound => null,
else => |e| return e,
};
}
 
pub const ErrorWasi = error{DirNotFound} || IteratorError;
 
/// Implementation of `next` that can return platform-dependent errors depending on the host platform.
/// When the host platform is Linux, `error.DirNotFound` can be returned if the directory being
/// iterated was deleted during iteration.
pub fn nextWasi(self: *Self) ErrorWasi!?Entry {
// We intentinally use fd_readdir even when linked with libc,
// since its implementation is exactly the same as below,
// and we avoid the code complexity here.
const w = os.wasi;
start_over: while (true) {
// According to the WASI spec, the last entry might be truncated,
// so we need to check if the left buffer contains the whole dirent.
if (self.end_index - self.index < @sizeOf(w.dirent_t)) {
var bufused: usize = undefined;
switch (w.fd_readdir(self.dir.fd, &self.buf, self.buf.len, self.cookie, &bufused)) {
.SUCCESS => {},
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
.NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration.
.NOTCAPABLE => return error.AccessDenied,
else => |err| return os.unexpectedErrno(err),
}
if (bufused == 0) return null;
self.index = 0;
self.end_index = bufused;
}
const entry = @as(*align(1) w.dirent_t, @ptrCast(&self.buf[self.index]));
const entry_size = @sizeOf(w.dirent_t);
const name_index = self.index + entry_size;
if (name_index + entry.d_namlen > self.end_index) {
// This case, the name is truncated, so we need to call readdir to store the entire name.
self.end_index = self.index; // Force fd_readdir in the next loop.
continue :start_over;
}
const name = self.buf[name_index .. name_index + entry.d_namlen];
 
const next_index = name_index + entry.d_namlen;
self.index = next_index;
self.cookie = entry.d_next;
 
// skip . and .. entries
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) {
continue :start_over;
}
 
const entry_kind: Entry.Kind = switch (entry.d_type) {
.BLOCK_DEVICE => .block_device,
.CHARACTER_DEVICE => .character_device,
.DIRECTORY => .directory,
.SYMBOLIC_LINK => .sym_link,
.REGULAR_FILE => .file,
.SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,
else => .unknown,
};
return Entry{
.name = name,
.kind = entry_kind,
};
}
}
 
pub fn reset(self: *Self) void {
self.index = 0;
self.end_index = 0;
self.cookie = os.wasi.DIRCOOKIE_START;
}
},
else => @compileError("unimplemented"),
};
 
pub fn iterate(self: IterableDir) Iterator {
return self.iterateImpl(true);
}
 
/// Like `iterate`, but will not reset the directory cursor before the first
/// iteration. This should only be used in cases where it is known that the
/// `IterableDir` has not had its cursor modified yet (e.g. it was just opened).
pub fn iterateAssumeFirstIteration(self: IterableDir) Iterator {
return self.iterateImpl(false);
}
 
fn iterateImpl(self: IterableDir, first_iter_start_value: bool) Iterator {
switch (builtin.os.tag) {
.macos,
.ios,
.freebsd,
.netbsd,
.dragonfly,
.openbsd,
.solaris,
.illumos,
=> return Iterator{
.dir = self.dir,
.seek = 0,
.index = 0,
.end_index = 0,
.buf = undefined,
.first_iter = first_iter_start_value,
},
.linux, .haiku => return Iterator{
.dir = self.dir,
.index = 0,
.end_index = 0,
.buf = undefined,
.first_iter = first_iter_start_value,
},
.windows => return Iterator{
.dir = self.dir,
.index = 0,
.end_index = 0,
.first_iter = first_iter_start_value,
.buf = undefined,
.name_data = undefined,
},
.wasi => return Iterator{
.dir = self.dir,
.cookie = os.wasi.DIRCOOKIE_START,
.index = 0,
.end_index = 0,
.buf = undefined,
},
else => @compileError("unimplemented"),
}
}
 
pub const Walker = struct {
stack: std.ArrayList(StackItem),
name_buffer: std.ArrayList(u8),
 
pub const WalkerEntry = struct {
/// The containing directory. This can be used to operate directly on `basename`
/// rather than `path`, avoiding `error.NameTooLong` for deeply nested paths.
/// The directory remains open until `next` or `deinit` is called.
dir: Dir,
basename: []const u8,
path: []const u8,
kind: IterableDir.Entry.Kind,
};
 
const StackItem = struct {
iter: IterableDir.Iterator,
dirname_len: usize,
};
 
/// After each call to this function, and on deinit(), the memory returned
/// from this function becomes invalid. A copy must be made in order to keep
/// a reference to the path.
pub fn next(self: *Walker) !?WalkerEntry {
while (self.stack.items.len != 0) {
// `top` and `containing` become invalid after appending to `self.stack`
var top = &self.stack.items[self.stack.items.len - 1];
var containing = top;
var dirname_len = top.dirname_len;
if (top.iter.next() catch |err| {
// If we get an error, then we want the user to be able to continue
// walking if they want, which means that we need to pop the directory
// that errored from the stack. Otherwise, all future `next` calls would
// likely just fail with the same error.
var item = self.stack.pop();
if (self.stack.items.len != 0) {
item.iter.dir.close();
}
return err;
}) |base| {
self.name_buffer.shrinkRetainingCapacity(dirname_len);
if (self.name_buffer.items.len != 0) {
try self.name_buffer.append(path.sep);
dirname_len += 1;
}
try self.name_buffer.appendSlice(base.name);
if (base.kind == .directory) {
var new_dir = top.iter.dir.openIterableDir(base.name, .{}) catch |err| switch (err) {
error.NameTooLong => unreachable, // no path sep in base.name
else => |e| return e,
};
{
errdefer new_dir.close();
try self.stack.append(StackItem{
.iter = new_dir.iterateAssumeFirstIteration(),
.dirname_len = self.name_buffer.items.len,
});
top = &self.stack.items[self.stack.items.len - 1];
containing = &self.stack.items[self.stack.items.len - 2];
}
}
return WalkerEntry{
.dir = containing.iter.dir,
.basename = self.name_buffer.items[dirname_len..],
.path = self.name_buffer.items,
.kind = base.kind,
};
} else {
var item = self.stack.pop();
if (self.stack.items.len != 0) {
item.iter.dir.close();
}
}
}
return null;
}
 
pub fn deinit(self: *Walker) void {
// Close any remaining directories except the initial one (which is always at index 0)
if (self.stack.items.len > 1) {
for (self.stack.items[1..]) |*item| {
item.iter.dir.close();
}
}
self.stack.deinit();
self.name_buffer.deinit();
}
};
 
/// Recursively iterates over a directory.
/// Must call `Walker.deinit` when done.
/// The order of returned file system entries is undefined.
/// `self` will not be closed after walking it.
pub fn walk(self: IterableDir, allocator: Allocator) !Walker {
var name_buffer = std.ArrayList(u8).init(allocator);
errdefer name_buffer.deinit();
 
var stack = std.ArrayList(Walker.StackItem).init(allocator);
errdefer stack.deinit();
 
try stack.append(Walker.StackItem{
.iter = self.iterate(),
.dirname_len = 0,
});
 
return Walker{
.stack = stack,
.name_buffer = name_buffer,
};
}
 
pub fn close(self: *IterableDir) void {
self.dir.close();
self.* = undefined;
}
 
pub const ChmodError = File.ChmodError;
 
/// Changes the mode of the directory.
/// The process must have the correct privileges in order to do this
/// successfully, or must have the effective user ID matching the owner
/// of the directory.
pub fn chmod(self: IterableDir, new_mode: File.Mode) ChmodError!void {
const file: File = .{
.handle = self.dir.fd,
.capable_io_mode = .blocking,
};
try file.chmod(new_mode);
}
 
/// Changes the owner and group of the directory.
/// The process must have the correct privileges in order to do this
/// successfully. The group may be changed by the owner of the directory to
/// any group of which the owner is a member. If the
/// owner or group is specified as `null`, the ID is not changed.
pub fn chown(self: IterableDir, owner: ?File.Uid, group: ?File.Gid) ChownError!void {
const file: File = .{
.handle = self.dir.fd,
.capable_io_mode = .blocking,
};
try file.chown(owner, group);
}
 
pub const ChownError = File.ChownError;
};
 
pub const Dir = struct {
fd: os.fd_t,
 
pub const iterate = @compileError("only 'IterableDir' can be iterated; 'IterableDir' can be obtained with 'openIterableDir'");
pub const walk = @compileError("only 'IterableDir' can be walked; 'IterableDir' can be obtained with 'openIterableDir'");
pub const chmod = @compileError("only 'IterableDir' can have its mode changed; 'IterableDir' can be obtained with 'openIterableDir'");
pub const chown = @compileError("only 'IterableDir' can have its owner changed; 'IterableDir' can be obtained with 'openIterableDir'");
 
pub const OpenError = error{
FileNotFound,
NotDir,
InvalidHandle,
AccessDenied,
SymLinkLoop,
ProcessFdQuotaExceeded,
NameTooLong,
SystemFdQuotaExceeded,
NoDevice,
SystemResources,
InvalidUtf8,
BadPathName,
DeviceBusy,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
} || os.UnexpectedError;
 
pub fn close(self: *Dir) void {
if (need_async_thread) {
std.event.Loop.instance.?.close(self.fd);
} else {
os.close(self.fd);
}
self.* = undefined;
}
 
/// Opens a file for reading or writing, without attempting to create a new file.
/// To create a new file, see `createFile`.
/// Call `File.close` to release the resource.
/// Asserts that the path parameter has no null bytes.
pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File {
if (builtin.os.tag == .windows) {
const path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.openFileW(path_w.span(), flags);
}
if (builtin.os.tag == .wasi and !builtin.link_libc) {
return self.openFileWasi(sub_path, flags);
}
const path_c = try os.toPosixPath(sub_path);
return self.openFileZ(&path_c, flags);
}
 
/// Same as `openFile` but WASI only.
pub fn openFileWasi(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File {
const w = os.wasi;
var fdflags: w.fdflags_t = 0x0;
var base: w.rights_t = 0x0;
if (flags.isRead()) {
base |= w.RIGHT.FD_READ | w.RIGHT.FD_TELL | w.RIGHT.FD_SEEK | w.RIGHT.FD_FILESTAT_GET;
}
if (flags.isWrite()) {
fdflags |= w.FDFLAG.APPEND;
base |= w.RIGHT.FD_WRITE |
w.RIGHT.FD_TELL |
w.RIGHT.FD_SEEK |
w.RIGHT.FD_DATASYNC |
w.RIGHT.FD_FDSTAT_SET_FLAGS |
w.RIGHT.FD_SYNC |
w.RIGHT.FD_ALLOCATE |
w.RIGHT.FD_ADVISE |
w.RIGHT.FD_FILESTAT_SET_TIMES |
w.RIGHT.FD_FILESTAT_SET_SIZE;
}
const fd = try os.openatWasi(self.fd, sub_path, 0x0, 0x0, fdflags, base, 0x0);
return File{ .handle = fd };
}
 
/// Same as `openFile` but the path parameter is null-terminated.
pub fn openFileZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File {
if (builtin.os.tag == .windows) {
const path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path);
return self.openFileW(path_w.span(), flags);
}
 
var os_flags: u32 = 0;
if (@hasDecl(os.O, "CLOEXEC")) os_flags = os.O.CLOEXEC;
 
// Use the O locking flags if the os supports them to acquire the lock
// atomically.
const has_flock_open_flags = @hasDecl(os.O, "EXLOCK");
if (has_flock_open_flags) {
// Note that the O.NONBLOCK flag is removed after the openat() call
// is successful.
const nonblocking_lock_flag: u32 = if (flags.lock_nonblocking)
os.O.NONBLOCK
else
0;
os_flags |= switch (flags.lock) {
.none => @as(u32, 0),
.shared => os.O.SHLOCK | nonblocking_lock_flag,
.exclusive => os.O.EXLOCK | nonblocking_lock_flag,
};
}
if (@hasDecl(os.O, "LARGEFILE")) {
os_flags |= os.O.LARGEFILE;
}
if (@hasDecl(os.O, "NOCTTY") and !flags.allow_ctty) {
os_flags |= os.O.NOCTTY;
}
os_flags |= switch (flags.mode) {
.read_only => @as(u32, os.O.RDONLY),
.write_only => @as(u32, os.O.WRONLY),
.read_write => @as(u32, os.O.RDWR),
};
const fd = if (flags.intended_io_mode != .blocking)
try std.event.Loop.instance.?.openatZ(self.fd, sub_path, os_flags, 0)
else
try os.openatZ(self.fd, sub_path, os_flags, 0);
errdefer os.close(fd);
 
// WASI doesn't have os.flock so we intetinally check OS prior to the inner if block
// since it is not compiltime-known and we need to avoid undefined symbol in Wasm.
if (@hasDecl(os.system, "LOCK") and builtin.target.os.tag != .wasi) {
if (!has_flock_open_flags and flags.lock != .none) {
// TODO: integrate async I/O
const lock_nonblocking = if (flags.lock_nonblocking) os.LOCK.NB else @as(i32, 0);
try os.flock(fd, switch (flags.lock) {
.none => unreachable,
.shared => os.LOCK.SH | lock_nonblocking,
.exclusive => os.LOCK.EX | lock_nonblocking,
});
}
}
 
if (has_flock_open_flags and flags.lock_nonblocking) {
var fl_flags = os.fcntl(fd, os.F.GETFL, 0) catch |err| switch (err) {
error.FileBusy => unreachable,
error.Locked => unreachable,
error.PermissionDenied => unreachable,
error.DeadLock => unreachable,
error.LockedRegionLimitExceeded => unreachable,
else => |e| return e,
};
fl_flags &= ~@as(usize, os.O.NONBLOCK);
_ = os.fcntl(fd, os.F.SETFL, fl_flags) catch |err| switch (err) {
error.FileBusy => unreachable,
error.Locked => unreachable,
error.PermissionDenied => unreachable,
error.DeadLock => unreachable,
error.LockedRegionLimitExceeded => unreachable,
else => |e| return e,
};
}
 
return File{
.handle = fd,
.capable_io_mode = .blocking,
.intended_io_mode = flags.intended_io_mode,
};
}
 
/// Same as `openFile` but Windows-only and the path parameter is
/// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded.
pub fn openFileW(self: Dir, sub_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File {
const w = os.windows;
const file: File = .{
.handle = try w.OpenFile(sub_path_w, .{
.dir = self.fd,
.access_mask = w.SYNCHRONIZE |
(if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) |
(if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0),
.creation = w.FILE_OPEN,
.io_mode = flags.intended_io_mode,
}),
.capable_io_mode = std.io.default_mode,
.intended_io_mode = flags.intended_io_mode,
};
errdefer file.close();
var io: w.IO_STATUS_BLOCK = undefined;
const range_off: w.LARGE_INTEGER = 0;
const range_len: w.LARGE_INTEGER = 1;
const exclusive = switch (flags.lock) {
.none => return file,
.shared => false,
.exclusive => true,
};
try w.LockFile(
file.handle,
null,
null,
null,
&io,
&range_off,
&range_len,
null,
@intFromBool(flags.lock_nonblocking),
@intFromBool(exclusive),
);
return file;
}
 
/// Creates, opens, or overwrites a file with write access.
/// Call `File.close` on the result when done.
/// Asserts that the path parameter has no null bytes.
pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File {
if (builtin.os.tag == .windows) {
const path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.createFileW(path_w.span(), flags);
}
if (builtin.os.tag == .wasi and !builtin.link_libc) {
return self.createFileWasi(sub_path, flags);
}
const path_c = try os.toPosixPath(sub_path);
return self.createFileZ(&path_c, flags);
}
 
/// Same as `createFile` but WASI only.
pub fn createFileWasi(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File {
const w = os.wasi;
var oflags = w.O.CREAT;
var base: w.rights_t = w.RIGHT.FD_WRITE |
w.RIGHT.FD_DATASYNC |
w.RIGHT.FD_SEEK |
w.RIGHT.FD_TELL |
w.RIGHT.FD_FDSTAT_SET_FLAGS |
w.RIGHT.FD_SYNC |
w.RIGHT.FD_ALLOCATE |
w.RIGHT.FD_ADVISE |
w.RIGHT.FD_FILESTAT_SET_TIMES |
w.RIGHT.FD_FILESTAT_SET_SIZE |
w.RIGHT.FD_FILESTAT_GET;
if (flags.read) {
base |= w.RIGHT.FD_READ;
}
if (flags.truncate) {
oflags |= w.O.TRUNC;
}
if (flags.exclusive) {
oflags |= w.O.EXCL;
}
const fd = try os.openatWasi(self.fd, sub_path, 0x0, oflags, 0x0, base, 0x0);
return File{ .handle = fd };
}
 
/// Same as `createFile` but the path parameter is null-terminated.
pub fn createFileZ(self: Dir, sub_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File {
if (builtin.os.tag == .windows) {
const path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c);
return self.createFileW(path_w.span(), flags);
}
 
// Use the O locking flags if the os supports them to acquire the lock
// atomically.
const has_flock_open_flags = @hasDecl(os.O, "EXLOCK");
// Note that the O.NONBLOCK flag is removed after the openat() call
// is successful.
const nonblocking_lock_flag: u32 = if (has_flock_open_flags and flags.lock_nonblocking)
os.O.NONBLOCK
else
0;
const lock_flag: u32 = if (has_flock_open_flags) switch (flags.lock) {
.none => @as(u32, 0),
.shared => os.O.SHLOCK | nonblocking_lock_flag,
.exclusive => os.O.EXLOCK | nonblocking_lock_flag,
} else 0;
 
const O_LARGEFILE = if (@hasDecl(os.O, "LARGEFILE")) os.O.LARGEFILE else 0;
const os_flags = lock_flag | O_LARGEFILE | os.O.CREAT | os.O.CLOEXEC |
(if (flags.truncate) @as(u32, os.O.TRUNC) else 0) |
(if (flags.read) @as(u32, os.O.RDWR) else os.O.WRONLY) |
(if (flags.exclusive) @as(u32, os.O.EXCL) else 0);
const fd = if (flags.intended_io_mode != .blocking)
try std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, os_flags, flags.mode)
else
try os.openatZ(self.fd, sub_path_c, os_flags, flags.mode);
errdefer os.close(fd);
 
// WASI doesn't have os.flock so we intetinally check OS prior to the inner if block
// since it is not compiltime-known and we need to avoid undefined symbol in Wasm.
if (builtin.target.os.tag != .wasi) {
if (!has_flock_open_flags and flags.lock != .none) {
// TODO: integrate async I/O
const lock_nonblocking = if (flags.lock_nonblocking) os.LOCK.NB else @as(i32, 0);
try os.flock(fd, switch (flags.lock) {
.none => unreachable,
.shared => os.LOCK.SH | lock_nonblocking,
.exclusive => os.LOCK.EX | lock_nonblocking,
});
}
}
 
if (has_flock_open_flags and flags.lock_nonblocking) {
var fl_flags = os.fcntl(fd, os.F.GETFL, 0) catch |err| switch (err) {
error.FileBusy => unreachable,
error.Locked => unreachable,
error.PermissionDenied => unreachable,
error.DeadLock => unreachable,
error.LockedRegionLimitExceeded => unreachable,
else => |e| return e,
};
fl_flags &= ~@as(usize, os.O.NONBLOCK);
_ = os.fcntl(fd, os.F.SETFL, fl_flags) catch |err| switch (err) {
error.FileBusy => unreachable,
error.Locked => unreachable,
error.PermissionDenied => unreachable,
error.DeadLock => unreachable,
error.LockedRegionLimitExceeded => unreachable,
else => |e| return e,
};
}
 
return File{
.handle = fd,
.capable_io_mode = .blocking,
.intended_io_mode = flags.intended_io_mode,
};
}
 
/// Same as `createFile` but Windows-only and the path parameter is
/// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded.
pub fn createFileW(self: Dir, sub_path_w: []const u16, flags: File.CreateFlags) File.OpenError!File {
const w = os.windows;
const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0;
const file: File = .{
.handle = try os.windows.OpenFile(sub_path_w, .{
.dir = self.fd,
.access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag,
.creation = if (flags.exclusive)
@as(u32, w.FILE_CREATE)
else if (flags.truncate)
@as(u32, w.FILE_OVERWRITE_IF)
else
@as(u32, w.FILE_OPEN_IF),
.io_mode = flags.intended_io_mode,
}),
.capable_io_mode = std.io.default_mode,
.intended_io_mode = flags.intended_io_mode,
};
errdefer file.close();
var io: w.IO_STATUS_BLOCK = undefined;
const range_off: w.LARGE_INTEGER = 0;
const range_len: w.LARGE_INTEGER = 1;
const exclusive = switch (flags.lock) {
.none => return file,
.shared => false,
.exclusive => true,
};
try w.LockFile(
file.handle,
null,
null,
null,
&io,
&range_off,
&range_len,
null,
@intFromBool(flags.lock_nonblocking),
@intFromBool(exclusive),
);
return file;
}
 
/// Creates a single directory with a relative or absolute path.
/// To create multiple directories to make an entire path, see `makePath`.
/// To operate on only absolute paths, see `makeDirAbsolute`.
pub fn makeDir(self: Dir, sub_path: []const u8) !void {
try os.mkdirat(self.fd, sub_path, default_new_dir_mode);
}
 
/// Creates a single directory with a relative or absolute null-terminated UTF-8-encoded path.
/// To create multiple directories to make an entire path, see `makePath`.
/// To operate on only absolute paths, see `makeDirAbsoluteZ`.
pub fn makeDirZ(self: Dir, sub_path: [*:0]const u8) !void {
try os.mkdiratZ(self.fd, sub_path, default_new_dir_mode);
}
 
/// Creates a single directory with a relative or absolute null-terminated WTF-16-encoded path.
/// To create multiple directories to make an entire path, see `makePath`.
/// To operate on only absolute paths, see `makeDirAbsoluteW`.
pub fn makeDirW(self: Dir, sub_path: [*:0]const u16) !void {
try os.mkdiratW(self.fd, sub_path, default_new_dir_mode);
}
 
/// Calls makeDir iteratively to make an entire path
/// (i.e. creating any parent directories that do not exist).
/// Returns success if the path already exists and is a directory.
/// This function is not atomic, and if it returns an error, the file system may
/// have been modified regardless.
pub fn makePath(self: Dir, sub_path: []const u8) !void {
var it = try path.componentIterator(sub_path);
var component = it.last() orelse return;
while (true) {
self.makeDir(component.path) catch |err| switch (err) {
error.PathAlreadyExists => {
// TODO stat the file and return an error if it's not a directory
// this is important because otherwise a dangling symlink
// could cause an infinite loop
},
error.FileNotFound => |e| {
component = it.previous() orelse return e;
continue;
},
else => |e| return e,
};
component = it.next() orelse return;
}
}
 
/// Calls makeOpenDirAccessMaskW iteratively to make an entire path
/// (i.e. creating any parent directories that do not exist).
/// Opens the dir if the path already exists and is a directory.
/// This function is not atomic, and if it returns an error, the file system may
/// have been modified regardless.
fn makeOpenPathAccessMaskW(self: Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) OpenError!Dir {
const w = os.windows;
var it = try path.componentIterator(sub_path);
// If there are no components in the path, then create a dummy component with the full path.
var component = it.last() orelse path.NativeUtf8ComponentIterator.Component{
.name = "",
.path = sub_path,
};
 
while (true) {
const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path);
const is_last = it.peekNext() == null;
var result = self.makeOpenDirAccessMaskW(sub_path_w.span().ptr, access_mask, .{
.no_follow = no_follow,
.create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE,
}) catch |err| switch (err) {
error.FileNotFound => |e| {
component = it.previous() orelse return e;
continue;
},
else => |e| return e,
};
 
component = it.next() orelse return result;
// Don't leak the intermediate file handles
result.close();
}
}
 
/// This function performs `makePath`, followed by `openDir`.
/// If supported by the OS, this operation is atomic. It is not atomic on
/// all operating systems.
/// On Windows, this function performs `makeOpenPathAccessMaskW`.
pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !Dir {
return switch (builtin.os.tag) {
.windows => {
const w = os.windows;
const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
w.SYNCHRONIZE | w.FILE_TRAVERSE;
 
return self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow);
},
else => {
return self.openDir(sub_path, open_dir_options) catch |err| switch (err) {
error.FileNotFound => {
try self.makePath(sub_path);
return self.openDir(sub_path, open_dir_options);
},
else => |e| return e,
};
},
};
}
 
/// This function performs `makePath`, followed by `openIterableDir`.
/// If supported by the OS, this operation is atomic. It is not atomic on
/// all operating systems.
pub fn makeOpenPathIterable(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !IterableDir {
return switch (builtin.os.tag) {
.windows => {
const w = os.windows;
const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
w.SYNCHRONIZE | w.FILE_TRAVERSE | w.FILE_LIST_DIRECTORY;
 
return IterableDir{
.dir = try self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow),
};
},
else => {
return self.openIterableDir(sub_path, open_dir_options) catch |err| switch (err) {
error.FileNotFound => {
try self.makePath(sub_path);
return self.openIterableDir(sub_path, open_dir_options);
},
else => |e| return e,
};
},
};
}
 
/// This function returns the canonicalized absolute pathname of
/// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this
/// `Dir` handle and returns the canonicalized absolute pathname of `pathname`
/// argument.
/// This function is not universally supported by all platforms.
/// Currently supported hosts are: Linux, macOS, and Windows.
/// See also `Dir.realpathZ`, `Dir.realpathW`, and `Dir.realpathAlloc`.
pub fn realpath(self: Dir, pathname: []const u8, out_buffer: []u8) ![]u8 {
if (builtin.os.tag == .wasi) {
@compileError("realpath is not available on WASI");
}
if (builtin.os.tag == .windows) {
const pathname_w = try os.windows.sliceToPrefixedFileW(self.fd, pathname);
return self.realpathW(pathname_w.span(), out_buffer);
}
const pathname_c = try os.toPosixPath(pathname);
return self.realpathZ(&pathname_c, out_buffer);
}
 
/// Same as `Dir.realpath` except `pathname` is null-terminated.
/// See also `Dir.realpath`, `realpathZ`.
pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) ![]u8 {
if (builtin.os.tag == .windows) {
const pathname_w = try os.windows.cStrToPrefixedFileW(self.fd, pathname);
return self.realpathW(pathname_w.span(), out_buffer);
}
 
const flags = if (builtin.os.tag == .linux) os.O.PATH | os.O.NONBLOCK | os.O.CLOEXEC else os.O.NONBLOCK | os.O.CLOEXEC;
const fd = os.openatZ(self.fd, pathname, flags, 0) catch |err| switch (err) {
error.FileLocksNotSupported => unreachable,
else => |e| return e,
};
defer os.close(fd);
 
// Use of MAX_PATH_BYTES here is valid as the realpath function does not
// have a variant that takes an arbitrary-size buffer.
// TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008
// NULL out parameter (GNU's canonicalize_file_name) to handle overelong
// paths. musl supports passing NULL but restricts the output to PATH_MAX
// anyway.
var buffer: [MAX_PATH_BYTES]u8 = undefined;
const out_path = try os.getFdPath(fd, &buffer);
 
if (out_path.len > out_buffer.len) {
return error.NameTooLong;
}
 
const result = out_buffer[0..out_path.len];
@memcpy(result, out_path);
return result;
}
 
/// Windows-only. Same as `Dir.realpath` except `pathname` is WTF16 encoded.
/// See also `Dir.realpath`, `realpathW`.
pub fn realpathW(self: Dir, pathname: []const u16, out_buffer: []u8) ![]u8 {
const w = os.windows;
 
const access_mask = w.GENERIC_READ | w.SYNCHRONIZE;
const share_access = w.FILE_SHARE_READ;
const creation = w.FILE_OPEN;
const h_file = blk: {
const res = w.OpenFile(pathname, .{
.dir = self.fd,
.access_mask = access_mask,
.share_access = share_access,
.creation = creation,
.io_mode = .blocking,
.filter = .any,
}) catch |err| switch (err) {
error.WouldBlock => unreachable,
else => |e| return e,
};
break :blk res;
};
defer w.CloseHandle(h_file);
 
// Use of MAX_PATH_BYTES here is valid as the realpath function does not
// have a variant that takes an arbitrary-size buffer.
// TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008
// NULL out parameter (GNU's canonicalize_file_name) to handle overelong
// paths. musl supports passing NULL but restricts the output to PATH_MAX
// anyway.
var buffer: [MAX_PATH_BYTES]u8 = undefined;
const out_path = try os.getFdPath(h_file, &buffer);
 
if (out_path.len > out_buffer.len) {
return error.NameTooLong;
}
 
const result = out_buffer[0..out_path.len];
@memcpy(result, out_path);
return result;
}
 
/// Same as `Dir.realpath` except caller must free the returned memory.
/// See also `Dir.realpath`.
pub fn realpathAlloc(self: Dir, allocator: Allocator, pathname: []const u8) ![]u8 {
// Use of MAX_PATH_BYTES here is valid as the realpath function does not
// have a variant that takes an arbitrary-size buffer.
// TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008
// NULL out parameter (GNU's canonicalize_file_name) to handle overelong
// paths. musl supports passing NULL but restricts the output to PATH_MAX
// anyway.
var buf: [MAX_PATH_BYTES]u8 = undefined;
return allocator.dupe(u8, try self.realpath(pathname, buf[0..]));
}
 
/// Changes the current working directory to the open directory handle.
/// This modifies global state and can have surprising effects in multi-
/// threaded applications. Most applications and especially libraries should
/// not call this function as a general rule, however it can have use cases
/// in, for example, implementing a shell, or child process execution.
/// Not all targets support this. For example, WASI does not have the concept
/// of a current working directory.
pub fn setAsCwd(self: Dir) !void {
if (builtin.os.tag == .wasi) {
@compileError("changing cwd is not currently possible in WASI");
}
if (builtin.os.tag == .windows) {
var dir_path_buffer: [os.windows.PATH_MAX_WIDE]u16 = undefined;
const dir_path = try os.windows.GetFinalPathNameByHandle(self.fd, .{}, &dir_path_buffer);
if (builtin.link_libc) {
return os.chdirW(dir_path);
}
return os.windows.SetCurrentDirectory(dir_path);
}
try os.fchdir(self.fd);
}
 
pub const OpenDirOptions = struct {
/// `true` means the opened directory can be used as the `Dir` parameter
/// for functions which operate based on an open directory handle. When `false`,
/// such operations are Illegal Behavior.
access_sub_paths: bool = true,
 
/// `true` means it won't dereference the symlinks.
no_follow: bool = false,
};
 
/// Opens a directory at the given path. The directory is a system resource that remains
/// open until `close` is called on the result.
///
/// Asserts that the path parameter has no null bytes.
pub fn openDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir {
if (builtin.os.tag == .windows) {
const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.openDirW(sub_path_w.span().ptr, args, false);
} else if (builtin.os.tag == .wasi and !builtin.link_libc) {
return self.openDirWasi(sub_path, args);
} else {
const sub_path_c = try os.toPosixPath(sub_path);
return self.openDirZ(&sub_path_c, args, false);
}
}
 
/// Opens an iterable directory at the given path. The directory is a system resource that remains
/// open until `close` is called on the result.
///
/// Asserts that the path parameter has no null bytes.
pub fn openIterableDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!IterableDir {
if (builtin.os.tag == .windows) {
const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path);
return IterableDir{ .dir = try self.openDirW(sub_path_w.span().ptr, args, true) };
} else if (builtin.os.tag == .wasi and !builtin.link_libc) {
return IterableDir{ .dir = try self.openDirWasi(sub_path, args) };
} else {
const sub_path_c = try os.toPosixPath(sub_path);
return IterableDir{ .dir = try self.openDirZ(&sub_path_c, args, true) };
}
}
 
/// Same as `openDir` except only WASI.
pub fn openDirWasi(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir {
const w = os.wasi;
var base: w.rights_t = w.RIGHT.FD_FILESTAT_GET | w.RIGHT.FD_FDSTAT_SET_FLAGS | w.RIGHT.FD_FILESTAT_SET_TIMES;
if (args.access_sub_paths) {
base |= w.RIGHT.FD_READDIR |
w.RIGHT.PATH_CREATE_DIRECTORY |
w.RIGHT.PATH_CREATE_FILE |
w.RIGHT.PATH_LINK_SOURCE |
w.RIGHT.PATH_LINK_TARGET |
w.RIGHT.PATH_OPEN |
w.RIGHT.PATH_READLINK |
w.RIGHT.PATH_RENAME_SOURCE |
w.RIGHT.PATH_RENAME_TARGET |
w.RIGHT.PATH_FILESTAT_GET |
w.RIGHT.PATH_FILESTAT_SET_SIZE |
w.RIGHT.PATH_FILESTAT_SET_TIMES |
w.RIGHT.PATH_SYMLINK |
w.RIGHT.PATH_REMOVE_DIRECTORY |
w.RIGHT.PATH_UNLINK_FILE;
}
const symlink_flags: w.lookupflags_t = if (args.no_follow) 0x0 else w.LOOKUP_SYMLINK_FOLLOW;
// TODO do we really need all the rights here?
const inheriting: w.rights_t = w.RIGHT.ALL ^ w.RIGHT.SOCK_SHUTDOWN;
 
const result = os.openatWasi(
self.fd,
sub_path,
symlink_flags,
w.O.DIRECTORY,
0x0,
base,
inheriting,
);
const fd = result catch |err| switch (err) {
error.FileTooBig => unreachable, // can't happen for directories
error.IsDir => unreachable, // we're providing O.DIRECTORY
error.NoSpaceLeft => unreachable, // not providing O.CREAT
error.PathAlreadyExists => unreachable, // not providing O.CREAT
error.FileLocksNotSupported => unreachable, // locking folders is not supported
error.WouldBlock => unreachable, // can't happen for directories
error.FileBusy => unreachable, // can't happen for directories
else => |e| return e,
};
return Dir{ .fd = fd };
}
 
/// Same as `openDir` except the parameter is null-terminated.
pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenDirOptions, iterable: bool) OpenError!Dir {
if (builtin.os.tag == .windows) {
const sub_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c);
return self.openDirW(sub_path_w.span().ptr, args, iterable);
}
const symlink_flags: u32 = if (args.no_follow) os.O.NOFOLLOW else 0x0;
if (!iterable) {
const O_PATH = if (@hasDecl(os.O, "PATH")) os.O.PATH else 0;
return self.openDirFlagsZ(sub_path_c, os.O.DIRECTORY | os.O.RDONLY | os.O.CLOEXEC | O_PATH | symlink_flags);
} else {
return self.openDirFlagsZ(sub_path_c, os.O.DIRECTORY | os.O.RDONLY | os.O.CLOEXEC | symlink_flags);
}
}
 
/// Same as `openDir` except the path parameter is WTF-16 encoded, NT-prefixed.
/// This function asserts the target OS is Windows.
pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenDirOptions, iterable: bool) OpenError!Dir {
const w = os.windows;
// TODO remove some of these flags if args.access_sub_paths is false
const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
w.SYNCHRONIZE | w.FILE_TRAVERSE;
const flags: u32 = if (iterable) base_flags | w.FILE_LIST_DIRECTORY else base_flags;
const dir = try self.makeOpenDirAccessMaskW(sub_path_w, flags, .{
.no_follow = args.no_follow,
.create_disposition = w.FILE_OPEN,
});
return dir;
}
 
/// `flags` must contain `os.O.DIRECTORY`.
fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: u32) OpenError!Dir {
const result = if (need_async_thread)
std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, flags, 0)
else
os.openatZ(self.fd, sub_path_c, flags, 0);
const fd = result catch |err| switch (err) {
error.FileTooBig => unreachable, // can't happen for directories
error.IsDir => unreachable, // we're providing O.DIRECTORY
error.NoSpaceLeft => unreachable, // not providing O.CREAT
error.PathAlreadyExists => unreachable, // not providing O.CREAT
error.FileLocksNotSupported => unreachable, // locking folders is not supported
error.WouldBlock => unreachable, // can't happen for directories
error.FileBusy => unreachable, // can't happen for directories
else => |e| return e,
};
return Dir{ .fd = fd };
}
 
const MakeOpenDirAccessMaskWOptions = struct {
no_follow: bool,
create_disposition: u32,
};
 
fn makeOpenDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) OpenError!Dir {
const w = os.windows;
 
var result = Dir{
.fd = undefined,
};
 
const path_len_bytes = @as(u16, @intCast(mem.sliceTo(sub_path_w, 0).len * 2));
var nt_name = w.UNICODE_STRING{
.Length = path_len_bytes,
.MaximumLength = path_len_bytes,
.Buffer = @constCast(sub_path_w),
};
var attr = w.OBJECT_ATTRIBUTES{
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
.RootDirectory = if (path.isAbsoluteWindowsW(sub_path_w)) null else self.fd,
.Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
.ObjectName = &nt_name,
.SecurityDescriptor = null,
.SecurityQualityOfService = null,
};
const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0;
var io: w.IO_STATUS_BLOCK = undefined;
const rc = w.ntdll.NtCreateFile(
&result.fd,
access_mask,
&attr,
&io,
null,
w.FILE_ATTRIBUTE_NORMAL,
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE,
flags.create_disposition,
w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point,
null,
0,
);
 
switch (rc) {
.SUCCESS => return result,
.OBJECT_NAME_INVALID => return error.BadPathName,
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
.NOT_A_DIRECTORY => return error.NotDir,
// This can happen if the directory has 'List folder contents' permission set to 'Deny'
// and the directory is trying to be opened for iteration.
.ACCESS_DENIED => return error.AccessDenied,
.INVALID_PARAMETER => unreachable,
else => return w.unexpectedStatus(rc),
}
}
 
pub const DeleteFileError = os.UnlinkError;
 
/// Delete a file name and possibly the file it refers to, based on an open directory handle.
/// Asserts that the path parameter has no null bytes.
pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void {
if (builtin.os.tag == .windows) {
const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.deleteFileW(sub_path_w.span());
} else if (builtin.os.tag == .wasi and !builtin.link_libc) {
os.unlinkat(self.fd, sub_path, 0) catch |err| switch (err) {
error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR
else => |e| return e,
};
} else {
const sub_path_c = try os.toPosixPath(sub_path);
return self.deleteFileZ(&sub_path_c);
}
}
 
/// Same as `deleteFile` except the parameter is null-terminated.
pub fn deleteFileZ(self: Dir, sub_path_c: [*:0]const u8) DeleteFileError!void {
os.unlinkatZ(self.fd, sub_path_c, 0) catch |err| switch (err) {
error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR
error.AccessDenied => |e| switch (builtin.os.tag) {
// non-Linux POSIX systems return EPERM when trying to delete a directory, so
// we need to handle that case specifically and translate the error
.macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => {
// Don't follow symlinks to match unlinkat (which acts on symlinks rather than follows them)
const fstat = os.fstatatZ(self.fd, sub_path_c, os.AT.SYMLINK_NOFOLLOW) catch return e;
const is_dir = fstat.mode & os.S.IFMT == os.S.IFDIR;
return if (is_dir) error.IsDir else e;
},
else => return e,
},
else => |e| return e,
};
}
 
/// Same as `deleteFile` except the parameter is WTF-16 encoded.
pub fn deleteFileW(self: Dir, sub_path_w: []const u16) DeleteFileError!void {
os.unlinkatW(self.fd, sub_path_w, 0) catch |err| switch (err) {
error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR
else => |e| return e,
};
}
 
pub const DeleteDirError = error{
DirNotEmpty,
FileNotFound,
AccessDenied,
FileBusy,
FileSystem,
SymLinkLoop,
NameTooLong,
NotDir,
SystemResources,
ReadOnlyFileSystem,
InvalidUtf8,
BadPathName,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
Unexpected,
};
 
/// Returns `error.DirNotEmpty` if the directory is not empty.
/// To delete a directory recursively, see `deleteTree`.
/// Asserts that the path parameter has no null bytes.
pub fn deleteDir(self: Dir, sub_path: []const u8) DeleteDirError!void {
if (builtin.os.tag == .windows) {
const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.deleteDirW(sub_path_w.span());
} else if (builtin.os.tag == .wasi and !builtin.link_libc) {
os.unlinkat(self.fd, sub_path, os.AT.REMOVEDIR) catch |err| switch (err) {
error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR
else => |e| return e,
};
} else {
const sub_path_c = try os.toPosixPath(sub_path);
return self.deleteDirZ(&sub_path_c);
}
}
 
/// Same as `deleteDir` except the parameter is null-terminated.
pub fn deleteDirZ(self: Dir, sub_path_c: [*:0]const u8) DeleteDirError!void {
os.unlinkatZ(self.fd, sub_path_c, os.AT.REMOVEDIR) catch |err| switch (err) {
error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR
else => |e| return e,
};
}
 
/// Same as `deleteDir` except the parameter is UTF16LE, NT prefixed.
/// This function is Windows-only.
pub fn deleteDirW(self: Dir, sub_path_w: []const u16) DeleteDirError!void {
os.unlinkatW(self.fd, sub_path_w, os.AT.REMOVEDIR) catch |err| switch (err) {
error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR
else => |e| return e,
};
}
 
pub const RenameError = os.RenameError;
 
/// Change the name or location of a file or directory.
/// If new_sub_path already exists, it will be replaced.
/// Renaming a file over an existing directory or a directory
/// over an existing file will fail with `error.IsDir` or `error.NotDir`
pub fn rename(self: Dir, old_sub_path: []const u8, new_sub_path: []const u8) RenameError!void {
return os.renameat(self.fd, old_sub_path, self.fd, new_sub_path);
}
 
/// Same as `rename` except the parameters are null-terminated.
pub fn renameZ(self: Dir, old_sub_path_z: [*:0]const u8, new_sub_path_z: [*:0]const u8) RenameError!void {
return os.renameatZ(self.fd, old_sub_path_z, self.fd, new_sub_path_z);
}
 
/// Same as `rename` except the parameters are UTF16LE, NT prefixed.
/// This function is Windows-only.
pub fn renameW(self: Dir, old_sub_path_w: []const u16, new_sub_path_w: []const u16) RenameError!void {
return os.renameatW(self.fd, old_sub_path_w, self.fd, new_sub_path_w);
}
 
/// Creates a symbolic link named `sym_link_path` which contains the string `target_path`.
/// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent
/// one; the latter case is known as a dangling link.
/// If `sym_link_path` exists, it will not be overwritten.
pub fn symLink(
self: Dir,
target_path: []const u8,
sym_link_path: []const u8,
flags: SymLinkFlags,
) !void {
if (builtin.os.tag == .wasi and !builtin.link_libc) {
return self.symLinkWasi(target_path, sym_link_path, flags);
}
if (builtin.os.tag == .windows) {
// Target path does not use sliceToPrefixedFileW because certain paths
// are handled differently when creating a symlink than they would be
// when converting to an NT namespaced path. CreateSymbolicLink in
// symLinkW will handle the necessary conversion.
var target_path_w: os.windows.PathSpace = undefined;
target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path);
target_path_w.data[target_path_w.len] = 0;
const sym_link_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sym_link_path);
return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags);
}
const target_path_c = try os.toPosixPath(target_path);
const sym_link_path_c = try os.toPosixPath(sym_link_path);
return self.symLinkZ(&target_path_c, &sym_link_path_c, flags);
}
 
/// WASI-only. Same as `symLink` except targeting WASI.
pub fn symLinkWasi(
self: Dir,
target_path: []const u8,
sym_link_path: []const u8,
_: SymLinkFlags,
) !void {
return os.symlinkat(target_path, self.fd, sym_link_path);
}
 
/// Same as `symLink`, except the pathname parameters are null-terminated.
pub fn symLinkZ(
self: Dir,
target_path_c: [*:0]const u8,
sym_link_path_c: [*:0]const u8,
flags: SymLinkFlags,
) !void {
if (builtin.os.tag == .windows) {
const target_path_w = try os.windows.cStrToPrefixedFileW(self.fd, target_path_c);
const sym_link_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sym_link_path_c);
return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags);
}
return os.symlinkatZ(target_path_c, self.fd, sym_link_path_c);
}
 
/// Windows-only. Same as `symLink` except the pathname parameters
/// are null-terminated, WTF16 encoded.
pub fn symLinkW(
self: Dir,
/// WTF-16, does not need to be NT-prefixed. The NT-prefixing
/// of this path is handled by CreateSymbolicLink.
target_path_w: [:0]const u16,
/// WTF-16, must be NT-prefixed or relative
sym_link_path_w: []const u16,
flags: SymLinkFlags,
) !void {
return os.windows.CreateSymbolicLink(self.fd, sym_link_path_w, target_path_w, flags.is_directory);
}
 
pub const ReadLinkError = os.ReadLinkError;
 
/// Read value of a symbolic link.
/// The return value is a slice of `buffer`, from index `0`.
/// Asserts that the path parameter has no null bytes.
pub fn readLink(self: Dir, sub_path: []const u8, buffer: []u8) ReadLinkError![]u8 {
if (builtin.os.tag == .wasi and !builtin.link_libc) {
return self.readLinkWasi(sub_path, buffer);
}
if (builtin.os.tag == .windows) {
const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.readLinkW(sub_path_w.span(), buffer);
}
const sub_path_c = try os.toPosixPath(sub_path);
return self.readLinkZ(&sub_path_c, buffer);
}
 
/// WASI-only. Same as `readLink` except targeting WASI.
pub fn readLinkWasi(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 {
return os.readlinkat(self.fd, sub_path, buffer);
}
 
/// Same as `readLink`, except the `pathname` parameter is null-terminated.
pub fn readLinkZ(self: Dir, sub_path_c: [*:0]const u8, buffer: []u8) ![]u8 {
if (builtin.os.tag == .windows) {
const sub_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c);
return self.readLinkW(sub_path_w.span(), buffer);
}
return os.readlinkatZ(self.fd, sub_path_c, buffer);
}
 
/// Windows-only. Same as `readLink` except the pathname parameter
/// is null-terminated, WTF16 encoded.
pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 {
return os.windows.ReadLink(self.fd, sub_path_w, buffer);
}
 
/// Read all of file contents using a preallocated buffer.
/// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len`
/// the situation is ambiguous. It could either mean that the entire file was read, and
/// it exactly fits the buffer, or it could mean the buffer was not big enough for the
/// entire file.
pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 {
var file = try self.openFile(file_path, .{});
defer file.close();
 
const end_index = try file.readAll(buffer);
return buffer[0..end_index];
}
 
/// On success, caller owns returned buffer.
/// If the file is larger than `max_bytes`, returns `error.FileTooBig`.
pub fn readFileAlloc(self: Dir, allocator: mem.Allocator, file_path: []const u8, max_bytes: usize) ![]u8 {
return self.readFileAllocOptions(allocator, file_path, max_bytes, null, @alignOf(u8), null);
}
 
/// On success, caller owns returned buffer.
/// If the file is larger than `max_bytes`, returns `error.FileTooBig`.
/// If `size_hint` is specified the initial buffer size is calculated using
/// that value, otherwise the effective file size is used instead.
/// Allows specifying alignment and a sentinel value.
pub fn readFileAllocOptions(
self: Dir,
allocator: mem.Allocator,
file_path: []const u8,
max_bytes: usize,
size_hint: ?usize,
comptime alignment: u29,
comptime optional_sentinel: ?u8,
) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) {
var file = try self.openFile(file_path, .{});
defer file.close();
 
// If the file size doesn't fit a usize it'll be certainly greater than
// `max_bytes`
const stat_size = size_hint orelse math.cast(usize, try file.getEndPos()) orelse
return error.FileTooBig;
 
return file.readToEndAllocOptions(allocator, max_bytes, stat_size, alignment, optional_sentinel);
}
 
pub const DeleteTreeError = error{
InvalidHandle,
AccessDenied,
FileTooBig,
SymLinkLoop,
ProcessFdQuotaExceeded,
NameTooLong,
SystemFdQuotaExceeded,
NoDevice,
SystemResources,
ReadOnlyFileSystem,
FileSystem,
FileBusy,
DeviceBusy,
 
/// One of the path components was not a directory.
/// This error is unreachable if `sub_path` does not contain a path separator.
NotDir,
 
/// On Windows, file paths must be valid Unicode.
InvalidUtf8,
 
/// On Windows, file paths cannot contain these characters:
/// '/', '*', '?', '"', '<', '>', '|'
BadPathName,
 
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
} || os.UnexpectedError;
 
/// Whether `full_path` describes a symlink, file, or directory, this function
/// removes it. If it cannot be removed because it is a non-empty directory,
/// this function recursively removes its entries and then tries again.
/// This operation is not atomic on most file systems.
pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {
var initial_iterable_dir = (try self.deleteTreeOpenInitialSubpath(sub_path, .file)) orelse return;
 
const StackItem = struct {
name: []const u8,
parent_dir: Dir,
iter: IterableDir.Iterator,
 
fn closeAll(items: []@This()) void {
for (items) |*item| item.iter.dir.close();
}
};
 
var stack_buffer: [16]StackItem = undefined;
var stack = std.ArrayListUnmanaged(StackItem).initBuffer(&stack_buffer);
defer StackItem.closeAll(stack.items);
 
stack.appendAssumeCapacity(.{
.name = sub_path,
.parent_dir = self,
.iter = initial_iterable_dir.iterateAssumeFirstIteration(),
});
 
process_stack: while (stack.items.len != 0) {
var top = &stack.items[stack.items.len - 1];
while (try top.iter.next()) |entry| {
var treat_as_dir = entry.kind == .directory;
handle_entry: while (true) {
if (treat_as_dir) {
if (stack.unusedCapacitySlice().len >= 1) {
var iterable_dir = top.iter.dir.openIterableDir(entry.name, .{ .no_follow = true }) catch |err| switch (err) {
error.NotDir => {
treat_as_dir = false;
continue :handle_entry;
},
error.FileNotFound => {
// That's fine, we were trying to remove this directory anyway.
break :handle_entry;
},
 
error.InvalidHandle,
error.AccessDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.InvalidUtf8,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
=> |e| return e,
};
stack.appendAssumeCapacity(.{
.name = entry.name,
.parent_dir = top.iter.dir,
.iter = iterable_dir.iterateAssumeFirstIteration(),
});
continue :process_stack;
} else {
try top.iter.dir.deleteTreeMinStackSizeWithKindHint(entry.name, entry.kind);
break :handle_entry;
}
} else {
if (top.iter.dir.deleteFile(entry.name)) {
break :handle_entry;
} else |err| switch (err) {
error.FileNotFound => break :handle_entry,
 
// Impossible because we do not pass any path separators.
error.NotDir => unreachable,
 
error.IsDir => {
treat_as_dir = true;
continue :handle_entry;
},
 
error.AccessDenied,
error.InvalidUtf8,
error.SymLinkLoop,
error.NameTooLong,
error.SystemResources,
error.ReadOnlyFileSystem,
error.FileSystem,
error.FileBusy,
error.BadPathName,
error.NetworkNotFound,
error.Unexpected,
=> |e| return e,
}
}
}
}
 
// On Windows, we can't delete until the dir's handle has been closed, so
// close it before we try to delete.
top.iter.dir.close();
 
// In order to avoid double-closing the directory when cleaning up
// the stack in the case of an error, we save the relevant portions and
// pop the value from the stack.
const parent_dir = top.parent_dir;
const name = top.name;
stack.items.len -= 1;
 
var need_to_retry: bool = false;
parent_dir.deleteDir(name) catch |err| switch (err) {
error.FileNotFound => {},
error.DirNotEmpty => need_to_retry = true,
else => |e| return e,
};
 
if (need_to_retry) {
// Since we closed the handle that the previous iterator used, we
// need to re-open the dir and re-create the iterator.
var iterable_dir = iterable_dir: {
var treat_as_dir = true;
handle_entry: while (true) {
if (treat_as_dir) {
break :iterable_dir parent_dir.openIterableDir(name, .{ .no_follow = true }) catch |err| switch (err) {
error.NotDir => {
treat_as_dir = false;
continue :handle_entry;
},
error.FileNotFound => {
// That's fine, we were trying to remove this directory anyway.
continue :process_stack;
},
 
error.InvalidHandle,
error.AccessDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.InvalidUtf8,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
=> |e| return e,
};
} else {
if (parent_dir.deleteFile(name)) {
continue :process_stack;
} else |err| switch (err) {
error.FileNotFound => continue :process_stack,
 
// Impossible because we do not pass any path separators.
error.NotDir => unreachable,
 
error.IsDir => {
treat_as_dir = true;
continue :handle_entry;
},
 
error.AccessDenied,
error.InvalidUtf8,
error.SymLinkLoop,
error.NameTooLong,
error.SystemResources,
error.ReadOnlyFileSystem,
error.FileSystem,
error.FileBusy,
error.BadPathName,
error.NetworkNotFound,
error.Unexpected,
=> |e| return e,
}
}
}
};
// We know there is room on the stack since we are just re-adding
// the StackItem that we previously popped.
stack.appendAssumeCapacity(.{
.name = name,
.parent_dir = parent_dir,
.iter = iterable_dir.iterateAssumeFirstIteration(),
});
continue :process_stack;
}
}
}
 
/// Like `deleteTree`, but only keeps one `Iterator` active at a time to minimize the function's stack size.
/// This is slower than `deleteTree` but uses less stack space.
pub fn deleteTreeMinStackSize(self: Dir, sub_path: []const u8) DeleteTreeError!void {
return self.deleteTreeMinStackSizeWithKindHint(sub_path, .file);
}
 
fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint: File.Kind) DeleteTreeError!void {
start_over: while (true) {
var iterable_dir = (try self.deleteTreeOpenInitialSubpath(sub_path, kind_hint)) orelse return;
var cleanup_dir_parent: ?IterableDir = null;
defer if (cleanup_dir_parent) |*d| d.close();
 
var cleanup_dir = true;
defer if (cleanup_dir) iterable_dir.close();
 
// Valid use of MAX_PATH_BYTES because dir_name_buf will only
// ever store a single path component that was returned from the
// filesystem.
var dir_name_buf: [MAX_PATH_BYTES]u8 = undefined;
var dir_name: []const u8 = sub_path;
 
// Here we must avoid recursion, in order to provide O(1) memory guarantee of this function.
// Go through each entry and if it is not a directory, delete it. If it is a directory,
// open it, and close the original directory. Repeat. Then start the entire operation over.
 
scan_dir: while (true) {
var dir_it = iterable_dir.iterateAssumeFirstIteration();
dir_it: while (try dir_it.next()) |entry| {
var treat_as_dir = entry.kind == .directory;
handle_entry: while (true) {
if (treat_as_dir) {
const new_dir = iterable_dir.dir.openIterableDir(entry.name, .{ .no_follow = true }) catch |err| switch (err) {
error.NotDir => {
treat_as_dir = false;
continue :handle_entry;
},
error.FileNotFound => {
// That's fine, we were trying to remove this directory anyway.
continue :dir_it;
},
 
error.InvalidHandle,
error.AccessDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.InvalidUtf8,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
=> |e| return e,
};
if (cleanup_dir_parent) |*d| d.close();
cleanup_dir_parent = iterable_dir;
iterable_dir = new_dir;
const result = dir_name_buf[0..entry.name.len];
@memcpy(result, entry.name);
dir_name = result;
continue :scan_dir;
} else {
if (iterable_dir.dir.deleteFile(entry.name)) {
continue :dir_it;
} else |err| switch (err) {
error.FileNotFound => continue :dir_it,
 
// Impossible because we do not pass any path separators.
error.NotDir => unreachable,
 
error.IsDir => {
treat_as_dir = true;
continue :handle_entry;
},
 
error.AccessDenied,
error.InvalidUtf8,
error.SymLinkLoop,
error.NameTooLong,
error.SystemResources,
error.ReadOnlyFileSystem,
error.FileSystem,
error.FileBusy,
error.BadPathName,
error.NetworkNotFound,
error.Unexpected,
=> |e| return e,
}
}
}
}
// Reached the end of the directory entries, which means we successfully deleted all of them.
// Now to remove the directory itself.
iterable_dir.close();
cleanup_dir = false;
 
if (cleanup_dir_parent) |d| {
d.dir.deleteDir(dir_name) catch |err| switch (err) {
// These two things can happen due to file system race conditions.
error.FileNotFound, error.DirNotEmpty => continue :start_over,
else => |e| return e,
};
continue :start_over;
} else {
self.deleteDir(sub_path) catch |err| switch (err) {
error.FileNotFound => return,
error.DirNotEmpty => continue :start_over,
else => |e| return e,
};
return;
}
}
}
}
 
/// On successful delete, returns null.
fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File.Kind) !?IterableDir {
return iterable_dir: {
// Treat as a file by default
var treat_as_dir = kind_hint == .directory;
 
handle_entry: while (true) {
if (treat_as_dir) {
break :iterable_dir self.openIterableDir(sub_path, .{ .no_follow = true }) catch |err| switch (err) {
error.NotDir => {
treat_as_dir = false;
continue :handle_entry;
},
error.FileNotFound => {
// That's fine, we were trying to remove this directory anyway.
return null;
},
 
error.InvalidHandle,
error.AccessDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.InvalidUtf8,
error.BadPathName,
error.DeviceBusy,
error.NetworkNotFound,
=> |e| return e,
};
} else {
if (self.deleteFile(sub_path)) {
return null;
} else |err| switch (err) {
error.FileNotFound => return null,
 
error.IsDir => {
treat_as_dir = true;
continue :handle_entry;
},
 
error.AccessDenied,
error.InvalidUtf8,
error.SymLinkLoop,
error.NameTooLong,
error.SystemResources,
error.ReadOnlyFileSystem,
error.NotDir,
error.FileSystem,
error.FileBusy,
error.BadPathName,
error.NetworkNotFound,
error.Unexpected,
=> |e| return e,
}
}
}
};
}
 
pub const WriteFileError = File.WriteError || File.OpenError;
 
/// Deprecated: use `writeFile2`.
pub fn writeFile(self: Dir, sub_path: []const u8, data: []const u8) WriteFileError!void {
return writeFile2(self, .{
.sub_path = sub_path,
.data = data,
.flags = .{},
});
}
 
pub const WriteFileOptions = struct {
sub_path: []const u8,
data: []const u8,
flags: File.CreateFlags = .{},
};
 
/// Writes content to the file system, using the file creation flags provided.
pub fn writeFile2(self: Dir, options: WriteFileOptions) WriteFileError!void {
var file = try self.createFile(options.sub_path, options.flags);
defer file.close();
try file.writeAll(options.data);
}
 
pub const AccessError = os.AccessError;
 
/// Test accessing `path`.
/// `path` is UTF-8-encoded.
/// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function.
/// For example, instead of testing if a file exists and then opening it, just
/// open it and handle the error for file not found.
pub fn access(self: Dir, sub_path: []const u8, flags: File.OpenFlags) AccessError!void {
if (builtin.os.tag == .windows) {
const sub_path_w = os.windows.sliceToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) {
error.AccessDenied => return error.PermissionDenied,
else => |e| return e,
};
return self.accessW(sub_path_w.span().ptr, flags);
}
const path_c = try os.toPosixPath(sub_path);
return self.accessZ(&path_c, flags);
}
 
/// Same as `access` except the path parameter is null-terminated.
pub fn accessZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) AccessError!void {
if (builtin.os.tag == .windows) {
const sub_path_w = os.windows.cStrToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) {
error.AccessDenied => return error.PermissionDenied,
else => |e| return e,
};
return self.accessW(sub_path_w.span().ptr, flags);
}
const os_mode = switch (flags.mode) {
.read_only => @as(u32, os.F_OK),
.write_only => @as(u32, os.W_OK),
.read_write => @as(u32, os.R_OK | os.W_OK),
};
const result = if (need_async_thread and flags.intended_io_mode != .blocking)
std.event.Loop.instance.?.faccessatZ(self.fd, sub_path, os_mode, 0)
else
os.faccessatZ(self.fd, sub_path, os_mode, 0);
return result;
}
 
/// Same as `access` except asserts the target OS is Windows and the path parameter is
/// * WTF-16 encoded
/// * null-terminated
/// * NtDll prefixed
/// TODO currently this ignores `flags`.
pub fn accessW(self: Dir, sub_path_w: [*:0]const u16, flags: File.OpenFlags) AccessError!void {
_ = flags;
return os.faccessatW(self.fd, sub_path_w, 0, 0);
}
 
/// Check the file size, mtime, and mode of `source_path` and `dest_path`. If they are equal, does nothing.
/// Otherwise, atomically copies `source_path` to `dest_path`. The destination file gains the mtime,
/// atime, and mode of the source file so that the next call to `updateFile` will not need a copy.
/// Returns the previous status of the file before updating.
/// If any of the directories do not exist for dest_path, they are created.
pub fn updateFile(
source_dir: Dir,
source_path: []const u8,
dest_dir: Dir,
dest_path: []const u8,
options: CopyFileOptions,
) !PrevStatus {
var src_file = try source_dir.openFile(source_path, .{});
defer src_file.close();
 
const src_stat = try src_file.stat();
const actual_mode = options.override_mode orelse src_stat.mode;
check_dest_stat: {
const dest_stat = blk: {
var dest_file = dest_dir.openFile(dest_path, .{}) catch |err| switch (err) {
error.FileNotFound => break :check_dest_stat,
else => |e| return e,
};
defer dest_file.close();
 
break :blk try dest_file.stat();
};
 
if (src_stat.size == dest_stat.size and
src_stat.mtime == dest_stat.mtime and
actual_mode == dest_stat.mode)
{
return PrevStatus.fresh;
}
}
 
if (path.dirname(dest_path)) |dirname| {
try dest_dir.makePath(dirname);
}
 
var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = actual_mode });
defer atomic_file.deinit();
 
try atomic_file.file.writeFileAll(src_file, .{ .in_len = src_stat.size });
try atomic_file.file.updateTimes(src_stat.atime, src_stat.mtime);
try atomic_file.finish();
return PrevStatus.stale;
}
 
pub const CopyFileError = File.OpenError || File.StatError || AtomicFile.InitError || CopyFileRawError || AtomicFile.FinishError;
 
/// Guaranteed to be atomic.
/// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and readily available,
/// there is a possibility of power loss or application termination leaving temporary files present
/// in the same directory as dest_path.
pub fn copyFile(source_dir: Dir, source_path: []const u8, dest_dir: Dir, dest_path: []const u8, options: CopyFileOptions) CopyFileError!void {
var in_file = try source_dir.openFile(source_path, .{});
defer in_file.close();
 
var size: ?u64 = null;
const mode = options.override_mode orelse blk: {
const st = try in_file.stat();
size = st.size;
break :blk st.mode;
};
 
var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = mode });
defer atomic_file.deinit();
 
try copy_file(in_file.handle, atomic_file.file.handle, size);
try atomic_file.finish();
}
 
pub const AtomicFileOptions = struct {
mode: File.Mode = File.default_mode,
};
 
/// Directly access the `.file` field, and then call `AtomicFile.finish`
/// to atomically replace `dest_path` with contents.
/// Always call `AtomicFile.deinit` to clean up, regardless of whether `AtomicFile.finish` succeeded.
/// `dest_path` must remain valid until `AtomicFile.deinit` is called.
pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) !AtomicFile {
if (path.dirname(dest_path)) |dirname| {
const dir = try self.openDir(dirname, .{});
return AtomicFile.init(path.basename(dest_path), options.mode, dir, true);
} else {
return AtomicFile.init(dest_path, options.mode, self, false);
}
}
 
pub const Stat = File.Stat;
pub const StatError = File.StatError;
 
pub fn stat(self: Dir) StatError!Stat {
const file: File = .{
.handle = self.fd,
.capable_io_mode = .blocking,
};
return file.stat();
}
 
pub const StatFileError = File.OpenError || File.StatError || os.FStatAtError;
 
/// Returns metadata for a file inside the directory.
///
/// On Windows, this requires three syscalls. On other operating systems, it
/// only takes one.
///
/// Symlinks are followed.
///
/// `sub_path` may be absolute, in which case `self` is ignored.
pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat {
if (builtin.os.tag == .windows) {
var file = try self.openFile(sub_path, .{});
defer file.close();
return file.stat();
}
if (builtin.os.tag == .wasi and !builtin.link_libc) {
const st = try os.fstatatWasi(self.fd, sub_path, os.wasi.LOOKUP_SYMLINK_FOLLOW);
return Stat.fromSystem(st);
}
const st = try os.fstatat(self.fd, sub_path, 0);
return Stat.fromSystem(st);
}
 
const Permissions = File.Permissions;
pub const SetPermissionsError = File.SetPermissionsError;
 
/// Sets permissions according to the provided `Permissions` struct.
/// This method is *NOT* available on WASI
pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!void {
const file: File = .{
.handle = self.fd,
.capable_io_mode = .blocking,
};
try file.setPermissions(permissions);
}
 
const Metadata = File.Metadata;
pub const MetadataError = File.MetadataError;
 
/// Returns a `Metadata` struct, representing the permissions on the directory
pub fn metadata(self: Dir) MetadataError!Metadata {
const file: File = .{
.handle = self.fd,
.capable_io_mode = .blocking,
};
return try file.metadata();
}
};
 
/// Returns a handle to the current working directory. It is not opened with iteration capability.
/// Closing the returned `Dir` is checked illegal behavior. Iterating over the result is illegal behavior.
/// On POSIX targets, this function is comptime-callable.
@@ -2821,33 +257,12 @@ pub fn openDirAbsolute(absolute_path: []const u8, flags: Dir.OpenDirOptions) Fil
/// Same as `openDirAbsolute` but the path parameter is null-terminated.
pub fn openDirAbsoluteZ(absolute_path_c: [*:0]const u8, flags: Dir.OpenDirOptions) File.OpenError!Dir {
assert(path.isAbsoluteZ(absolute_path_c));
return cwd().openDirZ(absolute_path_c, flags, false);
return cwd().openDirZ(absolute_path_c, flags);
}
/// Same as `openDirAbsolute` but the path parameter is null-terminated.
pub fn openDirAbsoluteW(absolute_path_c: [*:0]const u16, flags: Dir.OpenDirOptions) File.OpenError!Dir {
assert(path.isAbsoluteWindowsW(absolute_path_c));
return cwd().openDirW(absolute_path_c, flags, false);
}
 
/// Opens a directory at the given path. The directory is a system resource that remains
/// open until `close` is called on the result.
/// See `openIterableDirAbsoluteZ` for a function that accepts a null-terminated path.
///
/// Asserts that the path parameter has no null bytes.
pub fn openIterableDirAbsolute(absolute_path: []const u8, flags: Dir.OpenDirOptions) File.OpenError!IterableDir {
assert(path.isAbsolute(absolute_path));
return cwd().openIterableDir(absolute_path, flags);
}
 
/// Same as `openIterableDirAbsolute` but the path parameter is null-terminated.
pub fn openIterableDirAbsoluteZ(absolute_path_c: [*:0]const u8, flags: Dir.OpenDirOptions) File.OpenError!IterableDir {
assert(path.isAbsoluteZ(absolute_path_c));
return IterableDir{ .dir = try cwd().openDirZ(absolute_path_c, flags, true) };
}
/// Same as `openIterableDirAbsolute` but the path parameter is null-terminated.
pub fn openIterableDirAbsoluteW(absolute_path_c: [*:0]const u16, flags: Dir.OpenDirOptions) File.OpenError!IterableDir {
assert(path.isAbsoluteWindowsW(absolute_path_c));
return IterableDir{ .dir = try cwd().openDirW(absolute_path_c, flags, true) };
return cwd().openDirW(absolute_path_c, flags);
}
 
/// Opens a file for reading or writing, without attempting to create a new file, based on an absolute path.
@@ -2976,20 +391,16 @@ pub fn readLinkAbsoluteZ(pathname_c: [*:0]const u8, buffer: *[MAX_PATH_BYTES]u8)
return os.readlinkZ(pathname_c, buffer);
}
 
/// Use with `Dir.symLink` and `symLinkAbsolute` to specify whether the symlink
/// will point to a file or a directory. This value is ignored on all hosts
/// except Windows where creating symlinks to different resource types, requires
/// different flags. By default, `symLinkAbsolute` is assumed to point to a file.
pub const SymLinkFlags = struct {
is_directory: bool = false,
};
 
/// Creates a symbolic link named `sym_link_path` which contains the string `target_path`.
/// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent
/// one; the latter case is known as a dangling link.
/// If `sym_link_path` exists, it will not be overwritten.
/// See also `symLinkAbsoluteZ` and `symLinkAbsoluteW`.
pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags: SymLinkFlags) !void {
pub fn symLinkAbsolute(
target_path: []const u8,
sym_link_path: []const u8,
flags: Dir.SymLinkFlags,
) !void {
assert(path.isAbsolute(target_path));
assert(path.isAbsolute(sym_link_path));
if (builtin.os.tag == .windows) {
@@ -3004,7 +415,11 @@ pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags
/// Note that this function will by default try creating a symbolic link to a file. If you would
/// like to create a symbolic link to a directory, specify this with `SymLinkFlags{ .is_directory = true }`.
/// See also `symLinkAbsolute`, `symLinkAbsoluteZ`.
pub fn symLinkAbsoluteW(target_path_w: []const u16, sym_link_path_w: []const u16, flags: SymLinkFlags) !void {
pub fn symLinkAbsoluteW(
target_path_w: []const u16,
sym_link_path_w: []const u16,
flags: Dir.SymLinkFlags,
) !void {
assert(path.isAbsoluteWindowsWTF16(target_path_w));
assert(path.isAbsoluteWindowsWTF16(sym_link_path_w));
return os.windows.CreateSymbolicLink(null, sym_link_path_w, target_path_w, flags.is_directory);
@@ -3012,7 +427,11 @@ pub fn symLinkAbsoluteW(target_path_w: []const u16, sym_link_path_w: []const u16
 
/// Same as `symLinkAbsolute` except the parameters are null-terminated pointers.
/// See also `symLinkAbsolute`.
pub fn symLinkAbsoluteZ(target_path_c: [*:0]const u8, sym_link_path_c: [*:0]const u8, flags: SymLinkFlags) !void {
pub fn symLinkAbsoluteZ(
target_path_c: [*:0]const u8,
sym_link_path_c: [*:0]const u8,
flags: Dir.SymLinkFlags,
) !void {
assert(path.isAbsoluteZ(target_path_c));
assert(path.isAbsoluteZ(sym_link_path_c));
if (builtin.os.tag == .windows) {
@@ -3209,59 +628,6 @@ pub fn realpathAlloc(allocator: Allocator, pathname: []const u8) ![]u8 {
return allocator.dupe(u8, try os.realpath(pathname, &buf));
}
 
const CopyFileRawError = error{SystemResources} || os.CopyFileRangeError || os.SendFileError;
 
// Transfer all the data between two file descriptors in the most efficient way.
// The copy starts at offset 0, the initial offsets are preserved.
// No metadata is transferred over.
fn copy_file(fd_in: os.fd_t, fd_out: os.fd_t, maybe_size: ?u64) CopyFileRawError!void {
if (comptime builtin.target.isDarwin()) {
const rc = os.system.fcopyfile(fd_in, fd_out, null, os.system.COPYFILE_DATA);
switch (os.errno(rc)) {
.SUCCESS => return,
.INVAL => unreachable,
.NOMEM => return error.SystemResources,
// The source file is not a directory, symbolic link, or regular file.
// Try with the fallback path before giving up.
.OPNOTSUPP => {},
else => |err| return os.unexpectedErrno(err),
}
}
 
if (builtin.os.tag == .linux) {
// Try copy_file_range first as that works at the FS level and is the
// most efficient method (if available).
var offset: u64 = 0;
cfr_loop: while (true) {
// The kernel checks the u64 value `offset+count` for overflow, use
// a 32 bit value so that the syscall won't return EINVAL except for
// impossibly large files (> 2^64-1 - 2^32-1).
const amt = try os.copy_file_range(fd_in, offset, fd_out, offset, math.maxInt(u32), 0);
// Terminate as soon as we have copied size bytes or no bytes
if (maybe_size) |s| {
if (s == amt) break :cfr_loop;
}
if (amt == 0) break :cfr_loop;
offset += amt;
}
return;
}
 
// Sendfile is a zero-copy mechanism iff the OS supports it, otherwise the
// fallback code will copy the contents chunk by chunk.
const empty_iovec = [0]os.iovec_const{};
var offset: u64 = 0;
sendfile_loop: while (true) {
const amt = try os.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0);
// Terminate as soon as we have copied size bytes or no bytes
if (maybe_size) |s| {
if (s == amt) break :sendfile_loop;
}
if (amt == 0) break :sendfile_loop;
offset += amt;
}
}
 
test {
if (builtin.os.tag != .wasi) {
_ = &makeDirAbsolute;
@@ -3269,10 +635,11 @@ test {
_ = &copyFileAbsolute;
_ = &updateFileAbsolute;
}
_ = &Dir.copyFile;
_ = &AtomicFile;
_ = &Dir;
_ = &File;
_ = &path;
_ = @import("fs/test.zig");
_ = @import("fs/path.zig");
_ = @import("fs/file.zig");
_ = @import("fs/get_app_data_dir.zig");
_ = @import("fs/watch.zig");
}
 
filename was Deleted added: 4428, removed: 4469, total 0
@@ -0,0 +1,85 @@
file: File,
// TODO either replace this with rand_buf or use []u16 on Windows
tmp_path_buf: [tmp_path_len:0]u8,
dest_basename: []const u8,
file_open: bool,
file_exists: bool,
close_dir_on_deinit: bool,
dir: Dir,
 
pub const InitError = File.OpenError;
 
pub const random_bytes_len = 12;
const tmp_path_len = fs.base64_encoder.calcSize(random_bytes_len);
 
/// Note that the `Dir.atomicFile` API may be more handy than this lower-level function.
pub fn init(
dest_basename: []const u8,
mode: File.Mode,
dir: Dir,
close_dir_on_deinit: bool,
) InitError!AtomicFile {
var rand_buf: [random_bytes_len]u8 = undefined;
var tmp_path_buf: [tmp_path_len:0]u8 = undefined;
 
while (true) {
std.crypto.random.bytes(rand_buf[0..]);
const tmp_path = fs.base64_encoder.encode(&tmp_path_buf, &rand_buf);
tmp_path_buf[tmp_path.len] = 0;
 
const file = dir.createFile(
tmp_path,
.{ .mode = mode, .exclusive = true },
) catch |err| switch (err) {
error.PathAlreadyExists => continue,
else => |e| return e,
};
 
return AtomicFile{
.file = file,
.tmp_path_buf = tmp_path_buf,
.dest_basename = dest_basename,
.file_open = true,
.file_exists = true,
.close_dir_on_deinit = close_dir_on_deinit,
.dir = dir,
};
}
}
 
/// Always call deinit, even after a successful finish().
pub fn deinit(self: *AtomicFile) void {
if (self.file_open) {
self.file.close();
self.file_open = false;
}
if (self.file_exists) {
self.dir.deleteFile(&self.tmp_path_buf) catch {};
self.file_exists = false;
}
if (self.close_dir_on_deinit) {
self.dir.close();
}
self.* = undefined;
}
 
pub const FinishError = posix.RenameError;
 
pub fn finish(self: *AtomicFile) FinishError!void {
assert(self.file_exists);
if (self.file_open) {
self.file.close();
self.file_open = false;
}
try posix.renameat(self.dir.fd, self.tmp_path_buf[0..], self.dir.fd, self.dest_basename);
self.file_exists = false;
}
 
const AtomicFile = @This();
const std = @import("../std.zig");
const File = std.fs.File;
const Dir = std.fs.Dir;
const fs = std.fs;
const assert = std.debug.assert;
// https://github.com/ziglang/zig/issues/5019
const posix = std.os;
 
filename was Deleted added: 4428, removed: 4469, total 0
@@ -0,0 +1,2534 @@
fd: posix.fd_t,
 
pub const default_mode = 0o755;
 
pub const Entry = struct {
name: []const u8,
kind: Kind,
 
pub const Kind = File.Kind;
};
 
const IteratorError = error{ AccessDenied, SystemResources } || posix.UnexpectedError;
 
pub const Iterator = switch (builtin.os.tag) {
.macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => struct {
dir: Dir,
seek: i64,
buf: [1024]u8, // TODO align(@alignOf(posix.system.dirent)),
index: usize,
end_index: usize,
first_iter: bool,
 
const Self = @This();
 
pub const Error = IteratorError;
 
/// Memory such as file names referenced in this returned entry becomes invalid
/// with subsequent calls to `next`, as well as when this `Dir` is deinitialized.
pub fn next(self: *Self) Error!?Entry {
switch (builtin.os.tag) {
.macos, .ios => return self.nextDarwin(),
.freebsd, .netbsd, .dragonfly, .openbsd => return self.nextBsd(),
.solaris, .illumos => return self.nextSolaris(),
else => @compileError("unimplemented"),
}
}
 
fn nextDarwin(self: *Self) !?Entry {
start_over: while (true) {
if (self.index >= self.end_index) {
if (self.first_iter) {
posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions
self.first_iter = false;
}
const rc = posix.system.__getdirentries64(
self.dir.fd,
&self.buf,
self.buf.len,
&self.seek,
);
if (rc == 0) return null;
if (rc < 0) {
switch (posix.errno(rc)) {
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
else => |err| return posix.unexpectedErrno(err),
}
}
self.index = 0;
self.end_index = @as(usize, @intCast(rc));
}
const darwin_entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index]));
const next_index = self.index + darwin_entry.reclen();
self.index = next_index;
 
const name = @as([*]u8, @ptrCast(&darwin_entry.d_name))[0..darwin_entry.d_namlen];
 
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (darwin_entry.d_ino == 0)) {
continue :start_over;
}
 
const entry_kind: Entry.Kind = switch (darwin_entry.d_type) {
posix.DT.BLK => .block_device,
posix.DT.CHR => .character_device,
posix.DT.DIR => .directory,
posix.DT.FIFO => .named_pipe,
posix.DT.LNK => .sym_link,
posix.DT.REG => .file,
posix.DT.SOCK => .unix_domain_socket,
posix.DT.WHT => .whiteout,
else => .unknown,
};
return Entry{
.name = name,
.kind = entry_kind,
};
}
}
 
fn nextSolaris(self: *Self) !?Entry {
start_over: while (true) {
if (self.index >= self.end_index) {
if (self.first_iter) {
posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions
self.first_iter = false;
}
const rc = posix.system.getdents(self.dir.fd, &self.buf, self.buf.len);
switch (posix.errno(rc)) {
.SUCCESS => {},
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
else => |err| return posix.unexpectedErrno(err),
}
if (rc == 0) return null;
self.index = 0;
self.end_index = @as(usize, @intCast(rc));
}
const entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index]));
const next_index = self.index + entry.reclen();
self.index = next_index;
 
const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&entry.d_name)), 0);
if (mem.eql(u8, name, ".") or mem.eql(u8, name, ".."))
continue :start_over;
 
// Solaris dirent doesn't expose d_type, so we have to call stat to get it.
const stat_info = posix.fstatat(
self.dir.fd,
name,
posix.AT.SYMLINK_NOFOLLOW,
) catch |err| switch (err) {
error.NameTooLong => unreachable,
error.SymLinkLoop => unreachable,
error.FileNotFound => unreachable, // lost the race
else => |e| return e,
};
const entry_kind: Entry.Kind = switch (stat_info.mode & posix.S.IFMT) {
posix.S.IFIFO => .named_pipe,
posix.S.IFCHR => .character_device,
posix.S.IFDIR => .directory,
posix.S.IFBLK => .block_device,
posix.S.IFREG => .file,
posix.S.IFLNK => .sym_link,
posix.S.IFSOCK => .unix_domain_socket,
posix.S.IFDOOR => .door,
posix.S.IFPORT => .event_port,
else => .unknown,
};
return Entry{
.name = name,
.kind = entry_kind,
};
}
}
 
fn nextBsd(self: *Self) !?Entry {
start_over: while (true) {
if (self.index >= self.end_index) {
if (self.first_iter) {
posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions
self.first_iter = false;
}
const rc = if (builtin.os.tag == .netbsd)
posix.system.__getdents30(self.dir.fd, &self.buf, self.buf.len)
else
posix.system.getdents(self.dir.fd, &self.buf, self.buf.len);
switch (posix.errno(rc)) {
.SUCCESS => {},
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
// Introduced in freebsd 13.2: directory unlinked but still open.
// To be consistent, iteration ends if the directory being iterated is deleted during iteration.
.NOENT => return null,
else => |err| return posix.unexpectedErrno(err),
}
if (rc == 0) return null;
self.index = 0;
self.end_index = @as(usize, @intCast(rc));
}
const bsd_entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index]));
const next_index = self.index + bsd_entry.reclen();
self.index = next_index;
 
const name = @as([*]u8, @ptrCast(&bsd_entry.d_name))[0..bsd_entry.d_namlen];
 
const skip_zero_fileno = switch (builtin.os.tag) {
// d_fileno=0 is used to mark invalid entries or deleted files.
.openbsd, .netbsd => true,
else => false,
};
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or
(skip_zero_fileno and bsd_entry.d_fileno == 0))
{
continue :start_over;
}
 
const entry_kind: Entry.Kind = switch (bsd_entry.d_type) {
posix.DT.BLK => .block_device,
posix.DT.CHR => .character_device,
posix.DT.DIR => .directory,
posix.DT.FIFO => .named_pipe,
posix.DT.LNK => .sym_link,
posix.DT.REG => .file,
posix.DT.SOCK => .unix_domain_socket,
posix.DT.WHT => .whiteout,
else => .unknown,
};
return Entry{
.name = name,
.kind = entry_kind,
};
}
}
 
pub fn reset(self: *Self) void {
self.index = 0;
self.end_index = 0;
self.first_iter = true;
}
},
.haiku => struct {
dir: Dir,
buf: [1024]u8, // TODO align(@alignOf(posix.dirent64)),
index: usize,
end_index: usize,
first_iter: bool,
 
const Self = @This();
 
pub const Error = IteratorError;
 
/// Memory such as file names referenced in this returned entry becomes invalid
/// with subsequent calls to `next`, as well as when this `Dir` is deinitialized.
pub fn next(self: *Self) Error!?Entry {
start_over: while (true) {
// TODO: find a better max
const HAIKU_MAX_COUNT = 10000;
if (self.index >= self.end_index) {
if (self.first_iter) {
posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions
self.first_iter = false;
}
const rc = posix.system._kern_read_dir(
self.dir.fd,
&self.buf,
self.buf.len,
HAIKU_MAX_COUNT,
);
if (rc == 0) return null;
if (rc < 0) {
switch (posix.errno(rc)) {
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
else => |err| return posix.unexpectedErrno(err),
}
}
self.index = 0;
self.end_index = @as(usize, @intCast(rc));
}
const haiku_entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index]));
const next_index = self.index + haiku_entry.reclen();
self.index = next_index;
const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&haiku_entry.d_name)), 0);
 
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (haiku_entry.d_ino == 0)) {
continue :start_over;
}
 
var stat_info: posix.Stat = undefined;
const rc = posix.system._kern_read_stat(
self.dir.fd,
&haiku_entry.d_name,
false,
&stat_info,
0,
);
if (rc != 0) {
switch (posix.errno(rc)) {
.SUCCESS => {},
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
else => |err| return posix.unexpectedErrno(err),
}
}
const statmode = stat_info.mode & posix.S.IFMT;
 
const entry_kind: Entry.Kind = switch (statmode) {
posix.S.IFDIR => .directory,
posix.S.IFBLK => .block_device,
posix.S.IFCHR => .character_device,
posix.S.IFLNK => .sym_link,
posix.S.IFREG => .file,
posix.S.IFIFO => .named_pipe,
else => .unknown,
};
 
return Entry{
.name = name,
.kind = entry_kind,
};
}
}
 
pub fn reset(self: *Self) void {
self.index = 0;
self.end_index = 0;
self.first_iter = true;
}
},
.linux => struct {
dir: Dir,
// The if guard is solely there to prevent compile errors from missing `linux.dirent64`
// definition when compiling for other OSes. It doesn't do anything when compiling for Linux.
buf: [1024]u8 align(if (builtin.os.tag != .linux) 1 else @alignOf(linux.dirent64)),
index: usize,
end_index: usize,
first_iter: bool,
 
const Self = @This();
const linux = std.os.linux;
 
pub const Error = IteratorError;
 
/// Memory such as file names referenced in this returned entry becomes invalid
/// with subsequent calls to `next`, as well as when this `Dir` is deinitialized.
pub fn next(self: *Self) Error!?Entry {
return self.nextLinux() catch |err| switch (err) {
// To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration.
// This matches the behavior of non-Linux UNIX platforms.
error.DirNotFound => null,
else => |e| return e,
};
}
 
pub const ErrorLinux = error{DirNotFound} || IteratorError;
 
/// Implementation of `next` that can return `error.DirNotFound` if the directory being
/// iterated was deleted during iteration (this error is Linux specific).
pub fn nextLinux(self: *Self) ErrorLinux!?Entry {
start_over: while (true) {
if (self.index >= self.end_index) {
if (self.first_iter) {
posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions
self.first_iter = false;
}
const rc = linux.getdents64(self.dir.fd, &self.buf, self.buf.len);
switch (linux.getErrno(rc)) {
.SUCCESS => {},
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration.
.INVAL => return error.Unexpected, // Linux may in some cases return EINVAL when reading /proc/$PID/net.
.ACCES => return error.AccessDenied, // Do not have permission to iterate this directory.
else => |err| return posix.unexpectedErrno(err),
}
if (rc == 0) return null;
self.index = 0;
self.end_index = rc;
}
const linux_entry = @as(*align(1) linux.dirent64, @ptrCast(&self.buf[self.index]));
const next_index = self.index + linux_entry.reclen();
self.index = next_index;
 
const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&linux_entry.d_name)), 0);
 
// skip . and .. entries
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) {
continue :start_over;
}
 
const entry_kind: Entry.Kind = switch (linux_entry.d_type) {
linux.DT.BLK => .block_device,
linux.DT.CHR => .character_device,
linux.DT.DIR => .directory,
linux.DT.FIFO => .named_pipe,
linux.DT.LNK => .sym_link,
linux.DT.REG => .file,
linux.DT.SOCK => .unix_domain_socket,
else => .unknown,
};
return Entry{
.name = name,
.kind = entry_kind,
};
}
}
 
pub fn reset(self: *Self) void {
self.index = 0;
self.end_index = 0;
self.first_iter = true;
}
},
.windows => struct {
dir: Dir,
buf: [1024]u8 align(@alignOf(std.os.windows.FILE_BOTH_DIR_INFORMATION)),
index: usize,
end_index: usize,
first_iter: bool,
name_data: [fs.MAX_NAME_BYTES]u8,
 
const Self = @This();
 
pub const Error = IteratorError;
 
/// Memory such as file names referenced in this returned entry becomes invalid
/// with subsequent calls to `next`, as well as when this `Dir` is deinitialized.
pub fn next(self: *Self) Error!?Entry {
while (true) {
const w = std.os.windows;
if (self.index >= self.end_index) {
var io: w.IO_STATUS_BLOCK = undefined;
const rc = w.ntdll.NtQueryDirectoryFile(
self.dir.fd,
null,
null,
null,
&io,
&self.buf,
self.buf.len,
.FileBothDirectoryInformation,
w.FALSE,
null,
if (self.first_iter) @as(w.BOOLEAN, w.TRUE) else @as(w.BOOLEAN, w.FALSE),
);
self.first_iter = false;
if (io.Information == 0) return null;
self.index = 0;
self.end_index = io.Information;
switch (rc) {
.SUCCESS => {},
.ACCESS_DENIED => return error.AccessDenied, // Double-check that the Dir was opened with iteration ability
 
else => return w.unexpectedStatus(rc),
}
}
 
// While the official api docs guarantee FILE_BOTH_DIR_INFORMATION to be aligned properly
// this may not always be the case (e.g. due to faulty VM/Sandboxing tools)
const dir_info: *align(2) w.FILE_BOTH_DIR_INFORMATION = @ptrCast(@alignCast(&self.buf[self.index]));
if (dir_info.NextEntryOffset != 0) {
self.index += dir_info.NextEntryOffset;
} else {
self.index = self.buf.len;
}
 
const name_utf16le = @as([*]u16, @ptrCast(&dir_info.FileName))[0 .. dir_info.FileNameLength / 2];
 
if (mem.eql(u16, name_utf16le, &[_]u16{'.'}) or mem.eql(u16, name_utf16le, &[_]u16{ '.', '.' }))
continue;
// Trust that Windows gives us valid UTF-16LE
const name_utf8_len = std.unicode.utf16leToUtf8(self.name_data[0..], name_utf16le) catch unreachable;
const name_utf8 = self.name_data[0..name_utf8_len];
const kind: Entry.Kind = blk: {
const attrs = dir_info.FileAttributes;
if (attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk .directory;
if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk .sym_link;
break :blk .file;
};
return Entry{
.name = name_utf8,
.kind = kind,
};
}
}
 
pub fn reset(self: *Self) void {
self.index = 0;
self.end_index = 0;
self.first_iter = true;
}
},
.wasi => struct {
dir: Dir,
buf: [1024]u8, // TODO align(@alignOf(posix.wasi.dirent_t)),
cookie: u64,
index: usize,
end_index: usize,
 
const Self = @This();
 
pub const Error = IteratorError;
 
/// Memory such as file names referenced in this returned entry becomes invalid
/// with subsequent calls to `next`, as well as when this `Dir` is deinitialized.
pub fn next(self: *Self) Error!?Entry {
return self.nextWasi() catch |err| switch (err) {
// To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration.
// This matches the behavior of non-Linux UNIX platforms.
error.DirNotFound => null,
else => |e| return e,
};
}
 
pub const ErrorWasi = error{DirNotFound} || IteratorError;
 
/// Implementation of `next` that can return platform-dependent errors depending on the host platform.
/// When the host platform is Linux, `error.DirNotFound` can be returned if the directory being
/// iterated was deleted during iteration.
pub fn nextWasi(self: *Self) ErrorWasi!?Entry {
// We intentinally use fd_readdir even when linked with libc,
// since its implementation is exactly the same as below,
// and we avoid the code complexity here.
const w = std.os.wasi;
start_over: while (true) {
// According to the WASI spec, the last entry might be truncated,
// so we need to check if the left buffer contains the whole dirent.
if (self.end_index - self.index < @sizeOf(w.dirent_t)) {
var bufused: usize = undefined;
switch (w.fd_readdir(self.dir.fd, &self.buf, self.buf.len, self.cookie, &bufused)) {
.SUCCESS => {},
.BADF => unreachable, // Dir is invalid or was opened without iteration ability
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
.NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration.
.NOTCAPABLE => return error.AccessDenied,
else => |err| return posix.unexpectedErrno(err),
}
if (bufused == 0) return null;
self.index = 0;
self.end_index = bufused;
}
const entry = @as(*align(1) w.dirent_t, @ptrCast(&self.buf[self.index]));
const entry_size = @sizeOf(w.dirent_t);
const name_index = self.index + entry_size;
if (name_index + entry.d_namlen > self.end_index) {
// This case, the name is truncated, so we need to call readdir to store the entire name.
self.end_index = self.index; // Force fd_readdir in the next loop.
continue :start_over;
}
const name = self.buf[name_index .. name_index + entry.d_namlen];
 
const next_index = name_index + entry.d_namlen;
self.index = next_index;
self.cookie = entry.d_next;
 
// skip . and .. entries
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) {
continue :start_over;
}
 
const entry_kind: Entry.Kind = switch (entry.d_type) {
.BLOCK_DEVICE => .block_device,
.CHARACTER_DEVICE => .character_device,
.DIRECTORY => .directory,
.SYMBOLIC_LINK => .sym_link,
.REGULAR_FILE => .file,
.SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,
else => .unknown,
};
return Entry{
.name = name,
.kind = entry_kind,
};
}
}
 
pub fn reset(self: *Self) void {
self.index = 0;
self.end_index = 0;
self.cookie = std.os.wasi.DIRCOOKIE_START;
}
},
else => @compileError("unimplemented"),
};
 
pub fn iterate(self: Dir) Iterator {
return self.iterateImpl(true);
}
 
/// Like `iterate`, but will not reset the directory cursor before the first
/// iteration. This should only be used in cases where it is known that the
/// `Dir` has not had its cursor modified yet (e.g. it was just opened).
pub fn iterateAssumeFirstIteration(self: Dir) Iterator {
return self.iterateImpl(false);
}
 
fn iterateImpl(self: Dir, first_iter_start_value: bool) Iterator {
switch (builtin.os.tag) {
.macos,
.ios,
.freebsd,
.netbsd,
.dragonfly,
.openbsd,
.solaris,
.illumos,
=> return Iterator{
.dir = self,
.seek = 0,
.index = 0,
.end_index = 0,
.buf = undefined,
.first_iter = first_iter_start_value,
},
.linux, .haiku => return Iterator{
.dir = self,
.index = 0,
.end_index = 0,
.buf = undefined,
.first_iter = first_iter_start_value,
},
.windows => return Iterator{
.dir = self,
.index = 0,
.end_index = 0,
.first_iter = first_iter_start_value,
.buf = undefined,
.name_data = undefined,
},
.wasi => return Iterator{
.dir = self,
.cookie = std.os.wasi.DIRCOOKIE_START,
.index = 0,
.end_index = 0,
.buf = undefined,
},
else => @compileError("unimplemented"),
}
}
 
pub const Walker = struct {
stack: std.ArrayList(StackItem),
name_buffer: std.ArrayList(u8),
 
pub const WalkerEntry = struct {
/// The containing directory. This can be used to operate directly on `basename`
/// rather than `path`, avoiding `error.NameTooLong` for deeply nested paths.
/// The directory remains open until `next` or `deinit` is called.
dir: Dir,
basename: []const u8,
path: []const u8,
kind: Dir.Entry.Kind,
};
 
const StackItem = struct {
iter: Dir.Iterator,
dirname_len: usize,
};
 
/// After each call to this function, and on deinit(), the memory returned
/// from this function becomes invalid. A copy must be made in order to keep
/// a reference to the path.
pub fn next(self: *Walker) !?WalkerEntry {
while (self.stack.items.len != 0) {
// `top` and `containing` become invalid after appending to `self.stack`
var top = &self.stack.items[self.stack.items.len - 1];
var containing = top;
var dirname_len = top.dirname_len;
if (top.iter.next() catch |err| {
// If we get an error, then we want the user to be able to continue
// walking if they want, which means that we need to pop the directory
// that errored from the stack. Otherwise, all future `next` calls would
// likely just fail with the same error.
var item = self.stack.pop();
if (self.stack.items.len != 0) {
item.iter.dir.close();
}
return err;
}) |base| {
self.name_buffer.shrinkRetainingCapacity(dirname_len);
if (self.name_buffer.items.len != 0) {
try self.name_buffer.append(fs.path.sep);
dirname_len += 1;
}
try self.name_buffer.appendSlice(base.name);
if (base.kind == .directory) {
var new_dir = top.iter.dir.openDir(base.name, .{ .iterate = true }) catch |err| switch (err) {
error.NameTooLong => unreachable, // no path sep in base.name
else => |e| return e,
};
{
errdefer new_dir.close();
try self.stack.append(StackItem{
.iter = new_dir.iterateAssumeFirstIteration(),
.dirname_len = self.name_buffer.items.len,
});
top = &self.stack.items[self.stack.items.len - 1];
containing = &self.stack.items[self.stack.items.len - 2];
}
}
return WalkerEntry{
.dir = containing.iter.dir,
.basename = self.name_buffer.items[dirname_len..],
.path = self.name_buffer.items,
.kind = base.kind,
};
} else {
var item = self.stack.pop();
if (self.stack.items.len != 0) {
item.iter.dir.close();
}
}
}
return null;
}
 
pub fn deinit(self: *Walker) void {
// Close any remaining directories except the initial one (which is always at index 0)
if (self.stack.items.len > 1) {
for (self.stack.items[1..]) |*item| {
item.iter.dir.close();
}
}
self.stack.deinit();
self.name_buffer.deinit();
}
};
 
/// Recursively iterates over a directory.
/// `self` must have been opened with `OpenDirOptions{.iterate = true}`.
/// Must call `Walker.deinit` when done.
/// The order of returned file system entries is undefined.
/// `self` will not be closed after walking it.
pub fn walk(self: Dir, allocator: Allocator) !Walker {
var name_buffer = std.ArrayList(u8).init(allocator);
errdefer name_buffer.deinit();
 
var stack = std.ArrayList(Walker.StackItem).init(allocator);
errdefer stack.deinit();
 
try stack.append(Walker.StackItem{
.iter = self.iterate(),
.dirname_len = 0,
});
 
return Walker{
.stack = stack,
.name_buffer = name_buffer,
};
}
 
pub const OpenError = error{
FileNotFound,
NotDir,
InvalidHandle,
AccessDenied,
SymLinkLoop,
ProcessFdQuotaExceeded,
NameTooLong,
SystemFdQuotaExceeded,
NoDevice,
SystemResources,
InvalidUtf8,
BadPathName,
DeviceBusy,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
} || posix.UnexpectedError;
 
pub fn close(self: *Dir) void {
if (fs.need_async_thread) {
std.event.Loop.instance.?.close(self.fd);
} else {
posix.close(self.fd);
}
self.* = undefined;
}
 
/// Opens a file for reading or writing, without attempting to create a new file.
/// To create a new file, see `createFile`.
/// Call `File.close` to release the resource.
/// Asserts that the path parameter has no null bytes.
pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File {
if (builtin.os.tag == .windows) {
const path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.openFileW(path_w.span(), flags);
}
if (builtin.os.tag == .wasi and !builtin.link_libc) {
return self.openFileWasi(sub_path, flags);
}
const path_c = try posix.toPosixPath(sub_path);
return self.openFileZ(&path_c, flags);
}
 
/// Same as `openFile` but WASI only.
pub fn openFileWasi(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File {
const w = std.os.wasi;
var fdflags: w.fdflags_t = 0x0;
var base: w.rights_t = 0x0;
if (flags.isRead()) {
base |= w.RIGHT.FD_READ | w.RIGHT.FD_TELL | w.RIGHT.FD_SEEK | w.RIGHT.FD_FILESTAT_GET;
}
if (flags.isWrite()) {
fdflags |= w.FDFLAG.APPEND;
base |= w.RIGHT.FD_WRITE |
w.RIGHT.FD_TELL |
w.RIGHT.FD_SEEK |
w.RIGHT.FD_DATASYNC |
w.RIGHT.FD_FDSTAT_SET_FLAGS |
w.RIGHT.FD_SYNC |
w.RIGHT.FD_ALLOCATE |
w.RIGHT.FD_ADVISE |
w.RIGHT.FD_FILESTAT_SET_TIMES |
w.RIGHT.FD_FILESTAT_SET_SIZE;
}
const fd = try posix.openatWasi(self.fd, sub_path, 0x0, 0x0, fdflags, base, 0x0);
return File{ .handle = fd };
}
 
/// Same as `openFile` but the path parameter is null-terminated.
pub fn openFileZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File {
if (builtin.os.tag == .windows) {
const path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path);
return self.openFileW(path_w.span(), flags);
}
 
var os_flags: u32 = 0;
if (@hasDecl(posix.O, "CLOEXEC")) os_flags = posix.O.CLOEXEC;
 
// Use the O locking flags if the os supports them to acquire the lock
// atomically.
const has_flock_open_flags = @hasDecl(posix.O, "EXLOCK");
if (has_flock_open_flags) {
// Note that the O.NONBLOCK flag is removed after the openat() call
// is successful.
const nonblocking_lock_flag: u32 = if (flags.lock_nonblocking)
posix.O.NONBLOCK
else
0;
os_flags |= switch (flags.lock) {
.none => @as(u32, 0),
.shared => posix.O.SHLOCK | nonblocking_lock_flag,
.exclusive => posix.O.EXLOCK | nonblocking_lock_flag,
};
}
if (@hasDecl(posix.O, "LARGEFILE")) {
os_flags |= posix.O.LARGEFILE;
}
if (@hasDecl(posix.O, "NOCTTY") and !flags.allow_ctty) {
os_flags |= posix.O.NOCTTY;
}
os_flags |= switch (flags.mode) {
.read_only => @as(u32, posix.O.RDONLY),
.write_only => @as(u32, posix.O.WRONLY),
.read_write => @as(u32, posix.O.RDWR),
};
const fd = if (flags.intended_io_mode != .blocking)
try std.event.Loop.instance.?.openatZ(self.fd, sub_path, os_flags, 0)
else
try posix.openatZ(self.fd, sub_path, os_flags, 0);
errdefer posix.close(fd);
 
// WASI doesn't have posix.flock so we intetinally check OS prior to the inner if block
// since it is not compiltime-known and we need to avoid undefined symbol in Wasm.
if (@hasDecl(posix.system, "LOCK") and builtin.target.os.tag != .wasi) {
if (!has_flock_open_flags and flags.lock != .none) {
// TODO: integrate async I/O
const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0;
try posix.flock(fd, switch (flags.lock) {
.none => unreachable,
.shared => posix.LOCK.SH | lock_nonblocking,
.exclusive => posix.LOCK.EX | lock_nonblocking,
});
}
}
 
if (has_flock_open_flags and flags.lock_nonblocking) {
var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) {
error.FileBusy => unreachable,
error.Locked => unreachable,
error.PermissionDenied => unreachable,
error.DeadLock => unreachable,
error.LockedRegionLimitExceeded => unreachable,
else => |e| return e,
};
fl_flags &= ~@as(usize, posix.O.NONBLOCK);
_ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) {
error.FileBusy => unreachable,
error.Locked => unreachable,
error.PermissionDenied => unreachable,
error.DeadLock => unreachable,
error.LockedRegionLimitExceeded => unreachable,
else => |e| return e,
};
}
 
return File{
.handle = fd,
.capable_io_mode = .blocking,
.intended_io_mode = flags.intended_io_mode,
};
}
 
/// Same as `openFile` but Windows-only and the path parameter is
/// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded.
pub fn openFileW(self: Dir, sub_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File {
const w = std.os.windows;
const file: File = .{
.handle = try w.OpenFile(sub_path_w, .{
.dir = self.fd,
.access_mask = w.SYNCHRONIZE |
(if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) |
(if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0),
.creation = w.FILE_OPEN,
.io_mode = flags.intended_io_mode,
}),
.capable_io_mode = std.io.default_mode,
.intended_io_mode = flags.intended_io_mode,
};
errdefer file.close();
var io: w.IO_STATUS_BLOCK = undefined;
const range_off: w.LARGE_INTEGER = 0;
const range_len: w.LARGE_INTEGER = 1;
const exclusive = switch (flags.lock) {
.none => return file,
.shared => false,
.exclusive => true,
};
try w.LockFile(
file.handle,
null,
null,
null,
&io,
&range_off,
&range_len,
null,
@intFromBool(flags.lock_nonblocking),
@intFromBool(exclusive),
);
return file;
}
 
/// Creates, opens, or overwrites a file with write access.
/// Call `File.close` on the result when done.
/// Asserts that the path parameter has no null bytes.
pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File {
if (builtin.os.tag == .windows) {
const path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.createFileW(path_w.span(), flags);
}
if (builtin.os.tag == .wasi and !builtin.link_libc) {
return self.createFileWasi(sub_path, flags);
}
const path_c = try posix.toPosixPath(sub_path);
return self.createFileZ(&path_c, flags);
}
 
/// Same as `createFile` but WASI only.
pub fn createFileWasi(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File {
const w = std.os.wasi;
var oflags = w.O.CREAT;
var base: w.rights_t = w.RIGHT.FD_WRITE |
w.RIGHT.FD_DATASYNC |
w.RIGHT.FD_SEEK |
w.RIGHT.FD_TELL |
w.RIGHT.FD_FDSTAT_SET_FLAGS |
w.RIGHT.FD_SYNC |
w.RIGHT.FD_ALLOCATE |
w.RIGHT.FD_ADVISE |
w.RIGHT.FD_FILESTAT_SET_TIMES |
w.RIGHT.FD_FILESTAT_SET_SIZE |
w.RIGHT.FD_FILESTAT_GET;
if (flags.read) {
base |= w.RIGHT.FD_READ;
}
if (flags.truncate) {
oflags |= w.O.TRUNC;
}
if (flags.exclusive) {
oflags |= w.O.EXCL;
}
const fd = try posix.openatWasi(self.fd, sub_path, 0x0, oflags, 0x0, base, 0x0);
return File{ .handle = fd };
}
 
/// Same as `createFile` but the path parameter is null-terminated.
pub fn createFileZ(self: Dir, sub_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File {
if (builtin.os.tag == .windows) {
const path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path_c);
return self.createFileW(path_w.span(), flags);
}
 
// Use the O locking flags if the os supports them to acquire the lock
// atomically.
const has_flock_open_flags = @hasDecl(posix.O, "EXLOCK");
// Note that the O.NONBLOCK flag is removed after the openat() call
// is successful.
const nonblocking_lock_flag: u32 = if (has_flock_open_flags and flags.lock_nonblocking)
posix.O.NONBLOCK
else
0;
const lock_flag: u32 = if (has_flock_open_flags) switch (flags.lock) {
.none => @as(u32, 0),
.shared => posix.O.SHLOCK | nonblocking_lock_flag,
.exclusive => posix.O.EXLOCK | nonblocking_lock_flag,
} else 0;
 
const O_LARGEFILE = if (@hasDecl(posix.O, "LARGEFILE")) posix.O.LARGEFILE else 0;
const os_flags = lock_flag | O_LARGEFILE | posix.O.CREAT | posix.O.CLOEXEC |
(if (flags.truncate) @as(u32, posix.O.TRUNC) else 0) |
(if (flags.read) @as(u32, posix.O.RDWR) else posix.O.WRONLY) |
(if (flags.exclusive) @as(u32, posix.O.EXCL) else 0);
const fd = if (flags.intended_io_mode != .blocking)
try std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, os_flags, flags.mode)
else
try posix.openatZ(self.fd, sub_path_c, os_flags, flags.mode);
errdefer posix.close(fd);
 
// WASI doesn't have posix.flock so we intetinally check OS prior to the inner if block
// since it is not compiltime-known and we need to avoid undefined symbol in Wasm.
if (builtin.target.os.tag != .wasi) {
if (!has_flock_open_flags and flags.lock != .none) {
// TODO: integrate async I/O
const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0;
try posix.flock(fd, switch (flags.lock) {
.none => unreachable,
.shared => posix.LOCK.SH | lock_nonblocking,
.exclusive => posix.LOCK.EX | lock_nonblocking,
});
}
}
 
if (has_flock_open_flags and flags.lock_nonblocking) {
var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) {
error.FileBusy => unreachable,
error.Locked => unreachable,
error.PermissionDenied => unreachable,
error.DeadLock => unreachable,
error.LockedRegionLimitExceeded => unreachable,
else => |e| return e,
};
fl_flags &= ~@as(usize, posix.O.NONBLOCK);
_ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) {
error.FileBusy => unreachable,
error.Locked => unreachable,
error.PermissionDenied => unreachable,
error.DeadLock => unreachable,
error.LockedRegionLimitExceeded => unreachable,
else => |e| return e,
};
}
 
return File{
.handle = fd,
.capable_io_mode = .blocking,
.intended_io_mode = flags.intended_io_mode,
};
}
 
/// Same as `createFile` but Windows-only and the path parameter is
/// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded.
pub fn createFileW(self: Dir, sub_path_w: []const u16, flags: File.CreateFlags) File.OpenError!File {
const w = std.os.windows;
const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0;
const file: File = .{
.handle = try w.OpenFile(sub_path_w, .{
.dir = self.fd,
.access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag,
.creation = if (flags.exclusive)
@as(u32, w.FILE_CREATE)
else if (flags.truncate)
@as(u32, w.FILE_OVERWRITE_IF)
else
@as(u32, w.FILE_OPEN_IF),
.io_mode = flags.intended_io_mode,
}),
.capable_io_mode = std.io.default_mode,
.intended_io_mode = flags.intended_io_mode,
};
errdefer file.close();
var io: w.IO_STATUS_BLOCK = undefined;
const range_off: w.LARGE_INTEGER = 0;
const range_len: w.LARGE_INTEGER = 1;
const exclusive = switch (flags.lock) {
.none => return file,
.shared => false,
.exclusive => true,
};
try w.LockFile(
file.handle,
null,
null,
null,
&io,
&range_off,
&range_len,
null,
@intFromBool(flags.lock_nonblocking),
@intFromBool(exclusive),
);
return file;
}
 
/// Creates a single directory with a relative or absolute path.
/// To create multiple directories to make an entire path, see `makePath`.
/// To operate on only absolute paths, see `makeDirAbsolute`.
pub fn makeDir(self: Dir, sub_path: []const u8) !void {
try posix.mkdirat(self.fd, sub_path, default_mode);
}
 
/// Creates a single directory with a relative or absolute null-terminated UTF-8-encoded path.
/// To create multiple directories to make an entire path, see `makePath`.
/// To operate on only absolute paths, see `makeDirAbsoluteZ`.
pub fn makeDirZ(self: Dir, sub_path: [*:0]const u8) !void {
try posix.mkdiratZ(self.fd, sub_path, default_mode);
}
 
/// Creates a single directory with a relative or absolute null-terminated WTF-16-encoded path.
/// To create multiple directories to make an entire path, see `makePath`.
/// To operate on only absolute paths, see `makeDirAbsoluteW`.
pub fn makeDirW(self: Dir, sub_path: [*:0]const u16) !void {
try posix.mkdiratW(self.fd, sub_path, default_mode);
}
 
/// Calls makeDir iteratively to make an entire path
/// (i.e. creating any parent directories that do not exist).
/// Returns success if the path already exists and is a directory.
/// This function is not atomic, and if it returns an error, the file system may
/// have been modified regardless.
pub fn makePath(self: Dir, sub_path: []const u8) !void {
var it = try fs.path.componentIterator(sub_path);
var component = it.last() orelse return;
while (true) {
self.makeDir(component.path) catch |err| switch (err) {
error.PathAlreadyExists => {
// TODO stat the file and return an error if it's not a directory
// this is important because otherwise a dangling symlink
// could cause an infinite loop
},
error.FileNotFound => |e| {
component = it.previous() orelse return e;
continue;
},
else => |e| return e,
};
component = it.next() orelse return;
}
}
 
/// Calls makeOpenDirAccessMaskW iteratively to make an entire path
/// (i.e. creating any parent directories that do not exist).
/// Opens the dir if the path already exists and is a directory.
/// This function is not atomic, and if it returns an error, the file system may
/// have been modified regardless.
fn makeOpenPathAccessMaskW(self: Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) OpenError!Dir {
const w = std.os.windows;
var it = try fs.path.componentIterator(sub_path);
// If there are no components in the path, then create a dummy component with the full path.
var component = it.last() orelse fs.path.NativeUtf8ComponentIterator.Component{
.name = "",
.path = sub_path,
};
 
while (true) {
const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path);
const is_last = it.peekNext() == null;
var result = self.makeOpenDirAccessMaskW(sub_path_w.span().ptr, access_mask, .{
.no_follow = no_follow,
.create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE,
}) catch |err| switch (err) {
error.FileNotFound => |e| {
component = it.previous() orelse return e;
continue;
},
else => |e| return e,
};
 
component = it.next() orelse return result;
// Don't leak the intermediate file handles
result.close();
}
}
 
/// This function performs `makePath`, followed by `openDir`.
/// If supported by the OS, this operation is atomic. It is not atomic on
/// all operating systems.
/// On Windows, this function performs `makeOpenPathAccessMaskW`.
pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !Dir {
return switch (builtin.os.tag) {
.windows => {
const w = std.os.windows;
const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
w.SYNCHRONIZE | w.FILE_TRAVERSE |
(if (open_dir_options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0));
 
return self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow);
},
else => {
return self.openDir(sub_path, open_dir_options) catch |err| switch (err) {
error.FileNotFound => {
try self.makePath(sub_path);
return self.openDir(sub_path, open_dir_options);
},
else => |e| return e,
};
},
};
}
 
/// This function returns the canonicalized absolute pathname of
/// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this
/// `Dir` handle and returns the canonicalized absolute pathname of `pathname`
/// argument.
/// This function is not universally supported by all platforms.
/// Currently supported hosts are: Linux, macOS, and Windows.
/// See also `Dir.realpathZ`, `Dir.realpathW`, and `Dir.realpathAlloc`.
pub fn realpath(self: Dir, pathname: []const u8, out_buffer: []u8) ![]u8 {
if (builtin.os.tag == .wasi) {
@compileError("realpath is not available on WASI");
}
if (builtin.os.tag == .windows) {
const pathname_w = try std.os.windows.sliceToPrefixedFileW(self.fd, pathname);
return self.realpathW(pathname_w.span(), out_buffer);
}
const pathname_c = try posix.toPosixPath(pathname);
return self.realpathZ(&pathname_c, out_buffer);
}
 
/// Same as `Dir.realpath` except `pathname` is null-terminated.
/// See also `Dir.realpath`, `realpathZ`.
pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) ![]u8 {
if (builtin.os.tag == .windows) {
const pathname_w = try posix.windows.cStrToPrefixedFileW(self.fd, pathname);
return self.realpathW(pathname_w.span(), out_buffer);
}
 
const flags = if (builtin.os.tag == .linux)
posix.O.PATH | posix.O.NONBLOCK | posix.O.CLOEXEC
else
posix.O.NONBLOCK | posix.O.CLOEXEC;
const fd = posix.openatZ(self.fd, pathname, flags, 0) catch |err| switch (err) {
error.FileLocksNotSupported => unreachable,
else => |e| return e,
};
defer posix.close(fd);
 
// Use of MAX_PATH_BYTES here is valid as the realpath function does not
// have a variant that takes an arbitrary-size buffer.
// TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008
// NULL out parameter (GNU's canonicalize_file_name) to handle overelong
// paths. musl supports passing NULL but restricts the output to PATH_MAX
// anyway.
var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;
const out_path = try posix.getFdPath(fd, &buffer);
 
if (out_path.len > out_buffer.len) {
return error.NameTooLong;
}
 
const result = out_buffer[0..out_path.len];
@memcpy(result, out_path);
return result;
}
 
/// Windows-only. Same as `Dir.realpath` except `pathname` is WTF16 encoded.
/// See also `Dir.realpath`, `realpathW`.
pub fn realpathW(self: Dir, pathname: []const u16, out_buffer: []u8) ![]u8 {
const w = std.os.windows;
 
const access_mask = w.GENERIC_READ | w.SYNCHRONIZE;
const share_access = w.FILE_SHARE_READ;
const creation = w.FILE_OPEN;
const h_file = blk: {
const res = w.OpenFile(pathname, .{
.dir = self.fd,
.access_mask = access_mask,
.share_access = share_access,
.creation = creation,
.io_mode = .blocking,
.filter = .any,
}) catch |err| switch (err) {
error.WouldBlock => unreachable,
else => |e| return e,
};
break :blk res;
};
defer w.CloseHandle(h_file);
 
// Use of MAX_PATH_BYTES here is valid as the realpath function does not
// have a variant that takes an arbitrary-size buffer.
// TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008
// NULL out parameter (GNU's canonicalize_file_name) to handle overelong
// paths. musl supports passing NULL but restricts the output to PATH_MAX
// anyway.
var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;
const out_path = try posix.getFdPath(h_file, &buffer);
 
if (out_path.len > out_buffer.len) {
return error.NameTooLong;
}
 
const result = out_buffer[0..out_path.len];
@memcpy(result, out_path);
return result;
}
 
/// Same as `Dir.realpath` except caller must free the returned memory.
/// See also `Dir.realpath`.
pub fn realpathAlloc(self: Dir, allocator: Allocator, pathname: []const u8) ![]u8 {
// Use of MAX_PATH_BYTES here is valid as the realpath function does not
// have a variant that takes an arbitrary-size buffer.
// TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008
// NULL out parameter (GNU's canonicalize_file_name) to handle overelong
// paths. musl supports passing NULL but restricts the output to PATH_MAX
// anyway.
var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
return allocator.dupe(u8, try self.realpath(pathname, buf[0..]));
}
 
/// Changes the current working directory to the open directory handle.
/// This modifies global state and can have surprising effects in multi-
/// threaded applications. Most applications and especially libraries should
/// not call this function as a general rule, however it can have use cases
/// in, for example, implementing a shell, or child process execution.
/// Not all targets support this. For example, WASI does not have the concept
/// of a current working directory.
pub fn setAsCwd(self: Dir) !void {
if (builtin.os.tag == .wasi) {
@compileError("changing cwd is not currently possible in WASI");
}
if (builtin.os.tag == .windows) {
var dir_path_buffer: [std.os.windows.PATH_MAX_WIDE]u16 = undefined;
const dir_path = try std.os.windows.GetFinalPathNameByHandle(self.fd, .{}, &dir_path_buffer);
if (builtin.link_libc) {
return posix.chdirW(dir_path);
}
return std.os.windows.SetCurrentDirectory(dir_path);
}
try posix.fchdir(self.fd);
}
 
pub const OpenDirOptions = struct {
/// `true` means the opened directory can be used as the `Dir` parameter
/// for functions which operate based on an open directory handle. When `false`,
/// such operations are Illegal Behavior.
access_sub_paths: bool = true,
 
/// `true` means the opened directory can be scanned for the files and sub-directories
/// of the result. It means the `iterate` function can be called.
iterate: bool = false,
 
/// `true` means it won't dereference the symlinks.
no_follow: bool = false,
};
 
/// Opens a directory at the given path. The directory is a system resource that remains
/// open until `close` is called on the result.
/// The directory cannot be iterated unless the `iterate` option is set to `true`.
///
/// Asserts that the path parameter has no null bytes.
pub fn openDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir {
if (builtin.os.tag == .windows) {
const sub_path_w = try posix.windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.openDirW(sub_path_w.span().ptr, args);
} else if (builtin.os.tag == .wasi and !builtin.link_libc) {
return self.openDirWasi(sub_path, args);
} else {
const sub_path_c = try posix.toPosixPath(sub_path);
return self.openDirZ(&sub_path_c, args);
}
}
 
/// Same as `openDir` except only WASI.
pub fn openDirWasi(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir {
const w = std.os.wasi;
var base: w.rights_t = w.RIGHT.FD_FILESTAT_GET | w.RIGHT.FD_FDSTAT_SET_FLAGS | w.RIGHT.FD_FILESTAT_SET_TIMES;
if (args.access_sub_paths) {
base |= w.RIGHT.FD_READDIR |
w.RIGHT.PATH_CREATE_DIRECTORY |
w.RIGHT.PATH_CREATE_FILE |
w.RIGHT.PATH_LINK_SOURCE |
w.RIGHT.PATH_LINK_TARGET |
w.RIGHT.PATH_OPEN |
w.RIGHT.PATH_READLINK |
w.RIGHT.PATH_RENAME_SOURCE |
w.RIGHT.PATH_RENAME_TARGET |
w.RIGHT.PATH_FILESTAT_GET |
w.RIGHT.PATH_FILESTAT_SET_SIZE |
w.RIGHT.PATH_FILESTAT_SET_TIMES |
w.RIGHT.PATH_SYMLINK |
w.RIGHT.PATH_REMOVE_DIRECTORY |
w.RIGHT.PATH_UNLINK_FILE;
}
const symlink_flags: w.lookupflags_t = if (args.no_follow) 0x0 else w.LOOKUP_SYMLINK_FOLLOW;
// TODO do we really need all the rights here?
const inheriting: w.rights_t = w.RIGHT.ALL ^ w.RIGHT.SOCK_SHUTDOWN;
 
const result = posix.openatWasi(
self.fd,
sub_path,
symlink_flags,
w.O.DIRECTORY,
0x0,
base,
inheriting,
);
const fd = result catch |err| switch (err) {
error.FileTooBig => unreachable, // can't happen for directories
error.IsDir => unreachable, // we're providing O.DIRECTORY
error.NoSpaceLeft => unreachable, // not providing O.CREAT
error.PathAlreadyExists => unreachable, // not providing O.CREAT
error.FileLocksNotSupported => unreachable, // locking folders is not supported
error.WouldBlock => unreachable, // can't happen for directories
error.FileBusy => unreachable, // can't happen for directories
else => |e| return e,
};
return Dir{ .fd = fd };
}
 
/// Same as `openDir` except the parameter is null-terminated.
pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenDirOptions) OpenError!Dir {
if (builtin.os.tag == .windows) {
const sub_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path_c);
return self.openDirW(sub_path_w.span().ptr, args);
}
const symlink_flags: u32 = if (args.no_follow) posix.O.NOFOLLOW else 0x0;
if (!args.iterate) {
const O_PATH = if (@hasDecl(posix.O, "PATH")) posix.O.PATH else 0;
return self.openDirFlagsZ(sub_path_c, posix.O.DIRECTORY | posix.O.RDONLY | posix.O.CLOEXEC | O_PATH | symlink_flags);
} else {
return self.openDirFlagsZ(sub_path_c, posix.O.DIRECTORY | posix.O.RDONLY | posix.O.CLOEXEC | symlink_flags);
}
}
 
/// Same as `openDir` except the path parameter is WTF-16 encoded, NT-prefixed.
/// This function asserts the target OS is Windows.
pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenDirOptions) OpenError!Dir {
const w = std.os.windows;
// TODO remove some of these flags if args.access_sub_paths is false
const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
w.SYNCHRONIZE | w.FILE_TRAVERSE;
const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags;
const dir = try self.makeOpenDirAccessMaskW(sub_path_w, flags, .{
.no_follow = args.no_follow,
.create_disposition = w.FILE_OPEN,
});
return dir;
}
 
/// `flags` must contain `posix.O.DIRECTORY`.
fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: u32) OpenError!Dir {
const result = if (fs.need_async_thread)
std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, flags, 0)
else
posix.openatZ(self.fd, sub_path_c, flags, 0);
const fd = result catch |err| switch (err) {
error.FileTooBig => unreachable, // can't happen for directories
error.IsDir => unreachable, // we're providing O.DIRECTORY
error.NoSpaceLeft => unreachable, // not providing O.CREAT
error.PathAlreadyExists => unreachable, // not providing O.CREAT
error.FileLocksNotSupported => unreachable, // locking folders is not supported
error.WouldBlock => unreachable, // can't happen for directories
error.FileBusy => unreachable, // can't happen for directories
else => |e| return e,
};
return Dir{ .fd = fd };
}
 
const MakeOpenDirAccessMaskWOptions = struct {
no_follow: bool,
create_disposition: u32,
};
 
fn makeOpenDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) OpenError!Dir {
const w = std.os.windows;
 
var result = Dir{
.fd = undefined,
};
 
const path_len_bytes = @as(u16, @intCast(mem.sliceTo(sub_path_w, 0).len * 2));
var nt_name = w.UNICODE_STRING{
.Length = path_len_bytes,
.MaximumLength = path_len_bytes,
.Buffer = @constCast(sub_path_w),
};
var attr = w.OBJECT_ATTRIBUTES{
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
.RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w)) null else self.fd,
.Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
.ObjectName = &nt_name,
.SecurityDescriptor = null,
.SecurityQualityOfService = null,
};
const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0;
var io: w.IO_STATUS_BLOCK = undefined;
const rc = w.ntdll.NtCreateFile(
&result.fd,
access_mask,
&attr,
&io,
null,
w.FILE_ATTRIBUTE_NORMAL,
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE,
flags.create_disposition,
w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point,
null,
0,
);
 
switch (rc) {
.SUCCESS => return result,
.OBJECT_NAME_INVALID => return error.BadPathName,
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
.NOT_A_DIRECTORY => return error.NotDir,
// This can happen if the directory has 'List folder contents' permission set to 'Deny'
// and the directory is trying to be opened for iteration.
.ACCESS_DENIED => return error.AccessDenied,
.INVALID_PARAMETER => unreachable,
else => return w.unexpectedStatus(rc),
}
}
 
pub const DeleteFileError = posix.UnlinkError;
 
/// Delete a file name and possibly the file it refers to, based on an open directory handle.
/// Asserts that the path parameter has no null bytes.
pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void {
if (builtin.os.tag == .windows) {
const sub_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.deleteFileW(sub_path_w.span());
} else if (builtin.os.tag == .wasi and !builtin.link_libc) {
posix.unlinkat(self.fd, sub_path, 0) catch |err| switch (err) {
error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR
else => |e| return e,
};
} else {
const sub_path_c = try posix.toPosixPath(sub_path);
return self.deleteFileZ(&sub_path_c);
}
}
 
/// Same as `deleteFile` except the parameter is null-terminated.
pub fn deleteFileZ(self: Dir, sub_path_c: [*:0]const u8) DeleteFileError!void {
posix.unlinkatZ(self.fd, sub_path_c, 0) catch |err| switch (err) {
error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR
error.AccessDenied => |e| switch (builtin.os.tag) {
// non-Linux POSIX systems return EPERM when trying to delete a directory, so
// we need to handle that case specifically and translate the error
.macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => {
// Don't follow symlinks to match unlinkat (which acts on symlinks rather than follows them)
const fstat = posix.fstatatZ(self.fd, sub_path_c, posix.AT.SYMLINK_NOFOLLOW) catch return e;
const is_dir = fstat.mode & posix.S.IFMT == posix.S.IFDIR;
return if (is_dir) error.IsDir else e;
},
else => return e,
},
else => |e| return e,
};
}
 
/// Same as `deleteFile` except the parameter is WTF-16 encoded.
pub fn deleteFileW(self: Dir, sub_path_w: []const u16) DeleteFileError!void {
posix.unlinkatW(self.fd, sub_path_w, 0) catch |err| switch (err) {
error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR
else => |e| return e,
};
}
 
pub const DeleteDirError = error{
DirNotEmpty,
FileNotFound,
AccessDenied,
FileBusy,
FileSystem,
SymLinkLoop,
NameTooLong,
NotDir,
SystemResources,
ReadOnlyFileSystem,
InvalidUtf8,
BadPathName,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
Unexpected,
};
 
/// Returns `error.DirNotEmpty` if the directory is not empty.
/// To delete a directory recursively, see `deleteTree`.
/// Asserts that the path parameter has no null bytes.
pub fn deleteDir(self: Dir, sub_path: []const u8) DeleteDirError!void {
if (builtin.os.tag == .windows) {
const sub_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.deleteDirW(sub_path_w.span());
} else if (builtin.os.tag == .wasi and !builtin.link_libc) {
posix.unlinkat(self.fd, sub_path, posix.AT.REMOVEDIR) catch |err| switch (err) {
error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR
else => |e| return e,
};
} else {
const sub_path_c = try posix.toPosixPath(sub_path);
return self.deleteDirZ(&sub_path_c);
}
}
 
/// Same as `deleteDir` except the parameter is null-terminated.
pub fn deleteDirZ(self: Dir, sub_path_c: [*:0]const u8) DeleteDirError!void {
posix.unlinkatZ(self.fd, sub_path_c, posix.AT.REMOVEDIR) catch |err| switch (err) {
error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR
else => |e| return e,
};
}
 
/// Same as `deleteDir` except the parameter is UTF16LE, NT prefixed.
/// This function is Windows-only.
pub fn deleteDirW(self: Dir, sub_path_w: []const u16) DeleteDirError!void {
posix.unlinkatW(self.fd, sub_path_w, posix.AT.REMOVEDIR) catch |err| switch (err) {
error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR
else => |e| return e,
};
}
 
pub const RenameError = posix.RenameError;
 
/// Change the name or location of a file or directory.
/// If new_sub_path already exists, it will be replaced.
/// Renaming a file over an existing directory or a directory
/// over an existing file will fail with `error.IsDir` or `error.NotDir`
pub fn rename(self: Dir, old_sub_path: []const u8, new_sub_path: []const u8) RenameError!void {
return posix.renameat(self.fd, old_sub_path, self.fd, new_sub_path);
}
 
/// Same as `rename` except the parameters are null-terminated.
pub fn renameZ(self: Dir, old_sub_path_z: [*:0]const u8, new_sub_path_z: [*:0]const u8) RenameError!void {
return posix.renameatZ(self.fd, old_sub_path_z, self.fd, new_sub_path_z);
}
 
/// Same as `rename` except the parameters are UTF16LE, NT prefixed.
/// This function is Windows-only.
pub fn renameW(self: Dir, old_sub_path_w: []const u16, new_sub_path_w: []const u16) RenameError!void {
return posix.renameatW(self.fd, old_sub_path_w, self.fd, new_sub_path_w);
}
 
/// Use with `Dir.symLink` and `symLinkAbsolute` to specify whether the symlink
/// will point to a file or a directory. This value is ignored on all hosts
/// except Windows where creating symlinks to different resource types, requires
/// different flags. By default, `symLinkAbsolute` is assumed to point to a file.
pub const SymLinkFlags = struct {
is_directory: bool = false,
};
 
/// Creates a symbolic link named `sym_link_path` which contains the string `target_path`.
/// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent
/// one; the latter case is known as a dangling link.
/// If `sym_link_path` exists, it will not be overwritten.
pub fn symLink(
self: Dir,
target_path: []const u8,
sym_link_path: []const u8,
flags: SymLinkFlags,
) !void {
if (builtin.os.tag == .wasi and !builtin.link_libc) {
return self.symLinkWasi(target_path, sym_link_path, flags);
}
if (builtin.os.tag == .windows) {
// Target path does not use sliceToPrefixedFileW because certain paths
// are handled differently when creating a symlink than they would be
// when converting to an NT namespaced path. CreateSymbolicLink in
// symLinkW will handle the necessary conversion.
var target_path_w: std.os.windows.PathSpace = undefined;
target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path);
target_path_w.data[target_path_w.len] = 0;
const sym_link_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sym_link_path);
return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags);
}
const target_path_c = try posix.toPosixPath(target_path);
const sym_link_path_c = try posix.toPosixPath(sym_link_path);
return self.symLinkZ(&target_path_c, &sym_link_path_c, flags);
}
 
/// WASI-only. Same as `symLink` except targeting WASI.
pub fn symLinkWasi(
self: Dir,
target_path: []const u8,
sym_link_path: []const u8,
_: SymLinkFlags,
) !void {
return posix.symlinkat(target_path, self.fd, sym_link_path);
}
 
/// Same as `symLink`, except the pathname parameters are null-terminated.
pub fn symLinkZ(
self: Dir,
target_path_c: [*:0]const u8,
sym_link_path_c: [*:0]const u8,
flags: SymLinkFlags,
) !void {
if (builtin.os.tag == .windows) {
const target_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, target_path_c);
const sym_link_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sym_link_path_c);
return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags);
}
return posix.symlinkatZ(target_path_c, self.fd, sym_link_path_c);
}
 
/// Windows-only. Same as `symLink` except the pathname parameters
/// are null-terminated, WTF16 encoded.
pub fn symLinkW(
self: Dir,
/// WTF-16, does not need to be NT-prefixed. The NT-prefixing
/// of this path is handled by CreateSymbolicLink.
target_path_w: [:0]const u16,
/// WTF-16, must be NT-prefixed or relative
sym_link_path_w: []const u16,
flags: SymLinkFlags,
) !void {
return std.os.windows.CreateSymbolicLink(self.fd, sym_link_path_w, target_path_w, flags.is_directory);
}
 
pub const ReadLinkError = posix.ReadLinkError;
 
/// Read value of a symbolic link.
/// The return value is a slice of `buffer`, from index `0`.
/// Asserts that the path parameter has no null bytes.
pub fn readLink(self: Dir, sub_path: []const u8, buffer: []u8) ReadLinkError![]u8 {
if (builtin.os.tag == .wasi and !builtin.link_libc) {
return self.readLinkWasi(sub_path, buffer);
}
if (builtin.os.tag == .windows) {
const sub_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.readLinkW(sub_path_w.span(), buffer);
}
const sub_path_c = try posix.toPosixPath(sub_path);
return self.readLinkZ(&sub_path_c, buffer);
}
 
/// WASI-only. Same as `readLink` except targeting WASI.
pub fn readLinkWasi(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 {
return posix.readlinkat(self.fd, sub_path, buffer);
}
 
/// Same as `readLink`, except the `pathname` parameter is null-terminated.
pub fn readLinkZ(self: Dir, sub_path_c: [*:0]const u8, buffer: []u8) ![]u8 {
if (builtin.os.tag == .windows) {
const sub_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path_c);
return self.readLinkW(sub_path_w.span(), buffer);
}
return posix.readlinkatZ(self.fd, sub_path_c, buffer);
}
 
/// Windows-only. Same as `readLink` except the pathname parameter
/// is null-terminated, WTF16 encoded.
pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 {
return std.os.windows.ReadLink(self.fd, sub_path_w, buffer);
}
 
/// Read all of file contents using a preallocated buffer.
/// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len`
/// the situation is ambiguous. It could either mean that the entire file was read, and
/// it exactly fits the buffer, or it could mean the buffer was not big enough for the
/// entire file.
pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 {
var file = try self.openFile(file_path, .{});
defer file.close();
 
const end_index = try file.readAll(buffer);
return buffer[0..end_index];
}
 
/// On success, caller owns returned buffer.
/// If the file is larger than `max_bytes`, returns `error.FileTooBig`.
pub fn readFileAlloc(self: Dir, allocator: mem.Allocator, file_path: []const u8, max_bytes: usize) ![]u8 {
return self.readFileAllocOptions(allocator, file_path, max_bytes, null, @alignOf(u8), null);
}
 
/// On success, caller owns returned buffer.
/// If the file is larger than `max_bytes`, returns `error.FileTooBig`.
/// If `size_hint` is specified the initial buffer size is calculated using
/// that value, otherwise the effective file size is used instead.
/// Allows specifying alignment and a sentinel value.
pub fn readFileAllocOptions(
self: Dir,
allocator: mem.Allocator,
file_path: []const u8,
max_bytes: usize,
size_hint: ?usize,
comptime alignment: u29,
comptime optional_sentinel: ?u8,
) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) {
var file = try self.openFile(file_path, .{});
defer file.close();
 
// If the file size doesn't fit a usize it'll be certainly greater than
// `max_bytes`
const stat_size = size_hint orelse std.math.cast(usize, try file.getEndPos()) orelse
return error.FileTooBig;
 
return file.readToEndAllocOptions(allocator, max_bytes, stat_size, alignment, optional_sentinel);
}
 
pub const DeleteTreeError = error{
InvalidHandle,
AccessDenied,
FileTooBig,
SymLinkLoop,
ProcessFdQuotaExceeded,
NameTooLong,
SystemFdQuotaExceeded,
NoDevice,
SystemResources,
ReadOnlyFileSystem,
FileSystem,
FileBusy,
DeviceBusy,
 
/// One of the path components was not a directory.
/// This error is unreachable if `sub_path` does not contain a path separator.
NotDir,
 
/// On Windows, file paths must be valid Unicode.
InvalidUtf8,
 
/// On Windows, file paths cannot contain these characters:
/// '/', '*', '?', '"', '<', '>', '|'
BadPathName,
 
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
} || posix.UnexpectedError;
 
/// Whether `full_path` describes a symlink, file, or directory, this function
/// removes it. If it cannot be removed because it is a non-empty directory,
/// this function recursively removes its entries and then tries again.
/// This operation is not atomic on most file systems.
pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {
var initial_iterable_dir = (try self.deleteTreeOpenInitialSubpath(sub_path, .file)) orelse return;
 
const StackItem = struct {
name: []const u8,
parent_dir: Dir,
iter: Dir.Iterator,
 
fn closeAll(items: []@This()) void {
for (items) |*item| item.iter.dir.close();
}
};
 
var stack_buffer: [16]StackItem = undefined;
var stack = std.ArrayListUnmanaged(StackItem).initBuffer(&stack_buffer);
defer StackItem.closeAll(stack.items);
 
stack.appendAssumeCapacity(.{
.name = sub_path,
.parent_dir = self,
.iter = initial_iterable_dir.iterateAssumeFirstIteration(),
});
 
process_stack: while (stack.items.len != 0) {
var top = &stack.items[stack.items.len - 1];
while (try top.iter.next()) |entry| {
var treat_as_dir = entry.kind == .directory;
handle_entry: while (true) {
if (treat_as_dir) {
if (stack.unusedCapacitySlice().len >= 1) {
var iterable_dir = top.iter.dir.openDir(entry.name, .{
.no_follow = true,
.iterate = true,
}) catch |err| switch (err) {
error.NotDir => {
treat_as_dir = false;
continue :handle_entry;
},
error.FileNotFound => {
// That's fine, we were trying to remove this directory anyway.
break :handle_entry;
},
 
error.InvalidHandle,
error.AccessDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.InvalidUtf8,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
=> |e| return e,
};
stack.appendAssumeCapacity(.{
.name = entry.name,
.parent_dir = top.iter.dir,
.iter = iterable_dir.iterateAssumeFirstIteration(),
});
continue :process_stack;
} else {
try top.iter.dir.deleteTreeMinStackSizeWithKindHint(entry.name, entry.kind);
break :handle_entry;
}
} else {
if (top.iter.dir.deleteFile(entry.name)) {
break :handle_entry;
} else |err| switch (err) {
error.FileNotFound => break :handle_entry,
 
// Impossible because we do not pass any path separators.
error.NotDir => unreachable,
 
error.IsDir => {
treat_as_dir = true;
continue :handle_entry;
},
 
error.AccessDenied,
error.InvalidUtf8,
error.SymLinkLoop,
error.NameTooLong,
error.SystemResources,
error.ReadOnlyFileSystem,
error.FileSystem,
error.FileBusy,
error.BadPathName,
error.NetworkNotFound,
error.Unexpected,
=> |e| return e,
}
}
}
}
 
// On Windows, we can't delete until the dir's handle has been closed, so
// close it before we try to delete.
top.iter.dir.close();
 
// In order to avoid double-closing the directory when cleaning up
// the stack in the case of an error, we save the relevant portions and
// pop the value from the stack.
const parent_dir = top.parent_dir;
const name = top.name;
stack.items.len -= 1;
 
var need_to_retry: bool = false;
parent_dir.deleteDir(name) catch |err| switch (err) {
error.FileNotFound => {},
error.DirNotEmpty => need_to_retry = true,
else => |e| return e,
};
 
if (need_to_retry) {
// Since we closed the handle that the previous iterator used, we
// need to re-open the dir and re-create the iterator.
var iterable_dir = iterable_dir: {
var treat_as_dir = true;
handle_entry: while (true) {
if (treat_as_dir) {
break :iterable_dir parent_dir.openDir(name, .{
.no_follow = true,
.iterate = true,
}) catch |err| switch (err) {
error.NotDir => {
treat_as_dir = false;
continue :handle_entry;
},
error.FileNotFound => {
// That's fine, we were trying to remove this directory anyway.
continue :process_stack;
},
 
error.InvalidHandle,
error.AccessDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.InvalidUtf8,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
=> |e| return e,
};
} else {
if (parent_dir.deleteFile(name)) {
continue :process_stack;
} else |err| switch (err) {
error.FileNotFound => continue :process_stack,
 
// Impossible because we do not pass any path separators.
error.NotDir => unreachable,
 
error.IsDir => {
treat_as_dir = true;
continue :handle_entry;
},
 
error.AccessDenied,
error.InvalidUtf8,
error.SymLinkLoop,
error.NameTooLong,
error.SystemResources,
error.ReadOnlyFileSystem,
error.FileSystem,
error.FileBusy,
error.BadPathName,
error.NetworkNotFound,
error.Unexpected,
=> |e| return e,
}
}
}
};
// We know there is room on the stack since we are just re-adding
// the StackItem that we previously popped.
stack.appendAssumeCapacity(.{
.name = name,
.parent_dir = parent_dir,
.iter = iterable_dir.iterateAssumeFirstIteration(),
});
continue :process_stack;
}
}
}
 
/// Like `deleteTree`, but only keeps one `Iterator` active at a time to minimize the function's stack size.
/// This is slower than `deleteTree` but uses less stack space.
pub fn deleteTreeMinStackSize(self: Dir, sub_path: []const u8) DeleteTreeError!void {
return self.deleteTreeMinStackSizeWithKindHint(sub_path, .file);
}
 
fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint: File.Kind) DeleteTreeError!void {
start_over: while (true) {
var dir = (try self.deleteTreeOpenInitialSubpath(sub_path, kind_hint)) orelse return;
var cleanup_dir_parent: ?Dir = null;
defer if (cleanup_dir_parent) |*d| d.close();
 
var cleanup_dir = true;
defer if (cleanup_dir) dir.close();
 
// Valid use of MAX_PATH_BYTES because dir_name_buf will only
// ever store a single path component that was returned from the
// filesystem.
var dir_name_buf: [fs.MAX_PATH_BYTES]u8 = undefined;
var dir_name: []const u8 = sub_path;
 
// Here we must avoid recursion, in order to provide O(1) memory guarantee of this function.
// Go through each entry and if it is not a directory, delete it. If it is a directory,
// open it, and close the original directory. Repeat. Then start the entire operation over.
 
scan_dir: while (true) {
var dir_it = dir.iterateAssumeFirstIteration();
dir_it: while (try dir_it.next()) |entry| {
var treat_as_dir = entry.kind == .directory;
handle_entry: while (true) {
if (treat_as_dir) {
const new_dir = dir.openDir(entry.name, .{
.no_follow = true,
.iterate = true,
}) catch |err| switch (err) {
error.NotDir => {
treat_as_dir = false;
continue :handle_entry;
},
error.FileNotFound => {
// That's fine, we were trying to remove this directory anyway.
continue :dir_it;
},
 
error.InvalidHandle,
error.AccessDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.InvalidUtf8,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
=> |e| return e,
};
if (cleanup_dir_parent) |*d| d.close();
cleanup_dir_parent = dir;
dir = new_dir;
const result = dir_name_buf[0..entry.name.len];
@memcpy(result, entry.name);
dir_name = result;
continue :scan_dir;
} else {
if (dir.deleteFile(entry.name)) {
continue :dir_it;
} else |err| switch (err) {
error.FileNotFound => continue :dir_it,
 
// Impossible because we do not pass any path separators.
error.NotDir => unreachable,
 
error.IsDir => {
treat_as_dir = true;
continue :handle_entry;
},
 
error.AccessDenied,
error.InvalidUtf8,
error.SymLinkLoop,
error.NameTooLong,
error.SystemResources,
error.ReadOnlyFileSystem,
error.FileSystem,
error.FileBusy,
error.BadPathName,
error.NetworkNotFound,
error.Unexpected,
=> |e| return e,
}
}
}
}
// Reached the end of the directory entries, which means we successfully deleted all of them.
// Now to remove the directory itself.
dir.close();
cleanup_dir = false;
 
if (cleanup_dir_parent) |d| {
d.deleteDir(dir_name) catch |err| switch (err) {
// These two things can happen due to file system race conditions.
error.FileNotFound, error.DirNotEmpty => continue :start_over,
else => |e| return e,
};
continue :start_over;
} else {
self.deleteDir(sub_path) catch |err| switch (err) {
error.FileNotFound => return,
error.DirNotEmpty => continue :start_over,
else => |e| return e,
};
return;
}
}
}
}
 
/// On successful delete, returns null.
fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File.Kind) !?Dir {
return iterable_dir: {
// Treat as a file by default
var treat_as_dir = kind_hint == .directory;
 
handle_entry: while (true) {
if (treat_as_dir) {
break :iterable_dir self.openDir(sub_path, .{
.no_follow = true,
.iterate = true,
}) catch |err| switch (err) {
error.NotDir => {
treat_as_dir = false;
continue :handle_entry;
},
error.FileNotFound => {
// That's fine, we were trying to remove this directory anyway.
return null;
},
 
error.InvalidHandle,
error.AccessDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.InvalidUtf8,
error.BadPathName,
error.DeviceBusy,
error.NetworkNotFound,
=> |e| return e,
};
} else {
if (self.deleteFile(sub_path)) {
return null;
} else |err| switch (err) {
error.FileNotFound => return null,
 
error.IsDir => {
treat_as_dir = true;
continue :handle_entry;
},
 
error.AccessDenied,
error.InvalidUtf8,
error.SymLinkLoop,
error.NameTooLong,
error.SystemResources,
error.ReadOnlyFileSystem,
error.NotDir,
error.FileSystem,
error.FileBusy,
error.BadPathName,
error.NetworkNotFound,
error.Unexpected,
=> |e| return e,
}
}
}
};
}
 
pub const WriteFileError = File.WriteError || File.OpenError;
 
/// Deprecated: use `writeFile2`.
pub fn writeFile(self: Dir, sub_path: []const u8, data: []const u8) WriteFileError!void {
return writeFile2(self, .{
.sub_path = sub_path,
.data = data,
.flags = .{},
});
}
 
pub const WriteFileOptions = struct {
sub_path: []const u8,
data: []const u8,
flags: File.CreateFlags = .{},
};
 
/// Writes content to the file system, using the file creation flags provided.
pub fn writeFile2(self: Dir, options: WriteFileOptions) WriteFileError!void {
var file = try self.createFile(options.sub_path, options.flags);
defer file.close();
try file.writeAll(options.data);
}
 
pub const AccessError = posix.AccessError;
 
/// Test accessing `path`.
/// `path` is UTF-8-encoded.
/// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function.
/// For example, instead of testing if a file exists and then opening it, just
/// open it and handle the error for file not found.
pub fn access(self: Dir, sub_path: []const u8, flags: File.OpenFlags) AccessError!void {
if (builtin.os.tag == .windows) {
const sub_path_w = std.os.windows.sliceToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) {
error.AccessDenied => return error.PermissionDenied,
else => |e| return e,
};
return self.accessW(sub_path_w.span().ptr, flags);
}
const path_c = try posix.toPosixPath(sub_path);
return self.accessZ(&path_c, flags);
}
 
/// Same as `access` except the path parameter is null-terminated.
pub fn accessZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) AccessError!void {
if (builtin.os.tag == .windows) {
const sub_path_w = std.os.windows.cStrToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) {
error.AccessDenied => return error.PermissionDenied,
else => |e| return e,
};
return self.accessW(sub_path_w.span().ptr, flags);
}
const os_mode = switch (flags.mode) {
.read_only => @as(u32, posix.F_OK),
.write_only => @as(u32, posix.W_OK),
.read_write => @as(u32, posix.R_OK | posix.W_OK),
};
const result = if (fs.need_async_thread and flags.intended_io_mode != .blocking)
std.event.Loop.instance.?.faccessatZ(self.fd, sub_path, os_mode, 0)
else
posix.faccessatZ(self.fd, sub_path, os_mode, 0);
return result;
}
 
/// Same as `access` except asserts the target OS is Windows and the path parameter is
/// * WTF-16 encoded
/// * null-terminated
/// * NtDll prefixed
/// TODO currently this ignores `flags`.
pub fn accessW(self: Dir, sub_path_w: [*:0]const u16, flags: File.OpenFlags) AccessError!void {
_ = flags;
return posix.faccessatW(self.fd, sub_path_w, 0, 0);
}
 
pub const CopyFileOptions = struct {
/// When this is `null` the mode is copied from the source file.
override_mode: ?File.Mode = null,
};
 
pub const PrevStatus = enum {
stale,
fresh,
};
 
/// Check the file size, mtime, and mode of `source_path` and `dest_path`. If they are equal, does nothing.
/// Otherwise, atomically copies `source_path` to `dest_path`. The destination file gains the mtime,
/// atime, and mode of the source file so that the next call to `updateFile` will not need a copy.
/// Returns the previous status of the file before updating.
/// If any of the directories do not exist for dest_path, they are created.
pub fn updateFile(
source_dir: Dir,
source_path: []const u8,
dest_dir: Dir,
dest_path: []const u8,
options: CopyFileOptions,
) !PrevStatus {
var src_file = try source_dir.openFile(source_path, .{});
defer src_file.close();
 
const src_stat = try src_file.stat();
const actual_mode = options.override_mode orelse src_stat.mode;
check_dest_stat: {
const dest_stat = blk: {
var dest_file = dest_dir.openFile(dest_path, .{}) catch |err| switch (err) {
error.FileNotFound => break :check_dest_stat,
else => |e| return e,
};
defer dest_file.close();
 
break :blk try dest_file.stat();
};
 
if (src_stat.size == dest_stat.size and
src_stat.mtime == dest_stat.mtime and
actual_mode == dest_stat.mode)
{
return PrevStatus.fresh;
}
}
 
if (fs.path.dirname(dest_path)) |dirname| {
try dest_dir.makePath(dirname);
}
 
var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = actual_mode });
defer atomic_file.deinit();
 
try atomic_file.file.writeFileAll(src_file, .{ .in_len = src_stat.size });
try atomic_file.file.updateTimes(src_stat.atime, src_stat.mtime);
try atomic_file.finish();
return PrevStatus.stale;
}
 
pub const CopyFileError = File.OpenError || File.StatError ||
AtomicFile.InitError || CopyFileRawError || AtomicFile.FinishError;
 
/// Guaranteed to be atomic.
/// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and readily available,
/// there is a possibility of power loss or application termination leaving temporary files present
/// in the same directory as dest_path.
pub fn copyFile(
source_dir: Dir,
source_path: []const u8,
dest_dir: Dir,
dest_path: []const u8,
options: CopyFileOptions,
) CopyFileError!void {
var in_file = try source_dir.openFile(source_path, .{});
defer in_file.close();
 
var size: ?u64 = null;
const mode = options.override_mode orelse blk: {
const st = try in_file.stat();
size = st.size;
break :blk st.mode;
};
 
var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = mode });
defer atomic_file.deinit();
 
try copy_file(in_file.handle, atomic_file.file.handle, size);
try atomic_file.finish();
}
 
const CopyFileRawError = error{SystemResources} || posix.CopyFileRangeError || posix.SendFileError;
 
// Transfer all the data between two file descriptors in the most efficient way.
// The copy starts at offset 0, the initial offsets are preserved.
// No metadata is transferred over.
fn copy_file(fd_in: posix.fd_t, fd_out: posix.fd_t, maybe_size: ?u64) CopyFileRawError!void {
if (comptime builtin.target.isDarwin()) {
const rc = posix.system.fcopyfile(fd_in, fd_out, null, posix.system.COPYFILE_DATA);
switch (posix.errno(rc)) {
.SUCCESS => return,
.INVAL => unreachable,
.NOMEM => return error.SystemResources,
// The source file is not a directory, symbolic link, or regular file.
// Try with the fallback path before giving up.
.OPNOTSUPP => {},
else => |err| return posix.unexpectedErrno(err),
}
}
 
if (builtin.os.tag == .linux) {
// Try copy_file_range first as that works at the FS level and is the
// most efficient method (if available).
var offset: u64 = 0;
cfr_loop: while (true) {
// The kernel checks the u64 value `offset+count` for overflow, use
// a 32 bit value so that the syscall won't return EINVAL except for
// impossibly large files (> 2^64-1 - 2^32-1).
const amt = try posix.copy_file_range(fd_in, offset, fd_out, offset, std.math.maxInt(u32), 0);
// Terminate as soon as we have copied size bytes or no bytes
if (maybe_size) |s| {
if (s == amt) break :cfr_loop;
}
if (amt == 0) break :cfr_loop;
offset += amt;
}
return;
}
 
// Sendfile is a zero-copy mechanism iff the OS supports it, otherwise the
// fallback code will copy the contents chunk by chunk.
const empty_iovec = [0]posix.iovec_const{};
var offset: u64 = 0;
sendfile_loop: while (true) {
const amt = try posix.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0);
// Terminate as soon as we have copied size bytes or no bytes
if (maybe_size) |s| {
if (s == amt) break :sendfile_loop;
}
if (amt == 0) break :sendfile_loop;
offset += amt;
}
}
 
pub const AtomicFileOptions = struct {
mode: File.Mode = File.default_mode,
};
 
/// Directly access the `.file` field, and then call `AtomicFile.finish`
/// to atomically replace `dest_path` with contents.
/// Always call `AtomicFile.deinit` to clean up, regardless of whether `AtomicFile.finish` succeeded.
/// `dest_path` must remain valid until `AtomicFile.deinit` is called.
pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) !AtomicFile {
if (fs.path.dirname(dest_path)) |dirname| {
const dir = try self.openDir(dirname, .{});
return AtomicFile.init(fs.path.basename(dest_path), options.mode, dir, true);
} else {
return AtomicFile.init(dest_path, options.mode, self, false);
}
}
 
pub const Stat = File.Stat;
pub const StatError = File.StatError;
 
pub fn stat(self: Dir) StatError!Stat {
const file: File = .{
.handle = self.fd,
.capable_io_mode = .blocking,
};
return file.stat();
}
 
pub const StatFileError = File.OpenError || File.StatError || posix.FStatAtError;
 
/// Returns metadata for a file inside the directory.
///
/// On Windows, this requires three syscalls. On other operating systems, it
/// only takes one.
///
/// Symlinks are followed.
///
/// `sub_path` may be absolute, in which case `self` is ignored.
pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat {
if (builtin.os.tag == .windows) {
var file = try self.openFile(sub_path, .{});
defer file.close();
return file.stat();
}
if (builtin.os.tag == .wasi and !builtin.link_libc) {
const st = try posix.fstatatWasi(self.fd, sub_path, posix.wasi.LOOKUP_SYMLINK_FOLLOW);
return Stat.fromSystem(st);
}
const st = try posix.fstatat(self.fd, sub_path, 0);
return Stat.fromSystem(st);
}
 
pub const ChmodError = File.ChmodError;
 
/// Changes the mode of the directory.
/// The process must have the correct privileges in order to do this
/// successfully, or must have the effective user ID matching the owner
/// of the directory. Additionally, the directory must have been opened
/// with `OpenDirOptions{ .iterate = true }`.
pub fn chmod(self: Dir, new_mode: File.Mode) ChmodError!void {
const file: File = .{
.handle = self.fd,
.capable_io_mode = .blocking,
};
try file.chmod(new_mode);
}
 
/// Changes the owner and group of the directory.
/// The process must have the correct privileges in order to do this
/// successfully. The group may be changed by the owner of the directory to
/// any group of which the owner is a member. Additionally, the directory
/// must have been opened with `OpenDirOptions{ .iterate = true }`. If the
/// owner or group is specified as `null`, the ID is not changed.
pub fn chown(self: Dir, owner: ?File.Uid, group: ?File.Gid) ChownError!void {
const file: File = .{
.handle = self.fd,
.capable_io_mode = .blocking,
};
try file.chown(owner, group);
}
 
pub const ChownError = File.ChownError;
 
const Permissions = File.Permissions;
pub const SetPermissionsError = File.SetPermissionsError;
 
/// Sets permissions according to the provided `Permissions` struct.
/// This method is *NOT* available on WASI
pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!void {
const file: File = .{
.handle = self.fd,
.capable_io_mode = .blocking,
};
try file.setPermissions(permissions);
}
 
const Metadata = File.Metadata;
pub const MetadataError = File.MetadataError;
 
/// Returns a `Metadata` struct, representing the permissions on the directory
pub fn metadata(self: Dir) MetadataError!Metadata {
const file: File = .{
.handle = self.fd,
.capable_io_mode = .blocking,
};
return try file.metadata();
}
 
const Dir = @This();
const builtin = @import("builtin");
const std = @import("../std.zig");
const File = std.fs.File;
const AtomicFile = std.fs.AtomicFile;
// https://github.com/ziglang/zig/issues/5019
const posix = std.os;
const mem = std.mem;
const fs = std.fs;
const Allocator = std.mem.Allocator;
 
filename was Deleted added: 4428, removed: 4469, total 0
@@ -0,0 +1,1624 @@
/// The OS-specific file descriptor or file handle.
handle: Handle,
 
/// On some systems, such as Linux, file system file descriptors are incapable
/// of non-blocking I/O. This forces us to perform asynchronous I/O on a dedicated thread,
/// to achieve non-blocking file-system I/O. To do this, `File` must be aware of whether
/// it is a file system file descriptor, or, more specifically, whether the I/O is always
/// blocking.
capable_io_mode: io.ModeOverride = io.default_mode,
 
/// Furthermore, even when `std.options.io_mode` is async, it is still sometimes desirable
/// to perform blocking I/O, although not by default. For example, when printing a
/// stack trace to stderr. This field tracks both by acting as an overriding I/O mode.
/// When not building in async I/O mode, the type only has the `.blocking` tag, making
/// it a zero-bit type.
intended_io_mode: io.ModeOverride = io.default_mode,
 
pub const Handle = posix.fd_t;
pub const Mode = posix.mode_t;
pub const INode = posix.ino_t;
pub const Uid = posix.uid_t;
pub const Gid = posix.gid_t;
 
pub const Kind = enum {
block_device,
character_device,
directory,
named_pipe,
sym_link,
file,
unix_domain_socket,
whiteout,
door,
event_port,
unknown,
};
 
/// This is the default mode given to POSIX operating systems for creating
/// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first,
/// since most people would expect "-rw-r--r--", for example, when using
/// the `touch` command, which would correspond to `0o644`. However, POSIX
/// libc implementations use `0o666` inside `fopen` and then rely on the
/// process-scoped "umask" setting to adjust this number for file creation.
pub const default_mode = switch (builtin.os.tag) {
.windows => 0,
.wasi => 0,
else => 0o666,
};
 
pub const OpenError = error{
SharingViolation,
PathAlreadyExists,
FileNotFound,
AccessDenied,
PipeBusy,
NameTooLong,
/// On Windows, file paths must be valid Unicode.
InvalidUtf8,
/// On Windows, file paths cannot contain these characters:
/// '/', '*', '?', '"', '<', '>', '|'
BadPathName,
Unexpected,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
} || posix.OpenError || posix.FlockError;
 
pub const OpenMode = enum {
read_only,
write_only,
read_write,
};
 
pub const Lock = enum {
none,
shared,
exclusive,
};
 
pub const OpenFlags = struct {
mode: OpenMode = .read_only,
 
/// Open the file with an advisory lock to coordinate with other processes
/// accessing it at the same time. An exclusive lock will prevent other
/// processes from acquiring a lock. A shared lock will prevent other
/// processes from acquiring a exclusive lock, but does not prevent
/// other process from getting their own shared locks.
///
/// The lock is advisory, except on Linux in very specific circumstances[1].
/// This means that a process that does not respect the locking API can still get access
/// to the file, despite the lock.
///
/// On these operating systems, the lock is acquired atomically with
/// opening the file:
/// * Darwin
/// * DragonFlyBSD
/// * FreeBSD
/// * Haiku
/// * NetBSD
/// * OpenBSD
/// On these operating systems, the lock is acquired via a separate syscall
/// after opening the file:
/// * Linux
/// * Windows
///
/// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt
lock: Lock = .none,
 
/// Sets whether or not to wait until the file is locked to return. If set to true,
/// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file
/// is available to proceed.
/// In async I/O mode, non-blocking at the OS level is
/// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned,
/// and `false` means `error.WouldBlock` is handled by the event loop.
lock_nonblocking: bool = false,
 
/// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even
/// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions
/// related to opening the file, reading, writing, and locking.
intended_io_mode: io.ModeOverride = io.default_mode,
 
/// Set this to allow the opened file to automatically become the
/// controlling TTY for the current process.
allow_ctty: bool = false,
 
pub fn isRead(self: OpenFlags) bool {
return self.mode != .write_only;
}
 
pub fn isWrite(self: OpenFlags) bool {
return self.mode != .read_only;
}
};
 
pub const CreateFlags = struct {
/// Whether the file will be created with read access.
read: bool = false,
 
/// If the file already exists, and is a regular file, and the access
/// mode allows writing, it will be truncated to length 0.
truncate: bool = true,
 
/// Ensures that this open call creates the file, otherwise causes
/// `error.PathAlreadyExists` to be returned.
exclusive: bool = false,
 
/// Open the file with an advisory lock to coordinate with other processes
/// accessing it at the same time. An exclusive lock will prevent other
/// processes from acquiring a lock. A shared lock will prevent other
/// processes from acquiring a exclusive lock, but does not prevent
/// other process from getting their own shared locks.
///
/// The lock is advisory, except on Linux in very specific circumstances[1].
/// This means that a process that does not respect the locking API can still get access
/// to the file, despite the lock.
///
/// On these operating systems, the lock is acquired atomically with
/// opening the file:
/// * Darwin
/// * DragonFlyBSD
/// * FreeBSD
/// * Haiku
/// * NetBSD
/// * OpenBSD
/// On these operating systems, the lock is acquired via a separate syscall
/// after opening the file:
/// * Linux
/// * Windows
///
/// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt
lock: Lock = .none,
 
/// Sets whether or not to wait until the file is locked to return. If set to true,
/// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file
/// is available to proceed.
/// In async I/O mode, non-blocking at the OS level is
/// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned,
/// and `false` means `error.WouldBlock` is handled by the event loop.
lock_nonblocking: bool = false,
 
/// For POSIX systems this is the file system mode the file will
/// be created with. On other systems this is always 0.
mode: Mode = default_mode,
 
/// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even
/// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions
/// related to opening the file, reading, writing, and locking.
intended_io_mode: io.ModeOverride = io.default_mode,
};
 
/// Upon success, the stream is in an uninitialized state. To continue using it,
/// you must use the open() function.
pub fn close(self: File) void {
if (is_windows) {
windows.CloseHandle(self.handle);
} else if (self.capable_io_mode != self.intended_io_mode) {
std.event.Loop.instance.?.close(self.handle);
} else {
posix.close(self.handle);
}
}
 
pub const SyncError = posix.SyncError;
 
/// Blocks until all pending file contents and metadata modifications
/// for the file have been synchronized with the underlying filesystem.
///
/// Note that this does not ensure that metadata for the
/// directory containing the file has also reached disk.
pub fn sync(self: File) SyncError!void {
return posix.fsync(self.handle);
}
 
/// Test whether the file refers to a terminal.
/// See also `supportsAnsiEscapeCodes`.
pub fn isTty(self: File) bool {
return posix.isatty(self.handle);
}
 
/// Test whether ANSI escape codes will be treated as such.
pub fn supportsAnsiEscapeCodes(self: File) bool {
if (builtin.os.tag == .windows) {
var console_mode: windows.DWORD = 0;
if (windows.kernel32.GetConsoleMode(self.handle, &console_mode) != 0) {
if (console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true;
}
 
return posix.isCygwinPty(self.handle);
}
if (builtin.os.tag == .wasi) {
// WASI sanitizes stdout when fd is a tty so ANSI escape codes
// will not be interpreted as actual cursor commands, and
// stderr is always sanitized.
return false;
}
if (self.isTty()) {
if (self.handle == posix.STDOUT_FILENO or self.handle == posix.STDERR_FILENO) {
if (posix.getenvZ("TERM")) |term| {
if (std.mem.eql(u8, term, "dumb"))
return false;
}
}
return true;
}
return false;
}
 
pub const SetEndPosError = posix.TruncateError;
 
/// Shrinks or expands the file.
/// The file offset after this call is left unchanged.
pub fn setEndPos(self: File, length: u64) SetEndPosError!void {
try posix.ftruncate(self.handle, length);
}
 
pub const SeekError = posix.SeekError;
 
/// Repositions read/write file offset relative to the current offset.
/// TODO: integrate with async I/O
pub fn seekBy(self: File, offset: i64) SeekError!void {
return posix.lseek_CUR(self.handle, offset);
}
 
/// Repositions read/write file offset relative to the end.
/// TODO: integrate with async I/O
pub fn seekFromEnd(self: File, offset: i64) SeekError!void {
return posix.lseek_END(self.handle, offset);
}
 
/// Repositions read/write file offset relative to the beginning.
/// TODO: integrate with async I/O
pub fn seekTo(self: File, offset: u64) SeekError!void {
return posix.lseek_SET(self.handle, offset);
}
 
pub const GetSeekPosError = posix.SeekError || posix.FStatError;
 
/// TODO: integrate with async I/O
pub fn getPos(self: File) GetSeekPosError!u64 {
return posix.lseek_CUR_get(self.handle);
}
 
/// TODO: integrate with async I/O
pub fn getEndPos(self: File) GetSeekPosError!u64 {
if (builtin.os.tag == .windows) {
return windows.GetFileSizeEx(self.handle);
}
return (try self.stat()).size;
}
 
pub const ModeError = posix.FStatError;
 
/// TODO: integrate with async I/O
pub fn mode(self: File) ModeError!Mode {
if (builtin.os.tag == .windows) {
return 0;
}
return (try self.stat()).mode;
}
 
pub const Stat = struct {
/// A number that the system uses to point to the file metadata. This
/// number is not guaranteed to be unique across time, as some file
/// systems may reuse an inode after its file has been deleted. Some
/// systems may change the inode of a file over time.
///
/// On Linux, the inode is a structure that stores the metadata, and
/// the inode _number_ is what you see here: the index number of the
/// inode.
///
/// The FileIndex on Windows is similar. It is a number for a file that
/// is unique to each filesystem.
inode: INode,
size: u64,
/// This is available on POSIX systems and is always 0 otherwise.
mode: Mode,
kind: Kind,
 
/// Access time in nanoseconds, relative to UTC 1970-01-01.
atime: i128,
/// Last modification time in nanoseconds, relative to UTC 1970-01-01.
mtime: i128,
/// Creation time in nanoseconds, relative to UTC 1970-01-01.
ctime: i128,
 
pub fn fromSystem(st: posix.system.Stat) Stat {
const atime = st.atime();
const mtime = st.mtime();
const ctime = st.ctime();
const kind: Kind = if (builtin.os.tag == .wasi and !builtin.link_libc) switch (st.filetype) {
.BLOCK_DEVICE => .block_device,
.CHARACTER_DEVICE => .character_device,
.DIRECTORY => .directory,
.SYMBOLIC_LINK => .sym_link,
.REGULAR_FILE => .file,
.SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,
else => .unknown,
} else blk: {
const m = st.mode & posix.S.IFMT;
switch (m) {
posix.S.IFBLK => break :blk .block_device,
posix.S.IFCHR => break :blk .character_device,
posix.S.IFDIR => break :blk .directory,
posix.S.IFIFO => break :blk .named_pipe,
posix.S.IFLNK => break :blk .sym_link,
posix.S.IFREG => break :blk .file,
posix.S.IFSOCK => break :blk .unix_domain_socket,
else => {},
}
if (builtin.os.tag.isSolarish()) switch (m) {
posix.S.IFDOOR => break :blk .door,
posix.S.IFPORT => break :blk .event_port,
else => {},
};
 
break :blk .unknown;
};
 
return Stat{
.inode = st.ino,
.size = @as(u64, @bitCast(st.size)),
.mode = st.mode,
.kind = kind,
.atime = @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec,
.mtime = @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec,
.ctime = @as(i128, ctime.tv_sec) * std.time.ns_per_s + ctime.tv_nsec,
};
}
};
 
pub const StatError = posix.FStatError;
 
/// TODO: integrate with async I/O
pub fn stat(self: File) StatError!Stat {
if (builtin.os.tag == .windows) {
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
var info: windows.FILE_ALL_INFORMATION = undefined;
const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation);
switch (rc) {
.SUCCESS => {},
// Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
// size provided. This is treated as success because the type of variable-length information that this would be relevant for
// (name, volume name, etc) we don't care about.
.BUFFER_OVERFLOW => {},
.INVALID_PARAMETER => unreachable,
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
}
return Stat{
.inode = info.InternalInformation.IndexNumber,
.size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
.mode = 0,
.kind = if (info.StandardInformation.Directory == 0) .file else .directory,
.atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
.mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
.ctime = windows.fromSysTime(info.BasicInformation.CreationTime),
};
}
 
const st = try posix.fstat(self.handle);
return Stat.fromSystem(st);
}
 
pub const ChmodError = posix.FChmodError;
 
/// Changes the mode of the file.
/// The process must have the correct privileges in order to do this
/// successfully, or must have the effective user ID matching the owner
/// of the file.
pub fn chmod(self: File, new_mode: Mode) ChmodError!void {
try posix.fchmod(self.handle, new_mode);
}
 
pub const ChownError = posix.FChownError;
 
/// Changes the owner and group of the file.
/// The process must have the correct privileges in order to do this
/// successfully. The group may be changed by the owner of the file to
/// any group of which the owner is a member. If the owner or group is
/// specified as `null`, the ID is not changed.
pub fn chown(self: File, owner: ?Uid, group: ?Gid) ChownError!void {
try posix.fchown(self.handle, owner, group);
}
 
/// Cross-platform representation of permissions on a file.
/// The `readonly` and `setReadonly` are the only methods available across all platforms.
/// Platform-specific functionality is available through the `inner` field.
pub const Permissions = struct {
/// You may use the `inner` field to use platform-specific functionality
inner: switch (builtin.os.tag) {
.windows => PermissionsWindows,
else => PermissionsUnix,
},
 
const Self = @This();
 
/// Returns `true` if permissions represent an unwritable file.
/// On Unix, `true` is returned only if no class has write permissions.
pub fn readOnly(self: Self) bool {
return self.inner.readOnly();
}
 
/// Sets whether write permissions are provided.
/// On Unix, this affects *all* classes. If this is undesired, use `unixSet`.
/// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
pub fn setReadOnly(self: *Self, read_only: bool) void {
self.inner.setReadOnly(read_only);
}
};
 
pub const PermissionsWindows = struct {
attributes: windows.DWORD,
 
const Self = @This();
 
/// Returns `true` if permissions represent an unwritable file.
pub fn readOnly(self: Self) bool {
return self.attributes & windows.FILE_ATTRIBUTE_READONLY != 0;
}
 
/// Sets whether write permissions are provided.
/// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
pub fn setReadOnly(self: *Self, read_only: bool) void {
if (read_only) {
self.attributes |= windows.FILE_ATTRIBUTE_READONLY;
} else {
self.attributes &= ~@as(windows.DWORD, windows.FILE_ATTRIBUTE_READONLY);
}
}
};
 
pub const PermissionsUnix = struct {
mode: Mode,
 
const Self = @This();
 
/// Returns `true` if permissions represent an unwritable file.
/// `true` is returned only if no class has write permissions.
pub fn readOnly(self: Self) bool {
return self.mode & 0o222 == 0;
}
 
/// Sets whether write permissions are provided.
/// This affects *all* classes. If this is undesired, use `unixSet`.
/// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
pub fn setReadOnly(self: *Self, read_only: bool) void {
if (read_only) {
self.mode &= ~@as(Mode, 0o222);
} else {
self.mode |= @as(Mode, 0o222);
}
}
 
pub const Class = enum(u2) {
user = 2,
group = 1,
other = 0,
};
 
pub const Permission = enum(u3) {
read = 0o4,
write = 0o2,
execute = 0o1,
};
 
/// Returns `true` if the chosen class has the selected permission.
/// This method is only available on Unix platforms.
pub fn unixHas(self: Self, class: Class, permission: Permission) bool {
const mask = @as(Mode, @intFromEnum(permission)) << @as(u3, @intFromEnum(class)) * 3;
return self.mode & mask != 0;
}
 
/// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged.
/// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
pub fn unixSet(self: *Self, class: Class, permissions: struct {
read: ?bool = null,
write: ?bool = null,
execute: ?bool = null,
}) void {
const shift = @as(u3, @intFromEnum(class)) * 3;
if (permissions.read) |r| {
if (r) {
self.mode |= @as(Mode, 0o4) << shift;
} else {
self.mode &= ~(@as(Mode, 0o4) << shift);
}
}
if (permissions.write) |w| {
if (w) {
self.mode |= @as(Mode, 0o2) << shift;
} else {
self.mode &= ~(@as(Mode, 0o2) << shift);
}
}
if (permissions.execute) |x| {
if (x) {
self.mode |= @as(Mode, 0o1) << shift;
} else {
self.mode &= ~(@as(Mode, 0o1) << shift);
}
}
}
 
/// Returns a `Permissions` struct representing the permissions from the passed mode.
pub fn unixNew(new_mode: Mode) Self {
return Self{
.mode = new_mode,
};
}
};
 
pub const SetPermissionsError = ChmodError;
 
/// Sets permissions according to the provided `Permissions` struct.
/// This method is *NOT* available on WASI
pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void {
switch (builtin.os.tag) {
.windows => {
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
var info = windows.FILE_BASIC_INFORMATION{
.CreationTime = 0,
.LastAccessTime = 0,
.LastWriteTime = 0,
.ChangeTime = 0,
.FileAttributes = permissions.inner.attributes,
};
const rc = windows.ntdll.NtSetInformationFile(
self.handle,
&io_status_block,
&info,
@sizeOf(windows.FILE_BASIC_INFORMATION),
.FileBasicInformation,
);
switch (rc) {
.SUCCESS => return,
.INVALID_HANDLE => unreachable,
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
}
},
.wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod
else => {
try self.chmod(permissions.inner.mode);
},
}
}
 
/// Cross-platform representation of file metadata.
/// Platform-specific functionality is available through the `inner` field.
pub const Metadata = struct {
/// You may use the `inner` field to use platform-specific functionality
inner: switch (builtin.os.tag) {
.windows => MetadataWindows,
.linux => MetadataLinux,
else => MetadataUnix,
},
 
const Self = @This();
 
/// Returns the size of the file
pub fn size(self: Self) u64 {
return self.inner.size();
}
 
/// Returns a `Permissions` struct, representing the permissions on the file
pub fn permissions(self: Self) Permissions {
return self.inner.permissions();
}
 
/// Returns the `Kind` of file.
/// On Windows, can only return: `.file`, `.directory`, `.sym_link` or `.unknown`
pub fn kind(self: Self) Kind {
return self.inner.kind();
}
 
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
pub fn accessed(self: Self) i128 {
return self.inner.accessed();
}
 
/// Returns the time the file was modified in nanoseconds since UTC 1970-01-01
pub fn modified(self: Self) i128 {
return self.inner.modified();
}
 
/// Returns the time the file was created in nanoseconds since UTC 1970-01-01
/// On Windows, this cannot return null
/// On Linux, this returns null if the filesystem does not support creation times, or if the kernel is older than 4.11
/// On Unices, this returns null if the filesystem or OS does not support creation times
/// On MacOS, this returns the ctime if the filesystem does not support creation times; this is insanity, and yet another reason to hate on Apple
pub fn created(self: Self) ?i128 {
return self.inner.created();
}
};
 
pub const MetadataUnix = struct {
stat: posix.Stat,
 
const Self = @This();
 
/// Returns the size of the file
pub fn size(self: Self) u64 {
return @as(u64, @intCast(self.stat.size));
}
 
/// Returns a `Permissions` struct, representing the permissions on the file
pub fn permissions(self: Self) Permissions {
return Permissions{ .inner = PermissionsUnix{ .mode = self.stat.mode } };
}
 
/// Returns the `Kind` of the file
pub fn kind(self: Self) Kind {
if (builtin.os.tag == .wasi and !builtin.link_libc) return switch (self.stat.filetype) {
.BLOCK_DEVICE => .block_device,
.CHARACTER_DEVICE => .character_device,
.DIRECTORY => .directory,
.SYMBOLIC_LINK => .sym_link,
.REGULAR_FILE => .file,
.SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,
else => .unknown,
};
 
const m = self.stat.mode & posix.S.IFMT;
 
switch (m) {
posix.S.IFBLK => return .block_device,
posix.S.IFCHR => return .character_device,
posix.S.IFDIR => return .directory,
posix.S.IFIFO => return .named_pipe,
posix.S.IFLNK => return .sym_link,
posix.S.IFREG => return .file,
posix.S.IFSOCK => return .unix_domain_socket,
else => {},
}
 
if (builtin.os.tag.isSolarish()) switch (m) {
posix.S.IFDOOR => return .door,
posix.S.IFPORT => return .event_port,
else => {},
};
 
return .unknown;
}
 
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
pub fn accessed(self: Self) i128 {
const atime = self.stat.atime();
return @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec;
}
 
/// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01
pub fn modified(self: Self) i128 {
const mtime = self.stat.mtime();
return @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec;
}
 
/// Returns the time the file was created in nanoseconds since UTC 1970-01-01.
/// Returns null if this is not supported by the OS or filesystem
pub fn created(self: Self) ?i128 {
if (!@hasDecl(@TypeOf(self.stat), "birthtime")) return null;
const birthtime = self.stat.birthtime();
 
// If the filesystem doesn't support this the value *should* be:
// On FreeBSD: tv_nsec = 0, tv_sec = -1
// On NetBSD and OpenBSD: tv_nsec = 0, tv_sec = 0
// On MacOS, it is set to ctime -- we cannot detect this!!
switch (builtin.os.tag) {
.freebsd => if (birthtime.tv_sec == -1 and birthtime.tv_nsec == 0) return null,
.netbsd, .openbsd => if (birthtime.tv_sec == 0 and birthtime.tv_nsec == 0) return null,
.macos => {},
else => @compileError("Creation time detection not implemented for OS"),
}
 
return @as(i128, birthtime.tv_sec) * std.time.ns_per_s + birthtime.tv_nsec;
}
};
 
/// `MetadataUnix`, but using Linux's `statx` syscall.
/// On Linux versions below 4.11, `statx` will be filled with data from stat.
pub const MetadataLinux = struct {
statx: std.os.linux.Statx,
 
const Self = @This();
 
/// Returns the size of the file
pub fn size(self: Self) u64 {
return self.statx.size;
}
 
/// Returns a `Permissions` struct, representing the permissions on the file
pub fn permissions(self: Self) Permissions {
return Permissions{ .inner = PermissionsUnix{ .mode = self.statx.mode } };
}
 
/// Returns the `Kind` of the file
pub fn kind(self: Self) Kind {
const m = self.statx.mode & posix.S.IFMT;
 
switch (m) {
posix.S.IFBLK => return .block_device,
posix.S.IFCHR => return .character_device,
posix.S.IFDIR => return .directory,
posix.S.IFIFO => return .named_pipe,
posix.S.IFLNK => return .sym_link,
posix.S.IFREG => return .file,
posix.S.IFSOCK => return .unix_domain_socket,
else => {},
}
 
return .unknown;
}
 
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
pub fn accessed(self: Self) i128 {
return @as(i128, self.statx.atime.tv_sec) * std.time.ns_per_s + self.statx.atime.tv_nsec;
}
 
/// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01
pub fn modified(self: Self) i128 {
return @as(i128, self.statx.mtime.tv_sec) * std.time.ns_per_s + self.statx.mtime.tv_nsec;
}
 
/// Returns the time the file was created in nanoseconds since UTC 1970-01-01.
/// Returns null if this is not supported by the filesystem, or on kernels before than version 4.11
pub fn created(self: Self) ?i128 {
if (self.statx.mask & std.os.linux.STATX_BTIME == 0) return null;
return @as(i128, self.statx.btime.tv_sec) * std.time.ns_per_s + self.statx.btime.tv_nsec;
}
};
 
pub const MetadataWindows = struct {
attributes: windows.DWORD,
reparse_tag: windows.DWORD,
_size: u64,
access_time: i128,
modified_time: i128,
creation_time: i128,
 
const Self = @This();
 
/// Returns the size of the file
pub fn size(self: Self) u64 {
return self._size;
}
 
/// Returns a `Permissions` struct, representing the permissions on the file
pub fn permissions(self: Self) Permissions {
return Permissions{ .inner = PermissionsWindows{ .attributes = self.attributes } };
}
 
/// Returns the `Kind` of the file.
/// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown`
pub fn kind(self: Self) Kind {
if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
if (self.reparse_tag & 0x20000000 != 0) {
return .sym_link;
}
} else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
return .directory;
} else {
return .file;
}
return .unknown;
}
 
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
pub fn accessed(self: Self) i128 {
return self.access_time;
}
 
/// Returns the time the file was modified in nanoseconds since UTC 1970-01-01
pub fn modified(self: Self) i128 {
return self.modified_time;
}
 
/// Returns the time the file was created in nanoseconds since UTC 1970-01-01.
/// This never returns null, only returning an optional for compatibility with other OSes
pub fn created(self: Self) ?i128 {
return self.creation_time;
}
};
 
pub const MetadataError = posix.FStatError;
 
pub fn metadata(self: File) MetadataError!Metadata {
return Metadata{
.inner = switch (builtin.os.tag) {
.windows => blk: {
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
var info: windows.FILE_ALL_INFORMATION = undefined;
 
const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation);
switch (rc) {
.SUCCESS => {},
// Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
// size provided. This is treated as success because the type of variable-length information that this would be relevant for
// (name, volume name, etc) we don't care about.
.BUFFER_OVERFLOW => {},
.INVALID_PARAMETER => unreachable,
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
}
 
const reparse_tag: windows.DWORD = reparse_blk: {
if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]);
const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0]));
break :reparse_blk reparse_struct.ReparseTag;
}
break :reparse_blk 0;
};
 
break :blk MetadataWindows{
.attributes = info.BasicInformation.FileAttributes,
.reparse_tag = reparse_tag,
._size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
.access_time = windows.fromSysTime(info.BasicInformation.LastAccessTime),
.modified_time = windows.fromSysTime(info.BasicInformation.LastWriteTime),
.creation_time = windows.fromSysTime(info.BasicInformation.CreationTime),
};
},
.linux => blk: {
const l = std.os.linux;
var stx = std.mem.zeroes(l.Statx);
const rcx = l.statx(self.handle, "\x00", l.AT.EMPTY_PATH, l.STATX_TYPE |
l.STATX_MODE | l.STATX_ATIME | l.STATX_MTIME | l.STATX_BTIME, &stx);
 
switch (posix.errno(rcx)) {
.SUCCESS => {},
// NOSYS happens when `statx` is unsupported, which is the case on kernel versions before 4.11
// Here, we call `fstat` and fill `stx` with the data we need
.NOSYS => {
const st = try posix.fstat(self.handle);
 
stx.mode = @as(u16, @intCast(st.mode));
 
// Hacky conversion from timespec to statx_timestamp
stx.atime = std.mem.zeroes(l.statx_timestamp);
stx.atime.tv_sec = st.atim.tv_sec;
stx.atime.tv_nsec = @as(u32, @intCast(st.atim.tv_nsec)); // Guaranteed to succeed (tv_nsec is always below 10^9)
 
stx.mtime = std.mem.zeroes(l.statx_timestamp);
stx.mtime.tv_sec = st.mtim.tv_sec;
stx.mtime.tv_nsec = @as(u32, @intCast(st.mtim.tv_nsec));
 
stx.mask = l.STATX_BASIC_STATS | l.STATX_MTIME;
},
.BADF => unreachable,
.FAULT => unreachable,
.NOMEM => return error.SystemResources,
else => |err| return posix.unexpectedErrno(err),
}
 
break :blk MetadataLinux{
.statx = stx,
};
},
else => blk: {
const st = try posix.fstat(self.handle);
break :blk MetadataUnix{
.stat = st,
};
},
},
};
}
 
pub const UpdateTimesError = posix.FutimensError || windows.SetFileTimeError;
 
/// The underlying file system may have a different granularity than nanoseconds,
/// and therefore this function cannot guarantee any precision will be stored.
/// Further, the maximum value is limited by the system ABI. When a value is provided
/// that exceeds this range, the value is clamped to the maximum.
/// TODO: integrate with async I/O
pub fn updateTimes(
self: File,
/// access timestamp in nanoseconds
atime: i128,
/// last modification timestamp in nanoseconds
mtime: i128,
) UpdateTimesError!void {
if (builtin.os.tag == .windows) {
const atime_ft = windows.nanoSecondsToFileTime(atime);
const mtime_ft = windows.nanoSecondsToFileTime(mtime);
return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft);
}
const times = [2]posix.timespec{
posix.timespec{
.tv_sec = math.cast(isize, @divFloor(atime, std.time.ns_per_s)) orelse maxInt(isize),
.tv_nsec = math.cast(isize, @mod(atime, std.time.ns_per_s)) orelse maxInt(isize),
},
posix.timespec{
.tv_sec = math.cast(isize, @divFloor(mtime, std.time.ns_per_s)) orelse maxInt(isize),
.tv_nsec = math.cast(isize, @mod(mtime, std.time.ns_per_s)) orelse maxInt(isize),
},
};
try posix.futimens(self.handle, &times);
}
 
/// Reads all the bytes from the current position to the end of the file.
/// On success, caller owns returned buffer.
/// If the file is larger than `max_bytes`, returns `error.FileTooBig`.
pub fn readToEndAlloc(self: File, allocator: Allocator, max_bytes: usize) ![]u8 {
return self.readToEndAllocOptions(allocator, max_bytes, null, @alignOf(u8), null);
}
 
/// Reads all the bytes from the current position to the end of the file.
/// On success, caller owns returned buffer.
/// If the file is larger than `max_bytes`, returns `error.FileTooBig`.
/// If `size_hint` is specified the initial buffer size is calculated using
/// that value, otherwise an arbitrary value is used instead.
/// Allows specifying alignment and a sentinel value.
pub fn readToEndAllocOptions(
self: File,
allocator: Allocator,
max_bytes: usize,
size_hint: ?usize,
comptime alignment: u29,
comptime optional_sentinel: ?u8,
) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) {
// If no size hint is provided fall back to the size=0 code path
const size = size_hint orelse 0;
 
// The file size returned by stat is used as hint to set the buffer
// size. If the reported size is zero, as it happens on Linux for files
// in /proc, a small buffer is allocated instead.
const initial_cap = (if (size > 0) size else 1024) + @intFromBool(optional_sentinel != null);
var array_list = try std.ArrayListAligned(u8, alignment).initCapacity(allocator, initial_cap);
defer array_list.deinit();
 
self.reader().readAllArrayListAligned(alignment, &array_list, max_bytes) catch |err| switch (err) {
error.StreamTooLong => return error.FileTooBig,
else => |e| return e,
};
 
if (optional_sentinel) |sentinel| {
return try array_list.toOwnedSliceSentinel(sentinel);
} else {
return try array_list.toOwnedSlice();
}
}
 
pub const ReadError = posix.ReadError;
pub const PReadError = posix.PReadError;
 
pub fn read(self: File, buffer: []u8) ReadError!usize {
if (is_windows) {
return windows.ReadFile(self.handle, buffer, null, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return posix.read(self.handle, buffer);
} else {
return std.event.Loop.instance.?.read(self.handle, buffer, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it
/// means the file reached the end. Reaching the end of a file is not an error condition.
pub fn readAll(self: File, buffer: []u8) ReadError!usize {
var index: usize = 0;
while (index != buffer.len) {
const amt = try self.read(buffer[index..]);
if (amt == 0) break;
index += amt;
}
return index;
}
 
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize {
if (is_windows) {
return windows.ReadFile(self.handle, buffer, offset, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return posix.pread(self.handle, buffer, offset);
} else {
return std.event.Loop.instance.?.pread(self.handle, buffer, offset, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it
/// means the file reached the end. Reaching the end of a file is not an error condition.
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize {
var index: usize = 0;
while (index != buffer.len) {
const amt = try self.pread(buffer[index..], offset + index);
if (amt == 0) break;
index += amt;
}
return index;
}
 
/// See https://github.com/ziglang/zig/issues/7699
pub fn readv(self: File, iovecs: []const posix.iovec) ReadError!usize {
if (is_windows) {
// TODO improve this to use ReadFileScatter
if (iovecs.len == 0) return @as(usize, 0);
const first = iovecs[0];
return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return posix.readv(self.handle, iovecs);
} else {
return std.event.Loop.instance.?.readv(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// Returns the number of bytes read. If the number read is smaller than the total bytes
/// from all the buffers, it means the file reached the end. Reaching the end of a file
/// is not an error condition.
///
/// The `iovecs` parameter is mutable because:
/// * This function needs to mutate the fields in order to handle partial
/// reads from the underlying OS layer.
/// * The OS layer expects pointer addresses to be inside the application's address space
/// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer
/// addresses when the length is zero. So this function modifies the iov_base fields
/// when the length is zero.
///
/// Related open issue: https://github.com/ziglang/zig/issues/7699
pub fn readvAll(self: File, iovecs: []posix.iovec) ReadError!usize {
if (iovecs.len == 0) return 0;
 
// We use the address of this local variable for all zero-length
// vectors so that the OS does not complain that we are giving it
// addresses outside the application's address space.
var garbage: [1]u8 = undefined;
for (iovecs) |*v| {
if (v.iov_len == 0) v.iov_base = &garbage;
}
 
var i: usize = 0;
var off: usize = 0;
while (true) {
var amt = try self.readv(iovecs[i..]);
var eof = amt == 0;
off += amt;
while (amt >= iovecs[i].iov_len) {
amt -= iovecs[i].iov_len;
i += 1;
if (i >= iovecs.len) return off;
eof = false;
}
if (eof) return off;
iovecs[i].iov_base += amt;
iovecs[i].iov_len -= amt;
}
}
 
/// See https://github.com/ziglang/zig/issues/7699
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn preadv(self: File, iovecs: []const posix.iovec, offset: u64) PReadError!usize {
if (is_windows) {
// TODO improve this to use ReadFileScatter
if (iovecs.len == 0) return @as(usize, 0);
const first = iovecs[0];
return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return posix.preadv(self.handle, iovecs, offset);
} else {
return std.event.Loop.instance.?.preadv(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// Returns the number of bytes read. If the number read is smaller than the total bytes
/// from all the buffers, it means the file reached the end. Reaching the end of a file
/// is not an error condition.
/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
/// order to handle partial reads from the underlying OS layer.
/// See https://github.com/ziglang/zig/issues/7699
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn preadvAll(self: File, iovecs: []posix.iovec, offset: u64) PReadError!usize {
if (iovecs.len == 0) return 0;
 
var i: usize = 0;
var off: usize = 0;
while (true) {
var amt = try self.preadv(iovecs[i..], offset + off);
var eof = amt == 0;
off += amt;
while (amt >= iovecs[i].iov_len) {
amt -= iovecs[i].iov_len;
i += 1;
if (i >= iovecs.len) return off;
eof = false;
}
if (eof) return off;
iovecs[i].iov_base += amt;
iovecs[i].iov_len -= amt;
}
}
 
pub const WriteError = posix.WriteError;
pub const PWriteError = posix.PWriteError;
 
pub fn write(self: File, bytes: []const u8) WriteError!usize {
if (is_windows) {
return windows.WriteFile(self.handle, bytes, null, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return posix.write(self.handle, bytes);
} else {
return std.event.Loop.instance.?.write(self.handle, bytes, self.capable_io_mode != self.intended_io_mode);
}
}
 
pub fn writeAll(self: File, bytes: []const u8) WriteError!void {
var index: usize = 0;
while (index < bytes.len) {
index += try self.write(bytes[index..]);
}
}
 
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize {
if (is_windows) {
return windows.WriteFile(self.handle, bytes, offset, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return posix.pwrite(self.handle, bytes, offset);
} else {
return std.event.Loop.instance.?.pwrite(self.handle, bytes, offset, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void {
var index: usize = 0;
while (index < bytes.len) {
index += try self.pwrite(bytes[index..], offset + index);
}
}
 
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.net.Stream.writev`.
pub fn writev(self: File, iovecs: []const posix.iovec_const) WriteError!usize {
if (is_windows) {
// TODO improve this to use WriteFileScatter
if (iovecs.len == 0) return @as(usize, 0);
const first = iovecs[0];
return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return posix.writev(self.handle, iovecs);
} else {
return std.event.Loop.instance.?.writev(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// The `iovecs` parameter is mutable because:
/// * This function needs to mutate the fields in order to handle partial
/// writes from the underlying OS layer.
/// * The OS layer expects pointer addresses to be inside the application's address space
/// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer
/// addresses when the length is zero. So this function modifies the iov_base fields
/// when the length is zero.
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.net.Stream.writevAll`.
pub fn writevAll(self: File, iovecs: []posix.iovec_const) WriteError!void {
if (iovecs.len == 0) return;
 
// We use the address of this local variable for all zero-length
// vectors so that the OS does not complain that we are giving it
// addresses outside the application's address space.
var garbage: [1]u8 = undefined;
for (iovecs) |*v| {
if (v.iov_len == 0) v.iov_base = &garbage;
}
 
var i: usize = 0;
while (true) {
var amt = try self.writev(iovecs[i..]);
while (amt >= iovecs[i].iov_len) {
amt -= iovecs[i].iov_len;
i += 1;
if (i >= iovecs.len) return;
}
iovecs[i].iov_base += amt;
iovecs[i].iov_len -= amt;
}
}
 
/// See https://github.com/ziglang/zig/issues/7699
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn pwritev(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError!usize {
if (is_windows) {
// TODO improve this to use WriteFileScatter
if (iovecs.len == 0) return @as(usize, 0);
const first = iovecs[0];
return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return posix.pwritev(self.handle, iovecs, offset);
} else {
return std.event.Loop.instance.?.pwritev(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
/// order to handle partial writes from the underlying OS layer.
/// See https://github.com/ziglang/zig/issues/7699
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn pwritevAll(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError!void {
if (iovecs.len == 0) return;
 
var i: usize = 0;
var off: u64 = 0;
while (true) {
var amt = try self.pwritev(iovecs[i..], offset + off);
off += amt;
while (amt >= iovecs[i].iov_len) {
amt -= iovecs[i].iov_len;
i += 1;
if (i >= iovecs.len) return;
}
iovecs[i].iov_base += amt;
iovecs[i].iov_len -= amt;
}
}
 
pub const CopyRangeError = posix.CopyFileRangeError;
 
pub fn copyRange(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 {
const adjusted_len = math.cast(usize, len) orelse maxInt(usize);
const result = try posix.copy_file_range(in.handle, in_offset, out.handle, out_offset, adjusted_len, 0);
return result;
}
 
/// Returns the number of bytes copied. If the number read is smaller than `buffer.len`, it
/// means the in file reached the end. Reaching the end of a file is not an error condition.
pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 {
var total_bytes_copied: u64 = 0;
var in_off = in_offset;
var out_off = out_offset;
while (total_bytes_copied < len) {
const amt_copied = try copyRange(in, in_off, out, out_off, len - total_bytes_copied);
if (amt_copied == 0) return total_bytes_copied;
total_bytes_copied += amt_copied;
in_off += amt_copied;
out_off += amt_copied;
}
return total_bytes_copied;
}
 
pub const WriteFileOptions = struct {
in_offset: u64 = 0,
 
/// `null` means the entire file. `0` means no bytes from the file.
/// When this is `null`, trailers must be sent in a separate writev() call
/// due to a flaw in the BSD sendfile API. Other operating systems, such as
/// Linux, already do this anyway due to API limitations.
/// If the size of the source file is known, passing the size here will save one syscall.
in_len: ?u64 = null,
 
headers_and_trailers: []posix.iovec_const = &[0]posix.iovec_const{},
 
/// The trailer count is inferred from `headers_and_trailers.len - header_count`
header_count: usize = 0,
};
 
pub const WriteFileError = ReadError || error{EndOfStream} || WriteError;
 
pub fn writeFileAll(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void {
return self.writeFileAllSendfile(in_file, args) catch |err| switch (err) {
error.Unseekable,
error.FastOpenAlreadyInProgress,
error.MessageTooBig,
error.FileDescriptorNotASocket,
error.NetworkUnreachable,
error.NetworkSubsystemFailed,
=> return self.writeFileAllUnseekable(in_file, args),
 
else => |e| return e,
};
}
 
/// Does not try seeking in either of the File parameters.
/// See `writeFileAll` as an alternative to calling this.
pub fn writeFileAllUnseekable(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void {
const headers = args.headers_and_trailers[0..args.header_count];
const trailers = args.headers_and_trailers[args.header_count..];
 
try self.writevAll(headers);
 
try in_file.reader().skipBytes(args.in_offset, .{ .buf_size = 4096 });
 
var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
if (args.in_len) |len| {
var stream = std.io.limitedReader(in_file.reader(), len);
try fifo.pump(stream.reader(), self.writer());
} else {
try fifo.pump(in_file.reader(), self.writer());
}
 
try self.writevAll(trailers);
}
 
/// Low level function which can fail for OS-specific reasons.
/// See `writeFileAll` as an alternative to calling this.
/// TODO integrate with async I/O
fn writeFileAllSendfile(self: File, in_file: File, args: WriteFileOptions) posix.SendFileError!void {
const count = blk: {
if (args.in_len) |l| {
if (l == 0) {
return self.writevAll(args.headers_and_trailers);
} else {
break :blk l;
}
} else {
break :blk 0;
}
};
const headers = args.headers_and_trailers[0..args.header_count];
const trailers = args.headers_and_trailers[args.header_count..];
const zero_iovec = &[0]posix.iovec_const{};
// When reading the whole file, we cannot put the trailers in the sendfile() syscall,
// because we have no way to determine whether a partial write is past the end of the file or not.
const trls = if (count == 0) zero_iovec else trailers;
const offset = args.in_offset;
const out_fd = self.handle;
const in_fd = in_file.handle;
const flags = 0;
var amt: usize = 0;
hdrs: {
var i: usize = 0;
while (i < headers.len) {
amt = try posix.sendfile(out_fd, in_fd, offset, count, headers[i..], trls, flags);
while (amt >= headers[i].iov_len) {
amt -= headers[i].iov_len;
i += 1;
if (i >= headers.len) break :hdrs;
}
headers[i].iov_base += amt;
headers[i].iov_len -= amt;
}
}
if (count == 0) {
var off: u64 = amt;
while (true) {
amt = try posix.sendfile(out_fd, in_fd, offset + off, 0, zero_iovec, zero_iovec, flags);
if (amt == 0) break;
off += amt;
}
} else {
var off: u64 = amt;
while (off < count) {
amt = try posix.sendfile(out_fd, in_fd, offset + off, count - off, zero_iovec, trailers, flags);
off += amt;
}
amt = @as(usize, @intCast(off - count));
}
var i: usize = 0;
while (i < trailers.len) {
while (amt >= trailers[i].iov_len) {
amt -= trailers[i].iov_len;
i += 1;
if (i >= trailers.len) return;
}
trailers[i].iov_base += amt;
trailers[i].iov_len -= amt;
amt = try posix.writev(self.handle, trailers[i..]);
}
}
 
pub const Reader = io.Reader(File, ReadError, read);
 
pub fn reader(file: File) Reader {
return .{ .context = file };
}
 
pub const Writer = io.Writer(File, WriteError, write);
 
pub fn writer(file: File) Writer {
return .{ .context = file };
}
 
pub const SeekableStream = io.SeekableStream(
File,
SeekError,
GetSeekPosError,
seekTo,
seekBy,
getPos,
getEndPos,
);
 
pub fn seekableStream(file: File) SeekableStream {
return .{ .context = file };
}
 
const range_off: windows.LARGE_INTEGER = 0;
const range_len: windows.LARGE_INTEGER = 1;
 
pub const LockError = error{
SystemResources,
FileLocksNotSupported,
} || posix.UnexpectedError;
 
/// Blocks when an incompatible lock is held by another process.
/// A process may hold only one type of lock (shared or exclusive) on
/// a file. When a process terminates in any way, the lock is released.
///
/// Assumes the file is unlocked.
///
/// TODO: integrate with async I/O
pub fn lock(file: File, l: Lock) LockError!void {
if (is_windows) {
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
const exclusive = switch (l) {
.none => return,
.shared => false,
.exclusive => true,
};
return windows.LockFile(
file.handle,
null,
null,
null,
&io_status_block,
&range_off,
&range_len,
null,
windows.FALSE, // non-blocking=false
@intFromBool(exclusive),
) catch |err| switch (err) {
error.WouldBlock => unreachable, // non-blocking=false
else => |e| return e,
};
} else {
return posix.flock(file.handle, switch (l) {
.none => posix.LOCK.UN,
.shared => posix.LOCK.SH,
.exclusive => posix.LOCK.EX,
}) catch |err| switch (err) {
error.WouldBlock => unreachable, // non-blocking=false
else => |e| return e,
};
}
}
 
/// Assumes the file is locked.
pub fn unlock(file: File) void {
if (is_windows) {
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
return windows.UnlockFile(
file.handle,
&io_status_block,
&range_off,
&range_len,
null,
) catch |err| switch (err) {
error.RangeNotLocked => unreachable, // Function assumes unlocked.
error.Unexpected => unreachable, // Resource deallocation must succeed.
};
} else {
return posix.flock(file.handle, posix.LOCK.UN) catch |err| switch (err) {
error.WouldBlock => unreachable, // unlocking can't block
error.SystemResources => unreachable, // We are deallocating resources.
error.FileLocksNotSupported => unreachable, // We already got the lock.
error.Unexpected => unreachable, // Resource deallocation must succeed.
};
}
}
 
/// Attempts to obtain a lock, returning `true` if the lock is
/// obtained, and `false` if there was an existing incompatible lock held.
/// A process may hold only one type of lock (shared or exclusive) on
/// a file. When a process terminates in any way, the lock is released.
///
/// Assumes the file is unlocked.
///
/// TODO: integrate with async I/O
pub fn tryLock(file: File, l: Lock) LockError!bool {
if (is_windows) {
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
const exclusive = switch (l) {
.none => return,
.shared => false,
.exclusive => true,
};
windows.LockFile(
file.handle,
null,
null,
null,
&io_status_block,
&range_off,
&range_len,
null,
windows.TRUE, // non-blocking=true
@intFromBool(exclusive),
) catch |err| switch (err) {
error.WouldBlock => return false,
else => |e| return e,
};
} else {
posix.flock(file.handle, switch (l) {
.none => posix.LOCK.UN,
.shared => posix.LOCK.SH | posix.LOCK.NB,
.exclusive => posix.LOCK.EX | posix.LOCK.NB,
}) catch |err| switch (err) {
error.WouldBlock => return false,
else => |e| return e,
};
}
return true;
}
 
/// Assumes the file is already locked in exclusive mode.
/// Atomically modifies the lock to be in shared mode, without releasing it.
///
/// TODO: integrate with async I/O
pub fn downgradeLock(file: File) LockError!void {
if (is_windows) {
// On Windows it works like a semaphore + exclusivity flag. To implement this
// function, we first obtain another lock in shared mode. This changes the
// exclusivity flag, but increments the semaphore to 2. So we follow up with
// an NtUnlockFile which decrements the semaphore but does not modify the
// exclusivity flag.
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
windows.LockFile(
file.handle,
null,
null,
null,
&io_status_block,
&range_off,
&range_len,
null,
windows.TRUE, // non-blocking=true
windows.FALSE, // exclusive=false
) catch |err| switch (err) {
error.WouldBlock => unreachable, // File was not locked in exclusive mode.
else => |e| return e,
};
return windows.UnlockFile(
file.handle,
&io_status_block,
&range_off,
&range_len,
null,
) catch |err| switch (err) {
error.RangeNotLocked => unreachable, // File was not locked.
error.Unexpected => unreachable, // Resource deallocation must succeed.
};
} else {
return posix.flock(file.handle, posix.LOCK.SH | posix.LOCK.NB) catch |err| switch (err) {
error.WouldBlock => unreachable, // File was not locked in exclusive mode.
else => |e| return e,
};
}
}
 
const File = @This();
const std = @import("../std.zig");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
// https://github.com/ziglang/zig/issues/5019
const posix = std.os;
const io = std.io;
const math = std.math;
const assert = std.debug.assert;
const windows = std.os.windows;
const Os = std.builtin.Os;
const maxInt = std.math.maxInt;
const is_windows = builtin.os.tag == .windows;
 
ev/null added: 4428, removed: 4469, total 0
@@ -1,1622 +0,0 @@
const std = @import("../std.zig");
const builtin = @import("builtin");
const os = std.os;
const io = std.io;
const mem = std.mem;
const math = std.math;
const assert = std.debug.assert;
const windows = os.windows;
const Os = std.builtin.Os;
const maxInt = std.math.maxInt;
const is_windows = builtin.os.tag == .windows;
 
pub const File = struct {
/// The OS-specific file descriptor or file handle.
handle: Handle,
 
/// On some systems, such as Linux, file system file descriptors are incapable
/// of non-blocking I/O. This forces us to perform asynchronous I/O on a dedicated thread,
/// to achieve non-blocking file-system I/O. To do this, `File` must be aware of whether
/// it is a file system file descriptor, or, more specifically, whether the I/O is always
/// blocking.
capable_io_mode: io.ModeOverride = io.default_mode,
 
/// Furthermore, even when `std.options.io_mode` is async, it is still sometimes desirable
/// to perform blocking I/O, although not by default. For example, when printing a
/// stack trace to stderr. This field tracks both by acting as an overriding I/O mode.
/// When not building in async I/O mode, the type only has the `.blocking` tag, making
/// it a zero-bit type.
intended_io_mode: io.ModeOverride = io.default_mode,
 
pub const Handle = os.fd_t;
pub const Mode = os.mode_t;
pub const INode = os.ino_t;
pub const Uid = os.uid_t;
pub const Gid = os.gid_t;
 
pub const Kind = enum {
block_device,
character_device,
directory,
named_pipe,
sym_link,
file,
unix_domain_socket,
whiteout,
door,
event_port,
unknown,
};
 
/// This is the default mode given to POSIX operating systems for creating
/// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first,
/// since most people would expect "-rw-r--r--", for example, when using
/// the `touch` command, which would correspond to `0o644`. However, POSIX
/// libc implementations use `0o666` inside `fopen` and then rely on the
/// process-scoped "umask" setting to adjust this number for file creation.
pub const default_mode = switch (builtin.os.tag) {
.windows => 0,
.wasi => 0,
else => 0o666,
};
 
pub const OpenError = error{
SharingViolation,
PathAlreadyExists,
FileNotFound,
AccessDenied,
PipeBusy,
NameTooLong,
/// On Windows, file paths must be valid Unicode.
InvalidUtf8,
/// On Windows, file paths cannot contain these characters:
/// '/', '*', '?', '"', '<', '>', '|'
BadPathName,
Unexpected,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
} || os.OpenError || os.FlockError;
 
pub const OpenMode = enum {
read_only,
write_only,
read_write,
};
 
pub const Lock = enum {
none,
shared,
exclusive,
};
 
pub const OpenFlags = struct {
mode: OpenMode = .read_only,
 
/// Open the file with an advisory lock to coordinate with other processes
/// accessing it at the same time. An exclusive lock will prevent other
/// processes from acquiring a lock. A shared lock will prevent other
/// processes from acquiring a exclusive lock, but does not prevent
/// other process from getting their own shared locks.
///
/// The lock is advisory, except on Linux in very specific circumstances[1].
/// This means that a process that does not respect the locking API can still get access
/// to the file, despite the lock.
///
/// On these operating systems, the lock is acquired atomically with
/// opening the file:
/// * Darwin
/// * DragonFlyBSD
/// * FreeBSD
/// * Haiku
/// * NetBSD
/// * OpenBSD
/// On these operating systems, the lock is acquired via a separate syscall
/// after opening the file:
/// * Linux
/// * Windows
///
/// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt
lock: Lock = .none,
 
/// Sets whether or not to wait until the file is locked to return. If set to true,
/// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file
/// is available to proceed.
/// In async I/O mode, non-blocking at the OS level is
/// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned,
/// and `false` means `error.WouldBlock` is handled by the event loop.
lock_nonblocking: bool = false,
 
/// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even
/// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions
/// related to opening the file, reading, writing, and locking.
intended_io_mode: io.ModeOverride = io.default_mode,
 
/// Set this to allow the opened file to automatically become the
/// controlling TTY for the current process.
allow_ctty: bool = false,
 
pub fn isRead(self: OpenFlags) bool {
return self.mode != .write_only;
}
 
pub fn isWrite(self: OpenFlags) bool {
return self.mode != .read_only;
}
};
 
pub const CreateFlags = struct {
/// Whether the file will be created with read access.
read: bool = false,
 
/// If the file already exists, and is a regular file, and the access
/// mode allows writing, it will be truncated to length 0.
truncate: bool = true,
 
/// Ensures that this open call creates the file, otherwise causes
/// `error.PathAlreadyExists` to be returned.
exclusive: bool = false,
 
/// Open the file with an advisory lock to coordinate with other processes
/// accessing it at the same time. An exclusive lock will prevent other
/// processes from acquiring a lock. A shared lock will prevent other
/// processes from acquiring a exclusive lock, but does not prevent
/// other process from getting their own shared locks.
///
/// The lock is advisory, except on Linux in very specific circumstances[1].
/// This means that a process that does not respect the locking API can still get access
/// to the file, despite the lock.
///
/// On these operating systems, the lock is acquired atomically with
/// opening the file:
/// * Darwin
/// * DragonFlyBSD
/// * FreeBSD
/// * Haiku
/// * NetBSD
/// * OpenBSD
/// On these operating systems, the lock is acquired via a separate syscall
/// after opening the file:
/// * Linux
/// * Windows
///
/// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt
lock: Lock = .none,
 
/// Sets whether or not to wait until the file is locked to return. If set to true,
/// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file
/// is available to proceed.
/// In async I/O mode, non-blocking at the OS level is
/// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned,
/// and `false` means `error.WouldBlock` is handled by the event loop.
lock_nonblocking: bool = false,
 
/// For POSIX systems this is the file system mode the file will
/// be created with. On other systems this is always 0.
mode: Mode = default_mode,
 
/// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even
/// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions
/// related to opening the file, reading, writing, and locking.
intended_io_mode: io.ModeOverride = io.default_mode,
};
 
/// Upon success, the stream is in an uninitialized state. To continue using it,
/// you must use the open() function.
pub fn close(self: File) void {
if (is_windows) {
windows.CloseHandle(self.handle);
} else if (self.capable_io_mode != self.intended_io_mode) {
std.event.Loop.instance.?.close(self.handle);
} else {
os.close(self.handle);
}
}
 
pub const SyncError = os.SyncError;
 
/// Blocks until all pending file contents and metadata modifications
/// for the file have been synchronized with the underlying filesystem.
///
/// Note that this does not ensure that metadata for the
/// directory containing the file has also reached disk.
pub fn sync(self: File) SyncError!void {
return os.fsync(self.handle);
}
 
/// Test whether the file refers to a terminal.
/// See also `supportsAnsiEscapeCodes`.
pub fn isTty(self: File) bool {
return os.isatty(self.handle);
}
 
/// Test whether ANSI escape codes will be treated as such.
pub fn supportsAnsiEscapeCodes(self: File) bool {
if (builtin.os.tag == .windows) {
var console_mode: os.windows.DWORD = 0;
if (os.windows.kernel32.GetConsoleMode(self.handle, &console_mode) != 0) {
if (console_mode & os.windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true;
}
 
return os.isCygwinPty(self.handle);
}
if (builtin.os.tag == .wasi) {
// WASI sanitizes stdout when fd is a tty so ANSI escape codes
// will not be interpreted as actual cursor commands, and
// stderr is always sanitized.
return false;
}
if (self.isTty()) {
if (self.handle == os.STDOUT_FILENO or self.handle == os.STDERR_FILENO) {
if (os.getenvZ("TERM")) |term| {
if (std.mem.eql(u8, term, "dumb"))
return false;
}
}
return true;
}
return false;
}
 
pub const SetEndPosError = os.TruncateError;
 
/// Shrinks or expands the file.
/// The file offset after this call is left unchanged.
pub fn setEndPos(self: File, length: u64) SetEndPosError!void {
try os.ftruncate(self.handle, length);
}
 
pub const SeekError = os.SeekError;
 
/// Repositions read/write file offset relative to the current offset.
/// TODO: integrate with async I/O
pub fn seekBy(self: File, offset: i64) SeekError!void {
return os.lseek_CUR(self.handle, offset);
}
 
/// Repositions read/write file offset relative to the end.
/// TODO: integrate with async I/O
pub fn seekFromEnd(self: File, offset: i64) SeekError!void {
return os.lseek_END(self.handle, offset);
}
 
/// Repositions read/write file offset relative to the beginning.
/// TODO: integrate with async I/O
pub fn seekTo(self: File, offset: u64) SeekError!void {
return os.lseek_SET(self.handle, offset);
}
 
pub const GetSeekPosError = os.SeekError || os.FStatError;
 
/// TODO: integrate with async I/O
pub fn getPos(self: File) GetSeekPosError!u64 {
return os.lseek_CUR_get(self.handle);
}
 
/// TODO: integrate with async I/O
pub fn getEndPos(self: File) GetSeekPosError!u64 {
if (builtin.os.tag == .windows) {
return windows.GetFileSizeEx(self.handle);
}
return (try self.stat()).size;
}
 
pub const ModeError = os.FStatError;
 
/// TODO: integrate with async I/O
pub fn mode(self: File) ModeError!Mode {
if (builtin.os.tag == .windows) {
return 0;
}
return (try self.stat()).mode;
}
 
pub const Stat = struct {
/// A number that the system uses to point to the file metadata. This
/// number is not guaranteed to be unique across time, as some file
/// systems may reuse an inode after its file has been deleted. Some
/// systems may change the inode of a file over time.
///
/// On Linux, the inode is a structure that stores the metadata, and
/// the inode _number_ is what you see here: the index number of the
/// inode.
///
/// The FileIndex on Windows is similar. It is a number for a file that
/// is unique to each filesystem.
inode: INode,
size: u64,
/// This is available on POSIX systems and is always 0 otherwise.
mode: Mode,
kind: Kind,
 
/// Access time in nanoseconds, relative to UTC 1970-01-01.
atime: i128,
/// Last modification time in nanoseconds, relative to UTC 1970-01-01.
mtime: i128,
/// Creation time in nanoseconds, relative to UTC 1970-01-01.
ctime: i128,
 
pub fn fromSystem(st: os.system.Stat) Stat {
const atime = st.atime();
const mtime = st.mtime();
const ctime = st.ctime();
const kind: Kind = if (builtin.os.tag == .wasi and !builtin.link_libc) switch (st.filetype) {
.BLOCK_DEVICE => .block_device,
.CHARACTER_DEVICE => .character_device,
.DIRECTORY => .directory,
.SYMBOLIC_LINK => .sym_link,
.REGULAR_FILE => .file,
.SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,
else => .unknown,
} else blk: {
const m = st.mode & os.S.IFMT;
switch (m) {
os.S.IFBLK => break :blk .block_device,
os.S.IFCHR => break :blk .character_device,
os.S.IFDIR => break :blk .directory,
os.S.IFIFO => break :blk .named_pipe,
os.S.IFLNK => break :blk .sym_link,
os.S.IFREG => break :blk .file,
os.S.IFSOCK => break :blk .unix_domain_socket,
else => {},
}
if (builtin.os.tag.isSolarish()) switch (m) {
os.S.IFDOOR => break :blk .door,
os.S.IFPORT => break :blk .event_port,
else => {},
};
 
break :blk .unknown;
};
 
return Stat{
.inode = st.ino,
.size = @as(u64, @bitCast(st.size)),
.mode = st.mode,
.kind = kind,
.atime = @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec,
.mtime = @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec,
.ctime = @as(i128, ctime.tv_sec) * std.time.ns_per_s + ctime.tv_nsec,
};
}
};
 
pub const StatError = os.FStatError;
 
/// TODO: integrate with async I/O
pub fn stat(self: File) StatError!Stat {
if (builtin.os.tag == .windows) {
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
var info: windows.FILE_ALL_INFORMATION = undefined;
const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation);
switch (rc) {
.SUCCESS => {},
// Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
// size provided. This is treated as success because the type of variable-length information that this would be relevant for
// (name, volume name, etc) we don't care about.
.BUFFER_OVERFLOW => {},
.INVALID_PARAMETER => unreachable,
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
}
return Stat{
.inode = info.InternalInformation.IndexNumber,
.size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
.mode = 0,
.kind = if (info.StandardInformation.Directory == 0) .file else .directory,
.atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
.mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
.ctime = windows.fromSysTime(info.BasicInformation.CreationTime),
};
}
 
const st = try os.fstat(self.handle);
return Stat.fromSystem(st);
}
 
pub const ChmodError = std.os.FChmodError;
 
/// Changes the mode of the file.
/// The process must have the correct privileges in order to do this
/// successfully, or must have the effective user ID matching the owner
/// of the file.
pub fn chmod(self: File, new_mode: Mode) ChmodError!void {
try os.fchmod(self.handle, new_mode);
}
 
pub const ChownError = std.os.FChownError;
 
/// Changes the owner and group of the file.
/// The process must have the correct privileges in order to do this
/// successfully. The group may be changed by the owner of the file to
/// any group of which the owner is a member. If the owner or group is
/// specified as `null`, the ID is not changed.
pub fn chown(self: File, owner: ?Uid, group: ?Gid) ChownError!void {
try os.fchown(self.handle, owner, group);
}
 
/// Cross-platform representation of permissions on a file.
/// The `readonly` and `setReadonly` are the only methods available across all platforms.
/// Platform-specific functionality is available through the `inner` field.
pub const Permissions = struct {
/// You may use the `inner` field to use platform-specific functionality
inner: switch (builtin.os.tag) {
.windows => PermissionsWindows,
else => PermissionsUnix,
},
 
const Self = @This();
 
/// Returns `true` if permissions represent an unwritable file.
/// On Unix, `true` is returned only if no class has write permissions.
pub fn readOnly(self: Self) bool {
return self.inner.readOnly();
}
 
/// Sets whether write permissions are provided.
/// On Unix, this affects *all* classes. If this is undesired, use `unixSet`.
/// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
pub fn setReadOnly(self: *Self, read_only: bool) void {
self.inner.setReadOnly(read_only);
}
};
 
pub const PermissionsWindows = struct {
attributes: os.windows.DWORD,
 
const Self = @This();
 
/// Returns `true` if permissions represent an unwritable file.
pub fn readOnly(self: Self) bool {
return self.attributes & os.windows.FILE_ATTRIBUTE_READONLY != 0;
}
 
/// Sets whether write permissions are provided.
/// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
pub fn setReadOnly(self: *Self, read_only: bool) void {
if (read_only) {
self.attributes |= os.windows.FILE_ATTRIBUTE_READONLY;
} else {
self.attributes &= ~@as(os.windows.DWORD, os.windows.FILE_ATTRIBUTE_READONLY);
}
}
};
 
pub const PermissionsUnix = struct {
mode: Mode,
 
const Self = @This();
 
/// Returns `true` if permissions represent an unwritable file.
/// `true` is returned only if no class has write permissions.
pub fn readOnly(self: Self) bool {
return self.mode & 0o222 == 0;
}
 
/// Sets whether write permissions are provided.
/// This affects *all* classes. If this is undesired, use `unixSet`.
/// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
pub fn setReadOnly(self: *Self, read_only: bool) void {
if (read_only) {
self.mode &= ~@as(Mode, 0o222);
} else {
self.mode |= @as(Mode, 0o222);
}
}
 
pub const Class = enum(u2) {
user = 2,
group = 1,
other = 0,
};
 
pub const Permission = enum(u3) {
read = 0o4,
write = 0o2,
execute = 0o1,
};
 
/// Returns `true` if the chosen class has the selected permission.
/// This method is only available on Unix platforms.
pub fn unixHas(self: Self, class: Class, permission: Permission) bool {
const mask = @as(Mode, @intFromEnum(permission)) << @as(u3, @intFromEnum(class)) * 3;
return self.mode & mask != 0;
}
 
/// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged.
/// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
pub fn unixSet(self: *Self, class: Class, permissions: struct {
read: ?bool = null,
write: ?bool = null,
execute: ?bool = null,
}) void {
const shift = @as(u3, @intFromEnum(class)) * 3;
if (permissions.read) |r| {
if (r) {
self.mode |= @as(Mode, 0o4) << shift;
} else {
self.mode &= ~(@as(Mode, 0o4) << shift);
}
}
if (permissions.write) |w| {
if (w) {
self.mode |= @as(Mode, 0o2) << shift;
} else {
self.mode &= ~(@as(Mode, 0o2) << shift);
}
}
if (permissions.execute) |x| {
if (x) {
self.mode |= @as(Mode, 0o1) << shift;
} else {
self.mode &= ~(@as(Mode, 0o1) << shift);
}
}
}
 
/// Returns a `Permissions` struct representing the permissions from the passed mode.
pub fn unixNew(new_mode: Mode) Self {
return Self{
.mode = new_mode,
};
}
};
 
pub const SetPermissionsError = ChmodError;
 
/// Sets permissions according to the provided `Permissions` struct.
/// This method is *NOT* available on WASI
pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void {
switch (builtin.os.tag) {
.windows => {
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
var info = windows.FILE_BASIC_INFORMATION{
.CreationTime = 0,
.LastAccessTime = 0,
.LastWriteTime = 0,
.ChangeTime = 0,
.FileAttributes = permissions.inner.attributes,
};
const rc = windows.ntdll.NtSetInformationFile(
self.handle,
&io_status_block,
&info,
@sizeOf(windows.FILE_BASIC_INFORMATION),
.FileBasicInformation,
);
switch (rc) {
.SUCCESS => return,
.INVALID_HANDLE => unreachable,
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
}
},
.wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod
else => {
try self.chmod(permissions.inner.mode);
},
}
}
 
/// Cross-platform representation of file metadata.
/// Platform-specific functionality is available through the `inner` field.
pub const Metadata = struct {
/// You may use the `inner` field to use platform-specific functionality
inner: switch (builtin.os.tag) {
.windows => MetadataWindows,
.linux => MetadataLinux,
else => MetadataUnix,
},
 
const Self = @This();
 
/// Returns the size of the file
pub fn size(self: Self) u64 {
return self.inner.size();
}
 
/// Returns a `Permissions` struct, representing the permissions on the file
pub fn permissions(self: Self) Permissions {
return self.inner.permissions();
}
 
/// Returns the `Kind` of file.
/// On Windows, can only return: `.file`, `.directory`, `.sym_link` or `.unknown`
pub fn kind(self: Self) Kind {
return self.inner.kind();
}
 
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
pub fn accessed(self: Self) i128 {
return self.inner.accessed();
}
 
/// Returns the time the file was modified in nanoseconds since UTC 1970-01-01
pub fn modified(self: Self) i128 {
return self.inner.modified();
}
 
/// Returns the time the file was created in nanoseconds since UTC 1970-01-01
/// On Windows, this cannot return null
/// On Linux, this returns null if the filesystem does not support creation times, or if the kernel is older than 4.11
/// On Unices, this returns null if the filesystem or OS does not support creation times
/// On MacOS, this returns the ctime if the filesystem does not support creation times; this is insanity, and yet another reason to hate on Apple
pub fn created(self: Self) ?i128 {
return self.inner.created();
}
};
 
pub const MetadataUnix = struct {
stat: os.Stat,
 
const Self = @This();
 
/// Returns the size of the file
pub fn size(self: Self) u64 {
return @as(u64, @intCast(self.stat.size));
}
 
/// Returns a `Permissions` struct, representing the permissions on the file
pub fn permissions(self: Self) Permissions {
return Permissions{ .inner = PermissionsUnix{ .mode = self.stat.mode } };
}
 
/// Returns the `Kind` of the file
pub fn kind(self: Self) Kind {
if (builtin.os.tag == .wasi and !builtin.link_libc) return switch (self.stat.filetype) {
.BLOCK_DEVICE => .block_device,
.CHARACTER_DEVICE => .character_device,
.DIRECTORY => .directory,
.SYMBOLIC_LINK => .sym_link,
.REGULAR_FILE => .file,
.SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,
else => .unknown,
};
 
const m = self.stat.mode & os.S.IFMT;
 
switch (m) {
os.S.IFBLK => return .block_device,
os.S.IFCHR => return .character_device,
os.S.IFDIR => return .directory,
os.S.IFIFO => return .named_pipe,
os.S.IFLNK => return .sym_link,
os.S.IFREG => return .file,
os.S.IFSOCK => return .unix_domain_socket,
else => {},
}
 
if (builtin.os.tag.isSolarish()) switch (m) {
os.S.IFDOOR => return .door,
os.S.IFPORT => return .event_port,
else => {},
};
 
return .unknown;
}
 
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
pub fn accessed(self: Self) i128 {
const atime = self.stat.atime();
return @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec;
}
 
/// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01
pub fn modified(self: Self) i128 {
const mtime = self.stat.mtime();
return @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec;
}
 
/// Returns the time the file was created in nanoseconds since UTC 1970-01-01.
/// Returns null if this is not supported by the OS or filesystem
pub fn created(self: Self) ?i128 {
if (!@hasDecl(@TypeOf(self.stat), "birthtime")) return null;
const birthtime = self.stat.birthtime();
 
// If the filesystem doesn't support this the value *should* be:
// On FreeBSD: tv_nsec = 0, tv_sec = -1
// On NetBSD and OpenBSD: tv_nsec = 0, tv_sec = 0
// On MacOS, it is set to ctime -- we cannot detect this!!
switch (builtin.os.tag) {
.freebsd => if (birthtime.tv_sec == -1 and birthtime.tv_nsec == 0) return null,
.netbsd, .openbsd => if (birthtime.tv_sec == 0 and birthtime.tv_nsec == 0) return null,
.macos => {},
else => @compileError("Creation time detection not implemented for OS"),
}
 
return @as(i128, birthtime.tv_sec) * std.time.ns_per_s + birthtime.tv_nsec;
}
};
 
/// `MetadataUnix`, but using Linux's `statx` syscall.
/// On Linux versions below 4.11, `statx` will be filled with data from stat.
pub const MetadataLinux = struct {
statx: os.linux.Statx,
 
const Self = @This();
 
/// Returns the size of the file
pub fn size(self: Self) u64 {
return self.statx.size;
}
 
/// Returns a `Permissions` struct, representing the permissions on the file
pub fn permissions(self: Self) Permissions {
return Permissions{ .inner = PermissionsUnix{ .mode = self.statx.mode } };
}
 
/// Returns the `Kind` of the file
pub fn kind(self: Self) Kind {
const m = self.statx.mode & os.S.IFMT;
 
switch (m) {
os.S.IFBLK => return .block_device,
os.S.IFCHR => return .character_device,
os.S.IFDIR => return .directory,
os.S.IFIFO => return .named_pipe,
os.S.IFLNK => return .sym_link,
os.S.IFREG => return .file,
os.S.IFSOCK => return .unix_domain_socket,
else => {},
}
 
return .unknown;
}
 
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
pub fn accessed(self: Self) i128 {
return @as(i128, self.statx.atime.tv_sec) * std.time.ns_per_s + self.statx.atime.tv_nsec;
}
 
/// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01
pub fn modified(self: Self) i128 {
return @as(i128, self.statx.mtime.tv_sec) * std.time.ns_per_s + self.statx.mtime.tv_nsec;
}
 
/// Returns the time the file was created in nanoseconds since UTC 1970-01-01.
/// Returns null if this is not supported by the filesystem, or on kernels before than version 4.11
pub fn created(self: Self) ?i128 {
if (self.statx.mask & os.linux.STATX_BTIME == 0) return null;
return @as(i128, self.statx.btime.tv_sec) * std.time.ns_per_s + self.statx.btime.tv_nsec;
}
};
 
pub const MetadataWindows = struct {
attributes: windows.DWORD,
reparse_tag: windows.DWORD,
_size: u64,
access_time: i128,
modified_time: i128,
creation_time: i128,
 
const Self = @This();
 
/// Returns the size of the file
pub fn size(self: Self) u64 {
return self._size;
}
 
/// Returns a `Permissions` struct, representing the permissions on the file
pub fn permissions(self: Self) Permissions {
return Permissions{ .inner = PermissionsWindows{ .attributes = self.attributes } };
}
 
/// Returns the `Kind` of the file.
/// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown`
pub fn kind(self: Self) Kind {
if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
if (self.reparse_tag & 0x20000000 != 0) {
return .sym_link;
}
} else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
return .directory;
} else {
return .file;
}
return .unknown;
}
 
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
pub fn accessed(self: Self) i128 {
return self.access_time;
}
 
/// Returns the time the file was modified in nanoseconds since UTC 1970-01-01
pub fn modified(self: Self) i128 {
return self.modified_time;
}
 
/// Returns the time the file was created in nanoseconds since UTC 1970-01-01.
/// This never returns null, only returning an optional for compatibility with other OSes
pub fn created(self: Self) ?i128 {
return self.creation_time;
}
};
 
pub const MetadataError = os.FStatError;
 
pub fn metadata(self: File) MetadataError!Metadata {
return Metadata{
.inner = switch (builtin.os.tag) {
.windows => blk: {
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
var info: windows.FILE_ALL_INFORMATION = undefined;
 
const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation);
switch (rc) {
.SUCCESS => {},
// Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
// size provided. This is treated as success because the type of variable-length information that this would be relevant for
// (name, volume name, etc) we don't care about.
.BUFFER_OVERFLOW => {},
.INVALID_PARAMETER => unreachable,
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
}
 
const reparse_tag: windows.DWORD = reparse_blk: {
if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]);
const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0]));
break :reparse_blk reparse_struct.ReparseTag;
}
break :reparse_blk 0;
};
 
break :blk MetadataWindows{
.attributes = info.BasicInformation.FileAttributes,
.reparse_tag = reparse_tag,
._size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
.access_time = windows.fromSysTime(info.BasicInformation.LastAccessTime),
.modified_time = windows.fromSysTime(info.BasicInformation.LastWriteTime),
.creation_time = windows.fromSysTime(info.BasicInformation.CreationTime),
};
},
.linux => blk: {
var stx = mem.zeroes(os.linux.Statx);
const rcx = os.linux.statx(self.handle, "\x00", os.linux.AT.EMPTY_PATH, os.linux.STATX_TYPE | os.linux.STATX_MODE | os.linux.STATX_ATIME | os.linux.STATX_MTIME | os.linux.STATX_BTIME, &stx);
 
switch (os.errno(rcx)) {
.SUCCESS => {},
// NOSYS happens when `statx` is unsupported, which is the case on kernel versions before 4.11
// Here, we call `fstat` and fill `stx` with the data we need
.NOSYS => {
const st = try os.fstat(self.handle);
 
stx.mode = @as(u16, @intCast(st.mode));
 
// Hacky conversion from timespec to statx_timestamp
stx.atime = std.mem.zeroes(os.linux.statx_timestamp);
stx.atime.tv_sec = st.atim.tv_sec;
stx.atime.tv_nsec = @as(u32, @intCast(st.atim.tv_nsec)); // Guaranteed to succeed (tv_nsec is always below 10^9)
 
stx.mtime = std.mem.zeroes(os.linux.statx_timestamp);
stx.mtime.tv_sec = st.mtim.tv_sec;
stx.mtime.tv_nsec = @as(u32, @intCast(st.mtim.tv_nsec));
 
stx.mask = os.linux.STATX_BASIC_STATS | os.linux.STATX_MTIME;
},
.BADF => unreachable,
.FAULT => unreachable,
.NOMEM => return error.SystemResources,
else => |err| return os.unexpectedErrno(err),
}
 
break :blk MetadataLinux{
.statx = stx,
};
},
else => blk: {
const st = try os.fstat(self.handle);
break :blk MetadataUnix{
.stat = st,
};
},
},
};
}
 
pub const UpdateTimesError = os.FutimensError || windows.SetFileTimeError;
 
/// The underlying file system may have a different granularity than nanoseconds,
/// and therefore this function cannot guarantee any precision will be stored.
/// Further, the maximum value is limited by the system ABI. When a value is provided
/// that exceeds this range, the value is clamped to the maximum.
/// TODO: integrate with async I/O
pub fn updateTimes(
self: File,
/// access timestamp in nanoseconds
atime: i128,
/// last modification timestamp in nanoseconds
mtime: i128,
) UpdateTimesError!void {
if (builtin.os.tag == .windows) {
const atime_ft = windows.nanoSecondsToFileTime(atime);
const mtime_ft = windows.nanoSecondsToFileTime(mtime);
return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft);
}
const times = [2]os.timespec{
os.timespec{
.tv_sec = math.cast(isize, @divFloor(atime, std.time.ns_per_s)) orelse maxInt(isize),
.tv_nsec = math.cast(isize, @mod(atime, std.time.ns_per_s)) orelse maxInt(isize),
},
os.timespec{
.tv_sec = math.cast(isize, @divFloor(mtime, std.time.ns_per_s)) orelse maxInt(isize),
.tv_nsec = math.cast(isize, @mod(mtime, std.time.ns_per_s)) orelse maxInt(isize),
},
};
try os.futimens(self.handle, &times);
}
 
/// Reads all the bytes from the current position to the end of the file.
/// On success, caller owns returned buffer.
/// If the file is larger than `max_bytes`, returns `error.FileTooBig`.
pub fn readToEndAlloc(self: File, allocator: mem.Allocator, max_bytes: usize) ![]u8 {
return self.readToEndAllocOptions(allocator, max_bytes, null, @alignOf(u8), null);
}
 
/// Reads all the bytes from the current position to the end of the file.
/// On success, caller owns returned buffer.
/// If the file is larger than `max_bytes`, returns `error.FileTooBig`.
/// If `size_hint` is specified the initial buffer size is calculated using
/// that value, otherwise an arbitrary value is used instead.
/// Allows specifying alignment and a sentinel value.
pub fn readToEndAllocOptions(
self: File,
allocator: mem.Allocator,
max_bytes: usize,
size_hint: ?usize,
comptime alignment: u29,
comptime optional_sentinel: ?u8,
) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) {
// If no size hint is provided fall back to the size=0 code path
const size = size_hint orelse 0;
 
// The file size returned by stat is used as hint to set the buffer
// size. If the reported size is zero, as it happens on Linux for files
// in /proc, a small buffer is allocated instead.
const initial_cap = (if (size > 0) size else 1024) + @intFromBool(optional_sentinel != null);
var array_list = try std.ArrayListAligned(u8, alignment).initCapacity(allocator, initial_cap);
defer array_list.deinit();
 
self.reader().readAllArrayListAligned(alignment, &array_list, max_bytes) catch |err| switch (err) {
error.StreamTooLong => return error.FileTooBig,
else => |e| return e,
};
 
if (optional_sentinel) |sentinel| {
return try array_list.toOwnedSliceSentinel(sentinel);
} else {
return try array_list.toOwnedSlice();
}
}
 
pub const ReadError = os.ReadError;
pub const PReadError = os.PReadError;
 
pub fn read(self: File, buffer: []u8) ReadError!usize {
if (is_windows) {
return windows.ReadFile(self.handle, buffer, null, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return os.read(self.handle, buffer);
} else {
return std.event.Loop.instance.?.read(self.handle, buffer, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it
/// means the file reached the end. Reaching the end of a file is not an error condition.
pub fn readAll(self: File, buffer: []u8) ReadError!usize {
var index: usize = 0;
while (index != buffer.len) {
const amt = try self.read(buffer[index..]);
if (amt == 0) break;
index += amt;
}
return index;
}
 
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize {
if (is_windows) {
return windows.ReadFile(self.handle, buffer, offset, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return os.pread(self.handle, buffer, offset);
} else {
return std.event.Loop.instance.?.pread(self.handle, buffer, offset, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it
/// means the file reached the end. Reaching the end of a file is not an error condition.
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize {
var index: usize = 0;
while (index != buffer.len) {
const amt = try self.pread(buffer[index..], offset + index);
if (amt == 0) break;
index += amt;
}
return index;
}
 
/// See https://github.com/ziglang/zig/issues/7699
pub fn readv(self: File, iovecs: []const os.iovec) ReadError!usize {
if (is_windows) {
// TODO improve this to use ReadFileScatter
if (iovecs.len == 0) return @as(usize, 0);
const first = iovecs[0];
return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return os.readv(self.handle, iovecs);
} else {
return std.event.Loop.instance.?.readv(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// Returns the number of bytes read. If the number read is smaller than the total bytes
/// from all the buffers, it means the file reached the end. Reaching the end of a file
/// is not an error condition.
///
/// The `iovecs` parameter is mutable because:
/// * This function needs to mutate the fields in order to handle partial
/// reads from the underlying OS layer.
/// * The OS layer expects pointer addresses to be inside the application's address space
/// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer
/// addresses when the length is zero. So this function modifies the iov_base fields
/// when the length is zero.
///
/// Related open issue: https://github.com/ziglang/zig/issues/7699
pub fn readvAll(self: File, iovecs: []os.iovec) ReadError!usize {
if (iovecs.len == 0) return 0;
 
// We use the address of this local variable for all zero-length
// vectors so that the OS does not complain that we are giving it
// addresses outside the application's address space.
var garbage: [1]u8 = undefined;
for (iovecs) |*v| {
if (v.iov_len == 0) v.iov_base = &garbage;
}
 
var i: usize = 0;
var off: usize = 0;
while (true) {
var amt = try self.readv(iovecs[i..]);
var eof = amt == 0;
off += amt;
while (amt >= iovecs[i].iov_len) {
amt -= iovecs[i].iov_len;
i += 1;
if (i >= iovecs.len) return off;
eof = false;
}
if (eof) return off;
iovecs[i].iov_base += amt;
iovecs[i].iov_len -= amt;
}
}
 
/// See https://github.com/ziglang/zig/issues/7699
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn preadv(self: File, iovecs: []const os.iovec, offset: u64) PReadError!usize {
if (is_windows) {
// TODO improve this to use ReadFileScatter
if (iovecs.len == 0) return @as(usize, 0);
const first = iovecs[0];
return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return os.preadv(self.handle, iovecs, offset);
} else {
return std.event.Loop.instance.?.preadv(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// Returns the number of bytes read. If the number read is smaller than the total bytes
/// from all the buffers, it means the file reached the end. Reaching the end of a file
/// is not an error condition.
/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
/// order to handle partial reads from the underlying OS layer.
/// See https://github.com/ziglang/zig/issues/7699
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn preadvAll(self: File, iovecs: []os.iovec, offset: u64) PReadError!usize {
if (iovecs.len == 0) return 0;
 
var i: usize = 0;
var off: usize = 0;
while (true) {
var amt = try self.preadv(iovecs[i..], offset + off);
var eof = amt == 0;
off += amt;
while (amt >= iovecs[i].iov_len) {
amt -= iovecs[i].iov_len;
i += 1;
if (i >= iovecs.len) return off;
eof = false;
}
if (eof) return off;
iovecs[i].iov_base += amt;
iovecs[i].iov_len -= amt;
}
}
 
pub const WriteError = os.WriteError;
pub const PWriteError = os.PWriteError;
 
pub fn write(self: File, bytes: []const u8) WriteError!usize {
if (is_windows) {
return windows.WriteFile(self.handle, bytes, null, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return os.write(self.handle, bytes);
} else {
return std.event.Loop.instance.?.write(self.handle, bytes, self.capable_io_mode != self.intended_io_mode);
}
}
 
pub fn writeAll(self: File, bytes: []const u8) WriteError!void {
var index: usize = 0;
while (index < bytes.len) {
index += try self.write(bytes[index..]);
}
}
 
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize {
if (is_windows) {
return windows.WriteFile(self.handle, bytes, offset, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return os.pwrite(self.handle, bytes, offset);
} else {
return std.event.Loop.instance.?.pwrite(self.handle, bytes, offset, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void {
var index: usize = 0;
while (index < bytes.len) {
index += try self.pwrite(bytes[index..], offset + index);
}
}
 
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.net.Stream.writev`.
pub fn writev(self: File, iovecs: []const os.iovec_const) WriteError!usize {
if (is_windows) {
// TODO improve this to use WriteFileScatter
if (iovecs.len == 0) return @as(usize, 0);
const first = iovecs[0];
return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return os.writev(self.handle, iovecs);
} else {
return std.event.Loop.instance.?.writev(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// The `iovecs` parameter is mutable because:
/// * This function needs to mutate the fields in order to handle partial
/// writes from the underlying OS layer.
/// * The OS layer expects pointer addresses to be inside the application's address space
/// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer
/// addresses when the length is zero. So this function modifies the iov_base fields
/// when the length is zero.
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.net.Stream.writevAll`.
pub fn writevAll(self: File, iovecs: []os.iovec_const) WriteError!void {
if (iovecs.len == 0) return;
 
// We use the address of this local variable for all zero-length
// vectors so that the OS does not complain that we are giving it
// addresses outside the application's address space.
var garbage: [1]u8 = undefined;
for (iovecs) |*v| {
if (v.iov_len == 0) v.iov_base = &garbage;
}
 
var i: usize = 0;
while (true) {
var amt = try self.writev(iovecs[i..]);
while (amt >= iovecs[i].iov_len) {
amt -= iovecs[i].iov_len;
i += 1;
if (i >= iovecs.len) return;
}
iovecs[i].iov_base += amt;
iovecs[i].iov_len -= amt;
}
}
 
/// See https://github.com/ziglang/zig/issues/7699
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn pwritev(self: File, iovecs: []os.iovec_const, offset: u64) PWriteError!usize {
if (is_windows) {
// TODO improve this to use WriteFileScatter
if (iovecs.len == 0) return @as(usize, 0);
const first = iovecs[0];
return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode);
}
 
if (self.intended_io_mode == .blocking) {
return os.pwritev(self.handle, iovecs, offset);
} else {
return std.event.Loop.instance.?.pwritev(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode);
}
}
 
/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
/// order to handle partial writes from the underlying OS layer.
/// See https://github.com/ziglang/zig/issues/7699
/// On Windows, this function currently does alter the file pointer.
/// https://github.com/ziglang/zig/issues/12783
pub fn pwritevAll(self: File, iovecs: []os.iovec_const, offset: u64) PWriteError!void {
if (iovecs.len == 0) return;
 
var i: usize = 0;
var off: u64 = 0;
while (true) {
var amt = try self.pwritev(iovecs[i..], offset + off);
off += amt;
while (amt >= iovecs[i].iov_len) {
amt -= iovecs[i].iov_len;
i += 1;
if (i >= iovecs.len) return;
}
iovecs[i].iov_base += amt;
iovecs[i].iov_len -= amt;
}
}
 
pub const CopyRangeError = os.CopyFileRangeError;
 
pub fn copyRange(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 {
const adjusted_len = math.cast(usize, len) orelse math.maxInt(usize);
const result = try os.copy_file_range(in.handle, in_offset, out.handle, out_offset, adjusted_len, 0);
return result;
}
 
/// Returns the number of bytes copied. If the number read is smaller than `buffer.len`, it
/// means the in file reached the end. Reaching the end of a file is not an error condition.
pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 {
var total_bytes_copied: u64 = 0;
var in_off = in_offset;
var out_off = out_offset;
while (total_bytes_copied < len) {
const amt_copied = try copyRange(in, in_off, out, out_off, len - total_bytes_copied);
if (amt_copied == 0) return total_bytes_copied;
total_bytes_copied += amt_copied;
in_off += amt_copied;
out_off += amt_copied;
}
return total_bytes_copied;
}
 
pub const WriteFileOptions = struct {
in_offset: u64 = 0,
 
/// `null` means the entire file. `0` means no bytes from the file.
/// When this is `null`, trailers must be sent in a separate writev() call
/// due to a flaw in the BSD sendfile API. Other operating systems, such as
/// Linux, already do this anyway due to API limitations.
/// If the size of the source file is known, passing the size here will save one syscall.
in_len: ?u64 = null,
 
headers_and_trailers: []os.iovec_const = &[0]os.iovec_const{},
 
/// The trailer count is inferred from `headers_and_trailers.len - header_count`
header_count: usize = 0,
};
 
pub const WriteFileError = ReadError || error{EndOfStream} || WriteError;
 
pub fn writeFileAll(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void {
return self.writeFileAllSendfile(in_file, args) catch |err| switch (err) {
error.Unseekable,
error.FastOpenAlreadyInProgress,
error.MessageTooBig,
error.FileDescriptorNotASocket,
error.NetworkUnreachable,
error.NetworkSubsystemFailed,
=> return self.writeFileAllUnseekable(in_file, args),
 
else => |e| return e,
};
}
 
/// Does not try seeking in either of the File parameters.
/// See `writeFileAll` as an alternative to calling this.
pub fn writeFileAllUnseekable(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void {
const headers = args.headers_and_trailers[0..args.header_count];
const trailers = args.headers_and_trailers[args.header_count..];
 
try self.writevAll(headers);
 
try in_file.reader().skipBytes(args.in_offset, .{ .buf_size = 4096 });
 
var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
if (args.in_len) |len| {
var stream = std.io.limitedReader(in_file.reader(), len);
try fifo.pump(stream.reader(), self.writer());
} else {
try fifo.pump(in_file.reader(), self.writer());
}
 
try self.writevAll(trailers);
}
 
/// Low level function which can fail for OS-specific reasons.
/// See `writeFileAll` as an alternative to calling this.
/// TODO integrate with async I/O
fn writeFileAllSendfile(self: File, in_file: File, args: WriteFileOptions) os.SendFileError!void {
const count = blk: {
if (args.in_len) |l| {
if (l == 0) {
return self.writevAll(args.headers_and_trailers);
} else {
break :blk l;
}
} else {
break :blk 0;
}
};
const headers = args.headers_and_trailers[0..args.header_count];
const trailers = args.headers_and_trailers[args.header_count..];
const zero_iovec = &[0]os.iovec_const{};
// When reading the whole file, we cannot put the trailers in the sendfile() syscall,
// because we have no way to determine whether a partial write is past the end of the file or not.
const trls = if (count == 0) zero_iovec else trailers;
const offset = args.in_offset;
const out_fd = self.handle;
const in_fd = in_file.handle;
const flags = 0;
var amt: usize = 0;
hdrs: {
var i: usize = 0;
while (i < headers.len) {
amt = try os.sendfile(out_fd, in_fd, offset, count, headers[i..], trls, flags);
while (amt >= headers[i].iov_len) {
amt -= headers[i].iov_len;
i += 1;
if (i >= headers.len) break :hdrs;
}
headers[i].iov_base += amt;
headers[i].iov_len -= amt;
}
}
if (count == 0) {
var off: u64 = amt;
while (true) {
amt = try os.sendfile(out_fd, in_fd, offset + off, 0, zero_iovec, zero_iovec, flags);
if (amt == 0) break;
off += amt;
}
} else {
var off: u64 = amt;
while (off < count) {
amt = try os.sendfile(out_fd, in_fd, offset + off, count - off, zero_iovec, trailers, flags);
off += amt;
}
amt = @as(usize, @intCast(off - count));
}
var i: usize = 0;
while (i < trailers.len) {
while (amt >= trailers[i].iov_len) {
amt -= trailers[i].iov_len;
i += 1;
if (i >= trailers.len) return;
}
trailers[i].iov_base += amt;
trailers[i].iov_len -= amt;
amt = try os.writev(self.handle, trailers[i..]);
}
}
 
pub const Reader = io.Reader(File, ReadError, read);
 
pub fn reader(file: File) Reader {
return .{ .context = file };
}
 
pub const Writer = io.Writer(File, WriteError, write);
 
pub fn writer(file: File) Writer {
return .{ .context = file };
}
 
pub const SeekableStream = io.SeekableStream(
File,
SeekError,
GetSeekPosError,
seekTo,
seekBy,
getPos,
getEndPos,
);
 
pub fn seekableStream(file: File) SeekableStream {
return .{ .context = file };
}
 
const range_off: windows.LARGE_INTEGER = 0;
const range_len: windows.LARGE_INTEGER = 1;
 
pub const LockError = error{
SystemResources,
FileLocksNotSupported,
} || os.UnexpectedError;
 
/// Blocks when an incompatible lock is held by another process.
/// A process may hold only one type of lock (shared or exclusive) on
/// a file. When a process terminates in any way, the lock is released.
///
/// Assumes the file is unlocked.
///
/// TODO: integrate with async I/O
pub fn lock(file: File, l: Lock) LockError!void {
if (is_windows) {
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
const exclusive = switch (l) {
.none => return,
.shared => false,
.exclusive => true,
};
return windows.LockFile(
file.handle,
null,
null,
null,
&io_status_block,
&range_off,
&range_len,
null,
windows.FALSE, // non-blocking=false
@intFromBool(exclusive),
) catch |err| switch (err) {
error.WouldBlock => unreachable, // non-blocking=false
else => |e| return e,
};
} else {
return os.flock(file.handle, switch (l) {
.none => os.LOCK.UN,
.shared => os.LOCK.SH,
.exclusive => os.LOCK.EX,
}) catch |err| switch (err) {
error.WouldBlock => unreachable, // non-blocking=false
else => |e| return e,
};
}
}
 
/// Assumes the file is locked.
pub fn unlock(file: File) void {
if (is_windows) {
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
return windows.UnlockFile(
file.handle,
&io_status_block,
&range_off,
&range_len,
null,
) catch |err| switch (err) {
error.RangeNotLocked => unreachable, // Function assumes unlocked.
error.Unexpected => unreachable, // Resource deallocation must succeed.
};
} else {
return os.flock(file.handle, os.LOCK.UN) catch |err| switch (err) {
error.WouldBlock => unreachable, // unlocking can't block
error.SystemResources => unreachable, // We are deallocating resources.
error.FileLocksNotSupported => unreachable, // We already got the lock.
error.Unexpected => unreachable, // Resource deallocation must succeed.
};
}
}
 
/// Attempts to obtain a lock, returning `true` if the lock is
/// obtained, and `false` if there was an existing incompatible lock held.
/// A process may hold only one type of lock (shared or exclusive) on
/// a file. When a process terminates in any way, the lock is released.
///
/// Assumes the file is unlocked.
///
/// TODO: integrate with async I/O
pub fn tryLock(file: File, l: Lock) LockError!bool {
if (is_windows) {
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
const exclusive = switch (l) {
.none => return,
.shared => false,
.exclusive => true,
};
windows.LockFile(
file.handle,
null,
null,
null,
&io_status_block,
&range_off,
&range_len,
null,
windows.TRUE, // non-blocking=true
@intFromBool(exclusive),
) catch |err| switch (err) {
error.WouldBlock => return false,
else => |e| return e,
};
} else {
os.flock(file.handle, switch (l) {
.none => os.LOCK.UN,
.shared => os.LOCK.SH | os.LOCK.NB,
.exclusive => os.LOCK.EX | os.LOCK.NB,
}) catch |err| switch (err) {
error.WouldBlock => return false,
else => |e| return e,
};
}
return true;
}
 
/// Assumes the file is already locked in exclusive mode.
/// Atomically modifies the lock to be in shared mode, without releasing it.
///
/// TODO: integrate with async I/O
pub fn downgradeLock(file: File) LockError!void {
if (is_windows) {
// On Windows it works like a semaphore + exclusivity flag. To implement this
// function, we first obtain another lock in shared mode. This changes the
// exclusivity flag, but increments the semaphore to 2. So we follow up with
// an NtUnlockFile which decrements the semaphore but does not modify the
// exclusivity flag.
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
windows.LockFile(
file.handle,
null,
null,
null,
&io_status_block,
&range_off,
&range_len,
null,
windows.TRUE, // non-blocking=true
windows.FALSE, // exclusive=false
) catch |err| switch (err) {
error.WouldBlock => unreachable, // File was not locked in exclusive mode.
else => |e| return e,
};
return windows.UnlockFile(
file.handle,
&io_status_block,
&range_off,
&range_len,
null,
) catch |err| switch (err) {
error.RangeNotLocked => unreachable, // File was not locked.
error.Unexpected => unreachable, // Resource deallocation must succeed.
};
} else {
return os.flock(file.handle, os.LOCK.SH | os.LOCK.NB) catch |err| switch (err) {
error.WouldBlock => unreachable, // File was not locked in exclusive mode.
else => |e| return e,
};
}
}
};
 
lib/std/fs/test.zig added: 4428, removed: 4469, total 0
@@ -8,10 +8,8 @@ const wasi = std.os.wasi;
 
const ArenaAllocator = std.heap.ArenaAllocator;
const Dir = std.fs.Dir;
const IterableDir = std.fs.IterableDir;
const File = std.fs.File;
const tmpDir = testing.tmpDir;
const tmpIterableDir = testing.tmpIterableDir;
 
const PathType = enum {
relative,
@@ -74,19 +72,17 @@ const PathType = enum {
const TestContext = struct {
path_type: PathType,
arena: ArenaAllocator,
tmp: testing.TmpIterableDir,
tmp: testing.TmpDir,
dir: std.fs.Dir,
iterable_dir: std.fs.IterableDir,
transform_fn: *const PathType.TransformFn,
 
pub fn init(path_type: PathType, allocator: mem.Allocator, transform_fn: *const PathType.TransformFn) TestContext {
const tmp = tmpIterableDir(.{});
const tmp = tmpDir(.{ .iterate = true });
return .{
.path_type = path_type,
.arena = ArenaAllocator.init(allocator),
.tmp = tmp,
.dir = tmp.iterable_dir.dir,
.iterable_dir = tmp.iterable_dir,
.dir = tmp.dir,
.transform_fn = transform_fn,
};
}
@@ -323,28 +319,28 @@ fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void
}
 
test "Dir.Iterator" {
var tmp_dir = tmpIterableDir(.{});
var tmp_dir = tmpDir(.{ .iterate = true });
defer tmp_dir.cleanup();
 
// First, create a couple of entries to iterate over.
const file = try tmp_dir.iterable_dir.dir.createFile("some_file", .{});
const file = try tmp_dir.dir.createFile("some_file", .{});
file.close();
 
try tmp_dir.iterable_dir.dir.makeDir("some_dir");
try tmp_dir.dir.makeDir("some_dir");
 
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
 
var entries = std.ArrayList(IterableDir.Entry).init(allocator);
var entries = std.ArrayList(Dir.Entry).init(allocator);
 
// Create iterator.
var iter = tmp_dir.iterable_dir.iterate();
var iter = tmp_dir.dir.iterate();
while (try iter.next()) |entry| {
// We cannot just store `entry` as on Windows, we're re-using the name buffer
// which means we'll actually share the `name` pointer between entries!
const name = try allocator.dupe(u8, entry.name);
try entries.append(.{ .name = name, .kind = entry.kind });
try entries.append(Dir.Entry{ .name = name, .kind = entry.kind });
}
 
try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
@@ -353,7 +349,7 @@ test "Dir.Iterator" {
}
 
test "Dir.Iterator many entries" {
var tmp_dir = tmpIterableDir(.{});
var tmp_dir = tmpDir(.{ .iterate = true });
defer tmp_dir.cleanup();
 
const num = 1024;
@@ -361,7 +357,7 @@ test "Dir.Iterator many entries" {
var buf: [4]u8 = undefined; // Enough to store "1024".
while (i < num) : (i += 1) {
const name = try std.fmt.bufPrint(&buf, "{}", .{i});
const file = try tmp_dir.iterable_dir.dir.createFile(name, .{});
const file = try tmp_dir.dir.createFile(name, .{});
file.close();
}
 
@@ -369,10 +365,10 @@ test "Dir.Iterator many entries" {
defer arena.deinit();
const allocator = arena.allocator();
 
var entries = std.ArrayList(IterableDir.Entry).init(allocator);
var entries = std.ArrayList(Dir.Entry).init(allocator);
 
// Create iterator.
var iter = tmp_dir.iterable_dir.iterate();
var iter = tmp_dir.dir.iterate();
while (try iter.next()) |entry| {
// We cannot just store `entry` as on Windows, we're re-using the name buffer
// which means we'll actually share the `name` pointer between entries!
@@ -388,14 +384,14 @@ test "Dir.Iterator many entries" {
}
 
test "Dir.Iterator twice" {
var tmp_dir = tmpIterableDir(.{});
var tmp_dir = tmpDir(.{ .iterate = true });
defer tmp_dir.cleanup();
 
// First, create a couple of entries to iterate over.
const file = try tmp_dir.iterable_dir.dir.createFile("some_file", .{});
const file = try tmp_dir.dir.createFile("some_file", .{});
file.close();
 
try tmp_dir.iterable_dir.dir.makeDir("some_dir");
try tmp_dir.dir.makeDir("some_dir");
 
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
@@ -403,15 +399,15 @@ test "Dir.Iterator twice" {
 
var i: u8 = 0;
while (i < 2) : (i += 1) {
var entries = std.ArrayList(IterableDir.Entry).init(allocator);
var entries = std.ArrayList(Dir.Entry).init(allocator);
 
// Create iterator.
var iter = tmp_dir.iterable_dir.iterate();
var iter = tmp_dir.dir.iterate();
while (try iter.next()) |entry| {
// We cannot just store `entry` as on Windows, we're re-using the name buffer
// which means we'll actually share the `name` pointer between entries!
const name = try allocator.dupe(u8, entry.name);
try entries.append(.{ .name = name, .kind = entry.kind });
try entries.append(Dir.Entry{ .name = name, .kind = entry.kind });
}
 
try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
@@ -421,25 +417,25 @@ test "Dir.Iterator twice" {
}
 
test "Dir.Iterator reset" {
var tmp_dir = tmpIterableDir(.{});
var tmp_dir = tmpDir(.{ .iterate = true });
defer tmp_dir.cleanup();
 
// First, create a couple of entries to iterate over.
const file = try tmp_dir.iterable_dir.dir.createFile("some_file", .{});
const file = try tmp_dir.dir.createFile("some_file", .{});
file.close();
 
try tmp_dir.iterable_dir.dir.makeDir("some_dir");
try tmp_dir.dir.makeDir("some_dir");
 
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
 
// Create iterator.
var iter = tmp_dir.iterable_dir.iterate();
var iter = tmp_dir.dir.iterate();
 
var i: u8 = 0;
while (i < 2) : (i += 1) {
var entries = std.ArrayList(IterableDir.Entry).init(allocator);
var entries = std.ArrayList(Dir.Entry).init(allocator);
 
while (try iter.next()) |entry| {
// We cannot just store `entry` as on Windows, we're re-using the name buffer
@@ -461,10 +457,10 @@ test "Dir.Iterator but dir is deleted during iteration" {
defer tmp.cleanup();
 
// Create directory and setup an iterator for it
var iterable_subdir = try tmp.dir.makeOpenPathIterable("subdir", .{});
defer iterable_subdir.close();
var subdir = try tmp.dir.makeOpenPath("subdir", .{ .iterate = true });
defer subdir.close();
 
var iterator = iterable_subdir.iterate();
var iterator = subdir.iterate();
 
// Create something to iterate over within the subdir
try tmp.dir.makePath("subdir/b");
@@ -485,11 +481,11 @@ test "Dir.Iterator but dir is deleted during iteration" {
}
}
 
fn entryEql(lhs: IterableDir.Entry, rhs: IterableDir.Entry) bool {
fn entryEql(lhs: Dir.Entry, rhs: Dir.Entry) bool {
return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind;
}
 
fn contains(entries: *const std.ArrayList(IterableDir.Entry), el: IterableDir.Entry) bool {
fn contains(entries: *const std.ArrayList(Dir.Entry), el: Dir.Entry) bool {
for (entries.items) |entry| {
if (entryEql(entry, el)) return true;
}
@@ -963,10 +959,10 @@ test "makePath in a directory that no longer exists" {
try testing.expectError(error.FileNotFound, tmp.dir.makePath("sub-path"));
}
 
fn testFilenameLimits(iterable_dir: IterableDir, maxed_filename: []const u8) !void {
fn testFilenameLimits(iterable_dir: Dir, maxed_filename: []const u8) !void {
// setup, create a dir and a nested file both with maxed filenames, and walk the dir
{
var maxed_dir = try iterable_dir.dir.makeOpenPath(maxed_filename, .{});
var maxed_dir = try iterable_dir.makeOpenPath(maxed_filename, .{});
defer maxed_dir.close();
 
try maxed_dir.writeFile(maxed_filename, "");
@@ -983,27 +979,27 @@ fn testFilenameLimits(iterable_dir: IterableDir, maxed_filename: []const u8) !vo
}
 
// ensure that we can delete the tree
try iterable_dir.dir.deleteTree(maxed_filename);
try iterable_dir.deleteTree(maxed_filename);
}
 
test "max file name component lengths" {
var tmp = tmpIterableDir(.{});
var tmp = tmpDir(.{ .iterate = true });
defer tmp.cleanup();
 
if (builtin.os.tag == .windows) {
// U+FFFF is the character with the largest code point that is encoded as a single
// UTF-16 code unit, so Windows allows for NAME_MAX of them.
const maxed_windows_filename = ("\u{FFFF}".*) ** std.os.windows.NAME_MAX;
try testFilenameLimits(tmp.iterable_dir, &maxed_windows_filename);
try testFilenameLimits(tmp.dir, &maxed_windows_filename);
} else if (builtin.os.tag == .wasi) {
// On WASI, the maxed filename depends on the host OS, so in order for this test to
// work on any host, we need to use a length that will work for all platforms
// (i.e. the minimum MAX_NAME_BYTES of all supported platforms).
const maxed_wasi_filename = [_]u8{'1'} ** 255;
try testFilenameLimits(tmp.iterable_dir, &maxed_wasi_filename);
try testFilenameLimits(tmp.dir, &maxed_wasi_filename);
} else {
const maxed_ascii_filename = [_]u8{'1'} ** std.fs.MAX_NAME_BYTES;
try testFilenameLimits(tmp.iterable_dir, &maxed_ascii_filename);
try testFilenameLimits(tmp.dir, &maxed_ascii_filename);
}
}
 
@@ -1384,7 +1380,7 @@ test "open file with exclusive nonblocking lock twice (absolute paths)" {
test "walker" {
if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
 
var tmp = tmpIterableDir(.{});
var tmp = tmpDir(.{ .iterate = true });
defer tmp.cleanup();
 
// iteration order of walker is undefined, so need lookup maps to check against
@@ -1410,10 +1406,10 @@ test "walker" {
});
 
for (expected_paths.kvs) |kv| {
try tmp.iterable_dir.dir.makePath(kv.key);
try tmp.dir.makePath(kv.key);
}
 
var walker = try tmp.iterable_dir.walk(testing.allocator);
var walker = try tmp.dir.walk(testing.allocator);
defer walker.deinit();
 
var num_walked: usize = 0;
@@ -1437,17 +1433,17 @@ test "walker" {
test "walker without fully iterating" {
if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
 
var tmp = tmpIterableDir(.{});
var tmp = tmpDir(.{ .iterate = true });
defer tmp.cleanup();
 
var walker = try tmp.iterable_dir.walk(testing.allocator);
var walker = try tmp.dir.walk(testing.allocator);
defer walker.deinit();
 
// Create 2 directories inside the tmp directory, but then only iterate once before breaking.
// This ensures that walker doesn't try to close the initial directory when not fully iterating.
 
try tmp.iterable_dir.dir.makePath("a");
try tmp.iterable_dir.dir.makePath("b");
try tmp.dir.makePath("a");
try tmp.dir.makePath("b");
 
var num_walked: usize = 0;
while (try walker.next()) |_| {
@@ -1490,7 +1486,7 @@ test ". and .. in fs.Dir functions" {
 
try ctx.dir.writeFile(update_path, "something");
const prev_status = try ctx.dir.updateFile(file_path, ctx.dir, update_path, .{});
try testing.expectEqual(fs.PrevStatus.stale, prev_status);
try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status);
 
try ctx.dir.deleteDir(subdir_path);
}
@@ -1536,7 +1532,7 @@ test ". and .. in absolute functions" {
try update_file.writeAll("something");
update_file.close();
const prev_status = try fs.updateFileAbsolute(created_file_path, update_file_path, .{});
try testing.expectEqual(fs.PrevStatus.stale, prev_status);
try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status);
 
try fs.deleteDirAbsolute(subdir_path);
}
@@ -1556,11 +1552,11 @@ test "chmod" {
try testing.expectEqual(@as(File.Mode, 0o644), (try file.stat()).mode & 0o7777);
 
try tmp.dir.makeDir("test_dir");
var iterable_dir = try tmp.dir.openIterableDir("test_dir", .{});
defer iterable_dir.close();
var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true });
defer dir.close();
 
try iterable_dir.chmod(0o700);
try testing.expectEqual(@as(File.Mode, 0o700), (try iterable_dir.dir.stat()).mode & 0o7777);
try dir.chmod(0o700);
try testing.expectEqual(@as(File.Mode, 0o700), (try dir.stat()).mode & 0o7777);
}
 
test "chown" {
@@ -1576,9 +1572,9 @@ test "chown" {
 
try tmp.dir.makeDir("test_dir");
 
var iterable_dir = try tmp.dir.openIterableDir("test_dir", .{});
defer iterable_dir.close();
try iterable_dir.chown(null, null);
var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true });
defer dir.close();
try dir.chown(null, null);
}
 
test "File.Metadata" {
 
lib/std/os.zig added: 4428, removed: 4469, total 0
@@ -402,7 +402,7 @@ pub fn fchown(fd: fd_t, owner: ?uid_t, group: ?gid_t) FChownError!void {
switch (system.getErrno(res)) {
.SUCCESS => return,
.INTR => continue,
.BADF => unreachable, // Can be reached if the fd refers to a non-iterable directory.
.BADF => unreachable, // Can be reached if the fd refers to a directory opened without `OpenDirOptions{ .iterate = true }`
 
.FAULT => unreachable,
.INVAL => unreachable,
 
lib/std/testing.zig added: 4428, removed: 4469, total 0
@@ -543,22 +543,6 @@ pub const TmpDir = struct {
}
};
 
pub const TmpIterableDir = struct {
iterable_dir: std.fs.IterableDir,
parent_dir: std.fs.Dir,
sub_path: [sub_path_len]u8,
 
const random_bytes_count = 12;
const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count);
 
pub fn cleanup(self: *TmpIterableDir) void {
self.iterable_dir.close();
self.parent_dir.deleteTree(&self.sub_path) catch {};
self.parent_dir.close();
self.* = undefined;
}
};
 
pub fn tmpDir(opts: std.fs.Dir.OpenDirOptions) TmpDir {
var random_bytes: [TmpDir.random_bytes_count]u8 = undefined;
std.crypto.random.bytes(&random_bytes);
@@ -581,28 +565,6 @@ pub fn tmpDir(opts: std.fs.Dir.OpenDirOptions) TmpDir {
};
}
 
pub fn tmpIterableDir(opts: std.fs.Dir.OpenDirOptions) TmpIterableDir {
var random_bytes: [TmpIterableDir.random_bytes_count]u8 = undefined;
std.crypto.random.bytes(&random_bytes);
var sub_path: [TmpIterableDir.sub_path_len]u8 = undefined;
_ = std.fs.base64_encoder.encode(&sub_path, &random_bytes);
 
const cwd = std.fs.cwd();
var cache_dir = cwd.makeOpenPath("zig-cache", .{}) catch
@panic("unable to make tmp dir for testing: unable to make and open zig-cache dir");
defer cache_dir.close();
const parent_dir = cache_dir.makeOpenPath("tmp", .{}) catch
@panic("unable to make tmp dir for testing: unable to make and open zig-cache/tmp dir");
const dir = parent_dir.makeOpenPathIterable(&sub_path, opts) catch
@panic("unable to make tmp dir for testing: unable to make and open the tmp dir");
 
return .{
.iterable_dir = dir,
.parent_dir = parent_dir,
.sub_path = sub_path,
};
}
 
test "expectEqual nested array" {
const a = [2][2]f32{
[_]f32{ 1.0, 0.0 },
 
src/Package/Fetch.zig added: 4428, removed: 4469, total 0
@@ -280,7 +280,7 @@ pub fn run(f: *Fetch) RunError!void {
},
.remote => |remote| remote,
.path_or_url => |path_or_url| {
if (fs.cwd().openIterableDir(path_or_url, .{})) |dir| {
if (fs.cwd().openDir(path_or_url, .{ .iterate = true })) |dir| {
var resource: Resource = .{ .dir = dir };
return runResource(f, path_or_url, &resource, null);
} else |dir_err| {
@@ -363,7 +363,9 @@ fn runResource(
var tmp_directory: Cache.Directory = .{
.path = tmp_directory_path,
.handle = handle: {
const dir = cache_root.handle.makeOpenPathIterable(tmp_dir_sub_path, .{}) catch |err| {
const dir = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{
.iterate = true,
}) catch |err| {
try eb.addRootErrorMessage(.{
.msg = try eb.printString("unable to create temporary directory '{s}': {s}", .{
tmp_directory_path, @errorName(err),
@@ -371,7 +373,7 @@ fn runResource(
});
return error.FetchFailed;
};
break :handle dir.dir;
break :handle dir;
},
};
defer tmp_directory.handle.close();
@@ -400,9 +402,9 @@ fn runResource(
if (builtin.os.tag == .linux and f.job_queue.work_around_btrfs_bug) {
// https://github.com/ziglang/zig/issues/17095
tmp_directory.handle.close();
const iterable_dir = cache_root.handle.makeOpenPathIterable(tmp_dir_sub_path, .{}) catch
@panic("btrfs workaround failed");
tmp_directory.handle = iterable_dir.dir;
tmp_directory.handle = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{
.iterate = true,
}) catch @panic("btrfs workaround failed");
}
 
f.actual_hash = try computeHash(f, tmp_directory, filter);
@@ -717,7 +719,7 @@ const Resource = union(enum) {
file: fs.File,
http_request: std.http.Client.Request,
git: Git,
dir: fs.IterableDir,
dir: fs.Dir,
 
const Git = struct {
fetch_stream: git.Session.FetchStream,
@@ -1198,7 +1200,7 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource) anyerror!void
try out_dir.deleteTree(".git");
}
 
fn recursiveDirectoryCopy(f: *Fetch, dir: fs.IterableDir, tmp_dir: fs.Dir) anyerror!void {
fn recursiveDirectoryCopy(f: *Fetch, dir: fs.Dir, tmp_dir: fs.Dir) anyerror!void {
const gpa = f.arena.child_allocator;
// Recursive directory copy.
var it = try dir.walk(gpa);
@@ -1207,7 +1209,7 @@ fn recursiveDirectoryCopy(f: *Fetch, dir: fs.IterableDir, tmp_dir: fs.Dir) anyer
switch (entry.kind) {
.directory => {}, // omit empty directories
.file => {
dir.dir.copyFile(
dir.copyFile(
entry.path,
tmp_dir,
entry.path,
@@ -1215,14 +1217,14 @@ fn recursiveDirectoryCopy(f: *Fetch, dir: fs.IterableDir, tmp_dir: fs.Dir) anyer
) catch |err| switch (err) {
error.FileNotFound => {
if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(dirname);
try dir.dir.copyFile(entry.path, tmp_dir, entry.path, .{});
try dir.copyFile(entry.path, tmp_dir, entry.path, .{});
},
else => |e| return e,
};
},
.sym_link => {
var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
const link_name = try dir.dir.readLink(entry.path, &buf);
const link_name = try dir.readLink(entry.path, &buf);
// TODO: if this would create a symlink to outside
// the destination directory, fail with an error instead.
tmp_dir.symLink(link_name, entry.path, .{}) catch |err| switch (err) {
@@ -1296,7 +1298,7 @@ fn computeHash(
var sus_dirs: std.StringArrayHashMapUnmanaged(void) = .{};
defer sus_dirs.deinit(gpa);
 
var walker = try @as(fs.IterableDir, .{ .dir = tmp_directory.handle }).walk(gpa);
var walker = try tmp_directory.handle.walk(gpa);
defer walker.deinit();
 
{
 
src/Package/Fetch/git.zig added: 4428, removed: 4469, total 0
@@ -1384,11 +1384,11 @@ test "packfile indexing and checkout" {
var repository = try Repository.init(testing.allocator, pack_file, index_file);
defer repository.deinit();
 
var worktree = testing.tmpIterableDir(.{});
var worktree = testing.tmpDir(.{ .iterate = true });
defer worktree.cleanup();
 
const commit_id = try parseOid("dd582c0720819ab7130b103635bd7271b9fd4feb");
try repository.checkout(worktree.iterable_dir.dir, commit_id);
try repository.checkout(worktree.dir, commit_id);
 
const expected_files: []const []const u8 = &.{
"dir/file",
@@ -1410,7 +1410,7 @@ test "packfile indexing and checkout" {
var actual_files: std.ArrayListUnmanaged([]u8) = .{};
defer actual_files.deinit(testing.allocator);
defer for (actual_files.items) |file| testing.allocator.free(file);
var walker = try worktree.iterable_dir.walk(testing.allocator);
var walker = try worktree.dir.walk(testing.allocator);
defer walker.deinit();
while (try walker.next()) |entry| {
if (entry.kind != .file) continue;
@@ -1442,7 +1442,7 @@ test "packfile indexing and checkout" {
\\revision 19
\\
;
const actual_file_contents = try worktree.iterable_dir.dir.readFileAlloc(testing.allocator, "file", max_file_size);
const actual_file_contents = try worktree.dir.readFileAlloc(testing.allocator, "file", max_file_size);
defer testing.allocator.free(actual_file_contents);
try testing.expectEqualStrings(expected_file_contents, actual_file_contents);
}
 
src/main.zig added: 4428, removed: 4469, total 0
@@ -5698,13 +5698,13 @@ fn fmtPathDir(
parent_dir: fs.Dir,
parent_sub_path: []const u8,
) FmtError!void {
var iterable_dir = try parent_dir.openIterableDir(parent_sub_path, .{});
defer iterable_dir.close();
var dir = try parent_dir.openDir(parent_sub_path, .{ .iterate = true });
defer dir.close();
 
const stat = try iterable_dir.dir.stat();
const stat = try dir.stat();
if (try fmt.seen.fetchPut(stat.inode, {})) |_| return;
 
var dir_it = iterable_dir.iterate();
var dir_it = dir.iterate();
while (try dir_it.next()) |entry| {
const is_dir = entry.kind == .directory;
 
@@ -5715,9 +5715,9 @@ fn fmtPathDir(
defer fmt.gpa.free(full_path);
 
if (is_dir) {
try fmtPathDir(fmt, full_path, check_mode, iterable_dir.dir, entry.name);
try fmtPathDir(fmt, full_path, check_mode, dir, entry.name);
} else {
fmtPathFile(fmt, full_path, check_mode, iterable_dir.dir, entry.name) catch |err| {
fmtPathFile(fmt, full_path, check_mode, dir, entry.name) catch |err| {
warn("unable to format '{s}': {s}", .{ full_path, @errorName(err) });
fmt.any_error = true;
return;
 
src/windows_sdk.zig added: 4428, removed: 4469, total 0
@@ -14,7 +14,11 @@ const product_version_max_length = version_major_minor_max_length + ".65535".len
/// Iterates via `iterator` and collects all folders with names starting with `optional_prefix`
/// and similar to SemVer. Returns slice of folder names sorted in descending order.
/// Caller owns result.
fn iterateAndFilterBySemVer(iterator: *std.fs.IterableDir.Iterator, allocator: std.mem.Allocator, comptime optional_prefix: ?[]const u8) error{ OutOfMemory, VersionNotFound }![][]const u8 {
fn iterateAndFilterBySemVer(
iterator: *std.fs.Dir.Iterator,
allocator: std.mem.Allocator,
comptime optional_prefix: ?[]const u8,
) error{ OutOfMemory, VersionNotFound }![][]const u8 {
var dirs_filtered_list = std.ArrayList([]const u8).init(allocator);
errdefer {
for (dirs_filtered_list.items) |filtered_dir| allocator.free(filtered_dir);
@@ -476,7 +480,9 @@ pub const Windows81Sdk = struct {
if (!std.fs.path.isAbsolute(sdk_lib_dir_path)) return error.Windows81SdkNotFound;
 
// enumerate files in sdk path looking for latest version
var sdk_lib_dir = std.fs.openIterableDirAbsolute(sdk_lib_dir_path, .{}) catch |err| switch (err) {
var sdk_lib_dir = std.fs.openDirAbsolute(sdk_lib_dir_path, .{
.iterate = true,
}) catch |err| switch (err) {
error.NameTooLong => return error.PathTooLong,
else => return error.Windows81SdkNotFound,
};
@@ -727,7 +733,9 @@ const MsvcLibDir = struct {
if (!std.fs.path.isAbsolute(visualstudio_folder_path)) return error.PathNotFound;
// enumerate folders that contain `privateregistry.bin`, looking for all versions
// f.i. %localappdata%\Microsoft\VisualStudio\17.0_9e9cbb98\
var visualstudio_folder = std.fs.openIterableDirAbsolute(visualstudio_folder_path, .{}) catch return error.PathNotFound;
var visualstudio_folder = std.fs.openDirAbsolute(visualstudio_folder_path, .{
.iterate = true,
}) catch return error.PathNotFound;
defer visualstudio_folder.close();
 
var iterator = visualstudio_folder.iterate();
 
test/src/Cases.zig added: 4428, removed: 4469, total 0
@@ -368,7 +368,7 @@ pub fn addCompile(
/// Each file should include a test manifest as a contiguous block of comments at
/// the end of the file. The first line should be the test type, followed by a set of
/// key-value config values, followed by a blank line, then the expected output.
pub fn addFromDir(ctx: *Cases, dir: std.fs.IterableDir) void {
pub fn addFromDir(ctx: *Cases, dir: std.fs.Dir) void {
var current_file: []const u8 = "none";
ctx.addFromDirInner(dir, &current_file) catch |err| {
std.debug.panicExtra(
@@ -382,7 +382,7 @@ pub fn addFromDir(ctx: *Cases, dir: std.fs.IterableDir) void {
 
fn addFromDirInner(
ctx: *Cases,
iterable_dir: std.fs.IterableDir,
iterable_dir: std.fs.Dir,
/// This is kept up to date with the currently being processed file so
/// that if any errors occur the caller knows it happened during this file.
current_file: *[]const u8,
@@ -416,7 +416,7 @@ fn addFromDirInner(
}
 
const max_file_size = 10 * 1024 * 1024;
const src = try iterable_dir.dir.readFileAllocOptions(ctx.arena, filename, max_file_size, null, 1, 0);
const src = try iterable_dir.readFileAllocOptions(ctx.arena, filename, max_file_size, null, 1, 0);
 
// Parse the manifest
var manifest = try TestManifest.parse(ctx.arena, src);
@@ -1246,7 +1246,7 @@ pub fn main() !void {
var filenames = std.ArrayList([]const u8).init(arena);
 
const case_dirname = std.fs.path.dirname(case_file_path).?;
var iterable_dir = try std.fs.cwd().openIterableDir(case_dirname, .{});
var iterable_dir = try std.fs.cwd().openDir(case_dirname, .{ .iterate = true });
defer iterable_dir.close();
 
if (std.mem.endsWith(u8, case_file_path, ".0.zig")) {
@@ -1280,7 +1280,7 @@ pub fn main() !void {
 
for (batch) |filename| {
const max_file_size = 10 * 1024 * 1024;
const src = try iterable_dir.dir.readFileAllocOptions(arena, filename, max_file_size, null, 1, 0);
const src = try iterable_dir.readFileAllocOptions(arena, filename, max_file_size, null, 1, 0);
 
// Parse the manifest
var manifest = try TestManifest.parse(arena, src);
 
test/tests.zig added: 4428, removed: 4469, total 0
@@ -1288,7 +1288,7 @@ pub fn addCases(
 
var cases = @import("src/Cases.zig").init(gpa, arena);
 
var dir = try b.build_root.handle.openIterableDir("test/cases", .{});
var dir = try b.build_root.handle.openDir("test/cases", .{ .iterate = true });
defer dir.close();
 
cases.addFromDir(dir);
 
tools/generate_JSONTestSuite.zig added: 4428, removed: 4469, total 0
@@ -18,7 +18,7 @@ pub fn main() !void {
);
 
var names = std.ArrayList([]const u8).init(allocator);
var cwd = try std.fs.cwd().openIterableDir(".", .{});
var cwd = try std.fs.cwd().openDir(".", .{ .iterate = true });
var it = cwd.iterate();
while (try it.next()) |entry| {
try names.append(try allocator.dupe(u8, entry.name));
 
tools/process_headers.zig added: 4428, removed: 4469, total 0
@@ -382,14 +382,14 @@ pub fn main() !void {
try dir_stack.append(target_include_dir);
 
while (dir_stack.popOrNull()) |full_dir_name| {
var iterable_dir = std.fs.cwd().openIterableDir(full_dir_name, .{}) catch |err| switch (err) {
var dir = std.fs.cwd().openDir(full_dir_name, .{ .iterate = true }) catch |err| switch (err) {
error.FileNotFound => continue :search,
error.AccessDenied => continue :search,
else => return err,
};
defer iterable_dir.close();
defer dir.close();
 
var dir_it = iterable_dir.iterate();
var dir_it = dir.iterate();
 
while (try dir_it.next()) |entry| {
const full_path = try std.fs.path.join(allocator, &[_][]const u8{ full_dir_name, entry.name });
 
tools/update-license-headers.zig added: 4428, removed: 4469, total 0
@@ -14,9 +14,9 @@ pub fn main() !void {
 
const args = try std.process.argsAlloc(arena);
const path_to_walk = args[1];
const iterable_dir = try std.fs.cwd().openIterableDir(path_to_walk, .{});
const dir = try std.fs.cwd().openDir(path_to_walk, .{ .iterate = true });
 
var walker = try iterable_dir.walk(arena);
var walker = try dir.walk(arena);
defer walker.deinit();
 
var buffer: [500]u8 = undefined;
@@ -30,7 +30,7 @@ pub fn main() !void {
node.activate();
defer node.end();
 
const source = try iterable_dir.dir.readFileAlloc(arena, entry.path, 20 * 1024 * 1024);
const source = try dir.readFileAlloc(arena, entry.path, 20 * 1024 * 1024);
if (!std.mem.startsWith(u8, source, expected_header)) {
std.debug.print("no match: {s}\n", .{entry.path});
continue;
@@ -42,6 +42,6 @@ pub fn main() !void {
std.mem.copy(u8, new_source, new_header);
std.mem.copy(u8, new_source[new_header.len..], truncated_source);
 
try iterable_dir.dir.writeFile(entry.path, new_source);
try dir.writeFile(entry.path, new_source);
}
}
 
tools/update-linux-headers.zig added: 4428, removed: 4469, total 0
@@ -190,14 +190,14 @@ pub fn main() !void {
try dir_stack.append(target_include_dir);
 
while (dir_stack.popOrNull()) |full_dir_name| {
var iterable_dir = std.fs.cwd().openIterableDir(full_dir_name, .{}) catch |err| switch (err) {
var dir = std.fs.cwd().openDir(full_dir_name, .{ .iterate = true }) catch |err| switch (err) {
error.FileNotFound => continue :search,
error.AccessDenied => continue :search,
else => return err,
};
defer iterable_dir.close();
defer dir.close();
 
var dir_it = iterable_dir.iterate();
var dir_it = dir.iterate();
 
while (try dir_it.next()) |entry| {
const full_path = try std.fs.path.join(arena, &[_][]const u8{ full_dir_name, entry.name });
 
tools/update_glibc.zig added: 4428, removed: 4469, total 0
@@ -47,7 +47,7 @@ pub fn main() !void {
 
const dest_dir_path = try std.fmt.allocPrint(arena, "{s}/lib/libc/glibc", .{zig_src_path});
 
var dest_dir = fs.cwd().openIterableDir(dest_dir_path, .{}) catch |err| {
var dest_dir = fs.cwd().openDir(dest_dir_path, .{ .iterate = true }) catch |err| {
fatal("unable to open destination directory '{s}': {s}", .{
dest_dir_path, @errorName(err),
});
@@ -72,14 +72,14 @@ pub fn main() !void {
if (mem.endsWith(u8, entry.path, ext)) continue :walk;
}
 
glibc_src_dir.copyFile(entry.path, dest_dir.dir, entry.path, .{}) catch |err| {
glibc_src_dir.copyFile(entry.path, dest_dir, entry.path, .{}) catch |err| {
log.warn("unable to copy '{s}/{s}' to '{s}/{s}': {s}", .{
glibc_src_path, entry.path,
dest_dir_path, entry.path,
@errorName(err),
});
if (err == error.FileNotFound) {
try dest_dir.dir.deleteFile(entry.path);
try dest_dir.deleteFile(entry.path);
}
};
}
@@ -88,7 +88,7 @@ pub fn main() !void {
// Warn about duplicated files inside glibc/include/* that can be omitted
// because they are already in generic-glibc/*.
 
var include_dir = dest_dir.dir.openIterableDir("include", .{}) catch |err| {
var include_dir = dest_dir.openDir("include", .{ .iterate = true }) catch |err| {
fatal("unable to open directory '{s}/include': {s}", .{
dest_dir_path, @errorName(err),
});
@@ -125,7 +125,7 @@ pub fn main() !void {
generic_glibc_path, entry.path, @errorName(e),
}),
};
const glibc_include_contents = include_dir.dir.readFileAlloc(
const glibc_include_contents = include_dir.readFileAlloc(
arena,
entry.path,
max_file_size,
 
tools/update_spirv_features.zig added: 4428, removed: 4469, total 0
@@ -226,7 +226,7 @@ pub fn main() !void {
/// TODO: Unfortunately, neither repository contains a machine-readable list of extension dependencies.
fn gather_extensions(allocator: Allocator, spirv_registry_root: []const u8) ![]const []const u8 {
const extensions_path = try fs.path.join(allocator, &.{ spirv_registry_root, "extensions" });
var extensions_dir = try fs.cwd().openIterableDir(extensions_path, .{});
var extensions_dir = try fs.cwd().openDir(extensions_path, .{ .iterate = true });
defer extensions_dir.close();
 
var extensions = std.ArrayList([]const u8).init(allocator);
@@ -235,7 +235,7 @@ fn gather_extensions(allocator: Allocator, spirv_registry_root: []const u8) ![]c
while (try vendor_it.next()) |vendor_entry| {
std.debug.assert(vendor_entry.kind == .directory); // If this fails, the structure of SPIRV-Registry has changed.
 
const vendor_dir = try extensions_dir.dir.openIterableDir(vendor_entry.name, .{});
const vendor_dir = try extensions_dir.openDir(vendor_entry.name, .{ .iterate = true });
var ext_it = vendor_dir.iterate();
while (try ext_it.next()) |ext_entry| {
// There is both a HTML and asciidoc version of every spec (as well as some other directories),
@@ -258,7 +258,7 @@ fn gather_extensions(allocator: Allocator, spirv_registry_root: []const u8) ![]c
// SPV_EXT_name
// ```
 
const ext_spec = try vendor_dir.dir.readFileAlloc(allocator, ext_entry.name, std.math.maxInt(usize));
const ext_spec = try vendor_dir.readFileAlloc(allocator, ext_entry.name, std.math.maxInt(usize));
const name_strings = "Name Strings";
 
const name_strings_offset = std.mem.indexOf(u8, ext_spec, name_strings) orelse return error.InvalidRegistry;