1const builtin = @import("builtin");
2const native_os = builtin.os.tag;
3
4const std = @import("../std.zig");
5const Io = std.Io;
6const mem = std.mem;
7const Allocator = std.mem.Allocator;
8const wasi = std.os.wasi;
9const windows = std.os.windows;
10const ArenaAllocator = std.heap.ArenaAllocator;
11const Dir = std.Io.Dir;
12const File = std.Io.File;
13const SymLinkFlags = std.Io.Dir.SymLinkFlags;
14
15const testing = std.testing;
16const expect = std.testing.expect;
17const expectEqual = std.testing.expectEqual;
18const expectEqualSlices = std.testing.expectEqualSlices;
19const expectEqualStrings = std.testing.expectEqualStrings;
20const expectError = std.testing.expectError;
21const tmpDir = std.testing.tmpDir;
22
23// This is kept in sync with Io.Threaded.realPath .
24pub inline fn isRealPathSupported() bool {
25 return switch (native_os) {
26 .windows,
27 .driverkit,
28 .ios,
29 .maccatalyst,
30 .macos,
31 .tvos,
32 .visionos,
33 .watchos,
34 .linux,
35 .serenity,
36 .illumos,
37 .freebsd,
38 => true,
39 .dragonfly => builtin.os.version_range.semver.min.order(.{ .major = 6, .minor = 0, .patch = 0 }) != .lt,
40 else => false,
41 };
42}
43
44const PathType = enum {
45 relative,
46 absolute,
47 unc,
48
49 fn isSupported(self: PathType, target_os: std.Target.Os) bool {
50 return switch (self) {
51 .relative => true,
52 .absolute => isRealPathSupported(),
53 .unc => target_os.tag == .windows,
54 };
55 }
56
57 const TransformError = Dir.RealPathError || error{OutOfMemory};
58 const TransformFn = fn (Allocator, Io, Dir, relative_path: [:0]const u8) TransformError![:0]const u8;
59
60 fn getTransformFn(comptime path_type: PathType) TransformFn {
61 switch (path_type) {
62 .relative => return struct {
63 fn transform(allocator: Allocator, io: Io, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8 {
64 _ = allocator;
65 _ = io;
66 _ = dir;
67 return relative_path;
68 }
69 }.transform,
70 .absolute => return struct {
71 fn transform(allocator: Allocator, io: Io, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8 {
72 // The final path may not actually exist which would cause realpath to fail.
73 // So instead, we get the path of the dir and join it with the relative path.
74 var fd_path_buf: [Dir.max_path_bytes]u8 = undefined;
75 const dir_path = fd_path_buf[0..try dir.realPath(io, &fd_path_buf)];
76 return Dir.path.joinZ(allocator, &.{ dir_path, relative_path });
77 }
78 }.transform,
79 .unc => return struct {
80 fn transform(allocator: Allocator, io: Io, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8 {
81 // Any drive absolute path (C:\foo) can be converted into a UNC path by
82 // using '127.0.0.1' as the server name and '<drive letter>$' as the share name.
83 var fd_path_buf: [Dir.max_path_bytes]u8 = undefined;
84 const dir_path = fd_path_buf[0..try dir.realPath(io, &fd_path_buf)];
85 const windows_path_type = Dir.path.getWin32PathType(u8, dir_path);
86 switch (windows_path_type) {
87 .unc_absolute => return Dir.path.joinZ(allocator, &.{ dir_path, relative_path }),
88 .drive_absolute => {
89 // `C:\<...>` -> `\\127.0.0.1\C$\<...>`
90 const prepended = "\\\\127.0.0.1\\";
91 var path = try Dir.path.joinZ(allocator, &.{ prepended, dir_path, relative_path });
92 path[prepended.len + 1] = '$';
93 return path;
94 },
95 else => unreachable,
96 }
97 }
98 }.transform,
99 }
100 }
101};
102
103const TestContext = struct {
104 io: Io,
105 path_type: PathType,
106 path_sep: u8,
107 arena: ArenaAllocator,
108 tmp: testing.TmpDir,
109 dir: Dir,
110 transform_fn: *const PathType.TransformFn,
111
112 pub fn init(path_type: PathType, path_sep: u8, allocator: Allocator, transform_fn: *const PathType.TransformFn) TestContext {
113 const tmp = tmpDir(.{ .iterate = true });
114 return .{
115 .io = testing.io,
116 .path_type = path_type,
117 .path_sep = path_sep,
118 .arena = ArenaAllocator.init(allocator),
119 .tmp = tmp,
120 .dir = tmp.dir,
121 .transform_fn = transform_fn,
122 };
123 }
124
125 pub fn deinit(self: *TestContext) void {
126 self.arena.deinit();
127 self.tmp.cleanup();
128 }
129
130 /// Returns the `relative_path` transformed into the TestContext's `path_type`,
131 /// with any supported path separators replaced by `path_sep`.
132 /// The result is allocated by the TestContext's arena and will be free'd during
133 /// `TestContext.deinit`.
134 pub fn transformPath(self: *TestContext, relative_path: [:0]const u8) ![:0]const u8 {
135 const allocator = self.arena.allocator();
136 const transformed_path = try self.transform_fn(allocator, self.io, self.dir, relative_path);
137 if (native_os == .windows) {
138 const transformed_sep_path = try allocator.dupeSentinel(u8, transformed_path, 0);
139 std.mem.replaceScalar(u8, transformed_sep_path, switch (self.path_sep) {
140 '/' => '\\',
141 '\\' => '/',
142 else => unreachable,
143 }, self.path_sep);
144 return transformed_sep_path;
145 }
146 return transformed_path;
147 }
148
149 /// Replaces any path separators with the canonical path separator for the platform
150 /// (e.g. all path separators are converted to `\` on Windows).
151 /// If path separators are replaced, then the result is allocated by the
152 /// TestContext's arena and will be free'd during `TestContext.deinit`.
153 pub fn toCanonicalPathSep(self: *TestContext, path: [:0]const u8) ![:0]const u8 {
154 if (native_os == .windows) {
155 const allocator = self.arena.allocator();
156 const transformed_sep_path = try allocator.dupeSentinel(u8, path, 0);
157 std.mem.replaceScalar(u8, transformed_sep_path, '/', '\\');
158 return transformed_sep_path;
159 }
160 return path;
161 }
162};
163
164/// `test_func` must be a function that takes a `*TestContext` as a parameter and returns `!void`.
165/// `test_func` will be called once for each PathType that the current target supports,
166/// and will be passed a TestContext that can transform a relative path into the path type under test.
167/// The TestContext will also create a tmp directory for you (and will clean it up for you too).
168fn testWithAllSupportedPathTypes(test_func: anytype) !void {
169 try testWithPathTypeIfSupported(.relative, '/', test_func);
170 try testWithPathTypeIfSupported(.absolute, '/', test_func);
171 try testWithPathTypeIfSupported(.unc, '/', test_func);
172 try testWithPathTypeIfSupported(.relative, '\\', test_func);
173 try testWithPathTypeIfSupported(.absolute, '\\', test_func);
174 try testWithPathTypeIfSupported(.unc, '\\', test_func);
175}
176
177fn testWithPathTypeIfSupported(comptime path_type: PathType, comptime path_sep: u8, test_func: anytype) !void {
178 if (!(comptime path_type.isSupported(builtin.os))) return;
179 if (!(comptime Dir.path.isSep(path_sep))) return;
180
181 var ctx = TestContext.init(path_type, path_sep, testing.allocator, path_type.getTransformFn());
182 defer ctx.deinit();
183
184 try test_func(&ctx);
185}
186
187// For use in test setup. If the symlink creation fails on Windows with
188// AccessDenied/PermissionDenied/FileSystem, then make the test failure silent (it is not a Zig failure).
189fn setupSymlink(io: Io, dir: Dir, target: []const u8, link: []const u8, flags: SymLinkFlags) !void {
190 return dir.symLink(io, target, link, flags) catch |err| switch (err) {
191 // On Windows, symlinks require admin privileges and the underlying filesystem must support symlinks
192 error.AccessDenied, error.PermissionDenied, error.FileSystem => if (native_os == .windows) return error.SkipZigTest else return err,
193 else => return err,
194 };
195}
196
197// For use in test setup. If the symlink creation fails on Windows with
198// AccessDeniedPermissionDenied/FileSystem, then make the test failure silent (it is not a Zig failure).
199fn setupSymlinkAbsolute(io: Io, target: []const u8, link: []const u8, flags: SymLinkFlags) !void {
200 return Dir.symLinkAbsolute(io, target, link, flags) catch |err| switch (err) {
201 // On Windows, symlinks require admin privileges and the underlying filesystem must support symlinks
202 error.AccessDenied, error.PermissionDenied, error.FileSystem => if (native_os == .windows) return error.SkipZigTest else return err,
203 else => return err,
204 };
205}
206
207test "Dir.readLink" {
208 const io = testing.io;
209
210 try testWithAllSupportedPathTypes(struct {
211 fn impl(ctx: *TestContext) !void {
212 // Create some targets
213 const file_target_path = try ctx.transformPath("file.txt");
214 try ctx.dir.writeFile(io, .{ .sub_path = file_target_path, .data = "nonsense" });
215 const dir_target_path = try ctx.transformPath("subdir");
216 try ctx.dir.createDir(io, dir_target_path, .default_dir);
217
218 // On Windows, symlink targets always use the canonical path separator
219 const canonical_file_target_path = try ctx.toCanonicalPathSep(file_target_path);
220 const canonical_dir_target_path = try ctx.toCanonicalPathSep(dir_target_path);
221
222 // test 1: symlink to a file
223 try setupSymlink(io, ctx.dir, file_target_path, "symlink1", .{});
224 try testReadLink(io, ctx.dir, canonical_file_target_path, "symlink1");
225
226 // test 2: symlink to a directory (can be different on Windows)
227 try setupSymlink(io, ctx.dir, dir_target_path, "symlink2", .{ .is_directory = true });
228 try testReadLink(io, ctx.dir, canonical_dir_target_path, "symlink2");
229
230 // test 3: relative path symlink
231 const parent_file = ".." ++ Dir.path.sep_str ++ "target.txt";
232 const canonical_parent_file = try ctx.toCanonicalPathSep(parent_file);
233 var subdir = try ctx.dir.createDirPathOpen(io, "subdir", .{});
234 defer subdir.close(io);
235 try setupSymlink(io, subdir, canonical_parent_file, "relative-link.txt", .{});
236 try testReadLink(io, subdir, canonical_parent_file, "relative-link.txt");
237 }
238 }.impl);
239}
240
241test "Dir.readLink on non-symlinks" {
242 try testWithAllSupportedPathTypes(struct {
243 fn impl(ctx: *TestContext) !void {
244 const io = ctx.io;
245 const file_path = try ctx.transformPath("file.txt");
246 try ctx.dir.writeFile(io, .{ .sub_path = file_path, .data = "nonsense" });
247 const dir_path = try ctx.transformPath("subdir");
248 try ctx.dir.createDir(io, dir_path, .default_dir);
249
250 // file
251 var buffer: [Dir.max_path_bytes]u8 = undefined;
252 try std.testing.expectError(error.NotLink, ctx.dir.readLink(io, file_path, &buffer));
253
254 // dir
255 try std.testing.expectError(error.NotLink, ctx.dir.readLink(io, dir_path, &buffer));
256 }
257 }.impl);
258}
259
260fn testReadLink(io: Io, dir: Dir, target_path: []const u8, symlink_path: []const u8) !void {
261 var buffer: [Dir.max_path_bytes]u8 = undefined;
262 const actual = buffer[0..try dir.readLink(io, symlink_path, &buffer)];
263 try expectEqualStrings(target_path, actual);
264}
265
266fn testReadLinkAbsolute(io: Io, target_path: []const u8, symlink_path: []const u8) !void {
267 var buffer: [Dir.max_path_bytes]u8 = undefined;
268 const given = buffer[0..try Dir.readLinkAbsolute(io, symlink_path, &buffer)];
269 try expectEqualStrings(target_path, given);
270}
271
272test "File.stat on a File that is a symlink returns Kind.sym_link" {
273 const io = testing.io;
274
275 // This test requires getting a file descriptor of a symlink which is not
276 // possible on all targets.
277 switch (builtin.target.os.tag) {
278 .windows, .linux => {},
279 else => return error.SkipZigTest,
280 }
281
282 try testWithAllSupportedPathTypes(struct {
283 fn impl(ctx: *TestContext) !void {
284 const dir_target_path = try ctx.transformPath("subdir");
285 try ctx.dir.createDir(io, dir_target_path, .default_dir);
286
287 try setupSymlink(io, ctx.dir, dir_target_path, "symlink", .{ .is_directory = true });
288
289 var symlink: File = try ctx.dir.openFile(io, "symlink", .{
290 .follow_symlinks = false,
291 .path_only = true,
292 });
293 defer symlink.close(io);
294
295 const stat = try symlink.stat(io);
296 try expectEqual(File.Kind.sym_link, stat.kind);
297 }
298 }.impl);
299}
300
301test "Dir.statFile on a symlink" {
302 const io = testing.io;
303
304 try testWithAllSupportedPathTypes(struct {
305 fn impl(ctx: *TestContext) !void {
306 const dir_target_path = try ctx.transformPath("test_file");
307 try ctx.dir.writeFile(io, .{
308 .sub_path = dir_target_path,
309 .data = "Some test content",
310 });
311
312 try setupSymlink(io, ctx.dir, dir_target_path, "symlink", .{});
313
314 const file_stat = try ctx.dir.statFile(io, "test_file", .{ .follow_symlinks = false });
315 try testing.expectEqual(File.Kind.file, file_stat.kind);
316
317 const link_stat = try ctx.dir.statFile(io, "symlink", .{ .follow_symlinks = false });
318 try testing.expectEqual(File.Kind.sym_link, link_stat.kind);
319 }
320 }.impl);
321}
322
323test "openDir" {
324 const io = testing.io;
325
326 try testWithAllSupportedPathTypes(struct {
327 fn impl(ctx: *TestContext) !void {
328 const allocator = ctx.arena.allocator();
329 const subdir_path = try ctx.transformPath("subdir");
330 try ctx.dir.createDir(io, subdir_path, .default_dir);
331
332 for ([_][]const u8{ "", ".", ".." }) |sub_path| {
333 const dir_path = try Dir.path.join(allocator, &.{ subdir_path, sub_path });
334 var dir = try ctx.dir.openDir(io, dir_path, .{});
335 defer dir.close(io);
336 }
337 }
338 }.impl);
339}
340
341test "accessAbsolute" {
342 if (!isRealPathSupported()) return error.SkipZigTest;
343
344 const io = testing.io;
345 const gpa = testing.allocator;
346
347 var tmp = tmpDir(.{});
348 defer tmp.cleanup();
349
350 const base_path = try tmp.dir.realPathFileAlloc(io, ".", gpa);
351 defer gpa.free(base_path);
352
353 try Dir.accessAbsolute(io, base_path, .{});
354}
355
356test "openDirAbsolute" {
357 if (!isRealPathSupported()) return error.SkipZigTest;
358
359 const io = testing.io;
360 const gpa = testing.allocator;
361
362 var tmp = tmpDir(.{});
363 defer tmp.cleanup();
364
365 const tmp_ino = (try tmp.dir.stat(io)).inode;
366
367 try tmp.dir.createDir(io, "subdir", .default_dir);
368 const sub_path = try tmp.dir.realPathFileAlloc(io, "subdir", gpa);
369 defer gpa.free(sub_path);
370
371 // Can open sub_path
372 var tmp_sub = try Dir.openDirAbsolute(io, sub_path, .{});
373 defer tmp_sub.close(io);
374
375 const sub_ino = (try tmp_sub.stat(io)).inode;
376
377 {
378 // Can open sub_path + ".."
379 const dir_path = try Dir.path.join(testing.allocator, &.{ sub_path, ".." });
380 defer testing.allocator.free(dir_path);
381
382 var dir = try Dir.openDirAbsolute(io, dir_path, .{});
383 defer dir.close(io);
384
385 const ino = (try dir.stat(io)).inode;
386 try expectEqual(tmp_ino, ino);
387 }
388
389 {
390 // Can open sub_path + "."
391 const dir_path = try Dir.path.join(testing.allocator, &.{ sub_path, "." });
392 defer testing.allocator.free(dir_path);
393
394 var dir = try Dir.openDirAbsolute(io, dir_path, .{});
395 defer dir.close(io);
396
397 const ino = (try dir.stat(io)).inode;
398 try expectEqual(sub_ino, ino);
399 }
400
401 {
402 // Can open subdir + "..", with some extra "."
403 const dir_path = try Dir.path.join(testing.allocator, &.{ sub_path, ".", "..", "." });
404 defer testing.allocator.free(dir_path);
405
406 var dir = try Dir.openDirAbsolute(io, dir_path, .{});
407 defer dir.close(io);
408
409 const ino = (try dir.stat(io)).inode;
410 try expectEqual(tmp_ino, ino);
411 }
412}
413
414test "openDir cwd parent '..'" {
415 const io = testing.io;
416
417 var dir = Dir.cwd().openDir(io, "..", .{}) catch |err| {
418 if (native_os == .wasi and err == error.PermissionDenied) {
419 return; // This is okay. WASI disallows escaping from the fs sandbox
420 }
421 return err;
422 };
423 defer dir.close(io);
424}
425
426test "openDir non-cwd parent '..'" {
427 switch (native_os) {
428 .wasi, .netbsd, .openbsd => return error.SkipZigTest,
429 else => {},
430 }
431
432 const io = testing.io;
433 const gpa = testing.allocator;
434
435 var tmp = tmpDir(.{});
436 defer tmp.cleanup();
437
438 var subdir = try tmp.dir.createDirPathOpen(io, "subdir", .{});
439 defer subdir.close(io);
440
441 var dir = try subdir.openDir(io, "..", .{});
442 defer dir.close(io);
443
444 const expected_path = try tmp.dir.realPathFileAlloc(io, ".", gpa);
445 defer gpa.free(expected_path);
446
447 const actual_path = try dir.realPathFileAlloc(io, ".", gpa);
448 defer gpa.free(actual_path);
449
450 try expectEqualStrings(expected_path, actual_path);
451}
452
453test "readLinkAbsolute" {
454 if (!isRealPathSupported()) return error.SkipZigTest;
455
456 const io = testing.io;
457
458 var tmp = tmpDir(.{});
459 defer tmp.cleanup();
460
461 // Create some targets
462 try tmp.dir.writeFile(io, .{ .sub_path = "file.txt", .data = "nonsense" });
463 try tmp.dir.createDir(io, "subdir", .default_dir);
464
465 // Get base abs path
466 var arena_allocator = ArenaAllocator.init(testing.allocator);
467 defer arena_allocator.deinit();
468 const arena = arena_allocator.allocator();
469
470 const base_path = try tmp.dir.realPathFileAlloc(io, ".", arena);
471
472 {
473 const target_path = try Dir.path.join(arena, &.{ base_path, "file.txt" });
474 const symlink_path = try Dir.path.join(arena, &.{ base_path, "symlink1" });
475
476 // Create symbolic link by path
477 try setupSymlinkAbsolute(io, target_path, symlink_path, .{});
478 try testReadLinkAbsolute(io, target_path, symlink_path);
479 }
480 {
481 const target_path = try Dir.path.join(arena, &.{ base_path, "subdir" });
482 const symlink_path = try Dir.path.join(arena, &.{ base_path, "symlink2" });
483
484 // Create symbolic link to a directory by path
485 try setupSymlinkAbsolute(io, target_path, symlink_path, .{ .is_directory = true });
486 try testReadLinkAbsolute(io, target_path, symlink_path);
487 }
488}
489
490test "Dir.Iterator" {
491 const io = testing.io;
492
493 var tmp_dir = tmpDir(.{ .iterate = true });
494 defer tmp_dir.cleanup();
495
496 // First, create a couple of entries to iterate over.
497 const file = try tmp_dir.dir.createFile(io, "some_file", .{});
498 file.close(io);
499
500 try tmp_dir.dir.createDir(io, "some_dir", .default_dir);
501
502 var arena = ArenaAllocator.init(testing.allocator);
503 defer arena.deinit();
504 const allocator = arena.allocator();
505
506 var entries = std.array_list.Managed(Dir.Entry).init(allocator);
507
508 // Create iterator.
509 var iter = tmp_dir.dir.iterate();
510 while (try iter.next(io)) |entry| {
511 // We cannot just store `entry` as on Windows, we're re-using the name buffer
512 // which means we'll actually share the `name` pointer between entries!
513 const name = try allocator.dupe(u8, entry.name);
514 try entries.append(Dir.Entry{ .name = name, .kind = entry.kind, .inode = 0 });
515 }
516
517 try expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
518 try expect(contains(&entries, .{ .name = "some_file", .kind = .file, .inode = 0 }));
519 try expect(contains(&entries, .{ .name = "some_dir", .kind = .directory, .inode = 0 }));
520}
521
522test "Dir.Iterator many entries" {
523 const io = testing.io;
524
525 var tmp_dir = tmpDir(.{ .iterate = true });
526 defer tmp_dir.cleanup();
527
528 const num = 1024;
529 var i: usize = 0;
530 var buf: [4]u8 = undefined; // Enough to store "1024".
531 while (i < num) : (i += 1) {
532 const name = try std.mem.print(&buf, "{}", .{i});
533 const file = try tmp_dir.dir.createFile(io, name, .{});
534 file.close(io);
535 }
536
537 var arena = ArenaAllocator.init(testing.allocator);
538 defer arena.deinit();
539 const allocator = arena.allocator();
540
541 var entries = std.array_list.Managed(Dir.Entry).init(allocator);
542
543 // Create iterator.
544 var iter = tmp_dir.dir.iterate();
545 while (try iter.next(io)) |entry| {
546 // We cannot just store `entry` as on Windows, we're re-using the name buffer
547 // which means we'll actually share the `name` pointer between entries!
548 const name = try allocator.dupe(u8, entry.name);
549 try entries.append(.{ .name = name, .kind = entry.kind, .inode = 0 });
550 }
551
552 i = 0;
553 while (i < num) : (i += 1) {
554 const name = try std.mem.print(&buf, "{}", .{i});
555 try expect(contains(&entries, .{ .name = name, .kind = .file, .inode = 0 }));
556 }
557}
558
559test "Dir.Iterator twice" {
560 const io = testing.io;
561
562 var tmp_dir = tmpDir(.{ .iterate = true });
563 defer tmp_dir.cleanup();
564
565 // First, create a couple of entries to iterate over.
566 const file = try tmp_dir.dir.createFile(io, "some_file", .{});
567 file.close(io);
568
569 try tmp_dir.dir.createDir(io, "some_dir", .default_dir);
570
571 var arena = ArenaAllocator.init(testing.allocator);
572 defer arena.deinit();
573 const allocator = arena.allocator();
574
575 var i: u8 = 0;
576 while (i < 2) : (i += 1) {
577 var entries = std.array_list.Managed(Dir.Entry).init(allocator);
578
579 // Create iterator.
580 var iter = tmp_dir.dir.iterate();
581 while (try iter.next(io)) |entry| {
582 // We cannot just store `entry` as on Windows, we're re-using the name buffer
583 // which means we'll actually share the `name` pointer between entries!
584 const name = try allocator.dupe(u8, entry.name);
585 try entries.append(Dir.Entry{ .name = name, .kind = entry.kind, .inode = 0 });
586 }
587
588 try expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
589 try expect(contains(&entries, .{ .name = "some_file", .kind = .file, .inode = 0 }));
590 try expect(contains(&entries, .{ .name = "some_dir", .kind = .directory, .inode = 0 }));
591 }
592}
593
594test "Dir.Iterator reset" {
595 const io = testing.io;
596
597 var tmp_dir = tmpDir(.{ .iterate = true });
598 defer tmp_dir.cleanup();
599
600 // First, create a couple of entries to iterate over.
601 const file = try tmp_dir.dir.createFile(io, "some_file", .{});
602 file.close(io);
603
604 try tmp_dir.dir.createDir(io, "some_dir", .default_dir);
605
606 var arena = ArenaAllocator.init(testing.allocator);
607 defer arena.deinit();
608 const allocator = arena.allocator();
609
610 // Create iterator.
611 var iter = tmp_dir.dir.iterate();
612
613 var i: u8 = 0;
614 while (i < 2) : (i += 1) {
615 var entries = std.array_list.Managed(Dir.Entry).init(allocator);
616
617 while (try iter.next(io)) |entry| {
618 // We cannot just store `entry` as on Windows, we're re-using the name buffer
619 // which means we'll actually share the `name` pointer between entries!
620 const name = try allocator.dupe(u8, entry.name);
621 try entries.append(.{ .name = name, .kind = entry.kind, .inode = 0 });
622 }
623
624 try expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
625 try expect(contains(&entries, .{ .name = "some_file", .kind = .file, .inode = 0 }));
626 try expect(contains(&entries, .{ .name = "some_dir", .kind = .directory, .inode = 0 }));
627
628 iter.reader.reset();
629 }
630}
631
632test "Dir.Iterator but dir is deleted during iteration" {
633 const io = testing.io;
634
635 var tmp = std.testing.tmpDir(.{});
636 defer tmp.cleanup();
637
638 // Create directory and setup an iterator for it
639 var subdir = try tmp.dir.createDirPathOpen(io, "subdir", .{ .open_options = .{ .iterate = true } });
640 defer subdir.close(io);
641
642 var iterator = subdir.iterate();
643
644 // Create something to iterate over within the subdir
645 try tmp.dir.createDirPath(io, "subdir" ++ Dir.path.sep_str ++ "b");
646
647 // Then, before iterating, delete the directory that we're iterating.
648 // This is a contrived reproduction, but this could happen outside of the program, in another thread, etc.
649 // If we get an error while trying to delete, we can skip this test (this will happen on platforms
650 // like Windows which will give FileBusy if the directory is currently open for iteration).
651 tmp.dir.deleteTree(io, "subdir") catch return error.SkipZigTest;
652
653 // Now, when we try to iterate, the next call should return null immediately.
654 const entry = try iterator.next(io);
655 try testing.expect(entry == null);
656}
657
658fn entryEql(lhs: Dir.Entry, rhs: Dir.Entry) bool {
659 return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind;
660}
661
662fn contains(entries: *const std.array_list.Managed(Dir.Entry), el: Dir.Entry) bool {
663 for (entries.items) |entry| {
664 if (entryEql(entry, el)) return true;
665 }
666 return false;
667}
668
669test "Dir.realPath smoke test" {
670 if (!isRealPathSupported()) return error.SkipZigTest;
671
672 try testWithAllSupportedPathTypes(struct {
673 fn impl(ctx: *TestContext) !void {
674 const io = ctx.io;
675 const arena = ctx.arena.allocator();
676 const test_file_path = try ctx.transformPath("test_file");
677 const test_dir_path = try ctx.transformPath("test_dir");
678 var buf: [Dir.max_path_bytes]u8 = undefined;
679
680 // FileNotFound if the path doesn't exist
681 try expectError(error.FileNotFound, ctx.dir.realPathFileAlloc(io, test_file_path, arena));
682 try expectError(error.FileNotFound, ctx.dir.realPathFile(io, test_file_path, &buf));
683 try expectError(error.FileNotFound, ctx.dir.realPathFileAlloc(io, test_dir_path, arena));
684 try expectError(error.FileNotFound, ctx.dir.realPathFile(io, test_dir_path, &buf));
685
686 // Now create the file and dir
687 try ctx.dir.writeFile(io, .{ .sub_path = test_file_path, .data = "" });
688 try ctx.dir.createDir(io, test_dir_path, .default_dir);
689
690 const base_path = try ctx.transformPath(".");
691 const base_realpath = try ctx.dir.realPathFileAlloc(io, base_path, arena);
692 const expected_file_path = try Dir.path.join(arena, &.{ base_realpath, "test_file" });
693 const expected_dir_path = try Dir.path.join(arena, &.{ base_realpath, "test_dir" });
694
695 // First, test non-alloc version
696 {
697 const file_path = buf[0..try ctx.dir.realPathFile(io, test_file_path, &buf)];
698 try expectEqualStrings(expected_file_path, file_path);
699
700 const dir_path = buf[0..try ctx.dir.realPathFile(io, test_dir_path, &buf)];
701 try expectEqualStrings(expected_dir_path, dir_path);
702 }
703
704 // Next, test alloc version
705 {
706 const file_path = try ctx.dir.realPathFileAlloc(io, test_file_path, arena);
707 try expectEqualStrings(expected_file_path, file_path);
708
709 const dir_path = try ctx.dir.realPathFileAlloc(io, test_dir_path, arena);
710 try expectEqualStrings(expected_dir_path, dir_path);
711 }
712 }
713 }.impl);
714}
715
716test "readFileAlloc" {
717 const io = testing.io;
718
719 var tmp_dir = tmpDir(.{});
720 defer tmp_dir.cleanup();
721
722 var file = try tmp_dir.dir.createFile(io, "test_file", .{ .read = true });
723 defer file.close(io);
724
725 const buf1 = try tmp_dir.dir.readFileAlloc(io, "test_file", testing.allocator, .limited(1024));
726 defer testing.allocator.free(buf1);
727 try expectEqualStrings("", buf1);
728
729 const write_buf: []const u8 = "this is a test.\nthis is a test.\nthis is a test.\nthis is a test.\n";
730 try file.writeStreamingAll(io, write_buf);
731
732 {
733 // max_bytes > file_size
734 const buf2 = try tmp_dir.dir.readFileAlloc(io, "test_file", testing.allocator, .limited(1024));
735 defer testing.allocator.free(buf2);
736 try expectEqualStrings(write_buf, buf2);
737 }
738
739 {
740 // max_bytes == file_size
741 try expectError(
742 error.StreamTooLong,
743 tmp_dir.dir.readFileAlloc(io, "test_file", testing.allocator, .limited(write_buf.len)),
744 );
745 }
746
747 {
748 // max_bytes == file_size + 1
749 const buf2 = try tmp_dir.dir.readFileAlloc(io, "test_file", testing.allocator, .limited(write_buf.len + 1));
750 defer testing.allocator.free(buf2);
751 try expectEqualStrings(write_buf, buf2);
752 }
753
754 // max_bytes < file_size
755 try expectError(
756 error.StreamTooLong,
757 tmp_dir.dir.readFileAlloc(io, "test_file", testing.allocator, .limited(write_buf.len - 1)),
758 );
759}
760
761test "file operations with follow_symlinks=false" {
762 const io = testing.io;
763
764 var tmp_dir = tmpDir(.{});
765 defer tmp_dir.cleanup();
766
767 const contents = "this is a test.\nthis is a test.\nthis is a test.\nthis is a test.\n";
768 try tmp_dir.dir.writeFile(io, .{
769 .sub_path = "test_file",
770 .data = contents,
771 });
772
773 // Without lock
774 {
775 var file = try tmp_dir.dir.openFile(io, "test_file", .{ .follow_symlinks = false });
776 defer file.close(io);
777
778 var file_reader = file.reader(io, &.{});
779 const actual_contents = try file_reader.interface.allocRemaining(testing.allocator, .unlimited);
780 defer testing.allocator.free(actual_contents);
781
782 try std.testing.expectEqualSlices(u8, contents, actual_contents);
783 }
784
785 // With lock
786 {
787 var file = try tmp_dir.dir.openFile(io, "test_file", .{ .follow_symlinks = false, .lock = .exclusive });
788 defer file.close(io);
789
790 var file_reader = file.reader(io, &.{});
791 const actual_contents = try file_reader.interface.allocRemaining(testing.allocator, .unlimited);
792 defer testing.allocator.free(actual_contents);
793
794 try std.testing.expectEqualSlices(u8, contents, actual_contents);
795 }
796}
797
798test "Dir.statFile" {
799 try testWithAllSupportedPathTypes(struct {
800 fn impl(ctx: *TestContext) !void {
801 const io = ctx.io;
802 {
803 const test_file_name = try ctx.transformPath("test_file");
804
805 try expectError(error.FileNotFound, ctx.dir.statFile(io, test_file_name, .{}));
806
807 try ctx.dir.writeFile(io, .{ .sub_path = test_file_name, .data = "" });
808
809 const stat = try ctx.dir.statFile(io, test_file_name, .{});
810 try expectEqual(.file, stat.kind);
811 }
812 {
813 const test_dir_name = try ctx.transformPath("test_dir");
814
815 try expectError(error.FileNotFound, ctx.dir.statFile(io, test_dir_name, .{}));
816
817 try ctx.dir.createDir(io, test_dir_name, .default_dir);
818
819 const stat = try ctx.dir.statFile(io, test_dir_name, .{});
820 try expectEqual(.directory, stat.kind);
821 }
822 }
823 }.impl);
824}
825
826test "statFile on dangling symlink" {
827 try testWithAllSupportedPathTypes(struct {
828 fn impl(ctx: *TestContext) !void {
829 const io = ctx.io;
830 const symlink_name = try ctx.transformPath("dangling-symlink");
831 const symlink_target = "." ++ Dir.path.sep_str ++ "doesnotexist";
832
833 try setupSymlink(io, ctx.dir, symlink_target, symlink_name, .{});
834
835 try expectError(error.FileNotFound, ctx.dir.statFile(io, symlink_name, .{}));
836 }
837 }.impl);
838}
839
840test "directory operations on files" {
841 try testWithAllSupportedPathTypes(struct {
842 fn impl(ctx: *TestContext) !void {
843 const io = ctx.io;
844
845 const test_file_name = try ctx.transformPath("test_file");
846
847 var file = try ctx.dir.createFile(io, test_file_name, .{ .read = true });
848 file.close(io);
849
850 try expectError(error.PathAlreadyExists, ctx.dir.createDir(io, test_file_name, .default_dir));
851 try expectError(error.NotDir, ctx.dir.openDir(io, test_file_name, .{}));
852 try expectError(error.NotDir, ctx.dir.deleteDir(io, test_file_name));
853
854 if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
855 try expectError(error.PathAlreadyExists, Dir.createDirAbsolute(io, test_file_name, .default_dir));
856 try expectError(error.NotDir, Dir.deleteDirAbsolute(io, test_file_name));
857 }
858
859 // ensure the file still exists and is a file as a sanity check
860 file = try ctx.dir.openFile(io, test_file_name, .{});
861 const stat = try file.stat(io);
862 try expectEqual(File.Kind.file, stat.kind);
863 file.close(io);
864 }
865 }.impl);
866}
867
868test "file operations on directories" {
869 // TODO: fix this test on FreeBSD. https://github.com/ziglang/zig/issues/1759
870 if (native_os == .freebsd) return error.SkipZigTest;
871
872 const io = testing.io;
873
874 try testWithAllSupportedPathTypes(struct {
875 fn impl(ctx: *TestContext) !void {
876 const test_dir_name = try ctx.transformPath("test_dir");
877
878 try ctx.dir.createDir(io, test_dir_name, .default_dir);
879
880 try expectError(error.IsDir, ctx.dir.createFile(io, test_dir_name, .{}));
881 try expectError(error.IsDir, ctx.dir.deleteFile(io, test_dir_name));
882 switch (native_os) {
883 .netbsd => {
884 // no error when reading a directory. See https://github.com/ziglang/zig/issues/5732
885 const buf = try ctx.dir.readFileAlloc(io, test_dir_name, testing.allocator, .unlimited);
886 testing.allocator.free(buf);
887 },
888 else => {
889 try expectError(error.IsDir, ctx.dir.readFileAlloc(io, test_dir_name, testing.allocator, .unlimited));
890 },
891 }
892
893 if (native_os == .wasi and builtin.link_libc) {
894 // wasmtime unexpectedly succeeds here, see https://github.com/ziglang/zig/issues/20747
895 const handle = try ctx.dir.openFile(io, test_dir_name, .{ .mode = .read_write });
896 handle.close(io);
897 } else {
898 // Note: The `.mode = .read_write` is necessary to ensure the error occurs on all platforms.
899 try expectError(error.IsDir, ctx.dir.openFile(io, test_dir_name, .{ .mode = .read_write }));
900 }
901
902 {
903 const handle = try ctx.dir.openFile(io, test_dir_name, .{ .allow_directory = true, .mode = .read_only });
904 defer handle.close(io);
905
906 // Reading from the handle should fail
907 if (native_os != .netbsd) {
908 var buf: [1]u8 = undefined;
909 try expectError(error.IsDir, handle.readStreaming(io, &.{&buf}));
910 try expectError(error.IsDir, handle.readPositional(io, &.{&buf}, 0));
911 }
912 }
913 try expectError(error.IsDir, ctx.dir.openFile(io, test_dir_name, .{ .allow_directory = false, .mode = .read_only }));
914
915 if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
916 try expectError(error.IsDir, Dir.createFileAbsolute(io, test_dir_name, .{}));
917 try expectError(error.IsDir, Dir.deleteFileAbsolute(io, test_dir_name));
918 }
919
920 // ensure the directory still exists as a sanity check
921 var dir = try ctx.dir.openDir(io, test_dir_name, .{});
922 dir.close(io);
923 }
924 }.impl);
925}
926
927test "createDirPathOpen parent dirs do not exist" {
928 const io = testing.io;
929
930 var tmp_dir = tmpDir(.{});
931 defer tmp_dir.cleanup();
932
933 var dir = try tmp_dir.dir.createDirPathOpen(io, "root_dir/parent_dir/some_dir", .{});
934 dir.close(io);
935
936 // double check that the full directory structure was created
937 var dir_verification = try tmp_dir.dir.openDir(io, "root_dir/parent_dir/some_dir", .{});
938 dir_verification.close(io);
939}
940
941test "deleteDir" {
942 if (builtin.target.os.tag == .windows) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/35686
943
944 try testWithAllSupportedPathTypes(struct {
945 fn impl(ctx: *TestContext) !void {
946 const io = ctx.io;
947 const test_dir_path = try ctx.transformPath("test_dir");
948 const test_file_path = try ctx.transformPath("test_dir" ++ Dir.path.sep_str ++ "test_file");
949
950 // deleting a non-existent directory
951 try expectError(error.FileNotFound, ctx.dir.deleteDir(io, test_dir_path));
952
953 // deleting a non-empty directory
954 try ctx.dir.createDir(io, test_dir_path, .default_dir);
955 try ctx.dir.writeFile(io, .{ .sub_path = test_file_path, .data = "" });
956 try expectError(error.DirNotEmpty, ctx.dir.deleteDir(io, test_dir_path));
957
958 // deleting an empty directory
959 try ctx.dir.deleteFile(io, test_file_path);
960 try ctx.dir.deleteDir(io, test_dir_path);
961 }
962 }.impl);
963}
964
965test "Dir.rename files" {
966 try testWithAllSupportedPathTypes(struct {
967 fn impl(ctx: *TestContext) !void {
968 const io = ctx.io;
969 // Rename on Windows can hit intermittent AccessDenied errors
970 // when certain conditions are true about the host system.
971 // For now, skip this test when the path type is UNC to avoid them.
972 // See https://github.com/ziglang/zig/issues/17134
973 if (ctx.path_type == .unc) return;
974
975 const missing_file_path = try ctx.transformPath("missing_file_name");
976 const something_else_path = try ctx.transformPath("something_else");
977
978 try expectError(error.FileNotFound, ctx.dir.rename(missing_file_path, ctx.dir, something_else_path, io));
979
980 // Renaming files
981 const test_file_name = try ctx.transformPath("test_file");
982 const renamed_test_file_name = try ctx.transformPath("test_file_renamed");
983 var file = try ctx.dir.createFile(io, test_file_name, .{ .read = true });
984 file.close(io);
985 try ctx.dir.rename(test_file_name, ctx.dir, renamed_test_file_name, io);
986
987 // Ensure the file was renamed
988 try expectError(error.FileNotFound, ctx.dir.openFile(io, test_file_name, .{}));
989 file = try ctx.dir.openFile(io, renamed_test_file_name, .{});
990 file.close(io);
991
992 // Rename to self succeeds
993 try ctx.dir.rename(renamed_test_file_name, ctx.dir, renamed_test_file_name, io);
994
995 // Rename to existing file succeeds
996 const existing_file_path = try ctx.transformPath("existing_file");
997 var existing_file = try ctx.dir.createFile(io, existing_file_path, .{ .read = true });
998 existing_file.close(io);
999 try ctx.dir.rename(renamed_test_file_name, ctx.dir, existing_file_path, io);
1000
1001 try expectError(error.FileNotFound, ctx.dir.openFile(io, renamed_test_file_name, .{}));
1002 file = try ctx.dir.openFile(io, existing_file_path, .{});
1003 file.close(io);
1004 }
1005 }.impl);
1006}
1007
1008test "Dir.rename directories" {
1009 try testWithAllSupportedPathTypes(struct {
1010 fn impl(ctx: *TestContext) !void {
1011 const io = ctx.io;
1012
1013 // Rename on Windows can hit intermittent AccessDenied errors
1014 // when certain conditions are true about the host system.
1015 // For now, skip this test when the path type is UNC to avoid them.
1016 // See https://github.com/ziglang/zig/issues/17134
1017 if (ctx.path_type == .unc) return;
1018
1019 const test_dir_path = try ctx.transformPath("test_dir");
1020 const test_dir_renamed_path = try ctx.transformPath("test_dir_renamed");
1021
1022 // Renaming directories
1023 try ctx.dir.createDir(io, test_dir_path, .default_dir);
1024 try ctx.dir.rename(test_dir_path, ctx.dir, test_dir_renamed_path, io);
1025
1026 // Ensure the directory was renamed
1027 try expectError(error.FileNotFound, ctx.dir.openDir(io, test_dir_path, .{}));
1028 var dir = try ctx.dir.openDir(io, test_dir_renamed_path, .{});
1029
1030 // Put a file in the directory
1031 var file = try dir.createFile(io, "test_file", .{ .read = true });
1032 file.close(io);
1033 dir.close(io);
1034
1035 const test_dir_renamed_again_path = try ctx.transformPath("test_dir_renamed_again");
1036 try ctx.dir.rename(test_dir_renamed_path, ctx.dir, test_dir_renamed_again_path, io);
1037
1038 // Ensure the directory was renamed and the file still exists in it
1039 try expectError(error.FileNotFound, ctx.dir.openDir(io, test_dir_renamed_path, .{}));
1040 dir = try ctx.dir.openDir(io, test_dir_renamed_again_path, .{});
1041 file = try dir.openFile(io, "test_file", .{});
1042 file.close(io);
1043 dir.close(io);
1044 }
1045 }.impl);
1046}
1047
1048test "Dir.rename directory onto empty dir" {
1049 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
1050 if (native_os == .windows) return error.SkipZigTest;
1051
1052 try testWithAllSupportedPathTypes(struct {
1053 fn impl(ctx: *TestContext) !void {
1054 const io = ctx.io;
1055
1056 const test_dir_path = try ctx.transformPath("test_dir");
1057 const target_dir_path = try ctx.transformPath("target_dir_path");
1058
1059 try ctx.dir.createDir(io, test_dir_path, .default_dir);
1060 try ctx.dir.createDir(io, target_dir_path, .default_dir);
1061 try ctx.dir.rename(test_dir_path, ctx.dir, target_dir_path, io);
1062
1063 // Ensure the directory was renamed
1064 try expectError(error.FileNotFound, ctx.dir.openDir(io, test_dir_path, .{}));
1065 var dir = try ctx.dir.openDir(io, target_dir_path, .{});
1066 dir.close(io);
1067 }
1068 }.impl);
1069}
1070
1071test "Dir.rename directory onto non-empty dir" {
1072 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
1073 if (native_os == .windows) return error.SkipZigTest;
1074
1075 try testWithAllSupportedPathTypes(struct {
1076 fn impl(ctx: *TestContext) !void {
1077 const io = ctx.io;
1078 const test_dir_path = try ctx.transformPath("test_dir");
1079 const target_dir_path = try ctx.transformPath("target_dir_path");
1080
1081 try ctx.dir.createDir(io, test_dir_path, .default_dir);
1082
1083 var target_dir = try ctx.dir.createDirPathOpen(io, target_dir_path, .{});
1084 var file = try target_dir.createFile(io, "test_file", .{ .read = true });
1085 file.close(io);
1086 target_dir.close(io);
1087
1088 try expectError(error.DirNotEmpty, ctx.dir.rename(test_dir_path, ctx.dir, target_dir_path, io));
1089
1090 // Ensure the directory was not renamed
1091 var dir = try ctx.dir.openDir(io, test_dir_path, .{});
1092 dir.close(io);
1093 }
1094 }.impl);
1095}
1096
1097test "Dir.rename file <-> dir" {
1098 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
1099 if (native_os == .windows) return error.SkipZigTest;
1100
1101 try testWithAllSupportedPathTypes(struct {
1102 fn impl(ctx: *TestContext) !void {
1103 const io = ctx.io;
1104 const test_file_path = try ctx.transformPath("test_file");
1105 const test_dir_path = try ctx.transformPath("test_dir");
1106
1107 var file = try ctx.dir.createFile(io, test_file_path, .{ .read = true });
1108 file.close(io);
1109 try ctx.dir.createDir(io, test_dir_path, .default_dir);
1110 try expectError(error.IsDir, ctx.dir.rename(test_file_path, ctx.dir, test_dir_path, io));
1111 try expectError(error.NotDir, ctx.dir.rename(test_dir_path, ctx.dir, test_file_path, io));
1112 }
1113 }.impl);
1114}
1115
1116test "Dir.renamePreserve onto existing" {
1117 if (native_os == .windows) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/35359
1118
1119 try testWithAllSupportedPathTypes(struct {
1120 fn impl(ctx: *TestContext) !void {
1121 const io = ctx.io;
1122
1123 const test_file_path = try ctx.transformPath("test_file");
1124 const target_file_path = try ctx.transformPath("target_file");
1125 const test_dir_path = try ctx.transformPath("test_dir");
1126 const target_dir_path = try ctx.transformPath("target_dir");
1127
1128 try ctx.dir.writeFile(io, .{ .sub_path = test_file_path, .data = "" });
1129 try ctx.dir.writeFile(io, .{ .sub_path = target_file_path, .data = "" });
1130 try ctx.dir.createDir(io, test_dir_path, .default_dir);
1131 try ctx.dir.createDir(io, target_dir_path, .default_dir);
1132
1133 // file -> file
1134 try expectError(error.PathAlreadyExists, ctx.dir.renamePreserve(test_file_path, ctx.dir, target_file_path, io));
1135 // file -> dir
1136 try expectError(error.PathAlreadyExists, ctx.dir.renamePreserve(test_file_path, ctx.dir, target_dir_path, io));
1137
1138 // TODO: fix dir renaming on other systems, see https://codeberg.org/ziglang/zig/issues/35340
1139 if (native_os != .windows and native_os != .linux and !native_os.isDarwin()) {
1140 return;
1141 }
1142
1143 // dir -> file
1144 try expectError(error.PathAlreadyExists, ctx.dir.renamePreserve(test_dir_path, ctx.dir, target_file_path, io));
1145 // dir -> dir
1146 try expectError(error.PathAlreadyExists, ctx.dir.renamePreserve(test_dir_path, ctx.dir, target_dir_path, io));
1147
1148 // dir -> non-empty dir
1149 {
1150 const target_dir = try ctx.dir.openDir(io, target_dir_path, .{});
1151 defer target_dir.close(io);
1152 try target_dir.writeFile(io, .{ .sub_path = "test_file", .data = "" });
1153 }
1154 try expectError(error.PathAlreadyExists, ctx.dir.renamePreserve(test_dir_path, ctx.dir, target_dir_path, io));
1155 }
1156 }.impl);
1157}
1158
1159test "rename" {
1160 const io = testing.io;
1161
1162 var tmp_dir1 = tmpDir(.{});
1163 defer tmp_dir1.cleanup();
1164
1165 var tmp_dir2 = tmpDir(.{});
1166 defer tmp_dir2.cleanup();
1167
1168 // Renaming files
1169 const test_file_name = "test_file";
1170 const renamed_test_file_name = "test_file_renamed";
1171 var file = try tmp_dir1.dir.createFile(io, test_file_name, .{ .read = true });
1172 file.close(io);
1173 try Dir.rename(tmp_dir1.dir, test_file_name, tmp_dir2.dir, renamed_test_file_name, io);
1174
1175 // ensure the file was renamed
1176 try expectError(error.FileNotFound, tmp_dir1.dir.openFile(io, test_file_name, .{}));
1177 file = try tmp_dir2.dir.openFile(io, renamed_test_file_name, .{});
1178 file.close(io);
1179}
1180
1181test "renameAbsolute" {
1182 if (!isRealPathSupported()) return error.SkipZigTest;
1183
1184 const io = testing.io;
1185
1186 var tmp_dir = tmpDir(.{});
1187 defer tmp_dir.cleanup();
1188
1189 // Get base abs path
1190 var arena = ArenaAllocator.init(testing.allocator);
1191 defer arena.deinit();
1192 const allocator = arena.allocator();
1193
1194 const base_path = try tmp_dir.dir.realPathFileAlloc(io, ".", allocator);
1195
1196 try expectError(error.FileNotFound, Dir.renameAbsolute(
1197 try Dir.path.join(allocator, &.{ base_path, "missing_file_name" }),
1198 try Dir.path.join(allocator, &.{ base_path, "something_else" }),
1199 io,
1200 ));
1201
1202 // Renaming files
1203 const test_file_name = "test_file";
1204 const renamed_test_file_name = "test_file_renamed";
1205 var file = try tmp_dir.dir.createFile(io, test_file_name, .{ .read = true });
1206 file.close(io);
1207 try Dir.renameAbsolute(
1208 try Dir.path.join(allocator, &.{ base_path, test_file_name }),
1209 try Dir.path.join(allocator, &.{ base_path, renamed_test_file_name }),
1210 io,
1211 );
1212
1213 // ensure the file was renamed
1214 try expectError(error.FileNotFound, tmp_dir.dir.openFile(io, test_file_name, .{}));
1215 file = try tmp_dir.dir.openFile(io, renamed_test_file_name, .{});
1216 const stat = try file.stat(io);
1217 try expectEqual(File.Kind.file, stat.kind);
1218 file.close(io);
1219
1220 // Renaming directories
1221 const test_dir_name = "test_dir";
1222 const renamed_test_dir_name = "test_dir_renamed";
1223 try tmp_dir.dir.createDir(io, test_dir_name, .default_dir);
1224 try Dir.renameAbsolute(
1225 try Dir.path.join(allocator, &.{ base_path, test_dir_name }),
1226 try Dir.path.join(allocator, &.{ base_path, renamed_test_dir_name }),
1227 io,
1228 );
1229
1230 // ensure the directory was renamed
1231 try expectError(error.FileNotFound, tmp_dir.dir.openDir(io, test_dir_name, .{}));
1232 var dir = try tmp_dir.dir.openDir(io, renamed_test_dir_name, .{});
1233 dir.close(io);
1234}
1235
1236test "openExecutable" {
1237 if (native_os == .wasi) return error.SkipZigTest;
1238 if (native_os == .openbsd) return error.SkipZigTest;
1239
1240 const io = testing.io;
1241
1242 const self_exe_file = try std.process.openExecutable(io, .{});
1243 self_exe_file.close(io);
1244}
1245
1246test "executablePath" {
1247 if (native_os == .wasi) return error.SkipZigTest;
1248 if (native_os == .openbsd) return error.SkipZigTest;
1249
1250 const io = testing.io;
1251 var buf: [Dir.max_path_bytes]u8 = undefined;
1252 const len = try std.process.executablePath(io, &buf);
1253 const buf_self_exe_path = buf[0..len];
1254 const alloc_self_exe_path = try std.process.executablePathAlloc(io, testing.allocator);
1255 defer testing.allocator.free(alloc_self_exe_path);
1256 try expectEqualSlices(u8, buf_self_exe_path, alloc_self_exe_path);
1257}
1258
1259test "deleteTree does not follow symlinks" {
1260 const io = testing.io;
1261
1262 var tmp = tmpDir(.{});
1263 defer tmp.cleanup();
1264
1265 try tmp.dir.createDirPath(io, "b");
1266 {
1267 var a = try tmp.dir.createDirPathOpen(io, "a", .{});
1268 defer a.close(io);
1269
1270 try setupSymlink(io, a, "../b", "b", .{ .is_directory = true });
1271 }
1272
1273 try tmp.dir.deleteTree(io, "a");
1274
1275 try expectError(error.FileNotFound, tmp.dir.access(io, "a", .{}));
1276 try tmp.dir.access(io, "b", .{});
1277}
1278
1279test "deleteTree on a symlink" {
1280 const io = testing.io;
1281
1282 var tmp = tmpDir(.{});
1283 defer tmp.cleanup();
1284
1285 // Symlink to a file
1286 try tmp.dir.writeFile(io, .{ .sub_path = "file", .data = "" });
1287 try setupSymlink(io, tmp.dir, "file", "filelink", .{});
1288
1289 try tmp.dir.deleteTree(io, "filelink");
1290 try expectError(error.FileNotFound, tmp.dir.access(io, "filelink", .{}));
1291 try tmp.dir.access(io, "file", .{});
1292
1293 // Symlink to a directory
1294 try tmp.dir.createDirPath(io, "dir");
1295 try setupSymlink(io, tmp.dir, "dir", "dirlink", .{ .is_directory = true });
1296
1297 try tmp.dir.deleteTree(io, "dirlink");
1298 try expectError(error.FileNotFound, tmp.dir.access(io, "dirlink", .{}));
1299 try tmp.dir.access(io, "dir", .{});
1300}
1301
1302test "createDirPath, put some files in it, deleteTree" {
1303 try testWithAllSupportedPathTypes(struct {
1304 fn impl(ctx: *TestContext) !void {
1305 const io = ctx.io;
1306 const allocator = ctx.arena.allocator();
1307 const dir_path = try ctx.transformPath("os_test_tmp");
1308
1309 try ctx.dir.createDirPath(io, try Dir.path.join(allocator, &.{ "os_test_tmp", "b", "c" }));
1310 try ctx.dir.writeFile(io, .{
1311 .sub_path = try Dir.path.join(allocator, &.{ "os_test_tmp", "b", "c", "file.txt" }),
1312 .data = "nonsense",
1313 });
1314 try ctx.dir.writeFile(io, .{
1315 .sub_path = try Dir.path.join(allocator, &.{ "os_test_tmp", "b", "file2.txt" }),
1316 .data = "blah",
1317 });
1318
1319 try ctx.dir.deleteTree(io, dir_path);
1320 try expectError(error.FileNotFound, ctx.dir.openDir(io, dir_path, .{}));
1321 }
1322 }.impl);
1323}
1324
1325test "createDirPath, put some files in it, deleteTreeMinStackSize" {
1326 try testWithAllSupportedPathTypes(struct {
1327 fn impl(ctx: *TestContext) !void {
1328 const io = ctx.io;
1329 const allocator = ctx.arena.allocator();
1330 const dir_path = try ctx.transformPath("os_test_tmp");
1331
1332 try ctx.dir.createDirPath(io, try Dir.path.join(allocator, &.{ "os_test_tmp", "b", "c" }));
1333 try ctx.dir.writeFile(io, .{
1334 .sub_path = try Dir.path.join(allocator, &.{ "os_test_tmp", "b", "c", "file.txt" }),
1335 .data = "nonsense",
1336 });
1337 try ctx.dir.writeFile(io, .{
1338 .sub_path = try Dir.path.join(allocator, &.{ "os_test_tmp", "b", "file2.txt" }),
1339 .data = "blah",
1340 });
1341
1342 try ctx.dir.deleteTreeMinStackSize(io, dir_path);
1343 try expectError(error.FileNotFound, ctx.dir.openDir(io, dir_path, .{}));
1344 }
1345 }.impl);
1346}
1347
1348test "createDirPath in a directory that no longer exists" {
1349 if (native_os == .windows) return error.SkipZigTest; // Windows returns FileBusy if attempting to remove an open dir
1350 if (native_os == .dragonfly) return error.SkipZigTest; // DragonflyBSD does not produce error (hammer2 fs)
1351
1352 const io = testing.io;
1353
1354 var tmp = tmpDir(.{});
1355 defer tmp.cleanup();
1356 try tmp.parent_dir.deleteTree(io, &tmp.sub_path);
1357
1358 try expectError(error.FileNotFound, tmp.dir.createDirPath(io, "sub-path"));
1359}
1360
1361test "createDirPath but sub_path contains pre-existing file" {
1362 const io = testing.io;
1363
1364 var tmp = tmpDir(.{});
1365 defer tmp.cleanup();
1366
1367 try tmp.dir.createDir(io, "foo", .default_dir);
1368 try tmp.dir.writeFile(io, .{ .sub_path = "foo/bar", .data = "" });
1369
1370 try expectError(error.NotDir, tmp.dir.createDirPath(io, "foo/bar/baz"));
1371}
1372
1373fn expectDir(io: Io, dir: Dir, path: []const u8) !void {
1374 var d = try dir.openDir(io, path, .{});
1375 d.close(io);
1376}
1377
1378test "makepath existing directories" {
1379 const io = testing.io;
1380
1381 var tmp = tmpDir(.{});
1382 defer tmp.cleanup();
1383
1384 try tmp.dir.createDir(io, "A", .default_dir);
1385 var tmpA = try tmp.dir.openDir(io, "A", .{});
1386 defer tmpA.close(io);
1387 try tmpA.createDir(io, "B", .default_dir);
1388
1389 const testPath = "A" ++ Dir.path.sep_str ++ "B" ++ Dir.path.sep_str ++ "C";
1390 try tmp.dir.createDirPath(io, testPath);
1391
1392 try expectDir(io, tmp.dir, testPath);
1393}
1394
1395test "makepath through existing valid symlink" {
1396 const io = testing.io;
1397
1398 var tmp = tmpDir(.{});
1399 defer tmp.cleanup();
1400
1401 try tmp.dir.createDir(io, "realfolder", .default_dir);
1402 try setupSymlink(io, tmp.dir, "." ++ Dir.path.sep_str ++ "realfolder", "working-symlink", .{});
1403
1404 try tmp.dir.createDirPath(io, "working-symlink" ++ Dir.path.sep_str ++ "in-realfolder");
1405
1406 try expectDir(io, tmp.dir, "realfolder" ++ Dir.path.sep_str ++ "in-realfolder");
1407}
1408
1409test "makepath relative walks" {
1410 const io = testing.io;
1411
1412 var tmp = tmpDir(.{});
1413 defer tmp.cleanup();
1414
1415 const relPath = try Dir.path.join(testing.allocator, &.{
1416 "first", "..", "second", "..", "third", "..", "first", "A", "..", "B", "..", "C",
1417 });
1418 defer testing.allocator.free(relPath);
1419
1420 try tmp.dir.createDirPath(io, relPath);
1421
1422 // How .. is handled is different on Windows than non-Windows
1423 switch (native_os) {
1424 .windows => {
1425 // On Windows, .. is resolved before passing the path to NtCreateFile,
1426 // meaning everything except `first/C` drops out.
1427 try expectDir(io, tmp.dir, "first" ++ Dir.path.sep_str ++ "C");
1428 try expectError(error.FileNotFound, tmp.dir.access(io, "second", .{}));
1429 try expectError(error.FileNotFound, tmp.dir.access(io, "third", .{}));
1430 },
1431 else => {
1432 try expectDir(io, tmp.dir, "first" ++ Dir.path.sep_str ++ "A");
1433 try expectDir(io, tmp.dir, "first" ++ Dir.path.sep_str ++ "B");
1434 try expectDir(io, tmp.dir, "first" ++ Dir.path.sep_str ++ "C");
1435 try expectDir(io, tmp.dir, "second");
1436 try expectDir(io, tmp.dir, "third");
1437 },
1438 }
1439}
1440
1441test "makepath ignores '.'" {
1442 const io = testing.io;
1443
1444 var tmp = tmpDir(.{});
1445 defer tmp.cleanup();
1446
1447 // Path to create, with "." elements:
1448 const dotPath = try Dir.path.join(testing.allocator, &.{
1449 "first", ".", "second", ".", "third",
1450 });
1451 defer testing.allocator.free(dotPath);
1452
1453 // Path to expect to find:
1454 const expectedPath = try Dir.path.join(testing.allocator, &.{
1455 "first", "second", "third",
1456 });
1457 defer testing.allocator.free(expectedPath);
1458
1459 try tmp.dir.createDirPath(io, dotPath);
1460
1461 try expectDir(io, tmp.dir, expectedPath);
1462}
1463
1464fn testFilenameLimits(io: Io, iterable_dir: Dir, maxed_filename: []const u8, maxed_dirname: []const u8) !void {
1465 // create a file, a dir, and a nested file all with maxed filenames
1466 {
1467 try iterable_dir.writeFile(io, .{ .sub_path = maxed_filename, .data = "" });
1468
1469 var maxed_dir = try iterable_dir.createDirPathOpen(io, maxed_dirname, .{});
1470 defer maxed_dir.close(io);
1471
1472 try maxed_dir.writeFile(io, .{ .sub_path = maxed_filename, .data = "" });
1473 }
1474 // Low level API with minimum buffer length
1475 {
1476 var reader_buf: [Dir.Reader.min_buffer_len]u8 align(@alignOf(usize)) = undefined;
1477 var reader: Dir.Reader = .init(iterable_dir, &reader_buf);
1478
1479 var file_count: usize = 0;
1480 var dir_count: usize = 0;
1481 while (try reader.next(io)) |entry| {
1482 switch (entry.kind) {
1483 .file => {
1484 try expectEqualStrings(maxed_filename, entry.name);
1485 file_count += 1;
1486 },
1487 .directory => {
1488 try expectEqualStrings(maxed_dirname, entry.name);
1489 dir_count += 1;
1490 },
1491 else => return error.TestFailed,
1492 }
1493 }
1494 try expectEqual(@as(usize, 1), file_count);
1495 try expectEqual(@as(usize, 1), dir_count);
1496 }
1497 // High level walk API
1498 {
1499 var walker = try iterable_dir.walk(testing.allocator);
1500 defer walker.deinit();
1501
1502 var file_count: usize = 0;
1503 var dir_count: usize = 0;
1504 while (try walker.next(io)) |entry| {
1505 switch (entry.kind) {
1506 .file => {
1507 try expectEqualStrings(maxed_filename, entry.basename);
1508 file_count += 1;
1509 },
1510 .directory => {
1511 try expectEqualStrings(maxed_dirname, entry.basename);
1512 dir_count += 1;
1513 },
1514 else => return error.TestFailed,
1515 }
1516 }
1517 try expectEqual(@as(usize, 2), file_count);
1518 try expectEqual(@as(usize, 1), dir_count);
1519 }
1520
1521 // ensure that we can delete the tree
1522 try iterable_dir.deleteTree(io, maxed_filename);
1523}
1524
1525test "max file name component lengths" {
1526 const io = testing.io;
1527
1528 var tmp = tmpDir(.{ .iterate = true });
1529 defer tmp.cleanup();
1530
1531 if (native_os == .windows) {
1532 // U+FFFF is the character with the largest code point that is encoded as a single
1533 // WTF-16 code unit, so Windows allows for NAME_MAX of them.
1534 const codepoint1 = "\u{FFFF}".*;
1535 const buf1: [windows.NAME_MAX][codepoint1.len]u8 = @splat(codepoint1);
1536 const maxed_windows_filename1: []const u8 = @ptrCast(&buf1);
1537 // This is also a code point that is encoded as one WTF-16 code unit, but
1538 // three WTF-8 bytes, so it exercises the limits of both WTF-16 and WTF-8 encodings.
1539 const codepoint2 = "€".*;
1540 const buf2: [windows.NAME_MAX][codepoint2.len]u8 = @splat(codepoint2);
1541 const maxed_windows_filename2: []const u8 = @ptrCast(&buf2);
1542 try testFilenameLimits(io, tmp.dir, maxed_windows_filename1, maxed_windows_filename2);
1543 } else if (native_os == .wasi) {
1544 // On WASI, the maxed filename depends on the host OS, so in order for this test to
1545 // work on any host, we need to use a length that will work for all platforms
1546 // (i.e. the minimum max_name_bytes of all supported platforms).
1547 const maxed_wasi_filename1: [255]u8 = @splat('1');
1548 const maxed_wasi_filename2: [255]u8 = @splat('2');
1549 try testFilenameLimits(io, tmp.dir, &maxed_wasi_filename1, &maxed_wasi_filename2);
1550 } else {
1551 const maxed_ascii_filename1: [Dir.max_name_bytes]u8 = @splat('1');
1552 const maxed_ascii_filename2: [Dir.max_name_bytes]u8 = @splat('2');
1553 try testFilenameLimits(io, tmp.dir, &maxed_ascii_filename1, &maxed_ascii_filename2);
1554 }
1555}
1556
1557test "writev, readv" {
1558 const io = testing.io;
1559
1560 var tmp = tmpDir(.{});
1561 defer tmp.cleanup();
1562
1563 const line1 = "line1\n";
1564 const line2 = "line2\n";
1565
1566 var buf1: [line1.len]u8 = undefined;
1567 var buf2: [line2.len]u8 = undefined;
1568 var write_vecs: [2][]const u8 = .{ line1, line2 };
1569 var read_vecs: [2][]u8 = .{ &buf2, &buf1 };
1570
1571 var src_file = try tmp.dir.createFile(io, "test.txt", .{ .read = true });
1572 defer src_file.close(io);
1573
1574 var writer = src_file.writerStreaming(io, &.{});
1575
1576 try writer.interface.writeVecAll(&write_vecs);
1577 try writer.interface.flush();
1578 try expectEqual(@as(u64, line1.len + line2.len), try src_file.length(io));
1579
1580 var reader = writer.moveToReader();
1581 try reader.seekTo(0);
1582 try reader.interface.readVecAll(&read_vecs);
1583 try expectEqualStrings(&buf1, "line2\n");
1584 try expectEqualStrings(&buf2, "line1\n");
1585 try expectError(error.EndOfStream, reader.interface.readSliceAll(&buf1));
1586}
1587
1588test "pwritev, preadv" {
1589 const io = testing.io;
1590
1591 var tmp = tmpDir(.{});
1592 defer tmp.cleanup();
1593
1594 const line1 = "line1\n";
1595 const line2 = "line2\n";
1596 var lines: [2][]const u8 = .{ line1, line2 };
1597 var buf1: [line1.len]u8 = undefined;
1598 var buf2: [line2.len]u8 = undefined;
1599 var read_vecs: [2][]u8 = .{ &buf2, &buf1 };
1600
1601 var src_file = try tmp.dir.createFile(io, "test.txt", .{ .read = true });
1602 defer src_file.close(io);
1603
1604 var writer = src_file.writer(io, &.{});
1605
1606 try writer.seekTo(16);
1607 try writer.interface.writeVecAll(&lines);
1608 try writer.interface.flush();
1609 try expectEqual(@as(u64, 16 + line1.len + line2.len), try src_file.length(io));
1610
1611 var reader = writer.moveToReader();
1612 try reader.seekTo(16);
1613 try reader.interface.readVecAll(&read_vecs);
1614 try expectEqualStrings(&buf1, "line2\n");
1615 try expectEqualStrings(&buf2, "line1\n");
1616 try expectError(error.EndOfStream, reader.interface.readSliceAll(&buf1));
1617}
1618
1619test "access file" {
1620 try testWithAllSupportedPathTypes(struct {
1621 fn impl(ctx: *TestContext) !void {
1622 const io = ctx.io;
1623 const dir_path = try ctx.transformPath("os_test_tmp");
1624 const file_path = try ctx.transformPath("os_test_tmp" ++ Dir.path.sep_str ++ "file.txt");
1625
1626 try ctx.dir.createDirPath(io, dir_path);
1627 try expectError(error.FileNotFound, ctx.dir.access(io, file_path, .{}));
1628
1629 try ctx.dir.writeFile(io, .{ .sub_path = file_path, .data = "" });
1630 try ctx.dir.access(io, file_path, .{});
1631 try ctx.dir.deleteTree(io, dir_path);
1632 }
1633 }.impl);
1634}
1635
1636test "sendfile" {
1637 const io = testing.io;
1638
1639 var tmp = tmpDir(.{});
1640 defer tmp.cleanup();
1641
1642 try tmp.dir.createDirPath(io, "os_test_tmp");
1643
1644 var dir = try tmp.dir.openDir(io, "os_test_tmp", .{});
1645 defer dir.close(io);
1646
1647 const line1 = "line1\n";
1648 const line2 = "second line\n";
1649 var vecs = [_][]const u8{ line1, line2 };
1650
1651 var src_file = try dir.createFile(io, "sendfile1.txt", .{ .read = true });
1652 defer src_file.close(io);
1653 {
1654 var fw = src_file.writer(io, &.{});
1655 try fw.interface.writeVecAll(&vecs);
1656 }
1657
1658 var dest_file = try dir.createFile(io, "sendfile2.txt", .{ .read = true });
1659 defer dest_file.close(io);
1660
1661 const header1 = "header1\n";
1662 const header2 = "second header\n";
1663 const trailer1 = "trailer1\n";
1664 const trailer2 = "second trailer\n";
1665 var headers: [2][]const u8 = .{ header1, header2 };
1666 var trailers: [2][]const u8 = .{ trailer1, trailer2 };
1667
1668 var written_buf: [100]u8 = undefined;
1669 var file_reader = src_file.reader(io, &.{});
1670 var fallback_buffer: [50]u8 = undefined;
1671 var file_writer = dest_file.writer(io, &fallback_buffer);
1672 try file_writer.interface.writeVecAll(&headers);
1673 try file_reader.seekTo(1);
1674 try expectEqual(10, try file_writer.interface.sendFileAll(&file_reader, .limited(10)));
1675 try file_writer.interface.writeVecAll(&trailers);
1676 try file_writer.interface.flush();
1677 var fr = file_writer.moveToReader();
1678 try fr.seekTo(0);
1679 const amt = try fr.interface.readSliceShort(&written_buf);
1680 try expectEqualStrings("header1\nsecond header\nine1\nsecontrailer1\nsecond trailer\n", written_buf[0..amt]);
1681}
1682
1683test "sendfile with buffered data" {
1684 const io = testing.io;
1685
1686 var tmp = tmpDir(.{});
1687 defer tmp.cleanup();
1688
1689 try tmp.dir.createDirPath(io, "os_test_tmp");
1690
1691 var dir = try tmp.dir.openDir(io, "os_test_tmp", .{});
1692 defer dir.close(io);
1693
1694 var src_file = try dir.createFile(io, "sendfile1.txt", .{ .read = true });
1695 defer src_file.close(io);
1696
1697 try src_file.writeStreamingAll(io, "AAAABBBB");
1698
1699 var dest_file = try dir.createFile(io, "sendfile2.txt", .{ .read = true });
1700 defer dest_file.close(io);
1701
1702 var src_buffer: [32]u8 = undefined;
1703 var file_reader = src_file.reader(io, &src_buffer);
1704
1705 try file_reader.seekTo(0);
1706 try file_reader.interface.fill(8);
1707
1708 var fallback_buffer: [32]u8 = undefined;
1709 var file_writer = dest_file.writer(io, &fallback_buffer);
1710
1711 try expectEqual(4, try file_writer.interface.sendFileAll(&file_reader, .limited(4)));
1712
1713 var written_buf: [8]u8 = undefined;
1714 var fr = file_writer.moveToReader();
1715 try fr.seekTo(0);
1716 const amt = try fr.interface.readSliceShort(&written_buf);
1717
1718 try expectEqual(4, amt);
1719 try expectEqualSlices(u8, "AAAA", written_buf[0..amt]);
1720}
1721
1722test "copyFile" {
1723 try testWithAllSupportedPathTypes(struct {
1724 fn impl(ctx: *TestContext) !void {
1725 const io = ctx.io;
1726 const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP";
1727 const src_file = try ctx.transformPath("tmp_test_copy_file.txt");
1728 const dest_file = try ctx.transformPath("tmp_test_copy_file2.txt");
1729 const dest_file2 = try ctx.transformPath("tmp_test_copy_file3.txt");
1730
1731 try ctx.dir.writeFile(io, .{ .sub_path = src_file, .data = data });
1732 defer ctx.dir.deleteFile(io, src_file) catch {};
1733
1734 try ctx.dir.copyFile(src_file, ctx.dir, dest_file, io, .{});
1735 defer ctx.dir.deleteFile(io, dest_file) catch {};
1736
1737 try ctx.dir.copyFile(src_file, ctx.dir, dest_file2, io, .{});
1738 defer ctx.dir.deleteFile(io, dest_file2) catch {};
1739
1740 try expectFileContents(io, ctx.dir, dest_file, data);
1741 try expectFileContents(io, ctx.dir, dest_file2, data);
1742 }
1743 }.impl);
1744}
1745
1746fn expectFileContents(io: Io, dir: Dir, file_path: []const u8, data: []const u8) !void {
1747 const contents = try dir.readFileAlloc(io, file_path, testing.allocator, .limited(1000));
1748 defer testing.allocator.free(contents);
1749
1750 try expectEqualSlices(u8, data, contents);
1751}
1752
1753test "AtomicFile" {
1754 if (native_os == .windows) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/31389
1755
1756 try testWithAllSupportedPathTypes(struct {
1757 fn impl(ctx: *TestContext) !void {
1758 const io = ctx.io;
1759 const allocator = ctx.arena.allocator();
1760 const test_out_file = try ctx.transformPath("tmp_atomic_file_test_dest.txt");
1761 const test_content =
1762 \\ hello!
1763 \\ this is a test file
1764 ;
1765
1766 // link() succeeds with no file already present
1767 {
1768 var af = try ctx.dir.createFileAtomic(io, test_out_file, .{ .replace = false });
1769 defer af.deinit(io);
1770 try af.file.writeStreamingAll(io, test_content);
1771 try af.link(io);
1772 }
1773 // link() returns error.PathAlreadyExists if file already present
1774 {
1775 var af = try ctx.dir.createFileAtomic(io, test_out_file, .{ .replace = false });
1776 defer af.deinit(io);
1777 try af.file.writeStreamingAll(io, test_content);
1778 try expectError(error.PathAlreadyExists, af.link(io));
1779 }
1780 // replace() succeeds if file already present
1781 {
1782 var af = try ctx.dir.createFileAtomic(io, test_out_file, .{ .replace = true });
1783 defer af.deinit(io);
1784 try af.file.writeStreamingAll(io, test_content);
1785 try af.replace(io);
1786 }
1787 const content = try ctx.dir.readFileAlloc(io, test_out_file, allocator, .limited(9999));
1788 try expectEqualStrings(test_content, content);
1789
1790 try ctx.dir.deleteFile(io, test_out_file);
1791 }
1792 }.impl);
1793}
1794
1795test "open file with exclusive nonblocking lock twice" {
1796 if (native_os == .wasi) return error.SkipZigTest;
1797
1798 try testWithAllSupportedPathTypes(struct {
1799 fn impl(ctx: *TestContext) !void {
1800 const io = ctx.io;
1801 const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
1802
1803 const file1 = try ctx.dir.createFile(io, filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1804 defer file1.close(io);
1805
1806 const file2 = ctx.dir.createFile(io, filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1807 try expectError(error.WouldBlock, file2);
1808 }
1809 }.impl);
1810}
1811
1812test "open file with shared and exclusive nonblocking lock" {
1813 if (native_os == .wasi) return error.SkipZigTest;
1814
1815 try testWithAllSupportedPathTypes(struct {
1816 fn impl(ctx: *TestContext) !void {
1817 const io = ctx.io;
1818 const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
1819
1820 const file1 = try ctx.dir.createFile(io, filename, .{ .lock = .shared, .lock_nonblocking = true });
1821 defer file1.close(io);
1822
1823 const file2 = ctx.dir.createFile(io, filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1824 try expectError(error.WouldBlock, file2);
1825 }
1826 }.impl);
1827}
1828
1829test "open file with exclusive and shared nonblocking lock" {
1830 if (native_os == .wasi) return error.SkipZigTest;
1831
1832 try testWithAllSupportedPathTypes(struct {
1833 fn impl(ctx: *TestContext) !void {
1834 const io = ctx.io;
1835 const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
1836
1837 const file1 = try ctx.dir.createFile(io, filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1838 defer file1.close(io);
1839
1840 const file2 = ctx.dir.createFile(io, filename, .{ .lock = .shared, .lock_nonblocking = true });
1841 try expectError(error.WouldBlock, file2);
1842 }
1843 }.impl);
1844}
1845
1846test "open file with exclusive lock twice, make sure second lock waits" {
1847 testWithAllSupportedPathTypes(struct {
1848 fn impl(ctx: *TestContext) !void {
1849 const io = ctx.io;
1850 const filename = try ctx.transformPath("file_lock_test.txt");
1851
1852 const file = try ctx.dir.createFile(io, filename, .{ .lock = .exclusive });
1853 errdefer file.close(io);
1854
1855 const S = struct {
1856 fn checkFn(inner_ctx: *TestContext, path: []const u8, started: *Io.Event, locked: *Io.Event) !void {
1857 started.set(inner_ctx.io);
1858 const file1 = try inner_ctx.dir.createFile(inner_ctx.io, path, .{ .lock = .exclusive });
1859
1860 locked.set(inner_ctx.io);
1861 file1.close(inner_ctx.io);
1862 }
1863 };
1864
1865 var started: Io.Event = .unset;
1866 var locked: Io.Event = .unset;
1867
1868 var t = try io.concurrent(S.checkFn, .{ ctx, filename, &started, &locked });
1869 defer t.cancel(io) catch {};
1870
1871 // Wait for the spawned thread to start trying to acquire the exclusive file lock.
1872 // Then wait a bit to make sure that can't acquire it since we currently hold the file lock.
1873 try started.wait(io);
1874 try expectError(error.Timeout, locked.waitTimeout(io, .{ .duration = .{
1875 .raw = .fromMilliseconds(10),
1876 .clock = .awake,
1877 } }));
1878
1879 // Release the file lock which should unlock the thread to lock it and set the locked event.
1880 file.close(io);
1881 try locked.wait(io);
1882 try t.await(io);
1883 }
1884 }.impl) catch |err| switch (err) {
1885 error.ConcurrencyUnavailable => return error.SkipZigTest,
1886 else => |e| return e,
1887 };
1888}
1889
1890test "open file with exclusive nonblocking lock twice (absolute paths)" {
1891 if (native_os == .wasi) return error.SkipZigTest;
1892
1893 const io = testing.io;
1894
1895 var random_bytes: [12]u8 = undefined;
1896 io.random(&random_bytes);
1897
1898 var random_b64: [std.base64.url_safe.Encoder.calcSize(random_bytes.len)]u8 = undefined;
1899 _ = std.base64.url_safe.Encoder.encode(&random_b64, &random_bytes);
1900
1901 const sub_path = random_b64 ++ "-zig-test-absolute-paths.txt";
1902
1903 const gpa = testing.allocator;
1904
1905 const cwd = try std.process.currentPathAlloc(io, gpa);
1906 defer gpa.free(cwd);
1907
1908 const filename = try Dir.path.resolve(gpa, &.{ cwd, sub_path });
1909 defer gpa.free(filename);
1910
1911 defer Dir.deleteFileAbsolute(io, filename) catch {}; // createFileAbsolute can leave files on failures
1912 const file1 = try Dir.createFileAbsolute(io, filename, .{
1913 .lock = .exclusive,
1914 .lock_nonblocking = true,
1915 });
1916
1917 const file2 = Dir.createFileAbsolute(io, filename, .{
1918 .lock = .exclusive,
1919 .lock_nonblocking = true,
1920 });
1921 file1.close(io);
1922 try expectError(error.WouldBlock, file2);
1923}
1924
1925test "read from locked file" {
1926 try testWithAllSupportedPathTypes(struct {
1927 fn impl(ctx: *TestContext) !void {
1928 const io = ctx.io;
1929 const filename = try ctx.transformPath("read_lock_file_test.txt");
1930
1931 {
1932 const f = try ctx.dir.createFile(io, filename, .{ .read = true });
1933 defer f.close(io);
1934 var buffer: [1]u8 = undefined;
1935 _ = try f.readPositional(io, &.{&buffer}, 0);
1936 }
1937 {
1938 const f = try ctx.dir.createFile(io, filename, .{
1939 .read = true,
1940 .lock = .exclusive,
1941 });
1942 defer f.close(io);
1943 const f2 = try ctx.dir.openFile(io, filename, .{});
1944 defer f2.close(io);
1945 // On POSIX locks may be ignored, however on Windows they cause
1946 // LockViolation.
1947 var buffer: [1]u8 = undefined;
1948 if (builtin.os.tag == .windows) {
1949 try expectError(error.LockViolation, f2.readPositional(io, &.{&buffer}, 0));
1950 } else {
1951 try expectEqual(0, f2.readPositional(io, &.{&buffer}, 0));
1952 }
1953 }
1954 }
1955 }.impl);
1956}
1957
1958test "use Lock.none to unlock files" {
1959 if (native_os == .wasi) return error.SkipZigTest;
1960
1961 const io = testing.io;
1962
1963 var tmp = tmpDir(.{});
1964 defer tmp.cleanup();
1965
1966 // Create a locked file.
1967 const test_file = try tmp.dir.createFile(io, "test_file", .{ .lock = .exclusive, .lock_nonblocking = true });
1968 defer test_file.close(io);
1969
1970 // Attempt to unlock the file via fs.lock with Lock.none.
1971 try test_file.lock(io, .none);
1972
1973 // Attempt to open the file now that it should be unlocked.
1974 const test_file2 = try tmp.dir.openFile(io, "test_file", .{ .lock = .exclusive, .lock_nonblocking = true });
1975 defer test_file2.close(io);
1976
1977 // Make sure Lock.none works with tryLock as well.
1978 try testing.expect(try test_file2.tryLock(io, .none));
1979
1980 // Attempt to open the file since it should be unlocked again.
1981 const test_file3 = try tmp.dir.openFile(io, "test_file", .{ .lock = .exclusive, .lock_nonblocking = true });
1982 test_file3.close(io);
1983}
1984
1985test "walker" {
1986 const io = testing.io;
1987
1988 var tmp = tmpDir(.{ .iterate = true });
1989 defer tmp.cleanup();
1990
1991 // iteration order of walker is undefined, so need lookup maps to check against
1992
1993 const expected_paths = std.StaticStringMap(usize).initComptime(.{
1994 .{ "dir1", 1 },
1995 .{ "dir2", 1 },
1996 .{ "dir3", 1 },
1997 .{ "dir4", 1 },
1998 .{ "dir3" ++ Dir.path.sep_str ++ "sub1", 2 },
1999 .{ "dir3" ++ Dir.path.sep_str ++ "sub2", 2 },
2000 .{ "dir3" ++ Dir.path.sep_str ++ "sub2" ++ Dir.path.sep_str ++ "subsub1", 3 },
2001 });
2002
2003 const expected_basenames = std.StaticStringMap(void).initComptime(.{
2004 .{"dir1"},
2005 .{"dir2"},
2006 .{"dir3"},
2007 .{"dir4"},
2008 .{"sub1"},
2009 .{"sub2"},
2010 .{"subsub1"},
2011 });
2012
2013 for (expected_paths.keys()) |key| {
2014 try tmp.dir.createDirPath(io, key);
2015 }
2016
2017 var walker = try tmp.dir.walk(testing.allocator);
2018 defer walker.deinit();
2019
2020 var num_walked: usize = 0;
2021 while (try walker.next(io)) |entry| {
2022 expect(expected_basenames.has(entry.basename)) catch |err| {
2023 std.debug.print("found unexpected basename: {f}\n", .{std.ascii.hexEscape(entry.basename, .lower)});
2024 return err;
2025 };
2026 expect(expected_paths.has(entry.path)) catch |err| {
2027 std.debug.print("found unexpected path: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
2028 return err;
2029 };
2030 expectEqual(expected_paths.get(entry.path).?, entry.depth()) catch |err| {
2031 std.debug.print("path reported unexpected depth: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
2032 return err;
2033 };
2034 // make sure that the entry.dir is the containing dir
2035 var entry_dir = try entry.dir.openDir(io, entry.basename, .{});
2036 defer entry_dir.close(io);
2037 num_walked += 1;
2038 }
2039 try expectEqual(expected_paths.kvs.len, num_walked);
2040}
2041
2042test "selective walker, skip entries that start with ." {
2043 const io = testing.io;
2044
2045 var tmp = tmpDir(.{ .iterate = true });
2046 defer tmp.cleanup();
2047
2048 const paths_to_create: []const []const u8 = &.{
2049 "dir1/foo/.git/ignored",
2050 ".hidden/bar",
2051 "a/b/c",
2052 "a/baz",
2053 };
2054
2055 // iteration order of walker is undefined, so need lookup maps to check against
2056
2057 const expected_paths = std.StaticStringMap(usize).initComptime(.{
2058 .{ "dir1", 1 },
2059 .{ "dir1" ++ Dir.path.sep_str ++ "foo", 2 },
2060 .{ "a", 1 },
2061 .{ "a" ++ Dir.path.sep_str ++ "b", 2 },
2062 .{ "a" ++ Dir.path.sep_str ++ "b" ++ Dir.path.sep_str ++ "c", 3 },
2063 .{ "a" ++ Dir.path.sep_str ++ "baz", 2 },
2064 });
2065
2066 const expected_basenames = std.StaticStringMap(void).initComptime(.{
2067 .{"dir1"},
2068 .{"foo"},
2069 .{"a"},
2070 .{"b"},
2071 .{"c"},
2072 .{"baz"},
2073 });
2074
2075 for (paths_to_create) |path| {
2076 try tmp.dir.createDirPath(io, path);
2077 }
2078
2079 var walker = try tmp.dir.walkSelectively(testing.allocator);
2080 defer walker.deinit();
2081
2082 var num_walked: usize = 0;
2083 while (try walker.next(io)) |entry| {
2084 if (entry.basename[0] == '.') continue;
2085 if (entry.kind == .directory) {
2086 try walker.enter(io, entry);
2087 }
2088
2089 expect(expected_basenames.has(entry.basename)) catch |err| {
2090 std.debug.print("found unexpected basename: {f}\n", .{std.ascii.hexEscape(entry.basename, .lower)});
2091 return err;
2092 };
2093 expect(expected_paths.has(entry.path)) catch |err| {
2094 std.debug.print("found unexpected path: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
2095 return err;
2096 };
2097 expectEqual(expected_paths.get(entry.path).?, entry.depth()) catch |err| {
2098 std.debug.print("path reported unexpected depth: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
2099 return err;
2100 };
2101
2102 // make sure that the entry.dir is the containing dir
2103 var entry_dir = try entry.dir.openDir(io, entry.basename, .{});
2104 defer entry_dir.close(io);
2105 num_walked += 1;
2106 }
2107 try expectEqual(expected_paths.kvs.len, num_walked);
2108}
2109
2110test "walker without fully iterating" {
2111 const io = testing.io;
2112
2113 var tmp = tmpDir(.{ .iterate = true });
2114 defer tmp.cleanup();
2115
2116 var walker = try tmp.dir.walk(testing.allocator);
2117 defer walker.deinit();
2118
2119 // Create 2 directories inside the tmp directory, but then only iterate once before breaking.
2120 // This ensures that walker doesn't try to close the initial directory when not fully iterating.
2121
2122 try tmp.dir.createDirPath(io, "a");
2123 try tmp.dir.createDirPath(io, "b");
2124
2125 var num_walked: usize = 0;
2126 while (try walker.next(io)) |_| {
2127 num_walked += 1;
2128 break;
2129 }
2130 try expectEqual(@as(usize, 1), num_walked);
2131}
2132
2133test "'.' and '..' in Dir functions" {
2134 if (native_os == .windows) {
2135 // https://codeberg.org/ziglang/zig/issues/31561
2136 return error.SkipZigTest;
2137 }
2138
2139 try testWithAllSupportedPathTypes(struct {
2140 fn impl(ctx: *TestContext) !void {
2141 const io = ctx.io;
2142 const subdir_path = try ctx.transformPath("./subdir");
2143 const file_path = try ctx.transformPath("./subdir/../file");
2144 const copy_path = try ctx.transformPath("./subdir/../copy");
2145 const rename_path = try ctx.transformPath("./subdir/../rename");
2146 const update_path = try ctx.transformPath("./subdir/../update");
2147
2148 try ctx.dir.createDir(io, subdir_path, .default_dir);
2149 try ctx.dir.access(io, subdir_path, .{});
2150 var created_subdir = try ctx.dir.openDir(io, subdir_path, .{});
2151 created_subdir.close(io);
2152
2153 const created_file = try ctx.dir.createFile(io, file_path, .{});
2154 created_file.close(io);
2155 try ctx.dir.access(io, file_path, .{});
2156
2157 try ctx.dir.copyFile(file_path, ctx.dir, copy_path, io, .{});
2158 try ctx.dir.rename(copy_path, ctx.dir, rename_path, io);
2159 const renamed_file = try ctx.dir.openFile(io, rename_path, .{});
2160 renamed_file.close(io);
2161 try ctx.dir.deleteFile(io, rename_path);
2162
2163 try ctx.dir.writeFile(io, .{ .sub_path = update_path, .data = "something" });
2164 var dir = ctx.dir;
2165 const prev_status = try dir.updateFile(io, file_path, dir, update_path, .{});
2166 try expectEqual(Dir.PrevStatus.stale, prev_status);
2167
2168 try ctx.dir.deleteDir(io, subdir_path);
2169 }
2170 }.impl);
2171}
2172
2173test "'.' and '..' in absolute functions" {
2174 if (!isRealPathSupported()) return error.SkipZigTest;
2175
2176 const io = testing.io;
2177
2178 var tmp = tmpDir(.{});
2179 defer tmp.cleanup();
2180
2181 var arena = ArenaAllocator.init(testing.allocator);
2182 defer arena.deinit();
2183 const allocator = arena.allocator();
2184
2185 const base_path = try tmp.dir.realPathFileAlloc(io, ".", allocator);
2186
2187 const subdir_path = try Dir.path.join(allocator, &.{ base_path, "./subdir" });
2188 try Dir.createDirAbsolute(io, subdir_path, .default_dir);
2189 try Dir.accessAbsolute(io, subdir_path, .{});
2190 var created_subdir = try Dir.openDirAbsolute(io, subdir_path, .{});
2191 created_subdir.close(io);
2192
2193 const created_file_path = try Dir.path.join(allocator, &.{ subdir_path, "../file" });
2194 const created_file = try Dir.createFileAbsolute(io, created_file_path, .{});
2195 created_file.close(io);
2196 try Dir.accessAbsolute(io, created_file_path, .{});
2197
2198 const copied_file_path = try Dir.path.join(allocator, &.{ subdir_path, "../copy" });
2199 try Dir.copyFileAbsolute(created_file_path, copied_file_path, io, .{});
2200 const renamed_file_path = try Dir.path.join(allocator, &.{ subdir_path, "../rename" });
2201 try Dir.renameAbsolute(copied_file_path, renamed_file_path, io);
2202 const renamed_file = try Dir.openFileAbsolute(io, renamed_file_path, .{});
2203 renamed_file.close(io);
2204 try Dir.deleteFileAbsolute(io, renamed_file_path);
2205
2206 try Dir.deleteDirAbsolute(io, subdir_path);
2207}
2208
2209test "chmod" {
2210 if (native_os == .windows or native_os == .wasi) return error.SkipZigTest;
2211
2212 const io = testing.io;
2213
2214 var tmp = tmpDir(.{});
2215 defer tmp.cleanup();
2216
2217 const file = try tmp.dir.createFile(io, "test_file", .{ .permissions = .fromMode(0o600) });
2218 defer file.close(io);
2219 try expectEqual(0o600, (try file.stat(io)).permissions.toMode() & 0o7777);
2220
2221 try file.setPermissions(io, .fromMode(0o644));
2222 try expectEqual(0o644, (try file.stat(io)).permissions.toMode() & 0o7777);
2223
2224 try tmp.dir.createDir(io, "test_dir", .default_dir);
2225 var dir = try tmp.dir.openDir(io, "test_dir", .{ .iterate = true });
2226 defer dir.close(io);
2227
2228 try dir.setPermissions(io, .fromMode(0o700));
2229 try expectEqual(0o700, (try dir.stat(io)).permissions.toMode() & 0o7777);
2230}
2231
2232test "change ownership" {
2233 if (native_os == .windows or native_os == .wasi) return error.SkipZigTest;
2234
2235 const io = testing.io;
2236
2237 var tmp = tmpDir(.{});
2238 defer tmp.cleanup();
2239
2240 const file = try tmp.dir.createFile(io, "test_file", .{});
2241 defer file.close(io);
2242 try file.setOwner(io, null, null);
2243
2244 try tmp.dir.createDir(io, "test_dir", .default_dir);
2245
2246 var dir = try tmp.dir.openDir(io, "test_dir", .{ .iterate = true });
2247 defer dir.close(io);
2248 try dir.setOwner(io, null, null);
2249}
2250
2251test "invalid UTF-8/WTF-8 paths" {
2252 const expected_err = switch (native_os) {
2253 .wasi => error.BadPathName,
2254 .windows => error.BadPathName,
2255 else => return error.SkipZigTest,
2256 };
2257
2258 try testWithAllSupportedPathTypes(struct {
2259 fn impl(ctx: *TestContext) !void {
2260 const io = ctx.io;
2261 // This is both invalid UTF-8 and WTF-8, since \xFF is an invalid start byte
2262 const invalid_path = try ctx.transformPath("\xFF");
2263
2264 try expectError(expected_err, ctx.dir.openFile(io, invalid_path, .{}));
2265
2266 try expectError(expected_err, ctx.dir.createFile(io, invalid_path, .{}));
2267
2268 try expectError(expected_err, ctx.dir.createDir(io, invalid_path, .default_dir));
2269
2270 try expectError(expected_err, ctx.dir.createDirPath(io, invalid_path));
2271 try expectError(expected_err, ctx.dir.createDirPathOpen(io, invalid_path, .{}));
2272
2273 try expectError(expected_err, ctx.dir.openDir(io, invalid_path, .{}));
2274
2275 try expectError(expected_err, ctx.dir.deleteFile(io, invalid_path));
2276
2277 try expectError(expected_err, ctx.dir.deleteDir(io, invalid_path));
2278
2279 try expectError(expected_err, ctx.dir.rename(invalid_path, ctx.dir, invalid_path, io));
2280
2281 try expectError(expected_err, ctx.dir.symLink(io, invalid_path, invalid_path, .{}));
2282
2283 try expectError(expected_err, ctx.dir.readLink(io, invalid_path, &[_]u8{}));
2284
2285 try expectError(expected_err, ctx.dir.readFile(io, invalid_path, &[_]u8{}));
2286 try expectError(expected_err, ctx.dir.readFileAlloc(io, invalid_path, testing.allocator, .limited(0)));
2287
2288 try expectError(expected_err, ctx.dir.deleteTree(io, invalid_path));
2289 try expectError(expected_err, ctx.dir.deleteTreeMinStackSize(io, invalid_path));
2290
2291 try expectError(expected_err, ctx.dir.writeFile(io, .{ .sub_path = invalid_path, .data = "" }));
2292
2293 try expectError(expected_err, ctx.dir.access(io, invalid_path, .{}));
2294
2295 var dir = ctx.dir;
2296 try expectError(expected_err, dir.updateFile(io, invalid_path, dir, invalid_path, .{}));
2297 try expectError(expected_err, ctx.dir.copyFile(invalid_path, ctx.dir, invalid_path, io, .{}));
2298
2299 try expectError(expected_err, ctx.dir.statFile(io, invalid_path, .{}));
2300
2301 if (native_os != .wasi) {
2302 try expectError(expected_err, ctx.dir.realPathFile(io, invalid_path, &[_]u8{}));
2303 try expectError(expected_err, ctx.dir.realPathFileAlloc(io, invalid_path, testing.allocator));
2304 }
2305
2306 try expectError(expected_err, Dir.rename(ctx.dir, invalid_path, ctx.dir, invalid_path, io));
2307
2308 if (native_os != .wasi and ctx.path_type != .relative) {
2309 var buf: [Dir.max_path_bytes]u8 = undefined;
2310 try expectError(expected_err, Dir.copyFileAbsolute(invalid_path, invalid_path, io, .{}));
2311 try expectError(expected_err, Dir.createDirAbsolute(io, invalid_path, .default_dir));
2312 try expectError(expected_err, Dir.deleteDirAbsolute(io, invalid_path));
2313 try expectError(expected_err, Dir.renameAbsolute(invalid_path, invalid_path, io));
2314 try expectError(expected_err, Dir.openDirAbsolute(io, invalid_path, .{}));
2315 try expectError(expected_err, Dir.openFileAbsolute(io, invalid_path, .{}));
2316 try expectError(expected_err, Dir.accessAbsolute(io, invalid_path, .{}));
2317 try expectError(expected_err, Dir.createFileAbsolute(io, invalid_path, .{}));
2318 try expectError(expected_err, Dir.deleteFileAbsolute(io, invalid_path));
2319 try expectError(expected_err, Dir.readLinkAbsolute(io, invalid_path, &buf));
2320 try expectError(expected_err, Dir.symLinkAbsolute(io, invalid_path, invalid_path, .{}));
2321 try expectError(expected_err, Dir.realPathFileAbsolute(io, invalid_path, &buf));
2322 try expectError(expected_err, Dir.realPathFileAbsoluteAlloc(io, invalid_path, testing.allocator));
2323 }
2324 }
2325 }.impl);
2326}
2327
2328test "read file non vectored" {
2329 const io = std.testing.io;
2330
2331 var tmp_dir = testing.tmpDir(.{});
2332 defer tmp_dir.cleanup();
2333
2334 const contents = "hello, world!\n";
2335
2336 const file = try tmp_dir.dir.createFile(io, "input.txt", .{ .read = true });
2337 defer file.close(io);
2338 {
2339 var file_writer: File.Writer = .init(file, io, &.{});
2340 try file_writer.interface.writeAll(contents);
2341 try file_writer.interface.flush();
2342 }
2343
2344 var file_reader: std.Io.File.Reader = .init(file, io, &.{});
2345
2346 var write_buffer: [100]u8 = undefined;
2347 var w: std.Io.Writer = .fixed(&write_buffer);
2348
2349 var i: usize = 0;
2350 while (true) {
2351 i += file_reader.interface.stream(&w, .limited(3)) catch |err| switch (err) {
2352 error.EndOfStream => break,
2353 else => |e| return e,
2354 };
2355 }
2356 try expectEqualStrings(contents, w.buffered());
2357 try expectEqual(contents.len, i);
2358}
2359
2360test "seek keeping partial buffer" {
2361 const io = std.testing.io;
2362
2363 var tmp_dir = testing.tmpDir(.{});
2364 defer tmp_dir.cleanup();
2365
2366 const contents = "0123456789";
2367
2368 const file = try tmp_dir.dir.createFile(io, "input.txt", .{ .read = true });
2369 defer file.close(io);
2370 {
2371 var file_writer: File.Writer = .init(file, io, &.{});
2372 try file_writer.interface.writeAll(contents);
2373 try file_writer.interface.flush();
2374 }
2375
2376 var read_buffer: [3]u8 = undefined;
2377 var file_reader: Io.File.Reader = .init(file, io, &read_buffer);
2378
2379 try expectEqual(0, file_reader.logicalPos());
2380
2381 var buf: [4]u8 = undefined;
2382 try file_reader.interface.readSliceAll(&buf);
2383
2384 if (file_reader.interface.bufferedLen() != 3) {
2385 // Pass the test if the OS doesn't give us vectored reads.
2386 return;
2387 }
2388
2389 try expectEqual(4, file_reader.logicalPos());
2390 try expectEqual(7, file_reader.pos);
2391 try file_reader.seekTo(6);
2392 try expectEqual(6, file_reader.logicalPos());
2393 try expectEqual(7, file_reader.pos);
2394
2395 try expectEqualStrings("0123", &buf);
2396
2397 const n = try file_reader.interface.readSliceShort(&buf);
2398 try expectEqual(4, n);
2399
2400 try expectEqualStrings("6789", &buf);
2401}
2402
2403test "seekBy" {
2404 const io = testing.io;
2405
2406 var tmp_dir = testing.tmpDir(.{});
2407 defer tmp_dir.cleanup();
2408
2409 try tmp_dir.dir.writeFile(io, .{ .sub_path = "blah.txt", .data = "let's test seekBy" });
2410 const f = try tmp_dir.dir.openFile(io, "blah.txt", .{ .mode = .read_only });
2411 defer f.close(io);
2412 var buf: [10]u8 = undefined;
2413 var reader = f.readerStreaming(io, &buf);
2414 // Seek without any buffered data
2415 try reader.seekBy(2);
2416
2417 // Seek when the buffered data is sufficient to satisfy the seek amount
2418 try reader.interface.fill(2);
2419 try reader.seekBy(2);
2420
2421 var buffer: [20]u8 = undefined;
2422 const n = try reader.interface.readSliceShort(&buffer);
2423 try expectEqual(13, n);
2424 try expectEqualStrings("s test seekBy", buffer[0..n]);
2425}
2426
2427test "seekTo flushes buffered data" {
2428 var tmp = std.testing.tmpDir(.{});
2429 defer tmp.cleanup();
2430
2431 const io = std.testing.io;
2432
2433 const contents = "data";
2434
2435 const file = try tmp.dir.createFile(io, "seek.bin", .{ .read = true });
2436 defer file.close(io);
2437 {
2438 var buf: [16]u8 = undefined;
2439 var file_writer = file.writer(io, &buf);
2440
2441 try file_writer.interface.writeAll(contents);
2442 try file_writer.seekTo(8);
2443 try file_writer.interface.flush();
2444 }
2445
2446 var read_buffer: [16]u8 = undefined;
2447 var file_reader: std.Io.File.Reader = .init(file, io, &read_buffer);
2448
2449 var buf: [4]u8 = undefined;
2450 try file_reader.interface.readSliceAll(&buf);
2451 try expectEqualStrings(contents, &buf);
2452}
2453
2454test "File.Writer sendfile with buffered contents" {
2455 const io = testing.io;
2456
2457 var tmp_dir = testing.tmpDir(.{});
2458 defer tmp_dir.cleanup();
2459
2460 {
2461 try tmp_dir.dir.writeFile(io, .{ .sub_path = "a", .data = "bcd" });
2462 const in = try tmp_dir.dir.openFile(io, "a", .{});
2463 defer in.close(io);
2464 const out = try tmp_dir.dir.createFile(io, "b", .{});
2465 defer out.close(io);
2466
2467 var in_buf: [2]u8 = undefined;
2468 var in_r = in.reader(io, &in_buf);
2469 _ = try in_r.getSize(); // Catch seeks past end by populating size
2470 try in_r.interface.fill(2);
2471
2472 var out_buf: [1]u8 = undefined;
2473 var out_w = out.writerStreaming(io, &out_buf);
2474 try out_w.interface.writeByte('a');
2475 try expectEqual(3, try out_w.interface.sendFileAll(&in_r, .unlimited));
2476 try out_w.interface.flush();
2477 }
2478
2479 var check = try tmp_dir.dir.openFile(io, "b", .{});
2480 defer check.close(io);
2481 var check_buf: [4]u8 = undefined;
2482 var check_r = check.reader(io, &check_buf);
2483 try expectEqualStrings("abcd", try check_r.interface.take(4));
2484 try expectError(error.EndOfStream, check_r.interface.takeByte());
2485}
2486
2487test "readlink on Windows" {
2488 if (native_os != .windows) return error.SkipZigTest;
2489
2490 const io = testing.io;
2491
2492 try testReadLinkWindows(io, "C:\\ProgramData", "C:\\Users\\All Users");
2493 try testReadLinkWindows(io, "C:\\Users\\Default", "C:\\Users\\Default User");
2494 try testReadLinkWindows(io, "C:\\Users", "C:\\Documents and Settings");
2495}
2496
2497fn testReadLinkWindows(io: Io, target_path: []const u8, symlink_path: []const u8) !void {
2498 var buffer: [Dir.max_path_bytes]u8 = undefined;
2499 const len = try Dir.readLinkAbsolute(io, symlink_path, &buffer);
2500 const given = buffer[0..len];
2501 try expect(mem.eql(u8, target_path, given));
2502}
2503
2504test "readlinkat" {
2505 const io = testing.io;
2506
2507 var tmp = tmpDir(.{});
2508 defer tmp.cleanup();
2509
2510 // create file
2511 try tmp.dir.writeFile(io, .{ .sub_path = "file.txt", .data = "nonsense" });
2512
2513 // create a symbolic link
2514 try setupSymlink(io, tmp.dir, "file.txt", "link", .{});
2515
2516 // read the link
2517 var buffer: [Dir.max_path_bytes]u8 = undefined;
2518 const read_link = buffer[0..try tmp.dir.readLink(io, "link", &buffer)];
2519 try expectEqualStrings("file.txt", read_link);
2520}
2521
2522test "fchmodat smoke test" {
2523 if (!Io.File.Permissions.has_executable_bit) return error.SkipZigTest;
2524
2525 const io = testing.io;
2526
2527 var tmp = tmpDir(.{});
2528 defer tmp.cleanup();
2529
2530 try expectError(error.FileNotFound, tmp.dir.setFilePermissions(io, "regfile", .fromMode(0o666), .{}));
2531 const file = try tmp.dir.createFile(io, "regfile", .{
2532 .exclusive = true,
2533 .permissions = .fromMode(0o644),
2534 });
2535 file.close(io);
2536
2537 if ((builtin.cpu.arch == .riscv32 or builtin.cpu.arch.isLoongArch()) and
2538 builtin.os.tag == .linux and !builtin.link_libc)
2539 {
2540 return error.SkipZigTest; // No `fstatat()`.
2541 }
2542
2543 try tmp.dir.symLink(io, "regfile", "symlink", .{});
2544 const sym_mode = blk: {
2545 const st = try tmp.dir.statFile(io, "symlink", .{ .follow_symlinks = false });
2546 break :blk st.permissions.toMode() & 0b111_111_111;
2547 };
2548
2549 try tmp.dir.setFilePermissions(io, "regfile", .fromMode(0o640), .{});
2550 try expectMode(io, tmp.dir, "regfile", .fromMode(0o640));
2551 try tmp.dir.setFilePermissions(io, "regfile", .fromMode(0o600), .{ .follow_symlinks = false });
2552 try expectMode(io, tmp.dir, "regfile", .fromMode(0o600));
2553
2554 try tmp.dir.setFilePermissions(io, "symlink", .fromMode(0o640), .{});
2555 try expectMode(io, tmp.dir, "regfile", .fromMode(0o640));
2556 try expectMode(io, tmp.dir, "symlink", .fromMode(sym_mode));
2557
2558 var test_link = true;
2559 tmp.dir.setFilePermissions(io, "symlink", .fromMode(0o600), .{ .follow_symlinks = false }) catch |err| switch (err) {
2560 error.OperationUnsupported => test_link = false,
2561 else => |e| return e,
2562 };
2563 if (test_link) try expectMode(io, tmp.dir, "symlink", .fromMode(0o600));
2564 try expectMode(io, tmp.dir, "regfile", .fromMode(0o640));
2565}
2566
2567fn expectMode(io: Io, dir: Dir, file: []const u8, permissions: File.Permissions) !void {
2568 const mode = permissions.toMode();
2569 const st = try dir.statFile(io, file, .{ .follow_symlinks = false });
2570 const found_mode = st.permissions.toMode();
2571 try expectEqual(mode, found_mode & 0b111_111_111);
2572}
2573
2574test "isatty" {
2575 const io = testing.io;
2576
2577 var tmp = tmpDir(.{});
2578 defer tmp.cleanup();
2579
2580 var file = try tmp.dir.createFile(io, "foo", .{});
2581 defer file.close(io);
2582
2583 try expectEqual(false, try file.isTty(io));
2584}
2585
2586test "read positional empty buffer" {
2587 const io = testing.io;
2588
2589 var tmp = tmpDir(.{});
2590 defer tmp.cleanup();
2591
2592 var file = try tmp.dir.createFile(io, "pread_empty", .{ .read = true });
2593 defer file.close(io);
2594
2595 var buffer: [0]u8 = undefined;
2596 try expectEqual(0, try file.readPositional(io, &.{&buffer}, 0));
2597}
2598
2599test "write streaming empty buffer" {
2600 const io = testing.io;
2601
2602 var tmp = tmpDir(.{});
2603 defer tmp.cleanup();
2604
2605 var file = try tmp.dir.createFile(io, "write_empty", .{});
2606 defer file.close(io);
2607
2608 const buffer: [0]u8 = .{};
2609 try file.writeStreamingAll(io, &buffer);
2610}
2611
2612test "write positional empty buffer" {
2613 const io = testing.io;
2614
2615 var tmp = tmpDir(.{});
2616 defer tmp.cleanup();
2617
2618 var file = try tmp.dir.createFile(io, "pwrite_empty", .{});
2619 defer file.close(io);
2620
2621 const buffer: [0]u8 = .{};
2622 try expectEqual(0, try file.writePositional(io, &.{&buffer}, 0));
2623}
2624
2625test "access smoke test" {
2626 if (native_os == .wasi) return error.SkipZigTest;
2627 if (native_os == .windows) return error.SkipZigTest;
2628 if (native_os == .openbsd) return error.SkipZigTest;
2629
2630 const io = testing.io;
2631
2632 var tmp = tmpDir(.{});
2633 defer tmp.cleanup();
2634
2635 {
2636 // Create some file using `open`.
2637 const file = try tmp.dir.createFile(io, "some_file", .{ .read = true, .exclusive = true });
2638 file.close(io);
2639 }
2640
2641 {
2642 // Try to access() the file
2643 if (native_os == .windows) {
2644 try tmp.dir.access(io, "some_file", .{});
2645 } else {
2646 try tmp.dir.access(io, "some_file", .{ .read = true, .write = true });
2647 }
2648 }
2649
2650 {
2651 // Try to access() a non-existent file - should fail with error.FileNotFound
2652 try expectError(error.FileNotFound, tmp.dir.access(io, "some_other_file", .{}));
2653 }
2654
2655 {
2656 // Create some directory
2657 try tmp.dir.createDir(io, "some_dir", .default_dir);
2658 }
2659
2660 {
2661 // Try to access() the directory
2662 try tmp.dir.access(io, "some_dir", .{});
2663 }
2664}
2665
2666test "write streaming a long vector" {
2667 const io = testing.io;
2668
2669 var tmp = tmpDir(.{});
2670 defer tmp.cleanup();
2671
2672 var file = try tmp.dir.createFile(io, "pwritev", .{});
2673 defer file.close(io);
2674
2675 var vecs: [2000][]const u8 = undefined;
2676 for (&vecs) |*v| v.* = "a";
2677
2678 const n = try file.writePositional(io, &vecs, 0);
2679 try expect(n <= vecs.len);
2680}
2681
2682test "open smoke test" {
2683 if (native_os == .wasi) return error.SkipZigTest;
2684 if (native_os == .windows) return error.SkipZigTest;
2685 if (native_os == .openbsd) return error.SkipZigTest;
2686
2687 // TODO verify file attributes using `fstat`
2688
2689 var tmp = tmpDir(.{});
2690 defer tmp.cleanup();
2691
2692 const io = testing.io;
2693
2694 {
2695 // Create some file using `open`.
2696 const file = try tmp.dir.createFile(io, "some_file", .{ .exclusive = true });
2697 file.close(io);
2698 }
2699
2700 // Try this again with the same flags. This op should fail with error.PathAlreadyExists.
2701 try expectError(
2702 error.PathAlreadyExists,
2703 tmp.dir.createFile(io, "some_file", .{ .exclusive = true }),
2704 );
2705
2706 {
2707 // Try opening without exclusive flag.
2708 const file = try tmp.dir.createFile(io, "some_file", .{});
2709 file.close(io);
2710 }
2711
2712 try expectError(error.NotDir, tmp.dir.openDir(io, "some_file", .{}));
2713 try tmp.dir.createDir(io, "some_dir", .default_dir);
2714
2715 {
2716 const dir = try tmp.dir.openDir(io, "some_dir", .{});
2717 dir.close(io);
2718 }
2719
2720 // Try opening as file which should fail.
2721 try expectError(error.IsDir, tmp.dir.openFile(io, "some_dir", .{ .allow_directory = false }));
2722}
2723
2724test "hard link with different directories" {
2725 if (native_os == .wasi or native_os == .windows) return error.SkipZigTest;
2726
2727 const io = testing.io;
2728
2729 var tmp = tmpDir(.{});
2730 defer tmp.cleanup();
2731
2732 const target_name = "link-target";
2733 const link_name = "newlink";
2734
2735 const subdir = try tmp.dir.createDirPathOpen(io, "subdir", .{});
2736
2737 defer tmp.dir.deleteFile(io, target_name) catch {};
2738 try tmp.dir.writeFile(io, .{ .sub_path = target_name, .data = "example" });
2739
2740 // Test 1: link from file in subdir back up to target in parent directory
2741 tmp.dir.hardLink(target_name, subdir, link_name, io, .{}) catch |err| switch (err) {
2742 error.OperationUnsupported => return error.SkipZigTest,
2743 else => |e| return e,
2744 };
2745
2746 const efd = try tmp.dir.openFile(io, target_name, .{});
2747 defer efd.close(io);
2748
2749 const nfd = try subdir.openFile(io, link_name, .{});
2750 defer nfd.close(io);
2751
2752 {
2753 const e_stat = try efd.stat(io);
2754 const n_stat = try nfd.stat(io);
2755
2756 try expectEqual(e_stat.inode, n_stat.inode);
2757 try expectEqual(2, e_stat.nlink);
2758 try expectEqual(2, n_stat.nlink);
2759 }
2760
2761 // Test 2: remove link
2762 try subdir.deleteFile(io, link_name);
2763 const e_stat = try efd.stat(io);
2764 try expectEqual(1, e_stat.nlink);
2765}
2766
2767test "stat smoke test" {
2768 if (native_os == .wasi and !builtin.link_libc) return error.SkipZigTest;
2769
2770 const io = testing.io;
2771
2772 var tmp = tmpDir(.{});
2773 defer tmp.cleanup();
2774
2775 // create dummy file
2776 const contents = "nonsense";
2777 try tmp.dir.writeFile(io, .{ .sub_path = "file.txt", .data = contents });
2778
2779 // fetch file's info on the opened fd directly
2780 const file = try tmp.dir.openFile(io, "file.txt", .{});
2781 const stat = try file.stat(io);
2782 defer file.close(io);
2783
2784 // now repeat but using directory handle instead
2785 const statat = try tmp.dir.statFile(io, "file.txt", .{ .follow_symlinks = false });
2786
2787 try expectEqual(stat.inode, statat.inode);
2788 try expectEqual(stat.nlink, statat.nlink);
2789 try expectEqual(stat.size, statat.size);
2790 try expectEqual(stat.permissions, statat.permissions);
2791 try expectEqual(stat.kind, statat.kind);
2792 try expectEqual(stat.atime, statat.atime);
2793 try expectEqual(stat.mtime, statat.mtime);
2794 try expectEqual(stat.ctime, statat.ctime);
2795}