std: all Dir functions moved to std.Io

This commit is contained in:
Andrew Kelley
2025-12-03 20:37:43 -08:00
parent 81214278ca
commit d1d2c37af2
24 changed files with 3064 additions and 3650 deletions
-2
View File
@@ -436,8 +436,6 @@ set(ZIG_STAGE2_SOURCES
lib/std/fmt.zig
lib/std/fmt/parse_float.zig
lib/std/fs.zig
lib/std/fs/AtomicFile.zig
lib/std/fs/Dir.zig
lib/std/fs/File.zig
lib/std/fs/get_app_data_dir.zig
lib/std/fs/path.zig
+22 -13
View File
@@ -1,18 +1,20 @@
const std = @import("std");
const builtin = std.builtin;
const tests = @import("test/tests.zig");
const BufMap = std.BufMap;
const mem = std.mem;
const io = std.io;
const fs = std.fs;
const InstallDirectoryOptions = std.Build.InstallDirectoryOptions;
const assert = std.debug.assert;
const Io = std.Io;
const tests = @import("test/tests.zig");
const DevEnv = @import("src/dev.zig").Env;
const ValueInterpretMode = enum { direct, by_name };
const zig_version: std.SemanticVersion = .{ .major = 0, .minor = 16, .patch = 0 };
const stack_size = 46 * 1024 * 1024;
const ValueInterpretMode = enum { direct, by_name };
pub fn build(b: *std.Build) !void {
const only_c = b.option(bool, "only-c", "Translate the Zig compiler to C code, with only the C backend enabled") orelse false;
const target = b.standardTargetOptions(.{
@@ -306,8 +308,10 @@ pub fn build(b: *std.Build) !void {
if (enable_llvm) {
const cmake_cfg = if (static_llvm) null else blk: {
const io = b.graph.io;
const cwd: Io.Dir = .cwd();
if (findConfigH(b, config_h_path_option)) |config_h_path| {
const file_contents = fs.cwd().readFileAlloc(config_h_path, b.allocator, .limited(max_config_h_bytes)) catch unreachable;
const file_contents = cwd.readFileAlloc(io, config_h_path, b.allocator, .limited(max_config_h_bytes)) catch unreachable;
break :blk parseConfigH(b, file_contents);
} else {
std.log.warn("config.h could not be located automatically. Consider providing it explicitly via \"-Dconfig_h\"", .{});
@@ -1153,10 +1157,13 @@ const CMakeConfig = struct {
const max_config_h_bytes = 1 * 1024 * 1024;
fn findConfigH(b: *std.Build, config_h_path_option: ?[]const u8) ?[]const u8 {
const io = b.graph.io;
const cwd: Io.Dir = .cwd();
if (config_h_path_option) |path| {
var config_h_or_err = fs.cwd().openFile(path, .{});
var config_h_or_err = cwd.openFile(io, path, .{});
if (config_h_or_err) |*file| {
file.close();
file.close(io);
return path;
} else |_| {
std.log.err("Could not open provided config.h: \"{s}\"", .{path});
@@ -1166,13 +1173,13 @@ fn findConfigH(b: *std.Build, config_h_path_option: ?[]const u8) ?[]const u8 {
var check_dir = fs.path.dirname(b.graph.zig_exe).?;
while (true) {
var dir = fs.cwd().openDir(check_dir, .{}) catch unreachable;
defer dir.close();
var dir = cwd.openDir(io, check_dir, .{}) catch unreachable;
defer dir.close(io);
// Check if config.h is present in dir
var config_h_or_err = dir.openFile("config.h", .{});
var config_h_or_err = dir.openFile(io, "config.h", .{});
if (config_h_or_err) |*file| {
file.close();
file.close(io);
return fs.path.join(
b.allocator,
&[_][]const u8{ check_dir, "config.h" },
@@ -1183,9 +1190,9 @@ fn findConfigH(b: *std.Build, config_h_path_option: ?[]const u8) ?[]const u8 {
}
// Check if we reached the source root by looking for .git, and bail if so
var git_dir_or_err = dir.openDir(".git", .{});
var git_dir_or_err = dir.openDir(io, ".git", .{});
if (git_dir_or_err) |*git_dir| {
git_dir.close();
git_dir.close(io);
return null;
} else |_| {}
@@ -1581,6 +1588,8 @@ const llvm_libs_xtensa = [_][]const u8{
};
fn generateLangRef(b: *std.Build) std.Build.LazyPath {
const io = b.graph.io;
const doctest_exe = b.addExecutable(.{
.name = "doctest",
.root_module = b.createModule(.{
@@ -1590,7 +1599,7 @@ fn generateLangRef(b: *std.Build) std.Build.LazyPath {
}),
});
var dir = b.build_root.handle.openDir("doc/langref", .{ .iterate = true }) catch |err| {
var dir = b.build_root.handle.openDir(io, "doc/langref", .{ .iterate = true }) catch |err| {
std.debug.panic("unable to open '{f}doc/langref' directory: {s}", .{
b.build_root, @errorName(err),
});
+8 -6
View File
@@ -53,24 +53,26 @@ pub fn main() !void {
const cache_root = nextArg(args, &arg_idx) orelse fatal("missing cache root directory path", .{});
const global_cache_root = nextArg(args, &arg_idx) orelse fatal("missing global cache root directory path", .{});
const cwd: Io.Dir = .cwd();
const zig_lib_directory: std.Build.Cache.Directory = .{
.path = zig_lib_dir,
.handle = try std.fs.cwd().openDir(zig_lib_dir, .{}),
.handle = try cwd.openDir(io, zig_lib_dir, .{}),
};
const build_root_directory: std.Build.Cache.Directory = .{
.path = build_root,
.handle = try std.fs.cwd().openDir(build_root, .{}),
.handle = try cwd.openDir(io, build_root, .{}),
};
const local_cache_directory: std.Build.Cache.Directory = .{
.path = cache_root,
.handle = try std.fs.cwd().makeOpenPath(cache_root, .{}),
.handle = try cwd.makeOpenPath(io, cache_root, .{}),
};
const global_cache_directory: std.Build.Cache.Directory = .{
.path = global_cache_root,
.handle = try std.fs.cwd().makeOpenPath(global_cache_root, .{}),
.handle = try cwd.makeOpenPath(io, global_cache_root, .{}),
};
var graph: std.Build.Graph = .{
@@ -79,7 +81,7 @@ pub fn main() !void {
.cache = .{
.io = io,
.gpa = arena,
.manifest_dir = try local_cache_directory.handle.makeOpenPath("h", .{}),
.manifest_dir = try local_cache_directory.handle.makeOpenPath(io, "h", .{}),
},
.zig_exe = zig_exe,
.env_map = try process.getEnvMap(arena),
@@ -92,7 +94,7 @@ pub fn main() !void {
.time_report = false,
};
graph.cache.addPrefix(.{ .path = null, .handle = std.fs.cwd() });
graph.cache.addPrefix(.{ .path = null, .handle = cwd });
graph.cache.addPrefix(build_root_directory);
graph.cache.addPrefix(local_cache_directory);
graph.cache.addPrefix(global_cache_directory);
+3 -4
View File
@@ -1700,9 +1700,8 @@ pub fn addCheckFile(
}
pub fn truncateFile(b: *Build, dest_path: []const u8) (fs.Dir.MakeError || fs.Dir.StatFileError)!void {
if (b.verbose) {
log.info("truncate {s}", .{dest_path});
}
const io = b.graph.io;
if (b.verbose) log.info("truncate {s}", .{dest_path});
const cwd = fs.cwd();
var src_file = cwd.createFile(dest_path, .{}) catch |err| switch (err) {
error.FileNotFound => blk: {
@@ -1713,7 +1712,7 @@ pub fn truncateFile(b: *Build, dest_path: []const u8) (fs.Dir.MakeError || fs.Di
},
else => |e| return e,
};
src_file.close();
src_file.close(io);
}
/// References a file or directory relative to the source root.
+43 -40
View File
@@ -8,7 +8,6 @@ const builtin = @import("builtin");
const std = @import("std");
const Io = std.Io;
const crypto = std.crypto;
const fs = std.fs;
const assert = std.debug.assert;
const testing = std.testing;
const mem = std.mem;
@@ -18,7 +17,7 @@ const log = std.log.scoped(.cache);
gpa: Allocator,
io: Io,
manifest_dir: fs.Dir,
manifest_dir: Io.Dir,
hash: HashHelper = .{},
/// This value is accessed from multiple threads, protected by mutex.
recent_problematic_timestamp: Io.Timestamp = .zero,
@@ -71,7 +70,7 @@ const PrefixedPath = struct {
fn findPrefix(cache: *const Cache, file_path: []const u8) !PrefixedPath {
const gpa = cache.gpa;
const resolved_path = try fs.path.resolve(gpa, &.{file_path});
const resolved_path = try std.fs.path.resolve(gpa, &.{file_path});
errdefer gpa.free(resolved_path);
return findPrefixResolved(cache, resolved_path);
}
@@ -102,9 +101,9 @@ fn findPrefixResolved(cache: *const Cache, resolved_path: []u8) !PrefixedPath {
}
fn getPrefixSubpath(allocator: Allocator, prefix: []const u8, path: []u8) ![]u8 {
const relative = try fs.path.relative(allocator, prefix, path);
const relative = try std.fs.path.relative(allocator, prefix, path);
errdefer allocator.free(relative);
var component_iterator = fs.path.NativeComponentIterator.init(relative);
var component_iterator = std.fs.path.NativeComponentIterator.init(relative);
if (component_iterator.root() != null) {
return error.NotASubPath;
}
@@ -145,17 +144,17 @@ pub const File = struct {
max_file_size: ?usize,
/// Populated if the user calls `addOpenedFile`.
/// The handle is not owned here.
handle: ?fs.File,
handle: ?Io.File,
stat: Stat,
bin_digest: BinDigest,
contents: ?[]const u8,
pub const Stat = struct {
inode: fs.File.INode,
inode: Io.File.INode,
size: u64,
mtime: Io.Timestamp,
pub fn fromFs(fs_stat: fs.File.Stat) Stat {
pub fn fromFs(fs_stat: Io.File.Stat) Stat {
return .{
.inode = fs_stat.inode,
.size = fs_stat.size,
@@ -178,7 +177,7 @@ pub const File = struct {
file.max_file_size = if (file.max_file_size) |old| @max(old, new) else new;
}
pub fn updateHandle(file: *File, new_handle: ?fs.File) void {
pub fn updateHandle(file: *File, new_handle: ?Io.File) void {
const handle = new_handle orelse return;
file.handle = handle;
}
@@ -293,16 +292,16 @@ pub fn binToHex(bin_digest: BinDigest) HexDigest {
}
pub const Lock = struct {
manifest_file: fs.File,
manifest_file: Io.File,
pub fn release(lock: *Lock) void {
pub fn release(lock: *Lock, io: Io) void {
if (builtin.os.tag == .windows) {
// Windows does not guarantee that locks are immediately unlocked when
// the file handle is closed. See LockFileEx documentation.
lock.manifest_file.unlock();
}
lock.manifest_file.close();
lock.manifest_file.close(io);
lock.* = undefined;
}
};
@@ -311,7 +310,7 @@ pub const Manifest = struct {
cache: *Cache,
/// Current state for incremental hashing.
hash: HashHelper,
manifest_file: ?fs.File,
manifest_file: ?Io.File,
manifest_dirty: bool,
/// Set this flag to true before calling hit() in order to indicate that
/// upon a cache hit, the code using the cache will not modify the files
@@ -332,9 +331,9 @@ pub const Manifest = struct {
pub const Diagnostic = union(enum) {
none,
manifest_create: fs.File.OpenError,
manifest_read: fs.File.ReadError,
manifest_lock: fs.File.LockError,
manifest_create: Io.File.OpenError,
manifest_read: Io.File.Reader.Error,
manifest_lock: Io.File.LockError,
file_open: FileOp,
file_stat: FileOp,
file_read: FileOp,
@@ -393,10 +392,10 @@ pub const Manifest = struct {
}
/// Same as `addFilePath` except the file has already been opened.
pub fn addOpenedFile(m: *Manifest, path: Path, handle: ?fs.File, max_file_size: ?usize) !usize {
pub fn addOpenedFile(m: *Manifest, path: Path, handle: ?Io.File, max_file_size: ?usize) !usize {
const gpa = m.cache.gpa;
try m.files.ensureUnusedCapacity(gpa, 1);
const resolved_path = try fs.path.resolve(gpa, &.{
const resolved_path = try std.fs.path.resolve(gpa, &.{
path.root_dir.path orelse ".",
path.subPathOrDot(),
});
@@ -417,7 +416,7 @@ pub const Manifest = struct {
return addFileInner(self, prefixed_path, null, max_file_size);
}
fn addFileInner(self: *Manifest, prefixed_path: PrefixedPath, handle: ?fs.File, max_file_size: ?usize) usize {
fn addFileInner(self: *Manifest, prefixed_path: PrefixedPath, handle: ?Io.File, max_file_size: ?usize) usize {
const gop = self.files.getOrPutAssumeCapacityAdapted(prefixed_path, FilesAdapter{});
if (gop.found_existing) {
self.cache.gpa.free(prefixed_path.sub_path);
@@ -460,7 +459,7 @@ pub const Manifest = struct {
}
}
pub fn addDepFile(self: *Manifest, dir: fs.Dir, dep_file_sub_path: []const u8) !void {
pub fn addDepFile(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void {
assert(self.manifest_file == null);
return self.addDepFileMaybePost(dir, dep_file_sub_path);
}
@@ -702,7 +701,7 @@ pub const Manifest = struct {
const file_path = iter.rest();
const stat_size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat;
const stat_inode = fmt.parseInt(fs.File.INode, inode, 10) catch return error.InvalidFormat;
const stat_inode = fmt.parseInt(Io.File.INode, inode, 10) catch return error.InvalidFormat;
const stat_mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat;
const file_bin_digest = b: {
if (digest_str.len != hex_digest_len) return error.InvalidFormat;
@@ -772,7 +771,7 @@ pub const Manifest = struct {
return error.CacheCheckFailed;
},
};
defer this_file.close();
defer this_file.close(io);
const actual_stat = this_file.stat() catch |err| {
self.diagnostic = .{ .file_stat = .{
@@ -879,7 +878,7 @@ pub const Manifest = struct {
error.Canceled => return error.Canceled,
else => return true,
};
defer file.close();
defer file.close(io);
// Save locally and also save globally (we still hold the global lock).
const stat = file.stat() catch |err| switch (err) {
@@ -894,18 +893,20 @@ pub const Manifest = struct {
}
fn populateFileHash(self: *Manifest, ch_file: *File) !void {
const io = self.cache.io;
if (ch_file.handle) |handle| {
return populateFileHashHandle(self, ch_file, handle);
} else {
const pp = ch_file.prefixed_path;
const dir = self.cache.prefixes()[pp.prefix].handle;
const handle = try dir.openFile(pp.sub_path, .{});
defer handle.close();
defer handle.close(io);
return populateFileHashHandle(self, ch_file, handle);
}
}
fn populateFileHashHandle(self: *Manifest, ch_file: *File, handle: fs.File) !void {
fn populateFileHashHandle(self: *Manifest, ch_file: *File, handle: Io.File) !void {
const actual_stat = try handle.stat();
ch_file.stat = .{
.size = actual_stat.size,
@@ -1064,12 +1065,12 @@ pub const Manifest = struct {
self.hash.hasher.update(&new_file.bin_digest);
}
pub fn addDepFilePost(self: *Manifest, dir: fs.Dir, dep_file_sub_path: []const u8) !void {
pub fn addDepFilePost(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void {
assert(self.manifest_file != null);
return self.addDepFileMaybePost(dir, dep_file_sub_path);
}
fn addDepFileMaybePost(self: *Manifest, dir: fs.Dir, dep_file_sub_path: []const u8) !void {
fn addDepFileMaybePost(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void {
const gpa = self.cache.gpa;
const dep_file_contents = try dir.readFileAlloc(dep_file_sub_path, gpa, .limited(manifest_file_size_max));
defer gpa.free(dep_file_contents);
@@ -1148,7 +1149,7 @@ pub const Manifest = struct {
}
}
fn writeDirtyManifestToStream(self: *Manifest, fw: *fs.File.Writer) !void {
fn writeDirtyManifestToStream(self: *Manifest, fw: *Io.File.Writer) !void {
try fw.interface.writeAll(manifest_header ++ "\n");
for (self.files.keys()) |file| {
try fw.interface.print("{d} {d} {d} {x} {d} {s}\n", .{
@@ -1214,13 +1215,15 @@ pub const Manifest = struct {
/// `Manifest.hit` must be called first.
/// Don't forget to call `writeManifest` before this!
pub fn deinit(self: *Manifest) void {
const io = self.cache.io;
if (self.manifest_file) |file| {
if (builtin.os.tag == .windows) {
// See Lock.release for why this is required on Windows
file.unlock();
}
file.close();
file.close(io);
}
for (self.files.keys()) |*file| {
file.deinit(self.cache.gpa);
@@ -1281,7 +1284,7 @@ pub const Manifest = struct {
/// On operating systems that support symlinks, does a readlink. On other operating systems,
/// uses the file contents. Windows supports symlinks but only with elevated privileges, so
/// it is treated as not supporting symlinks.
pub fn readSmallFile(dir: fs.Dir, sub_path: []const u8, buffer: []u8) ![]u8 {
pub fn readSmallFile(dir: Io.Dir, sub_path: []const u8, buffer: []u8) ![]u8 {
if (builtin.os.tag == .windows) {
return dir.readFile(sub_path, buffer);
} else {
@@ -1293,7 +1296,7 @@ pub fn readSmallFile(dir: fs.Dir, sub_path: []const u8, buffer: []u8) ![]u8 {
/// uses the file contents. Windows supports symlinks but only with elevated privileges, so
/// it is treated as not supporting symlinks.
/// `data` must be a valid UTF-8 encoded file path and 255 bytes or fewer.
pub fn writeSmallFile(dir: fs.Dir, sub_path: []const u8, data: []const u8) !void {
pub fn writeSmallFile(dir: Io.Dir, sub_path: []const u8, data: []const u8) !void {
assert(data.len <= 255);
if (builtin.os.tag == .windows) {
return dir.writeFile(.{ .sub_path = sub_path, .data = data });
@@ -1302,7 +1305,7 @@ pub fn writeSmallFile(dir: fs.Dir, sub_path: []const u8, data: []const u8) !void
}
}
fn hashFile(file: fs.File, bin_digest: *[Hasher.mac_length]u8) fs.File.PReadError!void {
fn hashFile(file: Io.File, bin_digest: *[Hasher.mac_length]u8) Io.File.PReadError!void {
var buf: [1024]u8 = undefined;
var hasher = hasher_init;
var off: u64 = 0;
@@ -1316,7 +1319,7 @@ fn hashFile(file: fs.File, bin_digest: *[Hasher.mac_length]u8) fs.File.PReadErro
}
// Create/Write a file, close it, then grab its stat.mtime timestamp.
fn testGetCurrentFileTimestamp(dir: fs.Dir) !Io.Timestamp {
fn testGetCurrentFileTimestamp(io: Io, dir: Io.Dir) !Io.Timestamp {
const test_out_file = "test-filetimestamp.tmp";
var file = try dir.createFile(test_out_file, .{
@@ -1324,7 +1327,7 @@ fn testGetCurrentFileTimestamp(dir: fs.Dir) !Io.Timestamp {
.truncate = true,
});
defer {
file.close();
file.close(io);
dir.deleteFile(test_out_file) catch {};
}
@@ -1343,8 +1346,8 @@ test "cache file and then recall it" {
try tmp.dir.writeFile(.{ .sub_path = temp_file, .data = "Hello, world!\n" });
// Wait for file timestamps to tick
const initial_time = try testGetCurrentFileTimestamp(tmp.dir);
while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time.nanoseconds) {
const initial_time = try testGetCurrentFileTimestamp(io, tmp.dir);
while ((try testGetCurrentFileTimestamp(io, tmp.dir)).nanoseconds == initial_time.nanoseconds) {
try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
}
@@ -1358,7 +1361,7 @@ test "cache file and then recall it" {
.manifest_dir = try tmp.dir.makeOpenPath(temp_manifest_dir, .{}),
};
cache.addPrefix(.{ .path = null, .handle = tmp.dir });
defer cache.manifest_dir.close();
defer cache.manifest_dir.close(io);
{
var ch = cache.obtain();
@@ -1424,7 +1427,7 @@ test "check that changing a file makes cache fail" {
.manifest_dir = try tmp.dir.makeOpenPath(temp_manifest_dir, .{}),
};
cache.addPrefix(.{ .path = null, .handle = tmp.dir });
defer cache.manifest_dir.close();
defer cache.manifest_dir.close(io);
{
var ch = cache.obtain();
@@ -1484,7 +1487,7 @@ test "no file inputs" {
.manifest_dir = try tmp.dir.makeOpenPath(temp_manifest_dir, .{}),
};
cache.addPrefix(.{ .path = null, .handle = tmp.dir });
defer cache.manifest_dir.close();
defer cache.manifest_dir.close(io);
{
var man = cache.obtain();
@@ -1543,7 +1546,7 @@ test "Manifest with files added after initial hash work" {
.manifest_dir = try tmp.dir.makeOpenPath(temp_manifest_dir, .{}),
};
cache.addPrefix(.{ .path = null, .handle = tmp.dir });
defer cache.manifest_dir.close();
defer cache.manifest_dir.close(io);
{
var ch = cache.obtain();
+6 -4
View File
@@ -1,7 +1,9 @@
const Directory = @This();
const std = @import("../../std.zig");
const assert = std.debug.assert;
const Io = std.Io;
const fs = std.fs;
const assert = std.debug.assert;
const fmt = std.fmt;
const Allocator = std.mem.Allocator;
@@ -9,7 +11,7 @@ const Allocator = std.mem.Allocator;
/// directly, but it is needed when passing the directory to a child process.
/// `null` means cwd.
path: ?[]const u8,
handle: fs.Dir,
handle: Io.Dir,
pub fn clone(d: Directory, arena: Allocator) Allocator.Error!Directory {
return .{
@@ -21,7 +23,7 @@ pub fn clone(d: Directory, arena: Allocator) Allocator.Error!Directory {
pub fn cwd() Directory {
return .{
.path = null,
.handle = fs.cwd(),
.handle = .cwd(),
};
}
@@ -64,5 +66,5 @@ pub fn format(self: Directory, writer: *std.Io.Writer) std.Io.Writer.Error!void
}
pub fn eql(self: Directory, other: Directory) bool {
return self.handle.fd == other.handle.fd;
return self.handle.handle == other.handle.handle;
}
+12 -12
View File
@@ -2,8 +2,8 @@ const Path = @This();
const std = @import("../../std.zig");
const Io = std.Io;
const assert = std.debug.assert;
const fs = std.fs;
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Cache = std.Build.Cache;
@@ -62,8 +62,8 @@ pub fn joinStringZ(p: Path, gpa: Allocator, sub_path: []const u8) Allocator.Erro
pub fn openFile(
p: Path,
sub_path: []const u8,
flags: fs.File.OpenFlags,
) !fs.File {
flags: Io.File.OpenFlags,
) !Io.File {
var buf: [fs.max_path_bytes]u8 = undefined;
const joined_path = if (p.sub_path.len == 0) sub_path else p: {
break :p std.fmt.bufPrint(&buf, "{s}" ++ fs.path.sep_str ++ "{s}", .{
@@ -76,8 +76,8 @@ pub fn openFile(
pub fn openDir(
p: Path,
sub_path: []const u8,
args: fs.Dir.OpenOptions,
) fs.Dir.OpenError!fs.Dir {
args: Io.Dir.OpenOptions,
) Io.Dir.OpenError!Io.Dir {
var buf: [fs.max_path_bytes]u8 = undefined;
const joined_path = if (p.sub_path.len == 0) sub_path else p: {
break :p std.fmt.bufPrint(&buf, "{s}" ++ fs.path.sep_str ++ "{s}", .{
@@ -87,7 +87,7 @@ pub fn openDir(
return p.root_dir.handle.openDir(joined_path, args);
}
pub fn makeOpenPath(p: Path, sub_path: []const u8, opts: fs.Dir.OpenOptions) !fs.Dir {
pub fn makeOpenPath(p: Path, sub_path: []const u8, opts: Io.Dir.OpenOptions) !Io.Dir {
var buf: [fs.max_path_bytes]u8 = undefined;
const joined_path = if (p.sub_path.len == 0) sub_path else p: {
break :p std.fmt.bufPrint(&buf, "{s}" ++ fs.path.sep_str ++ "{s}", .{
@@ -97,7 +97,7 @@ pub fn makeOpenPath(p: Path, sub_path: []const u8, opts: fs.Dir.OpenOptions) !fs
return p.root_dir.handle.makeOpenPath(joined_path, opts);
}
pub fn statFile(p: Path, sub_path: []const u8) !fs.Dir.Stat {
pub fn statFile(p: Path, sub_path: []const u8) !Io.Dir.Stat {
var buf: [fs.max_path_bytes]u8 = undefined;
const joined_path = if (p.sub_path.len == 0) sub_path else p: {
break :p std.fmt.bufPrint(&buf, "{s}" ++ fs.path.sep_str ++ "{s}", .{
@@ -110,7 +110,7 @@ pub fn statFile(p: Path, sub_path: []const u8) !fs.Dir.Stat {
pub fn atomicFile(
p: Path,
sub_path: []const u8,
options: fs.Dir.AtomicFileOptions,
options: Io.Dir.AtomicFileOptions,
buf: *[fs.max_path_bytes]u8,
) !fs.AtomicFile {
const joined_path = if (p.sub_path.len == 0) sub_path else p: {
@@ -180,7 +180,7 @@ pub fn formatEscapeChar(path: Path, writer: *Io.Writer) Io.Writer.Error!void {
}
pub fn format(self: Path, writer: *Io.Writer) Io.Writer.Error!void {
if (std.fs.path.isAbsolute(self.sub_path)) {
if (fs.path.isAbsolute(self.sub_path)) {
try writer.writeAll(self.sub_path);
return;
}
@@ -225,9 +225,9 @@ pub const TableAdapter = struct {
pub fn hash(self: TableAdapter, a: Cache.Path) u32 {
_ = self;
const seed = switch (@typeInfo(@TypeOf(a.root_dir.handle.fd))) {
.pointer => @intFromPtr(a.root_dir.handle.fd),
.int => @as(u32, @bitCast(a.root_dir.handle.fd)),
const seed = switch (@typeInfo(@TypeOf(a.root_dir.handle.handle))) {
.pointer => @intFromPtr(a.root_dir.handle.handle),
.int => @as(u32, @bitCast(a.root_dir.handle.handle)),
else => @compileError("unimplemented hash function"),
};
return @truncate(Hash.hash(seed, a.sub_path));
+3 -7
View File
@@ -510,20 +510,16 @@ pub fn installFile(s: *Step, src_lazy_path: Build.LazyPath, dest_path: []const u
const io = b.graph.io;
const src_path = src_lazy_path.getPath3(b, s);
try handleVerbose(b, null, &.{ "install", "-C", b.fmt("{f}", .{src_path}), dest_path });
return Io.Dir.updateFile(src_path.root_dir.handle.adaptToNewApi(), io, src_path.sub_path, .cwd(), dest_path, .{}) catch |err| {
return s.fail("unable to update file from '{f}' to '{s}': {t}", .{
src_path, dest_path, err,
});
};
return Io.Dir.updateFile(src_path.root_dir.handle, io, src_path.sub_path, .cwd(), dest_path, .{}) catch |err|
return s.fail("unable to update file from '{f}' to '{s}': {t}", .{ src_path, dest_path, err });
}
/// Wrapper around `std.fs.Dir.makePathStatus` that handles verbose and error output.
pub fn installDir(s: *Step, dest_path: []const u8) !std.fs.Dir.MakePathStatus {
const b = s.owner;
try handleVerbose(b, null, &.{ "install", "-d", dest_path });
return std.fs.cwd().makePathStatus(dest_path) catch |err| {
return std.fs.cwd().makePathStatus(dest_path) catch |err|
return s.fail("unable to create dir '{s}': {t}", .{ dest_path, err });
};
}
fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool, web_server: ?*Build.WebServer, gpa: Allocator) !?Path {
+21 -14
View File
@@ -1,12 +1,15 @@
const Compile = @This();
const builtin = @import("builtin");
const std = @import("std");
const Io = std.Io;
const mem = std.mem;
const fs = std.fs;
const assert = std.debug.assert;
const panic = std.debug.panic;
const StringHashMap = std.StringHashMap;
const Sha256 = std.crypto.hash.sha2.Sha256;
const Allocator = mem.Allocator;
const Allocator = std.mem.Allocator;
const Step = std.Build.Step;
const LazyPath = std.Build.LazyPath;
const PkgConfigPkg = std.Build.PkgConfigPkg;
@@ -15,7 +18,6 @@ const RunError = std.Build.RunError;
const Module = std.Build.Module;
const InstallDir = std.Build.InstallDir;
const GeneratedFile = std.Build.GeneratedFile;
const Compile = @This();
const Path = std.Build.Cache.Path;
pub const base_id: Step.Id = .compile;
@@ -1561,19 +1563,22 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
}
// -I and -L arguments that appear after the last --mod argument apply to all modules.
const cwd: Io.Dir = .cwd();
const io = b.graph.io;
for (b.search_prefixes.items) |search_prefix| {
var prefix_dir = fs.cwd().openDir(search_prefix, .{}) catch |err| {
var prefix_dir = cwd.openDir(io, search_prefix, .{}) catch |err| {
return step.fail("unable to open prefix directory '{s}': {s}", .{
search_prefix, @errorName(err),
});
};
defer prefix_dir.close();
defer prefix_dir.close(io);
// Avoid passing -L and -I flags for nonexistent directories.
// This prevents a warning, that should probably be upgraded to an error in Zig's
// CLI parsing code, when the linker sees an -L directory that does not exist.
if (prefix_dir.access("lib", .{})) |_| {
if (prefix_dir.access(io, "lib", .{})) |_| {
try zig_args.appendSlice(&.{
"-L", b.pathJoin(&.{ search_prefix, "lib" }),
});
@@ -1584,7 +1589,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
}),
}
if (prefix_dir.access("include", .{})) |_| {
if (prefix_dir.access(io, "include", .{})) |_| {
try zig_args.appendSlice(&.{
"-I", b.pathJoin(&.{ search_prefix, "include" }),
});
@@ -1660,7 +1665,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
args_length += arg.len + 1; // +1 to account for null terminator
}
if (args_length >= 30 * 1024) {
try b.cache_root.handle.makePath("args");
try b.cache_root.handle.makePath(io, "args");
const args_to_escape = zig_args.items[2..];
var escaped_args = try std.array_list.Managed([]const u8).initCapacity(arena, args_to_escape.len);
@@ -1693,18 +1698,18 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
_ = try std.fmt.bufPrint(&args_hex_hash, "{x}", .{&args_hash});
const args_file = "args" ++ fs.path.sep_str ++ args_hex_hash;
if (b.cache_root.handle.access(args_file, .{})) |_| {
if (b.cache_root.handle.access(io, args_file, .{})) |_| {
// The args file is already present from a previous run.
} else |err| switch (err) {
error.FileNotFound => {
try b.cache_root.handle.makePath("tmp");
try b.cache_root.handle.makePath(io, "tmp");
const rand_int = std.crypto.random.int(u64);
const tmp_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
try b.cache_root.handle.writeFile(.{ .sub_path = tmp_path, .data = args });
defer b.cache_root.handle.deleteFile(tmp_path) catch {
try b.cache_root.handle.writeFile(io, .{ .sub_path = tmp_path, .data = args });
defer b.cache_root.handle.deleteFile(io, tmp_path) catch {
// It's fine if the temporary file can't be cleaned up.
};
b.cache_root.handle.rename(tmp_path, args_file) catch |rename_err| switch (rename_err) {
b.cache_root.handle.rename(io, tmp_path, args_file) catch |rename_err| switch (rename_err) {
error.PathAlreadyExists => {
// The args file was created by another concurrent build process.
},
@@ -1816,18 +1821,20 @@ pub fn doAtomicSymLinks(
filename_name_only: []const u8,
) !void {
const b = step.owner;
const io = b.graph.io;
const out_dir = fs.path.dirname(output_path) orelse ".";
const out_basename = fs.path.basename(output_path);
// sym link for libfoo.so.1 to libfoo.so.1.2.3
const major_only_path = b.pathJoin(&.{ out_dir, filename_major_only });
fs.cwd().atomicSymLink(out_basename, major_only_path, .{}) catch |err| {
const cwd: Io.Dir = .cwd();
cwd.atomicSymLink(io, out_basename, major_only_path, .{}) catch |err| {
return step.fail("unable to symlink {s} -> {s}: {s}", .{
major_only_path, out_basename, @errorName(err),
});
};
// sym link for libfoo.so to libfoo.so.1
const name_only_path = b.pathJoin(&.{ out_dir, filename_name_only });
fs.cwd().atomicSymLink(filename_major_only, name_only_path, .{}) catch |err| {
cwd.atomicSymLink(io, filename_major_only, name_only_path, .{}) catch |err| {
return step.fail("Unable to symlink {s} -> {s}: {s}", .{
name_only_path, filename_major_only, @errorName(err),
});
+3 -4
View File
@@ -58,16 +58,15 @@ pub fn create(owner: *std.Build, options: Options) *InstallDir {
fn make(step: *Step, options: Step.MakeOptions) !void {
_ = options;
const b = step.owner;
const io = b.graph.io;
const install_dir: *InstallDir = @fieldParentPtr("step", step);
step.clearWatchInputs();
const arena = b.allocator;
const dest_prefix = b.getInstallPath(install_dir.options.install_dir, install_dir.options.install_subdir);
const src_dir_path = install_dir.options.source_dir.getPath3(b, step);
const need_derived_inputs = try step.addDirectoryWatchInput(install_dir.options.source_dir);
var src_dir = src_dir_path.root_dir.handle.openDir(src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
return step.fail("unable to open source directory '{f}': {s}", .{
src_dir_path, @errorName(err),
});
var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
return step.fail("unable to open source directory '{f}': {t}", .{ src_dir_path, err });
};
defer src_dir.close();
var it = try src_dir.walk(arena);
+20 -22
View File
@@ -441,6 +441,7 @@ fn make(step: *Step, make_options: Step.MakeOptions) !void {
_ = make_options;
const b = step.owner;
const io = b.graph.io;
const options: *Options = @fieldParentPtr("step", step);
for (options.args.items) |item| {
@@ -468,18 +469,15 @@ fn make(step: *Step, make_options: Step.MakeOptions) !void {
// Optimize for the hot path. Stat the file, and if it already exists,
// cache hit.
if (b.cache_root.handle.access(sub_path, .{})) |_| {
if (b.cache_root.handle.access(io, sub_path, .{})) |_| {
// This is the hot path, success.
step.result_cached = true;
return;
} else |outer_err| switch (outer_err) {
error.FileNotFound => {
const sub_dirname = fs.path.dirname(sub_path).?;
b.cache_root.handle.makePath(sub_dirname) catch |e| {
return step.fail("unable to make path '{f}{s}': {s}", .{
b.cache_root, sub_dirname, @errorName(e),
});
};
b.cache_root.handle.makePath(io, sub_dirname) catch |e|
return step.fail("unable to make path '{f}{s}': {t}", .{ b.cache_root, sub_dirname, e });
const rand_int = std.crypto.random.int(u64);
const tmp_sub_path = "tmp" ++ fs.path.sep_str ++
@@ -487,40 +485,40 @@ fn make(step: *Step, make_options: Step.MakeOptions) !void {
basename;
const tmp_sub_path_dirname = fs.path.dirname(tmp_sub_path).?;
b.cache_root.handle.makePath(tmp_sub_path_dirname) catch |err| {
return step.fail("unable to make temporary directory '{f}{s}': {s}", .{
b.cache_root, tmp_sub_path_dirname, @errorName(err),
b.cache_root.handle.makePath(io, tmp_sub_path_dirname) catch |err| {
return step.fail("unable to make temporary directory '{f}{s}': {t}", .{
b.cache_root, tmp_sub_path_dirname, err,
});
};
b.cache_root.handle.writeFile(.{ .sub_path = tmp_sub_path, .data = options.contents.items }) catch |err| {
return step.fail("unable to write options to '{f}{s}': {s}", .{
b.cache_root, tmp_sub_path, @errorName(err),
b.cache_root.handle.writeFile(io, .{ .sub_path = tmp_sub_path, .data = options.contents.items }) catch |err| {
return step.fail("unable to write options to '{f}{s}': {t}", .{
b.cache_root, tmp_sub_path, err,
});
};
b.cache_root.handle.rename(tmp_sub_path, sub_path) catch |err| switch (err) {
b.cache_root.handle.rename(io, tmp_sub_path, sub_path) catch |err| switch (err) {
error.PathAlreadyExists => {
// Other process beat us to it. Clean up the temp file.
b.cache_root.handle.deleteFile(tmp_sub_path) catch |e| {
try step.addError("warning: unable to delete temp file '{f}{s}': {s}", .{
b.cache_root, tmp_sub_path, @errorName(e),
b.cache_root.handle.deleteFile(io, tmp_sub_path) catch |e| {
try step.addError("warning: unable to delete temp file '{f}{s}': {t}", .{
b.cache_root, tmp_sub_path, e,
});
};
step.result_cached = true;
return;
},
else => {
return step.fail("unable to rename options from '{f}{s}' to '{f}{s}': {s}", .{
b.cache_root, tmp_sub_path,
b.cache_root, sub_path,
@errorName(err),
return step.fail("unable to rename options from '{f}{s}' to '{f}{s}': {t}", .{
b.cache_root, tmp_sub_path,
b.cache_root, sub_path,
err,
});
},
};
},
else => |e| return step.fail("unable to access options file '{f}{s}': {s}", .{
b.cache_root, sub_path, @errorName(e),
else => |e| return step.fail("unable to access options file '{f}{s}': {t}", .{
b.cache_root, sub_path, e,
}),
}
}
+19 -8
View File
@@ -662,16 +662,27 @@ pub const VTable = struct {
futexWaitUncancelable: *const fn (?*anyopaque, ptr: *const u32, expected: u32) void,
futexWake: *const fn (?*anyopaque, ptr: *const u32, max_waiters: u32) void,
dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.Mode) Dir.MakeError!void,
dirMakePath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.Mode) Dir.MakePathError!Dir.MakePathStatus,
dirMakeOpenPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.OpenOptions) Dir.MakeOpenPathError!Dir,
dirMake: *const fn (?*anyopaque, Dir, []const u8, Dir.Mode) Dir.MakeError!void,
dirMakePath: *const fn (?*anyopaque, Dir, []const u8, Dir.Mode) Dir.MakePathError!Dir.MakePathStatus,
dirMakeOpenPath: *const fn (?*anyopaque, Dir, []const u8, Dir.OpenOptions) Dir.MakeOpenPathError!Dir,
dirStat: *const fn (?*anyopaque, Dir) Dir.StatError!Dir.Stat,
dirStatPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.StatPathOptions) Dir.StatPathError!File.Stat,
dirAccess: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.AccessOptions) Dir.AccessError!void,
dirCreateFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File,
dirOpenFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File,
dirOpenDir: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.OpenOptions) Dir.OpenError!Dir,
dirStatPath: *const fn (?*anyopaque, Dir, []const u8, Dir.StatPathOptions) Dir.StatPathError!File.Stat,
dirAccess: *const fn (?*anyopaque, Dir, []const u8, Dir.AccessOptions) Dir.AccessError!void,
dirCreateFile: *const fn (?*anyopaque, Dir, []const u8, File.CreateFlags) File.OpenError!File,
dirOpenFile: *const fn (?*anyopaque, Dir, []const u8, File.OpenFlags) File.OpenError!File,
dirOpenDir: *const fn (?*anyopaque, Dir, []const u8, Dir.OpenOptions) Dir.OpenError!Dir,
dirClose: *const fn (?*anyopaque, Dir) void,
dirRead: *const fn (?*anyopaque, *Dir.Reader, []Dir.Entry) Dir.Reader.Error!usize,
dirRealPath: *const fn (?*anyopaque, Dir, path_name: []const u8, out_buffer: []u8) Dir.RealPathError!usize,
dirDeleteFile: *const fn (?*anyopaque, Dir, []const u8) Dir.DeleteFileError!void,
dirDeleteDir: *const fn (?*anyopaque, Dir, []const u8) Dir.DeleteDirError!void,
dirRename: *const fn (?*anyopaque, old_dir: Dir, old_sub_path: []const u8, new_dir: Dir, new_sub_path: []const u8) Dir.RenameError!void,
dirSymLink: *const fn (?*anyopaque, Dir, target_path: []const u8, sym_link_path: []const u8, Dir.SymLinkFlags) Dir.RenameError!void,
dirReadLink: *const fn (?*anyopaque, Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize,
dirSetMode: *const fn (?*anyopaque, Dir, File.Mode) Dir.SetModeError!void,
dirSetOwner: *const fn (?*anyopaque, Dir, ?File.Uid, ?File.Gid) Dir.SetOwnerError!void,
dirSetPermissions: *const fn (?*anyopaque, Dir, Dir.Permissions) Dir.SetPermissionsError!void,
fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat,
fileClose: *const fn (?*anyopaque, File) void,
fileWriteStreaming: *const fn (?*anyopaque, File, buffer: [][]const u8) File.WriteStreamingError!usize,
+1202 -2
View File
@@ -6,12 +6,20 @@ const native_os = builtin.os.tag;
const std = @import("../std.zig");
const Io = std.Io;
const File = Io.File;
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
handle: Handle,
pub const Mode = Io.File.Mode;
pub const default_mode: Mode = 0o755;
pub const Entry = struct {
name: []const u8,
kind: File.Kind,
inode: File.INode,
};
/// Returns a handle to the current working directory.
///
/// It is not opened with iteration capability. Iterating over the result is
@@ -20,6 +28,8 @@ pub const default_mode: Mode = 0o755;
/// Closing the returned `Dir` is checked illegal behavior.
///
/// On POSIX targets, this function is comptime-callable.
///
/// On WASI, the value this returns is application-configurable.
pub fn cwd() Dir {
return switch (native_os) {
.windows => .{ .handle = std.os.windows.peb().ProcessParameters.CurrentDirectory.Handle },
@@ -28,6 +38,270 @@ pub fn cwd() Dir {
};
}
pub const Reader = struct {
dir: Dir,
state: State,
/// Stores I/O implementation specific data.
buffer: [2048]u8 align(@alignOf(usize)),
index: usize,
pub const State = enum {
/// Indicates the next call to `read` should rewind and start over the
/// directory listing.
reset,
reading,
finished,
};
pub const Error = error{
AccessDenied,
PermissionDenied,
SystemResources,
} || Io.UnexpectedError || Io.Cancelable;
pub fn init(dir: Dir) Reader {
return .{
.dir = dir,
.state = .reset,
.index = 0,
.buffer = undefined,
};
}
pub fn read(r: *Reader, io: Io, buffer: []Entry) Error!usize {
return io.vtable.dirRead(io.userdata, r, buffer);
}
};
pub const Iterator = struct {
reader: Reader,
buffer: [32]Entry,
/// Index of next entry in `buffer`.
index: usize,
/// Fill position of `buffer`.
end: usize,
pub const Error = Reader.Error;
pub fn init(dir: Dir, reader_state: Reader.State) Iterator {
return .{
.reader = .{
.dir = dir,
.state = reader_state,
.index = 0,
.buffer = undefined,
},
.buffer = undefined,
.index = 0,
.end = 0,
};
}
pub fn next(it: *Iterator, io: Io) Error!?Entry {
if (it.end - it.index == 0) {
if (it.reader.state == .finished) return null;
it.end = try it.reader.read(io, &it.buffer);
it.index = 0;
if (it.end - it.index == 0) {
assert(it.reader.state == .finished);
return null;
}
}
const index = it.index;
it.index = index + 1;
return it.buffer[index];
}
};
pub fn iterate(dir: Dir) Iterator {
return .init(dir, .reset);
}
/// 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(dir: Dir) Iterator {
return .init(dir, .reading);
}
pub const SelectiveWalker = struct {
stack: std.ArrayList(Walker.StackItem),
name_buffer: std.ArrayList(u8),
allocator: Allocator,
pub const Error = Io.Dir.Iterator.Error || Allocator.Error;
/// 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: *SelectiveWalker) Error!?Walker.Entry {
while (self.stack.items.len > 0) {
const top = &self.stack.items[self.stack.items.len - 1];
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;
}) |entry| {
self.name_buffer.shrinkRetainingCapacity(dirname_len);
if (self.name_buffer.items.len != 0) {
try self.name_buffer.append(self.allocator, std.fs.path.sep);
dirname_len += 1;
}
try self.name_buffer.ensureUnusedCapacity(self.allocator, entry.name.len + 1);
self.name_buffer.appendSliceAssumeCapacity(entry.name);
self.name_buffer.appendAssumeCapacity(0);
const walker_entry: Walker.Entry = .{
.dir = top.iter.dir,
.basename = self.name_buffer.items[dirname_len .. self.name_buffer.items.len - 1 :0],
.path = self.name_buffer.items[0 .. self.name_buffer.items.len - 1 :0],
.kind = entry.kind,
};
return walker_entry;
} else {
var item = self.stack.pop().?;
if (self.stack.items.len != 0) {
item.iter.dir.close();
}
}
}
return null;
}
/// Traverses into the directory, continuing walking one level down.
pub fn enter(self: *SelectiveWalker, entry: Walker.Entry) !void {
if (entry.kind != .directory) {
@branchHint(.cold);
return;
}
var new_dir = entry.dir.openDir(entry.basename, .{ .iterate = true }) catch |err| {
switch (err) {
error.NameTooLong => unreachable,
else => |e| return e,
}
};
errdefer new_dir.close();
try self.stack.append(self.allocator, .{
.iter = new_dir.iterateAssumeFirstIteration(),
.dirname_len = self.name_buffer.items.len - 1,
});
}
pub fn deinit(self: *SelectiveWalker) void {
self.name_buffer.deinit(self.allocator);
self.stack.deinit(self.allocator);
}
/// Leaves the current directory, continuing walking one level up.
/// If the current entry is a directory entry, then the "current directory"
/// will pertain to that entry if `enter` is called before `leave`.
pub fn leave(self: *SelectiveWalker) void {
var item = self.stack.pop().?;
if (self.stack.items.len != 0) {
@branchHint(.likely);
item.iter.dir.close();
}
}
};
/// Recursively iterates over a directory, but requires the user to
/// opt-in to recursing into each directory entry.
///
/// `dir` must have been opened with `OpenOptions{.iterate = true}`.
///
/// `Walker.deinit` releases allocated memory and directory handles.
///
/// The order of returned file system entries is undefined.
///
/// `dir` will not be closed after walking it.
///
/// See also `walk`.
pub fn walkSelectively(dir: Dir, allocator: Allocator) !SelectiveWalker {
var stack: std.ArrayList(Walker.StackItem) = .empty;
try stack.append(allocator, .{
.iter = dir.iterate(),
.dirname_len = 0,
});
return .{
.stack = stack,
.name_buffer = .{},
.allocator = allocator,
};
}
pub const Walker = struct {
inner: SelectiveWalker,
pub const Entry = 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: [:0]const u8,
path: [:0]const u8,
kind: Dir.Entry.Kind,
/// Returns the depth of the entry relative to the initial directory.
/// Returns 1 for a direct child of the initial directory, 2 for an entry
/// within a direct child of the initial directory, etc.
pub fn depth(self: Walker.Entry) usize {
return std.mem.countScalar(u8, self.path, std.fs.path.sep) + 1;
}
};
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) !?Walker.Entry {
const entry = try self.inner.next();
if (entry != null and entry.?.kind == .directory) {
try self.inner.enter(entry.?);
}
return entry;
}
pub fn deinit(self: *Walker) void {
self.inner.deinit();
}
/// Leaves the current directory, continuing walking one level up.
/// If the current entry is a directory entry, then the "current directory"
/// is the directory pertaining to the current entry.
pub fn leave(self: *Walker) void {
self.inner.leave();
}
};
/// Recursively iterates over a directory.
///
/// `dir` must have been opened with `OpenOptions{.iterate = true}`.
///
/// `Walker.deinit` releases allocated memory and directory handles.
///
/// The order of returned file system entries is undefined.
///
/// `dir` will not be closed after walking it.
///
/// See also `walkSelectively`.
pub fn walk(dir: Dir, allocator: Allocator) Allocator.Error!Walker {
return .{ .inner = try walkSelectively(dir, allocator) };
}
pub const Handle = std.posix.fd_t;
pub const PathNameError = error{
@@ -145,7 +419,7 @@ pub const WriteFileOptions = struct {
flags: File.CreateFlags = .{},
};
pub const WriteFileError = File.WriteError || File.OpenError || Io.Cancelable;
pub const WriteFileError = File.WriteError || File.OpenError;
/// Writes content to the file system, using the file creation flags provided.
pub fn writeFile(dir: Dir, io: Io, options: WriteFileOptions) WriteFileError!void {
@@ -179,7 +453,7 @@ pub fn updateFile(
dest_dir: Dir,
/// If directories in this path do not exist, they are created.
dest_path: []const u8,
options: std.fs.Dir.CopyFileOptions,
options: CopyFileOptions,
) !PrevStatus {
var src_file = try source_dir.openFile(io, source_path, .{});
defer src_file.close(io);
@@ -367,3 +641,929 @@ pub const StatPathOptions = struct {
pub fn statPath(dir: Dir, io: Io, sub_path: []const u8, options: StatPathOptions) StatPathError!Stat {
return io.vtable.dirStatPath(io.userdata, dir, sub_path, options);
}
pub const RealPathError = error{
FileNotFound,
AccessDenied,
PermissionDenied,
NameTooLong,
NotSupported,
NotDir,
SymLinkLoop,
InputOutput,
FileTooBig,
IsDir,
ProcessFdQuotaExceeded,
SystemFdQuotaExceeded,
NoDevice,
SystemResources,
NoSpaceLeft,
FileSystem,
DeviceBusy,
ProcessNotFound,
SharingViolation,
PipeBusy,
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
BadPathName,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
PathAlreadyExists,
/// On Windows, antivirus software is enabled by default. It can be
/// disabled, but Windows Update sometimes ignores the user's preference
/// and re-enables it. When enabled, antivirus software on Windows
/// intercepts file system operations and makes them significantly slower
/// in addition to possibly failing with this error code.
AntivirusInterference,
/// On Windows, the volume does not contain a recognized file system. File
/// system drivers might not be loaded, or the volume may be corrupt.
UnrecognizedVolume,
} || Io.Cancelable || Io.UnexpectedError;
/// 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.
///
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
///
/// This function is not universally supported by all platforms. Currently
/// supported hosts are: Linux, macOS, and Windows.
///
/// See also:
/// * `realpathAlloc`.
pub fn realPath(dir: Dir, io: Io, sub_path: []const u8, out_buffer: []u8) RealPathError!usize {
return io.vtable.dirRealPath(io.userdata, dir, sub_path, out_buffer);
}
pub const RealPathAllocError = RealPathError || Allocator.Error;
/// 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) RealPathAllocError![]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: [std.fs.max_path_bytes]u8 = undefined;
return allocator.dupe(u8, try self.realpath(pathname, &buf));
}
pub const DeleteFileError = error{
FileNotFound,
/// In WASI, this error may occur when the file descriptor does
/// not hold the required rights to unlink a resource by path relative to it.
AccessDenied,
PermissionDenied,
FileBusy,
FileSystem,
IsDir,
SymLinkLoop,
NameTooLong,
NotDir,
SystemResources,
ReadOnlyFileSystem,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
/// Windows: file paths cannot contain these characters:
/// '/', '*', '?', '"', '<', '>', '|'
BadPathName,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
} || Io.Cancelable || Io.UnexpectedError;
/// Delete a file name and possibly the file it refers to, based on an open directory handle.
///
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
///
/// Asserts that the path parameter has no null bytes.
pub fn deleteFile(dir: Dir, io: Io, sub_path: []const u8) DeleteFileError!void {
return io.vtable.dirDeleteFile(io.userdata, dir, sub_path);
}
pub const DeleteDirError = error{
DirNotEmpty,
FileNotFound,
AccessDenied,
PermissionDenied,
FileBusy,
FileSystem,
SymLinkLoop,
NameTooLong,
NotDir,
SystemResources,
ReadOnlyFileSystem,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
BadPathName,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
} || Io.Cancelable || Io.UnexpectedError;
/// Returns `error.DirNotEmpty` if the directory is not empty.
///
/// To delete a directory recursively, see `deleteTree`.
///
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
pub fn deleteDir(dir: Dir, io: Io, sub_path: []const u8) DeleteDirError!void {
return io.vtable.dirDeleteDir(io.userdata, dir, sub_path);
}
pub const RenameError = error{
/// In WASI, this error may occur when the file descriptor does
/// not hold the required rights to rename a resource by path relative to it.
///
/// On Windows, this error may be returned instead of PathAlreadyExists when
/// renaming a directory over an existing directory.
AccessDenied,
PermissionDenied,
FileBusy,
DiskQuota,
IsDir,
SymLinkLoop,
LinkQuotaExceeded,
NameTooLong,
FileNotFound,
NotDir,
SystemResources,
NoSpaceLeft,
PathAlreadyExists,
ReadOnlyFileSystem,
RenameAcrossMountPoints,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
BadPathName,
NoDevice,
SharingViolation,
PipeBusy,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
/// On Windows, antivirus software is enabled by default. It can be
/// disabled, but Windows Update sometimes ignores the user's preference
/// and re-enables it. When enabled, antivirus software on Windows
/// intercepts file system operations and makes them significantly slower
/// in addition to possibly failing with this error code.
AntivirusInterference,
} || Io.Cancelable || Io.UnexpectedError;
/// 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`
///
/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, both paths should be encoded as valid UTF-8.
/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
pub fn rename(
old_dir: Dir,
old_sub_path: []const u8,
new_dir: Dir,
new_sub_path: []const u8,
io: Io,
) RenameError!void {
return io.vtable.dirRename(io.userdata, old_dir, old_sub_path, new_dir, new_sub_path);
}
/// Use with `Dir.symLink`, `Dir.symLinkAtomic`, 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,
};
pub const SymLinkError = error{
/// In WASI, this error may occur when the file descriptor does
/// not hold the required rights to create a new symbolic link relative to it.
AccessDenied,
PermissionDenied,
DiskQuota,
PathAlreadyExists,
FileSystem,
SymLinkLoop,
FileNotFound,
SystemResources,
NoSpaceLeft,
ReadOnlyFileSystem,
NotDir,
NameTooLong,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
BadPathName,
} || Io.Cancelable || Io.UnexpectedError;
/// 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.
///
/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, both paths should be encoded as valid UTF-8.
/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
pub fn symLink(
dir: Dir,
io: Io,
target_path: []const u8,
sym_link_path: []const u8,
flags: SymLinkFlags,
) SymLinkError!void {
return io.vtable.dirSymLink(io.userdata, dir, target_path, sym_link_path, flags);
}
/// Same as `symLink`, except tries to create the symbolic link until it
/// succeeds or encounters an error other than `error.PathAlreadyExists`.
///
/// * On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// * On WASI, both paths should be encoded as valid UTF-8.
/// * On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
pub fn symLinkAtomic(
dir: Dir,
io: Io,
target_path: []const u8,
sym_link_path: []const u8,
flags: SymLinkFlags,
) !void {
if (dir.symLink(io, target_path, sym_link_path, flags)) {
return;
} else |err| switch (err) {
error.PathAlreadyExists => {},
else => |e| return e,
}
const dirname = std.fs.path.dirname(sym_link_path) orelse ".";
const rand_len = @sizeOf(u64) * 2;
const temp_path_len = dirname.len + 1 + rand_len;
var temp_path_buf: [std.fs.max_path_bytes]u8 = undefined;
if (temp_path_len > temp_path_buf.len) return error.NameTooLong;
@memcpy(temp_path_buf[0..dirname.len], dirname);
temp_path_buf[dirname.len] = std.fs.path.sep;
const temp_path = temp_path_buf[0..temp_path_len];
while (true) {
const random_integer = std.crypto.random.int(u64);
temp_path[dirname.len + 1 ..][0..rand_len].* = std.fmt.hex(random_integer);
if (dir.symLink(io, target_path, temp_path, flags)) {
return dir.rename(temp_path, dir, io, sym_link_path);
} else |err| switch (err) {
error.PathAlreadyExists => continue,
else => |e| return e,
}
}
}
pub const ReadLinkError = error{
/// In WASI, this error may occur when the file descriptor does
/// not hold the required rights to read value of a symbolic link relative to it.
AccessDenied,
PermissionDenied,
FileSystem,
SymLinkLoop,
NameTooLong,
FileNotFound,
SystemResources,
NotLink,
NotDir,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
BadPathName,
/// Windows-only. This error may occur if the opened reparse point is
/// of unsupported type.
UnsupportedReparsePointType,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
/// On Windows, antivirus software is enabled by default. It can be
/// disabled, but Windows Update sometimes ignores the user's preference
/// and re-enables it. When enabled, antivirus software on Windows
/// intercepts file system operations and makes them significantly slower
/// in addition to possibly failing with this error code.
AntivirusInterference,
} || Io.Cancelable || Io.UnexpectedError;
/// Obtain target of a symbolic link.
///
/// Returns how many bytes of `buffer` are populated.
///
/// Asserts that the path parameter has no null bytes.
///
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
pub fn readLink(dir: Dir, io: Io, sub_path: []const u8, buffer: []u8) ReadLinkError!usize {
return io.vtable.dirReadLink(io.userdata, dir, sub_path, buffer);
}
pub const ReadFileAllocError = File.OpenError || File.ReadError || Allocator.Error || error{
/// File size reached or exceeded the provided limit.
StreamTooLong,
};
/// Reads all the bytes from the named file. On success, caller owns returned
/// buffer.
///
/// If the file size is already known, a better alternative is to initialize a
/// `File.Reader`.
///
/// If the file size cannot be obtained, an error is returned. If
/// this is a realistic possibility, a better alternative is to initialize a
/// `File.Reader` which handles this seamlessly.
pub fn readFileAlloc(
dir: Dir,
io: Io,
/// On Windows, should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, should be encoded as valid UTF-8.
/// On other platforms, an opaque sequence of bytes with no particular encoding.
sub_path: []const u8,
/// Used to allocate the result.
gpa: Allocator,
/// If reached or exceeded, `error.StreamTooLong` is returned instead.
limit: Io.Limit,
) ReadFileAllocError![]u8 {
return readFileAllocOptions(dir, io, sub_path, gpa, limit, .of(u8), null);
}
/// Reads all the bytes from the named file. On success, caller owns returned
/// buffer.
///
/// If the file size is already known, a better alternative is to initialize a
/// `File.Reader`.
pub fn readFileAllocOptions(
dir: Dir,
io: Io,
/// On Windows, should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, should be encoded as valid UTF-8.
/// On other platforms, an opaque sequence of bytes with no particular encoding.
sub_path: []const u8,
/// Used to allocate the result.
gpa: Allocator,
/// If reached or exceeded, `error.StreamTooLong` is returned instead.
limit: Io.Limit,
comptime alignment: std.mem.Alignment,
comptime sentinel: ?u8,
) ReadFileAllocError!(if (sentinel) |s| [:s]align(alignment.toByteUnits()) u8 else []align(alignment.toByteUnits()) u8) {
var file = try dir.openFile(io, sub_path, .{});
defer file.close(io);
var file_reader = file.reader(io, &.{});
return file_reader.interface.allocRemainingAlignedSentinel(gpa, limit, alignment, sentinel) catch |err| switch (err) {
error.ReadFailed => return file_reader.err.?,
error.OutOfMemory, error.StreamTooLong => |e| return e,
};
}
pub const DeleteTreeError = error{
AccessDenied,
PermissionDenied,
FileTooBig,
SymLinkLoop,
ProcessFdQuotaExceeded,
NameTooLong,
SystemFdQuotaExceeded,
NoDevice,
SystemResources,
ReadOnlyFileSystem,
FileSystem,
FileBusy,
DeviceBusy,
ProcessNotFound,
/// One of the path components was not a directory.
/// This error is unreachable if `sub_path` does not contain a path separator.
NotDir,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
/// On Windows, file paths cannot contain these characters:
/// '/', '*', '?', '"', '<', '>', '|'
BadPathName,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
} || Io.Cancelable || Io.UnexpectedError;
/// Whether `sub_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.
///
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
pub fn deleteTree(dir: Dir, io: Io, sub_path: []const u8) DeleteTreeError!void {
var initial_iterable_dir = (try dir.deleteTreeOpenInitialSubpath(io, sub_path, .file)) orelse return;
const StackItem = struct {
name: []const u8,
parent_dir: Dir,
iter: Dir.Iterator,
fn closeAll(inner_io: Io, items: []@This()) void {
for (items) |*item| item.iter.dir.close(inner_io);
}
};
var stack_buffer: [16]StackItem = undefined;
var stack = std.ArrayList(StackItem).initBuffer(&stack_buffer);
defer StackItem.closeAll(io, stack.items);
stack.appendAssumeCapacity(.{
.name = sub_path,
.parent_dir = dir,
.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(io, entry.name, .{
.follow_symlinks = false,
.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.AccessDenied,
error.PermissionDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
error.Canceled,
=> |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(io, entry.name, entry.kind);
break :handle_entry;
}
} else {
if (top.iter.dir.deleteFile(io, 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.PermissionDenied,
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(io);
// 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, .{
.follow_symlinks = false,
.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.AccessDenied,
error.PermissionDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
error.Canceled,
=> |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.PermissionDenied,
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.
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
pub fn deleteTreeMinStackSize(dir: Dir, io: Io, sub_path: []const u8) DeleteTreeError!void {
return dir.deleteTreeMinStackSizeWithKindHint(io, sub_path, .file);
}
fn deleteTreeMinStackSizeWithKindHint(parent: Dir, io: Io, sub_path: []const u8, kind_hint: File.Kind) DeleteTreeError!void {
start_over: while (true) {
var dir = (try parent.deleteTreeOpenInitialSubpath(io, 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: [std.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, .{
.follow_symlinks = false,
.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.AccessDenied,
error.PermissionDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
error.Canceled,
=> |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.PermissionDenied,
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(io, 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 {
parent.deleteDir(io, 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(dir: 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 dir.openDir(sub_path, .{
.follow_symlinks = false,
.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.AccessDenied,
error.PermissionDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.BadPathName,
error.DeviceBusy,
error.NetworkNotFound,
error.Canceled,
=> |e| return e,
};
} else {
if (dir.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.PermissionDenied,
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 CopyFileOptions = struct {
/// When this is `null` the mode is copied from the source file.
override_mode: ?File.Mode = null,
};
pub const CopyFileError = File.OpenError || File.StatError ||
File.Atomic.InitError || File.Atomic.FinishError ||
File.ReadError || File.WriteError || error{InvalidFileName};
/// Atomically creates a new file at `dest_path` within `dest_dir` with the
/// same contents as `source_path` within `source_dir`, overwriting any already
/// existing file.
///
/// 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.
///
/// On Windows, both paths should be encoded as
/// [WTF-8](https://wtf-8.codeberg.page/). On WASI, both paths should be
/// encoded as valid UTF-8. On other platforms, both paths are an opaque
/// sequence of bytes with no particular encoding.
pub fn copyFile(
source_dir: Dir,
source_path: []const u8,
dest_dir: Dir,
dest_path: []const u8,
io: Io,
options: CopyFileOptions,
) CopyFileError!void {
const file = try source_dir.openFile(io, source_path, .{});
var file_reader: File.Reader = .init(.{ .handle = file.handle }, io, &.{});
defer file_reader.file.close(io);
const mode = options.override_mode orelse blk: {
const st = try file_reader.file.stat(io);
file_reader.size = st.size;
break :blk st.mode;
};
var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available.
var atomic_file = try dest_dir.atomicFile(io, dest_path, .{
.mode = mode,
.write_buffer = &buffer,
});
defer atomic_file.deinit(io);
_ = atomic_file.file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
error.ReadFailed => return file_reader.err.?,
error.WriteFailed => return atomic_file.file_writer.err.?,
};
try atomic_file.finish();
}
pub const AtomicFileOptions = struct {
mode: File.Mode = File.default_mode,
make_path: bool = false,
write_buffer: []u8,
};
/// Directly access the `.file` field, and then call `File.Atomic.finish` to
/// atomically replace `dest_path` with contents.
///
/// Always call `File.Atomic.deinit` to clean up, regardless of whether
/// `File.Atomic.finish` succeeded. `dest_path` must remain valid until
/// `File.Atomic.deinit` is called.
///
/// On Windows, `dest_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `dest_path` should be encoded as valid UTF-8.
/// On other platforms, `dest_path` is an opaque sequence of bytes with no particular encoding.
pub fn atomicFile(parent: Dir, io: Io, dest_path: []const u8, options: AtomicFileOptions) !File.Atomic {
if (std.fs.path.dirname(dest_path)) |dirname| {
const dir = if (options.make_path)
try parent.makeOpenPath(io, dirname, .{})
else
try parent.openDir(io, dirname, .{});
return .init(std.fs.path.basename(dest_path), options.mode, dir, true, options.write_buffer);
} else {
return .init(dest_path, options.mode, parent, false, options.write_buffer);
}
}
pub const SetModeError = File.SetModeError;
/// Also known as "chmod".
///
/// 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 `OpenOptions.iterate` set to `true`.
pub fn setMode(dir: Dir, io: Io, new_mode: File.Mode) SetModeError!void {
return io.vtable.dirSetMode(io.userdata, dir, new_mode);
}
pub const SetOwnerError = File.SetOwnerError;
/// Also known as "chown".
///
/// 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 `OpenOptions.iterate` set to `true`. If the
/// owner or group is specified as `null`, the ID is not changed.
pub fn setOwner(dir: Dir, io: Io, owner: ?File.Uid, group: ?File.Gid) SetOwnerError!void {
return io.vtable.dirSetOwner(io.userdata, dir, owner, group);
}
pub const SetPermissionsError = File.SetPermissionsError;
pub const Permissions = File.Permissions;
pub fn setPermissions(dir: Dir, io: Io, permissions: Permissions) SetPermissionsError!void {
return io.vtable.dirSetPermissions(io.userdata, dir, permissions);
}
+184 -6
View File
@@ -7,12 +7,23 @@ const is_windows = native_os == .windows;
const std = @import("../std.zig");
const Io = std.Io;
const assert = std.debug.assert;
const Dir = std.Io.Dir;
handle: Handle,
pub const Handle = std.posix.fd_t;
pub const Mode = std.posix.mode_t;
pub const INode = std.posix.ino_t;
pub const Uid = std.posix.uid_t;
pub const Gid = std.posix.gid_t;
/// 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: Mode = if (Mode == u0) 0 else 0o666;
pub const Kind = enum {
block_device,
@@ -92,6 +103,11 @@ pub const Lock = enum {
exclusive,
};
pub const LockError = error{
SystemResources,
FileLocksNotSupported,
} || Io.UnexpectedError;
pub const OpenFlags = struct {
mode: OpenMode = .read_only,
@@ -141,7 +157,53 @@ pub const OpenFlags = struct {
}
};
pub const CreateFlags = std.fs.File.CreateFlags;
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.
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,
};
pub const OpenError = error{
SharingViolation,
@@ -231,6 +293,17 @@ pub fn writePositional(file: File, io: Io, buffer: [][]const u8, offset: u64) Wr
return io.vtable.fileWritePositional(io.userdata, file, buffer, offset);
}
/// Opens a file for reading or writing, without attempting to create a new
/// file, based on an absolute path.
///
/// Returns an open resource to be released with `close`.
///
/// Asserts that the path is absolute. See `Dir.openFile` for a function that
/// operates on both absolute and relative paths.
///
/// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `absolute_path` should be encoded as valid UTF-8.
/// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding.
pub fn openAbsolute(io: Io, absolute_path: []const u8, flags: OpenFlags) OpenError!File {
assert(std.fs.path.isAbsolute(absolute_path));
return Io.Dir.cwd().openFile(io, absolute_path, flags);
@@ -364,11 +437,6 @@ pub const Reader = struct {
};
}
/// Takes a legacy `std.fs.File` to help with upgrading.
pub fn initAdapted(file: std.fs.File, io: Io, buffer: []u8) Reader {
return .init(.{ .handle = file.handle }, io, buffer);
}
pub fn initSize(file: File, io: Io, buffer: []u8, size: ?u64) Reader {
return .{
.io = io,
@@ -652,3 +720,113 @@ pub const Reader = struct {
return size - logicalPos(r) == 0;
}
};
pub const Atomic = struct {
file_writer: File.Writer,
random_integer: u64,
dest_basename: []const u8,
file_open: bool,
file_exists: bool,
close_dir_on_deinit: bool,
dir: Dir,
pub const InitError = File.OpenError;
/// 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,
write_buffer: []u8,
) InitError!Atomic {
while (true) {
const random_integer = std.crypto.random.int(u64);
const tmp_sub_path = std.fmt.hex(random_integer);
const file = dir.createFile(&tmp_sub_path, .{ .mode = mode, .exclusive = true }) catch |err| switch (err) {
error.PathAlreadyExists => continue,
else => |e| return e,
};
return .{
.file_writer = file.writer(write_buffer),
.random_integer = random_integer,
.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(af: *Atomic) void {
if (af.file_open) {
af.file_writer.file.close();
af.file_open = false;
}
if (af.file_exists) {
const tmp_sub_path = std.fmt.hex(af.random_integer);
af.dir.deleteFile(&tmp_sub_path) catch {};
af.file_exists = false;
}
if (af.close_dir_on_deinit) {
af.dir.close();
}
af.* = undefined;
}
pub const FlushError = File.WriteError;
pub fn flush(af: *Atomic) FlushError!void {
af.file_writer.interface.flush() catch |err| switch (err) {
error.WriteFailed => return af.file_writer.err.?,
};
}
pub const RenameIntoPlaceError = Dir.RenameError;
/// On Windows, this function introduces a period of time where some file
/// system operations on the destination file will result in
/// `error.AccessDenied`, including rename operations (such as the one used in
/// this function).
pub fn renameIntoPlace(af: *Atomic) RenameIntoPlaceError!void {
const io = af.file_writer.io;
assert(af.file_exists);
if (af.file_open) {
af.file_writer.file.close();
af.file_open = false;
}
const tmp_sub_path = std.fmt.hex(af.random_integer);
try af.dir.rename(&tmp_sub_path, af.dir, af.dest_basename, io);
af.file_exists = false;
}
pub const FinishError = FlushError || RenameIntoPlaceError;
/// Combination of `flush` followed by `renameIntoPlace`.
pub fn finish(af: *Atomic) FinishError!void {
try af.flush();
try af.renameIntoPlace();
}
};
pub const SetModeError = error{
AccessDenied,
PermissionDenied,
InputOutput,
SymLinkLoop,
FileNotFound,
SystemResources,
ReadOnlyFileSystem,
} || Io.Cancelable || Io.UnexpectedError;
pub const SetOwnerError = error{
AccessDenied,
PermissionDenied,
InputOutput,
SymLinkLoop,
FileNotFound,
SystemResources,
ReadOnlyFileSystem,
} || Io.Cancelable || Io.UnexpectedError;
+1445 -6
View File
@@ -657,12 +657,22 @@ pub fn io(t: *Threaded) Io {
.dirMakeOpenPath = dirMakeOpenPath,
.dirStat = dirStat,
.dirStatPath = dirStatPath,
.fileStat = fileStat,
.dirAccess = dirAccess,
.dirCreateFile = dirCreateFile,
.dirOpenFile = dirOpenFile,
.dirOpenDir = dirOpenDir,
.dirClose = dirClose,
.dirRealPath = dirRealPath,
.dirDeleteFile = dirDeleteFile,
.dirDeleteDir = dirDeleteDir,
.dirRename = dirRename,
.dirSymLink = dirSymLink,
.dirReadLink = dirReadLink,
.dirSetMode = dirSetMode,
.dirSetOwner = dirSetOwner,
.dirSetPermissions = dirSetPermissions,
.fileStat = fileStat,
.fileClose = fileClose,
.fileWriteStreaming = fileWriteStreaming,
.fileWritePositional = fileWritePositional,
@@ -753,12 +763,22 @@ pub fn ioBasic(t: *Threaded) Io {
.dirMakeOpenPath = dirMakeOpenPath,
.dirStat = dirStat,
.dirStatPath = dirStatPath,
.fileStat = fileStat,
.dirAccess = dirAccess,
.dirCreateFile = dirCreateFile,
.dirOpenFile = dirOpenFile,
.dirOpenDir = dirOpenDir,
.dirClose = dirClose,
.dirRealPath = dirRealPath,
.dirDeleteFile = dirDeleteFile,
.dirDeleteDir = dirDeleteDir,
.dirRename = dirRename,
.dirSymLink = dirSymLink,
.dirReadLink = dirReadLink,
.dirSetMode = dirSetMode,
.dirSetOwner = dirSetOwner,
.dirSetPermissions = dirSetPermissions,
.fileStat = fileStat,
.fileClose = fileClose,
.fileWriteStreaming = fileWriteStreaming,
.fileWritePositional = fileWritePositional,
@@ -3093,6 +3113,1335 @@ fn dirClose(userdata: ?*anyopaque, dir: Io.Dir) void {
posix.close(dir.handle);
}
const dirRealPath = switch (native_os) {
.windows => dirRealPathWindows,
else => dirRealPathPosix,
};
fn dirRealPathWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, out_buffer: []u8) Io.Dir.RealPathError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const w = windows;
const current_thread = Thread.getCurrent(t);
try current_thread.checkCancel();
var path_name_w = try w.sliceToPrefixedFileW(dir.handle, sub_path);
const access_mask = w.GENERIC_READ | w.SYNCHRONIZE;
const share_access = w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE;
const creation = w.FILE_OPEN;
const h_file = blk: {
const res = w.OpenFile(path_name_w.span(), .{
.dir = dir.handle,
.access_mask = access_mask,
.share_access = share_access,
.creation = creation,
.filter = .any,
}) catch |err| switch (err) {
error.WouldBlock => unreachable,
else => |e| return e,
};
break :blk res;
};
defer w.CloseHandle(h_file);
const wide_slice = w.GetFinalPathNameByHandle(h_file, .{}, out_buffer);
const len = std.unicode.calcWtf8Len(wide_slice);
if (len > out_buffer.len)
return error.NameTooLong;
return std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
}
fn dirRealPathPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, out_buffer: []u8) Io.Dir.RealPathError!usize {
if (native_os == .wasi) @compileError("unsupported operating system");
const max_path_bytes = std.fs.max_path_bytes;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
var flags: posix.O = .{};
if (@hasField(posix.O, "NONBLOCK")) flags.NONBLOCK = true;
if (@hasField(posix.O, "CLOEXEC")) flags.CLOEXEC = true;
if (@hasField(posix.O, "PATH")) flags.PATH = true;
try current_thread.beginSyscall();
const fd: posix.fd_t = while (true) {
const rc = openat_sym(dir.handle, sub_path_posix, flags, 0);
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
break @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.INVAL => return error.BadPathName,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.ACCES => return error.AccessDenied,
.FBIG => return error.FileTooBig,
.OVERFLOW => return error.FileTooBig,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MFILE => return error.ProcessFdQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NFILE => return error.SystemFdQuotaExceeded,
.NODEV => return error.NoDevice,
.NOENT => return error.FileNotFound,
.SRCH => return error.ProcessNotFound,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
.EXIST => return error.PathAlreadyExists,
.BUSY => return error.DeviceBusy,
.NXIO => return error.NoDevice,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
};
errdefer posix.close(fd);
switch (native_os) {
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
// On macOS, we can use F.GETPATH fcntl command to query the OS for
// the path to the file descriptor.
@memset(out_buffer, 0);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.fcntl(fd, posix.F.GETPATH, out_buffer))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => return error.FileNotFound,
.NOSPC => return error.NameTooLong,
.NOENT => return error.FileNotFound,
// TODO man pages for fcntl on macOS don't really tell you what
// errno values to expect when command is F.GETPATH...
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
return std.mem.indexOfScalar(u8, &out_buffer, 0) orelse out_buffer.len;
},
.linux, .serenity, .illumos => {
var procfs_buf: ["/proc/self/path/-2147483648\x00".len]u8 = undefined;
const template = if (native_os == .illumos) "/proc/self/path/{d}" else "/proc/self/fd/{d}";
const proc_path = std.fmt.bufPrintSentinel(&procfs_buf, template, .{fd}, 0) catch unreachable;
try current_thread.beginSyscall();
while (true) {
const rc = posix.system.readlink(proc_path, out_buffer.ptr, out_buffer.len);
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
const len: usize = @bitCast(rc);
return len;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.FAULT => |err| return errnoBug(err),
.INVAL => return error.NotLink,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.ILSEQ => |err| return errnoBug(err),
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
},
.freebsd => {
var kfile: std.c.kinfo_file = undefined;
kfile.structsize = std.c.KINFO_FILE_SIZE;
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(std.c.fcntl(fd, std.c.F.KINFO, @intFromPtr(&kfile)))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => return error.FileNotFound,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
const len = std.mem.indexOfScalar(u8, &kfile.path, 0) orelse kfile.path.len;
if (len == 0) return error.NameTooLong;
return len;
},
.netbsd, .dragonfly => {
@memset(out_buffer[0..max_path_bytes], 0);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(std.c.fcntl(fd, posix.F.GETPATH, out_buffer))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.BADF => return error.FileNotFound,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.RANGE => return error.NameTooLong,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
return std.mem.indexOfScalar(u8, &out_buffer, 0) orelse out_buffer.len;
},
else => @compileError("unsupported OS"),
}
comptime unreachable;
}
const dirDeleteFile = switch (native_os) {
.windows => dirDeleteFileWindows,
.wasi => dirDeleteFileWasi,
else => dirDeleteFilePosix,
};
fn dirDeleteFileWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteFileError!void {
return dirDeleteWindows(userdata, dir, sub_path, false);
}
fn dirDeleteFileWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteFileError!void {
if (builtin.link_libc) return dirDeleteFilePosix(userdata, dir, sub_path);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
const res = std.os.wasi.path_unlink_file(dir.handle, sub_path.ptr, sub_path.len);
switch (res) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.FAULT => |err| return errnoBug(err),
.IO => return error.FileSystem,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.ROFS => return error.ReadOnlyFileSystem,
.NOTEMPTY => return error.DirNotEmpty,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
.INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirDeleteFilePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteFileError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.unlinkat(dir.handle, sub_path_posix, 0))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
// Some systems return permission errors when trying to delete a
// directory, so we need to handle that case specifically and
// translate the error.
.PERM => switch (native_os) {
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .dragonfly, .openbsd, .illumos => {
// Don't follow symlinks to match unlinkat (which acts on symlinks rather than follows them).
var st = std.mem.zeroes(posix.Stat);
while (true) {
try current_thread.checkCancel();
switch (posix.errno(fstatat_sym(dir.handle, sub_path_posix, &st, posix.AT.SYMLINK_NOFOLLOW))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => continue,
.CANCELED => return current_thread.endSyscallCanceled(),
else => {
current_thread.endSyscall();
return error.PermissionDenied;
},
}
}
const is_dir = st.mode & posix.S.IFMT == posix.S.IFDIR;
if (is_dir)
return error.IsDir
else
return error.PermissionDenied;
},
else => {
current_thread.endSyscall();
return error.PermissionDenied;
},
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.BUSY => return error.FileBusy,
.FAULT => |err| return errnoBug(err),
.IO => return error.FileSystem,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.ROFS => return error.ReadOnlyFileSystem,
.EXIST => |err| return errnoBug(err),
.NOTEMPTY => |err| return errnoBug(err), // Not passing AT.REMOVEDIR
.ILSEQ => return error.BadPathName,
.INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirDeleteDir = switch (native_os) {
.windows => dirDeleteDirWindows,
.wasi => dirDeleteDirWasi,
else => dirDeleteDirPosix,
};
fn dirDeleteDirWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteDirError!void {
return dirDeleteWindows(userdata, dir, sub_path, true);
}
fn dirDeleteWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, remove_dir: bool) Io.Dir.DeleteFileError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const w = windows;
try current_thread.checkCancel();
const sub_path_w = try w.sliceToPrefixedFileW(dir.handle, sub_path);
const path_len_bytes = @as(u16, @intCast(sub_path_w.len * 2));
var nt_name: w.UNICODE_STRING = .{
.Length = path_len_bytes,
.MaximumLength = path_len_bytes,
// The Windows API makes this mutable, but it will not mutate here.
.Buffer = @constCast(sub_path_w.ptr),
};
if (sub_path_w[0] == '.' and sub_path_w[1] == 0) {
// Windows does not recognize this, but it does work with empty string.
nt_name.Length = 0;
}
if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) {
// Can't remove the parent directory with an open handle.
return error.FileBusy;
}
const create_options_flags: w.ULONG = if (remove_dir)
w.FILE_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT
else
w.FILE_NON_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT;
var attr: w.OBJECT_ATTRIBUTES = .{
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
.Attributes = w.OBJ_CASE_INSENSITIVE,
.ObjectName = &nt_name,
.SecurityDescriptor = null,
.SecurityQualityOfService = null,
};
var io_status_block: w.IO_STATUS_BLOCK = undefined;
var tmp_handle: w.HANDLE = undefined;
var rc = w.ntdll.NtCreateFile(
&tmp_handle,
w.SYNCHRONIZE | w.DELETE,
&attr,
&io_status_block,
null,
0,
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE,
w.FILE_OPEN,
create_options_flags,
null,
0,
);
switch (rc) {
.SUCCESS => {},
.OBJECT_NAME_INVALID => unreachable,
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
.BAD_NETWORK_PATH => return error.NetworkNotFound, // \\server was not found
.BAD_NETWORK_NAME => return error.NetworkNotFound, // \\server was found but \\server\share wasn't
.INVALID_PARAMETER => unreachable,
.FILE_IS_A_DIRECTORY => return error.IsDir,
.NOT_A_DIRECTORY => return error.NotDir,
.SHARING_VIOLATION => return error.FileBusy,
.ACCESS_DENIED => return error.AccessDenied,
.DELETE_PENDING => return,
else => return w.unexpectedStatus(rc),
}
defer w.CloseHandle(tmp_handle);
// FileDispositionInformationEx has varying levels of support:
// - FILE_DISPOSITION_INFORMATION_EX requires >= win10_rs1
// (INVALID_INFO_CLASS is returned if not supported)
// - Requires the NTFS filesystem
// (on filesystems like FAT32, INVALID_PARAMETER is returned)
// - FILE_DISPOSITION_POSIX_SEMANTICS requires >= win10_rs1
// - FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5
// (NOT_SUPPORTED is returned if a flag is unsupported)
//
// The strategy here is just to try using FileDispositionInformationEx and fall back to
// FileDispositionInformation if the return value lets us know that some aspect of it is not supported.
const need_fallback = need_fallback: {
try current_thread.checkCancel();
// Deletion with posix semantics if the filesystem supports it.
var info: w.FILE_DISPOSITION_INFORMATION_EX = .{
.Flags = w.FILE_DISPOSITION_DELETE |
w.FILE_DISPOSITION_POSIX_SEMANTICS |
w.FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE,
};
rc = w.ntdll.NtSetInformationFile(
tmp_handle,
&io_status_block,
&info,
@sizeOf(w.FILE_DISPOSITION_INFORMATION_EX),
.FileDispositionInformationEx,
);
switch (rc) {
.SUCCESS => return,
// The filesystem does not support FileDispositionInformationEx
.INVALID_PARAMETER,
// The operating system does not support FileDispositionInformationEx
.INVALID_INFO_CLASS,
// The operating system does not support one of the flags
.NOT_SUPPORTED,
=> break :need_fallback true,
// For all other statuses, fall down to the switch below to handle them.
else => break :need_fallback false,
}
};
if (need_fallback) {
try current_thread.checkCancel();
// Deletion with file pending semantics, which requires waiting or moving
// files to get them removed (from here).
var file_dispo: w.FILE_DISPOSITION_INFORMATION = .{
.DeleteFile = w.TRUE,
};
rc = w.ntdll.NtSetInformationFile(
tmp_handle,
&io_status_block,
&file_dispo,
@sizeOf(w.FILE_DISPOSITION_INFORMATION),
.FileDispositionInformation,
);
}
switch (rc) {
.SUCCESS => {},
.DIRECTORY_NOT_EMPTY => return error.DirNotEmpty,
.INVALID_PARAMETER => unreachable,
.CANNOT_DELETE => return error.AccessDenied,
.MEDIA_WRITE_PROTECTED => return error.AccessDenied,
.ACCESS_DENIED => return error.AccessDenied,
else => return w.unexpectedStatus(rc),
}
}
fn dirDeleteDirWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteDirError!void {
if (builtin.link_libc) return dirDeleteDirPosix(userdata, dir, sub_path);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
const res = std.os.wasi.path_remove_directory(dir.handle, sub_path.ptr, sub_path.len);
switch (res) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.FAULT => |err| return errnoBug(err),
.IO => return error.FileSystem,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.ROFS => return error.ReadOnlyFileSystem,
.NOTEMPTY => return error.DirNotEmpty,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
.INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirDeleteDirPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteDirError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.unlinkat(dir.handle, sub_path_posix, posix.AT.REMOVEDIR))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.FAULT => |err| return errnoBug(err),
.IO => return error.FileSystem,
.ISDIR => |err| return errnoBug(err),
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.ROFS => return error.ReadOnlyFileSystem,
.EXIST => |err| return errnoBug(err),
.NOTEMPTY => |err| return errnoBug(err), // Not passing AT.REMOVEDIR
.ILSEQ => return error.BadPathName,
.INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirRename = switch (native_os) {
.windows => dirRenameWindows,
.wasi => dirRenameWasi,
else => dirRenamePosix,
};
fn dirRenameWindows(
userdata: ?*anyopaque,
old_dir: Io.Dir,
old_sub_path: []const u8,
new_dir: Io.Dir,
new_sub_path: []const u8,
) Io.Dir.RenameError!void {
const w = windows;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const old_path_w = try windows.sliceToPrefixedFileW(old_dir.handle, old_sub_path);
const new_path_w = try windows.sliceToPrefixedFileW(new_dir.handle, new_sub_path);
const replace_if_exists = true;
try current_thread.checkCancel();
const src_fd = w.OpenFile(old_path_w, .{
.dir = old_dir.handle,
.access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | w.DELETE,
.creation = w.FILE_OPEN,
.filter = .any, // This function is supposed to rename both files and directories.
.follow_symlinks = false,
}) catch |err| switch (err) {
error.WouldBlock => unreachable, // Not possible without `.share_access_nonblocking = true`.
else => |e| return e,
};
defer w.CloseHandle(src_fd);
var rc: w.NTSTATUS = undefined;
// FileRenameInformationEx has varying levels of support:
// - FILE_RENAME_INFORMATION_EX requires >= win10_rs1
// (INVALID_INFO_CLASS is returned if not supported)
// - Requires the NTFS filesystem
// (on filesystems like FAT32, INVALID_PARAMETER is returned)
// - FILE_RENAME_POSIX_SEMANTICS requires >= win10_rs1
// - FILE_RENAME_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5
// (NOT_SUPPORTED is returned if a flag is unsupported)
//
// The strategy here is just to try using FileRenameInformationEx and fall back to
// FileRenameInformation if the return value lets us know that some aspect of it is not supported.
const need_fallback = need_fallback: {
const struct_buf_len = @sizeOf(w.FILE_RENAME_INFORMATION_EX) + (w.PATH_MAX_WIDE * 2);
var rename_info_buf: [struct_buf_len]u8 align(@alignOf(w.FILE_RENAME_INFORMATION_EX)) = undefined;
const struct_len = @sizeOf(w.FILE_RENAME_INFORMATION_EX) + new_path_w.len * 2;
if (struct_len > struct_buf_len) return error.NameTooLong;
const rename_info: *w.FILE_RENAME_INFORMATION_EX = @ptrCast(&rename_info_buf);
var io_status_block: w.IO_STATUS_BLOCK = undefined;
var flags: w.ULONG = w.FILE_RENAME_POSIX_SEMANTICS | w.FILE_RENAME_IGNORE_READONLY_ATTRIBUTE;
if (replace_if_exists) flags |= w.FILE_RENAME_REPLACE_IF_EXISTS;
rename_info.* = .{
.Flags = flags,
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle,
.FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong
.FileName = undefined,
};
@memcpy((&rename_info.FileName).ptr, new_path_w);
rc = w.ntdll.NtSetInformationFile(
src_fd,
&io_status_block,
rename_info,
@intCast(struct_len), // already checked for error.NameTooLong
.FileRenameInformationEx,
);
switch (rc) {
.SUCCESS => return,
// The filesystem does not support FileDispositionInformationEx
.INVALID_PARAMETER,
// The operating system does not support FileDispositionInformationEx
.INVALID_INFO_CLASS,
// The operating system does not support one of the flags
.NOT_SUPPORTED,
=> break :need_fallback true,
// For all other statuses, fall down to the switch below to handle them.
else => break :need_fallback false,
}
};
if (need_fallback) {
const struct_buf_len = @sizeOf(w.FILE_RENAME_INFORMATION) + (w.PATH_MAX_WIDE * 2);
var rename_info_buf: [struct_buf_len]u8 align(@alignOf(w.FILE_RENAME_INFORMATION)) = undefined;
const struct_len = @sizeOf(w.FILE_RENAME_INFORMATION) + new_path_w.len * 2;
if (struct_len > struct_buf_len) return error.NameTooLong;
const rename_info: *w.FILE_RENAME_INFORMATION = @ptrCast(&rename_info_buf);
var io_status_block: w.IO_STATUS_BLOCK = undefined;
rename_info.* = .{
.Flags = @intFromBool(replace_if_exists),
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle,
.FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong
.FileName = undefined,
};
@memcpy((&rename_info.FileName).ptr, new_path_w);
rc = w.ntdll.NtSetInformationFile(
src_fd,
&io_status_block,
rename_info,
@intCast(struct_len), // already checked for error.NameTooLong
.FileRenameInformation,
);
}
switch (rc) {
.SUCCESS => {},
.INVALID_HANDLE => unreachable,
.INVALID_PARAMETER => unreachable,
.OBJECT_PATH_SYNTAX_BAD => unreachable,
.ACCESS_DENIED => return error.AccessDenied,
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
.NOT_SAME_DEVICE => return error.RenameAcrossMountPoints,
.OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
.DIRECTORY_NOT_EMPTY => return error.PathAlreadyExists,
.FILE_IS_A_DIRECTORY => return error.IsDir,
.NOT_A_DIRECTORY => return error.NotDir,
else => return w.unexpectedStatus(rc),
}
}
fn dirRenameWasi(
userdata: ?*anyopaque,
old_dir: Io.Dir,
old_sub_path: []const u8,
new_dir: Io.Dir,
new_sub_path: []const u8,
) Io.Dir.RenameError!void {
if (builtin.link_libc) return dirRenamePosix(userdata, old_dir, old_sub_path, new_dir, new_sub_path);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.path_rename(old_dir.handle, old_sub_path.ptr, old_sub_path.len, new_dir.handle, new_sub_path.ptr, new_sub_path.len)) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.DQUOT => return error.DiskQuota,
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MLINK => return error.LinkQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.EXIST => return error.PathAlreadyExists,
.NOTEMPTY => return error.PathAlreadyExists,
.ROFS => return error.ReadOnlyFileSystem,
.XDEV => return error.RenameAcrossMountPoints,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirRenamePosix(
userdata: ?*anyopaque,
old_dir: Io.Dir,
old_sub_path: []const u8,
new_dir: Io.Dir,
new_sub_path: []const u8,
) Io.Dir.RenameError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var old_path_buffer: [posix.PATH_MAX]u8 = undefined;
var new_path_buffer: [posix.PATH_MAX]u8 = undefined;
const old_sub_path_posix = try pathToPosix(old_sub_path, &old_path_buffer);
const new_sub_path_posix = try pathToPosix(new_sub_path, &new_path_buffer);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.renameat(old_dir.handle, old_sub_path_posix, new_dir.handle, new_sub_path_posix))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.DQUOT => return error.DiskQuota,
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MLINK => return error.LinkQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.EXIST => return error.PathAlreadyExists,
.NOTEMPTY => return error.PathAlreadyExists,
.ROFS => return error.ReadOnlyFileSystem,
.XDEV => return error.RenameAcrossMountPoints,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirSymLink = switch (native_os) {
.windows => dirSymLinkWindows,
.wasi => dirSymLinkWasi,
else => dirSymLinkPosix,
};
fn dirSymLinkWindows(
userdata: ?*anyopaque,
dir: Io.Dir,
target_path: []const u8,
sym_link_path: []const u8,
flags: Io.Dir.SymLinkFlags,
) Io.Dir.SymLinkError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const w = windows;
try current_thread.checkCancel();
// 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: w.PathSpace = undefined;
target_path_w.len = try w.wtf8ToWtf16Le(&target_path_w.data, target_path);
target_path_w.data[target_path_w.len] = 0;
// However, we need to canonicalize any path separators to `\`, since if
// the target path is relative, then it must use `\` as the path separator.
std.mem.replaceScalar(
u16,
target_path_w.data[0..target_path_w.len],
std.mem.nativeToLittle(u16, '/'),
std.mem.nativeToLittle(u16, '\\'),
);
const sym_link_path_w = try w.sliceToPrefixedFileW(dir.handle, sym_link_path);
const SYMLINK_DATA = extern struct {
ReparseTag: w.ULONG,
ReparseDataLength: w.USHORT,
Reserved: w.USHORT,
SubstituteNameOffset: w.USHORT,
SubstituteNameLength: w.USHORT,
PrintNameOffset: w.USHORT,
PrintNameLength: w.USHORT,
Flags: w.ULONG,
};
const symlink_handle = w.OpenFile(sym_link_path_w.span(), .{
.access_mask = w.SYNCHRONIZE | w.GENERIC_READ | w.GENERIC_WRITE,
.dir = dir,
.creation = w.FILE_CREATE,
.filter = if (flags.is_directory) .dir_only else .file_only,
}) catch |err| switch (err) {
error.IsDir => return error.PathAlreadyExists,
error.NotDir => return error.Unexpected,
error.WouldBlock => return error.Unexpected,
error.PipeBusy => return error.Unexpected,
error.NoDevice => return error.Unexpected,
error.AntivirusInterference => return error.Unexpected,
else => |e| return e,
};
defer w.CloseHandle(symlink_handle);
// Relevant portions of the documentation:
// > Relative links are specified using the following conventions:
// > - Root relative—for example, "\Windows\System32" resolves to "current drive:\Windows\System32".
// > - Current working directoryrelative—for example, if the current working directory is
// > C:\Windows\System32, "C:File.txt" resolves to "C:\Windows\System32\File.txt".
// > Note: If you specify a current working directoryrelative link, it is created as an absolute
// > link, due to the way the current working directory is processed based on the user and the thread.
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
var is_target_absolute = false;
const final_target_path = target_path: {
if (w.hasCommonNtPrefix(u16, target_path)) {
// Already an NT path, no need to do anything to it
break :target_path target_path;
} else {
switch (w.getWin32PathType(u16, target_path)) {
// Rooted paths need to avoid getting put through wToPrefixedFileW
// (and they are treated as relative in this context)
// Note: It seems that rooted paths in symbolic links are relative to
// the drive that the symbolic exists on, not to the CWD's drive.
// So, if the symlink is on C:\ and the CWD is on D:\,
// it will still resolve the path relative to the root of
// the C:\ drive.
.rooted => break :target_path target_path,
// Keep relative paths relative, but anything else needs to get NT-prefixed.
else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path))
break :target_path target_path,
}
}
var prefixed_target_path = try w.wToPrefixedFileW(dir, target_path);
// We do this after prefixing to ensure that drive-relative paths are treated as absolute
is_target_absolute = std.fs.path.isAbsoluteWindowsWtf16(prefixed_target_path.span());
break :target_path prefixed_target_path.span();
};
// prepare reparse data buffer
var buffer: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4;
const header_len = @sizeOf(w.ULONG) + @sizeOf(w.USHORT) * 2;
const target_is_absolute = std.fs.path.isAbsoluteWindowsWtf16(final_target_path);
const symlink_data = SYMLINK_DATA{
.ReparseTag = w.IO_REPARSE_TAG_SYMLINK,
.ReparseDataLength = @intCast(buf_len - header_len),
.Reserved = 0,
.SubstituteNameOffset = @intCast(final_target_path.len * 2),
.SubstituteNameLength = @intCast(final_target_path.len * 2),
.PrintNameOffset = 0,
.PrintNameLength = @intCast(final_target_path.len * 2),
.Flags = if (!target_is_absolute) w.SYMLINK_FLAG_RELATIVE else 0,
};
@memcpy(buffer[0..@sizeOf(SYMLINK_DATA)], std.mem.asBytes(&symlink_data));
@memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
const paths_start = @sizeOf(SYMLINK_DATA) + final_target_path.len * 2;
@memcpy(buffer[paths_start..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
_ = try w.DeviceIoControl(symlink_handle, w.FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null);
}
fn dirSymLinkWasi(
userdata: ?*anyopaque,
dir: Io.Dir,
target_path: []const u8,
sym_link_path: []const u8,
flags: Io.Dir.SymLinkFlags,
) Io.Dir.SymLinkError!void {
if (builtin.link_libc) return dirSymLinkPosix(dir, target_path, sym_link_path, flags);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.path_symlink(target_path.ptr, target_path.len, dir.handle, sym_link_path.ptr, sym_link_path.len)) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err),
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.DQUOT => return error.DiskQuota,
.EXIST => return error.PathAlreadyExists,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.ROFS => return error.ReadOnlyFileSystem,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirSymLinkPosix(
userdata: ?*anyopaque,
dir: Io.Dir,
target_path: []const u8,
sym_link_path: []const u8,
flags: Io.Dir.SymLinkFlags,
) Io.Dir.SymLinkError!void {
_ = flags;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var target_path_buffer: [posix.PATH_MAX]u8 = undefined;
var sym_link_path_buffer: [posix.PATH_MAX]u8 = undefined;
const target_path_posix = try pathToPosix(target_path, &target_path_buffer);
const sym_link_path_posix = try pathToPosix(sym_link_path, &sym_link_path_buffer);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.symlinkat(target_path_posix, dir.handle, sym_link_path_posix))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.DQUOT => return error.DiskQuota,
.EXIST => return error.PathAlreadyExists,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.ROFS => return error.ReadOnlyFileSystem,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirReadLink = switch (native_os) {
.windows => dirReadLinkWindows,
.wasi => dirReadLinkWasi,
else => dirReadLinkPosix,
};
fn dirReadLinkWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, buffer: []u8) Io.Dir.ReadLinkError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const w = windows;
try current_thread.checkCancel();
var sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
const result_handle = w.OpenFile(sub_path_w.span(), .{
.access_mask = w.FILE_READ_ATTRIBUTES | w.SYNCHRONIZE,
.dir = dir,
.creation = w.FILE_OPEN,
.follow_symlinks = false,
.filter = .any,
}) catch |err| switch (err) {
error.IsDir, error.NotDir => return error.Unexpected, // filter = .any
error.PathAlreadyExists => return error.Unexpected, // FILE_OPEN
error.WouldBlock => return error.Unexpected,
error.NoDevice => return error.FileNotFound,
error.PipeBusy => return error.AccessDenied,
else => |e| return e,
};
defer w.CloseHandle(result_handle);
var reparse_buf: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 align(@alignOf(w.REPARSE_DATA_BUFFER)) = undefined;
_ = w.DeviceIoControl(result_handle, w.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]) catch |err| switch (err) {
error.AccessDenied => return error.Unexpected,
error.UnrecognizedVolume => return error.Unexpected,
else => |e| return e,
};
const reparse_struct: *const w.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0]));
const wide_result = switch (reparse_struct.ReparseTag) {
w.IO_REPARSE_TAG_SYMLINK => r: {
const buf: *const w.SYMBOLIC_LINK_REPARSE_BUFFER = @ptrCast(@alignCast(&reparse_struct.DataBuffer[0]));
const offset = buf.SubstituteNameOffset >> 1;
const len = buf.SubstituteNameLength >> 1;
const path_buf: [*]const u16 = &buf.PathBuffer;
const is_relative = buf.Flags & w.SYMLINK_FLAG_RELATIVE != 0;
break :r try w.parseReadLinkPath(path_buf[offset..][0..len], is_relative, buffer);
},
w.IO_REPARSE_TAG_MOUNT_POINT => r: {
const buf: *const w.MOUNT_POINT_REPARSE_BUFFER = @ptrCast(@alignCast(&reparse_struct.DataBuffer[0]));
const offset = buf.SubstituteNameOffset >> 1;
const len = buf.SubstituteNameLength >> 1;
const path_buf: [*]const u16 = &buf.PathBuffer;
break :r try w.parseReadLinkPath(path_buf[offset..][0..len], false, buffer);
},
else => return error.UnsupportedReparsePointType,
};
const len = std.unicode.calcWtf8Len(wide_result);
if (len > buffer.len) return error.NameTooLong;
return std.unicode.wtf16LeToWtf8(buffer, wide_result);
}
fn dirReadLinkWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, buffer: []u8) Io.Dir.ReadLinkError!usize {
if (builtin.link_libc) return dirReadLinkPosix(userdata, dir, sub_path, buffer);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var n: usize = undefined;
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.path_readlink(dir.handle, sub_path.ptr, sub_path.len, buffer.ptr, buffer.len, &n)) {
.SUCCESS => {
current_thread.endSyscall();
return buffer[0..n];
},
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.FAULT => |err| return errnoBug(err),
.INVAL => return error.NotLink,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirReadLinkPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, buffer: []u8) Io.Dir.ReadLinkError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var sub_path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &sub_path_buffer);
try current_thread.beginSyscall();
while (true) {
const rc = posix.system.readlinkat(dir.handle, sub_path_posix, buffer.ptr, buffer.len);
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
const len: usize = @bitCast(rc);
return len;
},
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.FAULT => |err| return errnoBug(err),
.INVAL => return error.NotLink,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirSetMode = switch (native_os) {
.windows => dirSetModeUnsupported,
else => dirSetModePosix,
};
fn dirSetModeUnsupported(userdata: ?*anyopaque, dir: Io.Dir, new_mode: Io.Dir.Mode) Io.Dir.SetModeError!void {
_ = userdata;
_ = dir;
_ = new_mode;
return error.Unexpected;
}
fn dirSetModePosix(userdata: ?*anyopaque, dir: Io.Dir, new_mode: Io.Dir.Mode) Io.Dir.SetModeError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.fchmod(dir.handle, new_mode))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ACCES => return error.AccessDenied,
.IO => return error.InputOutput,
.LOOP => return error.SymLinkLoop,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.FileNotFound,
.PERM => return error.PermissionDenied,
.ROFS => return error.ReadOnlyFileSystem,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirSetOwner = switch (native_os) {
.windows => dirSetOwnerUnsupported,
else => dirSetOwnerPosix,
};
fn dirSetOwnerUnsupported(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.Dir.SetOwnerError!void {
_ = userdata;
_ = dir;
_ = owner;
_ = group;
return error.Unexpected;
}
fn dirSetOwnerPosix(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.Dir.SetOwnerError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const uid = owner orelse ~@as(posix.uid_t, 0);
const gid = group orelse ~@as(posix.gid_t, 0);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.fchown(dir.handle, uid, gid))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // likely fd refers to directory opened without `Io.Dir.OpenOptions.iterate`
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ACCES => return error.AccessDenied,
.IO => return error.InputOutput,
.LOOP => return error.SymLinkLoop,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.FileNotFound,
.PERM => return error.PermissionDenied,
.ROFS => return error.ReadOnlyFileSystem,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirSetPermissions = switch (native_os) {
.windows => dirSetPermissionsWindows,
else => dirSetPermissionsPosix,
};
fn dirSetPermissionsWindows(
userdata: ?*anyopaque,
dir: Io.Dir,
permissions: Io.Dir.Permissions,
) Io.Dir.SetPermissionsError!void {
_ = userdata;
_ = dir;
_ = permissions;
@panic("TODO");
}
fn dirSetPermissionsPosix(
userdata: ?*anyopaque,
dir: Io.Dir,
permissions: Io.Dir.Permissions,
) Io.Dir.SetPermissionsError!void {
_ = userdata;
_ = dir;
_ = permissions;
@panic("TODO");
}
fn dirOpenDirWasi(
userdata: ?*anyopaque,
dir: Io.Dir,
@@ -3445,10 +4794,100 @@ fn fileReadPositionalWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8,
fn fileSeekBy(userdata: ?*anyopaque, file: Io.File, offset: i64) Io.File.SeekError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
_ = file;
_ = offset;
@panic("TODO implement fileSeekBy");
const current_thread = Thread.getCurrent(t);
const fd = file.handle;
if (native_os == .linux and !builtin.link_libc and @sizeOf(usize) == 4) {
var result: u64 = undefined;
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.llseek(fd, offset, &result, posix.SEEK.CUR))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
.SPIPE => return error.Unseekable,
.NXIO => return error.Unseekable,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
if (native_os == .windows) {
try current_thread.checkCancel();
return windows.SetFilePointerEx_CURRENT(fd, offset);
}
if (native_os == .wasi and !builtin.link_libc) {
var new_offset: std.os.wasi.filesize_t = undefined;
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.fd_seek(fd, offset, .CUR, &new_offset)) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
.SPIPE => return error.Unseekable,
.NXIO => return error.Unseekable,
.NOTCAPABLE => return error.AccessDenied,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
if (posix.SEEK == void) return error.Unseekable;
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(lseek_sym(fd, offset, posix.SEEK.CUR))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
.SPIPE => return error.Unseekable,
.NXIO => return error.Unseekable,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekError!void {
+46 -34
View File
@@ -1104,7 +1104,14 @@ pub inline fn stripInstructionPtrAuthCode(ptr: usize) usize {
return ptr;
}
fn printSourceAtAddress(gpa: Allocator, io: Io, debug_info: *SelfInfo, writer: *Writer, address: usize, tty_config: tty.Config) Writer.Error!void {
fn printSourceAtAddress(
gpa: Allocator,
io: Io,
debug_info: *SelfInfo,
writer: *Writer,
address: usize,
tty_config: tty.Config,
) Writer.Error!void {
const symbol: Symbol = debug_info.getSymbol(gpa, io, address) catch |err| switch (err) {
error.MissingDebugInfo,
error.UnsupportedDebugInfo,
@@ -1125,6 +1132,7 @@ fn printSourceAtAddress(gpa: Allocator, io: Io, debug_info: *SelfInfo, writer: *
};
defer if (symbol.source_location) |sl| gpa.free(sl.file_name);
return printLineInfo(
io,
writer,
symbol.source_location,
address,
@@ -1134,6 +1142,7 @@ fn printSourceAtAddress(gpa: Allocator, io: Io, debug_info: *SelfInfo, writer: *
);
}
fn printLineInfo(
io: Io,
writer: *Writer,
source_location: ?SourceLocation,
address: usize,
@@ -1159,7 +1168,7 @@ fn printLineInfo(
// Show the matching source code line if possible
if (source_location) |sl| {
if (printLineFromFile(writer, sl)) {
if (printLineFromFile(io, writer, sl)) {
if (sl.column > 0) {
// The caret already takes one char
const space_needed = @as(usize, @intCast(sl.column - 1));
@@ -1177,16 +1186,17 @@ fn printLineInfo(
}
}
}
fn printLineFromFile(writer: *Writer, source_location: SourceLocation) !void {
fn printLineFromFile(io: Io, writer: *Writer, source_location: SourceLocation) !void {
// Allow overriding the target-agnostic source line printing logic by exposing `root.debug.printLineFromFile`.
if (@hasDecl(root, "debug") and @hasDecl(root.debug, "printLineFromFile")) {
return root.debug.printLineFromFile(writer, source_location);
return root.debug.printLineFromFile(io, writer, source_location);
}
// Need this to always block even in async I/O mode, because this could potentially
// be called from e.g. the event loop code crashing.
var f = try fs.cwd().openFile(source_location.file_name, .{});
defer f.close();
const cwd: Io.Dir = .cwd();
var f = try cwd.openFile(io, source_location.file_name, .{});
defer f.close(io);
// TODO fstat and make sure that the file has the correct size
var buf: [4096]u8 = undefined;
@@ -1237,11 +1247,13 @@ fn printLineFromFile(writer: *Writer, source_location: SourceLocation) !void {
}
test printLineFromFile {
var aw: Writer.Allocating = .init(std.testing.allocator);
const io = std.testing.io;
const gpa = std.testing.allocator;
var aw: Writer.Allocating = .init(gpa);
defer aw.deinit();
const output_stream = &aw.writer;
const allocator = std.testing.allocator;
const join = std.fs.path.join;
const expectError = std.testing.expectError;
const expectEqualStrings = std.testing.expectEqualStrings;
@@ -1249,24 +1261,24 @@ test printLineFromFile {
var test_dir = std.testing.tmpDir(.{});
defer test_dir.cleanup();
// Relies on testing.tmpDir internals which is not ideal, but SourceLocation requires paths.
const test_dir_path = try join(allocator, &.{ ".zig-cache", "tmp", test_dir.sub_path[0..] });
defer allocator.free(test_dir_path);
const test_dir_path = try join(gpa, &.{ ".zig-cache", "tmp", test_dir.sub_path[0..] });
defer gpa.free(test_dir_path);
// Cases
{
const path = try join(allocator, &.{ test_dir_path, "one_line.zig" });
defer allocator.free(path);
const path = try join(gpa, &.{ test_dir_path, "one_line.zig" });
defer gpa.free(path);
try test_dir.dir.writeFile(.{ .sub_path = "one_line.zig", .data = "no new lines in this file, but one is printed anyway" });
try expectError(error.EndOfFile, printLineFromFile(output_stream, .{ .file_name = path, .line = 2, .column = 0 }));
try expectError(error.EndOfFile, printLineFromFile(io, output_stream, .{ .file_name = path, .line = 2, .column = 0 }));
try printLineFromFile(output_stream, .{ .file_name = path, .line = 1, .column = 0 });
try printLineFromFile(io, output_stream, .{ .file_name = path, .line = 1, .column = 0 });
try expectEqualStrings("no new lines in this file, but one is printed anyway\n", aw.written());
aw.clearRetainingCapacity();
}
{
const path = try fs.path.join(allocator, &.{ test_dir_path, "three_lines.zig" });
defer allocator.free(path);
const path = try fs.path.join(gpa, &.{ test_dir_path, "three_lines.zig" });
defer gpa.free(path);
try test_dir.dir.writeFile(.{
.sub_path = "three_lines.zig",
.data =
@@ -1276,19 +1288,19 @@ test printLineFromFile {
,
});
try printLineFromFile(output_stream, .{ .file_name = path, .line = 1, .column = 0 });
try printLineFromFile(io, output_stream, .{ .file_name = path, .line = 1, .column = 0 });
try expectEqualStrings("1\n", aw.written());
aw.clearRetainingCapacity();
try printLineFromFile(output_stream, .{ .file_name = path, .line = 3, .column = 0 });
try printLineFromFile(io, output_stream, .{ .file_name = path, .line = 3, .column = 0 });
try expectEqualStrings("3\n", aw.written());
aw.clearRetainingCapacity();
}
{
const file = try test_dir.dir.createFile("line_overlaps_page_boundary.zig", .{});
defer file.close();
const path = try fs.path.join(allocator, &.{ test_dir_path, "line_overlaps_page_boundary.zig" });
defer allocator.free(path);
const path = try fs.path.join(gpa, &.{ test_dir_path, "line_overlaps_page_boundary.zig" });
defer gpa.free(path);
const overlap = 10;
var buf: [16]u8 = undefined;
@@ -1299,55 +1311,55 @@ test printLineFromFile {
try writer.splatByteAll('a', overlap);
try writer.flush();
try printLineFromFile(output_stream, .{ .file_name = path, .line = 2, .column = 0 });
try printLineFromFile(io, output_stream, .{ .file_name = path, .line = 2, .column = 0 });
try expectEqualStrings(("a" ** overlap) ++ "\n", aw.written());
aw.clearRetainingCapacity();
}
{
const file = try test_dir.dir.createFile("file_ends_on_page_boundary.zig", .{});
defer file.close();
const path = try fs.path.join(allocator, &.{ test_dir_path, "file_ends_on_page_boundary.zig" });
defer allocator.free(path);
const path = try fs.path.join(gpa, &.{ test_dir_path, "file_ends_on_page_boundary.zig" });
defer gpa.free(path);
var file_writer = file.writer(&.{});
const writer = &file_writer.interface;
try writer.splatByteAll('a', std.heap.page_size_max);
try printLineFromFile(output_stream, .{ .file_name = path, .line = 1, .column = 0 });
try printLineFromFile(io, output_stream, .{ .file_name = path, .line = 1, .column = 0 });
try expectEqualStrings(("a" ** std.heap.page_size_max) ++ "\n", aw.written());
aw.clearRetainingCapacity();
}
{
const file = try test_dir.dir.createFile("very_long_first_line_spanning_multiple_pages.zig", .{});
defer file.close();
const path = try fs.path.join(allocator, &.{ test_dir_path, "very_long_first_line_spanning_multiple_pages.zig" });
defer allocator.free(path);
const path = try fs.path.join(gpa, &.{ test_dir_path, "very_long_first_line_spanning_multiple_pages.zig" });
defer gpa.free(path);
var file_writer = file.writer(&.{});
const writer = &file_writer.interface;
try writer.splatByteAll('a', 3 * std.heap.page_size_max);
try expectError(error.EndOfFile, printLineFromFile(output_stream, .{ .file_name = path, .line = 2, .column = 0 }));
try expectError(error.EndOfFile, printLineFromFile(io, output_stream, .{ .file_name = path, .line = 2, .column = 0 }));
try printLineFromFile(output_stream, .{ .file_name = path, .line = 1, .column = 0 });
try printLineFromFile(io, output_stream, .{ .file_name = path, .line = 1, .column = 0 });
try expectEqualStrings(("a" ** (3 * std.heap.page_size_max)) ++ "\n", aw.written());
aw.clearRetainingCapacity();
try writer.writeAll("a\na");
try printLineFromFile(output_stream, .{ .file_name = path, .line = 1, .column = 0 });
try printLineFromFile(io, output_stream, .{ .file_name = path, .line = 1, .column = 0 });
try expectEqualStrings(("a" ** (3 * std.heap.page_size_max)) ++ "a\n", aw.written());
aw.clearRetainingCapacity();
try printLineFromFile(output_stream, .{ .file_name = path, .line = 2, .column = 0 });
try printLineFromFile(io, output_stream, .{ .file_name = path, .line = 2, .column = 0 });
try expectEqualStrings("a\n", aw.written());
aw.clearRetainingCapacity();
}
{
const file = try test_dir.dir.createFile("file_of_newlines.zig", .{});
defer file.close();
const path = try fs.path.join(allocator, &.{ test_dir_path, "file_of_newlines.zig" });
defer allocator.free(path);
const path = try fs.path.join(gpa, &.{ test_dir_path, "file_of_newlines.zig" });
defer gpa.free(path);
var file_writer = file.writer(&.{});
const writer = &file_writer.interface;
@@ -1355,11 +1367,11 @@ test printLineFromFile {
try writer.splatByteAll('\n', real_file_start);
try writer.writeAll("abc\ndef");
try printLineFromFile(output_stream, .{ .file_name = path, .line = real_file_start + 1, .column = 0 });
try printLineFromFile(io, output_stream, .{ .file_name = path, .line = real_file_start + 1, .column = 0 });
try expectEqualStrings("abc\n", aw.written());
aw.clearRetainingCapacity();
try printLineFromFile(output_stream, .{ .file_name = path, .line = real_file_start + 2, .column = 0 });
try printLineFromFile(io, output_stream, .{ .file_name = path, .line = real_file_start + 2, .column = 0 });
try expectEqualStrings("def\n", aw.written());
aw.clearRetainingCapacity();
}
+14 -55
View File
@@ -15,9 +15,13 @@ const windows = std.os.windows;
const is_darwin = native_os.isDarwin();
pub const AtomicFile = @import("fs/AtomicFile.zig");
pub const Dir = @import("fs/Dir.zig");
pub const File = @import("fs/File.zig");
/// Deprecated.
pub const AtomicFile = std.Io.File.Atomic;
/// Deprecated.
pub const Dir = std.Io.Dir;
/// Deprecated.
pub const File = std.Io.File;
pub const path = @import("fs/path.zig");
pub const has_executable_bit = switch (native_os) {
@@ -153,42 +157,9 @@ pub fn deleteDirAbsoluteZ(dir_path: [*:0]const u8) !void {
return posix.rmdirZ(dir_path);
}
/// Same as `Dir.rename` except the paths are absolute.
/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, both paths should be encoded as valid UTF-8.
/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
pub fn renameAbsolute(old_path: []const u8, new_path: []const u8) !void {
assert(path.isAbsolute(old_path));
assert(path.isAbsolute(new_path));
return posix.rename(old_path, new_path);
}
/// Same as `renameAbsolute` except the path parameters are null-terminated.
pub fn renameAbsoluteZ(old_path: [*:0]const u8, new_path: [*:0]const u8) !void {
assert(path.isAbsoluteZ(old_path));
assert(path.isAbsoluteZ(new_path));
return posix.renameZ(old_path, new_path);
}
/// Same as `Dir.rename`, except `new_sub_path` is relative to `new_dir`
pub fn rename(old_dir: Dir, old_sub_path: []const u8, new_dir: Dir, new_sub_path: []const u8) !void {
return posix.renameat(old_dir.fd, old_sub_path, new_dir.fd, new_sub_path);
}
/// Same as `rename` except the parameters are null-terminated.
pub fn renameZ(old_dir: Dir, old_sub_path_z: [*:0]const u8, new_dir: Dir, new_sub_path_z: [*:0]const u8) !void {
return posix.renameatZ(old_dir.fd, old_sub_path_z, new_dir.fd, new_sub_path_z);
}
/// Deprecated in favor of `Io.Dir.cwd`.
pub fn cwd() Dir {
if (native_os == .windows) {
return .{ .fd = windows.peb().ProcessParameters.CurrentDirectory.Handle };
} else if (native_os == .wasi) {
return .{ .fd = std.options.wasiCwd() };
} else {
return .{ .fd = posix.AT.FDCWD };
}
pub fn cwd() Io.Dir {
return .cwd();
}
pub fn defaultWasiCwd() std.os.wasi.fd_t {
@@ -209,23 +180,11 @@ pub fn openDirAbsolute(absolute_path: []const u8, flags: Dir.OpenOptions) File.O
return cwd().openDir(absolute_path, flags);
}
/// Same as `openDirAbsolute` but the path parameter is null-terminated.
pub fn openDirAbsoluteZ(absolute_path_c: [*:0]const u8, flags: Dir.OpenOptions) File.OpenError!Dir {
assert(path.isAbsoluteZ(absolute_path_c));
return cwd().openDirZ(absolute_path_c, flags);
}
/// Opens a file for reading or writing, without attempting to create a new file, based on an absolute path.
/// Call `File.close` to release the resource.
/// Asserts that the path is absolute. See `Dir.openFile` for a function that
/// operates on both absolute and relative paths.
/// Asserts that the path parameter has no null bytes. See `openFileAbsoluteZ` for a function
/// that accepts a null-terminated path.
/// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `absolute_path` should be encoded as valid UTF-8.
/// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding.
pub fn openFileAbsolute(absolute_path: []const u8, flags: File.OpenFlags) File.OpenError!File {
assert(path.isAbsolute(absolute_path));
return cwd().openFile(absolute_path, flags);
/// Deprecated in favor of `Io.File.openAbsolute`.
pub fn openFileAbsolute(absolute_path: []const u8, flags: File.OpenFlags) Io.File.OpenError!Io.File {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
return Io.File.openAbsolute(io, absolute_path, flags);
}
/// Test accessing `path`.
-94
View File
@@ -1,94 +0,0 @@
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;
const posix = std.posix;
file_writer: File.Writer,
random_integer: u64,
dest_basename: []const u8,
file_open: bool,
file_exists: bool,
close_dir_on_deinit: bool,
dir: Dir,
pub const InitError = File.OpenError;
/// 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,
write_buffer: []u8,
) InitError!AtomicFile {
while (true) {
const random_integer = std.crypto.random.int(u64);
const tmp_sub_path = std.fmt.hex(random_integer);
const file = dir.createFile(&tmp_sub_path, .{ .mode = mode, .exclusive = true }) catch |err| switch (err) {
error.PathAlreadyExists => continue,
else => |e| return e,
};
return .{
.file_writer = file.writer(write_buffer),
.random_integer = random_integer,
.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(af: *AtomicFile) void {
if (af.file_open) {
af.file_writer.file.close();
af.file_open = false;
}
if (af.file_exists) {
const tmp_sub_path = std.fmt.hex(af.random_integer);
af.dir.deleteFile(&tmp_sub_path) catch {};
af.file_exists = false;
}
if (af.close_dir_on_deinit) {
af.dir.close();
}
af.* = undefined;
}
pub const FlushError = File.WriteError;
pub fn flush(af: *AtomicFile) FlushError!void {
af.file_writer.interface.flush() catch |err| switch (err) {
error.WriteFailed => return af.file_writer.err.?,
};
}
pub const RenameIntoPlaceError = posix.RenameError;
/// On Windows, this function introduces a period of time where some file
/// system operations on the destination file will result in
/// `error.AccessDenied`, including rename operations (such as the one used in
/// this function).
pub fn renameIntoPlace(af: *AtomicFile) RenameIntoPlaceError!void {
assert(af.file_exists);
if (af.file_open) {
af.file_writer.file.close();
af.file_open = false;
}
const tmp_sub_path = std.fmt.hex(af.random_integer);
try posix.renameat(af.dir.fd, &tmp_sub_path, af.dir.fd, af.dest_basename);
af.file_exists = false;
}
pub const FinishError = FlushError || RenameIntoPlaceError;
/// Combination of `flush` followed by `renameIntoPlace`.
pub fn finish(af: *AtomicFile) FinishError!void {
try af.flush();
try af.renameIntoPlace();
}
-2066
View File
@@ -1,2066 +0,0 @@
//! Deprecated in favor of `Io.Dir`.
const Dir = @This();
const builtin = @import("builtin");
const native_os = builtin.os.tag;
const std = @import("../std.zig");
const Io = std.Io;
const File = std.fs.File;
const AtomicFile = std.fs.AtomicFile;
const base64_encoder = fs.base64_encoder;
const posix = std.posix;
const mem = std.mem;
const path = fs.path;
const fs = std.fs;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const linux = std.os.linux;
const windows = std.os.windows;
const have_flock = @TypeOf(posix.system.flock) != void;
fd: Handle,
pub const Handle = 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,
PermissionDenied,
SystemResources,
} || posix.UnexpectedError;
pub const Iterator = switch (native_os) {
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .dragonfly, .openbsd, .illumos => struct {
dir: Dir,
seek: i64,
buf: [1024]u8 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 (native_os) {
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => return self.nextDarwin(),
.freebsd, .netbsd, .dragonfly, .openbsd => return self.nextBsd(),
.illumos => return self.nextIllumos(),
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.getdirentries(
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.name))[0..darwin_entry.namlen];
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (darwin_entry.ino == 0)) {
continue :start_over;
}
const entry_kind: Entry.Kind = switch (darwin_entry.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 nextIllumos(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.name)), 0);
if (mem.eql(u8, name, ".") or mem.eql(u8, name, ".."))
continue :start_over;
// illumos dirent doesn't expose 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 = 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 +
if (@hasField(posix.system.dirent, "reclen")) bsd_entry.reclen else bsd_entry.reclen();
self.index = next_index;
const name = @as([*]u8, @ptrCast(&bsd_entry.name))[0..bsd_entry.namlen];
const skip_zero_fileno = switch (native_os) {
// 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.fileno == 0))
{
continue :start_over;
}
const entry_kind: Entry.Kind = switch (bsd_entry.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: [@sizeOf(DirEnt) + posix.PATH_MAX]u8 align(@alignOf(DirEnt)),
offset: usize,
index: usize,
end_index: usize,
first_iter: bool,
const Self = @This();
const DirEnt = posix.system.DirEnt;
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) {
if (self.index >= self.end_index) {
if (self.first_iter) {
switch (@as(posix.E, @enumFromInt(posix.system._kern_rewind_dir(self.dir.fd)))) {
.SUCCESS => {},
.BADF => unreachable, // Dir is invalid
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
else => |err| return posix.unexpectedErrno(err),
}
self.first_iter = false;
}
const rc = posix.system._kern_read_dir(
self.dir.fd,
&self.buf,
self.buf.len,
self.buf.len / @sizeOf(DirEnt),
);
if (rc == 0) return null;
if (rc < 0) {
switch (@as(posix.E, @enumFromInt(rc))) {
.BADF => unreachable, // Dir is invalid
.FAULT => unreachable,
.NOTDIR => unreachable,
.INVAL => unreachable,
.OVERFLOW => unreachable,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
else => |err| return posix.unexpectedErrno(err),
}
}
self.offset = 0;
self.index = 0;
self.end_index = @intCast(rc);
}
const dirent: *DirEnt = @ptrCast(@alignCast(&self.buf[self.offset]));
self.offset += dirent.reclen;
self.index += 1;
const name = mem.span(dirent.getName());
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or dirent.ino == 0) continue;
var stat_info: posix.Stat = undefined;
switch (@as(posix.E, @enumFromInt(posix.system._kern_read_stat(
self.dir.fd,
name,
false,
&stat_info,
@sizeOf(posix.Stat),
)))) {
.SUCCESS => {},
.INVAL => unreachable,
.BADF => unreachable, // Dir is invalid
.NOMEM => return error.SystemResources,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.FAULT => unreachable,
.NAMETOOLONG => unreachable,
.LOOP => unreachable,
.NOENT => continue,
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,
buf: [1024]u8 align(@alignOf(linux.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 {
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.errno(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.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.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(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 {
const w = windows;
while (true) {
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,
.BothDirectory,
w.FALSE,
null,
@intFromBool(self.first_iter),
);
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_wtf16le = @as([*]u16, @ptrCast(&dir_info.FileName))[0 .. dir_info.FileNameLength / 2];
if (mem.eql(u16, name_wtf16le, &[_]u16{'.'}) or mem.eql(u16, name_wtf16le, &[_]u16{ '.', '.' }))
continue;
const name_wtf8_len = std.unicode.wtf16LeToWtf8(self.name_data[0..], name_wtf16le);
const name_wtf8 = self.name_data[0..name_wtf8_len];
const kind: Entry.Kind = blk: {
const attrs = dir_info.FileAttributes;
if (attrs.DIRECTORY) break :blk .directory;
if (attrs.REPARSE_POINT) break :blk .sym_link;
break :blk .file;
};
return Entry{
.name = name_wtf8,
.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 align(@alignOf(std.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 = 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.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.namlen];
const next_index = name_index + entry.namlen;
self.index = next_index;
self.cookie = entry.next;
// skip . and .. entries
if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) {
continue :start_over;
}
const entry_kind: Entry.Kind = switch (entry.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 (native_os) {
.driverkit,
.ios,
.maccatalyst,
.macos,
.tvos,
.visionos,
.watchos,
.freebsd,
.netbsd,
.dragonfly,
.openbsd,
.illumos,
=> return Iterator{
.dir = self,
.seek = 0,
.index = 0,
.end_index = 0,
.buf = undefined,
.first_iter = first_iter_start_value,
},
.linux => return Iterator{
.dir = self,
.index = 0,
.end_index = 0,
.buf = undefined,
.first_iter = first_iter_start_value,
},
.haiku => return Iterator{
.dir = self,
.offset = 0,
.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 SelectiveWalker = struct {
stack: std.ArrayList(Walker.StackItem),
name_buffer: std.ArrayList(u8),
allocator: Allocator,
pub const Error = IteratorError || Allocator.Error;
/// 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: *SelectiveWalker) Error!?Walker.Entry {
while (self.stack.items.len > 0) {
const top = &self.stack.items[self.stack.items.len - 1];
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;
}) |entry| {
self.name_buffer.shrinkRetainingCapacity(dirname_len);
if (self.name_buffer.items.len != 0) {
try self.name_buffer.append(self.allocator, fs.path.sep);
dirname_len += 1;
}
try self.name_buffer.ensureUnusedCapacity(self.allocator, entry.name.len + 1);
self.name_buffer.appendSliceAssumeCapacity(entry.name);
self.name_buffer.appendAssumeCapacity(0);
const walker_entry: Walker.Entry = .{
.dir = top.iter.dir,
.basename = self.name_buffer.items[dirname_len .. self.name_buffer.items.len - 1 :0],
.path = self.name_buffer.items[0 .. self.name_buffer.items.len - 1 :0],
.kind = entry.kind,
};
return walker_entry;
} else {
var item = self.stack.pop().?;
if (self.stack.items.len != 0) {
item.iter.dir.close();
}
}
}
return null;
}
/// Traverses into the directory, continuing walking one level down.
pub fn enter(self: *SelectiveWalker, entry: Walker.Entry) !void {
if (entry.kind != .directory) {
@branchHint(.cold);
return;
}
var new_dir = entry.dir.openDir(entry.basename, .{ .iterate = true }) catch |err| {
switch (err) {
error.NameTooLong => unreachable,
else => |e| return e,
}
};
errdefer new_dir.close();
try self.stack.append(self.allocator, .{
.iter = new_dir.iterateAssumeFirstIteration(),
.dirname_len = self.name_buffer.items.len - 1,
});
}
pub fn deinit(self: *SelectiveWalker) void {
self.name_buffer.deinit(self.allocator);
self.stack.deinit(self.allocator);
}
/// Leaves the current directory, continuing walking one level up.
/// If the current entry is a directory entry, then the "current directory"
/// will pertain to that entry if `enter` is called before `leave`.
pub fn leave(self: *SelectiveWalker) void {
var item = self.stack.pop().?;
if (self.stack.items.len != 0) {
@branchHint(.likely);
item.iter.dir.close();
}
}
};
/// Recursively iterates over a directory, but requires the user to
/// opt-in to recursing into each directory entry.
///
/// `self` must have been opened with `OpenOptions{.iterate = true}`.
///
/// `Walker.deinit` releases allocated memory and directory handles.
///
/// The order of returned file system entries is undefined.
///
/// `self` will not be closed after walking it.
///
/// See also `walk`.
pub fn walkSelectively(self: Dir, allocator: Allocator) !SelectiveWalker {
var stack: std.ArrayList(Walker.StackItem) = .empty;
try stack.append(allocator, .{
.iter = self.iterate(),
.dirname_len = 0,
});
return .{
.stack = stack,
.name_buffer = .{},
.allocator = allocator,
};
}
pub const Walker = struct {
inner: SelectiveWalker,
pub const Entry = 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: [:0]const u8,
path: [:0]const u8,
kind: Dir.Entry.Kind,
/// Returns the depth of the entry relative to the initial directory.
/// Returns 1 for a direct child of the initial directory, 2 for an entry
/// within a direct child of the initial directory, etc.
pub fn depth(self: Walker.Entry) usize {
return mem.countScalar(u8, self.path, fs.path.sep) + 1;
}
};
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) !?Walker.Entry {
const entry = try self.inner.next();
if (entry != null and entry.?.kind == .directory) {
try self.inner.enter(entry.?);
}
return entry;
}
pub fn deinit(self: *Walker) void {
self.inner.deinit();
}
/// Leaves the current directory, continuing walking one level up.
/// If the current entry is a directory entry, then the "current directory"
/// is the directory pertaining to the current entry.
pub fn leave(self: *Walker) void {
self.inner.leave();
}
};
/// Recursively iterates over a directory.
///
/// `self` must have been opened with `OpenOptions{.iterate = true}`.
///
/// `Walker.deinit` releases allocated memory and directory handles.
///
/// The order of returned file system entries is undefined.
///
/// `self` will not be closed after walking it.
///
/// See also `walkSelectively`.
pub fn walk(self: Dir, allocator: Allocator) Allocator.Error!Walker {
return .{
.inner = try walkSelectively(self, allocator),
};
}
pub const OpenError = Io.Dir.OpenError;
pub fn close(self: *Dir) void {
posix.close(self.fd);
self.* = undefined;
}
/// Deprecated in favor of `Io.Dir.openFile`.
pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
return .adaptFromNewApi(try Io.Dir.openFile(self.adaptToNewApi(), io, sub_path, flags));
}
/// Deprecated in favor of `Io.Dir.createFile`.
pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
const new_file = try Io.Dir.createFile(self.adaptToNewApi(), io, sub_path, flags);
return .adaptFromNewApi(new_file);
}
/// Deprecated in favor of `Io.Dir.MakeError`.
pub const MakeError = Io.Dir.MakeError;
/// Deprecated in favor of `Io.Dir.makeDir`.
pub fn makeDir(self: Dir, sub_path: []const u8) MakeError!void {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
return Io.Dir.makeDir(.{ .handle = self.fd }, io, sub_path);
}
/// Deprecated in favor of `Io.Dir.makeDir`.
pub fn makeDirZ(self: Dir, sub_path: [*:0]const u8) MakeError!void {
try posix.mkdiratZ(self.fd, sub_path, default_mode);
}
/// Deprecated in favor of `Io.Dir.makeDir`.
pub fn makeDirW(self: Dir, sub_path: [*:0]const u16) MakeError!void {
try posix.mkdiratW(self.fd, mem.span(sub_path), default_mode);
}
/// Deprecated in favor of `Io.Dir.makePath`.
pub fn makePath(self: Dir, sub_path: []const u8) MakePathError!void {
_ = try self.makePathStatus(sub_path);
}
/// Deprecated in favor of `Io.Dir.MakePathStatus`.
pub const MakePathStatus = Io.Dir.MakePathStatus;
/// Deprecated in favor of `Io.Dir.MakePathError`.
pub const MakePathError = Io.Dir.MakePathError;
/// Deprecated in favor of `Io.Dir.makePathStatus`.
pub fn makePathStatus(self: Dir, sub_path: []const u8) MakePathError!MakePathStatus {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
return Io.Dir.makePathStatus(.{ .handle = self.fd }, io, sub_path);
}
/// Deprecated in favor of `Io.Dir.makeOpenPath`.
pub fn makeOpenPath(dir: Dir, sub_path: []const u8, options: OpenOptions) Io.Dir.MakeOpenPathError!Dir {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
return .adaptFromNewApi(try Io.Dir.makeOpenPath(dir.adaptToNewApi(), io, sub_path, options));
}
pub const RealPathError = posix.RealPathError || error{Canceled};
/// 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.
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
/// 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) RealPathError![]u8 {
if (native_os == .wasi) {
@compileError("realpath is not available on WASI");
}
if (native_os == .windows) {
var pathname_w = try windows.sliceToPrefixedFileW(self.fd, pathname);
const wide_slice = try self.realpathW2(pathname_w.span(), &pathname_w.data);
const len = std.unicode.calcWtf8Len(wide_slice);
if (len > out_buffer.len)
return error.NameTooLong;
const end_index = std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
return out_buffer[0..end_index];
}
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) RealPathError![]u8 {
if (native_os == .windows) {
var pathname_w = try windows.cStrToPrefixedFileW(self.fd, pathname);
const wide_slice = try self.realpathW2(pathname_w.span(), &pathname_w.data);
const len = std.unicode.calcWtf8Len(wide_slice);
if (len > out_buffer.len)
return error.NameTooLong;
const end_index = std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
return out_buffer[0..end_index];
}
var flags: posix.O = .{};
if (@hasField(posix.O, "NONBLOCK")) flags.NONBLOCK = true;
if (@hasField(posix.O, "CLOEXEC")) flags.CLOEXEC = true;
if (@hasField(posix.O, "PATH")) flags.PATH = true;
const fd = posix.openatZ(self.fd, pathname, flags, 0) catch |err| switch (err) {
error.FileLocksNotSupported => return error.Unexpected,
error.FileBusy => return error.Unexpected,
error.WouldBlock => return error.Unexpected,
else => |e| return e,
};
defer posix.close(fd);
var buffer: [fs.max_path_bytes]u8 = undefined;
const out_path = try std.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;
}
/// Deprecated: use `realpathW2`.
///
/// Windows-only. Same as `Dir.realpath` except `pathname` is WTF16 LE encoded.
/// The result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// See also `Dir.realpath`, `realpathW`.
pub fn realpathW(self: Dir, pathname: []const u16, out_buffer: []u8) RealPathError![]u8 {
var wide_buf: [std.os.windows.PATH_MAX_WIDE]u16 = undefined;
const wide_slice = try self.realpathW2(pathname, &wide_buf);
const len = std.unicode.calcWtf8Len(wide_slice);
if (len > out_buffer.len) return error.NameTooLong;
const end_index = std.unicode.wtf16LeToWtf8(&out_buffer, wide_slice);
return out_buffer[0..end_index];
}
/// Windows-only. Same as `Dir.realpath` except
/// * `pathname` and the result are WTF-16 LE encoded
/// * `pathname` is relative or has the NT namespace prefix. See `windows.wToPrefixedFileW` for details.
///
/// Additionally, `pathname` will never be accessed after `out_buffer` has been written to, so it
/// is safe to reuse a single buffer for both.
///
/// See also `Dir.realpath`, `realpathW`.
pub fn realpathW2(self: Dir, pathname: []const u16, out_buffer: []u16) RealPathError![]u16 {
const w = windows;
const h_file = blk: {
const res = w.OpenFile(pathname, .{
.dir = self.fd,
.access_mask = .{
.STANDARD = .{ .SYNCHRONIZE = true },
.GENERIC = .{ .READ = true },
},
.creation = .OPEN,
.filter = .any,
}) catch |err| switch (err) {
error.WouldBlock => unreachable,
else => |e| return e,
};
break :blk res;
};
defer w.CloseHandle(h_file);
return w.GetFinalPathNameByHandle(h_file, .{}, out_buffer);
}
pub const RealPathAllocError = RealPathError || Allocator.Error;
/// 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) RealPathAllocError![]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 (native_os == .wasi) {
@compileError("changing cwd is not currently possible in WASI");
}
if (native_os == .windows) {
var dir_path_buffer: [windows.PATH_MAX_WIDE]u16 = undefined;
const dir_path = try windows.GetFinalPathNameByHandle(self.fd, .{}, &dir_path_buffer);
if (builtin.link_libc) {
return posix.chdirW(dir_path);
}
return windows.SetCurrentDirectory(dir_path);
}
try posix.fchdir(self.fd);
}
/// Deprecated in favor of `Io.Dir.OpenOptions`.
pub const OpenOptions = Io.Dir.OpenOptions;
/// Deprecated in favor of `Io.Dir.openDir`.
pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args));
}
pub const DeleteFileError = posix.UnlinkError;
/// Delete a file name and possibly the file it refers to, based on an open directory handle.
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
/// Asserts that the path parameter has no null bytes.
pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void {
if (native_os == .windows) {
const sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.deleteFileW(sub_path_w.span());
} else if (native_os == .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, error.PermissionDenied => |e| switch (native_os) {
// non-Linux POSIX systems return permission errors when trying to delete a
// directory, so we need to handle that case specifically and translate the error
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .dragonfly, .openbsd, .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 LE 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,
PermissionDenied,
FileBusy,
FileSystem,
SymLinkLoop,
NameTooLong,
NotDir,
SystemResources,
ReadOnlyFileSystem,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
BadPathName,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
ProcessNotFound,
Unexpected,
};
/// Returns `error.DirNotEmpty` if the directory is not empty.
/// To delete a directory recursively, see `deleteTree`.
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
/// Asserts that the path parameter has no null bytes.
pub fn deleteDir(self: Dir, sub_path: []const u8) DeleteDirError!void {
if (native_os == .windows) {
const sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.deleteDirW(sub_path_w.span());
} else if (native_os == .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 WTF16LE, 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`
/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, both paths should be encoded as valid UTF-8.
/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
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 WTF16LE, 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, windows.TRUE);
}
/// Use with `Dir.symLink`, `Dir.atomicSymLink`, 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.
/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, both paths should be encoded as valid UTF-8.
/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
pub fn symLink(
self: Dir,
target_path: []const u8,
sym_link_path: []const u8,
flags: SymLinkFlags,
) !void {
if (native_os == .wasi and !builtin.link_libc) {
return self.symLinkWasi(target_path, sym_link_path, flags);
}
if (native_os == .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: windows.PathSpace = undefined;
target_path_w.len = try windows.wtf8ToWtf16Le(&target_path_w.data, target_path);
target_path_w.data[target_path_w.len] = 0;
// However, we need to canonicalize any path separators to `\`, since if
// the target path is relative, then it must use `\` as the path separator.
mem.replaceScalar(
u16,
target_path_w.data[0..target_path_w.len],
mem.nativeToLittle(u16, '/'),
mem.nativeToLittle(u16, '\\'),
);
const sym_link_path_w = try 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 (native_os == .windows) {
const target_path_w = try windows.cStrToPrefixedFileW(self.fd, target_path_c);
const sym_link_path_w = try 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 WTF16 LE 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.
/// Any path separators must be `\`, not `/`.
target_path_w: [:0]const u16,
/// WTF-16, must be NT-prefixed or relative
sym_link_path_w: []const u16,
flags: SymLinkFlags,
) !void {
return windows.CreateSymbolicLink(self.fd, sym_link_path_w, target_path_w, flags.is_directory);
}
/// Same as `symLink`, except tries to create the symbolic link until it
/// succeeds or encounters an error other than `error.PathAlreadyExists`.
///
/// * On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// * On WASI, both paths should be encoded as valid UTF-8.
/// * On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
pub fn atomicSymLink(
dir: Dir,
target_path: []const u8,
sym_link_path: []const u8,
flags: SymLinkFlags,
) !void {
if (dir.symLink(target_path, sym_link_path, flags)) {
return;
} else |err| switch (err) {
error.PathAlreadyExists => {},
else => |e| return e,
}
const dirname = path.dirname(sym_link_path) orelse ".";
const rand_len = @sizeOf(u64) * 2;
const temp_path_len = dirname.len + 1 + rand_len;
var temp_path_buf: [fs.max_path_bytes]u8 = undefined;
if (temp_path_len > temp_path_buf.len) return error.NameTooLong;
@memcpy(temp_path_buf[0..dirname.len], dirname);
temp_path_buf[dirname.len] = path.sep;
const temp_path = temp_path_buf[0..temp_path_len];
while (true) {
const random_integer = std.crypto.random.int(u64);
temp_path[dirname.len + 1 ..][0..rand_len].* = std.fmt.hex(random_integer);
if (dir.symLink(target_path, temp_path, flags)) {
return dir.rename(temp_path, sym_link_path);
} else |err| switch (err) {
error.PathAlreadyExists => continue,
else => |e| return e,
}
}
}
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.
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
pub fn readLink(self: Dir, sub_path: []const u8, buffer: []u8) ReadLinkError![]u8 {
if (native_os == .wasi and !builtin.link_libc) {
return self.readLinkWasi(sub_path, buffer);
}
if (native_os == .windows) {
var sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path);
const result_w = try self.readLinkW(sub_path_w.span(), &sub_path_w.data);
const len = std.unicode.calcWtf8Len(result_w);
if (len > buffer.len) return error.NameTooLong;
const end_index = std.unicode.wtf16LeToWtf8(buffer, result_w);
return buffer[0..end_index];
}
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 `sub_path_c` parameter is null-terminated.
pub fn readLinkZ(self: Dir, sub_path_c: [*:0]const u8, buffer: []u8) ![]u8 {
if (native_os == .windows) {
var sub_path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path_c);
const result_w = try self.readLinkW(sub_path_w.span(), &sub_path_w.data);
const len = std.unicode.calcWtf8Len(result_w);
if (len > buffer.len) return error.NameTooLong;
const end_index = std.unicode.wtf16LeToWtf8(buffer, result_w);
return buffer[0..end_index];
}
return posix.readlinkatZ(self.fd, sub_path_c, buffer);
}
/// Windows-only. Same as `readLink` except the path parameter
/// is WTF-16 LE encoded, NT-prefixed.
///
/// `sub_path_w` will never be accessed after `buffer` has been written to, so it
/// is safe to reuse a single buffer for both.
pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u16) ![]u16 {
return windows.ReadLink(self.fd, sub_path_w, buffer);
}
/// Deprecated in favor of `Io.Dir.readFile`.
pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
return Io.Dir.readFile(.{ .handle = self.fd }, io, file_path, buffer);
}
pub const ReadFileAllocError = File.OpenError || File.ReadError || Allocator.Error || error{
/// File size reached or exceeded the provided limit.
StreamTooLong,
};
/// Reads all the bytes from the named file. On success, caller owns returned
/// buffer.
///
/// If the file size is already known, a better alternative is to initialize a
/// `File.Reader`.
///
/// If the file size cannot be obtained, an error is returned. If
/// this is a realistic possibility, a better alternative is to initialize a
/// `File.Reader` which handles this seamlessly.
pub fn readFileAlloc(
dir: Dir,
/// On Windows, should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, should be encoded as valid UTF-8.
/// On other platforms, an opaque sequence of bytes with no particular encoding.
sub_path: []const u8,
/// Used to allocate the result.
gpa: Allocator,
/// If reached or exceeded, `error.StreamTooLong` is returned instead.
limit: Io.Limit,
) ReadFileAllocError![]u8 {
return readFileAllocOptions(dir, sub_path, gpa, limit, .of(u8), null);
}
/// Reads all the bytes from the named file. On success, caller owns returned
/// buffer.
///
/// If the file size is already known, a better alternative is to initialize a
/// `File.Reader`.
///
/// TODO move this function to Io.Dir
pub fn readFileAllocOptions(
dir: Dir,
/// On Windows, should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, should be encoded as valid UTF-8.
/// On other platforms, an opaque sequence of bytes with no particular encoding.
sub_path: []const u8,
/// Used to allocate the result.
gpa: Allocator,
/// If reached or exceeded, `error.StreamTooLong` is returned instead.
limit: Io.Limit,
comptime alignment: std.mem.Alignment,
comptime sentinel: ?u8,
) ReadFileAllocError!(if (sentinel) |s| [:s]align(alignment.toByteUnits()) u8 else []align(alignment.toByteUnits()) u8) {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
var file = try dir.openFile(sub_path, .{});
defer file.close();
var file_reader = file.reader(io, &.{});
return file_reader.interface.allocRemainingAlignedSentinel(gpa, limit, alignment, sentinel) catch |err| switch (err) {
error.ReadFailed => return file_reader.err.?,
error.OutOfMemory, error.StreamTooLong => |e| return e,
};
}
pub const DeleteTreeError = error{
AccessDenied,
PermissionDenied,
FileTooBig,
SymLinkLoop,
ProcessFdQuotaExceeded,
NameTooLong,
SystemFdQuotaExceeded,
NoDevice,
SystemResources,
ReadOnlyFileSystem,
FileSystem,
FileBusy,
DeviceBusy,
ProcessNotFound,
/// One of the path components was not a directory.
/// This error is unreachable if `sub_path` does not contain a path separator.
NotDir,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
/// On Windows, file paths cannot contain these characters:
/// '/', '*', '?', '"', '<', '>', '|'
BadPathName,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
Canceled,
} || posix.UnexpectedError;
/// Whether `sub_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.
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
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.ArrayList(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, .{
.follow_symlinks = false,
.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.AccessDenied,
error.PermissionDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
error.Canceled,
=> |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.PermissionDenied,
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, .{
.follow_symlinks = false,
.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.AccessDenied,
error.PermissionDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
error.Canceled,
=> |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.PermissionDenied,
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.
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
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, .{
.follow_symlinks = false,
.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.AccessDenied,
error.PermissionDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.BadPathName,
error.NetworkNotFound,
error.DeviceBusy,
error.Canceled,
=> |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.PermissionDenied,
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, .{
.follow_symlinks = false,
.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.AccessDenied,
error.PermissionDenied,
error.SymLinkLoop,
error.ProcessFdQuotaExceeded,
error.NameTooLong,
error.SystemFdQuotaExceeded,
error.NoDevice,
error.SystemResources,
error.Unexpected,
error.BadPathName,
error.DeviceBusy,
error.NetworkNotFound,
error.Canceled,
=> |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.PermissionDenied,
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;
pub const WriteFileOptions = struct {
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
sub_path: []const u8,
data: []const u8,
flags: File.CreateFlags = .{},
};
/// Writes content to the file system, using the file creation flags provided.
pub fn writeFile(self: Dir, options: WriteFileOptions) WriteFileError!void {
var file = try self.createFile(options.sub_path, options.flags);
defer file.close();
try file.writeAll(options.data);
}
/// Deprecated in favor of `Io.Dir.AccessError`.
pub const AccessError = Io.Dir.AccessError;
/// Deprecated in favor of `Io.Dir.access`.
pub fn access(self: Dir, sub_path: []const u8, options: Io.Dir.AccessOptions) AccessError!void {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
return Io.Dir.access(self.adaptToNewApi(), io, sub_path, options);
}
pub const CopyFileOptions = struct {
/// When this is `null` the mode is copied from the source file.
override_mode: ?File.Mode = null,
};
pub const CopyFileError = File.OpenError || File.StatError ||
AtomicFile.InitError || AtomicFile.FinishError ||
File.ReadError || File.WriteError || error{InvalidFileName};
/// Atomically creates a new file at `dest_path` within `dest_dir` with the
/// same contents as `source_path` within `source_dir`, overwriting any already
/// existing file.
///
/// 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.
///
/// On Windows, both paths should be encoded as
/// [WTF-8](https://wtf-8.codeberg.page/). On WASI, both paths should be
/// encoded as valid UTF-8. On other platforms, both paths are an opaque
/// sequence of bytes with no particular encoding.
///
/// TODO move this function to Io.Dir
pub fn copyFile(
source_dir: Dir,
source_path: []const u8,
dest_dir: Dir,
dest_path: []const u8,
options: CopyFileOptions,
) CopyFileError!void {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
const file = try source_dir.openFile(source_path, .{});
var file_reader: File.Reader = .init(.{ .handle = file.handle }, io, &.{});
defer file_reader.file.close(io);
const mode = options.override_mode orelse blk: {
const st = try file_reader.file.stat(io);
file_reader.size = st.size;
break :blk st.mode;
};
var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available.
var atomic_file = try dest_dir.atomicFile(dest_path, .{
.mode = mode,
.write_buffer = &buffer,
});
defer atomic_file.deinit();
_ = atomic_file.file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
error.ReadFailed => return file_reader.err.?,
error.WriteFailed => return atomic_file.file_writer.err.?,
};
try atomic_file.finish();
}
pub const AtomicFileOptions = struct {
mode: File.Mode = File.default_mode,
make_path: bool = false,
write_buffer: []u8,
};
/// 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.
/// On Windows, `dest_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `dest_path` should be encoded as valid UTF-8.
/// On other platforms, `dest_path` is an opaque sequence of bytes with no particular encoding.
pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) !AtomicFile {
if (fs.path.dirname(dest_path)) |dirname| {
const dir = if (options.make_path)
try self.makeOpenPath(dirname, .{})
else
try self.openDir(dirname, .{});
return .init(fs.path.basename(dest_path), options.mode, dir, true, options.write_buffer);
} else {
return .init(dest_path, options.mode, self, false, options.write_buffer);
}
}
pub const Stat = File.Stat;
pub const StatError = File.StatError;
/// Deprecated in favor of `Io.Dir.stat`.
pub fn stat(self: Dir) StatError!Stat {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
return Io.Dir.stat(.{ .handle = self.fd }, io);
}
pub const StatFileError = File.OpenError || File.StatError || posix.FStatAtError;
/// Deprecated in favor of `Io.Dir.statPath`.
pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
return Io.Dir.statPath(.{ .handle = self.fd }, io, sub_path, .{});
}
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 `OpenOptions{ .iterate = true }`.
pub fn chmod(self: Dir, new_mode: File.Mode) ChmodError!void {
const file: File = .{ .handle = self.fd };
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 `OpenOptions{ .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 };
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 };
try file.setPermissions(permissions);
}
pub fn adaptToNewApi(dir: Dir) Io.Dir {
return .{ .handle = dir.fd };
}
pub fn adaptFromNewApi(dir: Io.Dir) Dir {
return .{ .fd = dir.handle };
}
+9 -90
View File
@@ -22,18 +22,10 @@ handle: Handle,
pub const Handle = Io.File.Handle;
pub const Mode = Io.File.Mode;
pub const INode = Io.File.INode;
pub const Uid = posix.uid_t;
pub const Gid = posix.gid_t;
pub const Uid = Io.File.Uid;
pub const Gid = Io.File.Gid;
pub const Kind = Io.File.Kind;
/// 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: Mode = if (Mode == u0) 0 else 0o666;
/// Deprecated in favor of `Io.File.OpenError`.
pub const OpenError = Io.File.OpenError || error{WouldBlock};
/// Deprecated in favor of `Io.File.OpenMode`.
@@ -43,53 +35,7 @@ pub const Lock = Io.File.Lock;
/// Deprecated in favor of `Io.File.OpenFlags`.
pub const OpenFlags = Io.File.OpenFlags;
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.
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,
};
pub const CreateFlags = std.Io.File.CreateFlags;
pub fn stdout() File {
return .{ .handle = if (is_windows) windows.peb().ProcessParameters.hStdOutput else posix.STDOUT_FILENO };
@@ -259,33 +205,6 @@ 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 || StatError;
/// TODO: integrate with async I/O
pub fn getPos(self: File) GetSeekPosError!u64 {
return posix.lseek_CUR_get(self.handle);
}
pub const GetEndPosError = std.os.windows.GetFileSizeError || StatError;
/// TODO: integrate with async I/O
@@ -306,11 +225,13 @@ pub fn mode(self: File) ModeError!Mode {
return (try self.stat()).mode;
}
/// Deprecated in favor of `Io.File.Stat`.
pub const Stat = Io.File.Stat;
/// Deprecated in favor of `Io.File.StatError`.
pub const StatError = posix.FStatError;
/// Returns `Stat` containing basic information about the `File`.
/// Deprecated in favor of `Io.File.stat`.
pub fn stat(self: File) StatError!Stat {
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.ioBasic();
@@ -710,7 +631,7 @@ pub const Writer = struct {
Unexpected,
};
pub const SeekError = File.SeekError;
pub const SeekError = Io.File.SeekError;
/// Number of slices to store on the stack, when trying to send as many byte
/// vectors through the underlying write calls as possible.
@@ -1268,10 +1189,8 @@ pub fn writerStreaming(file: File, buffer: []u8) Writer {
const range_off: windows.LARGE_INTEGER = 0;
const range_len: windows.LARGE_INTEGER = 1;
pub const LockError = error{
SystemResources,
FileLocksNotSupported,
} || posix.UnexpectedError;
/// Deprecated
pub const LockError = Io.File.LockError;
/// Blocks when an incompatible lock is held by another process.
/// A process may hold only one type of lock (shared or exclusive) on
-15
View File
@@ -2065,21 +2065,6 @@ test "chown" {
try dir.chown(null, null);
}
test "delete a setAsCwd directory on Windows" {
if (native_os != .windows) return error.SkipZigTest;
var tmp = tmpDir(.{});
// Set tmp dir as current working directory.
try tmp.dir.setAsCwd();
tmp.dir.close();
try testing.expectError(error.FileBusy, tmp.parent_dir.deleteTree(&tmp.sub_path));
// Now set the parent dir as the current working dir for clean up.
try tmp.parent_dir.setAsCwd();
try tmp.parent_dir.deleteTree(&tmp.sub_path);
// Close the parent "tmp" so we don't leak the HANDLE.
tmp.parent_dir.close();
}
test "invalid UTF-8/WTF-8 paths" {
const expected_err = switch (native_os) {
.wasi => error.BadPathName,
-130
View File
@@ -21,7 +21,6 @@ const mem = std.mem;
const elf = std.elf;
const fs = std.fs;
const dl = @import("dynamic_library.zig");
const max_path_bytes = std.fs.max_path_bytes;
const posix = std.posix;
const native_os = builtin.os.tag;
@@ -56,135 +55,6 @@ pub var argv: [][*:0]u8 = if (builtin.link_libc) undefined else switch (native_o
else => undefined,
};
pub fn isGetFdPathSupportedOnTarget(os: std.Target.Os) bool {
return switch (os.tag) {
.windows,
.driverkit,
.ios,
.maccatalyst,
.macos,
.tvos,
.visionos,
.watchos,
.linux,
.illumos,
.freebsd,
.serenity,
=> true,
.dragonfly => os.version_range.semver.max.order(.{ .major = 6, .minor = 0, .patch = 0 }) != .lt,
.netbsd => os.version_range.semver.max.order(.{ .major = 10, .minor = 0, .patch = 0 }) != .lt,
else => false,
};
}
/// Return canonical path of handle `fd`.
///
/// This function is very host-specific and is not universally supported by all hosts.
/// For example, while it generally works on Linux, macOS, FreeBSD or Windows, it is
/// unsupported on WASI.
///
/// * On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// * On other platforms, the result is an opaque sequence of bytes with no particular encoding.
///
/// Calling this function is usually a bug.
pub fn getFdPath(fd: std.posix.fd_t, out_buffer: *[max_path_bytes]u8) std.posix.RealPathError![]u8 {
if (!comptime isGetFdPathSupportedOnTarget(builtin.os)) {
@compileError("querying for canonical path of a handle is unsupported on this host");
}
switch (native_os) {
.windows => {
var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined;
const wide_slice = try windows.GetFinalPathNameByHandle(fd, .{}, wide_buf[0..]);
const end_index = std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
return out_buffer[0..end_index];
},
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
// On macOS, we can use F.GETPATH fcntl command to query the OS for
// the path to the file descriptor.
@memset(out_buffer[0..max_path_bytes], 0);
switch (posix.errno(posix.system.fcntl(fd, posix.F.GETPATH, out_buffer))) {
.SUCCESS => {},
.BADF => return error.FileNotFound,
.NOSPC => return error.NameTooLong,
.NOENT => return error.FileNotFound,
// TODO man pages for fcntl on macOS don't really tell you what
// errno values to expect when command is F.GETPATH...
else => |err| return posix.unexpectedErrno(err),
}
const len = mem.findScalar(u8, out_buffer[0..], 0) orelse max_path_bytes;
return out_buffer[0..len];
},
.linux, .serenity => {
var procfs_buf: ["/proc/self/fd/-2147483648\x00".len]u8 = undefined;
const proc_path = std.fmt.bufPrintSentinel(procfs_buf[0..], "/proc/self/fd/{d}", .{fd}, 0) catch unreachable;
const target = posix.readlinkZ(proc_path, out_buffer) catch |err| {
switch (err) {
error.NotLink => unreachable,
error.BadPathName => unreachable,
error.UnsupportedReparsePointType => unreachable, // Windows-only
error.NetworkNotFound => unreachable, // Windows-only
else => |e| return e,
}
};
return target;
},
.illumos => {
var procfs_buf: ["/proc/self/path/-2147483648\x00".len]u8 = undefined;
const proc_path = std.fmt.bufPrintSentinel(procfs_buf[0..], "/proc/self/path/{d}", .{fd}, 0) catch unreachable;
const target = posix.readlinkZ(proc_path, out_buffer) catch |err| switch (err) {
error.UnsupportedReparsePointType => unreachable,
error.NotLink => unreachable,
else => |e| return e,
};
return target;
},
.freebsd => {
var kfile: std.c.kinfo_file = undefined;
kfile.structsize = std.c.KINFO_FILE_SIZE;
switch (posix.errno(std.c.fcntl(fd, std.c.F.KINFO, @intFromPtr(&kfile)))) {
.SUCCESS => {},
.BADF => return error.FileNotFound,
else => |err| return posix.unexpectedErrno(err),
}
const len = mem.findScalar(u8, &kfile.path, 0) orelse max_path_bytes;
if (len == 0) return error.NameTooLong;
const result = out_buffer[0..len];
@memcpy(result, kfile.path[0..len]);
return result;
},
.dragonfly => {
@memset(out_buffer[0..max_path_bytes], 0);
switch (posix.errno(std.c.fcntl(fd, posix.F.GETPATH, out_buffer))) {
.SUCCESS => {},
.BADF => return error.FileNotFound,
.RANGE => return error.NameTooLong,
else => |err| return posix.unexpectedErrno(err),
}
const len = mem.findScalar(u8, out_buffer[0..], 0) orelse max_path_bytes;
return out_buffer[0..len];
},
.netbsd => {
@memset(out_buffer[0..max_path_bytes], 0);
switch (posix.errno(std.c.fcntl(fd, posix.F.GETPATH, out_buffer))) {
.SUCCESS => {},
.ACCES => return error.AccessDenied,
.BADF => return error.FileNotFound,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.RANGE => return error.NameTooLong,
else => |err| return posix.unexpectedErrno(err),
}
const len = mem.findScalar(u8, out_buffer[0..], 0) orelse max_path_bytes;
return out_buffer[0..len];
},
else => unreachable, // made unreachable by isGetFdPathSupportedOnTarget above
}
}
pub const FstatError = error{
SystemResources,
AccessDenied,
-1014
View File
@@ -319,35 +319,6 @@ pub const FChmodError = error{
ReadOnlyFileSystem,
} || UnexpectedError;
/// Changes the mode of the file referred to by the file descriptor.
///
/// 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 fchmod(fd: fd_t, mode: mode_t) FChmodError!void {
if (!fs.has_executable_bit) @compileError("fchmod unsupported by target OS");
while (true) {
const res = system.fchmod(fd, mode);
switch (errno(res)) {
.SUCCESS => return,
.INTR => continue,
.BADF => unreachable,
.FAULT => unreachable,
.INVAL => unreachable,
.ACCES => return error.AccessDenied,
.IO => return error.InputOutput,
.LOOP => return error.SymLinkLoop,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.FileNotFound,
.PERM => return error.PermissionDenied,
.ROFS => return error.ReadOnlyFileSystem,
else => |err| return unexpectedErrno(err),
}
}
}
pub const FChmodAtError = FChmodError || error{
/// A component of `path` exceeded `NAME_MAX`, or the entire path exceeded
/// `PATH_MAX`.
@@ -533,50 +504,6 @@ fn fchmodat2(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtEr
}
}
pub const FChownError = error{
AccessDenied,
PermissionDenied,
InputOutput,
SymLinkLoop,
FileNotFound,
SystemResources,
ReadOnlyFileSystem,
} || UnexpectedError;
/// Changes the owner and group of the file referred to by the file descriptor.
/// 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 fchown(fd: fd_t, owner: ?uid_t, group: ?gid_t) FChownError!void {
switch (native_os) {
.windows, .wasi => @compileError("Unsupported OS"),
else => {},
}
while (true) {
const res = system.fchown(fd, owner orelse ~@as(uid_t, 0), group orelse ~@as(gid_t, 0));
switch (errno(res)) {
.SUCCESS => return,
.INTR => continue,
.BADF => unreachable, // Can be reached if the fd refers to a directory opened without `Dir.OpenOptions{ .iterate = true }`
.FAULT => unreachable,
.INVAL => unreachable,
.ACCES => return error.AccessDenied,
.IO => return error.InputOutput,
.LOOP => return error.SymLinkLoop,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.FileNotFound,
.PERM => return error.PermissionDenied,
.ROFS => return error.ReadOnlyFileSystem,
else => |err| return unexpectedErrno(err),
}
}
}
pub const RebootError = error{
PermissionDenied,
} || UnexpectedError;
@@ -1926,150 +1853,6 @@ pub fn getcwd(out_buffer: []u8) GetCwdError![]u8 {
}
}
pub const SymLinkError = error{
/// In WASI, this error may occur when the file descriptor does
/// not hold the required rights to create a new symbolic link relative to it.
AccessDenied,
PermissionDenied,
DiskQuota,
PathAlreadyExists,
FileSystem,
SymLinkLoop,
FileNotFound,
SystemResources,
NoSpaceLeft,
ReadOnlyFileSystem,
NotDir,
NameTooLong,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
BadPathName,
} || UnexpectedError;
/// 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.
/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, both paths should be encoded as valid UTF-8.
/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
/// If `sym_link_path` exists, it will not be overwritten.
/// See also `symlinkZ.
pub fn symlink(target_path: []const u8, sym_link_path: []const u8) SymLinkError!void {
if (native_os == .windows) {
@compileError("symlink is not supported on Windows; use std.os.windows.CreateSymbolicLink instead");
} else if (native_os == .wasi and !builtin.link_libc) {
return symlinkat(target_path, AT.FDCWD, sym_link_path);
}
const target_path_c = try toPosixPath(target_path);
const sym_link_path_c = try toPosixPath(sym_link_path);
return symlinkZ(&target_path_c, &sym_link_path_c);
}
/// This is the same as `symlink` except the parameters are null-terminated pointers.
/// See also `symlink`.
pub fn symlinkZ(target_path: [*:0]const u8, sym_link_path: [*:0]const u8) SymLinkError!void {
if (native_os == .windows) {
@compileError("symlink is not supported on Windows; use std.os.windows.CreateSymbolicLink instead");
} else if (native_os == .wasi and !builtin.link_libc) {
return symlinkatZ(target_path, fs.cwd().fd, sym_link_path);
}
switch (errno(system.symlink(target_path, sym_link_path))) {
.SUCCESS => return,
.FAULT => unreachable,
.INVAL => unreachable,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.DQUOT => return error.DiskQuota,
.EXIST => return error.PathAlreadyExists,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.ROFS => return error.ReadOnlyFileSystem,
.ILSEQ => return error.BadPathName,
else => |err| return unexpectedErrno(err),
}
}
/// Similar to `symlink`, however, creates a symbolic link named `sym_link_path` which contains the string
/// `target_path` **relative** to `newdirfd` directory handle.
/// 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.
/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, both paths should be encoded as valid UTF-8.
/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
/// If `sym_link_path` exists, it will not be overwritten.
/// See also `symlinkatWasi`, `symlinkatZ` and `symlinkatW`.
pub fn symlinkat(target_path: []const u8, newdirfd: fd_t, sym_link_path: []const u8) SymLinkError!void {
if (native_os == .windows) {
@compileError("symlinkat is not supported on Windows; use std.os.windows.CreateSymbolicLink instead");
} else if (native_os == .wasi and !builtin.link_libc) {
return symlinkatWasi(target_path, newdirfd, sym_link_path);
}
const target_path_c = try toPosixPath(target_path);
const sym_link_path_c = try toPosixPath(sym_link_path);
return symlinkatZ(&target_path_c, newdirfd, &sym_link_path_c);
}
/// WASI-only. The same as `symlinkat` but targeting WASI.
/// See also `symlinkat`.
pub fn symlinkatWasi(target_path: []const u8, newdirfd: fd_t, sym_link_path: []const u8) SymLinkError!void {
switch (wasi.path_symlink(target_path.ptr, target_path.len, newdirfd, sym_link_path.ptr, sym_link_path.len)) {
.SUCCESS => {},
.FAULT => unreachable,
.INVAL => unreachable,
.BADF => unreachable,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.DQUOT => return error.DiskQuota,
.EXIST => return error.PathAlreadyExists,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.ROFS => return error.ReadOnlyFileSystem,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return unexpectedErrno(err),
}
}
/// The same as `symlinkat` except the parameters are null-terminated pointers.
/// See also `symlinkat`.
pub fn symlinkatZ(target_path: [*:0]const u8, newdirfd: fd_t, sym_link_path: [*:0]const u8) SymLinkError!void {
if (native_os == .windows) {
@compileError("symlinkat is not supported on Windows; use std.os.windows.CreateSymbolicLink instead");
} else if (native_os == .wasi and !builtin.link_libc) {
return symlinkat(mem.sliceTo(target_path, 0), newdirfd, mem.sliceTo(sym_link_path, 0));
}
switch (errno(system.symlinkat(target_path, newdirfd, sym_link_path))) {
.SUCCESS => return,
.FAULT => unreachable,
.INVAL => unreachable,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.DQUOT => return error.DiskQuota,
.EXIST => return error.PathAlreadyExists,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.ROFS => return error.ReadOnlyFileSystem,
.ILSEQ => return error.BadPathName,
else => |err| return unexpectedErrno(err),
}
}
pub const LinkError = UnexpectedError || error{
AccessDenied,
PermissionDenied,
@@ -2216,334 +1999,6 @@ pub fn linkat(
return try linkatZ(olddir, &old, newdir, &new, flags);
}
pub const UnlinkError = error{
FileNotFound,
/// In WASI, this error may occur when the file descriptor does
/// not hold the required rights to unlink a resource by path relative to it.
AccessDenied,
PermissionDenied,
FileBusy,
FileSystem,
IsDir,
SymLinkLoop,
NameTooLong,
NotDir,
SystemResources,
ReadOnlyFileSystem,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
/// Windows: file paths cannot contain these characters:
/// '/', '*', '?', '"', '<', '>', '|'
BadPathName,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
} || UnexpectedError;
/// Delete a name and possibly the file it refers to.
/// On Windows, `file_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `file_path` should be encoded as valid UTF-8.
/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
/// See also `unlinkZ`.
pub fn unlink(file_path: []const u8) UnlinkError!void {
if (native_os == .wasi and !builtin.link_libc) {
return unlinkat(AT.FDCWD, file_path, 0) catch |err| switch (err) {
error.DirNotEmpty => unreachable, // only occurs when targeting directories
else => |e| return e,
};
} else if (native_os == .windows) {
const file_path_w = try windows.sliceToPrefixedFileW(null, file_path);
return unlinkW(file_path_w.span());
} else {
const file_path_c = try toPosixPath(file_path);
return unlinkZ(&file_path_c);
}
}
/// Same as `unlink` except the parameter is null terminated.
pub fn unlinkZ(file_path: [*:0]const u8) UnlinkError!void {
if (native_os == .windows) {
const file_path_w = try windows.cStrToPrefixedFileW(null, file_path);
return unlinkW(file_path_w.span());
} else if (native_os == .wasi and !builtin.link_libc) {
return unlink(mem.sliceTo(file_path, 0));
}
switch (errno(system.unlink(file_path))) {
.SUCCESS => return,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.FAULT => unreachable,
.INVAL => unreachable,
.IO => return error.FileSystem,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.ROFS => return error.ReadOnlyFileSystem,
.ILSEQ => return error.BadPathName,
else => |err| return unexpectedErrno(err),
}
}
/// Windows-only. Same as `unlink` except the parameter is null-terminated, WTF16 LE encoded.
pub fn unlinkW(file_path_w: []const u16) UnlinkError!void {
windows.DeleteFile(file_path_w, .{ .dir = fs.cwd().fd }) catch |err| switch (err) {
error.DirNotEmpty => unreachable, // we're not passing .remove_dir = true
else => |e| return e,
};
}
pub const UnlinkatError = UnlinkError || error{
/// When passing `AT.REMOVEDIR`, this error occurs when the named directory is not empty.
DirNotEmpty,
};
/// Delete a file name and possibly the file it refers to, based on an open directory handle.
/// On Windows, `file_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `file_path` should be encoded as valid UTF-8.
/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
/// Asserts that the path parameter has no null bytes.
pub fn unlinkat(dirfd: fd_t, file_path: []const u8, flags: u32) UnlinkatError!void {
if (native_os == .windows) {
const file_path_w = try windows.sliceToPrefixedFileW(dirfd, file_path);
return unlinkatW(dirfd, file_path_w.span(), flags);
} else if (native_os == .wasi and !builtin.link_libc) {
return unlinkatWasi(dirfd, file_path, flags);
} else {
const file_path_c = try toPosixPath(file_path);
return unlinkatZ(dirfd, &file_path_c, flags);
}
}
/// WASI-only. Same as `unlinkat` but targeting WASI.
/// See also `unlinkat`.
pub fn unlinkatWasi(dirfd: fd_t, file_path: []const u8, flags: u32) UnlinkatError!void {
const remove_dir = (flags & AT.REMOVEDIR) != 0;
const res = if (remove_dir)
wasi.path_remove_directory(dirfd, file_path.ptr, file_path.len)
else
wasi.path_unlink_file(dirfd, file_path.ptr, file_path.len);
switch (res) {
.SUCCESS => return,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.FAULT => unreachable,
.IO => return error.FileSystem,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.ROFS => return error.ReadOnlyFileSystem,
.NOTEMPTY => return error.DirNotEmpty,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
.INVAL => unreachable, // invalid flags, or pathname has . as last component
.BADF => unreachable, // always a race condition
else => |err| return unexpectedErrno(err),
}
}
/// Same as `unlinkat` but `file_path` is a null-terminated string.
pub fn unlinkatZ(dirfd: fd_t, file_path_c: [*:0]const u8, flags: u32) UnlinkatError!void {
if (native_os == .windows) {
const file_path_w = try windows.cStrToPrefixedFileW(dirfd, file_path_c);
return unlinkatW(dirfd, file_path_w.span(), flags);
} else if (native_os == .wasi and !builtin.link_libc) {
return unlinkat(dirfd, mem.sliceTo(file_path_c, 0), flags);
}
switch (errno(system.unlinkat(dirfd, file_path_c, flags))) {
.SUCCESS => return,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.FAULT => unreachable,
.IO => return error.FileSystem,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.ROFS => return error.ReadOnlyFileSystem,
.EXIST => return error.DirNotEmpty,
.NOTEMPTY => return error.DirNotEmpty,
.ILSEQ => return error.BadPathName,
.INVAL => unreachable, // invalid flags, or pathname has . as last component
.BADF => unreachable, // always a race condition
else => |err| return unexpectedErrno(err),
}
}
/// Same as `unlinkat` but `sub_path_w` is WTF16LE, NT prefixed. Windows only.
pub fn unlinkatW(dirfd: fd_t, sub_path_w: []const u16, flags: u32) UnlinkatError!void {
const remove_dir = (flags & AT.REMOVEDIR) != 0;
return windows.DeleteFile(sub_path_w, .{ .dir = dirfd, .remove_dir = remove_dir });
}
pub const RenameError = error{
/// In WASI, this error may occur when the file descriptor does
/// not hold the required rights to rename a resource by path relative to it.
///
/// On Windows, this error may be returned instead of PathAlreadyExists when
/// renaming a directory over an existing directory.
AccessDenied,
PermissionDenied,
FileBusy,
DiskQuota,
IsDir,
SymLinkLoop,
LinkQuotaExceeded,
NameTooLong,
FileNotFound,
NotDir,
SystemResources,
NoSpaceLeft,
PathAlreadyExists,
ReadOnlyFileSystem,
RenameAcrossMountPoints,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
BadPathName,
NoDevice,
SharingViolation,
PipeBusy,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
/// On Windows, antivirus software is enabled by default. It can be
/// disabled, but Windows Update sometimes ignores the user's preference
/// and re-enables it. When enabled, antivirus software on Windows
/// intercepts file system operations and makes them significantly slower
/// in addition to possibly failing with this error code.
AntivirusInterference,
} || UnexpectedError;
/// Change the name or location of a file.
/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, both paths should be encoded as valid UTF-8.
/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
pub fn rename(old_path: []const u8, new_path: []const u8) RenameError!void {
if (native_os == .wasi and !builtin.link_libc) {
return renameat(AT.FDCWD, old_path, AT.FDCWD, new_path);
} else if (native_os == .windows) {
const old_path_w = try windows.sliceToPrefixedFileW(null, old_path);
const new_path_w = try windows.sliceToPrefixedFileW(null, new_path);
return renameW(old_path_w.span().ptr, new_path_w.span().ptr);
} else {
const old_path_c = try toPosixPath(old_path);
const new_path_c = try toPosixPath(new_path);
return renameZ(&old_path_c, &new_path_c);
}
}
/// Same as `rename` except the parameters are null-terminated.
pub fn renameZ(old_path: [*:0]const u8, new_path: [*:0]const u8) RenameError!void {
if (native_os == .windows) {
const old_path_w = try windows.cStrToPrefixedFileW(null, old_path);
const new_path_w = try windows.cStrToPrefixedFileW(null, new_path);
return renameW(old_path_w.span().ptr, new_path_w.span().ptr);
} else if (native_os == .wasi and !builtin.link_libc) {
return rename(mem.sliceTo(old_path, 0), mem.sliceTo(new_path, 0));
}
switch (errno(system.rename(old_path, new_path))) {
.SUCCESS => return,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.DQUOT => return error.DiskQuota,
.FAULT => unreachable,
.INVAL => unreachable,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MLINK => return error.LinkQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.EXIST => return error.PathAlreadyExists,
.NOTEMPTY => return error.PathAlreadyExists,
.ROFS => return error.ReadOnlyFileSystem,
.XDEV => return error.RenameAcrossMountPoints,
.ILSEQ => return error.BadPathName,
else => |err| return unexpectedErrno(err),
}
}
/// Same as `rename` except the parameters are null-terminated and WTF16LE encoded.
/// Assumes target is Windows.
pub fn renameW(old_path: [*:0]const u16, new_path: [*:0]const u16) RenameError!void {
const cwd_handle = std.fs.cwd().fd;
return windows.RenameFile(cwd_handle, mem.span(old_path), cwd_handle, mem.span(new_path), true);
}
/// Change the name or location of a file based on an open directory handle.
/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, both paths should be encoded as valid UTF-8.
/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
pub fn renameat(
old_dir_fd: fd_t,
old_path: []const u8,
new_dir_fd: fd_t,
new_path: []const u8,
) RenameError!void {
if (native_os == .windows) {
const old_path_w = try windows.sliceToPrefixedFileW(old_dir_fd, old_path);
const new_path_w = try windows.sliceToPrefixedFileW(new_dir_fd, new_path);
return renameatW(old_dir_fd, old_path_w.span(), new_dir_fd, new_path_w.span(), windows.TRUE);
} else if (native_os == .wasi and !builtin.link_libc) {
const old: RelativePathWasi = .{ .dir_fd = old_dir_fd, .relative_path = old_path };
const new: RelativePathWasi = .{ .dir_fd = new_dir_fd, .relative_path = new_path };
return renameatWasi(old, new);
} else {
const old_path_c = try toPosixPath(old_path);
const new_path_c = try toPosixPath(new_path);
return renameatZ(old_dir_fd, &old_path_c, new_dir_fd, &new_path_c);
}
}
/// WASI-only. Same as `renameat` expect targeting WASI.
/// See also `renameat`.
fn renameatWasi(old: RelativePathWasi, new: RelativePathWasi) RenameError!void {
switch (wasi.path_rename(old.dir_fd, old.relative_path.ptr, old.relative_path.len, new.dir_fd, new.relative_path.ptr, new.relative_path.len)) {
.SUCCESS => return,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.DQUOT => return error.DiskQuota,
.FAULT => unreachable,
.INVAL => unreachable,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MLINK => return error.LinkQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.EXIST => return error.PathAlreadyExists,
.NOTEMPTY => return error.PathAlreadyExists,
.ROFS => return error.ReadOnlyFileSystem,
.XDEV => return error.RenameAcrossMountPoints,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return unexpectedErrno(err),
}
}
/// An fd-relative file path
///
/// This is currently only used for WASI-specific functionality, but the concept
@@ -2555,58 +2010,6 @@ const RelativePathWasi = struct {
relative_path: []const u8,
};
/// Same as `renameat` except the parameters are null-terminated.
pub fn renameatZ(
old_dir_fd: fd_t,
old_path: [*:0]const u8,
new_dir_fd: fd_t,
new_path: [*:0]const u8,
) RenameError!void {
if (native_os == .windows) {
const old_path_w = try windows.cStrToPrefixedFileW(old_dir_fd, old_path);
const new_path_w = try windows.cStrToPrefixedFileW(new_dir_fd, new_path);
return renameatW(old_dir_fd, old_path_w.span(), new_dir_fd, new_path_w.span(), windows.TRUE);
} else if (native_os == .wasi and !builtin.link_libc) {
return renameat(old_dir_fd, mem.sliceTo(old_path, 0), new_dir_fd, mem.sliceTo(new_path, 0));
}
switch (errno(system.renameat(old_dir_fd, old_path, new_dir_fd, new_path))) {
.SUCCESS => return,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.DQUOT => return error.DiskQuota,
.FAULT => unreachable,
.INVAL => unreachable,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MLINK => return error.LinkQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.EXIST => return error.PathAlreadyExists,
.NOTEMPTY => return error.PathAlreadyExists,
.ROFS => return error.ReadOnlyFileSystem,
.XDEV => return error.RenameAcrossMountPoints,
.ILSEQ => return error.BadPathName,
else => |err| return unexpectedErrno(err),
}
}
/// Same as `renameat` but Windows-only and the path parameters are
/// [WTF-16](https://wtf-8.codeberg.page/#potentially-ill-formed-utf-16) encoded.
pub fn renameatW(
old_dir_fd: fd_t,
old_path_w: []const u16,
new_dir_fd: fd_t,
new_path_w: []const u16,
ReplaceIfExists: windows.BOOLEAN,
) RenameError!void {
return windows.RenameFile(old_dir_fd, old_path_w, new_dir_fd, new_path_w, ReplaceIfExists != 0);
}
/// On Windows, `sub_dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_dir_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_dir_path` is an opaque sequence of bytes with no particular encoding.
@@ -2723,84 +2126,6 @@ pub fn mkdirW(dir_path_w: []const u16, mode: mode_t) MakeDirError!void {
windows.CloseHandle(sub_dir_handle);
}
pub const DeleteDirError = error{
AccessDenied,
PermissionDenied,
FileBusy,
SymLinkLoop,
NameTooLong,
FileNotFound,
SystemResources,
NotDir,
DirNotEmpty,
ReadOnlyFileSystem,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
BadPathName,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
} || UnexpectedError;
/// Deletes an empty directory.
/// On Windows, `dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `dir_path` should be encoded as valid UTF-8.
/// On other platforms, `dir_path` is an opaque sequence of bytes with no particular encoding.
pub fn rmdir(dir_path: []const u8) DeleteDirError!void {
if (native_os == .wasi and !builtin.link_libc) {
return unlinkat(AT.FDCWD, dir_path, AT.REMOVEDIR) catch |err| switch (err) {
error.FileSystem => unreachable, // only occurs when targeting files
error.IsDir => unreachable, // only occurs when targeting files
else => |e| return e,
};
} else if (native_os == .windows) {
const dir_path_w = try windows.sliceToPrefixedFileW(null, dir_path);
return rmdirW(dir_path_w.span());
} else {
const dir_path_c = try toPosixPath(dir_path);
return rmdirZ(&dir_path_c);
}
}
/// Same as `rmdir` except the parameter is null-terminated.
/// On Windows, `dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `dir_path` should be encoded as valid UTF-8.
/// On other platforms, `dir_path` is an opaque sequence of bytes with no particular encoding.
pub fn rmdirZ(dir_path: [*:0]const u8) DeleteDirError!void {
if (native_os == .windows) {
const dir_path_w = try windows.cStrToPrefixedFileW(null, dir_path);
return rmdirW(dir_path_w.span());
} else if (native_os == .wasi and !builtin.link_libc) {
return rmdir(mem.sliceTo(dir_path, 0));
}
switch (errno(system.rmdir(dir_path))) {
.SUCCESS => return,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.FAULT => unreachable,
.INVAL => return error.BadPathName,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.EXIST => return error.DirNotEmpty,
.NOTEMPTY => return error.DirNotEmpty,
.ROFS => return error.ReadOnlyFileSystem,
.ILSEQ => return error.BadPathName,
else => |err| return unexpectedErrno(err),
}
}
/// Windows-only. Same as `rmdir` except the parameter is WTF-16 LE encoded.
pub fn rmdirW(dir_path_w: []const u16) DeleteDirError!void {
return windows.DeleteFile(dir_path_w, .{ .dir = fs.cwd().fd, .remove_dir = true }) catch |err| switch (err) {
error.IsDir => unreachable,
else => |e| return e,
};
}
pub const ChangeCurDirError = error{
AccessDenied,
FileSystem,
@@ -2889,194 +2214,6 @@ pub fn fchdir(dirfd: fd_t) FchdirError!void {
}
}
pub const ReadLinkError = error{
/// In WASI, this error may occur when the file descriptor does
/// not hold the required rights to read value of a symbolic link relative to it.
AccessDenied,
PermissionDenied,
FileSystem,
SymLinkLoop,
NameTooLong,
FileNotFound,
SystemResources,
NotLink,
NotDir,
/// WASI: file paths must be valid UTF-8.
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
BadPathName,
/// Windows-only. This error may occur if the opened reparse point is
/// of unsupported type.
UnsupportedReparsePointType,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
/// On Windows, antivirus software is enabled by default. It can be
/// disabled, but Windows Update sometimes ignores the user's preference
/// and re-enables it. When enabled, antivirus software on Windows
/// intercepts file system operations and makes them significantly slower
/// in addition to possibly failing with this error code.
AntivirusInterference,
} || UnexpectedError;
/// Read value of a symbolic link.
/// On Windows, `file_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `file_path` should be encoded as valid UTF-8.
/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
/// The return value is a slice of `out_buffer` from index 0.
/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, the result is encoded as UTF-8.
/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
pub fn readlink(file_path: []const u8, out_buffer: []u8) ReadLinkError![]u8 {
if (native_os == .wasi and !builtin.link_libc) {
return readlinkat(AT.FDCWD, file_path, out_buffer);
} else if (native_os == .windows) {
var file_path_w = try windows.sliceToPrefixedFileW(null, file_path);
const result_w = try readlinkW(file_path_w.span(), &file_path_w.data);
const len = std.unicode.calcWtf8Len(result_w);
if (len > out_buffer.len) return error.NameTooLong;
const end_index = std.unicode.wtf16LeToWtf8(out_buffer, result_w);
return out_buffer[0..end_index];
} else {
const file_path_c = try toPosixPath(file_path);
return readlinkZ(&file_path_c, out_buffer);
}
}
/// Windows-only. Same as `readlink` except `file_path` is WTF-16 LE encoded, NT-prefixed.
/// The result is encoded as WTF-16 LE.
///
/// `file_path` will never be accessed after `out_buffer` has been written to, so it
/// is safe to reuse a single buffer for both.
///
/// See also `readlinkZ`.
pub fn readlinkW(file_path: []const u16, out_buffer: []u16) ReadLinkError![]u16 {
return windows.ReadLink(fs.cwd().fd, file_path, out_buffer);
}
/// Same as `readlink` except `file_path` is null-terminated.
pub fn readlinkZ(file_path: [*:0]const u8, out_buffer: []u8) ReadLinkError![]u8 {
if (native_os == .windows) {
var file_path_w = try windows.cStrToPrefixedFileW(null, file_path);
const result_w = try readlinkW(file_path_w.span(), &file_path_w.data);
const len = std.unicode.calcWtf8Len(result_w);
if (len > out_buffer.len) return error.NameTooLong;
const end_index = std.unicode.wtf16LeToWtf8(out_buffer, result_w);
return out_buffer[0..end_index];
} else if (native_os == .wasi and !builtin.link_libc) {
return readlink(mem.sliceTo(file_path, 0), out_buffer);
}
const rc = system.readlink(file_path, out_buffer.ptr, out_buffer.len);
switch (errno(rc)) {
.SUCCESS => return out_buffer[0..@bitCast(rc)],
.ACCES => return error.AccessDenied,
.FAULT => unreachable,
.INVAL => return error.NotLink,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.ILSEQ => return error.BadPathName,
else => |err| return unexpectedErrno(err),
}
}
/// Similar to `readlink` except reads value of a symbolink link **relative** to `dirfd` directory handle.
/// On Windows, `file_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `file_path` should be encoded as valid UTF-8.
/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
/// The return value is a slice of `out_buffer` from index 0.
/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, the result is encoded as UTF-8.
/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
/// See also `readlinkatWasi`, `realinkatZ` and `realinkatW`.
pub fn readlinkat(dirfd: fd_t, file_path: []const u8, out_buffer: []u8) ReadLinkError![]u8 {
if (native_os == .wasi and !builtin.link_libc) {
return readlinkatWasi(dirfd, file_path, out_buffer);
}
if (native_os == .windows) {
var file_path_w = try windows.sliceToPrefixedFileW(dirfd, file_path);
const result_w = try readlinkatW(dirfd, file_path_w.span(), &file_path_w.data);
const len = std.unicode.calcWtf8Len(result_w);
if (len > out_buffer.len) return error.NameTooLong;
const end_index = std.unicode.wtf16LeToWtf8(out_buffer, result_w);
return out_buffer[0..end_index];
}
const file_path_c = try toPosixPath(file_path);
return readlinkatZ(dirfd, &file_path_c, out_buffer);
}
/// WASI-only. Same as `readlinkat` but targets WASI.
/// See also `readlinkat`.
pub fn readlinkatWasi(dirfd: fd_t, file_path: []const u8, out_buffer: []u8) ReadLinkError![]u8 {
var bufused: usize = undefined;
switch (wasi.path_readlink(dirfd, file_path.ptr, file_path.len, out_buffer.ptr, out_buffer.len, &bufused)) {
.SUCCESS => return out_buffer[0..bufused],
.ACCES => return error.AccessDenied,
.FAULT => unreachable,
.INVAL => return error.NotLink,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return unexpectedErrno(err),
}
}
/// Windows-only. Same as `readlinkat` except `file_path` WTF16 LE encoded, NT-prefixed.
/// The result is encoded as WTF-16 LE.
///
/// `file_path` will never be accessed after `out_buffer` has been written to, so it
/// is safe to reuse a single buffer for both.
///
/// See also `readlinkat`.
pub fn readlinkatW(dirfd: fd_t, file_path: []const u16, out_buffer: []u16) ReadLinkError![]u16 {
return windows.ReadLink(dirfd, file_path, out_buffer);
}
/// Same as `readlinkat` except `file_path` is null-terminated.
/// See also `readlinkat`.
pub fn readlinkatZ(dirfd: fd_t, file_path: [*:0]const u8, out_buffer: []u8) ReadLinkError![]u8 {
if (native_os == .windows) {
var file_path_w = try windows.cStrToPrefixedFileW(dirfd, file_path);
const result_w = try readlinkatW(dirfd, file_path_w.span(), &file_path_w.data);
const len = std.unicode.calcWtf8Len(result_w);
if (len > out_buffer.len) return error.NameTooLong;
const end_index = std.unicode.wtf16LeToWtf8(out_buffer, result_w);
return out_buffer[0..end_index];
} else if (native_os == .wasi and !builtin.link_libc) {
return readlinkat(dirfd, mem.sliceTo(file_path, 0), out_buffer);
}
const rc = system.readlinkat(dirfd, file_path, out_buffer.ptr, out_buffer.len);
switch (errno(rc)) {
.SUCCESS => return out_buffer[0..@bitCast(rc)],
.ACCES => return error.AccessDenied,
.FAULT => unreachable,
.INVAL => return error.NotLink,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.ILSEQ => return error.BadPathName,
else => |err| return unexpectedErrno(err),
}
}
pub const SetEidError = error{
InvalidUserId,
PermissionDenied,
@@ -4814,157 +3951,6 @@ pub fn flock(fd: fd_t, operation: i32) FlockError!void {
}
}
pub const RealPathError = error{
FileNotFound,
AccessDenied,
PermissionDenied,
NameTooLong,
NotSupported,
NotDir,
SymLinkLoop,
InputOutput,
FileTooBig,
IsDir,
ProcessFdQuotaExceeded,
SystemFdQuotaExceeded,
NoDevice,
SystemResources,
NoSpaceLeft,
FileSystem,
DeviceBusy,
ProcessNotFound,
SharingViolation,
PipeBusy,
/// Windows: file paths provided by the user must be valid WTF-8.
/// https://wtf-8.codeberg.page/
BadPathName,
/// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound,
PathAlreadyExists,
/// On Windows, antivirus software is enabled by default. It can be
/// disabled, but Windows Update sometimes ignores the user's preference
/// and re-enables it. When enabled, antivirus software on Windows
/// intercepts file system operations and makes them significantly slower
/// in addition to possibly failing with this error code.
AntivirusInterference,
/// On Windows, the volume does not contain a recognized file system. File
/// system drivers might not be loaded, or the volume may be corrupt.
UnrecognizedVolume,
Canceled,
} || UnexpectedError;
/// Return the canonicalized absolute pathname.
///
/// Expands all symbolic links and resolves references to `.`, `..`, and
/// extra `/` characters in `pathname`.
///
/// On Windows, `pathname` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
///
/// On other platforms, `pathname` is an opaque sequence of bytes with no particular encoding.
///
/// The return value is a slice of `out_buffer`, but not necessarily from the beginning.
///
/// See also `realpathZ` and `realpathW`.
///
/// * On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// * On other platforms, the result is an opaque sequence of bytes with no particular encoding.
///
/// Calling this function is usually a bug.
pub fn realpath(pathname: []const u8, out_buffer: *[max_path_bytes]u8) RealPathError![]u8 {
if (native_os == .windows) {
var pathname_w = try windows.sliceToPrefixedFileW(null, pathname);
const wide_slice = try realpathW2(pathname_w.span(), &pathname_w.data);
const end_index = std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
return out_buffer[0..end_index];
} else if (native_os == .wasi and !builtin.link_libc) {
@compileError("WASI does not support os.realpath");
}
const pathname_c = try toPosixPath(pathname);
return realpathZ(&pathname_c, out_buffer);
}
/// Same as `realpath` except `pathname` is null-terminated.
///
/// Calling this function is usually a bug.
pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[max_path_bytes]u8) RealPathError![]u8 {
if (native_os == .windows) {
var pathname_w = try windows.cStrToPrefixedFileW(null, pathname);
const wide_slice = try realpathW2(pathname_w.span(), &pathname_w.data);
const end_index = std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
return out_buffer[0..end_index];
} else if (native_os == .wasi and !builtin.link_libc) {
return realpath(mem.sliceTo(pathname, 0), out_buffer);
}
if (!builtin.link_libc) {
const flags: O = switch (native_os) {
.linux => .{
.NONBLOCK = true,
.CLOEXEC = true,
.PATH = true,
},
else => .{
.NONBLOCK = true,
.CLOEXEC = true,
},
};
const fd = openZ(pathname, flags, 0) catch |err| switch (err) {
error.FileLocksNotSupported => unreachable,
error.WouldBlock => unreachable,
error.FileBusy => unreachable, // not asking for write permissions
else => |e| return e,
};
defer close(fd);
return std.os.getFdPath(fd, out_buffer);
}
const result_path = std.c.realpath(pathname, out_buffer) orelse switch (@as(E, @enumFromInt(std.c._errno().*))) {
.SUCCESS => unreachable,
.INVAL => unreachable,
.BADF => unreachable,
.FAULT => unreachable,
.ACCES => return error.AccessDenied,
.NOENT => return error.FileNotFound,
.OPNOTSUPP => return error.NotSupported,
.NOTDIR => return error.NotDir,
.NAMETOOLONG => return error.NameTooLong,
.LOOP => return error.SymLinkLoop,
.IO => return error.InputOutput,
else => |err| return unexpectedErrno(err),
};
return mem.sliceTo(result_path, 0);
}
/// Deprecated: use `realpathW2`.
///
/// Same as `realpath` except `pathname` is WTF16LE-encoded.
///
/// The result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
///
/// Calling this function is usually a bug.
pub fn realpathW(pathname: []const u16, out_buffer: *[max_path_bytes]u8) RealPathError![]u8 {
return fs.cwd().realpathW(pathname, out_buffer);
}
/// Same as `realpath` except `pathname` is WTF16LE-encoded.
///
/// The result is encoded as WTF16LE.
///
/// Calling this function is usually a bug.
pub fn realpathW2(pathname: []const u16, out_buffer: *[std.os.windows.PATH_MAX_WIDE]u16) RealPathError![]u16 {
return fs.cwd().realpathW2(pathname, out_buffer);
}
/// Spurious wakeups are possible and no precision of timing is guaranteed.
pub fn nanosleep(seconds: u64, nanoseconds: u64) void {
var req = timespec{
+4 -2
View File
@@ -786,7 +786,9 @@ test glibcVerFromLinkName {
}
fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion {
var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) {
const cwd: Io.Dir = .cwd();
var dir = cwd.openDir(io, rpath, .{}) catch |err| switch (err) {
error.NameTooLong => return error.Unexpected,
error.BadPathName => return error.Unexpected,
error.DeviceBusy => return error.Unexpected,
@@ -805,7 +807,7 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion {
error.Unexpected => |e| return e,
error.Canceled => |e| return e,
};
defer dir.close();
defer dir.close(io);
// Now we have a candidate for the path to libc shared object. In
// the past, we used readlink() here because the link name would