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 {
19491949 return self.symLinkWasi(target_path, sym_link_path, flags);
19501950 }
19511951 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;
19531959 const sym_link_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sym_link_path);
19541960 return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags);
19551961 }
......@@ -1987,7 +1993,10 @@ pub const Dir = struct {
19871993 /// are null-terminated, WTF16 encoded.
19881994 pub fn symLinkW(
19891995 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
19912000 sym_link_path_w: []const u16,
19922001 flags: SymLinkFlags,
19932002 ) !void {
lib/std/fs/test.zig+562-415
......@@ -13,38 +13,167 @@ const File = std.fs.File;
1313const tmpDir = testing.tmpDir;
1414const tmpIterableDir = testing.tmpIterableDir;
1515
16test "Dir.readLink" {
17 var tmp = tmpDir(.{});
18 defer tmp.cleanup();
19
20 // Create some targets
21 try tmp.dir.writeFile("file.txt", "nonsense");
22 try tmp.dir.makeDir("subdir");
16const PathType = enum {
17 relative,
18 absolute,
19 unc,
20
21 pub fn isSupported(self: PathType, target_os: std.Target.Os) bool {
22 return switch (self) {
23 .relative => true,
24 .absolute => std.os.isGetFdPathSupportedOnTarget(target_os),
25 .unc => target_os.tag == .windows,
26 };
27 }
2328
24 {
25 // Create symbolic link by path
26 tmp.dir.symLink("file.txt", "symlink1", .{}) catch |err| switch (err) {
27 // Symlink requires admin privileges on windows, so this test can legitimately fail.
28 error.AccessDenied => return error.SkipZigTest,
29 else => return err,
29 pub const TransformError = std.os.RealPathError || error{OutOfMemory};
30 pub const TransformFn = fn (allocator: mem.Allocator, dir: Dir, relative_path: []const u8) TransformError![]const u8;
31
32 pub fn getTransformFn(comptime path_type: PathType) TransformFn {
33 switch (path_type) {
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,
3091 };
31 try testReadLink(tmp.dir, "file.txt", "symlink1");
3292 }
33 {
34 // Create symbolic link by path
35 tmp.dir.symLink("subdir", "symlink2", .{ .is_directory = true }) catch |err| switch (err) {
36 // Symlink requires admin privileges on windows, so this test can legitimately fail.
37 error.AccessDenied => return error.SkipZigTest,
38 else => return err,
93
94 pub fn deinit(self: *TestContext) void {
95 self.arena.deinit();
96 self.tmp.cleanup();
97 }
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;
39122 };
40 try testReadLink(tmp.dir, "subdir", "symlink2");
41123 }
42124}
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
44157fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !void {
45158 var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;
46159 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);
48177}
49178
50179test "accessAbsolute" {
......@@ -174,7 +303,7 @@ test "readLinkAbsolute" {
174303fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void {
175304 var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;
176305 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);
178307}
179308
180309test "Dir.Iterator" {
......@@ -202,7 +331,7 @@ test "Dir.Iterator" {
202331 try entries.append(.{ .name = name, .kind = entry.kind });
203332 }
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 '..'
206335 try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
207336 try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
208337}
......@@ -269,7 +398,7 @@ test "Dir.Iterator twice" {
269398 try entries.append(.{ .name = name, .kind = entry.kind });
270399 }
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 '..'
273402 try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
274403 try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
275404 }
......@@ -303,7 +432,7 @@ test "Dir.Iterator reset" {
303432 try entries.append(.{ .name = name, .kind = entry.kind });
304433 }
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 '..'
307436 try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
308437 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
352481}
353482
354483test "Dir.realpath smoke test" {
355 switch (builtin.os.tag) {
356 .linux, .windows, .macos, .ios, .watchos, .tvos, .solaris => {},
357 else => return error.SkipZigTest,
358 }
359
360 var tmp_dir = tmpDir(.{});
361 defer tmp_dir.cleanup();
362
363 var file = try tmp_dir.dir.createFile("test_file", .{ .lock = .shared });
364 // We need to close the file immediately as otherwise on Windows we'll end up
365 // with a sharing violation.
366 file.close();
367
368 try tmp_dir.dir.makeDir("test_dir");
369
370 var arena = ArenaAllocator.init(testing.allocator);
371 defer arena.deinit();
372 const allocator = arena.allocator();
373
374 const base_path = blk: {
375 const relative_path = try fs.path.join(allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..] });
376 break :blk try fs.realpathAlloc(allocator, relative_path);
377 };
378
379 // First, test non-alloc version
380 {
381 var buf1: [fs.MAX_PATH_BYTES]u8 = undefined;
382
383 const file_path = try tmp_dir.dir.realpath("test_file", buf1[0..]);
384 const expected_file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_file" });
385 try testing.expectEqualStrings(expected_file_path, file_path);
386
387 const dir_path = try tmp_dir.dir.realpath("test_dir", buf1[0..]);
388 const expected_dir_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_dir" });
389 try testing.expectEqualStrings(expected_dir_path, dir_path);
390 }
391
392 // Next, test alloc version
393 {
394 const file_path = try tmp_dir.dir.realpathAlloc(allocator, "test_file");
395 const expected_file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_file" });
396 try testing.expectEqualStrings(expected_file_path, file_path);
397
398 const dir_path = try tmp_dir.dir.realpathAlloc(allocator, "test_dir");
399 const expected_dir_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "test_dir" });
400 try testing.expectEqualStrings(expected_dir_path, dir_path);
401 }
484 if (!comptime std.os.isGetFdPathSupportedOnTarget(builtin.os)) return error.SkipZigTest;
485
486 try testWithAllSupportedPathTypes(struct {
487 fn impl(ctx: *TestContext) !void {
488 const test_file_path = try ctx.transformPath("test_file");
489 const test_dir_path = try ctx.transformPath("test_dir");
490 var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
491
492 // FileNotFound if the path doesn't exist
493 try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(testing.allocator, test_file_path));
494 try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_file_path, &buf));
495 try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(testing.allocator, test_dir_path));
496 try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_dir_path, &buf));
497
498 // Now create the file and dir
499 try ctx.dir.writeFile(test_file_path, "");
500 try ctx.dir.makeDir(test_dir_path);
501
502 const base_path = try ctx.transformPath(".");
503 const base_realpath = try ctx.dir.realpathAlloc(testing.allocator, base_path);
504 defer testing.allocator.free(base_realpath);
505 const expected_file_path = try fs.path.join(
506 testing.allocator,
507 &[_][]const u8{ base_realpath, "test_file" },
508 );
509 defer testing.allocator.free(expected_file_path);
510 const expected_dir_path = try fs.path.join(
511 testing.allocator,
512 &[_][]const u8{ base_realpath, "test_dir" },
513 );
514 defer testing.allocator.free(expected_dir_path);
515
516 // First, test non-alloc version
517 {
518 const file_path = try ctx.dir.realpath(test_file_path, &buf);
519 try testing.expectEqualStrings(expected_file_path, file_path);
520
521 const dir_path = try ctx.dir.realpath(test_dir_path, &buf);
522 try testing.expectEqualStrings(expected_dir_path, dir_path);
523 }
524
525 // Next, test alloc version
526 {
527 const file_path = try ctx.dir.realpathAlloc(testing.allocator, test_file_path);
528 defer testing.allocator.free(file_path);
529 try testing.expectEqualStrings(expected_file_path, file_path);
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);
402537}
403538
404539test "readAllAlloc" {
......@@ -410,7 +545,7 @@ test "readAllAlloc" {
410545
411546 const buf1 = try file.readToEndAlloc(testing.allocator, 1024);
412547 defer testing.allocator.free(buf1);
413 try testing.expect(buf1.len == 0);
548 try testing.expectEqual(@as(usize, 0), buf1.len);
414549
415550 const write_buf: []const u8 = "this is a test.\nthis is a test.\nthis is a test.\nthis is a test.\n";
416551 try file.writeAll(write_buf);
......@@ -420,14 +555,14 @@ test "readAllAlloc" {
420555 const buf2 = try file.readToEndAlloc(testing.allocator, 1024);
421556 defer testing.allocator.free(buf2);
422557 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);
424559 try file.seekTo(0);
425560
426561 // max_bytes == file_size
427562 const buf3 = try file.readToEndAlloc(testing.allocator, write_buf.len);
428563 defer testing.allocator.free(buf3);
429564 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);
431566 try file.seekTo(0);
432567
433568 // max_bytes < file_size
......@@ -435,211 +570,221 @@ test "readAllAlloc" {
435570}
436571
437572test "directory operations on files" {
438 var tmp_dir = tmpDir(.{});
439 defer tmp_dir.cleanup();
440
441 const test_file_name = "test_file";
442
443 var file = try tmp_dir.dir.createFile(test_file_name, .{ .read = true });
444 file.close();
445
446 try testing.expectError(error.PathAlreadyExists, tmp_dir.dir.makeDir(test_file_name));
447 try testing.expectError(error.NotDir, tmp_dir.dir.openDir(test_file_name, .{}));
448 try testing.expectError(error.NotDir, tmp_dir.dir.deleteDir(test_file_name));
449
450 switch (builtin.os.tag) {
451 .wasi, .freebsd, .netbsd, .openbsd, .dragonfly => {},
452 else => {
453 const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_file_name);
454 defer testing.allocator.free(absolute_path);
455
456 try testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(absolute_path));
457 try testing.expectError(error.NotDir, fs.deleteDirAbsolute(absolute_path));
458 },
459 }
460
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();
573 try testWithAllSupportedPathTypes(struct {
574 fn impl(ctx: *TestContext) !void {
575 const test_file_name = try ctx.transformPath("test_file");
576
577 var file = try ctx.dir.createFile(test_file_name, .{ .read = true });
578 file.close();
579
580 try testing.expectError(error.PathAlreadyExists, ctx.dir.makeDir(test_file_name));
581 try testing.expectError(error.NotDir, ctx.dir.openDir(test_file_name, .{}));
582 try testing.expectError(error.NotDir, ctx.dir.deleteDir(test_file_name));
583
584 if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
585 try testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(test_file_name));
586 try testing.expectError(error.NotDir, fs.deleteDirAbsolute(test_file_name));
587 }
588
589 // ensure the file still exists and is a file as a sanity check
590 file = try ctx.dir.openFile(test_file_name, .{});
591 const stat = try file.stat();
592 try testing.expectEqual(File.Kind.file, stat.kind);
593 file.close();
594 }
595 }.impl);
466596}
467597
468598test "file operations on directories" {
469599 // TODO: fix this test on FreeBSD. https://github.com/ziglang/zig/issues/1759
470600 if (builtin.os.tag == .freebsd) return error.SkipZigTest;
471601
472 var tmp_dir = tmpDir(.{});
473 defer tmp_dir.cleanup();
474
475 const test_dir_name = "test_dir";
476
477 try tmp_dir.dir.makeDir(test_dir_name);
478
479 try testing.expectError(error.IsDir, tmp_dir.dir.createFile(test_dir_name, .{}));
480 try testing.expectError(error.IsDir, tmp_dir.dir.deleteFile(test_dir_name));
481 switch (builtin.os.tag) {
482 // no error when reading a directory.
483 .dragonfly, .netbsd => {},
484 // Currently, WASI will return error.Unexpected (via ENOTCAPABLE) when attempting fd_read on a directory handle.
485 // TODO: Re-enable on WASI once https://github.com/bytecodealliance/wasmtime/issues/1935 is resolved.
486 .wasi => {},
487 else => {
488 try testing.expectError(error.IsDir, tmp_dir.dir.readFileAlloc(testing.allocator, test_dir_name, std.math.maxInt(usize)));
489 },
490 }
491 // Note: The `.mode = .read_write` is necessary to ensure the error occurs on all platforms.
492 // TODO: Add a read-only test as well, see https://github.com/ziglang/zig/issues/5732
493 try testing.expectError(error.IsDir, tmp_dir.dir.openFile(test_dir_name, .{ .mode = .read_write }));
494
495 switch (builtin.os.tag) {
496 .wasi, .freebsd, .netbsd, .openbsd, .dragonfly => {},
497 else => {
498 const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_dir_name);
499 defer testing.allocator.free(absolute_path);
500
501 try testing.expectError(error.IsDir, fs.createFileAbsolute(absolute_path, .{}));
502 try testing.expectError(error.IsDir, fs.deleteFileAbsolute(absolute_path));
503 },
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();
602 try testWithAllSupportedPathTypes(struct {
603 fn impl(ctx: *TestContext) !void {
604 const test_dir_name = try ctx.transformPath("test_dir");
605
606 try ctx.dir.makeDir(test_dir_name);
607
608 try testing.expectError(error.IsDir, ctx.dir.createFile(test_dir_name, .{}));
609 try testing.expectError(error.IsDir, ctx.dir.deleteFile(test_dir_name));
610 switch (builtin.os.tag) {
611 // no error when reading a directory.
612 .dragonfly, .netbsd => {},
613 // 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.
615 .wasi => {},
616 else => {
617 try testing.expectError(error.IsDir, ctx.dir.readFileAlloc(testing.allocator, test_dir_name, std.math.maxInt(usize)));
618 },
619 }
620 // 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
622 try testing.expectError(error.IsDir, ctx.dir.openFile(test_dir_name, .{ .mode = .read_write }));
623
624 if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
625 try testing.expectError(error.IsDir, fs.createFileAbsolute(test_dir_name, .{}));
626 try testing.expectError(error.IsDir, fs.deleteFileAbsolute(test_dir_name));
627 }
628
629 // ensure the directory still exists as a sanity check
630 var dir = try ctx.dir.openDir(test_dir_name, .{});
631 dir.close();
632 }
633 }.impl);
509634}
510635
511636test "deleteDir" {
512 var tmp_dir = tmpDir(.{});
513 defer tmp_dir.cleanup();
514
515 // deleting a non-existent directory
516 try testing.expectError(error.FileNotFound, tmp_dir.dir.deleteDir("test_dir"));
517
518 var dir = try tmp_dir.dir.makeOpenPath("test_dir", .{});
519 var file = try dir.createFile("test_file", .{});
520 file.close();
521 dir.close();
522
523 // deleting a non-empty directory
524 try testing.expectError(error.DirNotEmpty, tmp_dir.dir.deleteDir("test_dir"));
525
526 dir = try tmp_dir.dir.openDir("test_dir", .{});
527 try dir.deleteFile("test_file");
528 dir.close();
529
530 // deleting an empty directory
531 try tmp_dir.dir.deleteDir("test_dir");
637 try testWithAllSupportedPathTypes(struct {
638 fn impl(ctx: *TestContext) !void {
639 const test_dir_path = try ctx.transformPath("test_dir");
640 const test_file_path = try ctx.transformPath("test_dir" ++ std.fs.path.sep_str ++ "test_file");
641
642 // deleting a non-existent directory
643 try testing.expectError(error.FileNotFound, ctx.dir.deleteDir(test_dir_path));
644
645 // deleting a non-empty directory
646 try ctx.dir.makeDir(test_dir_path);
647 try ctx.dir.writeFile(test_file_path, "");
648 try testing.expectError(error.DirNotEmpty, ctx.dir.deleteDir(test_dir_path));
649
650 // deleting an empty directory
651 try ctx.dir.deleteFile(test_file_path);
652 try ctx.dir.deleteDir(test_dir_path);
653 }
654 }.impl);
532655}
533656
534657test "Dir.rename files" {
535 var tmp_dir = tmpDir(.{});
536 defer tmp_dir.cleanup();
537
538 try testing.expectError(error.FileNotFound, tmp_dir.dir.rename("missing_file_name", "something_else"));
539
540 // Renaming files
541 const test_file_name = "test_file";
542 const renamed_test_file_name = "test_file_renamed";
543 var file = try tmp_dir.dir.createFile(test_file_name, .{ .read = true });
544 file.close();
545 try tmp_dir.dir.rename(test_file_name, renamed_test_file_name);
546
547 // Ensure the file was renamed
548 try testing.expectError(error.FileNotFound, tmp_dir.dir.openFile(test_file_name, .{}));
549 file = try tmp_dir.dir.openFile(renamed_test_file_name, .{});
550 file.close();
551
552 // Rename to self succeeds
553 try tmp_dir.dir.rename(renamed_test_file_name, renamed_test_file_name);
554
555 // Rename to existing file succeeds
556 var existing_file = try tmp_dir.dir.createFile("existing_file", .{ .read = true });
557 existing_file.close();
558 try tmp_dir.dir.rename(renamed_test_file_name, "existing_file");
559
560 try testing.expectError(error.FileNotFound, tmp_dir.dir.openFile(renamed_test_file_name, .{}));
561 file = try tmp_dir.dir.openFile("existing_file", .{});
562 file.close();
658 try testWithAllSupportedPathTypes(struct {
659 fn impl(ctx: *TestContext) !void {
660 const missing_file_path = try ctx.transformPath("missing_file_name");
661 const something_else_path = try ctx.transformPath("something_else");
662
663 try testing.expectError(error.FileNotFound, ctx.dir.rename(missing_file_path, something_else_path));
664
665 // Renaming files
666 const test_file_name = try ctx.transformPath("test_file");
667 const renamed_test_file_name = try ctx.transformPath("test_file_renamed");
668 var file = try ctx.dir.createFile(test_file_name, .{ .read = true });
669 file.close();
670 try ctx.dir.rename(test_file_name, renamed_test_file_name);
671
672 // Ensure the file was renamed
673 try testing.expectError(error.FileNotFound, ctx.dir.openFile(test_file_name, .{}));
674 file = try ctx.dir.openFile(renamed_test_file_name, .{});
675 file.close();
676
677 // Rename to self succeeds
678 try ctx.dir.rename(renamed_test_file_name, renamed_test_file_name);
679
680 // Rename to existing file succeeds
681 const existing_file_path = try ctx.transformPath("existing_file");
682 var existing_file = try ctx.dir.createFile(existing_file_path, .{ .read = true });
683 existing_file.close();
684 try ctx.dir.rename(renamed_test_file_name, existing_file_path);
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);
563691}
564692
565693test "Dir.rename directories" {
566 var tmp_dir = tmpDir(.{});
567 defer tmp_dir.cleanup();
568
569 // Renaming directories
570 try tmp_dir.dir.makeDir("test_dir");
571 try tmp_dir.dir.rename("test_dir", "test_dir_renamed");
572
573 // Ensure the directory was renamed
574 try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir("test_dir", .{}));
575 var dir = try tmp_dir.dir.openDir("test_dir_renamed", .{});
576
577 // Put a file in the directory
578 var file = try dir.createFile("test_file", .{ .read = true });
579 file.close();
580 dir.close();
581
582 try tmp_dir.dir.rename("test_dir_renamed", "test_dir_renamed_again");
583
584 // Ensure the directory was renamed and the file still exists in it
585 try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir("test_dir_renamed", .{}));
586 dir = try tmp_dir.dir.openDir("test_dir_renamed_again", .{});
587 file = try dir.openFile("test_file", .{});
588 file.close();
589 dir.close();
694 try testWithAllSupportedPathTypes(struct {
695 fn impl(ctx: *TestContext) !void {
696 const test_dir_path = try ctx.transformPath("test_dir");
697 const test_dir_renamed_path = try ctx.transformPath("test_dir_renamed");
698
699 // Renaming directories
700 try ctx.dir.makeDir(test_dir_path);
701 try ctx.dir.rename(test_dir_path, test_dir_renamed_path);
702
703 // Ensure the directory was renamed
704 try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_path, .{}));
705 var dir = try ctx.dir.openDir(test_dir_renamed_path, .{});
706
707 // Put a file in the directory
708 var file = try dir.createFile("test_file", .{ .read = true });
709 file.close();
710 dir.close();
711
712 const test_dir_renamed_again_path = try ctx.transformPath("test_dir_renamed_again");
713 try ctx.dir.rename(test_dir_renamed_path, test_dir_renamed_again_path);
714
715 // Ensure the directory was renamed and the file still exists in it
716 try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_renamed_path, .{}));
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);
590723}
591724
592725test "Dir.rename directory onto empty dir" {
593726 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
594727 if (builtin.os.tag == .windows) return error.SkipZigTest;
595728
596 var tmp_dir = testing.tmpDir(.{});
597 defer tmp_dir.cleanup();
729 try testWithAllSupportedPathTypes(struct {
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");
600 try tmp_dir.dir.makeDir("target_dir");
601 try tmp_dir.dir.rename("test_dir", "target_dir");
734 try ctx.dir.makeDir(test_dir_path);
735 try ctx.dir.makeDir(target_dir_path);
736 try ctx.dir.rename(test_dir_path, target_dir_path);
602737
603 // Ensure the directory was renamed
604 try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir("test_dir", .{}));
605 var dir = try tmp_dir.dir.openDir("target_dir", .{});
606 dir.close();
738 // Ensure the directory was renamed
739 try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_path, .{}));
740 var dir = try ctx.dir.openDir(target_dir_path, .{});
741 dir.close();
742 }
743 }.impl);
607744}
608745
609746test "Dir.rename directory onto non-empty dir" {
610747 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
611748 if (builtin.os.tag == .windows) return error.SkipZigTest;
612749
613 var tmp_dir = testing.tmpDir(.{});
614 defer tmp_dir.cleanup();
750 try testWithAllSupportedPathTypes(struct {
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", .{});
619 var file = try target_dir.createFile("test_file", .{ .read = true });
620 file.close();
621 target_dir.close();
757 var target_dir = try ctx.dir.makeOpenPath(target_dir_path, .{});
758 var file = try target_dir.createFile("test_file", .{ .read = true });
759 file.close();
760 target_dir.close();
622761
623 // 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"));
762 // Rename should fail with PathAlreadyExists if target_dir is non-empty
763 try testing.expectError(error.PathAlreadyExists, ctx.dir.rename(test_dir_path, target_dir_path));
625764
626 // Ensure the directory was not renamed
627 var dir = try tmp_dir.dir.openDir("test_dir", .{});
628 dir.close();
765 // Ensure the directory was not renamed
766 var dir = try ctx.dir.openDir(test_dir_path, .{});
767 dir.close();
768 }
769 }.impl);
629770}
630771
631772test "Dir.rename file <-> dir" {
632773 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
633774 if (builtin.os.tag == .windows) return error.SkipZigTest;
634775
635 var tmp_dir = tmpDir(.{});
636 defer tmp_dir.cleanup();
776 try testWithAllSupportedPathTypes(struct {
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 });
639 file.close();
640 try tmp_dir.dir.makeDir("test_dir");
641 try testing.expectError(error.IsDir, tmp_dir.dir.rename("test_file", "test_dir"));
642 try testing.expectError(error.NotDir, tmp_dir.dir.rename("test_dir", "test_file"));
781 var file = try ctx.dir.createFile(test_file_path, .{ .read = true });
782 file.close();
783 try ctx.dir.makeDir(test_dir_path);
784 try testing.expectError(error.IsDir, ctx.dir.rename(test_file_path, test_dir_path));
785 try testing.expectError(error.NotDir, ctx.dir.rename(test_dir_path, test_file_path));
786 }
787 }.impl);
643788}
644789
645790test "rename" {
......@@ -697,7 +842,7 @@ test "renameAbsolute" {
697842 try testing.expectError(error.FileNotFound, tmp_dir.dir.openFile(test_file_name, .{}));
698843 file = try tmp_dir.dir.openFile(renamed_test_file_name, .{});
699844 const stat = try file.stat();
700 try testing.expect(stat.kind == .file);
845 try testing.expectEqual(File.Kind.file, stat.kind);
701846 file.close();
702847
703848 // Renaming directories
......@@ -723,35 +868,33 @@ test "openSelfExe" {
723868}
724869
725870test "makePath, put some files in it, deleteTree" {
726 var tmp = tmpDir(.{});
727 defer tmp.cleanup();
871 try testWithAllSupportedPathTypes(struct {
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");
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");
731 try tmp.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");
733 if (tmp.dir.openDir("os_test_tmp", .{})) |dir| {
734 _ = dir;
735 @panic("expected error");
736 } else |err| {
737 try testing.expect(err == error.FileNotFound);
738 }
875 try ctx.dir.makePath("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c");
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");
877 try ctx.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "file2.txt", "blah");
878
879 try ctx.dir.deleteTree(dir_path);
880 try testing.expectError(error.FileNotFound, ctx.dir.openDir(dir_path, .{}));
881 }
882 }.impl);
739883}
740884
741885test "makePath, put some files in it, deleteTreeMinStackSize" {
742 var tmp = tmpDir(.{});
743 defer tmp.cleanup();
886 try testWithAllSupportedPathTypes(struct {
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");
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");
747 try tmp.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");
749 if (tmp.dir.openDir("os_test_tmp", .{})) |dir| {
750 _ = dir;
751 @panic("expected error");
752 } else |err| {
753 try testing.expect(err == error.FileNotFound);
754 }
890 try ctx.dir.makePath("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c");
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");
892 try ctx.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "file2.txt", "blah");
893
894 try ctx.dir.deleteTreeMinStackSize(dir_path);
895 try testing.expectError(error.FileNotFound, ctx.dir.openDir(dir_path, .{}));
896 }
897 }.impl);
755898}
756899
757900test "makePath in a directory that no longer exists" {
......@@ -792,9 +935,9 @@ test "max file name component lengths" {
792935 defer tmp.cleanup();
793936
794937 if (builtin.os.tag == .windows) {
795 // € is the character with the largest codepoint that is encoded as a single u16 in UTF-16,
796 // so Windows allows for NAME_MAX of them
797 const maxed_windows_filename = ("€".*) ** std.os.windows.NAME_MAX;
938 // U+FFFF is the character with the largest code point that is encoded as a single
939 // UTF-16 code unit, so Windows allows for NAME_MAX of them.
940 const maxed_windows_filename = ("\u{FFFF}".*) ** std.os.windows.NAME_MAX;
798941 try testFilenameLimits(tmp.iterable_dir, &maxed_windows_filename);
799942 } else if (builtin.os.tag == .wasi) {
800943 // On WASI, the maxed filename depends on the host OS, so in order for this test to
......@@ -892,22 +1035,19 @@ test "pwritev, preadv" {
8921035}
8931036
8941037test "access file" {
895 if (builtin.os.tag == .wasi) return error.SkipZigTest;
896
897 var tmp = tmpDir(.{});
898 defer tmp.cleanup();
1038 try testWithAllSupportedPathTypes(struct {
1039 fn impl(ctx: *TestContext) !void {
1040 const dir_path = try ctx.transformPath("os_test_tmp");
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");
901 if (tmp.dir.access("os_test_tmp" ++ fs.path.sep_str ++ "file.txt", .{})) |ok| {
902 _ = ok;
903 @panic("expected error");
904 } else |err| {
905 try testing.expect(err == error.FileNotFound);
906 }
1043 try ctx.dir.makePath(dir_path);
1044 try testing.expectError(error.FileNotFound, ctx.dir.access(file_path, .{}));
9071045
908 try tmp.dir.writeFile("os_test_tmp" ++ fs.path.sep_str ++ "file.txt", "");
909 try tmp.dir.access("os_test_tmp" ++ fs.path.sep_str ++ "file.txt", .{});
910 try tmp.dir.deleteTree("os_test_tmp");
1046 try ctx.dir.writeFile(file_path, "");
1047 try ctx.dir.access(file_path, .{});
1048 try ctx.dir.deleteTree(dir_path);
1049 }
1050 }.impl);
9111051}
9121052
9131053test "sendfile" {
......@@ -972,7 +1112,7 @@ test "sendfile" {
9721112 .header_count = 2,
9731113 });
9741114 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]);
9761116}
9771117
9781118test "copyRangeAll" {
......@@ -998,29 +1138,30 @@ test "copyRangeAll" {
9981138 _ = try src_file.copyRangeAll(0, dest_file, 0, data.len);
9991139
10001140 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]);
10021142}
10031143
1004test "fs.copyFile" {
1005 const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP";
1006 const src_file = "tmp_test_copy_file.txt";
1007 const dest_file = "tmp_test_copy_file2.txt";
1008 const dest_file2 = "tmp_test_copy_file3.txt";
1144test "copyFile" {
1145 try testWithAllSupportedPathTypes(struct {
1146 fn impl(ctx: *TestContext) !void {
1147 const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP";
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(.{});
1011 defer tmp.cleanup();
1152 try ctx.dir.writeFile(src_file, data);
1153 defer ctx.dir.deleteFile(src_file) catch {};
10121154
1013 try tmp.dir.writeFile(src_file, data);
1014 defer tmp.dir.deleteFile(src_file) catch {};
1155 try ctx.dir.copyFile(src_file, ctx.dir, dest_file, .{});
1156 defer ctx.dir.deleteFile(dest_file) catch {};
10151157
1016 try tmp.dir.copyFile(src_file, tmp.dir, dest_file, .{});
1017 defer tmp.dir.deleteFile(dest_file) catch {};
1158 try ctx.dir.copyFile(src_file, ctx.dir, dest_file2, .{ .override_mode = File.default_mode });
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 });
1020 defer tmp.dir.deleteFile(dest_file2) catch {};
1021
1022 try expectFileContents(tmp.dir, dest_file, data);
1023 try expectFileContents(tmp.dir, dest_file2, data);
1161 try expectFileContents(ctx.dir, dest_file, data);
1162 try expectFileContents(ctx.dir, dest_file2, data);
1163 }
1164 }.impl);
10241165}
10251166
10261167fn 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 {
10311172}
10321173
10331174test "AtomicFile" {
1034 const test_out_file = "tmp_atomic_file_test_dest.txt";
1035 const test_content =
1036 \\ hello!
1037 \\ this is a test file
1038 ;
1039
1040 var tmp = tmpDir(.{});
1041 defer tmp.cleanup();
1042
1043 {
1044 var af = try tmp.dir.atomicFile(test_out_file, .{});
1045 defer af.deinit();
1046 try af.file.writeAll(test_content);
1047 try af.finish();
1048 }
1049 const content = try tmp.dir.readFileAlloc(testing.allocator, test_out_file, 9999);
1050 defer testing.allocator.free(content);
1051 try testing.expect(mem.eql(u8, content, test_content));
1052
1053 try tmp.dir.deleteFile(test_out_file);
1054}
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));
1175 try testWithAllSupportedPathTypes(struct {
1176 fn impl(ctx: *TestContext) !void {
1177 const test_out_file = try ctx.transformPath("tmp_atomic_file_test_dest.txt");
1178 const test_content =
1179 \\ hello!
1180 \\ this is a test file
1181 ;
1182
1183 {
1184 var af = try ctx.dir.atomicFile(test_out_file, .{});
1185 defer af.deinit();
1186 try af.file.writeAll(test_content);
1187 try af.finish();
1188 }
1189 const content = try ctx.dir.readFileAlloc(testing.allocator, test_out_file, 9999);
1190 defer testing.allocator.free(content);
1191 try testing.expectEqualStrings(test_content, content);
1192
1193 try ctx.dir.deleteFile(test_out_file);
1194 }
1195 }.impl);
10611196}
10621197
10631198test "open file with exclusive nonblocking lock twice" {
10641199 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(.{});
1069 defer tmp.cleanup();
1205 const file1 = try ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1206 defer file1.close();
10701207
1071 const file1 = try tmp.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1072 defer file1.close();
1073
1074 const file2 = tmp.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1075 try testing.expectError(error.WouldBlock, file2);
1208 const file2 = ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1209 try testing.expectError(error.WouldBlock, file2);
1210 }
1211 }.impl);
10761212}
10771213
10781214test "open file with shared and exclusive nonblocking lock" {
10791215 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(.{});
1084 defer tmp.cleanup();
1221 const file1 = try ctx.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });
1222 defer file1.close();
10851223
1086 const file1 = try tmp.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });
1087 defer file1.close();
1088
1089 const file2 = tmp.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1090 try testing.expectError(error.WouldBlock, file2);
1224 const file2 = ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1225 try testing.expectError(error.WouldBlock, file2);
1226 }
1227 }.impl);
10911228}
10921229
10931230test "open file with exclusive and shared nonblocking lock" {
10941231 if (builtin.os.tag == .wasi) return error.SkipZigTest;
10951232
1096 const filename = "file_nonblocking_lock_test.txt";
1097
1098 var tmp = tmpDir(.{});
1099 defer tmp.cleanup();
1233 try testWithAllSupportedPathTypes(struct {
1234 fn impl(ctx: *TestContext) !void {
1235 const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
11001236
1101 const file1 = try tmp.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1102 defer file1.close();
1237 const file1 = try ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1238 defer file1.close();
11031239
1104 const file2 = tmp.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });
1105 try testing.expectError(error.WouldBlock, file2);
1240 const file2 = ctx.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });
1241 try testing.expectError(error.WouldBlock, file2);
1242 }
1243 }.impl);
11061244}
11071245
11081246test "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" {
11131251 return error.SkipZigTest;
11141252 }
11151253
1116 const filename = "file_lock_test.txt";
1117
1118 var tmp = tmpDir(.{});
1119 defer tmp.cleanup();
1120
1121 const file = try tmp.dir.createFile(filename, .{ .lock = .exclusive });
1122 errdefer file.close();
1123
1124 const S = struct {
1125 fn checkFn(dir: *fs.Dir, started: *std.Thread.ResetEvent, locked: *std.Thread.ResetEvent) !void {
1126 started.set();
1127 const file1 = try dir.createFile(filename, .{ .lock = .exclusive });
1128
1129 locked.set();
1130 file1.close();
1254 try testWithAllSupportedPathTypes(struct {
1255 fn impl(ctx: *TestContext) !void {
1256 const filename = try ctx.transformPath("file_lock_test.txt");
1257
1258 const file = try ctx.dir.createFile(filename, .{ .lock = .exclusive });
1259 errdefer file.close();
1260
1261 const S = struct {
1262 fn checkFn(dir: *fs.Dir, path: []const u8, started: *std.Thread.ResetEvent, locked: *std.Thread.ResetEvent) !void {
1263 started.set();
1264 const file1 = try dir.createFile(path, .{ .lock = .exclusive });
1265
1266 locked.set();
1267 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();
11311290 }
1132 };
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();
1291 }.impl);
11521292}
11531293
11541294test "open file with exclusive nonblocking lock twice (absolute paths)" {
......@@ -1264,29 +1404,36 @@ test "walker without fully iterating" {
12641404test ". and .. in fs.Dir functions" {
12651405 if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
12661406
1267 var tmp = tmpDir(.{});
1268 defer tmp.cleanup();
1269
1270 try tmp.dir.makeDir("./subdir");
1271 try tmp.dir.access("./subdir", .{});
1272 var created_subdir = try tmp.dir.openDir("./subdir", .{});
1273 created_subdir.close();
1274
1275 const created_file = try tmp.dir.createFile("./subdir/../file", .{});
1276 created_file.close();
1277 try tmp.dir.access("./subdir/../file", .{});
1278
1279 try tmp.dir.copyFile("./subdir/../file", tmp.dir, "./subdir/../copy", .{});
1280 try tmp.dir.rename("./subdir/../copy", "./subdir/../rename");
1281 const renamed_file = try tmp.dir.openFile("./subdir/../rename", .{});
1282 renamed_file.close();
1283 try tmp.dir.deleteFile("./subdir/../rename");
1284
1285 try tmp.dir.writeFile("./subdir/../update", "something");
1286 const prev_status = try tmp.dir.updateFile("./subdir/../file", tmp.dir, "./subdir/../update", .{});
1287 try testing.expectEqual(fs.PrevStatus.stale, prev_status);
1288
1289 try tmp.dir.deleteDir("./subdir");
1407 try testWithAllSupportedPathTypes(struct {
1408 fn impl(ctx: *TestContext) !void {
1409 const subdir_path = try ctx.transformPath("./subdir");
1410 const file_path = try ctx.transformPath("./subdir/../file");
1411 const copy_path = try ctx.transformPath("./subdir/../copy");
1412 const rename_path = try ctx.transformPath("./subdir/../rename");
1413 const update_path = try ctx.transformPath("./subdir/../update");
1414
1415 try ctx.dir.makeDir(subdir_path);
1416 try ctx.dir.access(subdir_path, .{});
1417 var created_subdir = try ctx.dir.openDir(subdir_path, .{});
1418 created_subdir.close();
1419
1420 const created_file = try ctx.dir.createFile(file_path, .{});
1421 created_file.close();
1422 try ctx.dir.access(file_path, .{});
1423
1424 try ctx.dir.copyFile(file_path, ctx.dir, copy_path, .{});
1425 try ctx.dir.rename(copy_path, rename_path);
1426 const renamed_file = try ctx.dir.openFile(rename_path, .{});
1427 renamed_file.close();
1428 try ctx.dir.deleteFile(rename_path);
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);
12901437}
12911438
12921439test ". and .. in absolute functions" {
......@@ -1342,17 +1489,17 @@ test "chmod" {
13421489
13431490 const file = try tmp.dir.createFile("test_file", .{ .mode = 0o600 });
13441491 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
13471494 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
13501497 try tmp.dir.makeDir("test_dir");
13511498 var iterable_dir = try tmp.dir.openIterableDir("test_dir", .{});
13521499 defer iterable_dir.close();
13531500
13541501 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);
13561503}
13571504
13581505test "chown" {
......@@ -1381,8 +1528,8 @@ test "File.Metadata" {
13811528 defer file.close();
13821529
13831530 const metadata = try file.metadata();
1384 try testing.expect(metadata.kind() == .file);
1385 try testing.expect(metadata.size() == 0);
1531 try testing.expectEqual(File.Kind.file, metadata.kind());
1532 try testing.expectEqual(@as(u64, 0), metadata.size());
13861533 _ = metadata.accessed();
13871534 _ = metadata.modified();
13881535 _ = 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
51695169 return getFdPath(h_file, out_buffer);
51705170}
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
51725188/// Return canonical path of handle `fd`.
51735189/// This function is very host-specific and is not universally supported by all hosts.
51745190/// For example, while it generally works on Linux, macOS, FreeBSD or Windows, it is
51755191/// unsupported on WASI.
51765192pub 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 }
51775196 switch (builtin.os.tag) {
51785197 .windows => {
51795198 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 {
52765295 }
52775296 },
52785297 .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 }
52825298 @memset(out_buffer[0..MAX_PATH_BYTES], 0);
52835299 switch (errno(system.fcntl(fd, F.GETPATH, out_buffer))) {
52845300 .SUCCESS => {},
......@@ -5290,9 +5306,6 @@ pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 {
52905306 return out_buffer[0..len];
52915307 },
52925308 .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 }
52965309 @memset(out_buffer[0..MAX_PATH_BYTES], 0);
52975310 switch (errno(system.fcntl(fd, F.GETPATH, out_buffer))) {
52985311 .SUCCESS => {},
......@@ -5306,7 +5319,7 @@ pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 {
53065319 const len = mem.indexOfScalar(u8, out_buffer[0..], @as(u8, 0)) orelse MAX_PATH_BYTES;
53075320 return out_buffer[0..len];
53085321 },
5309 else => @compileError("querying for canonical path of a handle is unsupported on this host"),
5322 else => unreachable, // made unreachable by isGetFdPathSupportedOnTarget above
53105323 }
53115324}
53125325
lib/std/os/test.zig+2-2
......@@ -193,7 +193,7 @@ test "symlink with relative paths" {
193193 os.windows.CreateSymbolicLink(
194194 cwd.fd,
195195 &[_]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' },
197197 false,
198198 ) catch |err| switch (err) {
199199 // Symlink requires admin privileges on windows, so this test can legitimately fail.
......@@ -351,7 +351,7 @@ test "readlinkat" {
351351 os.windows.CreateSymbolicLink(
352352 tmp.dir.fd,
353353 &[_]u16{ 'l', 'i', 'n', 'k' },
354 &[_]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
354 &[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
355355 false,
356356 ) catch |err| switch (err) {
357357 // 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{
704704 NameTooLong,
705705 NoDevice,
706706 NetworkNotFound,
707 BadPathName,
707708 Unexpected,
708709};
709710
......@@ -716,7 +717,7 @@ pub const CreateSymbolicLinkError = error{
716717pub fn CreateSymbolicLink(
717718 dir: ?HANDLE,
718719 sym_link_path: []const u16,
719 target_path: []const u16,
720 target_path: [:0]const u16,
720721 is_directory: bool,
721722) CreateSymbolicLinkError!void {
722723 const SYMLINK_DATA = extern struct {
......@@ -745,25 +746,58 @@ pub fn CreateSymbolicLink(
745746 };
746747 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
748781 // prepare reparse data buffer
749782 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;
751784 const header_len = @sizeOf(ULONG) + @sizeOf(USHORT) * 2;
785 const target_is_absolute = std.fs.path.isAbsoluteWindowsWTF16(final_target_path);
752786 const symlink_data = SYMLINK_DATA{
753787 .ReparseTag = IO_REPARSE_TAG_SYMLINK,
754788 .ReparseDataLength = @as(u16, @intCast(buf_len - header_len)),
755789 .Reserved = 0,
756 .SubstituteNameOffset = @as(u16, @intCast(target_path.len * 2)),
757 .SubstituteNameLength = @as(u16, @intCast(target_path.len * 2)),
790 .SubstituteNameOffset = @as(u16, @intCast(final_target_path.len * 2)),
791 .SubstituteNameLength = @as(u16, @intCast(final_target_path.len * 2)),
758792 .PrintNameOffset = 0,
759 .PrintNameLength = @as(u16, @intCast(target_path.len * 2)),
760 .Flags = if (dir) |_| SYMLINK_FLAG_RELATIVE else 0,
793 .PrintNameLength = @as(u16, @intCast(final_target_path.len * 2)),
794 .Flags = if (!target_is_absolute) SYMLINK_FLAG_RELATIVE else 0,
761795 };
762796
763797 @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)));
765 const paths_start = @sizeOf(SYMLINK_DATA) + target_path.len * 2;
766 @memcpy(buffer[paths_start..][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)));
799 const paths_start = @sizeOf(SYMLINK_DATA) + final_target_path.len * 2;
800 @memcpy(buffer[paths_start..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
767801 _ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null);
768802}
769803
......@@ -861,12 +895,15 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin
861895}
862896
863897fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 {
864 const prefix = [_]u16{ '\\', '?', '?', '\\' };
865 var start_index: usize = 0;
866 if (!is_relative and std.mem.startsWith(u16, path, &prefix)) {
867 start_index = prefix.len;
868 }
869 const out_len = std.unicode.utf16leToUtf8(out_buffer, path[start_index..]) catch unreachable;
898 const win32_namespace_path = path: {
899 if (is_relative) break :path path;
900 const win32_path = ntToWin32Namespace(path) catch |err| switch (err) {
901 error.NameTooLong => unreachable,
902 error.NotNtPath => break :path path,
903 };
904 break :path win32_path.span();
905 };
906 const out_len = std.unicode.utf16leToUtf8(out_buffer, win32_namespace_path) catch unreachable;
870907 return out_buffer[0..out_len];
871908}
872909
......@@ -1189,8 +1226,18 @@ pub fn GetFinalPathNameByHandle(
11891226
11901227 const file_path_begin_index = mem.indexOfPos(u16, final_path, expected_prefix.len, &[_]u16{'\\'}) orelse unreachable;
11911228 const volume_name_u16 = final_path[0..file_path_begin_index];
1229 const device_name_u16 = volume_name_u16[expected_prefix.len..];
11921230 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
11941241 // Get DOS volume name. DOS volume names are actually symbolic link objects to the
11951242 // actual NT volume. For example:
11961243 // (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C:
......@@ -2296,25 +2343,26 @@ pub const NamespacePrefix = enum {
22962343 nt,
22972344};
22982345
2346/// If `T` is `u16`, then `path` should be encoded as UTF-16LE.
22992347pub fn getNamespacePrefix(comptime T: type, path: []const T) NamespacePrefix {
23002348 if (path.len < 4) return .none;
2301 var all_backslash = switch (path[0]) {
2349 var all_backslash = switch (mem.littleToNative(T, path[0])) {
23022350 '\\' => true,
23032351 '/' => false,
23042352 else => return .none,
23052353 };
2306 all_backslash = all_backslash and switch (path[3]) {
2354 all_backslash = all_backslash and switch (mem.littleToNative(T, path[3])) {
23072355 '\\' => true,
23082356 '/' => false,
23092357 else => return .none,
23102358 };
2311 switch (path[1]) {
2312 '?' => if (path[2] == '?' and all_backslash) return .nt else return .none,
2359 switch (mem.littleToNative(T, path[1])) {
2360 '?' => if (mem.littleToNative(T, path[2]) == '?' and all_backslash) return .nt else return .none,
23132361 '\\' => {},
23142362 '/' => all_backslash = false,
23152363 else => return .none,
23162364 }
2317 return switch (path[2]) {
2365 return switch (mem.littleToNative(T, path[2])) {
23182366 '?' => if (all_backslash) .verbatim else .fake_verbatim,
23192367 '.' => .local_device,
23202368 else => .none,
......@@ -2349,6 +2397,7 @@ pub const UnprefixedPathType = enum {
23492397
23502398/// Get the path type of a path that is known to not have any namespace prefixes
23512399/// (`\\?\`, `\\.\`, `\??\`).
2400/// If `T` is `u16`, then `path` should be encoded as UTF-16LE.
23522401pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathType {
23532402 if (path.len < 1) return .relative;
23542403
......@@ -2357,18 +2406,18 @@ pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathTy
23572406 }
23582407
23592408 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]))) {
23612410 // \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;
23632412 // 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;
23652414 // \\x
23662415 return .unc_absolute;
23672416 } else {
23682417 // x
2369 if (path.len < 2 or path[1] != ':') return .relative;
2418 if (path.len < 2 or mem.littleToNative(T, path[1]) != ':') return .relative;
23702419 // 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;
23722421 // x:
23732422 return .drive_relative;
23742423 }
......@@ -2393,6 +2442,73 @@ test getUnprefixedPathType {
23932442 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));
23942443}
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
23962512fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {
23972513 const result = kernel32.GetFullPathNameW(path, @as(u32, @intCast(out.len)), out.ptr, null);
23982514 if (result == 0) {