authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-08-17 12:15:51-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-08-17 12:15:51-07:00
log8c1329b222ab620d7388d766e9e558baa502ce93
tree0eff94eeb94ce59d696ecb2f363411caa9e41d7c
parent5ae5dc507b10116f90f1e1491d70566f99ac0981
parent2b44961a208341e55969cca959608d80adcf3e0e
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #16847 from squeek502/fs-fixes

`std.fs`: Improve tests and fix some bugs that were uncovered

5 files changed, 736 insertions(+), 451 deletions(-)

lib/std/fs.zig+11-2
...@@ -1949,7 +1949,13 @@ pub const Dir = struct {...@@ -1949,7 +1949,13 @@ pub const Dir = struct {
1949 return self.symLinkWasi(target_path, sym_link_path, flags);1949 return self.symLinkWasi(target_path, sym_link_path, flags);
1950 }1950 }
1951 if (builtin.os.tag == .windows) {1951 if (builtin.os.tag == .windows) {
1952 const target_path_w = try os.windows.sliceToPrefixedFileW(self.fd, target_path);1952 // Target path does not use sliceToPrefixedFileW because certain paths
1953 // are handled differently when creating a symlink than they would be
1954 // when converting to an NT namespaced path. CreateSymbolicLink in
1955 // symLinkW will handle the necessary conversion.
1956 var target_path_w: os.windows.PathSpace = undefined;
1957 target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path);
1958 target_path_w.data[target_path_w.len] = 0;
1953 const sym_link_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sym_link_path);1959 const sym_link_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sym_link_path);
1954 return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags);1960 return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags);
1955 }1961 }
...@@ -1987,7 +1993,10 @@ pub const Dir = struct {...@@ -1987,7 +1993,10 @@ pub const Dir = struct {
1987 /// are null-terminated, WTF16 encoded.1993 /// are null-terminated, WTF16 encoded.
1988 pub fn symLinkW(1994 pub fn symLinkW(
1989 self: Dir,1995 self: Dir,
1990 target_path_w: []const u16,1996 /// WTF-16, does not need to be NT-prefixed. The NT-prefixing
1997 /// of this path is handled by CreateSymbolicLink.
1998 target_path_w: [:0]const u16,
1999 /// WTF-16, must be NT-prefixed or relative
1991 sym_link_path_w: []const u16,2000 sym_link_path_w: []const u16,
1992 flags: SymLinkFlags,2001 flags: SymLinkFlags,
1993 ) !void {2002 ) !void {
lib/std/fs/test.zig+562-415
...@@ -13,38 +13,167 @@ const File = std.fs.File;...@@ -13,38 +13,167 @@ const File = std.fs.File;
13const tmpDir = testing.tmpDir;13const tmpDir = testing.tmpDir;
14const tmpIterableDir = testing.tmpIterableDir;14const tmpIterableDir = testing.tmpIterableDir;
1515
16test "Dir.readLink" {16const PathType = enum {
17 var tmp = tmpDir(.{});17 relative,
18 defer tmp.cleanup();18 absolute,
1919 unc,
20 // Create some targets20
21 try tmp.dir.writeFile("file.txt", "nonsense");21 pub fn isSupported(self: PathType, target_os: std.Target.Os) bool {
22 try tmp.dir.makeDir("subdir");22 return switch (self) {
23 .relative => true,
24 .absolute => std.os.isGetFdPathSupportedOnTarget(target_os),
25 .unc => target_os.tag == .windows,
26 };
27 }
2328
24 {29 pub const TransformError = std.os.RealPathError || error{OutOfMemory};
25 // Create symbolic link by path30 pub const TransformFn = fn (allocator: mem.Allocator, dir: Dir, relative_path: []const u8) TransformError![]const u8;
26 tmp.dir.symLink("file.txt", "symlink1", .{}) catch |err| switch (err) {31
27 // Symlink requires admin privileges on windows, so this test can legitimately fail.32 pub fn getTransformFn(comptime path_type: PathType) TransformFn {
28 error.AccessDenied => return error.SkipZigTest,33 switch (path_type) {
29 else => return err,34 .relative => return struct {
35 fn transform(allocator: mem.Allocator, dir: Dir, relative_path: []const u8) TransformError![]const u8 {
36 _ = allocator;
37 _ = dir;
38 return relative_path;
39 }
40 }.transform,
41 .absolute => return struct {
42 fn transform(allocator: mem.Allocator, dir: Dir, relative_path: []const u8) TransformError![]const u8 {
43 // The final path may not actually exist which would cause realpath to fail.
44 // So instead, we get the path of the dir and join it with the relative path.
45 var fd_path_buf: [fs.MAX_PATH_BYTES]u8 = undefined;
46 const dir_path = try os.getFdPath(dir.fd, &fd_path_buf);
47 return fs.path.join(allocator, &.{ dir_path, relative_path });
48 }
49 }.transform,
50 .unc => return struct {
51 fn transform(allocator: mem.Allocator, dir: Dir, relative_path: []const u8) TransformError![]const u8 {
52 // Any drive absolute path (C:\foo) can be converted into a UNC path by
53 // using 'localhost' as the server name and '<drive letter>$' as the share name.
54 var fd_path_buf: [fs.MAX_PATH_BYTES]u8 = undefined;
55 const dir_path = try os.getFdPath(dir.fd, &fd_path_buf);
56 const windows_path_type = std.os.windows.getUnprefixedPathType(u8, dir_path);
57 switch (windows_path_type) {
58 .unc_absolute => return fs.path.join(allocator, &.{ dir_path, relative_path }),
59 .drive_absolute => {
60 // `C:\<...>` -> `\\localhost\C$\<...>`
61 const prepended = "\\\\localhost\\";
62 var path = try fs.path.join(allocator, &.{ prepended, dir_path, relative_path });
63 path[prepended.len + 1] = '$';
64 return path;
65 },
66 else => unreachable,
67 }
68 }
69 }.transform,
70 }
71 }
72};
73
74const TestContext = struct {
75 path_type: PathType,
76 arena: ArenaAllocator,
77 tmp: testing.TmpIterableDir,
78 dir: std.fs.Dir,
79 iterable_dir: std.fs.IterableDir,
80 transform_fn: *const PathType.TransformFn,
81
82 pub fn init(path_type: PathType, allocator: mem.Allocator, transform_fn: *const PathType.TransformFn) TestContext {
83 var tmp = tmpIterableDir(.{});
84 return .{
85 .path_type = path_type,
86 .arena = ArenaAllocator.init(allocator),
87 .tmp = tmp,
88 .dir = tmp.iterable_dir.dir,
89 .iterable_dir = tmp.iterable_dir,
90 .transform_fn = transform_fn,
30 };91 };
31 try testReadLink(tmp.dir, "file.txt", "symlink1");
32 }92 }
33 {93
34 // Create symbolic link by path94 pub fn deinit(self: *TestContext) void {
35 tmp.dir.symLink("subdir", "symlink2", .{ .is_directory = true }) catch |err| switch (err) {95 self.arena.deinit();
36 // Symlink requires admin privileges on windows, so this test can legitimately fail.96 self.tmp.cleanup();
37 error.AccessDenied => return error.SkipZigTest,97 }
38 else => return err,98
99 /// Returns the `relative_path` transformed into the TestContext's `path_type`.
100 /// The result is allocated by the TestContext's arena and will be free'd during
101 /// `TestContext.deinit`.
102 pub fn transformPath(self: *TestContext, relative_path: []const u8) ![]const u8 {
103 return self.transform_fn(self.arena.allocator(), self.dir, relative_path);
104 }
105};
106
107/// `test_func` must be a function that takes a `*TestContext` as a parameter and returns `!void`.
108/// `test_func` will be called once for each PathType that the current target supports,
109/// and will be passed a TestContext that can transform a relative path into the path type under test.
110/// The TestContext will also create a tmp directory for you (and will clean it up for you too).
111fn testWithAllSupportedPathTypes(test_func: anytype) !void {
112 inline for (@typeInfo(PathType).Enum.fields) |enum_field| {
113 const path_type = @field(PathType, enum_field.name);
114 if (!(comptime path_type.isSupported(builtin.os))) continue;
115
116 var ctx = TestContext.init(path_type, testing.allocator, path_type.getTransformFn());
117 defer ctx.deinit();
118
119 test_func(&ctx) catch |err| {
120 std.debug.print("path type: {s}\n", .{enum_field.name});
121 return err;
39 };122 };
40 try testReadLink(tmp.dir, "subdir", "symlink2");
41 }123 }
42}124}
43125
126test "Dir.readLink" {
127 try testWithAllSupportedPathTypes(struct {
128 fn impl(ctx: *TestContext) !void {
129 // Create some targets
130 const file_target_path = try ctx.transformPath("file.txt");
131 try ctx.dir.writeFile(file_target_path, "nonsense");
132 const dir_target_path = try ctx.transformPath("subdir");
133 try ctx.dir.makeDir(dir_target_path);
134
135 {
136 // Create symbolic link by path
137 ctx.dir.symLink(file_target_path, "symlink1", .{}) catch |err| switch (err) {
138 // Symlink requires admin privileges on windows, so this test can legitimately fail.
139 error.AccessDenied => return error.SkipZigTest,
140 else => return err,
141 };
142 try testReadLink(ctx.dir, file_target_path, "symlink1");
143 }
144 {
145 // Create symbolic link by path
146 ctx.dir.symLink(dir_target_path, "symlink2", .{ .is_directory = true }) catch |err| switch (err) {
147 // Symlink requires admin privileges on windows, so this test can legitimately fail.
148 error.AccessDenied => return error.SkipZigTest,
149 else => return err,
150 };
151 try testReadLink(ctx.dir, dir_target_path, "symlink2");
152 }
153 }
154 }.impl);
155}
156
44fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !void {157fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !void {
45 var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;158 var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;
46 const given = try dir.readLink(symlink_path, buffer[0..]);159 const given = try dir.readLink(symlink_path, buffer[0..]);
47 try testing.expect(mem.eql(u8, target_path, given));160 try testing.expectEqualStrings(target_path, given);
161}
162
163test "openDir" {
164 try testWithAllSupportedPathTypes(struct {
165 fn impl(ctx: *TestContext) !void {
166 const subdir_path = try ctx.transformPath("subdir");
167 try ctx.dir.makeDir(subdir_path);
168
169 for ([_][]const u8{ "", ".", ".." }) |sub_path| {
170 const dir_path = try fs.path.join(testing.allocator, &[_][]const u8{ subdir_path, sub_path });
171 defer testing.allocator.free(dir_path);
172 var dir = try ctx.dir.openDir(dir_path, .{});
173 defer dir.close();
174 }
175 }
176 }.impl);
48}177}
49178
50test "accessAbsolute" {179test "accessAbsolute" {
...@@ -174,7 +303,7 @@ test "readLinkAbsolute" {...@@ -174,7 +303,7 @@ test "readLinkAbsolute" {
174fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void {303fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void {
175 var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;304 var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;
176 const given = try fs.readLinkAbsolute(symlink_path, buffer[0..]);305 const given = try fs.readLinkAbsolute(symlink_path, buffer[0..]);
177 try testing.expect(mem.eql(u8, target_path, given));306 try testing.expectEqualStrings(target_path, given);
178}307}
179308
180test "Dir.Iterator" {309test "Dir.Iterator" {
...@@ -202,7 +331,7 @@ test "Dir.Iterator" {...@@ -202,7 +331,7 @@ test "Dir.Iterator" {
202 try entries.append(.{ .name = name, .kind = entry.kind });331 try entries.append(.{ .name = name, .kind = entry.kind });
203 }332 }
204333
205 try testing.expect(entries.items.len == 2); // note that the Iterator skips '.' and '..'334 try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
206 try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));335 try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
207 try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));336 try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
208}337}
...@@ -269,7 +398,7 @@ test "Dir.Iterator twice" {...@@ -269,7 +398,7 @@ test "Dir.Iterator twice" {
269 try entries.append(.{ .name = name, .kind = entry.kind });398 try entries.append(.{ .name = name, .kind = entry.kind });
270 }399 }
271400
272 try testing.expect(entries.items.len == 2); // note that the Iterator skips '.' and '..'401 try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
273 try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));402 try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
274 try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));403 try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
275 }404 }
...@@ -303,7 +432,7 @@ test "Dir.Iterator reset" {...@@ -303,7 +432,7 @@ test "Dir.Iterator reset" {
303 try entries.append(.{ .name = name, .kind = entry.kind });432 try entries.append(.{ .name = name, .kind = entry.kind });
304 }433 }
305434
306 try testing.expect(entries.items.len == 2); // note that the Iterator skips '.' and '..'435 try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
307 try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));436 try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
308 try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));437 try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
309438
...@@ -352,53 +481,59 @@ fn contains(entries: *const std.ArrayList(IterableDir.Entry), el: IterableDir.En...@@ -352,53 +481,59 @@ fn contains(entries: *const std.ArrayList(IterableDir.Entry), el: IterableDir.En
352}481}
353482
354test "Dir.realpath smoke test" {483test "Dir.realpath smoke test" {
355 switch (builtin.os.tag) {484 if (!comptime std.os.isGetFdPathSupportedOnTarget(builtin.os)) return error.SkipZigTest;
356 .linux, .windows, .macos, .ios, .watchos, .tvos, .solaris => {},485
357 else => return error.SkipZigTest,486 try testWithAllSupportedPathTypes(struct {
358 }487 fn impl(ctx: *TestContext) !void {
359488 const test_file_path = try ctx.transformPath("test_file");
360 var tmp_dir = tmpDir(.{});489 const test_dir_path = try ctx.transformPath("test_dir");
361 defer tmp_dir.cleanup();490 var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
362491
363 var file = try tmp_dir.dir.createFile("test_file", .{ .lock = .shared });492 // FileNotFound if the path doesn't exist
364 // We need to close the file immediately as otherwise on Windows we'll end up493 try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(testing.allocator, test_file_path));
365 // with a sharing violation.494 try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_file_path, &buf));
366 file.close();495 try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(testing.allocator, test_dir_path));
367496 try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_dir_path, &buf));
368 try tmp_dir.dir.makeDir("test_dir");497
369498 // Now create the file and dir
370 var arena = ArenaAllocator.init(testing.allocator);499 try ctx.dir.writeFile(test_file_path, "");
371 defer arena.deinit();500 try ctx.dir.makeDir(test_dir_path);
372 const allocator = arena.allocator();501
373502 const base_path = try ctx.transformPath(".");
374 const base_path = blk: {503 const base_realpath = try ctx.dir.realpathAlloc(testing.allocator, base_path);
375 const relative_path = try fs.path.join(allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..] });504 defer testing.allocator.free(base_realpath);
376 break :blk try fs.realpathAlloc(allocator, relative_path);505 const expected_file_path = try fs.path.join(
377 };506 testing.allocator,
378507 &[_][]const u8{ base_realpath, "test_file" },
379 // First, test non-alloc version508 );
380 {509 defer testing.allocator.free(expected_file_path);
381 var buf1: [fs.MAX_PATH_BYTES]u8 = undefined;510 const expected_dir_path = try fs.path.join(
382511 testing.allocator,
383 const file_path = try tmp_dir.dir.realpath("test_file", buf1[0..]);512 &[_][]const u8{ base_realpath, "test_dir" },
384 const expected_file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_file" });513 );
385 try testing.expectEqualStrings(expected_file_path, file_path);514 defer testing.allocator.free(expected_dir_path);
386515
387 const dir_path = try tmp_dir.dir.realpath("test_dir", buf1[0..]);516 // First, test non-alloc version
388 const expected_dir_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_dir" });517 {
389 try testing.expectEqualStrings(expected_dir_path, dir_path);518 const file_path = try ctx.dir.realpath(test_file_path, &buf);
390 }519 try testing.expectEqualStrings(expected_file_path, file_path);
391520
392 // Next, test alloc version521 const dir_path = try ctx.dir.realpath(test_dir_path, &buf);
393 {522 try testing.expectEqualStrings(expected_dir_path, dir_path);
394 const file_path = try tmp_dir.dir.realpathAlloc(allocator, "test_file");523 }
395 const expected_file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_file" });524
396 try testing.expectEqualStrings(expected_file_path, file_path);525 // Next, test alloc version
397526 {
398 const dir_path = try tmp_dir.dir.realpathAlloc(allocator, "test_dir");527 const file_path = try ctx.dir.realpathAlloc(testing.allocator, test_file_path);
399 const expected_dir_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_dir" });528 defer testing.allocator.free(file_path);
400 try testing.expectEqualStrings(expected_dir_path, dir_path);529 try testing.expectEqualStrings(expected_file_path, file_path);
401 }530
531 const dir_path = try ctx.dir.realpathAlloc(testing.allocator, test_dir_path);
532 defer testing.allocator.free(dir_path);
533 try testing.expectEqualStrings(expected_dir_path, dir_path);
534 }
535 }
536 }.impl);
402}537}
403538
404test "readAllAlloc" {539test "readAllAlloc" {
...@@ -410,7 +545,7 @@ test "readAllAlloc" {...@@ -410,7 +545,7 @@ test "readAllAlloc" {
410545
411 const buf1 = try file.readToEndAlloc(testing.allocator, 1024);546 const buf1 = try file.readToEndAlloc(testing.allocator, 1024);
412 defer testing.allocator.free(buf1);547 defer testing.allocator.free(buf1);
413 try testing.expect(buf1.len == 0);548 try testing.expectEqual(@as(usize, 0), buf1.len);
414549
415 const write_buf: []const u8 = "this is a test.\nthis is a test.\nthis is a test.\nthis is a test.\n";550 const write_buf: []const u8 = "this is a test.\nthis is a test.\nthis is a test.\nthis is a test.\n";
416 try file.writeAll(write_buf);551 try file.writeAll(write_buf);
...@@ -420,14 +555,14 @@ test "readAllAlloc" {...@@ -420,14 +555,14 @@ test "readAllAlloc" {
420 const buf2 = try file.readToEndAlloc(testing.allocator, 1024);555 const buf2 = try file.readToEndAlloc(testing.allocator, 1024);
421 defer testing.allocator.free(buf2);556 defer testing.allocator.free(buf2);
422 try testing.expectEqual(write_buf.len, buf2.len);557 try testing.expectEqual(write_buf.len, buf2.len);
423 try testing.expect(std.mem.eql(u8, write_buf, buf2));558 try testing.expectEqualStrings(write_buf, buf2);
424 try file.seekTo(0);559 try file.seekTo(0);
425560
426 // max_bytes == file_size561 // max_bytes == file_size
427 const buf3 = try file.readToEndAlloc(testing.allocator, write_buf.len);562 const buf3 = try file.readToEndAlloc(testing.allocator, write_buf.len);
428 defer testing.allocator.free(buf3);563 defer testing.allocator.free(buf3);
429 try testing.expectEqual(write_buf.len, buf3.len);564 try testing.expectEqual(write_buf.len, buf3.len);
430 try testing.expect(std.mem.eql(u8, write_buf, buf3));565 try testing.expectEqualStrings(write_buf, buf3);
431 try file.seekTo(0);566 try file.seekTo(0);
432567
433 // max_bytes < file_size568 // max_bytes < file_size
...@@ -435,211 +570,221 @@ test "readAllAlloc" {...@@ -435,211 +570,221 @@ test "readAllAlloc" {
435}570}
436571
437test "directory operations on files" {572test "directory operations on files" {
438 var tmp_dir = tmpDir(.{});573 try testWithAllSupportedPathTypes(struct {
439 defer tmp_dir.cleanup();574 fn impl(ctx: *TestContext) !void {
440575 const test_file_name = try ctx.transformPath("test_file");
441 const test_file_name = "test_file";576
442577 var file = try ctx.dir.createFile(test_file_name, .{ .read = true });
443 var file = try tmp_dir.dir.createFile(test_file_name, .{ .read = true });578 file.close();
444 file.close();579
445580 try testing.expectError(error.PathAlreadyExists, ctx.dir.makeDir(test_file_name));
446 try testing.expectError(error.PathAlreadyExists, tmp_dir.dir.makeDir(test_file_name));581 try testing.expectError(error.NotDir, ctx.dir.openDir(test_file_name, .{}));
447 try testing.expectError(error.NotDir, tmp_dir.dir.openDir(test_file_name, .{}));582 try testing.expectError(error.NotDir, ctx.dir.deleteDir(test_file_name));
448 try testing.expectError(error.NotDir, tmp_dir.dir.deleteDir(test_file_name));583
449584 if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
450 switch (builtin.os.tag) {585 try testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(test_file_name));
451 .wasi, .freebsd, .netbsd, .openbsd, .dragonfly => {},586 try testing.expectError(error.NotDir, fs.deleteDirAbsolute(test_file_name));
452 else => {587 }
453 const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_file_name);588
454 defer testing.allocator.free(absolute_path);589 // ensure the file still exists and is a file as a sanity check
455590 file = try ctx.dir.openFile(test_file_name, .{});
456 try testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(absolute_path));591 const stat = try file.stat();
457 try testing.expectError(error.NotDir, fs.deleteDirAbsolute(absolute_path));592 try testing.expectEqual(File.Kind.file, stat.kind);
458 },593 file.close();
459 }594 }
460595 }.impl);
461 // ensure the file still exists and is a file as a sanity check
462 file = try tmp_dir.dir.openFile(test_file_name, .{});
463 const stat = try file.stat();
464 try testing.expect(stat.kind == .file);
465 file.close();
466}596}
467597
468test "file operations on directories" {598test "file operations on directories" {
469 // TODO: fix this test on FreeBSD. https://github.com/ziglang/zig/issues/1759599 // TODO: fix this test on FreeBSD. https://github.com/ziglang/zig/issues/1759
470 if (builtin.os.tag == .freebsd) return error.SkipZigTest;600 if (builtin.os.tag == .freebsd) return error.SkipZigTest;
471601
472 var tmp_dir = tmpDir(.{});602 try testWithAllSupportedPathTypes(struct {
473 defer tmp_dir.cleanup();603 fn impl(ctx: *TestContext) !void {
474604 const test_dir_name = try ctx.transformPath("test_dir");
475 const test_dir_name = "test_dir";605
476606 try ctx.dir.makeDir(test_dir_name);
477 try tmp_dir.dir.makeDir(test_dir_name);607
478608 try testing.expectError(error.IsDir, ctx.dir.createFile(test_dir_name, .{}));
479 try testing.expectError(error.IsDir, tmp_dir.dir.createFile(test_dir_name, .{}));609 try testing.expectError(error.IsDir, ctx.dir.deleteFile(test_dir_name));
480 try testing.expectError(error.IsDir, tmp_dir.dir.deleteFile(test_dir_name));610 switch (builtin.os.tag) {
481 switch (builtin.os.tag) {611 // no error when reading a directory.
482 // no error when reading a directory.612 .dragonfly, .netbsd => {},
483 .dragonfly, .netbsd => {},613 // Currently, WASI will return error.Unexpected (via ENOTCAPABLE) when attempting fd_read on a directory handle.
484 // Currently, WASI will return error.Unexpected (via ENOTCAPABLE) when attempting fd_read on a directory handle.614 // TODO: Re-enable on WASI once https://github.com/bytecodealliance/wasmtime/issues/1935 is resolved.
485 // TODO: Re-enable on WASI once https://github.com/bytecodealliance/wasmtime/issues/1935 is resolved.615 .wasi => {},
486 .wasi => {},616 else => {
487 else => {617 try testing.expectError(error.IsDir, ctx.dir.readFileAlloc(testing.allocator, test_dir_name, std.math.maxInt(usize)));
488 try testing.expectError(error.IsDir, tmp_dir.dir.readFileAlloc(testing.allocator, test_dir_name, std.math.maxInt(usize)));618 },
489 },619 }
490 }620 // Note: The `.mode = .read_write` is necessary to ensure the error occurs on all platforms.
491 // Note: The `.mode = .read_write` is necessary to ensure the error occurs on all platforms.621 // TODO: Add a read-only test as well, see https://github.com/ziglang/zig/issues/5732
492 // TODO: Add a read-only test as well, see https://github.com/ziglang/zig/issues/5732622 try testing.expectError(error.IsDir, ctx.dir.openFile(test_dir_name, .{ .mode = .read_write }));
493 try testing.expectError(error.IsDir, tmp_dir.dir.openFile(test_dir_name, .{ .mode = .read_write }));623
494624 if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
495 switch (builtin.os.tag) {625 try testing.expectError(error.IsDir, fs.createFileAbsolute(test_dir_name, .{}));
496 .wasi, .freebsd, .netbsd, .openbsd, .dragonfly => {},626 try testing.expectError(error.IsDir, fs.deleteFileAbsolute(test_dir_name));
497 else => {627 }
498 const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_dir_name);628
499 defer testing.allocator.free(absolute_path);629 // ensure the directory still exists as a sanity check
500630 var dir = try ctx.dir.openDir(test_dir_name, .{});
501 try testing.expectError(error.IsDir, fs.createFileAbsolute(absolute_path, .{}));631 dir.close();
502 try testing.expectError(error.IsDir, fs.deleteFileAbsolute(absolute_path));632 }
503 },633 }.impl);
504 }
505
506 // ensure the directory still exists as a sanity check
507 var dir = try tmp_dir.dir.openDir(test_dir_name, .{});
508 dir.close();
509}634}
510635
511test "deleteDir" {636test "deleteDir" {
512 var tmp_dir = tmpDir(.{});637 try testWithAllSupportedPathTypes(struct {
513 defer tmp_dir.cleanup();638 fn impl(ctx: *TestContext) !void {
514639 const test_dir_path = try ctx.transformPath("test_dir");
515 // deleting a non-existent directory640 const test_file_path = try ctx.transformPath("test_dir" ++ std.fs.path.sep_str ++ "test_file");
516 try testing.expectError(error.FileNotFound, tmp_dir.dir.deleteDir("test_dir"));641
517642 // deleting a non-existent directory
518 var dir = try tmp_dir.dir.makeOpenPath("test_dir", .{});643 try testing.expectError(error.FileNotFound, ctx.dir.deleteDir(test_dir_path));
519 var file = try dir.createFile("test_file", .{});644
520 file.close();645 // deleting a non-empty directory
521 dir.close();646 try ctx.dir.makeDir(test_dir_path);
522647 try ctx.dir.writeFile(test_file_path, "");
523 // deleting a non-empty directory648 try testing.expectError(error.DirNotEmpty, ctx.dir.deleteDir(test_dir_path));
524 try testing.expectError(error.DirNotEmpty, tmp_dir.dir.deleteDir("test_dir"));649
525650 // deleting an empty directory
526 dir = try tmp_dir.dir.openDir("test_dir", .{});651 try ctx.dir.deleteFile(test_file_path);
527 try dir.deleteFile("test_file");652 try ctx.dir.deleteDir(test_dir_path);
528 dir.close();653 }
529654 }.impl);
530 // deleting an empty directory
531 try tmp_dir.dir.deleteDir("test_dir");
532}655}
533656
534test "Dir.rename files" {657test "Dir.rename files" {
535 var tmp_dir = tmpDir(.{});658 try testWithAllSupportedPathTypes(struct {
536 defer tmp_dir.cleanup();659 fn impl(ctx: *TestContext) !void {
537660 const missing_file_path = try ctx.transformPath("missing_file_name");
538 try testing.expectError(error.FileNotFound, tmp_dir.dir.rename("missing_file_name", "something_else"));661 const something_else_path = try ctx.transformPath("something_else");
539662
540 // Renaming files663 try testing.expectError(error.FileNotFound, ctx.dir.rename(missing_file_path, something_else_path));
541 const test_file_name = "test_file";664
542 const renamed_test_file_name = "test_file_renamed";665 // Renaming files
543 var file = try tmp_dir.dir.createFile(test_file_name, .{ .read = true });666 const test_file_name = try ctx.transformPath("test_file");
544 file.close();667 const renamed_test_file_name = try ctx.transformPath("test_file_renamed");
545 try tmp_dir.dir.rename(test_file_name, renamed_test_file_name);668 var file = try ctx.dir.createFile(test_file_name, .{ .read = true });
546669 file.close();
547 // Ensure the file was renamed670 try ctx.dir.rename(test_file_name, renamed_test_file_name);
548 try testing.expectError(error.FileNotFound, tmp_dir.dir.openFile(test_file_name, .{}));671
549 file = try tmp_dir.dir.openFile(renamed_test_file_name, .{});672 // Ensure the file was renamed
550 file.close();673 try testing.expectError(error.FileNotFound, ctx.dir.openFile(test_file_name, .{}));
551674 file = try ctx.dir.openFile(renamed_test_file_name, .{});
552 // Rename to self succeeds675 file.close();
553 try tmp_dir.dir.rename(renamed_test_file_name, renamed_test_file_name);676
554677 // Rename to self succeeds
555 // Rename to existing file succeeds678 try ctx.dir.rename(renamed_test_file_name, renamed_test_file_name);
556 var existing_file = try tmp_dir.dir.createFile("existing_file", .{ .read = true });679
557 existing_file.close();680 // Rename to existing file succeeds
558 try tmp_dir.dir.rename(renamed_test_file_name, "existing_file");681 const existing_file_path = try ctx.transformPath("existing_file");
559682 var existing_file = try ctx.dir.createFile(existing_file_path, .{ .read = true });
560 try testing.expectError(error.FileNotFound, tmp_dir.dir.openFile(renamed_test_file_name, .{}));683 existing_file.close();
561 file = try tmp_dir.dir.openFile("existing_file", .{});684 try ctx.dir.rename(renamed_test_file_name, existing_file_path);
562 file.close();685
686 try testing.expectError(error.FileNotFound, ctx.dir.openFile(renamed_test_file_name, .{}));
687 file = try ctx.dir.openFile(existing_file_path, .{});
688 file.close();
689 }
690 }.impl);
563}691}
564692
565test "Dir.rename directories" {693test "Dir.rename directories" {
566 var tmp_dir = tmpDir(.{});694 try testWithAllSupportedPathTypes(struct {
567 defer tmp_dir.cleanup();695 fn impl(ctx: *TestContext) !void {
568696 const test_dir_path = try ctx.transformPath("test_dir");
569 // Renaming directories697 const test_dir_renamed_path = try ctx.transformPath("test_dir_renamed");
570 try tmp_dir.dir.makeDir("test_dir");698
571 try tmp_dir.dir.rename("test_dir", "test_dir_renamed");699 // Renaming directories
572700 try ctx.dir.makeDir(test_dir_path);
573 // Ensure the directory was renamed701 try ctx.dir.rename(test_dir_path, test_dir_renamed_path);
574 try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir("test_dir", .{}));702
575 var dir = try tmp_dir.dir.openDir("test_dir_renamed", .{});703 // Ensure the directory was renamed
576704 try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_path, .{}));
577 // Put a file in the directory705 var dir = try ctx.dir.openDir(test_dir_renamed_path, .{});
578 var file = try dir.createFile("test_file", .{ .read = true });706
579 file.close();707 // Put a file in the directory
580 dir.close();708 var file = try dir.createFile("test_file", .{ .read = true });
581709 file.close();
582 try tmp_dir.dir.rename("test_dir_renamed", "test_dir_renamed_again");710 dir.close();
583711
584 // Ensure the directory was renamed and the file still exists in it712 const test_dir_renamed_again_path = try ctx.transformPath("test_dir_renamed_again");
585 try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir("test_dir_renamed", .{}));713 try ctx.dir.rename(test_dir_renamed_path, test_dir_renamed_again_path);
586 dir = try tmp_dir.dir.openDir("test_dir_renamed_again", .{});714
587 file = try dir.openFile("test_file", .{});715 // Ensure the directory was renamed and the file still exists in it
588 file.close();716 try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_renamed_path, .{}));
589 dir.close();717 dir = try ctx.dir.openDir(test_dir_renamed_again_path, .{});
718 file = try dir.openFile("test_file", .{});
719 file.close();
720 dir.close();
721 }
722 }.impl);
590}723}
591724
592test "Dir.rename directory onto empty dir" {725test "Dir.rename directory onto empty dir" {
593 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364726 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
594 if (builtin.os.tag == .windows) return error.SkipZigTest;727 if (builtin.os.tag == .windows) return error.SkipZigTest;
595728
596 var tmp_dir = testing.tmpDir(.{});729 try testWithAllSupportedPathTypes(struct {
597 defer tmp_dir.cleanup();730 fn impl(ctx: *TestContext) !void {
731 const test_dir_path = try ctx.transformPath("test_dir");
732 const target_dir_path = try ctx.transformPath("target_dir_path");
598733
599 try tmp_dir.dir.makeDir("test_dir");734 try ctx.dir.makeDir(test_dir_path);
600 try tmp_dir.dir.makeDir("target_dir");735 try ctx.dir.makeDir(target_dir_path);
601 try tmp_dir.dir.rename("test_dir", "target_dir");736 try ctx.dir.rename(test_dir_path, target_dir_path);
602737
603 // Ensure the directory was renamed738 // Ensure the directory was renamed
604 try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir("test_dir", .{}));739 try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_path, .{}));
605 var dir = try tmp_dir.dir.openDir("target_dir", .{});740 var dir = try ctx.dir.openDir(target_dir_path, .{});
606 dir.close();741 dir.close();
742 }
743 }.impl);
607}744}
608745
609test "Dir.rename directory onto non-empty dir" {746test "Dir.rename directory onto non-empty dir" {
610 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364747 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
611 if (builtin.os.tag == .windows) return error.SkipZigTest;748 if (builtin.os.tag == .windows) return error.SkipZigTest;
612749
613 var tmp_dir = testing.tmpDir(.{});750 try testWithAllSupportedPathTypes(struct {
614 defer tmp_dir.cleanup();751 fn impl(ctx: *TestContext) !void {
752 const test_dir_path = try ctx.transformPath("test_dir");
753 const target_dir_path = try ctx.transformPath("target_dir_path");
615754
616 try tmp_dir.dir.makeDir("test_dir");755 try ctx.dir.makeDir(test_dir_path);
617756
618 var target_dir = try tmp_dir.dir.makeOpenPath("target_dir", .{});757 var target_dir = try ctx.dir.makeOpenPath(target_dir_path, .{});
619 var file = try target_dir.createFile("test_file", .{ .read = true });758 var file = try target_dir.createFile("test_file", .{ .read = true });
620 file.close();759 file.close();
621 target_dir.close();760 target_dir.close();
622761
623 // Rename should fail with PathAlreadyExists if target_dir is non-empty762 // Rename should fail with PathAlreadyExists if target_dir is non-empty
624 try testing.expectError(error.PathAlreadyExists, tmp_dir.dir.rename("test_dir", "target_dir"));763 try testing.expectError(error.PathAlreadyExists, ctx.dir.rename(test_dir_path, target_dir_path));
625764
626 // Ensure the directory was not renamed765 // Ensure the directory was not renamed
627 var dir = try tmp_dir.dir.openDir("test_dir", .{});766 var dir = try ctx.dir.openDir(test_dir_path, .{});
628 dir.close();767 dir.close();
768 }
769 }.impl);
629}770}
630771
631test "Dir.rename file <-> dir" {772test "Dir.rename file <-> dir" {
632 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364773 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
633 if (builtin.os.tag == .windows) return error.SkipZigTest;774 if (builtin.os.tag == .windows) return error.SkipZigTest;
634775
635 var tmp_dir = tmpDir(.{});776 try testWithAllSupportedPathTypes(struct {
636 defer tmp_dir.cleanup();777 fn impl(ctx: *TestContext) !void {
778 const test_file_path = try ctx.transformPath("test_file");
779 const test_dir_path = try ctx.transformPath("test_dir");
637780
638 var file = try tmp_dir.dir.createFile("test_file", .{ .read = true });781 var file = try ctx.dir.createFile(test_file_path, .{ .read = true });
639 file.close();782 file.close();
640 try tmp_dir.dir.makeDir("test_dir");783 try ctx.dir.makeDir(test_dir_path);
641 try testing.expectError(error.IsDir, tmp_dir.dir.rename("test_file", "test_dir"));784 try testing.expectError(error.IsDir, ctx.dir.rename(test_file_path, test_dir_path));
642 try testing.expectError(error.NotDir, tmp_dir.dir.rename("test_dir", "test_file"));785 try testing.expectError(error.NotDir, ctx.dir.rename(test_dir_path, test_file_path));
786 }
787 }.impl);
643}788}
644789
645test "rename" {790test "rename" {
...@@ -697,7 +842,7 @@ test "renameAbsolute" {...@@ -697,7 +842,7 @@ test "renameAbsolute" {
697 try testing.expectError(error.FileNotFound, tmp_dir.dir.openFile(test_file_name, .{}));842 try testing.expectError(error.FileNotFound, tmp_dir.dir.openFile(test_file_name, .{}));
698 file = try tmp_dir.dir.openFile(renamed_test_file_name, .{});843 file = try tmp_dir.dir.openFile(renamed_test_file_name, .{});
699 const stat = try file.stat();844 const stat = try file.stat();
700 try testing.expect(stat.kind == .file);845 try testing.expectEqual(File.Kind.file, stat.kind);
701 file.close();846 file.close();
702847
703 // Renaming directories848 // Renaming directories
...@@ -723,35 +868,33 @@ test "openSelfExe" {...@@ -723,35 +868,33 @@ test "openSelfExe" {
723}868}
724869
725test "makePath, put some files in it, deleteTree" {870test "makePath, put some files in it, deleteTree" {
726 var tmp = tmpDir(.{});871 try testWithAllSupportedPathTypes(struct {
727 defer tmp.cleanup();872 fn impl(ctx: *TestContext) !void {
873 const dir_path = try ctx.transformPath("os_test_tmp");
728874
729 try tmp.dir.makePath("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c");875 try ctx.dir.makePath("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c");
730 try tmp.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c" ++ fs.path.sep_str ++ "file.txt", "nonsense");876 try ctx.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c" ++ fs.path.sep_str ++ "file.txt", "nonsense");
731 try tmp.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "file2.txt", "blah");877 try ctx.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "file2.txt", "blah");
732 try tmp.dir.deleteTree("os_test_tmp");878
733 if (tmp.dir.openDir("os_test_tmp", .{})) |dir| {879 try ctx.dir.deleteTree(dir_path);
734 _ = dir;880 try testing.expectError(error.FileNotFound, ctx.dir.openDir(dir_path, .{}));
735 @panic("expected error");881 }
736 } else |err| {882 }.impl);
737 try testing.expect(err == error.FileNotFound);
738 }
739}883}
740884
741test "makePath, put some files in it, deleteTreeMinStackSize" {885test "makePath, put some files in it, deleteTreeMinStackSize" {
742 var tmp = tmpDir(.{});886 try testWithAllSupportedPathTypes(struct {
743 defer tmp.cleanup();887 fn impl(ctx: *TestContext) !void {
888 const dir_path = try ctx.transformPath("os_test_tmp");
744889
745 try tmp.dir.makePath("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c");890 try ctx.dir.makePath("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c");
746 try tmp.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c" ++ fs.path.sep_str ++ "file.txt", "nonsense");891 try ctx.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c" ++ fs.path.sep_str ++ "file.txt", "nonsense");
747 try tmp.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "file2.txt", "blah");892 try ctx.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "file2.txt", "blah");
748 try tmp.dir.deleteTreeMinStackSize("os_test_tmp");893
749 if (tmp.dir.openDir("os_test_tmp", .{})) |dir| {894 try ctx.dir.deleteTreeMinStackSize(dir_path);
750 _ = dir;895 try testing.expectError(error.FileNotFound, ctx.dir.openDir(dir_path, .{}));
751 @panic("expected error");896 }
752 } else |err| {897 }.impl);
753 try testing.expect(err == error.FileNotFound);
754 }
755}898}
756899
757test "makePath in a directory that no longer exists" {900test "makePath in a directory that no longer exists" {
...@@ -792,9 +935,9 @@ test "max file name component lengths" {...@@ -792,9 +935,9 @@ test "max file name component lengths" {
792 defer tmp.cleanup();935 defer tmp.cleanup();
793936
794 if (builtin.os.tag == .windows) {937 if (builtin.os.tag == .windows) {
795 // € is the character with the largest codepoint that is encoded as a single u16 in UTF-16,938 // U+FFFF is the character with the largest code point that is encoded as a single
796 // so Windows allows for NAME_MAX of them939 // UTF-16 code unit, so Windows allows for NAME_MAX of them.
797 const maxed_windows_filename = ("€".*) ** std.os.windows.NAME_MAX;940 const maxed_windows_filename = ("\u{FFFF}".*) ** std.os.windows.NAME_MAX;
798 try testFilenameLimits(tmp.iterable_dir, &maxed_windows_filename);941 try testFilenameLimits(tmp.iterable_dir, &maxed_windows_filename);
799 } else if (builtin.os.tag == .wasi) {942 } else if (builtin.os.tag == .wasi) {
800 // On WASI, the maxed filename depends on the host OS, so in order for this test to943 // On WASI, the maxed filename depends on the host OS, so in order for this test to
...@@ -892,22 +1035,19 @@ test "pwritev, preadv" {...@@ -892,22 +1035,19 @@ test "pwritev, preadv" {
892}1035}
8931036
894test "access file" {1037test "access file" {
895 if (builtin.os.tag == .wasi) return error.SkipZigTest;1038 try testWithAllSupportedPathTypes(struct {
8961039 fn impl(ctx: *TestContext) !void {
897 var tmp = tmpDir(.{});1040 const dir_path = try ctx.transformPath("os_test_tmp");
898 defer tmp.cleanup();1041 const file_path = try ctx.transformPath("os_test_tmp" ++ fs.path.sep_str ++ "file.txt");
8991042
900 try tmp.dir.makePath("os_test_tmp");1043 try ctx.dir.makePath(dir_path);
901 if (tmp.dir.access("os_test_tmp" ++ fs.path.sep_str ++ "file.txt", .{})) |ok| {1044 try testing.expectError(error.FileNotFound, ctx.dir.access(file_path, .{}));
902 _ = ok;
903 @panic("expected error");
904 } else |err| {
905 try testing.expect(err == error.FileNotFound);
906 }
9071045
908 try tmp.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "file.txt", "");1046 try ctx.dir.writeFile(file_path, "");
909 try tmp.dir.access("os_test_tmp" ++ fs.path.sep_str ++ "file.txt", .{});1047 try ctx.dir.access(file_path, .{});
910 try tmp.dir.deleteTree("os_test_tmp");1048 try ctx.dir.deleteTree(dir_path);
1049 }
1050 }.impl);
911}1051}
9121052
913test "sendfile" {1053test "sendfile" {
...@@ -972,7 +1112,7 @@ test "sendfile" {...@@ -972,7 +1112,7 @@ test "sendfile" {
972 .header_count = 2,1112 .header_count = 2,
973 });1113 });
974 const amt = try dest_file.preadAll(&written_buf, 0);1114 const amt = try dest_file.preadAll(&written_buf, 0);
975 try testing.expect(mem.eql(u8, written_buf[0..amt], "header1\nsecond header\nine1\nsecontrailer1\nsecond trailer\n"));1115 try testing.expectEqualStrings("header1\nsecond header\nine1\nsecontrailer1\nsecond trailer\n", written_buf[0..amt]);
976}1116}
9771117
978test "copyRangeAll" {1118test "copyRangeAll" {
...@@ -998,29 +1138,30 @@ test "copyRangeAll" {...@@ -998,29 +1138,30 @@ test "copyRangeAll" {
998 _ = try src_file.copyRangeAll(0, dest_file, 0, data.len);1138 _ = try src_file.copyRangeAll(0, dest_file, 0, data.len);
9991139
1000 const amt = try dest_file.preadAll(&written_buf, 0);1140 const amt = try dest_file.preadAll(&written_buf, 0);
1001 try testing.expect(mem.eql(u8, written_buf[0..amt], data));1141 try testing.expectEqualStrings(data, written_buf[0..amt]);
1002}1142}
10031143
1004test "fs.copyFile" {1144test "copyFile" {
1005 const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP";1145 try testWithAllSupportedPathTypes(struct {
1006 const src_file = "tmp_test_copy_file.txt";1146 fn impl(ctx: *TestContext) !void {
1007 const dest_file = "tmp_test_copy_file2.txt";1147 const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP";
1008 const dest_file2 = "tmp_test_copy_file3.txt";1148 const src_file = try ctx.transformPath("tmp_test_copy_file.txt");
1149 const dest_file = try ctx.transformPath("tmp_test_copy_file2.txt");
1150 const dest_file2 = try ctx.transformPath("tmp_test_copy_file3.txt");
10091151
1010 var tmp = tmpDir(.{});1152 try ctx.dir.writeFile(src_file, data);
1011 defer tmp.cleanup();1153 defer ctx.dir.deleteFile(src_file) catch {};
10121154
1013 try tmp.dir.writeFile(src_file, data);1155 try ctx.dir.copyFile(src_file, ctx.dir, dest_file, .{});
1014 defer tmp.dir.deleteFile(src_file) catch {};1156 defer ctx.dir.deleteFile(dest_file) catch {};
10151157
1016 try tmp.dir.copyFile(src_file, tmp.dir, dest_file, .{});1158 try ctx.dir.copyFile(src_file, ctx.dir, dest_file2, .{ .override_mode = File.default_mode });
1017 defer tmp.dir.deleteFile(dest_file) catch {};1159 defer ctx.dir.deleteFile(dest_file2) catch {};
10181160
1019 try tmp.dir.copyFile(src_file, tmp.dir, dest_file2, .{ .override_mode = File.default_mode });1161 try expectFileContents(ctx.dir, dest_file, data);
1020 defer tmp.dir.deleteFile(dest_file2) catch {};1162 try expectFileContents(ctx.dir, dest_file2, data);
10211163 }
1022 try expectFileContents(tmp.dir, dest_file, data);1164 }.impl);
1023 try expectFileContents(tmp.dir, dest_file2, data);
1024}1165}
10251166
1026fn expectFileContents(dir: Dir, file_path: []const u8, data: []const u8) !void {1167fn expectFileContents(dir: Dir, file_path: []const u8, data: []const u8) !void {
...@@ -1031,78 +1172,75 @@ fn expectFileContents(dir: Dir, file_path: []const u8, data: []const u8) !void {...@@ -1031,78 +1172,75 @@ fn expectFileContents(dir: Dir, file_path: []const u8, data: []const u8) !void {
1031}1172}
10321173
1033test "AtomicFile" {1174test "AtomicFile" {
1034 const test_out_file = "tmp_atomic_file_test_dest.txt";1175 try testWithAllSupportedPathTypes(struct {
1035 const test_content =1176 fn impl(ctx: *TestContext) !void {
1036 \\ hello!1177 const test_out_file = try ctx.transformPath("tmp_atomic_file_test_dest.txt");
1037 \\ this is a test file1178 const test_content =
1038 ;1179 \\ hello!
10391180 \\ this is a test file
1040 var tmp = tmpDir(.{});1181 ;
1041 defer tmp.cleanup();1182
10421183 {
1043 {1184 var af = try ctx.dir.atomicFile(test_out_file, .{});
1044 var af = try tmp.dir.atomicFile(test_out_file, .{});1185 defer af.deinit();
1045 defer af.deinit();1186 try af.file.writeAll(test_content);
1046 try af.file.writeAll(test_content);1187 try af.finish();
1047 try af.finish();1188 }
1048 }1189 const content = try ctx.dir.readFileAlloc(testing.allocator, test_out_file, 9999);
1049 const content = try tmp.dir.readFileAlloc(testing.allocator, test_out_file, 9999);1190 defer testing.allocator.free(content);
1050 defer testing.allocator.free(content);1191 try testing.expectEqualStrings(test_content, content);
1051 try testing.expect(mem.eql(u8, content, test_content));1192
10521193 try ctx.dir.deleteFile(test_out_file);
1053 try tmp.dir.deleteFile(test_out_file);1194 }
1054}1195 }.impl);
1055
1056test "realpath" {
1057 if (builtin.os.tag == .wasi) return error.SkipZigTest;
1058
1059 var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
1060 try testing.expectError(error.FileNotFound, fs.realpath("definitely_bogus_does_not_exist1234", &buf));
1061}1196}
10621197
1063test "open file with exclusive nonblocking lock twice" {1198test "open file with exclusive nonblocking lock twice" {
1064 if (builtin.os.tag == .wasi) return error.SkipZigTest;1199 if (builtin.os.tag == .wasi) return error.SkipZigTest;
10651200
1066 const filename = "file_nonblocking_lock_test.txt";1201 try testWithAllSupportedPathTypes(struct {
1202 fn impl(ctx: *TestContext) !void {
1203 const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
10671204
1068 var tmp = tmpDir(.{});1205 const file1 = try ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1069 defer tmp.cleanup();1206 defer file1.close();
10701207
1071 const file1 = try tmp.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });1208 const file2 = ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1072 defer file1.close();1209 try testing.expectError(error.WouldBlock, file2);
10731210 }
1074 const file2 = tmp.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });1211 }.impl);
1075 try testing.expectError(error.WouldBlock, file2);
1076}1212}
10771213
1078test "open file with shared and exclusive nonblocking lock" {1214test "open file with shared and exclusive nonblocking lock" {
1079 if (builtin.os.tag == .wasi) return error.SkipZigTest;1215 if (builtin.os.tag == .wasi) return error.SkipZigTest;
10801216
1081 const filename = "file_nonblocking_lock_test.txt";1217 try testWithAllSupportedPathTypes(struct {
1218 fn impl(ctx: *TestContext) !void {
1219 const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
10821220
1083 var tmp = tmpDir(.{});1221 const file1 = try ctx.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });
1084 defer tmp.cleanup();1222 defer file1.close();
10851223
1086 const file1 = try tmp.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });1224 const file2 = ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1087 defer file1.close();1225 try testing.expectError(error.WouldBlock, file2);
10881226 }
1089 const file2 = tmp.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });1227 }.impl);
1090 try testing.expectError(error.WouldBlock, file2);
1091}1228}
10921229
1093test "open file with exclusive and shared nonblocking lock" {1230test "open file with exclusive and shared nonblocking lock" {
1094 if (builtin.os.tag == .wasi) return error.SkipZigTest;1231 if (builtin.os.tag == .wasi) return error.SkipZigTest;
10951232
1096 const filename = "file_nonblocking_lock_test.txt";1233 try testWithAllSupportedPathTypes(struct {
10971234 fn impl(ctx: *TestContext) !void {
1098 var tmp = tmpDir(.{});1235 const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
1099 defer tmp.cleanup();
11001236
1101 const file1 = try tmp.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });1237 const file1 = try ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1102 defer file1.close();1238 defer file1.close();
11031239
1104 const file2 = tmp.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });1240 const file2 = ctx.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });
1105 try testing.expectError(error.WouldBlock, file2);1241 try testing.expectError(error.WouldBlock, file2);
1242 }
1243 }.impl);
1106}1244}
11071245
1108test "open file with exclusive lock twice, make sure second lock waits" {1246test "open file with exclusive lock twice, make sure second lock waits" {
...@@ -1113,42 +1251,44 @@ test "open file with exclusive lock twice, make sure second lock waits" {...@@ -1113,42 +1251,44 @@ test "open file with exclusive lock twice, make sure second lock waits" {
1113 return error.SkipZigTest;1251 return error.SkipZigTest;
1114 }1252 }
11151253
1116 const filename = "file_lock_test.txt";1254 try testWithAllSupportedPathTypes(struct {
11171255 fn impl(ctx: *TestContext) !void {
1118 var tmp = tmpDir(.{});1256 const filename = try ctx.transformPath("file_lock_test.txt");
1119 defer tmp.cleanup();1257
11201258 const file = try ctx.dir.createFile(filename, .{ .lock = .exclusive });
1121 const file = try tmp.dir.createFile(filename, .{ .lock = .exclusive });1259 errdefer file.close();
1122 errdefer file.close();1260
11231261 const S = struct {
1124 const S = struct {1262 fn checkFn(dir: *fs.Dir, path: []const u8, started: *std.Thread.ResetEvent, locked: *std.Thread.ResetEvent) !void {
1125 fn checkFn(dir: *fs.Dir, started: *std.Thread.ResetEvent, locked: *std.Thread.ResetEvent) !void {1263 started.set();
1126 started.set();1264 const file1 = try dir.createFile(path, .{ .lock = .exclusive });
1127 const file1 = try dir.createFile(filename, .{ .lock = .exclusive });1265
11281266 locked.set();
1129 locked.set();1267 file1.close();
1130 file1.close();1268 }
1269 };
1270
1271 var started = std.Thread.ResetEvent{};
1272 var locked = std.Thread.ResetEvent{};
1273
1274 const t = try std.Thread.spawn(.{}, S.checkFn, .{
1275 &ctx.dir,
1276 filename,
1277 &started,
1278 &locked,
1279 });
1280 defer t.join();
1281
1282 // Wait for the spawned thread to start trying to acquire the exclusive file lock.
1283 // Then wait a bit to make sure that can't acquire it since we currently hold the file lock.
1284 started.wait();
1285 try testing.expectError(error.Timeout, locked.timedWait(10 * std.time.ns_per_ms));
1286
1287 // Release the file lock which should unlock the thread to lock it and set the locked event.
1288 file.close();
1289 locked.wait();
1131 }1290 }
1132 };1291 }.impl);
1133
1134 var started = std.Thread.ResetEvent{};
1135 var locked = std.Thread.ResetEvent{};
1136
1137 const t = try std.Thread.spawn(.{}, S.checkFn, .{
1138 &tmp.dir,
1139 &started,
1140 &locked,
1141 });
1142 defer t.join();
1143
1144 // Wait for the spawned thread to start trying to acquire the exclusive file lock.
1145 // Then wait a bit to make sure that can't acquire it since we currently hold the file lock.
1146 started.wait();
1147 try testing.expectError(error.Timeout, locked.timedWait(10 * std.time.ns_per_ms));
1148
1149 // Release the file lock which should unlock the thread to lock it and set the locked event.
1150 file.close();
1151 locked.wait();
1152}1292}
11531293
1154test "open file with exclusive nonblocking lock twice (absolute paths)" {1294test "open file with exclusive nonblocking lock twice (absolute paths)" {
...@@ -1264,29 +1404,36 @@ test "walker without fully iterating" {...@@ -1264,29 +1404,36 @@ test "walker without fully iterating" {
1264test ". and .. in fs.Dir functions" {1404test ". and .. in fs.Dir functions" {
1265 if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;1405 if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
12661406
1267 var tmp = tmpDir(.{});1407 try testWithAllSupportedPathTypes(struct {
1268 defer tmp.cleanup();1408 fn impl(ctx: *TestContext) !void {
12691409 const subdir_path = try ctx.transformPath("./subdir");
1270 try tmp.dir.makeDir("./subdir");1410 const file_path = try ctx.transformPath("./subdir/../file");
1271 try tmp.dir.access("./subdir", .{});1411 const copy_path = try ctx.transformPath("./subdir/../copy");
1272 var created_subdir = try tmp.dir.openDir("./subdir", .{});1412 const rename_path = try ctx.transformPath("./subdir/../rename");
1273 created_subdir.close();1413 const update_path = try ctx.transformPath("./subdir/../update");
12741414
1275 const created_file = try tmp.dir.createFile("./subdir/../file", .{});1415 try ctx.dir.makeDir(subdir_path);
1276 created_file.close();1416 try ctx.dir.access(subdir_path, .{});
1277 try tmp.dir.access("./subdir/../file", .{});1417 var created_subdir = try ctx.dir.openDir(subdir_path, .{});
12781418 created_subdir.close();
1279 try tmp.dir.copyFile("./subdir/../file", tmp.dir, "./subdir/../copy", .{});1419
1280 try tmp.dir.rename("./subdir/../copy", "./subdir/../rename");1420 const created_file = try ctx.dir.createFile(file_path, .{});
1281 const renamed_file = try tmp.dir.openFile("./subdir/../rename", .{});1421 created_file.close();
1282 renamed_file.close();1422 try ctx.dir.access(file_path, .{});
1283 try tmp.dir.deleteFile("./subdir/../rename");1423
12841424 try ctx.dir.copyFile(file_path, ctx.dir, copy_path, .{});
1285 try tmp.dir.writeFile("./subdir/../update", "something");1425 try ctx.dir.rename(copy_path, rename_path);
1286 const prev_status = try tmp.dir.updateFile("./subdir/../file", tmp.dir, "./subdir/../update", .{});1426 const renamed_file = try ctx.dir.openFile(rename_path, .{});
1287 try testing.expectEqual(fs.PrevStatus.stale, prev_status);1427 renamed_file.close();
12881428 try ctx.dir.deleteFile(rename_path);
1289 try tmp.dir.deleteDir("./subdir");1429
1430 try ctx.dir.writeFile(update_path, "something");
1431 const prev_status = try ctx.dir.updateFile(file_path, ctx.dir, update_path, .{});
1432 try testing.expectEqual(fs.PrevStatus.stale, prev_status);
1433
1434 try ctx.dir.deleteDir(subdir_path);
1435 }
1436 }.impl);
1290}1437}
12911438
1292test ". and .. in absolute functions" {1439test ". and .. in absolute functions" {
...@@ -1342,17 +1489,17 @@ test "chmod" {...@@ -1342,17 +1489,17 @@ test "chmod" {
13421489
1343 const file = try tmp.dir.createFile("test_file", .{ .mode = 0o600 });1490 const file = try tmp.dir.createFile("test_file", .{ .mode = 0o600 });
1344 defer file.close();1491 defer file.close();
1345 try testing.expect((try file.stat()).mode & 0o7777 == 0o600);1492 try testing.expectEqual(@as(File.Mode, 0o600), (try file.stat()).mode & 0o7777);
13461493
1347 try file.chmod(0o644);1494 try file.chmod(0o644);
1348 try testing.expect((try file.stat()).mode & 0o7777 == 0o644);1495 try testing.expectEqual(@as(File.Mode, 0o644), (try file.stat()).mode & 0o7777);
13491496
1350 try tmp.dir.makeDir("test_dir");1497 try tmp.dir.makeDir("test_dir");
1351 var iterable_dir = try tmp.dir.openIterableDir("test_dir", .{});1498 var iterable_dir = try tmp.dir.openIterableDir("test_dir", .{});
1352 defer iterable_dir.close();1499 defer iterable_dir.close();
13531500
1354 try iterable_dir.chmod(0o700);1501 try iterable_dir.chmod(0o700);
1355 try testing.expect((try iterable_dir.dir.stat()).mode & 0o7777 == 0o700);1502 try testing.expectEqual(@as(File.Mode, 0o700), (try iterable_dir.dir.stat()).mode & 0o7777);
1356}1503}
13571504
1358test "chown" {1505test "chown" {
...@@ -1381,8 +1528,8 @@ test "File.Metadata" {...@@ -1381,8 +1528,8 @@ test "File.Metadata" {
1381 defer file.close();1528 defer file.close();
13821529
1383 const metadata = try file.metadata();1530 const metadata = try file.metadata();
1384 try testing.expect(metadata.kind() == .file);1531 try testing.expectEqual(File.Kind.file, metadata.kind());
1385 try testing.expect(metadata.size() == 0);1532 try testing.expectEqual(@as(u64, 0), metadata.size());
1386 _ = metadata.accessed();1533 _ = metadata.accessed();
1387 _ = metadata.modified();1534 _ = metadata.modified();
1388 _ = metadata.created();1535 _ = metadata.created();
lib/std/os.zig+20-7
...@@ -5169,11 +5169,30 @@ pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPat...@@ -5169,11 +5169,30 @@ pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPat
5169 return getFdPath(h_file, out_buffer);5169 return getFdPath(h_file, out_buffer);
5170}5170}
51715171
5172pub fn isGetFdPathSupportedOnTarget(os: std.Target.Os) bool {
5173 return switch (os.tag) {
5174 // zig fmt: off
5175 .windows,
5176 .macos, .ios, .watchos, .tvos,
5177 .linux,
5178 .solaris,
5179 .freebsd,
5180 => true,
5181 // zig fmt: on
5182 .dragonfly => os.version_range.semver.max.order(.{ .major = 6, .minor = 0, .patch = 0 }) != .lt,
5183 .netbsd => os.version_range.semver.max.order(.{ .major = 10, .minor = 0, .patch = 0 }) != .lt,
5184 else => false,
5185 };
5186}
5187
5172/// Return canonical path of handle `fd`.5188/// Return canonical path of handle `fd`.
5173/// This function is very host-specific and is not universally supported by all hosts.5189/// This function is very host-specific and is not universally supported by all hosts.
5174/// For example, while it generally works on Linux, macOS, FreeBSD or Windows, it is5190/// For example, while it generally works on Linux, macOS, FreeBSD or Windows, it is
5175/// unsupported on WASI.5191/// unsupported on WASI.
5176pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 {5192pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 {
5193 if (!comptime isGetFdPathSupportedOnTarget(builtin.os)) {
5194 @compileError("querying for canonical path of a handle is unsupported on this host");
5195 }
5177 switch (builtin.os.tag) {5196 switch (builtin.os.tag) {
5178 .windows => {5197 .windows => {
5179 var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined;5198 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 {...@@ -5276,9 +5295,6 @@ pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 {
5276 }5295 }
5277 },5296 },
5278 .dragonfly => {5297 .dragonfly => {
5279 if (comptime builtin.os.version_range.semver.max.order(.{ .major = 6, .minor = 0, .patch = 0 }) == .lt) {
5280 @compileError("querying for canonical path of a handle is unsupported on this host");
5281 }
5282 @memset(out_buffer[0..MAX_PATH_BYTES], 0);5298 @memset(out_buffer[0..MAX_PATH_BYTES], 0);
5283 switch (errno(system.fcntl(fd, F.GETPATH, out_buffer))) {5299 switch (errno(system.fcntl(fd, F.GETPATH, out_buffer))) {
5284 .SUCCESS => {},5300 .SUCCESS => {},
...@@ -5290,9 +5306,6 @@ pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 {...@@ -5290,9 +5306,6 @@ pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 {
5290 return out_buffer[0..len];5306 return out_buffer[0..len];
5291 },5307 },
5292 .netbsd => {5308 .netbsd => {
5293 if (comptime builtin.os.version_range.semver.max.order(.{ .major = 10, .minor = 0, .patch = 0 }) == .lt) {
5294 @compileError("querying for canonical path of a handle is unsupported on this host");
5295 }
5296 @memset(out_buffer[0..MAX_PATH_BYTES], 0);5309 @memset(out_buffer[0..MAX_PATH_BYTES], 0);
5297 switch (errno(system.fcntl(fd, F.GETPATH, out_buffer))) {5310 switch (errno(system.fcntl(fd, F.GETPATH, out_buffer))) {
5298 .SUCCESS => {},5311 .SUCCESS => {},
...@@ -5306,7 +5319,7 @@ pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 {...@@ -5306,7 +5319,7 @@ pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 {
5306 const len = mem.indexOfScalar(u8, out_buffer[0..], @as(u8, 0)) orelse MAX_PATH_BYTES;5319 const len = mem.indexOfScalar(u8, out_buffer[0..], @as(u8, 0)) orelse MAX_PATH_BYTES;
5307 return out_buffer[0..len];5320 return out_buffer[0..len];
5308 },5321 },
5309 else => @compileError("querying for canonical path of a handle is unsupported on this host"),5322 else => unreachable, // made unreachable by isGetFdPathSupportedOnTarget above
5310 }5323 }
5311}5324}
53125325
lib/std/os/test.zig+2-2
...@@ -193,7 +193,7 @@ test "symlink with relative paths" {...@@ -193,7 +193,7 @@ test "symlink with relative paths" {
193 os.windows.CreateSymbolicLink(193 os.windows.CreateSymbolicLink(
194 cwd.fd,194 cwd.fd,
195 &[_]u16{ 's', 'y', 'm', 'l', 'i', 'n', 'k', 'e', 'd' },195 &[_]u16{ 's', 'y', 'm', 'l', 'i', 'n', 'k', 'e', 'd' },
196 &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },196 &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
197 false,197 false,
198 ) catch |err| switch (err) {198 ) catch |err| switch (err) {
199 // Symlink requires admin privileges on windows, so this test can legitimately fail.199 // Symlink requires admin privileges on windows, so this test can legitimately fail.
...@@ -351,7 +351,7 @@ test "readlinkat" {...@@ -351,7 +351,7 @@ test "readlinkat" {
351 os.windows.CreateSymbolicLink(351 os.windows.CreateSymbolicLink(
352 tmp.dir.fd,352 tmp.dir.fd,
353 &[_]u16{ 'l', 'i', 'n', 'k' },353 &[_]u16{ 'l', 'i', 'n', 'k' },
354 &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },354 &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
355 false,355 false,
356 ) catch |err| switch (err) {356 ) catch |err| switch (err) {
357 // Symlink requires admin privileges on windows, so this test can legitimately fail.357 // Symlink requires admin privileges on windows, so this test can legitimately fail.
lib/std/os/windows.zig+141-25
...@@ -704,6 +704,7 @@ pub const CreateSymbolicLinkError = error{...@@ -704,6 +704,7 @@ pub const CreateSymbolicLinkError = error{
704 NameTooLong,704 NameTooLong,
705 NoDevice,705 NoDevice,
706 NetworkNotFound,706 NetworkNotFound,
707 BadPathName,
707 Unexpected,708 Unexpected,
708};709};
709710
...@@ -716,7 +717,7 @@ pub const CreateSymbolicLinkError = error{...@@ -716,7 +717,7 @@ pub const CreateSymbolicLinkError = error{
716pub fn CreateSymbolicLink(717pub fn CreateSymbolicLink(
717 dir: ?HANDLE,718 dir: ?HANDLE,
718 sym_link_path: []const u16,719 sym_link_path: []const u16,
719 target_path: []const u16,720 target_path: [:0]const u16,
720 is_directory: bool,721 is_directory: bool,
721) CreateSymbolicLinkError!void {722) CreateSymbolicLinkError!void {
722 const SYMLINK_DATA = extern struct {723 const SYMLINK_DATA = extern struct {
...@@ -745,25 +746,58 @@ pub fn CreateSymbolicLink(...@@ -745,25 +746,58 @@ pub fn CreateSymbolicLink(
745 };746 };
746 defer CloseHandle(symlink_handle);747 defer CloseHandle(symlink_handle);
747748
749 // Relevant portions of the documentation:
750 // > Relative links are specified using the following conventions:
751 // > - Root relative—for example, "\Windows\System32" resolves to "current drive:\Windows\System32".
752 // > - Current working directory–relative—for example, if the current working directory is
753 // > C:\Windows\System32, "C:File.txt" resolves to "C:\Windows\System32\File.txt".
754 // > Note: If you specify a current working directory–relative link, it is created as an absolute
755 // > link, due to the way the current working directory is processed based on the user and the thread.
756 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
757 var is_target_absolute = false;
758 const final_target_path = target_path: {
759 switch (getNamespacePrefix(u16, target_path)) {
760 .none => switch (getUnprefixedPathType(u16, target_path)) {
761 // Rooted paths need to avoid getting put through wToPrefixedFileW
762 // (and they are treated as relative in this context)
763 // Note: It seems that rooted paths in symbolic links are relative to
764 // the drive that the symbolic exists on, not to the CWD's drive.
765 // So, if the symlink is on C:\ and the CWD is on D:\,
766 // it will still resolve the path relative to the root of
767 // the C:\ drive.
768 .rooted => break :target_path target_path,
769 else => {},
770 },
771 // Already an NT path, no need to do anything to it
772 .nt => break :target_path target_path,
773 else => {},
774 }
775 var prefixed_target_path = try wToPrefixedFileW(dir, target_path);
776 // We do this after prefixing to ensure that drive-relative paths are treated as absolute
777 is_target_absolute = std.fs.path.isAbsoluteWindowsWTF16(prefixed_target_path.span());
778 break :target_path prefixed_target_path.span();
779 };
780
748 // prepare reparse data buffer781 // prepare reparse data buffer
749 var buffer: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;782 var buffer: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
750 const buf_len = @sizeOf(SYMLINK_DATA) + target_path.len * 4;783 const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4;
751 const header_len = @sizeOf(ULONG) + @sizeOf(USHORT) * 2;784 const header_len = @sizeOf(ULONG) + @sizeOf(USHORT) * 2;
785 const target_is_absolute = std.fs.path.isAbsoluteWindowsWTF16(final_target_path);
752 const symlink_data = SYMLINK_DATA{786 const symlink_data = SYMLINK_DATA{
753 .ReparseTag = IO_REPARSE_TAG_SYMLINK,787 .ReparseTag = IO_REPARSE_TAG_SYMLINK,
754 .ReparseDataLength = @as(u16, @intCast(buf_len - header_len)),788 .ReparseDataLength = @as(u16, @intCast(buf_len - header_len)),
755 .Reserved = 0,789 .Reserved = 0,
756 .SubstituteNameOffset = @as(u16, @intCast(target_path.len * 2)),790 .SubstituteNameOffset = @as(u16, @intCast(final_target_path.len * 2)),
757 .SubstituteNameLength = @as(u16, @intCast(target_path.len * 2)),791 .SubstituteNameLength = @as(u16, @intCast(final_target_path.len * 2)),
758 .PrintNameOffset = 0,792 .PrintNameOffset = 0,
759 .PrintNameLength = @as(u16, @intCast(target_path.len * 2)),793 .PrintNameLength = @as(u16, @intCast(final_target_path.len * 2)),
760 .Flags = if (dir) |_| SYMLINK_FLAG_RELATIVE else 0,794 .Flags = if (!target_is_absolute) SYMLINK_FLAG_RELATIVE else 0,
761 };795 };
762796
763 @memcpy(buffer[0..@sizeOf(SYMLINK_DATA)], std.mem.asBytes(&symlink_data));797 @memcpy(buffer[0..@sizeOf(SYMLINK_DATA)], std.mem.asBytes(&symlink_data));
764 @memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. target_path.len * 2], @as([*]const u8, @ptrCast(target_path)));798 @memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
765 const paths_start = @sizeOf(SYMLINK_DATA) + target_path.len * 2;799 const paths_start = @sizeOf(SYMLINK_DATA) + final_target_path.len * 2;
766 @memcpy(buffer[paths_start..][0 .. target_path.len * 2], @as([*]const u8, @ptrCast(target_path)));800 @memcpy(buffer[paths_start..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
767 _ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null);801 _ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null);
768}802}
769803
...@@ -861,12 +895,15 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin...@@ -861,12 +895,15 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin
861}895}
862896
863fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 {897fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 {
864 const prefix = [_]u16{ '\\', '?', '?', '\\' };898 const win32_namespace_path = path: {
865 var start_index: usize = 0;899 if (is_relative) break :path path;
866 if (!is_relative and std.mem.startsWith(u16, path, &prefix)) {900 const win32_path = ntToWin32Namespace(path) catch |err| switch (err) {
867 start_index = prefix.len;901 error.NameTooLong => unreachable,
868 }902 error.NotNtPath => break :path path,
869 const out_len = std.unicode.utf16leToUtf8(out_buffer, path[start_index..]) catch unreachable;903 };
904 break :path win32_path.span();
905 };
906 const out_len = std.unicode.utf16leToUtf8(out_buffer, win32_namespace_path) catch unreachable;
870 return out_buffer[0..out_len];907 return out_buffer[0..out_len];
871}908}
872909
...@@ -1189,8 +1226,18 @@ pub fn GetFinalPathNameByHandle(...@@ -1189,8 +1226,18 @@ pub fn GetFinalPathNameByHandle(
11891226
1190 const file_path_begin_index = mem.indexOfPos(u16, final_path, expected_prefix.len, &[_]u16{'\\'}) orelse unreachable;1227 const file_path_begin_index = mem.indexOfPos(u16, final_path, expected_prefix.len, &[_]u16{'\\'}) orelse unreachable;
1191 const volume_name_u16 = final_path[0..file_path_begin_index];1228 const volume_name_u16 = final_path[0..file_path_begin_index];
1229 const device_name_u16 = volume_name_u16[expected_prefix.len..];
1192 const file_name_u16 = final_path[file_path_begin_index..];1230 const file_name_u16 = final_path[file_path_begin_index..];
11931231
1232 // MUP is Multiple UNC Provider, and indicates that the path is a UNC
1233 // path. In this case, the canonical UNC path can be gotten by just
1234 // dropping the \Device\Mup\ and making sure the path begins with \\
1235 if (mem.eql(u16, device_name_u16, std.unicode.utf8ToUtf16LeStringLiteral("Mup"))) {
1236 out_buffer[0] = '\\';
1237 mem.copyForwards(u16, out_buffer[1..][0..file_name_u16.len], file_name_u16);
1238 return out_buffer[0 .. 1 + file_name_u16.len];
1239 }
1240
1194 // Get DOS volume name. DOS volume names are actually symbolic link objects to the1241 // Get DOS volume name. DOS volume names are actually symbolic link objects to the
1195 // actual NT volume. For example:1242 // actual NT volume. For example:
1196 // (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C:1243 // (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C:
...@@ -2296,25 +2343,26 @@ pub const NamespacePrefix = enum {...@@ -2296,25 +2343,26 @@ pub const NamespacePrefix = enum {
2296 nt,2343 nt,
2297};2344};
22982345
2346/// If `T` is `u16`, then `path` should be encoded as UTF-16LE.
2299pub fn getNamespacePrefix(comptime T: type, path: []const T) NamespacePrefix {2347pub fn getNamespacePrefix(comptime T: type, path: []const T) NamespacePrefix {
2300 if (path.len < 4) return .none;2348 if (path.len < 4) return .none;
2301 var all_backslash = switch (path[0]) {2349 var all_backslash = switch (mem.littleToNative(T, path[0])) {
2302 '\\' => true,2350 '\\' => true,
2303 '/' => false,2351 '/' => false,
2304 else => return .none,2352 else => return .none,
2305 };2353 };
2306 all_backslash = all_backslash and switch (path[3]) {2354 all_backslash = all_backslash and switch (mem.littleToNative(T, path[3])) {
2307 '\\' => true,2355 '\\' => true,
2308 '/' => false,2356 '/' => false,
2309 else => return .none,2357 else => return .none,
2310 };2358 };
2311 switch (path[1]) {2359 switch (mem.littleToNative(T, path[1])) {
2312 '?' => if (path[2] == '?' and all_backslash) return .nt else return .none,2360 '?' => if (mem.littleToNative(T, path[2]) == '?' and all_backslash) return .nt else return .none,
2313 '\\' => {},2361 '\\' => {},
2314 '/' => all_backslash = false,2362 '/' => all_backslash = false,
2315 else => return .none,2363 else => return .none,
2316 }2364 }
2317 return switch (path[2]) {2365 return switch (mem.littleToNative(T, path[2])) {
2318 '?' => if (all_backslash) .verbatim else .fake_verbatim,2366 '?' => if (all_backslash) .verbatim else .fake_verbatim,
2319 '.' => .local_device,2367 '.' => .local_device,
2320 else => .none,2368 else => .none,
...@@ -2349,6 +2397,7 @@ pub const UnprefixedPathType = enum {...@@ -2349,6 +2397,7 @@ pub const UnprefixedPathType = enum {
23492397
2350/// Get the path type of a path that is known to not have any namespace prefixes2398/// Get the path type of a path that is known to not have any namespace prefixes
2351/// (`\\?\`, `\\.\`, `\??\`).2399/// (`\\?\`, `\\.\`, `\??\`).
2400/// If `T` is `u16`, then `path` should be encoded as UTF-16LE.
2352pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathType {2401pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathType {
2353 if (path.len < 1) return .relative;2402 if (path.len < 1) return .relative;
23542403
...@@ -2357,18 +2406,18 @@ pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathTy...@@ -2357,18 +2406,18 @@ pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathTy
2357 }2406 }
23582407
2359 const windows_path = std.fs.path.PathType.windows;2408 const windows_path = std.fs.path.PathType.windows;
2360 if (windows_path.isSep(T, path[0])) {2409 if (windows_path.isSep(T, mem.littleToNative(T, path[0]))) {
2361 // \x2410 // \x
2362 if (path.len < 2 or !windows_path.isSep(T, path[1])) return .rooted;2411 if (path.len < 2 or !windows_path.isSep(T, mem.littleToNative(T, path[1]))) return .rooted;
2363 // exactly \\. or \\? with nothing trailing2412 // exactly \\. or \\? with nothing trailing
2364 if (path.len == 3 and (path[2] == '.' or path[2] == '?')) return .root_local_device;2413 if (path.len == 3 and (mem.littleToNative(T, path[2]) == '.' or mem.littleToNative(T, path[2]) == '?')) return .root_local_device;
2365 // \\x2414 // \\x
2366 return .unc_absolute;2415 return .unc_absolute;
2367 } else {2416 } else {
2368 // x2417 // x
2369 if (path.len < 2 or path[1] != ':') return .relative;2418 if (path.len < 2 or mem.littleToNative(T, path[1]) != ':') return .relative;
2370 // x:\2419 // x:\
2371 if (path.len > 2 and windows_path.isSep(T, path[2])) return .drive_absolute;2420 if (path.len > 2 and windows_path.isSep(T, mem.littleToNative(T, path[2]))) return .drive_absolute;
2372 // x:2421 // x:
2373 return .drive_relative;2422 return .drive_relative;
2374 }2423 }
...@@ -2393,6 +2442,73 @@ test getUnprefixedPathType {...@@ -2393,6 +2442,73 @@ test getUnprefixedPathType {
2393 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));2442 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));
2394}2443}
23952444
2445/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
2446/// The possible transformations are:
2447/// \??\C:\Some\Path -> C:\Some\Path
2448/// \??\UNC\server\share\foo -> \\server\share\foo
2449/// If the path does not have the NT namespace prefix, then `error.NotNtPath` is returned.
2450///
2451/// Functionality is based on the ReactOS test cases found here:
2452/// https://github.com/reactos/reactos/blob/master/modules/rostests/apitests/ntdll/RtlNtPathNameToDosPathName.c
2453///
2454/// `path` should be encoded as UTF-16LE.
2455pub fn ntToWin32Namespace(path: []const u16) !PathSpace {
2456 if (path.len > PATH_MAX_WIDE) return error.NameTooLong;
2457
2458 var path_space: PathSpace = undefined;
2459 const namespace_prefix = getNamespacePrefix(u16, path);
2460 switch (namespace_prefix) {
2461 .nt => {
2462 var dest_index: usize = 0;
2463 var after_prefix = path[4..]; // after the `\??\`
2464 // The prefix \??\UNC\ means this is a UNC path, in which case the
2465 // `\??\UNC\` should be replaced by `\\` (two backslashes)
2466 // TODO: the "UNC" should technically be matched case-insensitively, but
2467 // it's unlikely to matter since most/all paths passed into this
2468 // function will have come from the OS meaning it should have
2469 // the 'canonical' uppercase UNC.
2470 const is_unc = after_prefix.len >= 4 and
2471 std.mem.eql(u16, after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
2472 std.fs.path.PathType.windows.isSep(u16, std.mem.littleToNative(u16, after_prefix[3]));
2473 if (is_unc) {
2474 path_space.data[0] = comptime std.mem.nativeToLittle(u16, '\\');
2475 dest_index += 1;
2476 // We want to include the last `\` of `\??\UNC\`
2477 after_prefix = path[7..];
2478 }
2479 @memcpy(path_space.data[dest_index..][0..after_prefix.len], after_prefix);
2480 path_space.len = dest_index + after_prefix.len;
2481 path_space.data[path_space.len] = 0;
2482 return path_space;
2483 },
2484 else => return error.NotNtPath,
2485 }
2486}
2487
2488test "ntToWin32Namespace" {
2489 const L = std.unicode.utf8ToUtf16LeStringLiteral;
2490
2491 try testNtToWin32Namespace(L("UNC"), L("\\??\\UNC"));
2492 try testNtToWin32Namespace(L("\\\\"), L("\\??\\UNC\\"));
2493 try testNtToWin32Namespace(L("\\\\path1"), L("\\??\\UNC\\path1"));
2494 try testNtToWin32Namespace(L("\\\\path1\\path2"), L("\\??\\UNC\\path1\\path2"));
2495
2496 try testNtToWin32Namespace(L(""), L("\\??\\"));
2497 try testNtToWin32Namespace(L("C:"), L("\\??\\C:"));
2498 try testNtToWin32Namespace(L("C:\\"), L("\\??\\C:\\"));
2499 try testNtToWin32Namespace(L("C:\\test"), L("\\??\\C:\\test"));
2500 try testNtToWin32Namespace(L("C:\\test\\"), L("\\??\\C:\\test\\"));
2501
2502 try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("foo")));
2503 try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("C:\\test")));
2504 try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("\\\\.\\test")));
2505}
2506
2507fn testNtToWin32Namespace(expected: []const u16, path: []const u16) !void {
2508 const converted = try ntToWin32Namespace(path);
2509 try std.testing.expectEqualSlices(u16, expected, converted.span());
2510}
2511
2396fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {2512fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {
2397 const result = kernel32.GetFullPathNameW(path, @as(u32, @intCast(out.len)), out.ptr, null);2513 const result = kernel32.GetFullPathNameW(path, @as(u32, @intCast(out.len)), out.ptr, null);
2398 if (result == 0) {2514 if (result == 0) {