diff --git a/CMakeLists.txt b/CMakeLists.txt index 7090f88527..87424ab522 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -437,8 +437,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 diff --git a/build.zig b/build.zig index 4f1d8b98bb..42e8b6fcb6 100644 --- a/build.zig +++ b/build.zig @@ -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\"", .{}); @@ -1146,10 +1150,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}); @@ -1159,13 +1166,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" }, @@ -1176,9 +1183,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 |_| {} @@ -1574,6 +1581,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(.{ @@ -1583,7 +1592,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), }); diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index 272b1b8077..fb059122b9 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -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); diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 50a2804938..fcd94ce134 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -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. diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig index e68c3e7892..dd324d76ec 100644 --- a/lib/std/Build/Cache.zig +++ b/lib/std/Build/Cache.zig @@ -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(); diff --git a/lib/std/Build/Cache/Directory.zig b/lib/std/Build/Cache/Directory.zig index a105a91ed6..305ef25361 100644 --- a/lib/std/Build/Cache/Directory.zig +++ b/lib/std/Build/Cache/Directory.zig @@ -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; } diff --git a/lib/std/Build/Cache/Path.zig b/lib/std/Build/Cache/Path.zig index 92290cfdf4..f6f76c1e8f 100644 --- a/lib/std/Build/Cache/Path.zig +++ b/lib/std/Build/Cache/Path.zig @@ -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)); diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index c247e69461..33fe755c2b 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -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 { diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig index 4f9900ab59..082dc4ffdc 100644 --- a/lib/std/Build/Step/Compile.zig +++ b/lib/std/Build/Step/Compile.zig @@ -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; @@ -1694,19 +1696,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" }), }); @@ -1717,7 +1722,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" }), }); @@ -1793,7 +1798,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); @@ -1826,18 +1831,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. }, @@ -1949,18 +1954,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), }); diff --git a/lib/std/Build/Step/InstallDir.zig b/lib/std/Build/Step/InstallDir.zig index fd8a7d113f..ecb0959cc7 100644 --- a/lib/std/Build/Step/InstallDir.zig +++ b/lib/std/Build/Step/InstallDir.zig @@ -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); diff --git a/lib/std/Build/Step/Options.zig b/lib/std/Build/Step/Options.zig index 441928d5b8..9f5665e93a 100644 --- a/lib/std/Build/Step/Options.zig +++ b/lib/std/Build/Step/Options.zig @@ -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, }), } } diff --git a/lib/std/Io.zig b/lib/std/Io.zig index d3d570b709..e3826ee50f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -662,16 +662,27 @@ pub const VTable = struct { conditionWaitUncancelable: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) void, conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) 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, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 9116434191..c743ba7cca 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -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); +} diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 3f73169085..ee81f9fcaa 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -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; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index b4d6ede23a..a31f2e1a24 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -416,12 +416,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, @@ -512,12 +522,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, @@ -2912,6 +2932,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 directory–relative—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 directory–relative 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, @@ -3264,10 +4613,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 { diff --git a/lib/std/debug.zig b/lib/std/debug.zig index f93612df15..aee5aaba9a 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -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(); } diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 26ce27e2b3..7ee6421b68 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -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`. diff --git a/lib/std/fs/AtomicFile.zig b/lib/std/fs/AtomicFile.zig deleted file mode 100644 index 96793aec72..0000000000 --- a/lib/std/fs/AtomicFile.zig +++ /dev/null @@ -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(); -} diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig deleted file mode 100644 index afb524b685..0000000000 --- a/lib/std/fs/Dir.zig +++ /dev/null @@ -1,2067 +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, - .FileBothDirectoryInformation, - w.FALSE, - null, - if (self.first_iter) @as(w.BOOLEAN, w.TRUE) else @as(w.BOOLEAN, w.FALSE), - ); - self.first_iter = false; - if (io.Information == 0) return null; - self.index = 0; - self.end_index = io.Information; - switch (rc) { - .SUCCESS => {}, - .ACCESS_DENIED => return error.AccessDenied, // Double-check that the Dir was opened with iteration ability - - else => return w.unexpectedStatus(rc), - } - } - - // While the official api docs guarantee FILE_BOTH_DIR_INFORMATION to be aligned properly - // this may not always be the case (e.g. due to faulty VM/Sandboxing tools) - const dir_info: *align(2) w.FILE_BOTH_DIR_INFORMATION = @ptrCast(@alignCast(&self.buf[self.index])); - if (dir_info.NextEntryOffset != 0) { - self.index += dir_info.NextEntryOffset; - } else { - self.index = self.buf.len; - } - - const name_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 & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk .directory; - if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk .sym_link; - break :blk .file; - }; - return Entry{ - .name = name_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 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(pathname, .{ - .dir = self.fd, - .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); - - 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 }; -} diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index 073ab6777b..fda8b5b490 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -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 diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 05e5de5f22..2372f8b6aa 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -2022,21 +2022,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, diff --git a/lib/std/os.zig b/lib/std/os.zig index 110c92da2b..fdd5ab2480 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -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.indexOfScalar(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.indexOfScalar(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.indexOfScalar(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.indexOfScalar(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, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 4176910537..9df8319d5a 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -308,35 +308,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`. @@ -516,50 +487,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; @@ -1911,150 +1838,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, @@ -2201,334 +1984,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 @@ -2540,58 +1995,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. @@ -2705,84 +2108,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, @@ -2871,194 +2196,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, @@ -4801,157 +3938,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{ diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 77bdaf5837..a0c47c3072 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -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