diff --git a/lib/std/fs.zig b/lib/std/fs.zig index f0be483ca8d8..f01bcb4f22c8 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1949,7 +1949,13 @@ pub const Dir = struct { return self.symLinkWasi(target_path, sym_link_path, flags); } if (builtin.os.tag == .windows) { - const target_path_w = try os.windows.sliceToPrefixedFileW(self.fd, target_path); + // Target path does not use sliceToPrefixedFileW because certain paths + // are handled differently when creating a symlink than they would be + // when converting to an NT namespaced path. CreateSymbolicLink in + // symLinkW will handle the necessary conversion. + var target_path_w: os.windows.PathSpace = undefined; + target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path); + target_path_w.data[target_path_w.len] = 0; const sym_link_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sym_link_path); return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); } @@ -1987,7 +1993,10 @@ pub const Dir = struct { /// are null-terminated, WTF16 encoded. pub fn symLinkW( self: Dir, - target_path_w: []const u16, + /// WTF-16, does not need to be NT-prefixed. The NT-prefixing + /// of this path is handled by CreateSymbolicLink. + target_path_w: [:0]const u16, + /// WTF-16, must be NT-prefixed or relative sym_link_path_w: []const u16, flags: SymLinkFlags, ) !void { diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 50804bf614d3..f168a34e9f9b 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -13,38 +13,167 @@ 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"); +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, + }; + } - { - // 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, + pub const TransformError = std.os.RealPathError || error{OutOfMemory}; + pub const TransformFn = fn (allocator: mem.Allocator, dir: Dir, relative_path: []const u8) TransformError![]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: []const u8) TransformError![]const u8 { + _ = allocator; + _ = dir; + return relative_path; + } + }.transform, + .absolute => return struct { + fn transform(allocator: mem.Allocator, dir: Dir, relative_path: []const u8) TransformError![]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 os.getFdPath(dir.fd, &fd_path_buf); + return fs.path.join(allocator, &.{ dir_path, relative_path }); + } + }.transform, + .unc => return struct { + fn transform(allocator: mem.Allocator, dir: Dir, relative_path: []const u8) TransformError![]const u8 { + // Any drive absolute path (C:\foo) can be converted into a UNC path by + // using 'localhost' as the server name and '$' as the share name. + var fd_path_buf: [fs.MAX_PATH_BYTES]u8 = undefined; + const dir_path = try os.getFdPath(dir.fd, &fd_path_buf); + const windows_path_type = std.os.windows.getUnprefixedPathType(u8, dir_path); + switch (windows_path_type) { + .unc_absolute => return fs.path.join(allocator, &.{ dir_path, relative_path }), + .drive_absolute => { + // `C:\<...>` -> `\\localhost\C$\<...>` + const prepended = "\\\\localhost\\"; + var path = try fs.path.join(allocator, &.{ prepended, dir_path, relative_path }); + path[prepended.len + 1] = '$'; + return path; + }, + else => unreachable, + } + } + }.transform, + } + } +}; + +const TestContext = struct { + path_type: PathType, + arena: ArenaAllocator, + tmp: testing.TmpIterableDir, + dir: std.fs.Dir, + iterable_dir: std.fs.IterableDir, + transform_fn: *const PathType.TransformFn, + + pub fn init(path_type: PathType, allocator: mem.Allocator, transform_fn: *const PathType.TransformFn) TestContext { + var tmp = tmpIterableDir(.{}); + return .{ + .path_type = path_type, + .arena = ArenaAllocator.init(allocator), + .tmp = tmp, + .dir = tmp.iterable_dir.dir, + .iterable_dir = tmp.iterable_dir, + .transform_fn = transform_fn, }; - 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, + + pub fn deinit(self: *TestContext) void { + self.arena.deinit(); + self.tmp.cleanup(); + } + + /// Returns the `relative_path` transformed into the TestContext's `path_type`. + /// The result is allocated by the TestContext's arena and will be free'd during + /// `TestContext.deinit`. + pub fn transformPath(self: *TestContext, relative_path: []const u8) ![]const u8 { + return self.transform_fn(self.arena.allocator(), self.dir, relative_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 { + inline for (@typeInfo(PathType).Enum.fields) |enum_field| { + const path_type = @field(PathType, enum_field.name); + if (!(comptime path_type.isSupported(builtin.os))) continue; + + var ctx = TestContext.init(path_type, testing.allocator, path_type.getTransformFn()); + defer ctx.deinit(); + + test_func(&ctx) catch |err| { + std.debug.print("path type: {s}\n", .{enum_field.name}); + return err; }; - try testReadLink(tmp.dir, "subdir", "symlink2"); } } +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(file_target_path, "nonsense"); + const dir_target_path = try ctx.transformPath("subdir"); + try ctx.dir.makeDir(dir_target_path); + + { + // Create symbolic link by path + ctx.dir.symLink(file_target_path, "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(ctx.dir, file_target_path, "symlink1"); + } + { + // Create symbolic link by path + ctx.dir.symLink(dir_target_path, "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(ctx.dir, dir_target_path, "symlink2"); + } + } + }.impl); +} + 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)); + try testing.expectEqualStrings(target_path, given); +} + +test "openDir" { + try testWithAllSupportedPathTypes(struct { + fn impl(ctx: *TestContext) !void { + 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(testing.allocator, &[_][]const u8{ subdir_path, sub_path }); + defer testing.allocator.free(dir_path); + var dir = try ctx.dir.openDir(dir_path, .{}); + defer dir.close(); + } + } + }.impl); } test "accessAbsolute" { @@ -171,7 +300,7 @@ test "readLinkAbsolute" { 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)); + try testing.expectEqualStrings(target_path, given); } test "Dir.Iterator" { @@ -199,7 +328,7 @@ test "Dir.Iterator" { try entries.append(.{ .name = name, .kind = entry.kind }); } - try testing.expect(entries.items.len == 2); // note that the Iterator skips '.' and '..' + 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 })); } @@ -266,7 +395,7 @@ test "Dir.Iterator twice" { try entries.append(.{ .name = name, .kind = entry.kind }); } - try testing.expect(entries.items.len == 2); // note that the Iterator skips '.' and '..' + 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 })); } @@ -300,7 +429,7 @@ test "Dir.Iterator reset" { try entries.append(.{ .name = name, .kind = entry.kind }); } - try testing.expect(entries.items.len == 2); // note that the Iterator skips '.' and '..' + 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 })); @@ -349,53 +478,59 @@ fn contains(entries: *const std.ArrayList(IterableDir.Entry), el: IterableDir.En } 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 = .shared }); - // We need to close the file immediately as otherwise on Windows we'll end up - // with a sharing violation. - file.close(); - - try tmp_dir.dir.makeDir("test_dir"); - - 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_file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_file" }); - try testing.expectEqualStrings(expected_file_path, file_path); - - const dir_path = try tmp_dir.dir.realpath("test_dir", buf1[0..]); - const expected_dir_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_dir" }); - try testing.expectEqualStrings(expected_dir_path, dir_path); - } - - // Next, test alloc version - { - const file_path = try tmp_dir.dir.realpathAlloc(allocator, "test_file"); - const expected_file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_file" }); - try testing.expectEqualStrings(expected_file_path, file_path); - - const dir_path = try tmp_dir.dir.realpathAlloc(allocator, "test_dir"); - const expected_dir_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_dir" }); - try testing.expectEqualStrings(expected_dir_path, dir_path); - } + if (!comptime std.os.isGetFdPathSupportedOnTarget(builtin.os)) 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 buf: [fs.MAX_PATH_BYTES]u8 = undefined; + + // FileNotFound if the path doesn't exist + try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(testing.allocator, test_file_path)); + try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_file_path, &buf)); + try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(testing.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(test_file_path, ""); + try ctx.dir.makeDir(test_dir_path); + + const base_path = try ctx.transformPath("."); + const base_realpath = try ctx.dir.realpathAlloc(testing.allocator, base_path); + defer testing.allocator.free(base_realpath); + const expected_file_path = try fs.path.join( + testing.allocator, + &[_][]const u8{ base_realpath, "test_file" }, + ); + defer testing.allocator.free(expected_file_path); + const expected_dir_path = try fs.path.join( + testing.allocator, + &[_][]const u8{ base_realpath, "test_dir" }, + ); + defer testing.allocator.free(expected_dir_path); + + // 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(testing.allocator, test_file_path); + defer testing.allocator.free(file_path); + try testing.expectEqualStrings(expected_file_path, file_path); + + const dir_path = try ctx.dir.realpathAlloc(testing.allocator, test_dir_path); + defer testing.allocator.free(dir_path); + try testing.expectEqualStrings(expected_dir_path, dir_path); + } + } + }.impl); } test "readAllAlloc" { @@ -407,7 +542,7 @@ test "readAllAlloc" { const buf1 = try file.readToEndAlloc(testing.allocator, 1024); defer testing.allocator.free(buf1); - try testing.expect(buf1.len == 0); + try testing.expectEqual(@as(usize, 0), buf1.len); 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); @@ -417,14 +552,14 @@ test "readAllAlloc" { 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 testing.expectEqualStrings(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 testing.expectEqualStrings(write_buf, buf3); try file.seekTo(0); // max_bytes < file_size @@ -432,211 +567,221 @@ test "readAllAlloc" { } 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(); + 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 (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(); + 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 (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, ctx.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, 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 "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 - 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"); + 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" ++ std.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(test_file_path, ""); + 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" { - 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(); + try testWithAllSupportedPathTypes(struct { + fn impl(ctx: *TestContext) !void { + 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" { - 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(); + try testWithAllSupportedPathTypes(struct { + fn impl(ctx: *TestContext) !void { + 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 (builtin.os.tag == .windows) return error.SkipZigTest; - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); + 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 tmp_dir.dir.makeDir("test_dir"); - try tmp_dir.dir.makeDir("target_dir"); - try tmp_dir.dir.rename("test_dir", "target_dir"); + 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, tmp_dir.dir.openDir("test_dir", .{})); - var dir = try tmp_dir.dir.openDir("target_dir", .{}); - dir.close(); + // 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 (builtin.os.tag == .windows) return error.SkipZigTest; - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); + 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 tmp_dir.dir.makeDir("test_dir"); + try ctx.dir.makeDir(test_dir_path); - 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(); + 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, tmp_dir.dir.rename("test_dir", "target_dir")); + // 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 tmp_dir.dir.openDir("test_dir", .{}); - dir.close(); + // 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 (builtin.os.tag == .windows) return error.SkipZigTest; - var tmp_dir = tmpDir(.{}); - defer tmp_dir.cleanup(); + 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 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")); + 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" { @@ -694,7 +839,7 @@ test "renameAbsolute" { 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); + try testing.expectEqual(File.Kind.file, stat.kind); file.close(); // Renaming directories @@ -720,35 +865,33 @@ test "openSelfExe" { } test "makePath, put some files in it, deleteTree" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); + try testWithAllSupportedPathTypes(struct { + fn impl(ctx: *TestContext) !void { + const dir_path = try ctx.transformPath("os_test_tmp"); - 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); - } + try ctx.dir.makePath("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c"); + try ctx.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c" ++ fs.path.sep_str ++ "file.txt", "nonsense"); + try ctx.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "file2.txt", "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" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); + try testWithAllSupportedPathTypes(struct { + fn impl(ctx: *TestContext) !void { + const dir_path = try ctx.transformPath("os_test_tmp"); - 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); - } + try ctx.dir.makePath("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c"); + try ctx.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c" ++ fs.path.sep_str ++ "file.txt", "nonsense"); + try ctx.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "file2.txt", "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" { @@ -789,9 +932,9 @@ test "max file name component lengths" { 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; + // U+FFFF is the character with the largest code point that is encoded as a single + // UTF-16 code unit, so Windows allows for NAME_MAX of them. + const maxed_windows_filename = ("\u{FFFF}".*) ** std.os.windows.NAME_MAX; try testFilenameLimits(tmp.iterable_dir, &maxed_windows_filename); } else if (builtin.os.tag == .wasi) { // On WASI, the maxed filename depends on the host OS, so in order for this test to @@ -889,22 +1032,19 @@ test "pwritev, preadv" { } test "access file" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; + 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"); - var tmp = tmpDir(.{}); - defer tmp.cleanup(); + try ctx.dir.makePath(dir_path); + try testing.expectError(error.FileNotFound, ctx.dir.access(file_path, .{})); - 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"); + try ctx.dir.writeFile(file_path, ""); + try ctx.dir.access(file_path, .{}); + try ctx.dir.deleteTree(dir_path); + } + }.impl); } test "sendfile" { @@ -969,7 +1109,7 @@ test "sendfile" { .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")); + try testing.expectEqualStrings("header1\nsecond header\nine1\nsecontrailer1\nsecond trailer\n", written_buf[0..amt]); } test "copyRangeAll" { @@ -995,29 +1135,30 @@ test "copyRangeAll" { _ = 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)); + try testing.expectEqualStrings(data, written_buf[0..amt]); } -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(); +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 tmp.dir.writeFile(src_file, data); - defer tmp.dir.deleteFile(src_file) catch {}; + try ctx.dir.writeFile(src_file, data); + defer ctx.dir.deleteFile(src_file) catch {}; - try tmp.dir.copyFile(src_file, tmp.dir, dest_file, .{}); - defer tmp.dir.deleteFile(dest_file) catch {}; + try ctx.dir.copyFile(src_file, ctx.dir, dest_file, .{}); + defer ctx.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 ctx.dir.copyFile(src_file, ctx.dir, dest_file2, .{ .override_mode = File.default_mode }); + defer ctx.dir.deleteFile(dest_file2) catch {}; - try expectFileContents(tmp.dir, dest_file, data); - try expectFileContents(tmp.dir, dest_file2, data); + 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 { @@ -1028,78 +1169,75 @@ fn expectFileContents(dir: Dir, file_path: []const u8, data: []const u8) !void { } 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)); + try testWithAllSupportedPathTypes(struct { + fn impl(ctx: *TestContext) !void { + const test_out_file = try ctx.transformPath("tmp_atomic_file_test_dest.txt"); + const test_content = + \\ hello! + \\ this is a test file + ; + + { + var af = try ctx.dir.atomicFile(test_out_file, .{}); + defer af.deinit(); + try af.file.writeAll(test_content); + try af.finish(); + } + const content = try ctx.dir.readFileAlloc(testing.allocator, test_out_file, 9999); + defer testing.allocator.free(content); + try testing.expectEqualStrings(test_content, content); + + try ctx.dir.deleteFile(test_out_file); + } + }.impl); } 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(); + try testWithAllSupportedPathTypes(struct { + fn impl(ctx: *TestContext) !void { + const filename = try ctx.transformPath("file_nonblocking_lock_test.txt"); - const file1 = try tmp.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true }); - defer file1.close(); + const file1 = try ctx.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); + 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 (builtin.os.tag == .wasi) return error.SkipZigTest; - const filename = "file_nonblocking_lock_test.txt"; - - var tmp = tmpDir(.{}); - defer tmp.cleanup(); + try testWithAllSupportedPathTypes(struct { + fn impl(ctx: *TestContext) !void { + const filename = try ctx.transformPath("file_nonblocking_lock_test.txt"); - const file1 = try tmp.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true }); - defer file1.close(); + const file1 = try ctx.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); + 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 (builtin.os.tag == .wasi) return error.SkipZigTest; - const filename = "file_nonblocking_lock_test.txt"; - - var tmp = tmpDir(.{}); - defer tmp.cleanup(); + try testWithAllSupportedPathTypes(struct { + fn impl(ctx: *TestContext) !void { + const filename = try ctx.transformPath("file_nonblocking_lock_test.txt"); - const file1 = try tmp.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true }); - defer file1.close(); + const file1 = try ctx.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); + 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" { @@ -1110,42 +1248,44 @@ test "open file with exclusive lock twice, make sure second lock waits" { 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(); + 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{}; + var locked = std.Thread.ResetEvent{}; + + 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(); } - }; - - 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(); + }.impl); } test "open file with exclusive nonblocking lock twice (absolute paths)" { @@ -1261,29 +1401,36 @@ test "walker without fully iterating" { 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"); + try testWithAllSupportedPathTypes(struct { + fn impl(ctx: *TestContext) !void { + 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(update_path, "something"); + const prev_status = try ctx.dir.updateFile(file_path, ctx.dir, update_path, .{}); + try testing.expectEqual(fs.PrevStatus.stale, prev_status); + + try ctx.dir.deleteDir(subdir_path); + } + }.impl); } test ". and .. in absolute functions" { @@ -1339,17 +1486,17 @@ test "chmod" { const file = try tmp.dir.createFile("test_file", .{ .mode = 0o600 }); defer file.close(); - try testing.expect((try file.stat()).mode & 0o7777 == 0o600); + try testing.expectEqual(@as(File.Mode, 0o600), (try file.stat()).mode & 0o7777); try file.chmod(0o644); - try testing.expect((try file.stat()).mode & 0o7777 == 0o644); + try testing.expectEqual(@as(File.Mode, 0o644), (try file.stat()).mode & 0o7777); try tmp.dir.makeDir("test_dir"); var iterable_dir = try tmp.dir.openIterableDir("test_dir", .{}); defer iterable_dir.close(); try iterable_dir.chmod(0o700); - try testing.expect((try iterable_dir.dir.stat()).mode & 0o7777 == 0o700); + try testing.expectEqual(@as(File.Mode, 0o700), (try iterable_dir.dir.stat()).mode & 0o7777); } test "chown" { @@ -1378,8 +1525,8 @@ test "File.Metadata" { defer file.close(); const metadata = try file.metadata(); - try testing.expect(metadata.kind() == .file); - try testing.expect(metadata.size() == 0); + try testing.expectEqual(File.Kind.file, metadata.kind()); + try testing.expectEqual(@as(u64, 0), metadata.size()); _ = metadata.accessed(); _ = metadata.modified(); _ = metadata.created(); diff --git a/lib/std/os.zig b/lib/std/os.zig index 0a1bcc9ac1e0..43aa6a9b5296 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -5169,11 +5169,30 @@ pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPat return getFdPath(h_file, out_buffer); } +pub fn isGetFdPathSupportedOnTarget(os: std.Target.Os) bool { + return switch (os.tag) { + // zig fmt: off + .windows, + .macos, .ios, .watchos, .tvos, + .linux, + .solaris, + .freebsd, + => true, + // zig fmt: on + .dragonfly => os.version_range.semver.max.order(.{ .major = 6, .minor = 0, .patch = 0 }) != .lt, + .netbsd => os.version_range.semver.max.order(.{ .major = 10, .minor = 0, .patch = 0 }) != .lt, + else => false, + }; +} + /// Return canonical path of handle `fd`. /// This function is very host-specific and is not universally supported by all hosts. /// For example, while it generally works on Linux, macOS, FreeBSD or Windows, it is /// unsupported on WASI. pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { + if (!comptime isGetFdPathSupportedOnTarget(builtin.os)) { + @compileError("querying for canonical path of a handle is unsupported on this host"); + } switch (builtin.os.tag) { .windows => { var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined; @@ -5276,9 +5295,6 @@ pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { } }, .dragonfly => { - if (comptime builtin.os.version_range.semver.max.order(.{ .major = 6, .minor = 0, .patch = 0 }) == .lt) { - @compileError("querying for canonical path of a handle is unsupported on this host"); - } @memset(out_buffer[0..MAX_PATH_BYTES], 0); switch (errno(system.fcntl(fd, F.GETPATH, out_buffer))) { .SUCCESS => {}, @@ -5290,9 +5306,6 @@ pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { return out_buffer[0..len]; }, .netbsd => { - if (comptime builtin.os.version_range.semver.max.order(.{ .major = 10, .minor = 0, .patch = 0 }) == .lt) { - @compileError("querying for canonical path of a handle is unsupported on this host"); - } @memset(out_buffer[0..MAX_PATH_BYTES], 0); switch (errno(system.fcntl(fd, F.GETPATH, out_buffer))) { .SUCCESS => {}, @@ -5306,7 +5319,7 @@ pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { const len = mem.indexOfScalar(u8, out_buffer[0..], @as(u8, 0)) orelse MAX_PATH_BYTES; return out_buffer[0..len]; }, - else => @compileError("querying for canonical path of a handle is unsupported on this host"), + else => unreachable, // made unreachable by isGetFdPathSupportedOnTarget above } } diff --git a/lib/std/os/test.zig b/lib/std/os/test.zig index d5451f64ac61..b9ea4f2e2df4 100644 --- a/lib/std/os/test.zig +++ b/lib/std/os/test.zig @@ -193,7 +193,7 @@ test "symlink with relative paths" { os.windows.CreateSymbolicLink( cwd.fd, &[_]u16{ 's', 'y', 'm', 'l', 'i', 'n', 'k', 'e', 'd' }, - &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, + &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, false, ) catch |err| switch (err) { // Symlink requires admin privileges on windows, so this test can legitimately fail. @@ -351,7 +351,7 @@ test "readlinkat" { os.windows.CreateSymbolicLink( tmp.dir.fd, &[_]u16{ 'l', 'i', 'n', 'k' }, - &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, + &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' }, false, ) catch |err| switch (err) { // Symlink requires admin privileges on windows, so this test can legitimately fail. diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 3340087556ed..d0ea558a45c0 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -704,6 +704,7 @@ pub const CreateSymbolicLinkError = error{ NameTooLong, NoDevice, NetworkNotFound, + BadPathName, Unexpected, }; @@ -716,7 +717,7 @@ pub const CreateSymbolicLinkError = error{ pub fn CreateSymbolicLink( dir: ?HANDLE, sym_link_path: []const u16, - target_path: []const u16, + target_path: [:0]const u16, is_directory: bool, ) CreateSymbolicLinkError!void { const SYMLINK_DATA = extern struct { @@ -745,25 +746,58 @@ pub fn CreateSymbolicLink( }; defer CloseHandle(symlink_handle); + // Relevant portions of the documentation: + // > Relative links are specified using the following conventions: + // > - Root relative—for example, "\Windows\System32" resolves to "current drive:\Windows\System32". + // > - Current working directory–relative—for example, if the current working directory is + // > C:\Windows\System32, "C:File.txt" resolves to "C:\Windows\System32\File.txt". + // > Note: If you specify a current working directory–relative link, it is created as an absolute + // > link, due to the way the current working directory is processed based on the user and the thread. + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw + var is_target_absolute = false; + const final_target_path = target_path: { + switch (getNamespacePrefix(u16, target_path)) { + .none => switch (getUnprefixedPathType(u16, target_path)) { + // Rooted paths need to avoid getting put through wToPrefixedFileW + // (and they are treated as relative in this context) + // Note: It seems that rooted paths in symbolic links are relative to + // the drive that the symbolic exists on, not to the CWD's drive. + // So, if the symlink is on C:\ and the CWD is on D:\, + // it will still resolve the path relative to the root of + // the C:\ drive. + .rooted => break :target_path target_path, + else => {}, + }, + // Already an NT path, no need to do anything to it + .nt => break :target_path target_path, + else => {}, + } + var prefixed_target_path = try wToPrefixedFileW(dir, target_path); + // We do this after prefixing to ensure that drive-relative paths are treated as absolute + is_target_absolute = std.fs.path.isAbsoluteWindowsWTF16(prefixed_target_path.span()); + break :target_path prefixed_target_path.span(); + }; + // prepare reparse data buffer var buffer: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; - const buf_len = @sizeOf(SYMLINK_DATA) + target_path.len * 4; + const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4; const header_len = @sizeOf(ULONG) + @sizeOf(USHORT) * 2; + const target_is_absolute = std.fs.path.isAbsoluteWindowsWTF16(final_target_path); const symlink_data = SYMLINK_DATA{ .ReparseTag = IO_REPARSE_TAG_SYMLINK, .ReparseDataLength = @as(u16, @intCast(buf_len - header_len)), .Reserved = 0, - .SubstituteNameOffset = @as(u16, @intCast(target_path.len * 2)), - .SubstituteNameLength = @as(u16, @intCast(target_path.len * 2)), + .SubstituteNameOffset = @as(u16, @intCast(final_target_path.len * 2)), + .SubstituteNameLength = @as(u16, @intCast(final_target_path.len * 2)), .PrintNameOffset = 0, - .PrintNameLength = @as(u16, @intCast(target_path.len * 2)), - .Flags = if (dir) |_| SYMLINK_FLAG_RELATIVE else 0, + .PrintNameLength = @as(u16, @intCast(final_target_path.len * 2)), + .Flags = if (!target_is_absolute) SYMLINK_FLAG_RELATIVE else 0, }; @memcpy(buffer[0..@sizeOf(SYMLINK_DATA)], std.mem.asBytes(&symlink_data)); - @memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. target_path.len * 2], @as([*]const u8, @ptrCast(target_path))); - const paths_start = @sizeOf(SYMLINK_DATA) + target_path.len * 2; - @memcpy(buffer[paths_start..][0 .. target_path.len * 2], @as([*]const u8, @ptrCast(target_path))); + @memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path))); + const paths_start = @sizeOf(SYMLINK_DATA) + final_target_path.len * 2; + @memcpy(buffer[paths_start..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path))); _ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null); } @@ -861,12 +895,15 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin } fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 { - const prefix = [_]u16{ '\\', '?', '?', '\\' }; - var start_index: usize = 0; - if (!is_relative and std.mem.startsWith(u16, path, &prefix)) { - start_index = prefix.len; - } - const out_len = std.unicode.utf16leToUtf8(out_buffer, path[start_index..]) catch unreachable; + const win32_namespace_path = path: { + if (is_relative) break :path path; + const win32_path = ntToWin32Namespace(path) catch |err| switch (err) { + error.NameTooLong => unreachable, + error.NotNtPath => break :path path, + }; + break :path win32_path.span(); + }; + const out_len = std.unicode.utf16leToUtf8(out_buffer, win32_namespace_path) catch unreachable; return out_buffer[0..out_len]; } @@ -1189,8 +1226,18 @@ pub fn GetFinalPathNameByHandle( const file_path_begin_index = mem.indexOfPos(u16, final_path, expected_prefix.len, &[_]u16{'\\'}) orelse unreachable; const volume_name_u16 = final_path[0..file_path_begin_index]; + const device_name_u16 = volume_name_u16[expected_prefix.len..]; const file_name_u16 = final_path[file_path_begin_index..]; + // MUP is Multiple UNC Provider, and indicates that the path is a UNC + // path. In this case, the canonical UNC path can be gotten by just + // dropping the \Device\Mup\ and making sure the path begins with \\ + if (mem.eql(u16, device_name_u16, std.unicode.utf8ToUtf16LeStringLiteral("Mup"))) { + out_buffer[0] = '\\'; + mem.copyForwards(u16, out_buffer[1..][0..file_name_u16.len], file_name_u16); + return out_buffer[0 .. 1 + file_name_u16.len]; + } + // Get DOS volume name. DOS volume names are actually symbolic link objects to the // actual NT volume. For example: // (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C: @@ -2296,25 +2343,26 @@ pub const NamespacePrefix = enum { nt, }; +/// If `T` is `u16`, then `path` should be encoded as UTF-16LE. pub fn getNamespacePrefix(comptime T: type, path: []const T) NamespacePrefix { if (path.len < 4) return .none; - var all_backslash = switch (path[0]) { + var all_backslash = switch (mem.littleToNative(T, path[0])) { '\\' => true, '/' => false, else => return .none, }; - all_backslash = all_backslash and switch (path[3]) { + all_backslash = all_backslash and switch (mem.littleToNative(T, path[3])) { '\\' => true, '/' => false, else => return .none, }; - switch (path[1]) { - '?' => if (path[2] == '?' and all_backslash) return .nt else return .none, + switch (mem.littleToNative(T, path[1])) { + '?' => if (mem.littleToNative(T, path[2]) == '?' and all_backslash) return .nt else return .none, '\\' => {}, '/' => all_backslash = false, else => return .none, } - return switch (path[2]) { + return switch (mem.littleToNative(T, path[2])) { '?' => if (all_backslash) .verbatim else .fake_verbatim, '.' => .local_device, else => .none, @@ -2349,6 +2397,7 @@ pub const UnprefixedPathType = enum { /// Get the path type of a path that is known to not have any namespace prefixes /// (`\\?\`, `\\.\`, `\??\`). +/// If `T` is `u16`, then `path` should be encoded as UTF-16LE. pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathType { if (path.len < 1) return .relative; @@ -2357,18 +2406,18 @@ pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathTy } const windows_path = std.fs.path.PathType.windows; - if (windows_path.isSep(T, path[0])) { + if (windows_path.isSep(T, mem.littleToNative(T, path[0]))) { // \x - if (path.len < 2 or !windows_path.isSep(T, path[1])) return .rooted; + if (path.len < 2 or !windows_path.isSep(T, mem.littleToNative(T, path[1]))) return .rooted; // exactly \\. or \\? with nothing trailing - if (path.len == 3 and (path[2] == '.' or path[2] == '?')) return .root_local_device; + if (path.len == 3 and (mem.littleToNative(T, path[2]) == '.' or mem.littleToNative(T, path[2]) == '?')) return .root_local_device; // \\x return .unc_absolute; } else { // x - if (path.len < 2 or path[1] != ':') return .relative; + if (path.len < 2 or mem.littleToNative(T, path[1]) != ':') return .relative; // x:\ - if (path.len > 2 and windows_path.isSep(T, path[2])) return .drive_absolute; + if (path.len > 2 and windows_path.isSep(T, mem.littleToNative(T, path[2]))) return .drive_absolute; // x: return .drive_relative; } @@ -2393,6 +2442,73 @@ test getUnprefixedPathType { try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c")); } +/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation. +/// The possible transformations are: +/// \??\C:\Some\Path -> C:\Some\Path +/// \??\UNC\server\share\foo -> \\server\share\foo +/// If the path does not have the NT namespace prefix, then `error.NotNtPath` is returned. +/// +/// Functionality is based on the ReactOS test cases found here: +/// https://github.com/reactos/reactos/blob/master/modules/rostests/apitests/ntdll/RtlNtPathNameToDosPathName.c +/// +/// `path` should be encoded as UTF-16LE. +pub fn ntToWin32Namespace(path: []const u16) !PathSpace { + if (path.len > PATH_MAX_WIDE) return error.NameTooLong; + + var path_space: PathSpace = undefined; + const namespace_prefix = getNamespacePrefix(u16, path); + switch (namespace_prefix) { + .nt => { + var dest_index: usize = 0; + var after_prefix = path[4..]; // after the `\??\` + // The prefix \??\UNC\ means this is a UNC path, in which case the + // `\??\UNC\` should be replaced by `\\` (two backslashes) + // TODO: the "UNC" should technically be matched case-insensitively, but + // it's unlikely to matter since most/all paths passed into this + // function will have come from the OS meaning it should have + // the 'canonical' uppercase UNC. + const is_unc = after_prefix.len >= 4 and + std.mem.eql(u16, after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and + std.fs.path.PathType.windows.isSep(u16, std.mem.littleToNative(u16, after_prefix[3])); + if (is_unc) { + path_space.data[0] = comptime std.mem.nativeToLittle(u16, '\\'); + dest_index += 1; + // We want to include the last `\` of `\??\UNC\` + after_prefix = path[7..]; + } + @memcpy(path_space.data[dest_index..][0..after_prefix.len], after_prefix); + path_space.len = dest_index + after_prefix.len; + path_space.data[path_space.len] = 0; + return path_space; + }, + else => return error.NotNtPath, + } +} + +test "ntToWin32Namespace" { + const L = std.unicode.utf8ToUtf16LeStringLiteral; + + try testNtToWin32Namespace(L("UNC"), L("\\??\\UNC")); + try testNtToWin32Namespace(L("\\\\"), L("\\??\\UNC\\")); + try testNtToWin32Namespace(L("\\\\path1"), L("\\??\\UNC\\path1")); + try testNtToWin32Namespace(L("\\\\path1\\path2"), L("\\??\\UNC\\path1\\path2")); + + try testNtToWin32Namespace(L(""), L("\\??\\")); + try testNtToWin32Namespace(L("C:"), L("\\??\\C:")); + try testNtToWin32Namespace(L("C:\\"), L("\\??\\C:\\")); + try testNtToWin32Namespace(L("C:\\test"), L("\\??\\C:\\test")); + try testNtToWin32Namespace(L("C:\\test\\"), L("\\??\\C:\\test\\")); + + try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("foo"))); + try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("C:\\test"))); + try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("\\\\.\\test"))); +} + +fn testNtToWin32Namespace(expected: []const u16, path: []const u16) !void { + const converted = try ntToWin32Namespace(path); + try std.testing.expectEqualSlices(u16, expected, converted.span()); +} + fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize { const result = kernel32.GetFullPathNameW(path, @as(u32, @intCast(out.len)), out.ptr, null); if (result == 0) {