const std = @import("../std.zig"); const builtin = @import("builtin"); const testing = std.testing; const os = std.os; const fs = std.fs; const mem = std.mem; const wasi = std.os.wasi; const ArenaAllocator = std.heap.ArenaAllocator; const Dir = std.fs.Dir; const IterableDir = std.fs.IterableDir; const File = std.fs.File; const tmpDir = testing.tmpDir; const tmpIterableDir = testing.tmpIterableDir; test "Dir.readLink" { var tmp = tmpDir(.{}); defer tmp.cleanup(); // Create some targets try tmp.dir.writeFile("file.txt", "nonsense"); try tmp.dir.makeDir("subdir"); { // Create symbolic link by path tmp.dir.symLink("file.txt", "symlink1", .{}) catch |err| switch (err) { // Symlink requires admin privileges on windows, so this test can legitimately fail. error.AccessDenied => return error.SkipZigTest, else => return err, }; try testReadLink(tmp.dir, "file.txt", "symlink1"); } { // Create symbolic link by path tmp.dir.symLink("subdir", "symlink2", .{ .is_directory = true }) catch |err| switch (err) { // Symlink requires admin privileges on windows, so this test can legitimately fail. error.AccessDenied => return error.SkipZigTest, else => return err, }; try testReadLink(tmp.dir, "subdir", "symlink2"); } } fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !void { var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; const given = try dir.readLink(symlink_path, buffer[0..]); try testing.expect(mem.eql(u8, target_path, given)); } test "accessAbsolute" { if (builtin.os.tag == .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 = blk: { const relative_path = try fs.path.join(allocator, &[_][]const u8{ "zig-cache", "tmp", tmp.sub_path[0..] }); break :blk try fs.realpathAlloc(allocator, relative_path); }; try fs.accessAbsolute(base_path, .{}); } test "openDirAbsolute" { if (builtin.os.tag == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makeDir("subdir"); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const base_path = blk: { const relative_path = try fs.path.join(allocator, &[_][]const u8{ "zig-cache", "tmp", tmp.sub_path[0..], "subdir" }); break :blk try fs.realpathAlloc(allocator, relative_path); }; { var dir = try fs.openDirAbsolute(base_path, .{}); defer dir.close(); } for ([_][]const u8{ ".", ".." }) |sub_path| { const dir_path = try fs.path.join(allocator, &[_][]const u8{ base_path, sub_path }); defer allocator.free(dir_path); var dir = try fs.openDirAbsolute(dir_path, .{}); defer dir.close(); } } test "openDir cwd parent .." { if (builtin.os.tag == .wasi) return error.SkipZigTest; var dir = try fs.cwd().openDir("..", .{}); defer dir.close(); } test "readLinkAbsolute" { if (builtin.os.tag == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); // Create some targets try tmp.dir.writeFile("file.txt", "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 = blk: { const relative_path = try fs.path.join(allocator, &[_][]const u8{ "zig-cache", "tmp", tmp.sub_path[0..] }); break :blk try fs.realpathAlloc(allocator, relative_path); }; { const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "file.txt" }); const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "symlink1" }); // Create symbolic link by path fs.symLinkAbsolute(target_path, symlink_path, .{}) catch |err| switch (err) { // Symlink requires admin privileges on windows, so this test can legitimately fail. error.AccessDenied => return error.SkipZigTest, else => return err, }; try testReadLinkAbsolute(target_path, symlink_path); } { const target_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "subdir" }); const symlink_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "symlink2" }); // Create symbolic link by path fs.symLinkAbsolute(target_path, symlink_path, .{ .is_directory = true }) catch |err| switch (err) { // Symlink requires admin privileges on windows, so this test can legitimately fail. error.AccessDenied => return error.SkipZigTest, else => return err, }; try testReadLinkAbsolute(target_path, symlink_path); } } 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.expect(mem.eql(u8, target_path, given)); } test "Dir.Iterator" { var tmp_dir = tmpIterableDir(.{}); defer tmp_dir.cleanup(); // First, create a couple of entries to iterate over. const file = try tmp_dir.iterable_dir.dir.createFile("some_file", .{}); file.close(); try tmp_dir.iterable_dir.dir.makeDir("some_dir"); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); var entries = std.ArrayList(IterableDir.Entry).init(allocator); // Create iterator. var iter = tmp_dir.iterable_dir.iterate(); while (try iter.next()) |entry| { // We cannot just store `entry` as on Windows, we're re-using the name buffer // which means we'll actually share the `name` pointer between entries! const name = try allocator.dupe(u8, entry.name); try entries.append(.{ .name = name, .kind = entry.kind }); } try testing.expect(entries.items.len == 2); // 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 = tmpIterableDir(.{}); 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.iterable_dir.dir.createFile(name, .{}); file.close(); } var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); var entries = std.ArrayList(IterableDir.Entry).init(allocator); // Create iterator. var iter = tmp_dir.iterable_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 = tmpIterableDir(.{}); defer tmp_dir.cleanup(); // First, create a couple of entries to iterate over. const file = try tmp_dir.iterable_dir.dir.createFile("some_file", .{}); file.close(); try tmp_dir.iterable_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.ArrayList(IterableDir.Entry).init(allocator); // Create iterator. var iter = tmp_dir.iterable_dir.iterate(); while (try iter.next()) |entry| { // We cannot just store `entry` as on Windows, we're re-using the name buffer // which means we'll actually share the `name` pointer between entries! const name = try allocator.dupe(u8, entry.name); try entries.append(.{ .name = name, .kind = entry.kind }); } try testing.expect(entries.items.len == 2); // 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 = tmpIterableDir(.{}); defer tmp_dir.cleanup(); // First, create a couple of entries to iterate over. const file = try tmp_dir.iterable_dir.dir.createFile("some_file", .{}); file.close(); try tmp_dir.iterable_dir.dir.makeDir("some_dir"); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); // Create iterator. var iter = tmp_dir.iterable_dir.iterate(); var i: u8 = 0; while (i < 2) : (i += 1) { var entries = std.ArrayList(IterableDir.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.expect(entries.items.len == 2); // 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 iterable_subdir = try tmp.dir.makeOpenPathIterable("subdir", .{}); defer iterable_subdir.close(); var iterator = iterable_subdir.iterate(); // Create something to iterate over within the subdir try tmp.dir.makePath("subdir/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 (builtin.os.tag == .linux) { try std.testing.expectError(error.DirNotFound, iterator.nextLinux()); } } fn entryEql(lhs: IterableDir.Entry, rhs: IterableDir.Entry) bool { return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind; } fn contains(entries: *const std.ArrayList(IterableDir.Entry), el: IterableDir.Entry) bool { for (entries.items) |entry| { if (entryEql(entry, el)) return true; } return false; } test "Dir.realpath smoke test" { switch (builtin.os.tag) { .linux, .windows, .macos, .ios, .watchos, .tvos, .solaris => {}, else => return error.SkipZigTest, } var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); var file = try tmp_dir.dir.createFile("test_file", .{ .lock = File.Lock.Shared }); // We need to close the file immediately as otherwise on Windows we'll end up // with a sharing violation. file.close(); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const base_path = blk: { const relative_path = try fs.path.join(allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..] }); break :blk try fs.realpathAlloc(allocator, relative_path); }; // First, test non-alloc version { var buf1: [fs.MAX_PATH_BYTES]u8 = undefined; const file_path = try tmp_dir.dir.realpath("test_file", buf1[0..]); const expected_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_file" }); try testing.expect(mem.eql(u8, file_path, expected_path)); } // Next, test alloc version { const file_path = try tmp_dir.dir.realpathAlloc(allocator, "test_file"); const expected_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_file" }); try testing.expect(mem.eql(u8, file_path, expected_path)); } } test "readAllAlloc" { 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 file.readToEndAlloc(testing.allocator, 1024); defer testing.allocator.free(buf1); try testing.expect(buf1.len == 0); 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); try file.seekTo(0); // max_bytes > file_size const buf2 = try file.readToEndAlloc(testing.allocator, 1024); defer testing.allocator.free(buf2); try testing.expectEqual(write_buf.len, buf2.len); try testing.expect(std.mem.eql(u8, write_buf, buf2)); try file.seekTo(0); // max_bytes == file_size const buf3 = try file.readToEndAlloc(testing.allocator, write_buf.len); defer testing.allocator.free(buf3); try testing.expectEqual(write_buf.len, buf3.len); try testing.expect(std.mem.eql(u8, write_buf, buf3)); try file.seekTo(0); // max_bytes < file_size try testing.expectError(error.FileTooBig, file.readToEndAlloc(testing.allocator, write_buf.len - 1)); } test "directory operations on files" { var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); const test_file_name = "test_file"; var file = try tmp_dir.dir.createFile(test_file_name, .{ .read = true }); file.close(); try testing.expectError(error.PathAlreadyExists, tmp_dir.dir.makeDir(test_file_name)); try testing.expectError(error.NotDir, tmp_dir.dir.openDir(test_file_name, .{})); try testing.expectError(error.NotDir, tmp_dir.dir.deleteDir(test_file_name)); switch (builtin.os.tag) { .wasi, .freebsd, .netbsd, .openbsd, .dragonfly => {}, else => { const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_file_name); defer testing.allocator.free(absolute_path); try testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(absolute_path)); try testing.expectError(error.NotDir, fs.deleteDirAbsolute(absolute_path)); }, } // ensure the file still exists and is a file as a sanity check file = try tmp_dir.dir.openFile(test_file_name, .{}); const stat = try file.stat(); try testing.expect(stat.kind == .File); file.close(); } test "file operations on directories" { // TODO: fix this test on FreeBSD. https://github.com/ziglang/zig/issues/1759 if (builtin.os.tag == .freebsd) return error.SkipZigTest; var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); const test_dir_name = "test_dir"; try tmp_dir.dir.makeDir(test_dir_name); try testing.expectError(error.IsDir, tmp_dir.dir.createFile(test_dir_name, .{})); try testing.expectError(error.IsDir, tmp_dir.dir.deleteFile(test_dir_name)); switch (builtin.os.tag) { // no error when reading a directory. .dragonfly, .netbsd => {}, // Currently, WASI will return error.Unexpected (via ENOTCAPABLE) when attempting fd_read on a directory handle. // TODO: Re-enable on WASI once https://github.com/bytecodealliance/wasmtime/issues/1935 is resolved. .wasi => {}, else => { try testing.expectError(error.IsDir, tmp_dir.dir.readFileAlloc(testing.allocator, test_dir_name, std.math.maxInt(usize))); }, } // 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, tmp_dir.dir.openFile(test_dir_name, .{ .mode = .read_write })); switch (builtin.os.tag) { .wasi, .freebsd, .netbsd, .openbsd, .dragonfly => {}, else => { const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_dir_name); defer testing.allocator.free(absolute_path); try testing.expectError(error.IsDir, fs.createFileAbsolute(absolute_path, .{})); try testing.expectError(error.IsDir, fs.deleteFileAbsolute(absolute_path)); }, } // ensure the directory still exists as a sanity check var dir = try tmp_dir.dir.openDir(test_dir_name, .{}); dir.close(); } test "deleteDir" { var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); // deleting a non-existent directory try testing.expectError(error.FileNotFound, tmp_dir.dir.deleteDir("test_dir")); var dir = try tmp_dir.dir.makeOpenPath("test_dir", .{}); var file = try dir.createFile("test_file", .{}); file.close(); dir.close(); // deleting a non-empty directory // TODO: Re-enable this check on Windows, see https://github.com/ziglang/zig/issues/5537 if (builtin.os.tag != .windows) { try testing.expectError(error.DirNotEmpty, tmp_dir.dir.deleteDir("test_dir")); } dir = try tmp_dir.dir.openDir("test_dir", .{}); try dir.deleteFile("test_file"); dir.close(); // deleting an empty directory try tmp_dir.dir.deleteDir("test_dir"); } test "Dir.rename files" { var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); try testing.expectError(error.FileNotFound, tmp_dir.dir.rename("missing_file_name", "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 tmp_dir.dir.rename(test_file_name, 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, .{}); file.close(); // Rename to self succeeds try tmp_dir.dir.rename(renamed_test_file_name, renamed_test_file_name); // Rename to existing file succeeds var existing_file = try tmp_dir.dir.createFile("existing_file", .{ .read = true }); existing_file.close(); try tmp_dir.dir.rename(renamed_test_file_name, "existing_file"); try testing.expectError(error.FileNotFound, tmp_dir.dir.openFile(renamed_test_file_name, .{})); file = try tmp_dir.dir.openFile("existing_file", .{}); file.close(); } test "Dir.rename directories" { var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); // Renaming directories try tmp_dir.dir.makeDir("test_dir"); try tmp_dir.dir.rename("test_dir", "test_dir_renamed"); // Ensure the directory was renamed try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir("test_dir", .{})); var dir = try tmp_dir.dir.openDir("test_dir_renamed", .{}); // Put a file in the directory var file = try dir.createFile("test_file", .{ .read = true }); file.close(); dir.close(); try tmp_dir.dir.rename("test_dir_renamed", "test_dir_renamed_again"); // Ensure the directory was renamed and the file still exists in it try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir("test_dir_renamed", .{})); dir = try tmp_dir.dir.openDir("test_dir_renamed_again", .{}); file = try dir.openFile("test_file", .{}); file.close(); dir.close(); } test "Dir.rename directory onto empty dir" { // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364 if (builtin.os.tag == .windows) return error.SkipZigTest; var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); try tmp_dir.dir.makeDir("test_dir"); try tmp_dir.dir.makeDir("target_dir"); try tmp_dir.dir.rename("test_dir", "target_dir"); // Ensure the directory was renamed try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir("test_dir", .{})); var dir = try tmp_dir.dir.openDir("target_dir", .{}); dir.close(); } test "Dir.rename directory onto non-empty dir" { // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364 if (builtin.os.tag == .windows) return error.SkipZigTest; var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); try tmp_dir.dir.makeDir("test_dir"); var target_dir = try tmp_dir.dir.makeOpenPath("target_dir", .{}); 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, tmp_dir.dir.rename("test_dir", "target_dir")); // Ensure the directory was not renamed var dir = try tmp_dir.dir.openDir("test_dir", .{}); dir.close(); } test "Dir.rename file <-> dir" { // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364 if (builtin.os.tag == .windows) return error.SkipZigTest; var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); var file = try tmp_dir.dir.createFile("test_file", .{ .read = true }); file.close(); try tmp_dir.dir.makeDir("test_dir"); try testing.expectError(error.IsDir, tmp_dir.dir.rename("test_file", "test_dir")); try testing.expectError(error.NotDir, tmp_dir.dir.rename("test_dir", "test_file")); } 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 (builtin.os.tag == .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 = blk: { const relative_path = try fs.path.join(allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..] }); break :blk try fs.realpathAlloc(allocator, relative_path); }; try testing.expectError(error.FileNotFound, fs.renameAbsolute( try fs.path.join(allocator, &[_][]const u8{ base_path, "missing_file_name" }), try fs.path.join(allocator, &[_][]const u8{ 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, &[_][]const u8{ base_path, test_file_name }), try fs.path.join(allocator, &[_][]const u8{ 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.expect(stat.kind == .File); 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, &[_][]const u8{ base_path, test_dir_name }), try fs.path.join(allocator, &[_][]const u8{ 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 (builtin.os.tag == .wasi) return error.SkipZigTest; const self_exe_file = try std.fs.openSelfExe(.{}); self_exe_file.close(); } test "makePath, put some files in it, deleteTree" { var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makePath("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c"); try tmp.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c" ++ fs.path.sep_str ++ "file.txt", "nonsense"); try tmp.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "file2.txt", "blah"); try tmp.dir.deleteTree("os_test_tmp"); if (tmp.dir.openDir("os_test_tmp", .{})) |dir| { _ = dir; @panic("expected error"); } else |err| { try testing.expect(err == error.FileNotFound); } } test "makePath, put some files in it, deleteTreeMinStackSize" { var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makePath("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c"); try tmp.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c" ++ fs.path.sep_str ++ "file.txt", "nonsense"); try tmp.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "file2.txt", "blah"); try tmp.dir.deleteTreeMinStackSize("os_test_tmp"); if (tmp.dir.openDir("os_test_tmp", .{})) |dir| { _ = dir; @panic("expected error"); } else |err| { try testing.expect(err == error.FileNotFound); } } test "makePath in a directory that no longer exists" { if (builtin.os.tag == .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")); } fn testFilenameLimits(iterable_dir: IterableDir, maxed_filename: []const u8) !void { // setup, create a dir and a nested file both with maxed filenames, and walk the dir { var maxed_dir = try iterable_dir.dir.makeOpenPath(maxed_filename, .{}); defer maxed_dir.close(); try maxed_dir.writeFile(maxed_filename, ""); 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.dir.deleteTree(maxed_filename); } test "max file name component lengths" { var tmp = tmpIterableDir(.{}); defer tmp.cleanup(); if (builtin.os.tag == .windows) { // € is the character with the largest codepoint that is encoded as a single u16 in UTF-16, // so Windows allows for NAME_MAX of them const maxed_windows_filename = ("€".*) ** std.os.windows.NAME_MAX; try testFilenameLimits(tmp.iterable_dir, &maxed_windows_filename); } else if (builtin.os.tag == .wasi) { // On WASI, the maxed filename depends on the host OS, so in order for this test to // work on any host, we need to use a length that will work for all platforms // (i.e. the minimum MAX_NAME_BYTES of all supported platforms). const maxed_wasi_filename = [_]u8{'1'} ** 255; try testFilenameLimits(tmp.iterable_dir, &maxed_wasi_filename); } else { const maxed_ascii_filename = [_]u8{'1'} ** std.fs.MAX_NAME_BYTES; try testFilenameLimits(tmp.iterable_dir, &maxed_ascii_filename); } } test "writev, readv" { 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 = [_]std.os.iovec_const{ .{ .iov_base = line1, .iov_len = line1.len, }, .{ .iov_base = line2, .iov_len = line2.len, }, }; var read_vecs = [_]std.os.iovec{ .{ .iov_base = &buf2, .iov_len = buf2.len, }, .{ .iov_base = &buf1, .iov_len = buf1.len, }, }; var src_file = try tmp.dir.createFile("test.txt", .{ .read = true }); defer src_file.close(); try src_file.writevAll(&write_vecs); try testing.expectEqual(@as(u64, line1.len + line2.len), try src_file.getEndPos()); try src_file.seekTo(0); const read = try src_file.readvAll(&read_vecs); try testing.expectEqual(@as(usize, line1.len + line2.len), read); try testing.expectEqualStrings(&buf1, "line2\n"); try testing.expectEqualStrings(&buf2, "line1\n"); } test "pwritev, preadv" { 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 = [_]std.os.iovec_const{ .{ .iov_base = line1, .iov_len = line1.len, }, .{ .iov_base = line2, .iov_len = line2.len, }, }; var read_vecs = [_]std.os.iovec{ .{ .iov_base = &buf2, .iov_len = buf2.len, }, .{ .iov_base = &buf1, .iov_len = buf1.len, }, }; var src_file = try tmp.dir.createFile("test.txt", .{ .read = true }); defer src_file.close(); try src_file.pwritevAll(&write_vecs, 16); try testing.expectEqual(@as(u64, 16 + line1.len + line2.len), try src_file.getEndPos()); const read = try src_file.preadvAll(&read_vecs, 16); try testing.expectEqual(@as(usize, line1.len + line2.len), read); try testing.expectEqualStrings(&buf1, "line2\n"); try testing.expectEqualStrings(&buf2, "line1\n"); } test "access file" { if (builtin.os.tag == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makePath("os_test_tmp"); if (tmp.dir.access("os_test_tmp" ++ fs.path.sep_str ++ "file.txt", .{})) |ok| { _ = ok; @panic("expected error"); } else |err| { try testing.expect(err == error.FileNotFound); } try tmp.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "file.txt", ""); try tmp.dir.access("os_test_tmp" ++ fs.path.sep_str ++ "file.txt", .{}); try tmp.dir.deleteTree("os_test_tmp"); } test "sendfile" { var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makePath("os_test_tmp"); defer tmp.dir.deleteTree("os_test_tmp") catch {}; var dir = try tmp.dir.openDir("os_test_tmp", .{}); defer dir.close(); const line1 = "line1\n"; const line2 = "second line\n"; var vecs = [_]std.os.iovec_const{ .{ .iov_base = line1, .iov_len = line1.len, }, .{ .iov_base = line2, .iov_len = line2.len, }, }; var src_file = try dir.createFile("sendfile1.txt", .{ .read = true }); defer src_file.close(); try src_file.writevAll(&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 hdtr = [_]std.os.iovec_const{ .{ .iov_base = header1, .iov_len = header1.len, }, .{ .iov_base = header2, .iov_len = header2.len, }, .{ .iov_base = trailer1, .iov_len = trailer1.len, }, .{ .iov_base = trailer2, .iov_len = trailer2.len, }, }; var written_buf: [100]u8 = undefined; try dest_file.writeFileAll(src_file, .{ .in_offset = 1, .in_len = 10, .headers_and_trailers = &hdtr, .header_count = 2, }); const amt = try dest_file.preadAll(&written_buf, 0); try testing.expect(mem.eql(u8, written_buf[0..amt], "header1\nsecond header\nine1\nsecontrailer1\nsecond trailer\n")); } test "copyRangeAll" { var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makePath("os_test_tmp"); defer tmp.dir.deleteTree("os_test_tmp") catch {}; var dir = try tmp.dir.openDir("os_test_tmp", .{}); defer dir.close(); var src_file = try dir.createFile("file1.txt", .{ .read = true }); defer src_file.close(); const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP"; try src_file.writeAll(data); var dest_file = try dir.createFile("file2.txt", .{ .read = true }); defer dest_file.close(); var written_buf: [100]u8 = undefined; _ = try src_file.copyRangeAll(0, dest_file, 0, data.len); const amt = try dest_file.preadAll(&written_buf, 0); try testing.expect(mem.eql(u8, written_buf[0..amt], data)); } test "fs.copyFile" { const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP"; const src_file = "tmp_test_copy_file.txt"; const dest_file = "tmp_test_copy_file2.txt"; const dest_file2 = "tmp_test_copy_file3.txt"; var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(src_file, data); defer tmp.dir.deleteFile(src_file) catch {}; try tmp.dir.copyFile(src_file, tmp.dir, dest_file, .{}); defer tmp.dir.deleteFile(dest_file) catch {}; try tmp.dir.copyFile(src_file, tmp.dir, dest_file2, .{ .override_mode = File.default_mode }); defer tmp.dir.deleteFile(dest_file2) catch {}; try expectFileContents(tmp.dir, dest_file, data); try expectFileContents(tmp.dir, dest_file2, data); } fn expectFileContents(dir: Dir, file_path: []const u8, data: []const u8) !void { const contents = try dir.readFileAlloc(testing.allocator, file_path, 1000); defer testing.allocator.free(contents); try testing.expectEqualSlices(u8, data, contents); } test "AtomicFile" { const test_out_file = "tmp_atomic_file_test_dest.txt"; const test_content = \\ hello! \\ this is a test file ; var tmp = tmpDir(.{}); defer tmp.cleanup(); { var af = try tmp.dir.atomicFile(test_out_file, .{}); defer af.deinit(); try af.file.writeAll(test_content); try af.finish(); } const content = try tmp.dir.readFileAlloc(testing.allocator, test_out_file, 9999); defer testing.allocator.free(content); try testing.expect(mem.eql(u8, content, test_content)); try tmp.dir.deleteFile(test_out_file); } test "realpath" { if (builtin.os.tag == .wasi) return error.SkipZigTest; var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; try testing.expectError(error.FileNotFound, fs.realpath("definitely_bogus_does_not_exist1234", &buf)); } test "open file with exclusive nonblocking lock twice" { if (builtin.os.tag == .wasi) return error.SkipZigTest; const filename = "file_nonblocking_lock_test.txt"; var tmp = tmpDir(.{}); defer tmp.cleanup(); const file1 = try tmp.dir.createFile(filename, .{ .lock = .Exclusive, .lock_nonblocking = true }); defer file1.close(); const file2 = tmp.dir.createFile(filename, .{ .lock = .Exclusive, .lock_nonblocking = true }); try testing.expectError(error.WouldBlock, file2); } test "open file with shared and exclusive nonblocking lock" { if (builtin.os.tag == .wasi) return error.SkipZigTest; const filename = "file_nonblocking_lock_test.txt"; var tmp = tmpDir(.{}); defer tmp.cleanup(); const file1 = try tmp.dir.createFile(filename, .{ .lock = .Shared, .lock_nonblocking = true }); defer file1.close(); const file2 = tmp.dir.createFile(filename, .{ .lock = .Exclusive, .lock_nonblocking = true }); try testing.expectError(error.WouldBlock, file2); } test "open file with exclusive and shared nonblocking lock" { if (builtin.os.tag == .wasi) return error.SkipZigTest; const filename = "file_nonblocking_lock_test.txt"; var tmp = tmpDir(.{}); defer tmp.cleanup(); const file1 = try tmp.dir.createFile(filename, .{ .lock = .Exclusive, .lock_nonblocking = true }); defer file1.close(); const file2 = tmp.dir.createFile(filename, .{ .lock = .Shared, .lock_nonblocking = true }); try testing.expectError(error.WouldBlock, file2); } test "open file with exclusive lock twice, make sure second lock waits" { if (builtin.single_threaded) return error.SkipZigTest; if (std.io.is_async) { // This test starts its own threads and is not compatible with async I/O. return error.SkipZigTest; } const filename = "file_lock_test.txt"; var tmp = tmpDir(.{}); defer tmp.cleanup(); const file = try tmp.dir.createFile(filename, .{ .lock = .Exclusive }); errdefer file.close(); const S = struct { fn checkFn(dir: *fs.Dir, started: *std.Thread.ResetEvent, locked: *std.Thread.ResetEvent) !void { started.set(); const file1 = try dir.createFile(filename, .{ .lock = .Exclusive }); locked.set(); file1.close(); } }; var started = std.Thread.ResetEvent{}; var locked = std.Thread.ResetEvent{}; const t = try std.Thread.spawn(.{}, S.checkFn, .{ &tmp.dir, &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(); } test "open file with exclusive nonblocking lock twice (absolute paths)" { if (builtin.os.tag == .wasi) return error.SkipZigTest; const allocator = testing.allocator; const cwd = try std.process.getCwdAlloc(allocator); defer allocator.free(cwd); const file_paths: [2][]const u8 = .{ cwd, "zig-test-absolute-paths.txt" }; const filename = try fs.path.resolve(allocator, &file_paths); defer allocator.free(filename); 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); try fs.deleteFileAbsolute(filename); } test "walker" { if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; var tmp = tmpIterableDir(.{}); defer tmp.cleanup(); // iteration order of walker is undefined, so need lookup maps to check against const expected_paths = std.ComptimeStringMap(void, .{ .{"dir1"}, .{"dir2"}, .{"dir3"}, .{"dir4"}, .{"dir3" ++ std.fs.path.sep_str ++ "sub1"}, .{"dir3" ++ std.fs.path.sep_str ++ "sub2"}, .{"dir3" ++ std.fs.path.sep_str ++ "sub2" ++ std.fs.path.sep_str ++ "subsub1"}, }); const expected_basenames = std.ComptimeStringMap(void, .{ .{"dir1"}, .{"dir2"}, .{"dir3"}, .{"dir4"}, .{"sub1"}, .{"sub2"}, .{"subsub1"}, }); for (expected_paths.kvs) |kv| { try tmp.iterable_dir.dir.makePath(kv.key); } var walker = try tmp.iterable_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: {s}\n", .{std.fmt.fmtSliceEscapeLower(entry.basename)}); return err; }; testing.expect(expected_paths.has(entry.path)) catch |err| { std.debug.print("found unexpected path: {s}\n", .{std.fmt.fmtSliceEscapeLower(entry.path)}); 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" { if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; var tmp = tmpIterableDir(.{}); defer tmp.cleanup(); var walker = try tmp.iterable_dir.walk(testing.allocator); defer walker.deinit(); // Create 2 directories inside the tmp directory, but then only iterate once before breaking. // This ensures that walker doesn't try to close the initial directory when not fully iterating. try tmp.iterable_dir.dir.makePath("a"); try tmp.iterable_dir.dir.makePath("b"); 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 (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.makeDir("./subdir"); try tmp.dir.access("./subdir", .{}); var created_subdir = try tmp.dir.openDir("./subdir", .{}); created_subdir.close(); const created_file = try tmp.dir.createFile("./subdir/../file", .{}); created_file.close(); try tmp.dir.access("./subdir/../file", .{}); try tmp.dir.copyFile("./subdir/../file", tmp.dir, "./subdir/../copy", .{}); try tmp.dir.rename("./subdir/../copy", "./subdir/../rename"); const renamed_file = try tmp.dir.openFile("./subdir/../rename", .{}); renamed_file.close(); try tmp.dir.deleteFile("./subdir/../rename"); try tmp.dir.writeFile("./subdir/../update", "something"); const prev_status = try tmp.dir.updateFile("./subdir/../file", tmp.dir, "./subdir/../update", .{}); try testing.expectEqual(fs.PrevStatus.stale, prev_status); try tmp.dir.deleteDir("./subdir"); } test ". and .. in absolute functions" { if (builtin.os.tag == .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 = blk: { const relative_path = try fs.path.join(allocator, &[_][]const u8{ "zig-cache", "tmp", tmp.sub_path[0..] }); break :blk try fs.realpathAlloc(allocator, relative_path); }; const subdir_path = try fs.path.join(allocator, &[_][]const u8{ 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, &[_][]const u8{ 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, &[_][]const u8{ subdir_path, "../copy" }); try fs.copyFileAbsolute(created_file_path, copied_file_path, .{}); const renamed_file_path = try fs.path.join(allocator, &[_][]const u8{ 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); const update_file_path = try fs.path.join(allocator, &[_][]const u8{ subdir_path, "../update" }); const update_file = try fs.createFileAbsolute(update_file_path, .{}); try update_file.writeAll("something"); update_file.close(); const prev_status = try fs.updateFileAbsolute(created_file_path, update_file_path, .{}); try testing.expectEqual(fs.PrevStatus.stale, prev_status); try fs.deleteDirAbsolute(subdir_path); } test "chmod" { if (builtin.os.tag == .windows or builtin.os.tag == .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.expect((try file.stat()).mode & 0o7777 == 0o600); try file.chmod(0o644); try testing.expect((try file.stat()).mode & 0o7777 == 0o644); try tmp.dir.makeDir("test_dir"); var iterable_dir = try tmp.dir.openIterableDir("test_dir", .{}); defer iterable_dir.close(); try iterable_dir.chmod(0o700); try testing.expect((try iterable_dir.dir.stat()).mode & 0o7777 == 0o700); } test "chown" { if (builtin.os.tag == .windows or builtin.os.tag == .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 iterable_dir = try tmp.dir.openIterableDir("test_dir", .{}); defer iterable_dir.close(); try iterable_dir.chown(null, null); } test "File.Metadata" { var tmp = tmpDir(.{}); defer tmp.cleanup(); const file = try tmp.dir.createFile("test_file", .{ .read = true }); defer file.close(); const metadata = try file.metadata(); try testing.expect(metadata.kind() == .File); try testing.expect(metadata.size() == 0); _ = metadata.accessed(); _ = metadata.modified(); _ = metadata.created(); } test "File.Permissions" { if (builtin.os.tag == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); const file = try tmp.dir.createFile("test_file", .{ .read = true }); defer file.close(); const metadata = try file.metadata(); var permissions = metadata.permissions(); try testing.expect(!permissions.readOnly()); permissions.setReadOnly(true); try testing.expect(permissions.readOnly()); try file.setPermissions(permissions); const new_permissions = (try file.metadata()).permissions(); try testing.expect(new_permissions.readOnly()); // Must be set to non-read-only to delete permissions.setReadOnly(false); try file.setPermissions(permissions); } test "File.PermissionsUnix" { if (builtin.os.tag == .windows or builtin.os.tag == .wasi) return error.SkipZigTest; var tmp = tmpDir(.{}); defer tmp.cleanup(); const file = try tmp.dir.createFile("test_file", .{ .mode = 0o666, .read = true }); defer file.close(); const metadata = try file.metadata(); var permissions = metadata.permissions(); permissions.setReadOnly(true); try testing.expect(permissions.readOnly()); try testing.expect(!permissions.inner.unixHas(.user, .write)); permissions.inner.unixSet(.user, .{ .write = true }); try testing.expect(!permissions.readOnly()); try testing.expect(permissions.inner.unixHas(.user, .write)); try testing.expect(permissions.inner.mode & 0o400 != 0); permissions.setReadOnly(true); try file.setPermissions(permissions); permissions = (try file.metadata()).permissions(); try testing.expect(permissions.readOnly()); // Must be set to non-read-only to delete permissions.setReadOnly(false); try file.setPermissions(permissions); const permissions_unix = File.PermissionsUnix.unixNew(0o754); try testing.expect(permissions_unix.unixHas(.user, .execute)); try testing.expect(!permissions_unix.unixHas(.other, .execute)); }