const builtin = @import("builtin"); const native_os = builtin.os.tag; const std = @import("../std.zig"); const Io = std.Io; const testing = std.testing; const fs = std.fs; const mem = std.mem; const wasi = std.os.wasi; const windows = std.os.windows; const posix = std.posix; const ArenaAllocator = std.heap.ArenaAllocator; const Dir = std.fs.Dir; const File = std.fs.File; const tmpDir = testing.tmpDir; const SymLinkFlags = std.fs.Dir.SymLinkFlags; const PathType = enum { relative, absolute, unc, pub fn isSupported(self: PathType, target_os: std.Target.Os) bool { return switch (self) { .relative => true, .absolute => std.os.isGetFdPathSupportedOnTarget(target_os), .unc => target_os.tag == .windows, }; } pub const TransformError = posix.RealPathError || error{OutOfMemory}; pub const TransformFn = fn (allocator: mem.Allocator, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8; pub fn getTransformFn(comptime path_type: PathType) TransformFn { switch (path_type) { .relative => return struct { fn transform(allocator: mem.Allocator, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8 { _ = allocator; _ = dir; return relative_path; } }.transform, .absolute => return struct { fn transform(allocator: mem.Allocator, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8 { // The final path may not actually exist which would cause realpath to fail. // So instead, we get the path of the dir and join it with the relative path. var fd_path_buf: [fs.max_path_bytes]u8 = undefined; const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf); return fs.path.joinZ(allocator, &.{ dir_path, relative_path }); } }.transform, .unc => return struct { fn transform(allocator: mem.Allocator, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8 { // Any drive absolute path (C:\foo) can be converted into a UNC path by // using '127.0.0.1' as the server name and '$' as the share name. var fd_path_buf: [fs.max_path_bytes]u8 = undefined; const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf); const windows_path_type = windows.getWin32PathType(u8, dir_path); switch (windows_path_type) { .unc_absolute => return fs.path.joinZ(allocator, &.{ dir_path, relative_path }), .drive_absolute => { // `C:\<...>` -> `\\127.0.0.1\C$\<...>` const prepended = "\\\\127.0.0.1\\"; var path = try fs.path.joinZ(allocator, &.{ prepended, dir_path, relative_path }); path[prepended.len + 1] = '$'; return path; }, else => unreachable, } } }.transform, } } }; const TestContext = struct { io: Io, path_type: PathType, path_sep: u8, arena: ArenaAllocator, tmp: testing.TmpDir, dir: std.fs.Dir, transform_fn: *const PathType.TransformFn, pub fn init(path_type: PathType, path_sep: u8, allocator: mem.Allocator, transform_fn: *const PathType.TransformFn) TestContext { const tmp = tmpDir(.{ .iterate = true }); return .{ .io = testing.io, .path_type = path_type, .path_sep = path_sep, .arena = ArenaAllocator.init(allocator), .tmp = tmp, .dir = tmp.dir, .transform_fn = transform_fn, }; } pub fn deinit(self: *TestContext) void { self.arena.deinit(); self.tmp.cleanup(); } /// Returns the `relative_path` transformed into the TestContext's `path_type`, /// with any supported path separators replaced by `path_sep`. /// The result is allocated by the TestContext's arena and will be free'd during /// `TestContext.deinit`. pub fn transformPath(self: *TestContext, relative_path: [:0]const u8) ![:0]const u8 { const allocator = self.arena.allocator(); const transformed_path = try self.transform_fn(allocator, self.dir, relative_path); if (native_os == .windows) { const transformed_sep_path = try allocator.dupeZ(u8, transformed_path); std.mem.replaceScalar(u8, transformed_sep_path, switch (self.path_sep) { '/' => '\\', '\\' => '/', else => unreachable, }, self.path_sep); return transformed_sep_path; } return transformed_path; } /// Replaces any path separators with the canonical path separator for the platform /// (e.g. all path separators are converted to `\` on Windows). /// If path separators are replaced, then the result is allocated by the /// TestContext's arena and will be free'd during `TestContext.deinit`. pub fn toCanonicalPathSep(self: *TestContext, path: [:0]const u8) ![:0]const u8 { if (native_os == .windows) { const allocator = self.arena.allocator(); const transformed_sep_path = try allocator.dupeZ(u8, path); std.mem.replaceScalar(u8, transformed_sep_path, '/', '\\'); return transformed_sep_path; } return path; } }; /// `test_func` must be a function that takes a `*TestContext` as a parameter and returns `!void`. /// `test_func` will be called once for each PathType that the current target supports, /// and will be passed a TestContext that can transform a relative path into the path type under test. /// The TestContext will also create a tmp directory for you (and will clean it up for you too). fn testWithAllSupportedPathTypes(test_func: anytype) !void { try testWithPathTypeIfSupported(.relative, '/', test_func); try testWithPathTypeIfSupported(.absolute, '/', test_func); try testWithPathTypeIfSupported(.unc, '/', test_func); try testWithPathTypeIfSupported(.relative, '\\', test_func); try testWithPathTypeIfSupported(.absolute, '\\', test_func); try testWithPathTypeIfSupported(.unc, '\\', test_func); } fn testWithPathTypeIfSupported(comptime path_type: PathType, comptime path_sep: u8, test_func: anytype) !void { if (!(comptime path_type.isSupported(builtin.os))) return; if (!(comptime fs.path.isSep(path_sep))) return; var ctx = TestContext.init(path_type, path_sep, testing.allocator, path_type.getTransformFn()); defer ctx.deinit(); try test_func(&ctx); } // For use in test setup. If the symlink creation fails on Windows with // AccessDenied, then make the test failure silent (it is not a Zig failure). fn setupSymlink(dir: Dir, target: []const u8, link: []const u8, flags: SymLinkFlags) !void { return dir.symLink(target, link, flags) catch |err| switch (err) { // Symlink requires admin privileges on windows, so this test can legitimately fail. error.AccessDenied => if (native_os == .windows) return error.SkipZigTest else return err, else => return err, }; } // For use in test setup. If the symlink creation fails on Windows with // AccessDenied, then make the test failure silent (it is not a Zig failure). fn setupSymlinkAbsolute(target: []const u8, link: []const u8, flags: SymLinkFlags) !void { return fs.symLinkAbsolute(target, link, flags) catch |err| switch (err) { error.AccessDenied => if (native_os == .windows) return error.SkipZigTest else return err, else => return err, }; } test "Dir.readLink" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { // Create some targets const file_target_path = try ctx.transformPath("file.txt"); try ctx.dir.writeFile(.{ .sub_path = file_target_path, .data = "nonsense" }); const dir_target_path = try ctx.transformPath("subdir"); try ctx.dir.makeDir(dir_target_path); // On Windows, symlink targets always use the canonical path separator const canonical_file_target_path = try ctx.toCanonicalPathSep(file_target_path); const canonical_dir_target_path = try ctx.toCanonicalPathSep(dir_target_path); // test 1: symlink to a file try setupSymlink(ctx.dir, file_target_path, "symlink1", .{}); try testReadLink(ctx.dir, canonical_file_target_path, "symlink1"); if (builtin.os.tag == .windows) { try testReadLinkW(testing.allocator, ctx.dir, canonical_file_target_path, "symlink1"); } // test 2: symlink to a directory (can be different on Windows) try setupSymlink(ctx.dir, dir_target_path, "symlink2", .{ .is_directory = true }); try testReadLink(ctx.dir, canonical_dir_target_path, "symlink2"); if (builtin.os.tag == .windows) { try testReadLinkW(testing.allocator, ctx.dir, canonical_dir_target_path, "symlink2"); } // test 3: relative path symlink const parent_file = ".." ++ fs.path.sep_str ++ "target.txt"; const canonical_parent_file = try ctx.toCanonicalPathSep(parent_file); var subdir = try ctx.dir.makeOpenPath("subdir", .{}); defer subdir.close(); try setupSymlink(subdir, canonical_parent_file, "relative-link.txt", .{}); try testReadLink(subdir, canonical_parent_file, "relative-link.txt"); if (builtin.os.tag == .windows) { try testReadLinkW(testing.allocator, subdir, canonical_parent_file, "relative-link.txt"); } } }.impl); } fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !void { var buffer: [fs.max_path_bytes]u8 = undefined; const actual = try dir.readLink(symlink_path, buffer[0..]); try testing.expectEqualStrings(target_path, actual); } fn testReadLinkW(allocator: mem.Allocator, dir: Dir, target_path: []const u8, symlink_path: []const u8) !void { const target_path_w = try std.unicode.wtf8ToWtf16LeAlloc(allocator, target_path); defer allocator.free(target_path_w); // Calling the W functions directly requires the path to be NT-prefixed const symlink_path_w = try std.os.windows.sliceToPrefixedFileW(dir.fd, symlink_path); const wtf16_buffer = try allocator.alloc(u16, target_path_w.len); defer allocator.free(wtf16_buffer); const actual = try dir.readLinkW(symlink_path_w.span(), wtf16_buffer); try testing.expectEqualSlices(u16, target_path_w, actual); } fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void { var buffer: [fs.max_path_bytes]u8 = undefined; const given = try fs.readLinkAbsolute(symlink_path, buffer[0..]); try testing.expectEqualStrings(target_path, given); } test "File.stat on a File that is a symlink returns Kind.sym_link" { // This test requires getting a file descriptor of a symlink which // is not possible on all targets switch (builtin.target.os.tag) { .windows, .linux => {}, else => return error.SkipZigTest, } try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const dir_target_path = try ctx.transformPath("subdir"); try ctx.dir.makeDir(dir_target_path); try setupSymlink(ctx.dir, dir_target_path, "symlink", .{ .is_directory = true }); var symlink = switch (builtin.target.os.tag) { .windows => windows_symlink: { const sub_path_w = try windows.cStrToPrefixedFileW(ctx.dir.fd, "symlink"); var result = Dir{ .fd = undefined, }; const path_len_bytes = @as(u16, @intCast(sub_path_w.span().len * 2)); var nt_name = windows.UNICODE_STRING{ .Length = path_len_bytes, .MaximumLength = path_len_bytes, .Buffer = @constCast(&sub_path_w.data), }; var attr = windows.OBJECT_ATTRIBUTES{ .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else ctx.dir.fd, .Attributes = 0, .ObjectName = &nt_name, .SecurityDescriptor = null, .SecurityQualityOfService = null, }; var io: windows.IO_STATUS_BLOCK = undefined; const rc = windows.ntdll.NtCreateFile( &result.fd, windows.STANDARD_RIGHTS_READ | windows.FILE_READ_ATTRIBUTES | windows.FILE_READ_EA | windows.SYNCHRONIZE | windows.FILE_TRAVERSE, &attr, &io, null, windows.FILE_ATTRIBUTE_NORMAL, windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE, windows.FILE_OPEN, // FILE_OPEN_REPARSE_POINT is the important thing here windows.FILE_OPEN_REPARSE_POINT | windows.FILE_DIRECTORY_FILE | windows.FILE_SYNCHRONOUS_IO_NONALERT | windows.FILE_OPEN_FOR_BACKUP_INTENT, null, 0, ); switch (rc) { .SUCCESS => break :windows_symlink result, else => return windows.unexpectedStatus(rc), } }, .linux => linux_symlink: { const sub_path_c = try posix.toPosixPath("symlink"); // the O_NOFOLLOW | O_PATH combination can obtain a fd to a symlink // note that if O_DIRECTORY is set, then this will error with ENOTDIR const flags: posix.O = .{ .NOFOLLOW = true, .PATH = true, .ACCMODE = .RDONLY, .CLOEXEC = true, }; const fd = try posix.openatZ(ctx.dir.fd, &sub_path_c, flags, 0); break :linux_symlink Dir{ .fd = fd }; }, else => unreachable, }; defer symlink.close(); const stat = try symlink.stat(); try testing.expectEqual(File.Kind.sym_link, stat.kind); } }.impl); } test "openDir" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const allocator = ctx.arena.allocator(); const subdir_path = try ctx.transformPath("subdir"); try ctx.dir.makeDir(subdir_path); for ([_][]const u8{ "", ".", ".." }) |sub_path| { const dir_path = try fs.path.join(allocator, &.{ subdir_path, sub_path }); var dir = try ctx.dir.openDir(dir_path, .{}); defer dir.close(); } } }.impl); } test "accessAbsolute" { if (native_os == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); const base_path = try tmp.dir.realpathAlloc(testing.allocator, "."); defer testing.allocator.free(base_path); try fs.accessAbsolute(base_path, .{}); } test "openDirAbsolute" { if (native_os == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); const tmp_ino = (try tmp.dir.stat()).inode; try tmp.dir.makeDir("subdir"); const sub_path = try tmp.dir.realpathAlloc(testing.allocator, "subdir"); defer testing.allocator.free(sub_path); // Can open sub_path var tmp_sub = try fs.openDirAbsolute(sub_path, .{}); defer tmp_sub.close(); const sub_ino = (try tmp_sub.stat()).inode; { // Can open sub_path + ".." const dir_path = try fs.path.join(testing.allocator, &.{ sub_path, ".." }); defer testing.allocator.free(dir_path); var dir = try fs.openDirAbsolute(dir_path, .{}); defer dir.close(); const ino = (try dir.stat()).inode; try testing.expectEqual(tmp_ino, ino); } { // Can open sub_path + "." const dir_path = try fs.path.join(testing.allocator, &.{ sub_path, "." }); defer testing.allocator.free(dir_path); var dir = try fs.openDirAbsolute(dir_path, .{}); defer dir.close(); const ino = (try dir.stat()).inode; try testing.expectEqual(sub_ino, ino); } { // Can open subdir + "..", with some extra "." const dir_path = try fs.path.join(testing.allocator, &.{ sub_path, ".", "..", "." }); defer testing.allocator.free(dir_path); var dir = try fs.openDirAbsolute(dir_path, .{}); defer dir.close(); const ino = (try dir.stat()).inode; try testing.expectEqual(tmp_ino, ino); } } test "openDir cwd parent '..'" { var dir = fs.cwd().openDir("..", .{}) catch |err| { if (native_os == .wasi and err == error.PermissionDenied) { return; // This is okay. WASI disallows escaping from the fs sandbox } return err; }; defer dir.close(); } test "openDir non-cwd parent '..'" { switch (native_os) { .wasi, .netbsd, .openbsd => return error.SkipZigTest, else => {}, } var tmp = tmpDir(.{}); defer tmp.cleanup(); var subdir = try tmp.dir.makeOpenPath("subdir", .{}); defer subdir.close(); var dir = try subdir.openDir("..", .{}); defer dir.close(); const expected_path = try tmp.dir.realpathAlloc(testing.allocator, "."); defer testing.allocator.free(expected_path); const actual_path = try dir.realpathAlloc(testing.allocator, "."); defer testing.allocator.free(actual_path); try testing.expectEqualStrings(expected_path, actual_path); } test "readLinkAbsolute" { if (native_os == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); // Create some targets try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = "nonsense" }); try tmp.dir.makeDir("subdir"); // Get base abs path var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const base_path = try tmp.dir.realpathAlloc(allocator, "."); { const target_path = try fs.path.join(allocator, &.{ base_path, "file.txt" }); const symlink_path = try fs.path.join(allocator, &.{ base_path, "symlink1" }); // Create symbolic link by path try setupSymlinkAbsolute(target_path, symlink_path, .{}); try testReadLinkAbsolute(target_path, symlink_path); } { const target_path = try fs.path.join(allocator, &.{ base_path, "subdir" }); const symlink_path = try fs.path.join(allocator, &.{ base_path, "symlink2" }); // Create symbolic link to a directory by path try setupSymlinkAbsolute(target_path, symlink_path, .{ .is_directory = true }); try testReadLinkAbsolute(target_path, symlink_path); } } test "Dir.Iterator" { var tmp_dir = tmpDir(.{ .iterate = true }); defer tmp_dir.cleanup(); // First, create a couple of entries to iterate over. const file = try tmp_dir.dir.createFile("some_file", .{}); file.close(); try tmp_dir.dir.makeDir("some_dir"); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); var entries = std.array_list.Managed(Dir.Entry).init(allocator); // Create iterator. var iter = tmp_dir.dir.iterate(); while (try iter.next()) |entry| { // We cannot just store `entry` as on Windows, we're re-using the name buffer // which means we'll actually share the `name` pointer between entries! const name = try allocator.dupe(u8, entry.name); try entries.append(Dir.Entry{ .name = name, .kind = entry.kind }); } try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..' try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file })); try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory })); } test "Dir.Iterator many entries" { var tmp_dir = tmpDir(.{ .iterate = true }); defer tmp_dir.cleanup(); const num = 1024; var i: usize = 0; var buf: [4]u8 = undefined; // Enough to store "1024". while (i < num) : (i += 1) { const name = try std.fmt.bufPrint(&buf, "{}", .{i}); const file = try tmp_dir.dir.createFile(name, .{}); file.close(); } var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); var entries = std.array_list.Managed(Dir.Entry).init(allocator); // Create iterator. var iter = tmp_dir.dir.iterate(); while (try iter.next()) |entry| { // We cannot just store `entry` as on Windows, we're re-using the name buffer // which means we'll actually share the `name` pointer between entries! const name = try allocator.dupe(u8, entry.name); try entries.append(.{ .name = name, .kind = entry.kind }); } i = 0; while (i < num) : (i += 1) { const name = try std.fmt.bufPrint(&buf, "{}", .{i}); try testing.expect(contains(&entries, .{ .name = name, .kind = .file })); } } test "Dir.Iterator twice" { var tmp_dir = tmpDir(.{ .iterate = true }); defer tmp_dir.cleanup(); // First, create a couple of entries to iterate over. const file = try tmp_dir.dir.createFile("some_file", .{}); file.close(); try tmp_dir.dir.makeDir("some_dir"); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); var i: u8 = 0; while (i < 2) : (i += 1) { var entries = std.array_list.Managed(Dir.Entry).init(allocator); // Create iterator. var iter = tmp_dir.dir.iterate(); while (try iter.next()) |entry| { // We cannot just store `entry` as on Windows, we're re-using the name buffer // which means we'll actually share the `name` pointer between entries! const name = try allocator.dupe(u8, entry.name); try entries.append(Dir.Entry{ .name = name, .kind = entry.kind }); } try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..' try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file })); try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory })); } } test "Dir.Iterator reset" { var tmp_dir = tmpDir(.{ .iterate = true }); defer tmp_dir.cleanup(); // First, create a couple of entries to iterate over. const file = try tmp_dir.dir.createFile("some_file", .{}); file.close(); try tmp_dir.dir.makeDir("some_dir"); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); // Create iterator. var iter = tmp_dir.dir.iterate(); var i: u8 = 0; while (i < 2) : (i += 1) { var entries = std.array_list.Managed(Dir.Entry).init(allocator); while (try iter.next()) |entry| { // We cannot just store `entry` as on Windows, we're re-using the name buffer // which means we'll actually share the `name` pointer between entries! const name = try allocator.dupe(u8, entry.name); try entries.append(.{ .name = name, .kind = entry.kind }); } try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..' try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file })); try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory })); iter.reset(); } } test "Dir.Iterator but dir is deleted during iteration" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); // Create directory and setup an iterator for it var subdir = try tmp.dir.makeOpenPath("subdir", .{ .iterate = true }); defer subdir.close(); var iterator = subdir.iterate(); // Create something to iterate over within the subdir try tmp.dir.makePath("subdir" ++ fs.path.sep_str ++ "b"); // Then, before iterating, delete the directory that we're iterating. // This is a contrived reproduction, but this could happen outside of the program, in another thread, etc. // If we get an error while trying to delete, we can skip this test (this will happen on platforms // like Windows which will give FileBusy if the directory is currently open for iteration). tmp.dir.deleteTree("subdir") catch return error.SkipZigTest; // Now, when we try to iterate, the next call should return null immediately. const entry = try iterator.next(); try std.testing.expect(entry == null); // On Linux, we can opt-in to receiving a more specific error by calling `nextLinux` if (native_os == .linux) { try std.testing.expectError(error.DirNotFound, iterator.nextLinux()); } } fn entryEql(lhs: Dir.Entry, rhs: Dir.Entry) bool { return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind; } fn contains(entries: *const std.array_list.Managed(Dir.Entry), el: Dir.Entry) bool { for (entries.items) |entry| { if (entryEql(entry, el)) return true; } return false; } test "Dir.realpath smoke test" { if (!comptime std.os.isGetFdPathSupportedOnTarget(builtin.os)) return error.SkipZigTest; try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const allocator = ctx.arena.allocator(); const test_file_path = try ctx.transformPath("test_file"); const test_dir_path = try ctx.transformPath("test_dir"); var buf: [fs.max_path_bytes]u8 = undefined; // FileNotFound if the path doesn't exist try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(allocator, test_file_path)); try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_file_path, &buf)); try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(allocator, test_dir_path)); try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_dir_path, &buf)); // Now create the file and dir try ctx.dir.writeFile(.{ .sub_path = test_file_path, .data = "" }); try ctx.dir.makeDir(test_dir_path); const base_path = try ctx.transformPath("."); const base_realpath = try ctx.dir.realpathAlloc(allocator, base_path); const expected_file_path = try fs.path.join( allocator, &.{ base_realpath, "test_file" }, ); const expected_dir_path = try fs.path.join( allocator, &.{ base_realpath, "test_dir" }, ); // First, test non-alloc version { const file_path = try ctx.dir.realpath(test_file_path, &buf); try testing.expectEqualStrings(expected_file_path, file_path); const dir_path = try ctx.dir.realpath(test_dir_path, &buf); try testing.expectEqualStrings(expected_dir_path, dir_path); } // Next, test alloc version { const file_path = try ctx.dir.realpathAlloc(allocator, test_file_path); try testing.expectEqualStrings(expected_file_path, file_path); const dir_path = try ctx.dir.realpathAlloc(allocator, test_dir_path); try testing.expectEqualStrings(expected_dir_path, dir_path); } } }.impl); } test "readFileAlloc" { var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); var file = try tmp_dir.dir.createFile("test_file", .{ .read = true }); defer file.close(); const buf1 = try tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(1024)); defer testing.allocator.free(buf1); try testing.expectEqualStrings("", buf1); const write_buf: []const u8 = "this is a test.\nthis is a test.\nthis is a test.\nthis is a test.\n"; try file.writeAll(write_buf); { // max_bytes > file_size const buf2 = try tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(1024)); defer testing.allocator.free(buf2); try testing.expectEqualStrings(write_buf, buf2); } { // max_bytes == file_size try testing.expectError( error.StreamTooLong, tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(write_buf.len)), ); } { // max_bytes == file_size + 1 const buf2 = try tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(write_buf.len + 1)); defer testing.allocator.free(buf2); try testing.expectEqualStrings(write_buf, buf2); } // max_bytes < file_size try testing.expectError( error.StreamTooLong, tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(write_buf.len - 1)), ); } test "Dir.statFile" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const test_file_name = try ctx.transformPath("test_file"); try testing.expectError(error.FileNotFound, ctx.dir.statFile(test_file_name)); try ctx.dir.writeFile(.{ .sub_path = test_file_name, .data = "" }); const stat = try ctx.dir.statFile(test_file_name); try testing.expectEqual(File.Kind.file, stat.kind); } }.impl); } test "statFile on dangling symlink" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const symlink_name = try ctx.transformPath("dangling-symlink"); const symlink_target = "." ++ fs.path.sep_str ++ "doesnotexist"; try setupSymlink(ctx.dir, symlink_target, symlink_name, .{}); try std.testing.expectError(error.FileNotFound, ctx.dir.statFile(symlink_name)); } }.impl); } test "directory operations on files" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const test_file_name = try ctx.transformPath("test_file"); var file = try ctx.dir.createFile(test_file_name, .{ .read = true }); file.close(); try testing.expectError(error.PathAlreadyExists, ctx.dir.makeDir(test_file_name)); try testing.expectError(error.NotDir, ctx.dir.openDir(test_file_name, .{})); try testing.expectError(error.NotDir, ctx.dir.deleteDir(test_file_name)); if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) { try testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(test_file_name)); try testing.expectError(error.NotDir, fs.deleteDirAbsolute(test_file_name)); } // ensure the file still exists and is a file as a sanity check file = try ctx.dir.openFile(test_file_name, .{}); const stat = try file.stat(); try testing.expectEqual(File.Kind.file, stat.kind); file.close(); } }.impl); } test "file operations on directories" { // TODO: fix this test on FreeBSD. https://github.com/ziglang/zig/issues/1759 if (native_os == .freebsd) return error.SkipZigTest; try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const test_dir_name = try ctx.transformPath("test_dir"); try ctx.dir.makeDir(test_dir_name); try testing.expectError(error.IsDir, ctx.dir.createFile(test_dir_name, .{})); try testing.expectError(error.IsDir, ctx.dir.deleteFile(test_dir_name)); switch (native_os) { .dragonfly, .netbsd => { // no error when reading a directory. See https://github.com/ziglang/zig/issues/5732 const buf = try ctx.dir.readFileAlloc(test_dir_name, testing.allocator, .unlimited); testing.allocator.free(buf); }, .wasi => { // WASI return EBADF, which gets mapped to NotOpenForReading. // See https://github.com/bytecodealliance/wasmtime/issues/1935 try testing.expectError(error.NotOpenForReading, ctx.dir.readFileAlloc(test_dir_name, testing.allocator, .unlimited)); }, else => { try testing.expectError(error.IsDir, ctx.dir.readFileAlloc(test_dir_name, testing.allocator, .unlimited)); }, } if (native_os == .wasi and builtin.link_libc) { // wasmtime unexpectedly succeeds here, see https://github.com/ziglang/zig/issues/20747 const handle = try ctx.dir.openFile(test_dir_name, .{ .mode = .read_write }); handle.close(); } else { // Note: The `.mode = .read_write` is necessary to ensure the error occurs on all platforms. // TODO: Add a read-only test as well, see https://github.com/ziglang/zig/issues/5732 try testing.expectError(error.IsDir, ctx.dir.openFile(test_dir_name, .{ .mode = .read_write })); } if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) { try testing.expectError(error.IsDir, fs.createFileAbsolute(test_dir_name, .{})); try testing.expectError(error.IsDir, fs.deleteFileAbsolute(test_dir_name)); } // ensure the directory still exists as a sanity check var dir = try ctx.dir.openDir(test_dir_name, .{}); dir.close(); } }.impl); } test "makeOpenPath parent dirs do not exist" { var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); var dir = try tmp_dir.dir.makeOpenPath("root_dir/parent_dir/some_dir", .{}); dir.close(); // double check that the full directory structure was created var dir_verification = try tmp_dir.dir.openDir("root_dir/parent_dir/some_dir", .{}); dir_verification.close(); } test "deleteDir" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const test_dir_path = try ctx.transformPath("test_dir"); const test_file_path = try ctx.transformPath("test_dir" ++ fs.path.sep_str ++ "test_file"); // deleting a non-existent directory try testing.expectError(error.FileNotFound, ctx.dir.deleteDir(test_dir_path)); // deleting a non-empty directory try ctx.dir.makeDir(test_dir_path); try ctx.dir.writeFile(.{ .sub_path = test_file_path, .data = "" }); try testing.expectError(error.DirNotEmpty, ctx.dir.deleteDir(test_dir_path)); // deleting an empty directory try ctx.dir.deleteFile(test_file_path); try ctx.dir.deleteDir(test_dir_path); } }.impl); } test "Dir.rename files" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { // Rename on Windows can hit intermittent AccessDenied errors // when certain conditions are true about the host system. // For now, skip this test when the path type is UNC to avoid them. // See https://github.com/ziglang/zig/issues/17134 if (ctx.path_type == .unc) return; const missing_file_path = try ctx.transformPath("missing_file_name"); const something_else_path = try ctx.transformPath("something_else"); try testing.expectError(error.FileNotFound, ctx.dir.rename(missing_file_path, something_else_path)); // Renaming files const test_file_name = try ctx.transformPath("test_file"); const renamed_test_file_name = try ctx.transformPath("test_file_renamed"); var file = try ctx.dir.createFile(test_file_name, .{ .read = true }); file.close(); try ctx.dir.rename(test_file_name, renamed_test_file_name); // Ensure the file was renamed try testing.expectError(error.FileNotFound, ctx.dir.openFile(test_file_name, .{})); file = try ctx.dir.openFile(renamed_test_file_name, .{}); file.close(); // Rename to self succeeds try ctx.dir.rename(renamed_test_file_name, renamed_test_file_name); // Rename to existing file succeeds const existing_file_path = try ctx.transformPath("existing_file"); var existing_file = try ctx.dir.createFile(existing_file_path, .{ .read = true }); existing_file.close(); try ctx.dir.rename(renamed_test_file_name, existing_file_path); try testing.expectError(error.FileNotFound, ctx.dir.openFile(renamed_test_file_name, .{})); file = try ctx.dir.openFile(existing_file_path, .{}); file.close(); } }.impl); } test "Dir.rename directories" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { // Rename on Windows can hit intermittent AccessDenied errors // when certain conditions are true about the host system. // For now, skip this test when the path type is UNC to avoid them. // See https://github.com/ziglang/zig/issues/17134 if (ctx.path_type == .unc) return; const test_dir_path = try ctx.transformPath("test_dir"); const test_dir_renamed_path = try ctx.transformPath("test_dir_renamed"); // Renaming directories try ctx.dir.makeDir(test_dir_path); try ctx.dir.rename(test_dir_path, test_dir_renamed_path); // Ensure the directory was renamed try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_path, .{})); var dir = try ctx.dir.openDir(test_dir_renamed_path, .{}); // Put a file in the directory var file = try dir.createFile("test_file", .{ .read = true }); file.close(); dir.close(); const test_dir_renamed_again_path = try ctx.transformPath("test_dir_renamed_again"); try ctx.dir.rename(test_dir_renamed_path, test_dir_renamed_again_path); // Ensure the directory was renamed and the file still exists in it try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_renamed_path, .{})); dir = try ctx.dir.openDir(test_dir_renamed_again_path, .{}); file = try dir.openFile("test_file", .{}); file.close(); dir.close(); } }.impl); } test "Dir.rename directory onto empty dir" { // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364 if (native_os == .windows) return error.SkipZigTest; try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const test_dir_path = try ctx.transformPath("test_dir"); const target_dir_path = try ctx.transformPath("target_dir_path"); try ctx.dir.makeDir(test_dir_path); try ctx.dir.makeDir(target_dir_path); try ctx.dir.rename(test_dir_path, target_dir_path); // Ensure the directory was renamed try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_path, .{})); var dir = try ctx.dir.openDir(target_dir_path, .{}); dir.close(); } }.impl); } test "Dir.rename directory onto non-empty dir" { // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364 if (native_os == .windows) return error.SkipZigTest; try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const test_dir_path = try ctx.transformPath("test_dir"); const target_dir_path = try ctx.transformPath("target_dir_path"); try ctx.dir.makeDir(test_dir_path); var target_dir = try ctx.dir.makeOpenPath(target_dir_path, .{}); var file = try target_dir.createFile("test_file", .{ .read = true }); file.close(); target_dir.close(); // Rename should fail with PathAlreadyExists if target_dir is non-empty try testing.expectError(error.PathAlreadyExists, ctx.dir.rename(test_dir_path, target_dir_path)); // Ensure the directory was not renamed var dir = try ctx.dir.openDir(test_dir_path, .{}); dir.close(); } }.impl); } test "Dir.rename file <-> dir" { // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364 if (native_os == .windows) return error.SkipZigTest; try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const test_file_path = try ctx.transformPath("test_file"); const test_dir_path = try ctx.transformPath("test_dir"); var file = try ctx.dir.createFile(test_file_path, .{ .read = true }); file.close(); try ctx.dir.makeDir(test_dir_path); try testing.expectError(error.IsDir, ctx.dir.rename(test_file_path, test_dir_path)); try testing.expectError(error.NotDir, ctx.dir.rename(test_dir_path, test_file_path)); } }.impl); } test "rename" { var tmp_dir1 = tmpDir(.{}); defer tmp_dir1.cleanup(); var tmp_dir2 = tmpDir(.{}); defer tmp_dir2.cleanup(); // Renaming files const test_file_name = "test_file"; const renamed_test_file_name = "test_file_renamed"; var file = try tmp_dir1.dir.createFile(test_file_name, .{ .read = true }); file.close(); try fs.rename(tmp_dir1.dir, test_file_name, tmp_dir2.dir, renamed_test_file_name); // ensure the file was renamed try testing.expectError(error.FileNotFound, tmp_dir1.dir.openFile(test_file_name, .{})); file = try tmp_dir2.dir.openFile(renamed_test_file_name, .{}); file.close(); } test "renameAbsolute" { if (native_os == .wasi) return error.SkipZigTest; var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); // Get base abs path var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const base_path = try tmp_dir.dir.realpathAlloc(allocator, "."); try testing.expectError(error.FileNotFound, fs.renameAbsolute( try fs.path.join(allocator, &.{ base_path, "missing_file_name" }), try fs.path.join(allocator, &.{ base_path, "something_else" }), )); // Renaming files const test_file_name = "test_file"; const renamed_test_file_name = "test_file_renamed"; var file = try tmp_dir.dir.createFile(test_file_name, .{ .read = true }); file.close(); try fs.renameAbsolute( try fs.path.join(allocator, &.{ base_path, test_file_name }), try fs.path.join(allocator, &.{ base_path, renamed_test_file_name }), ); // ensure the file was renamed try testing.expectError(error.FileNotFound, tmp_dir.dir.openFile(test_file_name, .{})); file = try tmp_dir.dir.openFile(renamed_test_file_name, .{}); const stat = try file.stat(); try testing.expectEqual(File.Kind.file, stat.kind); file.close(); // Renaming directories const test_dir_name = "test_dir"; const renamed_test_dir_name = "test_dir_renamed"; try tmp_dir.dir.makeDir(test_dir_name); try fs.renameAbsolute( try fs.path.join(allocator, &.{ base_path, test_dir_name }), try fs.path.join(allocator, &.{ base_path, renamed_test_dir_name }), ); // ensure the directory was renamed try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir(test_dir_name, .{})); var dir = try tmp_dir.dir.openDir(renamed_test_dir_name, .{}); dir.close(); } test "openSelfExe" { if (native_os == .wasi) return error.SkipZigTest; const self_exe_file = try std.fs.openSelfExe(.{}); self_exe_file.close(); } test "selfExePath" { if (native_os == .wasi) return error.SkipZigTest; var buf: [fs.max_path_bytes]u8 = undefined; const buf_self_exe_path = try std.fs.selfExePath(&buf); const alloc_self_exe_path = try std.fs.selfExePathAlloc(testing.allocator); defer testing.allocator.free(alloc_self_exe_path); try testing.expectEqualSlices(u8, buf_self_exe_path, alloc_self_exe_path); } test "deleteTree does not follow symlinks" { var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makePath("b"); { var a = try tmp.dir.makeOpenPath("a", .{}); defer a.close(); try setupSymlink(a, "../b", "b", .{ .is_directory = true }); } try tmp.dir.deleteTree("a"); try testing.expectError(error.FileNotFound, tmp.dir.access("a", .{})); try tmp.dir.access("b", .{}); } test "deleteTree on a symlink" { var tmp = tmpDir(.{}); defer tmp.cleanup(); // Symlink to a file try tmp.dir.writeFile(.{ .sub_path = "file", .data = "" }); try setupSymlink(tmp.dir, "file", "filelink", .{}); try tmp.dir.deleteTree("filelink"); try testing.expectError(error.FileNotFound, tmp.dir.access("filelink", .{})); try tmp.dir.access("file", .{}); // Symlink to a directory try tmp.dir.makePath("dir"); try setupSymlink(tmp.dir, "dir", "dirlink", .{ .is_directory = true }); try tmp.dir.deleteTree("dirlink"); try testing.expectError(error.FileNotFound, tmp.dir.access("dirlink", .{})); try tmp.dir.access("dir", .{}); } test "makePath, put some files in it, deleteTree" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const allocator = ctx.arena.allocator(); const dir_path = try ctx.transformPath("os_test_tmp"); try ctx.dir.makePath(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c" })); try ctx.dir.writeFile(.{ .sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c", "file.txt" }), .data = "nonsense", }); try ctx.dir.writeFile(.{ .sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "file2.txt" }), .data = "blah", }); try ctx.dir.deleteTree(dir_path); try testing.expectError(error.FileNotFound, ctx.dir.openDir(dir_path, .{})); } }.impl); } test "makePath, put some files in it, deleteTreeMinStackSize" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const allocator = ctx.arena.allocator(); const dir_path = try ctx.transformPath("os_test_tmp"); try ctx.dir.makePath(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c" })); try ctx.dir.writeFile(.{ .sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c", "file.txt" }), .data = "nonsense", }); try ctx.dir.writeFile(.{ .sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "file2.txt" }), .data = "blah", }); try ctx.dir.deleteTreeMinStackSize(dir_path); try testing.expectError(error.FileNotFound, ctx.dir.openDir(dir_path, .{})); } }.impl); } test "makePath in a directory that no longer exists" { if (native_os == .windows) return error.SkipZigTest; // Windows returns FileBusy if attempting to remove an open dir var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.parent_dir.deleteTree(&tmp.sub_path); try testing.expectError(error.FileNotFound, tmp.dir.makePath("sub-path")); } test "makePath but sub_path contains pre-existing file" { var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makeDir("foo"); try tmp.dir.writeFile(.{ .sub_path = "foo/bar", .data = "" }); try testing.expectError(error.NotDir, tmp.dir.makePath("foo/bar/baz")); } fn expectDir(dir: Dir, path: []const u8) !void { var d = try dir.openDir(path, .{}); d.close(); } test "makepath existing directories" { var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makeDir("A"); var tmpA = try tmp.dir.openDir("A", .{}); defer tmpA.close(); try tmpA.makeDir("B"); const testPath = "A" ++ fs.path.sep_str ++ "B" ++ fs.path.sep_str ++ "C"; try tmp.dir.makePath(testPath); try expectDir(tmp.dir, testPath); } test "makepath through existing valid symlink" { var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makeDir("realfolder"); try setupSymlink(tmp.dir, "." ++ fs.path.sep_str ++ "realfolder", "working-symlink", .{}); try tmp.dir.makePath("working-symlink" ++ fs.path.sep_str ++ "in-realfolder"); try expectDir(tmp.dir, "realfolder" ++ fs.path.sep_str ++ "in-realfolder"); } test "makepath relative walks" { var tmp = tmpDir(.{}); defer tmp.cleanup(); const relPath = try fs.path.join(testing.allocator, &.{ "first", "..", "second", "..", "third", "..", "first", "A", "..", "B", "..", "C", }); defer testing.allocator.free(relPath); try tmp.dir.makePath(relPath); // How .. is handled is different on Windows than non-Windows switch (native_os) { .windows => { // On Windows, .. is resolved before passing the path to NtCreateFile, // meaning everything except `first/C` drops out. try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "C"); try testing.expectError(error.FileNotFound, tmp.dir.access("second", .{})); try testing.expectError(error.FileNotFound, tmp.dir.access("third", .{})); }, else => { try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "A"); try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "B"); try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "C"); try expectDir(tmp.dir, "second"); try expectDir(tmp.dir, "third"); }, } } test "makepath ignores '.'" { var tmp = tmpDir(.{}); defer tmp.cleanup(); // Path to create, with "." elements: const dotPath = try fs.path.join(testing.allocator, &.{ "first", ".", "second", ".", "third", }); defer testing.allocator.free(dotPath); // Path to expect to find: const expectedPath = try fs.path.join(testing.allocator, &.{ "first", "second", "third", }); defer testing.allocator.free(expectedPath); try tmp.dir.makePath(dotPath); try expectDir(tmp.dir, expectedPath); } fn testFilenameLimits(iterable_dir: Dir, maxed_filename: []const u8) !void { // setup, create a dir and a nested file both with maxed filenames, and walk the dir { var maxed_dir = try iterable_dir.makeOpenPath(maxed_filename, .{}); defer maxed_dir.close(); try maxed_dir.writeFile(.{ .sub_path = maxed_filename, .data = "" }); var walker = try iterable_dir.walk(testing.allocator); defer walker.deinit(); var count: usize = 0; while (try walker.next()) |entry| { try testing.expectEqualStrings(maxed_filename, entry.basename); count += 1; } try testing.expectEqual(@as(usize, 2), count); } // ensure that we can delete the tree try iterable_dir.deleteTree(maxed_filename); } test "max file name component lengths" { var tmp = tmpDir(.{ .iterate = true }); defer tmp.cleanup(); if (native_os == .windows) { // U+FFFF is the character with the largest code point that is encoded as a single // UTF-16 code unit, so Windows allows for NAME_MAX of them. const maxed_windows_filename = ("\u{FFFF}".*) ** windows.NAME_MAX; try testFilenameLimits(tmp.dir, &maxed_windows_filename); } else if (native_os == .wasi) { // On WASI, the maxed filename depends on the host OS, so in order for this test to // work on any host, we need to use a length that will work for all platforms // (i.e. the minimum max_name_bytes of all supported platforms). const maxed_wasi_filename = [_]u8{'1'} ** 255; try testFilenameLimits(tmp.dir, &maxed_wasi_filename); } else { const maxed_ascii_filename = [_]u8{'1'} ** std.fs.max_name_bytes; try testFilenameLimits(tmp.dir, &maxed_ascii_filename); } } test "writev, readv" { const io = testing.io; var tmp = tmpDir(.{}); defer tmp.cleanup(); const line1 = "line1\n"; const line2 = "line2\n"; var buf1: [line1.len]u8 = undefined; var buf2: [line2.len]u8 = undefined; var write_vecs: [2][]const u8 = .{ line1, line2 }; var read_vecs: [2][]u8 = .{ &buf2, &buf1 }; var src_file = try tmp.dir.createFile("test.txt", .{ .read = true }); defer src_file.close(); var writer = src_file.writerStreaming(&.{}); try writer.interface.writeVecAll(&write_vecs); try writer.interface.flush(); try testing.expectEqual(@as(u64, line1.len + line2.len), try src_file.getEndPos()); var reader = writer.moveToReader(io); try reader.seekTo(0); try reader.interface.readVecAll(&read_vecs); try testing.expectEqualStrings(&buf1, "line2\n"); try testing.expectEqualStrings(&buf2, "line1\n"); try testing.expectError(error.EndOfStream, reader.interface.readSliceAll(&buf1)); } test "pwritev, preadv" { const io = testing.io; var tmp = tmpDir(.{}); defer tmp.cleanup(); const line1 = "line1\n"; const line2 = "line2\n"; var lines: [2][]const u8 = .{ line1, line2 }; var buf1: [line1.len]u8 = undefined; var buf2: [line2.len]u8 = undefined; var read_vecs: [2][]u8 = .{ &buf2, &buf1 }; var src_file = try tmp.dir.createFile("test.txt", .{ .read = true }); defer src_file.close(); var writer = src_file.writer(&.{}); try writer.seekTo(16); try writer.interface.writeVecAll(&lines); try writer.interface.flush(); try testing.expectEqual(@as(u64, 16 + line1.len + line2.len), try src_file.getEndPos()); var reader = writer.moveToReader(io); try reader.seekTo(16); try reader.interface.readVecAll(&read_vecs); try testing.expectEqualStrings(&buf1, "line2\n"); try testing.expectEqualStrings(&buf2, "line1\n"); try testing.expectError(error.EndOfStream, reader.interface.readSliceAll(&buf1)); } test "setEndPos" { // https://github.com/ziglang/zig/issues/20747 (open fd does not have write permission) if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; if (builtin.cpu.arch.isMIPS64() and (builtin.abi == .gnuabin32 or builtin.abi == .muslabin32)) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/23806 const io = testing.io; var tmp = tmpDir(.{}); defer tmp.cleanup(); const file_name = "afile.txt"; try tmp.dir.writeFile(.{ .sub_path = file_name, .data = "ninebytes" }); const f = try tmp.dir.openFile(file_name, .{ .mode = .read_write }); defer f.close(); const initial_size = try f.getEndPos(); var buffer: [32]u8 = undefined; var reader = f.reader(io, &.{}); { try f.setEndPos(initial_size); try testing.expectEqual(initial_size, try f.getEndPos()); try reader.seekTo(0); try testing.expectEqual(initial_size, try reader.interface.readSliceShort(&buffer)); try testing.expectEqualStrings("ninebytes", buffer[0..@intCast(initial_size)]); } { const larger = initial_size + 4; try f.setEndPos(larger); try testing.expectEqual(larger, try f.getEndPos()); try reader.seekTo(0); try testing.expectEqual(larger, try reader.interface.readSliceShort(&buffer)); try testing.expectEqualStrings("ninebytes\x00\x00\x00\x00", buffer[0..@intCast(larger)]); } { const smaller = initial_size - 5; try f.setEndPos(smaller); try testing.expectEqual(smaller, try f.getEndPos()); try reader.seekTo(0); try testing.expectEqual(smaller, try reader.interface.readSliceShort(&buffer)); try testing.expectEqualStrings("nine", buffer[0..@intCast(smaller)]); } try f.setEndPos(0); try testing.expectEqual(0, try f.getEndPos()); try reader.seekTo(0); try testing.expectEqual(0, try reader.interface.readSliceShort(&buffer)); } test "access file" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const dir_path = try ctx.transformPath("os_test_tmp"); const file_path = try ctx.transformPath("os_test_tmp" ++ fs.path.sep_str ++ "file.txt"); try ctx.dir.makePath(dir_path); try testing.expectError(error.FileNotFound, ctx.dir.access(file_path, .{})); try ctx.dir.writeFile(.{ .sub_path = file_path, .data = "" }); try ctx.dir.access(file_path, .{}); try ctx.dir.deleteTree(dir_path); } }.impl); } test "sendfile" { const io = testing.io; var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makePath("os_test_tmp"); var dir = try tmp.dir.openDir("os_test_tmp", .{}); defer dir.close(); const line1 = "line1\n"; const line2 = "second line\n"; var vecs = [_][]const u8{ line1, line2 }; var src_file = try dir.createFile("sendfile1.txt", .{ .read = true }); defer src_file.close(); { var fw = src_file.writer(&.{}); try fw.interface.writeVecAll(&vecs); } var dest_file = try dir.createFile("sendfile2.txt", .{ .read = true }); defer dest_file.close(); const header1 = "header1\n"; const header2 = "second header\n"; const trailer1 = "trailer1\n"; const trailer2 = "second trailer\n"; var headers: [2][]const u8 = .{ header1, header2 }; var trailers: [2][]const u8 = .{ trailer1, trailer2 }; var written_buf: [100]u8 = undefined; var file_reader = src_file.reader(io, &.{}); var fallback_buffer: [50]u8 = undefined; var file_writer = dest_file.writer(&fallback_buffer); try file_writer.interface.writeVecAll(&headers); try file_reader.seekTo(1); try testing.expectEqual(10, try file_writer.interface.sendFileAll(&file_reader, .limited(10))); try file_writer.interface.writeVecAll(&trailers); try file_writer.interface.flush(); var fr = file_writer.moveToReader(io); try fr.seekTo(0); const amt = try fr.interface.readSliceShort(&written_buf); try testing.expectEqualStrings("header1\nsecond header\nine1\nsecontrailer1\nsecond trailer\n", written_buf[0..amt]); } test "sendfile with buffered data" { const io = testing.io; var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makePath("os_test_tmp"); var dir = try tmp.dir.openDir("os_test_tmp", .{}); defer dir.close(); var src_file = try dir.createFile("sendfile1.txt", .{ .read = true }); defer src_file.close(); try src_file.writeAll("AAAABBBB"); var dest_file = try dir.createFile("sendfile2.txt", .{ .read = true }); defer dest_file.close(); var src_buffer: [32]u8 = undefined; var file_reader = src_file.reader(io, &src_buffer); try file_reader.seekTo(0); try file_reader.interface.fill(8); var fallback_buffer: [32]u8 = undefined; var file_writer = dest_file.writer(&fallback_buffer); try std.testing.expectEqual(4, try file_writer.interface.sendFileAll(&file_reader, .limited(4))); var written_buf: [8]u8 = undefined; var fr = file_writer.moveToReader(io); try fr.seekTo(0); const amt = try fr.interface.readSliceShort(&written_buf); try std.testing.expectEqual(4, amt); try std.testing.expectEqualSlices(u8, "AAAA", written_buf[0..amt]); } test "copyFile" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP"; const src_file = try ctx.transformPath("tmp_test_copy_file.txt"); const dest_file = try ctx.transformPath("tmp_test_copy_file2.txt"); const dest_file2 = try ctx.transformPath("tmp_test_copy_file3.txt"); try ctx.dir.writeFile(.{ .sub_path = src_file, .data = data }); defer ctx.dir.deleteFile(src_file) catch {}; try ctx.dir.copyFile(src_file, ctx.dir, dest_file, .{}); defer ctx.dir.deleteFile(dest_file) catch {}; try ctx.dir.copyFile(src_file, ctx.dir, dest_file2, .{ .override_mode = File.default_mode }); defer ctx.dir.deleteFile(dest_file2) catch {}; try expectFileContents(ctx.dir, dest_file, data); try expectFileContents(ctx.dir, dest_file2, data); } }.impl); } fn expectFileContents(dir: Dir, file_path: []const u8, data: []const u8) !void { const contents = try dir.readFileAlloc(file_path, testing.allocator, .limited(1000)); defer testing.allocator.free(contents); try testing.expectEqualSlices(u8, data, contents); } test "AtomicFile" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const allocator = ctx.arena.allocator(); const test_out_file = try ctx.transformPath("tmp_atomic_file_test_dest.txt"); const test_content = \\ hello! \\ this is a test file ; { var buffer: [100]u8 = undefined; var af = try ctx.dir.atomicFile(test_out_file, .{ .write_buffer = &buffer }); defer af.deinit(); try af.file_writer.interface.writeAll(test_content); try af.finish(); } const content = try ctx.dir.readFileAlloc(test_out_file, allocator, .limited(9999)); try testing.expectEqualStrings(test_content, content); try ctx.dir.deleteFile(test_out_file); } }.impl); } test "open file with exclusive nonblocking lock twice" { if (native_os == .wasi) return error.SkipZigTest; try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const filename = try ctx.transformPath("file_nonblocking_lock_test.txt"); const file1 = try ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true }); defer file1.close(); const file2 = ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true }); try testing.expectError(error.WouldBlock, file2); } }.impl); } test "open file with shared and exclusive nonblocking lock" { if (native_os == .wasi) return error.SkipZigTest; try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const filename = try ctx.transformPath("file_nonblocking_lock_test.txt"); const file1 = try ctx.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true }); defer file1.close(); const file2 = ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true }); try testing.expectError(error.WouldBlock, file2); } }.impl); } test "open file with exclusive and shared nonblocking lock" { if (native_os == .wasi) return error.SkipZigTest; try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const filename = try ctx.transformPath("file_nonblocking_lock_test.txt"); const file1 = try ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true }); defer file1.close(); const file2 = ctx.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true }); try testing.expectError(error.WouldBlock, file2); } }.impl); } test "open file with exclusive lock twice, make sure second lock waits" { if (builtin.single_threaded) return error.SkipZigTest; try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const filename = try ctx.transformPath("file_lock_test.txt"); const file = try ctx.dir.createFile(filename, .{ .lock = .exclusive }); errdefer file.close(); const S = struct { fn checkFn(dir: *fs.Dir, path: []const u8, started: *std.Thread.ResetEvent, locked: *std.Thread.ResetEvent) !void { started.set(); const file1 = try dir.createFile(path, .{ .lock = .exclusive }); locked.set(); file1.close(); } }; var started: std.Thread.ResetEvent = .unset; var locked: std.Thread.ResetEvent = .unset; const t = try std.Thread.spawn(.{}, S.checkFn, .{ &ctx.dir, filename, &started, &locked, }); defer t.join(); // Wait for the spawned thread to start trying to acquire the exclusive file lock. // Then wait a bit to make sure that can't acquire it since we currently hold the file lock. started.wait(); try testing.expectError(error.Timeout, locked.timedWait(10 * std.time.ns_per_ms)); // Release the file lock which should unlock the thread to lock it and set the locked event. file.close(); locked.wait(); } }.impl); } test "open file with exclusive nonblocking lock twice (absolute paths)" { if (native_os == .wasi) return error.SkipZigTest; var random_bytes: [12]u8 = undefined; std.crypto.random.bytes(&random_bytes); var random_b64: [fs.base64_encoder.calcSize(random_bytes.len)]u8 = undefined; _ = fs.base64_encoder.encode(&random_b64, &random_bytes); const sub_path = random_b64 ++ "-zig-test-absolute-paths.txt"; const gpa = testing.allocator; const cwd = try std.process.getCwdAlloc(gpa); defer gpa.free(cwd); const filename = try fs.path.resolve(gpa, &.{ cwd, sub_path }); defer gpa.free(filename); defer fs.deleteFileAbsolute(filename) catch {}; // createFileAbsolute can leave files on failures const file1 = try fs.createFileAbsolute(filename, .{ .lock = .exclusive, .lock_nonblocking = true, }); const file2 = fs.createFileAbsolute(filename, .{ .lock = .exclusive, .lock_nonblocking = true, }); file1.close(); try testing.expectError(error.WouldBlock, file2); } test "read from locked file" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const filename = try ctx.transformPath("read_lock_file_test.txt"); { const f = try ctx.dir.createFile(filename, .{ .read = true }); defer f.close(); var buffer: [1]u8 = undefined; _ = try f.read(&buffer); } { const f = try ctx.dir.createFile(filename, .{ .read = true, .lock = .exclusive, }); defer f.close(); const f2 = try ctx.dir.openFile(filename, .{}); defer f2.close(); var buffer: [1]u8 = undefined; if (builtin.os.tag == .windows) { try std.testing.expectError(error.LockViolation, f2.read(&buffer)); } else { try std.testing.expectEqual(0, f2.read(&buffer)); } } } }.impl); } test "walker" { var tmp = tmpDir(.{ .iterate = true }); defer tmp.cleanup(); // iteration order of walker is undefined, so need lookup maps to check against const expected_paths = std.StaticStringMap(usize).initComptime(.{ .{ "dir1", 1 }, .{ "dir2", 1 }, .{ "dir3", 1 }, .{ "dir4", 1 }, .{ "dir3" ++ fs.path.sep_str ++ "sub1", 2 }, .{ "dir3" ++ fs.path.sep_str ++ "sub2", 2 }, .{ "dir3" ++ fs.path.sep_str ++ "sub2" ++ fs.path.sep_str ++ "subsub1", 3 }, }); const expected_basenames = std.StaticStringMap(void).initComptime(.{ .{"dir1"}, .{"dir2"}, .{"dir3"}, .{"dir4"}, .{"sub1"}, .{"sub2"}, .{"subsub1"}, }); for (expected_paths.keys()) |key| { try tmp.dir.makePath(key); } var walker = try tmp.dir.walk(testing.allocator); defer walker.deinit(); var num_walked: usize = 0; while (try walker.next()) |entry| { testing.expect(expected_basenames.has(entry.basename)) catch |err| { std.debug.print("found unexpected basename: {f}\n", .{std.ascii.hexEscape(entry.basename, .lower)}); return err; }; testing.expect(expected_paths.has(entry.path)) catch |err| { std.debug.print("found unexpected path: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)}); return err; }; testing.expectEqual(expected_paths.get(entry.path).?, entry.depth()) catch |err| { std.debug.print("path reported unexpected depth: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)}); return err; }; // make sure that the entry.dir is the containing dir var entry_dir = try entry.dir.openDir(entry.basename, .{}); defer entry_dir.close(); num_walked += 1; } try testing.expectEqual(expected_paths.kvs.len, num_walked); } test "selective walker, skip entries that start with ." { var tmp = tmpDir(.{ .iterate = true }); defer tmp.cleanup(); const paths_to_create: []const []const u8 = &.{ "dir1/foo/.git/ignored", ".hidden/bar", "a/b/c", "a/baz", }; // iteration order of walker is undefined, so need lookup maps to check against const expected_paths = std.StaticStringMap(usize).initComptime(.{ .{ "dir1", 1 }, .{ "dir1" ++ fs.path.sep_str ++ "foo", 2 }, .{ "a", 1 }, .{ "a" ++ fs.path.sep_str ++ "b", 2 }, .{ "a" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c", 3 }, .{ "a" ++ fs.path.sep_str ++ "baz", 2 }, }); const expected_basenames = std.StaticStringMap(void).initComptime(.{ .{"dir1"}, .{"foo"}, .{"a"}, .{"b"}, .{"c"}, .{"baz"}, }); for (paths_to_create) |path| { try tmp.dir.makePath(path); } var walker = try tmp.dir.walkSelectively(testing.allocator); defer walker.deinit(); var num_walked: usize = 0; while (try walker.next()) |entry| { if (entry.basename[0] == '.') continue; if (entry.kind == .directory) { try walker.enter(entry); } testing.expect(expected_basenames.has(entry.basename)) catch |err| { std.debug.print("found unexpected basename: {f}\n", .{std.ascii.hexEscape(entry.basename, .lower)}); return err; }; testing.expect(expected_paths.has(entry.path)) catch |err| { std.debug.print("found unexpected path: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)}); return err; }; testing.expectEqual(expected_paths.get(entry.path).?, entry.depth()) catch |err| { std.debug.print("path reported unexpected depth: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)}); return err; }; // make sure that the entry.dir is the containing dir var entry_dir = try entry.dir.openDir(entry.basename, .{}); defer entry_dir.close(); num_walked += 1; } try testing.expectEqual(expected_paths.kvs.len, num_walked); } test "walker without fully iterating" { var tmp = tmpDir(.{ .iterate = true }); defer tmp.cleanup(); var walker = try tmp.dir.walk(testing.allocator); defer walker.deinit(); // Create 2 directories inside the tmp directory, but then only iterate once before breaking. // This ensures that walker doesn't try to close the initial directory when not fully iterating. try tmp.dir.makePath("a"); try tmp.dir.makePath("b"); var num_walked: usize = 0; while (try walker.next()) |_| { num_walked += 1; break; } try testing.expectEqual(@as(usize, 1), num_walked); } test "'.' and '..' in fs.Dir functions" { if (native_os == .windows and builtin.cpu.arch == .aarch64) { // https://github.com/ziglang/zig/issues/17134 return error.SkipZigTest; } try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const io = ctx.io; const subdir_path = try ctx.transformPath("./subdir"); const file_path = try ctx.transformPath("./subdir/../file"); const copy_path = try ctx.transformPath("./subdir/../copy"); const rename_path = try ctx.transformPath("./subdir/../rename"); const update_path = try ctx.transformPath("./subdir/../update"); try ctx.dir.makeDir(subdir_path); try ctx.dir.access(subdir_path, .{}); var created_subdir = try ctx.dir.openDir(subdir_path, .{}); created_subdir.close(); const created_file = try ctx.dir.createFile(file_path, .{}); created_file.close(); try ctx.dir.access(file_path, .{}); try ctx.dir.copyFile(file_path, ctx.dir, copy_path, .{}); try ctx.dir.rename(copy_path, rename_path); const renamed_file = try ctx.dir.openFile(rename_path, .{}); renamed_file.close(); try ctx.dir.deleteFile(rename_path); try ctx.dir.writeFile(.{ .sub_path = update_path, .data = "something" }); var dir = ctx.dir.adaptToNewApi(); const prev_status = try dir.updateFile(io, file_path, dir, update_path, .{}); try testing.expectEqual(Io.Dir.PrevStatus.stale, prev_status); try ctx.dir.deleteDir(subdir_path); } }.impl); } test "'.' and '..' in absolute functions" { if (native_os == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const base_path = try tmp.dir.realpathAlloc(allocator, "."); const subdir_path = try fs.path.join(allocator, &.{ base_path, "./subdir" }); try fs.makeDirAbsolute(subdir_path); try fs.accessAbsolute(subdir_path, .{}); var created_subdir = try fs.openDirAbsolute(subdir_path, .{}); created_subdir.close(); const created_file_path = try fs.path.join(allocator, &.{ subdir_path, "../file" }); const created_file = try fs.createFileAbsolute(created_file_path, .{}); created_file.close(); try fs.accessAbsolute(created_file_path, .{}); const copied_file_path = try fs.path.join(allocator, &.{ subdir_path, "../copy" }); try fs.copyFileAbsolute(created_file_path, copied_file_path, .{}); const renamed_file_path = try fs.path.join(allocator, &.{ subdir_path, "../rename" }); try fs.renameAbsolute(copied_file_path, renamed_file_path); const renamed_file = try fs.openFileAbsolute(renamed_file_path, .{}); renamed_file.close(); try fs.deleteFileAbsolute(renamed_file_path); try fs.deleteDirAbsolute(subdir_path); } test "chmod" { if (native_os == .windows or native_os == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); const file = try tmp.dir.createFile("test_file", .{ .mode = 0o600 }); defer file.close(); try testing.expectEqual(@as(File.Mode, 0o600), (try file.stat()).mode & 0o7777); try file.chmod(0o644); try testing.expectEqual(@as(File.Mode, 0o644), (try file.stat()).mode & 0o7777); try tmp.dir.makeDir("test_dir"); var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true }); defer dir.close(); try dir.chmod(0o700); try testing.expectEqual(@as(File.Mode, 0o700), (try dir.stat()).mode & 0o7777); } test "chown" { if (native_os == .windows or native_os == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); const file = try tmp.dir.createFile("test_file", .{}); defer file.close(); try file.chown(null, null); try tmp.dir.makeDir("test_dir"); var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true }); defer dir.close(); 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, .windows => error.BadPathName, else => return error.SkipZigTest, }; try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { const io = ctx.io; // This is both invalid UTF-8 and WTF-8, since \xFF is an invalid start byte const invalid_path = try ctx.transformPath("\xFF"); try testing.expectError(expected_err, ctx.dir.openFile(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.createFile(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.makeDir(invalid_path)); try testing.expectError(expected_err, ctx.dir.makePath(invalid_path)); try testing.expectError(expected_err, ctx.dir.makeOpenPath(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.openDir(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.deleteFile(invalid_path)); try testing.expectError(expected_err, ctx.dir.deleteDir(invalid_path)); try testing.expectError(expected_err, ctx.dir.rename(invalid_path, invalid_path)); try testing.expectError(expected_err, ctx.dir.symLink(invalid_path, invalid_path, .{})); if (native_os == .wasi) { try testing.expectError(expected_err, ctx.dir.symLinkWasi(invalid_path, invalid_path, .{})); } try testing.expectError(expected_err, ctx.dir.readLink(invalid_path, &[_]u8{})); if (native_os == .wasi) { try testing.expectError(expected_err, ctx.dir.readLinkWasi(invalid_path, &[_]u8{})); } try testing.expectError(expected_err, ctx.dir.readFile(invalid_path, &[_]u8{})); try testing.expectError(expected_err, ctx.dir.readFileAlloc(invalid_path, testing.allocator, .limited(0))); try testing.expectError(expected_err, ctx.dir.deleteTree(invalid_path)); try testing.expectError(expected_err, ctx.dir.deleteTreeMinStackSize(invalid_path)); try testing.expectError(expected_err, ctx.dir.writeFile(.{ .sub_path = invalid_path, .data = "" })); try testing.expectError(expected_err, ctx.dir.access(invalid_path, .{})); var dir = ctx.dir.adaptToNewApi(); try testing.expectError(expected_err, dir.updateFile(io, invalid_path, dir, invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.copyFile(invalid_path, ctx.dir, invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.statFile(invalid_path)); if (native_os != .wasi) { try testing.expectError(expected_err, ctx.dir.realpath(invalid_path, &[_]u8{})); try testing.expectError(expected_err, ctx.dir.realpathAlloc(testing.allocator, invalid_path)); } try testing.expectError(expected_err, fs.rename(ctx.dir, invalid_path, ctx.dir, invalid_path)); if (native_os != .wasi and ctx.path_type != .relative) { try testing.expectError(expected_err, fs.copyFileAbsolute(invalid_path, invalid_path, .{})); try testing.expectError(expected_err, fs.makeDirAbsolute(invalid_path)); try testing.expectError(expected_err, fs.deleteDirAbsolute(invalid_path)); try testing.expectError(expected_err, fs.renameAbsolute(invalid_path, invalid_path)); try testing.expectError(expected_err, fs.openDirAbsolute(invalid_path, .{})); try testing.expectError(expected_err, fs.openFileAbsolute(invalid_path, .{})); try testing.expectError(expected_err, fs.accessAbsolute(invalid_path, .{})); try testing.expectError(expected_err, fs.createFileAbsolute(invalid_path, .{})); try testing.expectError(expected_err, fs.deleteFileAbsolute(invalid_path)); try testing.expectError(expected_err, fs.deleteTreeAbsolute(invalid_path)); var readlink_buf: [fs.max_path_bytes]u8 = undefined; try testing.expectError(expected_err, fs.readLinkAbsolute(invalid_path, &readlink_buf)); try testing.expectError(expected_err, fs.symLinkAbsolute(invalid_path, invalid_path, .{})); try testing.expectError(expected_err, fs.realpathAlloc(testing.allocator, invalid_path)); } } }.impl); } test "read file non vectored" { const io = std.testing.io; var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); const contents = "hello, world!\n"; const file = try tmp_dir.dir.createFile("input.txt", .{ .read = true }); defer file.close(); { var file_writer: std.fs.File.Writer = .init(file, &.{}); try file_writer.interface.writeAll(contents); try file_writer.interface.flush(); } var file_reader: std.Io.File.Reader = .initAdapted(file, io, &.{}); var write_buffer: [100]u8 = undefined; var w: std.Io.Writer = .fixed(&write_buffer); var i: usize = 0; while (true) { i += file_reader.interface.stream(&w, .limited(3)) catch |err| switch (err) { error.EndOfStream => break, else => |e| return e, }; } try testing.expectEqualStrings(contents, w.buffered()); try testing.expectEqual(contents.len, i); } test "seek keeping partial buffer" { const io = std.testing.io; var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); const contents = "0123456789"; const file = try tmp_dir.dir.createFile("input.txt", .{ .read = true }); defer file.close(); { var file_writer: std.fs.File.Writer = .init(file, &.{}); try file_writer.interface.writeAll(contents); try file_writer.interface.flush(); } var read_buffer: [3]u8 = undefined; var file_reader: Io.File.Reader = .initAdapted(file, io, &read_buffer); try testing.expectEqual(0, file_reader.logicalPos()); var buf: [4]u8 = undefined; try file_reader.interface.readSliceAll(&buf); if (file_reader.interface.bufferedLen() != 3) { // Pass the test if the OS doesn't give us vectored reads. return; } try testing.expectEqual(4, file_reader.logicalPos()); try testing.expectEqual(7, file_reader.pos); try file_reader.seekTo(6); try testing.expectEqual(6, file_reader.logicalPos()); try testing.expectEqual(7, file_reader.pos); try testing.expectEqualStrings("0123", &buf); const n = try file_reader.interface.readSliceShort(&buf); try testing.expectEqual(4, n); try testing.expectEqualStrings("6789", &buf); } test "seekBy" { const io = testing.io; var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); try tmp_dir.dir.writeFile(.{ .sub_path = "blah.txt", .data = "let's test seekBy" }); const f = try tmp_dir.dir.openFile("blah.txt", .{ .mode = .read_only }); defer f.close(); var reader = f.readerStreaming(io, &.{}); try reader.seekBy(2); var buffer: [20]u8 = undefined; const n = try reader.interface.readSliceShort(&buffer); try testing.expectEqual(15, n); try testing.expectEqualStrings("t's test seekBy", buffer[0..15]); } test "seekTo flushes buffered data" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const io = std.testing.io; const contents = "data"; const file = try tmp.dir.createFile("seek.bin", .{ .read = true }); defer file.close(); { var buf: [16]u8 = undefined; var file_writer = std.fs.File.writer(file, &buf); try file_writer.interface.writeAll(contents); try file_writer.seekTo(8); try file_writer.interface.flush(); } var read_buffer: [16]u8 = undefined; var file_reader: std.Io.File.Reader = .initAdapted(file, io, &read_buffer); var buf: [4]u8 = undefined; try file_reader.interface.readSliceAll(&buf); try std.testing.expectEqualStrings(contents, &buf); } test "File.Writer sendfile with buffered contents" { const io = testing.io; var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); { try tmp_dir.dir.writeFile(.{ .sub_path = "a", .data = "bcd" }); const in = try tmp_dir.dir.openFile("a", .{}); defer in.close(); const out = try tmp_dir.dir.createFile("b", .{}); defer out.close(); var in_buf: [2]u8 = undefined; var in_r = in.reader(io, &in_buf); _ = try in_r.getSize(); // Catch seeks past end by populating size try in_r.interface.fill(2); var out_buf: [1]u8 = undefined; var out_w = out.writerStreaming(&out_buf); try out_w.interface.writeByte('a'); try testing.expectEqual(3, try out_w.interface.sendFileAll(&in_r, .unlimited)); try out_w.interface.flush(); } var check = try tmp_dir.dir.openFile("b", .{}); defer check.close(); var check_buf: [4]u8 = undefined; var check_r = check.reader(io, &check_buf); try testing.expectEqualStrings("abcd", try check_r.interface.take(4)); try testing.expectError(error.EndOfStream, check_r.interface.takeByte()); }