master
1const builtin = @import("builtin");
2const native_os = builtin.os.tag;
3
4const std = @import("../std.zig");
5const Io = std.Io;
6const testing = std.testing;
7const fs = std.fs;
8const mem = std.mem;
9const wasi = std.os.wasi;
10const windows = std.os.windows;
11const posix = std.posix;
12
13const ArenaAllocator = std.heap.ArenaAllocator;
14const Dir = std.fs.Dir;
15const File = std.fs.File;
16const tmpDir = testing.tmpDir;
17const SymLinkFlags = std.fs.Dir.SymLinkFlags;
18
19const PathType = enum {
20 relative,
21 absolute,
22 unc,
23
24 pub fn isSupported(self: PathType, target_os: std.Target.Os) bool {
25 return switch (self) {
26 .relative => true,
27 .absolute => std.os.isGetFdPathSupportedOnTarget(target_os),
28 .unc => target_os.tag == .windows,
29 };
30 }
31
32 pub const TransformError = posix.RealPathError || error{OutOfMemory};
33 pub const TransformFn = fn (allocator: mem.Allocator, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8;
34
35 pub fn getTransformFn(comptime path_type: PathType) TransformFn {
36 switch (path_type) {
37 .relative => return struct {
38 fn transform(allocator: mem.Allocator, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8 {
39 _ = allocator;
40 _ = dir;
41 return relative_path;
42 }
43 }.transform,
44 .absolute => return struct {
45 fn transform(allocator: mem.Allocator, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8 {
46 // The final path may not actually exist which would cause realpath to fail.
47 // So instead, we get the path of the dir and join it with the relative path.
48 var fd_path_buf: [fs.max_path_bytes]u8 = undefined;
49 const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf);
50 return fs.path.joinZ(allocator, &.{ dir_path, relative_path });
51 }
52 }.transform,
53 .unc => return struct {
54 fn transform(allocator: mem.Allocator, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8 {
55 // Any drive absolute path (C:\foo) can be converted into a UNC path by
56 // using '127.0.0.1' as the server name and '<drive letter>$' as the share name.
57 var fd_path_buf: [fs.max_path_bytes]u8 = undefined;
58 const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf);
59 const windows_path_type = windows.getWin32PathType(u8, dir_path);
60 switch (windows_path_type) {
61 .unc_absolute => return fs.path.joinZ(allocator, &.{ dir_path, relative_path }),
62 .drive_absolute => {
63 // `C:\<...>` -> `\\127.0.0.1\C$\<...>`
64 const prepended = "\\\\127.0.0.1\\";
65 var path = try fs.path.joinZ(allocator, &.{ prepended, dir_path, relative_path });
66 path[prepended.len + 1] = '$';
67 return path;
68 },
69 else => unreachable,
70 }
71 }
72 }.transform,
73 }
74 }
75};
76
77const TestContext = struct {
78 io: Io,
79 path_type: PathType,
80 path_sep: u8,
81 arena: ArenaAllocator,
82 tmp: testing.TmpDir,
83 dir: std.fs.Dir,
84 transform_fn: *const PathType.TransformFn,
85
86 pub fn init(path_type: PathType, path_sep: u8, allocator: mem.Allocator, transform_fn: *const PathType.TransformFn) TestContext {
87 const tmp = tmpDir(.{ .iterate = true });
88 return .{
89 .io = testing.io,
90 .path_type = path_type,
91 .path_sep = path_sep,
92 .arena = ArenaAllocator.init(allocator),
93 .tmp = tmp,
94 .dir = tmp.dir,
95 .transform_fn = transform_fn,
96 };
97 }
98
99 pub fn deinit(self: *TestContext) void {
100 self.arena.deinit();
101 self.tmp.cleanup();
102 }
103
104 /// Returns the `relative_path` transformed into the TestContext's `path_type`,
105 /// with any supported path separators replaced by `path_sep`.
106 /// The result is allocated by the TestContext's arena and will be free'd during
107 /// `TestContext.deinit`.
108 pub fn transformPath(self: *TestContext, relative_path: [:0]const u8) ![:0]const u8 {
109 const allocator = self.arena.allocator();
110 const transformed_path = try self.transform_fn(allocator, self.dir, relative_path);
111 if (native_os == .windows) {
112 const transformed_sep_path = try allocator.dupeZ(u8, transformed_path);
113 std.mem.replaceScalar(u8, transformed_sep_path, switch (self.path_sep) {
114 '/' => '\\',
115 '\\' => '/',
116 else => unreachable,
117 }, self.path_sep);
118 return transformed_sep_path;
119 }
120 return transformed_path;
121 }
122
123 /// Replaces any path separators with the canonical path separator for the platform
124 /// (e.g. all path separators are converted to `\` on Windows).
125 /// If path separators are replaced, then the result is allocated by the
126 /// TestContext's arena and will be free'd during `TestContext.deinit`.
127 pub fn toCanonicalPathSep(self: *TestContext, path: [:0]const u8) ![:0]const u8 {
128 if (native_os == .windows) {
129 const allocator = self.arena.allocator();
130 const transformed_sep_path = try allocator.dupeZ(u8, path);
131 std.mem.replaceScalar(u8, transformed_sep_path, '/', '\\');
132 return transformed_sep_path;
133 }
134 return path;
135 }
136};
137
138/// `test_func` must be a function that takes a `*TestContext` as a parameter and returns `!void`.
139/// `test_func` will be called once for each PathType that the current target supports,
140/// and will be passed a TestContext that can transform a relative path into the path type under test.
141/// The TestContext will also create a tmp directory for you (and will clean it up for you too).
142fn testWithAllSupportedPathTypes(test_func: anytype) !void {
143 try testWithPathTypeIfSupported(.relative, '/', test_func);
144 try testWithPathTypeIfSupported(.absolute, '/', test_func);
145 try testWithPathTypeIfSupported(.unc, '/', test_func);
146 try testWithPathTypeIfSupported(.relative, '\\', test_func);
147 try testWithPathTypeIfSupported(.absolute, '\\', test_func);
148 try testWithPathTypeIfSupported(.unc, '\\', test_func);
149}
150
151fn testWithPathTypeIfSupported(comptime path_type: PathType, comptime path_sep: u8, test_func: anytype) !void {
152 if (!(comptime path_type.isSupported(builtin.os))) return;
153 if (!(comptime fs.path.isSep(path_sep))) return;
154
155 var ctx = TestContext.init(path_type, path_sep, testing.allocator, path_type.getTransformFn());
156 defer ctx.deinit();
157
158 try test_func(&ctx);
159}
160
161// For use in test setup. If the symlink creation fails on Windows with
162// AccessDenied, then make the test failure silent (it is not a Zig failure).
163fn setupSymlink(dir: Dir, target: []const u8, link: []const u8, flags: SymLinkFlags) !void {
164 return dir.symLink(target, link, flags) catch |err| switch (err) {
165 // Symlink requires admin privileges on windows, so this test can legitimately fail.
166 error.AccessDenied => if (native_os == .windows) return error.SkipZigTest else return err,
167 else => return err,
168 };
169}
170
171// For use in test setup. If the symlink creation fails on Windows with
172// AccessDenied, then make the test failure silent (it is not a Zig failure).
173fn setupSymlinkAbsolute(target: []const u8, link: []const u8, flags: SymLinkFlags) !void {
174 return fs.symLinkAbsolute(target, link, flags) catch |err| switch (err) {
175 error.AccessDenied => if (native_os == .windows) return error.SkipZigTest else return err,
176 else => return err,
177 };
178}
179
180test "Dir.readLink" {
181 try testWithAllSupportedPathTypes(struct {
182 fn impl(ctx: *TestContext) !void {
183 // Create some targets
184 const file_target_path = try ctx.transformPath("file.txt");
185 try ctx.dir.writeFile(.{ .sub_path = file_target_path, .data = "nonsense" });
186 const dir_target_path = try ctx.transformPath("subdir");
187 try ctx.dir.makeDir(dir_target_path);
188
189 // On Windows, symlink targets always use the canonical path separator
190 const canonical_file_target_path = try ctx.toCanonicalPathSep(file_target_path);
191 const canonical_dir_target_path = try ctx.toCanonicalPathSep(dir_target_path);
192
193 // test 1: symlink to a file
194 try setupSymlink(ctx.dir, file_target_path, "symlink1", .{});
195 try testReadLink(ctx.dir, canonical_file_target_path, "symlink1");
196 if (builtin.os.tag == .windows) {
197 try testReadLinkW(testing.allocator, ctx.dir, canonical_file_target_path, "symlink1");
198 }
199
200 // test 2: symlink to a directory (can be different on Windows)
201 try setupSymlink(ctx.dir, dir_target_path, "symlink2", .{ .is_directory = true });
202 try testReadLink(ctx.dir, canonical_dir_target_path, "symlink2");
203 if (builtin.os.tag == .windows) {
204 try testReadLinkW(testing.allocator, ctx.dir, canonical_dir_target_path, "symlink2");
205 }
206
207 // test 3: relative path symlink
208 const parent_file = ".." ++ fs.path.sep_str ++ "target.txt";
209 const canonical_parent_file = try ctx.toCanonicalPathSep(parent_file);
210 var subdir = try ctx.dir.makeOpenPath("subdir", .{});
211 defer subdir.close();
212 try setupSymlink(subdir, canonical_parent_file, "relative-link.txt", .{});
213 try testReadLink(subdir, canonical_parent_file, "relative-link.txt");
214 if (builtin.os.tag == .windows) {
215 try testReadLinkW(testing.allocator, subdir, canonical_parent_file, "relative-link.txt");
216 }
217 }
218 }.impl);
219}
220
221fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !void {
222 var buffer: [fs.max_path_bytes]u8 = undefined;
223 const actual = try dir.readLink(symlink_path, buffer[0..]);
224 try testing.expectEqualStrings(target_path, actual);
225}
226
227fn testReadLinkW(allocator: mem.Allocator, dir: Dir, target_path: []const u8, symlink_path: []const u8) !void {
228 const target_path_w = try std.unicode.wtf8ToWtf16LeAlloc(allocator, target_path);
229 defer allocator.free(target_path_w);
230 // Calling the W functions directly requires the path to be NT-prefixed
231 const symlink_path_w = try std.os.windows.sliceToPrefixedFileW(dir.fd, symlink_path);
232 const wtf16_buffer = try allocator.alloc(u16, target_path_w.len);
233 defer allocator.free(wtf16_buffer);
234 const actual = try dir.readLinkW(symlink_path_w.span(), wtf16_buffer);
235 try testing.expectEqualSlices(u16, target_path_w, actual);
236}
237
238fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void {
239 var buffer: [fs.max_path_bytes]u8 = undefined;
240 const given = try fs.readLinkAbsolute(symlink_path, buffer[0..]);
241 try testing.expectEqualStrings(target_path, given);
242}
243
244test "File.stat on a File that is a symlink returns Kind.sym_link" {
245 // This test requires getting a file descriptor of a symlink which
246 // is not possible on all targets
247 switch (builtin.target.os.tag) {
248 .windows, .linux => {},
249 else => return error.SkipZigTest,
250 }
251
252 try testWithAllSupportedPathTypes(struct {
253 fn impl(ctx: *TestContext) !void {
254 const dir_target_path = try ctx.transformPath("subdir");
255 try ctx.dir.makeDir(dir_target_path);
256
257 try setupSymlink(ctx.dir, dir_target_path, "symlink", .{ .is_directory = true });
258
259 var symlink = switch (builtin.target.os.tag) {
260 .windows => windows_symlink: {
261 const sub_path_w = try windows.cStrToPrefixedFileW(ctx.dir.fd, "symlink");
262
263 var result = Dir{
264 .fd = undefined,
265 };
266
267 const path_len_bytes = @as(u16, @intCast(sub_path_w.span().len * 2));
268 var nt_name = windows.UNICODE_STRING{
269 .Length = path_len_bytes,
270 .MaximumLength = path_len_bytes,
271 .Buffer = @constCast(&sub_path_w.data),
272 };
273 var attr = windows.OBJECT_ATTRIBUTES{
274 .Length = @sizeOf(windows.OBJECT_ATTRIBUTES),
275 .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else ctx.dir.fd,
276 .Attributes = 0,
277 .ObjectName = &nt_name,
278 .SecurityDescriptor = null,
279 .SecurityQualityOfService = null,
280 };
281 var io: windows.IO_STATUS_BLOCK = undefined;
282 const rc = windows.ntdll.NtCreateFile(
283 &result.fd,
284 windows.STANDARD_RIGHTS_READ | windows.FILE_READ_ATTRIBUTES | windows.FILE_READ_EA | windows.SYNCHRONIZE | windows.FILE_TRAVERSE,
285 &attr,
286 &io,
287 null,
288 windows.FILE_ATTRIBUTE_NORMAL,
289 windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE,
290 windows.FILE_OPEN,
291 // FILE_OPEN_REPARSE_POINT is the important thing here
292 windows.FILE_OPEN_REPARSE_POINT | windows.FILE_DIRECTORY_FILE | windows.FILE_SYNCHRONOUS_IO_NONALERT | windows.FILE_OPEN_FOR_BACKUP_INTENT,
293 null,
294 0,
295 );
296
297 switch (rc) {
298 .SUCCESS => break :windows_symlink result,
299 else => return windows.unexpectedStatus(rc),
300 }
301 },
302 .linux => linux_symlink: {
303 const sub_path_c = try posix.toPosixPath("symlink");
304 // the O_NOFOLLOW | O_PATH combination can obtain a fd to a symlink
305 // note that if O_DIRECTORY is set, then this will error with ENOTDIR
306 const flags: posix.O = .{
307 .NOFOLLOW = true,
308 .PATH = true,
309 .ACCMODE = .RDONLY,
310 .CLOEXEC = true,
311 };
312 const fd = try posix.openatZ(ctx.dir.fd, &sub_path_c, flags, 0);
313 break :linux_symlink Dir{ .fd = fd };
314 },
315 else => unreachable,
316 };
317 defer symlink.close();
318
319 const stat = try symlink.stat();
320 try testing.expectEqual(File.Kind.sym_link, stat.kind);
321 }
322 }.impl);
323}
324
325test "openDir" {
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.makeDir(subdir_path);
331
332 for ([_][]const u8{ "", ".", ".." }) |sub_path| {
333 const dir_path = try fs.path.join(allocator, &.{ subdir_path, sub_path });
334 var dir = try ctx.dir.openDir(dir_path, .{});
335 defer dir.close();
336 }
337 }
338 }.impl);
339}
340
341test "accessAbsolute" {
342 if (native_os == .wasi) return error.SkipZigTest;
343
344 var tmp = tmpDir(.{});
345 defer tmp.cleanup();
346
347 const base_path = try tmp.dir.realpathAlloc(testing.allocator, ".");
348 defer testing.allocator.free(base_path);
349
350 try fs.accessAbsolute(base_path, .{});
351}
352
353test "openDirAbsolute" {
354 if (native_os == .wasi) return error.SkipZigTest;
355
356 var tmp = tmpDir(.{});
357 defer tmp.cleanup();
358
359 const tmp_ino = (try tmp.dir.stat()).inode;
360
361 try tmp.dir.makeDir("subdir");
362 const sub_path = try tmp.dir.realpathAlloc(testing.allocator, "subdir");
363 defer testing.allocator.free(sub_path);
364
365 // Can open sub_path
366 var tmp_sub = try fs.openDirAbsolute(sub_path, .{});
367 defer tmp_sub.close();
368
369 const sub_ino = (try tmp_sub.stat()).inode;
370
371 {
372 // Can open sub_path + ".."
373 const dir_path = try fs.path.join(testing.allocator, &.{ sub_path, ".." });
374 defer testing.allocator.free(dir_path);
375
376 var dir = try fs.openDirAbsolute(dir_path, .{});
377 defer dir.close();
378
379 const ino = (try dir.stat()).inode;
380 try testing.expectEqual(tmp_ino, ino);
381 }
382
383 {
384 // Can open sub_path + "."
385 const dir_path = try fs.path.join(testing.allocator, &.{ sub_path, "." });
386 defer testing.allocator.free(dir_path);
387
388 var dir = try fs.openDirAbsolute(dir_path, .{});
389 defer dir.close();
390
391 const ino = (try dir.stat()).inode;
392 try testing.expectEqual(sub_ino, ino);
393 }
394
395 {
396 // Can open subdir + "..", with some extra "."
397 const dir_path = try fs.path.join(testing.allocator, &.{ sub_path, ".", "..", "." });
398 defer testing.allocator.free(dir_path);
399
400 var dir = try fs.openDirAbsolute(dir_path, .{});
401 defer dir.close();
402
403 const ino = (try dir.stat()).inode;
404 try testing.expectEqual(tmp_ino, ino);
405 }
406}
407
408test "openDir cwd parent '..'" {
409 var dir = fs.cwd().openDir("..", .{}) catch |err| {
410 if (native_os == .wasi and err == error.PermissionDenied) {
411 return; // This is okay. WASI disallows escaping from the fs sandbox
412 }
413 return err;
414 };
415 defer dir.close();
416}
417
418test "openDir non-cwd parent '..'" {
419 switch (native_os) {
420 .wasi, .netbsd, .openbsd => return error.SkipZigTest,
421 else => {},
422 }
423
424 var tmp = tmpDir(.{});
425 defer tmp.cleanup();
426
427 var subdir = try tmp.dir.makeOpenPath("subdir", .{});
428 defer subdir.close();
429
430 var dir = try subdir.openDir("..", .{});
431 defer dir.close();
432
433 const expected_path = try tmp.dir.realpathAlloc(testing.allocator, ".");
434 defer testing.allocator.free(expected_path);
435
436 const actual_path = try dir.realpathAlloc(testing.allocator, ".");
437 defer testing.allocator.free(actual_path);
438
439 try testing.expectEqualStrings(expected_path, actual_path);
440}
441
442test "readLinkAbsolute" {
443 if (native_os == .wasi) return error.SkipZigTest;
444
445 var tmp = tmpDir(.{});
446 defer tmp.cleanup();
447
448 // Create some targets
449 try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = "nonsense" });
450 try tmp.dir.makeDir("subdir");
451
452 // Get base abs path
453 var arena = ArenaAllocator.init(testing.allocator);
454 defer arena.deinit();
455 const allocator = arena.allocator();
456
457 const base_path = try tmp.dir.realpathAlloc(allocator, ".");
458
459 {
460 const target_path = try fs.path.join(allocator, &.{ base_path, "file.txt" });
461 const symlink_path = try fs.path.join(allocator, &.{ base_path, "symlink1" });
462
463 // Create symbolic link by path
464 try setupSymlinkAbsolute(target_path, symlink_path, .{});
465 try testReadLinkAbsolute(target_path, symlink_path);
466 }
467 {
468 const target_path = try fs.path.join(allocator, &.{ base_path, "subdir" });
469 const symlink_path = try fs.path.join(allocator, &.{ base_path, "symlink2" });
470
471 // Create symbolic link to a directory by path
472 try setupSymlinkAbsolute(target_path, symlink_path, .{ .is_directory = true });
473 try testReadLinkAbsolute(target_path, symlink_path);
474 }
475}
476
477test "Dir.Iterator" {
478 var tmp_dir = tmpDir(.{ .iterate = true });
479 defer tmp_dir.cleanup();
480
481 // First, create a couple of entries to iterate over.
482 const file = try tmp_dir.dir.createFile("some_file", .{});
483 file.close();
484
485 try tmp_dir.dir.makeDir("some_dir");
486
487 var arena = ArenaAllocator.init(testing.allocator);
488 defer arena.deinit();
489 const allocator = arena.allocator();
490
491 var entries = std.array_list.Managed(Dir.Entry).init(allocator);
492
493 // Create iterator.
494 var iter = tmp_dir.dir.iterate();
495 while (try iter.next()) |entry| {
496 // We cannot just store `entry` as on Windows, we're re-using the name buffer
497 // which means we'll actually share the `name` pointer between entries!
498 const name = try allocator.dupe(u8, entry.name);
499 try entries.append(Dir.Entry{ .name = name, .kind = entry.kind });
500 }
501
502 try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
503 try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
504 try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
505}
506
507test "Dir.Iterator many entries" {
508 var tmp_dir = tmpDir(.{ .iterate = true });
509 defer tmp_dir.cleanup();
510
511 const num = 1024;
512 var i: usize = 0;
513 var buf: [4]u8 = undefined; // Enough to store "1024".
514 while (i < num) : (i += 1) {
515 const name = try std.fmt.bufPrint(&buf, "{}", .{i});
516 const file = try tmp_dir.dir.createFile(name, .{});
517 file.close();
518 }
519
520 var arena = ArenaAllocator.init(testing.allocator);
521 defer arena.deinit();
522 const allocator = arena.allocator();
523
524 var entries = std.array_list.Managed(Dir.Entry).init(allocator);
525
526 // Create iterator.
527 var iter = tmp_dir.dir.iterate();
528 while (try iter.next()) |entry| {
529 // We cannot just store `entry` as on Windows, we're re-using the name buffer
530 // which means we'll actually share the `name` pointer between entries!
531 const name = try allocator.dupe(u8, entry.name);
532 try entries.append(.{ .name = name, .kind = entry.kind });
533 }
534
535 i = 0;
536 while (i < num) : (i += 1) {
537 const name = try std.fmt.bufPrint(&buf, "{}", .{i});
538 try testing.expect(contains(&entries, .{ .name = name, .kind = .file }));
539 }
540}
541
542test "Dir.Iterator twice" {
543 var tmp_dir = tmpDir(.{ .iterate = true });
544 defer tmp_dir.cleanup();
545
546 // First, create a couple of entries to iterate over.
547 const file = try tmp_dir.dir.createFile("some_file", .{});
548 file.close();
549
550 try tmp_dir.dir.makeDir("some_dir");
551
552 var arena = ArenaAllocator.init(testing.allocator);
553 defer arena.deinit();
554 const allocator = arena.allocator();
555
556 var i: u8 = 0;
557 while (i < 2) : (i += 1) {
558 var entries = std.array_list.Managed(Dir.Entry).init(allocator);
559
560 // Create iterator.
561 var iter = tmp_dir.dir.iterate();
562 while (try iter.next()) |entry| {
563 // We cannot just store `entry` as on Windows, we're re-using the name buffer
564 // which means we'll actually share the `name` pointer between entries!
565 const name = try allocator.dupe(u8, entry.name);
566 try entries.append(Dir.Entry{ .name = name, .kind = entry.kind });
567 }
568
569 try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
570 try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
571 try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
572 }
573}
574
575test "Dir.Iterator reset" {
576 var tmp_dir = tmpDir(.{ .iterate = true });
577 defer tmp_dir.cleanup();
578
579 // First, create a couple of entries to iterate over.
580 const file = try tmp_dir.dir.createFile("some_file", .{});
581 file.close();
582
583 try tmp_dir.dir.makeDir("some_dir");
584
585 var arena = ArenaAllocator.init(testing.allocator);
586 defer arena.deinit();
587 const allocator = arena.allocator();
588
589 // Create iterator.
590 var iter = tmp_dir.dir.iterate();
591
592 var i: u8 = 0;
593 while (i < 2) : (i += 1) {
594 var entries = std.array_list.Managed(Dir.Entry).init(allocator);
595
596 while (try iter.next()) |entry| {
597 // We cannot just store `entry` as on Windows, we're re-using the name buffer
598 // which means we'll actually share the `name` pointer between entries!
599 const name = try allocator.dupe(u8, entry.name);
600 try entries.append(.{ .name = name, .kind = entry.kind });
601 }
602
603 try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
604 try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
605 try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
606
607 iter.reset();
608 }
609}
610
611test "Dir.Iterator but dir is deleted during iteration" {
612 var tmp = std.testing.tmpDir(.{});
613 defer tmp.cleanup();
614
615 // Create directory and setup an iterator for it
616 var subdir = try tmp.dir.makeOpenPath("subdir", .{ .iterate = true });
617 defer subdir.close();
618
619 var iterator = subdir.iterate();
620
621 // Create something to iterate over within the subdir
622 try tmp.dir.makePath("subdir" ++ fs.path.sep_str ++ "b");
623
624 // Then, before iterating, delete the directory that we're iterating.
625 // This is a contrived reproduction, but this could happen outside of the program, in another thread, etc.
626 // If we get an error while trying to delete, we can skip this test (this will happen on platforms
627 // like Windows which will give FileBusy if the directory is currently open for iteration).
628 tmp.dir.deleteTree("subdir") catch return error.SkipZigTest;
629
630 // Now, when we try to iterate, the next call should return null immediately.
631 const entry = try iterator.next();
632 try std.testing.expect(entry == null);
633
634 // On Linux, we can opt-in to receiving a more specific error by calling `nextLinux`
635 if (native_os == .linux) {
636 try std.testing.expectError(error.DirNotFound, iterator.nextLinux());
637 }
638}
639
640fn entryEql(lhs: Dir.Entry, rhs: Dir.Entry) bool {
641 return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind;
642}
643
644fn contains(entries: *const std.array_list.Managed(Dir.Entry), el: Dir.Entry) bool {
645 for (entries.items) |entry| {
646 if (entryEql(entry, el)) return true;
647 }
648 return false;
649}
650
651test "Dir.realpath smoke test" {
652 if (!comptime std.os.isGetFdPathSupportedOnTarget(builtin.os)) return error.SkipZigTest;
653
654 try testWithAllSupportedPathTypes(struct {
655 fn impl(ctx: *TestContext) !void {
656 const allocator = ctx.arena.allocator();
657 const test_file_path = try ctx.transformPath("test_file");
658 const test_dir_path = try ctx.transformPath("test_dir");
659 var buf: [fs.max_path_bytes]u8 = undefined;
660
661 // FileNotFound if the path doesn't exist
662 try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(allocator, test_file_path));
663 try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_file_path, &buf));
664 try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(allocator, test_dir_path));
665 try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_dir_path, &buf));
666
667 // Now create the file and dir
668 try ctx.dir.writeFile(.{ .sub_path = test_file_path, .data = "" });
669 try ctx.dir.makeDir(test_dir_path);
670
671 const base_path = try ctx.transformPath(".");
672 const base_realpath = try ctx.dir.realpathAlloc(allocator, base_path);
673 const expected_file_path = try fs.path.join(
674 allocator,
675 &.{ base_realpath, "test_file" },
676 );
677 const expected_dir_path = try fs.path.join(
678 allocator,
679 &.{ base_realpath, "test_dir" },
680 );
681
682 // First, test non-alloc version
683 {
684 const file_path = try ctx.dir.realpath(test_file_path, &buf);
685 try testing.expectEqualStrings(expected_file_path, file_path);
686
687 const dir_path = try ctx.dir.realpath(test_dir_path, &buf);
688 try testing.expectEqualStrings(expected_dir_path, dir_path);
689 }
690
691 // Next, test alloc version
692 {
693 const file_path = try ctx.dir.realpathAlloc(allocator, test_file_path);
694 try testing.expectEqualStrings(expected_file_path, file_path);
695
696 const dir_path = try ctx.dir.realpathAlloc(allocator, test_dir_path);
697 try testing.expectEqualStrings(expected_dir_path, dir_path);
698 }
699 }
700 }.impl);
701}
702
703test "readFileAlloc" {
704 var tmp_dir = tmpDir(.{});
705 defer tmp_dir.cleanup();
706
707 var file = try tmp_dir.dir.createFile("test_file", .{ .read = true });
708 defer file.close();
709
710 const buf1 = try tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(1024));
711 defer testing.allocator.free(buf1);
712 try testing.expectEqualStrings("", buf1);
713
714 const write_buf: []const u8 = "this is a test.\nthis is a test.\nthis is a test.\nthis is a test.\n";
715 try file.writeAll(write_buf);
716
717 {
718 // max_bytes > file_size
719 const buf2 = try tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(1024));
720 defer testing.allocator.free(buf2);
721 try testing.expectEqualStrings(write_buf, buf2);
722 }
723
724 {
725 // max_bytes == file_size
726 try testing.expectError(
727 error.StreamTooLong,
728 tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(write_buf.len)),
729 );
730 }
731
732 {
733 // max_bytes == file_size + 1
734 const buf2 = try tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(write_buf.len + 1));
735 defer testing.allocator.free(buf2);
736 try testing.expectEqualStrings(write_buf, buf2);
737 }
738
739 // max_bytes < file_size
740 try testing.expectError(
741 error.StreamTooLong,
742 tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(write_buf.len - 1)),
743 );
744}
745
746test "Dir.statFile" {
747 try testWithAllSupportedPathTypes(struct {
748 fn impl(ctx: *TestContext) !void {
749 const test_file_name = try ctx.transformPath("test_file");
750
751 try testing.expectError(error.FileNotFound, ctx.dir.statFile(test_file_name));
752
753 try ctx.dir.writeFile(.{ .sub_path = test_file_name, .data = "" });
754
755 const stat = try ctx.dir.statFile(test_file_name);
756 try testing.expectEqual(File.Kind.file, stat.kind);
757 }
758 }.impl);
759}
760
761test "statFile on dangling symlink" {
762 try testWithAllSupportedPathTypes(struct {
763 fn impl(ctx: *TestContext) !void {
764 const symlink_name = try ctx.transformPath("dangling-symlink");
765 const symlink_target = "." ++ fs.path.sep_str ++ "doesnotexist";
766
767 try setupSymlink(ctx.dir, symlink_target, symlink_name, .{});
768
769 try std.testing.expectError(error.FileNotFound, ctx.dir.statFile(symlink_name));
770 }
771 }.impl);
772}
773
774test "directory operations on files" {
775 try testWithAllSupportedPathTypes(struct {
776 fn impl(ctx: *TestContext) !void {
777 const test_file_name = try ctx.transformPath("test_file");
778
779 var file = try ctx.dir.createFile(test_file_name, .{ .read = true });
780 file.close();
781
782 try testing.expectError(error.PathAlreadyExists, ctx.dir.makeDir(test_file_name));
783 try testing.expectError(error.NotDir, ctx.dir.openDir(test_file_name, .{}));
784 try testing.expectError(error.NotDir, ctx.dir.deleteDir(test_file_name));
785
786 if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
787 try testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(test_file_name));
788 try testing.expectError(error.NotDir, fs.deleteDirAbsolute(test_file_name));
789 }
790
791 // ensure the file still exists and is a file as a sanity check
792 file = try ctx.dir.openFile(test_file_name, .{});
793 const stat = try file.stat();
794 try testing.expectEqual(File.Kind.file, stat.kind);
795 file.close();
796 }
797 }.impl);
798}
799
800test "file operations on directories" {
801 // TODO: fix this test on FreeBSD. https://github.com/ziglang/zig/issues/1759
802 if (native_os == .freebsd) return error.SkipZigTest;
803
804 try testWithAllSupportedPathTypes(struct {
805 fn impl(ctx: *TestContext) !void {
806 const test_dir_name = try ctx.transformPath("test_dir");
807
808 try ctx.dir.makeDir(test_dir_name);
809
810 try testing.expectError(error.IsDir, ctx.dir.createFile(test_dir_name, .{}));
811 try testing.expectError(error.IsDir, ctx.dir.deleteFile(test_dir_name));
812 switch (native_os) {
813 .dragonfly, .netbsd => {
814 // no error when reading a directory. See https://github.com/ziglang/zig/issues/5732
815 const buf = try ctx.dir.readFileAlloc(test_dir_name, testing.allocator, .unlimited);
816 testing.allocator.free(buf);
817 },
818 .wasi => {
819 // WASI return EBADF, which gets mapped to NotOpenForReading.
820 // See https://github.com/bytecodealliance/wasmtime/issues/1935
821 try testing.expectError(error.NotOpenForReading, ctx.dir.readFileAlloc(test_dir_name, testing.allocator, .unlimited));
822 },
823 else => {
824 try testing.expectError(error.IsDir, ctx.dir.readFileAlloc(test_dir_name, testing.allocator, .unlimited));
825 },
826 }
827
828 if (native_os == .wasi and builtin.link_libc) {
829 // wasmtime unexpectedly succeeds here, see https://github.com/ziglang/zig/issues/20747
830 const handle = try ctx.dir.openFile(test_dir_name, .{ .mode = .read_write });
831 handle.close();
832 } else {
833 // Note: The `.mode = .read_write` is necessary to ensure the error occurs on all platforms.
834 // TODO: Add a read-only test as well, see https://github.com/ziglang/zig/issues/5732
835 try testing.expectError(error.IsDir, ctx.dir.openFile(test_dir_name, .{ .mode = .read_write }));
836 }
837
838 if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
839 try testing.expectError(error.IsDir, fs.createFileAbsolute(test_dir_name, .{}));
840 try testing.expectError(error.IsDir, fs.deleteFileAbsolute(test_dir_name));
841 }
842
843 // ensure the directory still exists as a sanity check
844 var dir = try ctx.dir.openDir(test_dir_name, .{});
845 dir.close();
846 }
847 }.impl);
848}
849
850test "makeOpenPath parent dirs do not exist" {
851 var tmp_dir = tmpDir(.{});
852 defer tmp_dir.cleanup();
853
854 var dir = try tmp_dir.dir.makeOpenPath("root_dir/parent_dir/some_dir", .{});
855 dir.close();
856
857 // double check that the full directory structure was created
858 var dir_verification = try tmp_dir.dir.openDir("root_dir/parent_dir/some_dir", .{});
859 dir_verification.close();
860}
861
862test "deleteDir" {
863 try testWithAllSupportedPathTypes(struct {
864 fn impl(ctx: *TestContext) !void {
865 const test_dir_path = try ctx.transformPath("test_dir");
866 const test_file_path = try ctx.transformPath("test_dir" ++ fs.path.sep_str ++ "test_file");
867
868 // deleting a non-existent directory
869 try testing.expectError(error.FileNotFound, ctx.dir.deleteDir(test_dir_path));
870
871 // deleting a non-empty directory
872 try ctx.dir.makeDir(test_dir_path);
873 try ctx.dir.writeFile(.{ .sub_path = test_file_path, .data = "" });
874 try testing.expectError(error.DirNotEmpty, ctx.dir.deleteDir(test_dir_path));
875
876 // deleting an empty directory
877 try ctx.dir.deleteFile(test_file_path);
878 try ctx.dir.deleteDir(test_dir_path);
879 }
880 }.impl);
881}
882
883test "Dir.rename files" {
884 try testWithAllSupportedPathTypes(struct {
885 fn impl(ctx: *TestContext) !void {
886 // Rename on Windows can hit intermittent AccessDenied errors
887 // when certain conditions are true about the host system.
888 // For now, skip this test when the path type is UNC to avoid them.
889 // See https://github.com/ziglang/zig/issues/17134
890 if (ctx.path_type == .unc) return;
891
892 const missing_file_path = try ctx.transformPath("missing_file_name");
893 const something_else_path = try ctx.transformPath("something_else");
894
895 try testing.expectError(error.FileNotFound, ctx.dir.rename(missing_file_path, something_else_path));
896
897 // Renaming files
898 const test_file_name = try ctx.transformPath("test_file");
899 const renamed_test_file_name = try ctx.transformPath("test_file_renamed");
900 var file = try ctx.dir.createFile(test_file_name, .{ .read = true });
901 file.close();
902 try ctx.dir.rename(test_file_name, renamed_test_file_name);
903
904 // Ensure the file was renamed
905 try testing.expectError(error.FileNotFound, ctx.dir.openFile(test_file_name, .{}));
906 file = try ctx.dir.openFile(renamed_test_file_name, .{});
907 file.close();
908
909 // Rename to self succeeds
910 try ctx.dir.rename(renamed_test_file_name, renamed_test_file_name);
911
912 // Rename to existing file succeeds
913 const existing_file_path = try ctx.transformPath("existing_file");
914 var existing_file = try ctx.dir.createFile(existing_file_path, .{ .read = true });
915 existing_file.close();
916 try ctx.dir.rename(renamed_test_file_name, existing_file_path);
917
918 try testing.expectError(error.FileNotFound, ctx.dir.openFile(renamed_test_file_name, .{}));
919 file = try ctx.dir.openFile(existing_file_path, .{});
920 file.close();
921 }
922 }.impl);
923}
924
925test "Dir.rename directories" {
926 try testWithAllSupportedPathTypes(struct {
927 fn impl(ctx: *TestContext) !void {
928 // Rename on Windows can hit intermittent AccessDenied errors
929 // when certain conditions are true about the host system.
930 // For now, skip this test when the path type is UNC to avoid them.
931 // See https://github.com/ziglang/zig/issues/17134
932 if (ctx.path_type == .unc) return;
933
934 const test_dir_path = try ctx.transformPath("test_dir");
935 const test_dir_renamed_path = try ctx.transformPath("test_dir_renamed");
936
937 // Renaming directories
938 try ctx.dir.makeDir(test_dir_path);
939 try ctx.dir.rename(test_dir_path, test_dir_renamed_path);
940
941 // Ensure the directory was renamed
942 try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_path, .{}));
943 var dir = try ctx.dir.openDir(test_dir_renamed_path, .{});
944
945 // Put a file in the directory
946 var file = try dir.createFile("test_file", .{ .read = true });
947 file.close();
948 dir.close();
949
950 const test_dir_renamed_again_path = try ctx.transformPath("test_dir_renamed_again");
951 try ctx.dir.rename(test_dir_renamed_path, test_dir_renamed_again_path);
952
953 // Ensure the directory was renamed and the file still exists in it
954 try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_renamed_path, .{}));
955 dir = try ctx.dir.openDir(test_dir_renamed_again_path, .{});
956 file = try dir.openFile("test_file", .{});
957 file.close();
958 dir.close();
959 }
960 }.impl);
961}
962
963test "Dir.rename directory onto empty dir" {
964 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
965 if (native_os == .windows) return error.SkipZigTest;
966
967 try testWithAllSupportedPathTypes(struct {
968 fn impl(ctx: *TestContext) !void {
969 const test_dir_path = try ctx.transformPath("test_dir");
970 const target_dir_path = try ctx.transformPath("target_dir_path");
971
972 try ctx.dir.makeDir(test_dir_path);
973 try ctx.dir.makeDir(target_dir_path);
974 try ctx.dir.rename(test_dir_path, target_dir_path);
975
976 // Ensure the directory was renamed
977 try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_path, .{}));
978 var dir = try ctx.dir.openDir(target_dir_path, .{});
979 dir.close();
980 }
981 }.impl);
982}
983
984test "Dir.rename directory onto non-empty dir" {
985 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
986 if (native_os == .windows) return error.SkipZigTest;
987
988 try testWithAllSupportedPathTypes(struct {
989 fn impl(ctx: *TestContext) !void {
990 const test_dir_path = try ctx.transformPath("test_dir");
991 const target_dir_path = try ctx.transformPath("target_dir_path");
992
993 try ctx.dir.makeDir(test_dir_path);
994
995 var target_dir = try ctx.dir.makeOpenPath(target_dir_path, .{});
996 var file = try target_dir.createFile("test_file", .{ .read = true });
997 file.close();
998 target_dir.close();
999
1000 // Rename should fail with PathAlreadyExists if target_dir is non-empty
1001 try testing.expectError(error.PathAlreadyExists, ctx.dir.rename(test_dir_path, target_dir_path));
1002
1003 // Ensure the directory was not renamed
1004 var dir = try ctx.dir.openDir(test_dir_path, .{});
1005 dir.close();
1006 }
1007 }.impl);
1008}
1009
1010test "Dir.rename file <-> dir" {
1011 // TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
1012 if (native_os == .windows) return error.SkipZigTest;
1013
1014 try testWithAllSupportedPathTypes(struct {
1015 fn impl(ctx: *TestContext) !void {
1016 const test_file_path = try ctx.transformPath("test_file");
1017 const test_dir_path = try ctx.transformPath("test_dir");
1018
1019 var file = try ctx.dir.createFile(test_file_path, .{ .read = true });
1020 file.close();
1021 try ctx.dir.makeDir(test_dir_path);
1022 try testing.expectError(error.IsDir, ctx.dir.rename(test_file_path, test_dir_path));
1023 try testing.expectError(error.NotDir, ctx.dir.rename(test_dir_path, test_file_path));
1024 }
1025 }.impl);
1026}
1027
1028test "rename" {
1029 var tmp_dir1 = tmpDir(.{});
1030 defer tmp_dir1.cleanup();
1031
1032 var tmp_dir2 = tmpDir(.{});
1033 defer tmp_dir2.cleanup();
1034
1035 // Renaming files
1036 const test_file_name = "test_file";
1037 const renamed_test_file_name = "test_file_renamed";
1038 var file = try tmp_dir1.dir.createFile(test_file_name, .{ .read = true });
1039 file.close();
1040 try fs.rename(tmp_dir1.dir, test_file_name, tmp_dir2.dir, renamed_test_file_name);
1041
1042 // ensure the file was renamed
1043 try testing.expectError(error.FileNotFound, tmp_dir1.dir.openFile(test_file_name, .{}));
1044 file = try tmp_dir2.dir.openFile(renamed_test_file_name, .{});
1045 file.close();
1046}
1047
1048test "renameAbsolute" {
1049 if (native_os == .wasi) return error.SkipZigTest;
1050
1051 var tmp_dir = tmpDir(.{});
1052 defer tmp_dir.cleanup();
1053
1054 // Get base abs path
1055 var arena = ArenaAllocator.init(testing.allocator);
1056 defer arena.deinit();
1057 const allocator = arena.allocator();
1058
1059 const base_path = try tmp_dir.dir.realpathAlloc(allocator, ".");
1060
1061 try testing.expectError(error.FileNotFound, fs.renameAbsolute(
1062 try fs.path.join(allocator, &.{ base_path, "missing_file_name" }),
1063 try fs.path.join(allocator, &.{ base_path, "something_else" }),
1064 ));
1065
1066 // Renaming files
1067 const test_file_name = "test_file";
1068 const renamed_test_file_name = "test_file_renamed";
1069 var file = try tmp_dir.dir.createFile(test_file_name, .{ .read = true });
1070 file.close();
1071 try fs.renameAbsolute(
1072 try fs.path.join(allocator, &.{ base_path, test_file_name }),
1073 try fs.path.join(allocator, &.{ base_path, renamed_test_file_name }),
1074 );
1075
1076 // ensure the file was renamed
1077 try testing.expectError(error.FileNotFound, tmp_dir.dir.openFile(test_file_name, .{}));
1078 file = try tmp_dir.dir.openFile(renamed_test_file_name, .{});
1079 const stat = try file.stat();
1080 try testing.expectEqual(File.Kind.file, stat.kind);
1081 file.close();
1082
1083 // Renaming directories
1084 const test_dir_name = "test_dir";
1085 const renamed_test_dir_name = "test_dir_renamed";
1086 try tmp_dir.dir.makeDir(test_dir_name);
1087 try fs.renameAbsolute(
1088 try fs.path.join(allocator, &.{ base_path, test_dir_name }),
1089 try fs.path.join(allocator, &.{ base_path, renamed_test_dir_name }),
1090 );
1091
1092 // ensure the directory was renamed
1093 try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir(test_dir_name, .{}));
1094 var dir = try tmp_dir.dir.openDir(renamed_test_dir_name, .{});
1095 dir.close();
1096}
1097
1098test "openSelfExe" {
1099 if (native_os == .wasi) return error.SkipZigTest;
1100
1101 const self_exe_file = try std.fs.openSelfExe(.{});
1102 self_exe_file.close();
1103}
1104
1105test "selfExePath" {
1106 if (native_os == .wasi) return error.SkipZigTest;
1107
1108 var buf: [fs.max_path_bytes]u8 = undefined;
1109 const buf_self_exe_path = try std.fs.selfExePath(&buf);
1110 const alloc_self_exe_path = try std.fs.selfExePathAlloc(testing.allocator);
1111 defer testing.allocator.free(alloc_self_exe_path);
1112 try testing.expectEqualSlices(u8, buf_self_exe_path, alloc_self_exe_path);
1113}
1114
1115test "deleteTree does not follow symlinks" {
1116 var tmp = tmpDir(.{});
1117 defer tmp.cleanup();
1118
1119 try tmp.dir.makePath("b");
1120 {
1121 var a = try tmp.dir.makeOpenPath("a", .{});
1122 defer a.close();
1123
1124 try setupSymlink(a, "../b", "b", .{ .is_directory = true });
1125 }
1126
1127 try tmp.dir.deleteTree("a");
1128
1129 try testing.expectError(error.FileNotFound, tmp.dir.access("a", .{}));
1130 try tmp.dir.access("b", .{});
1131}
1132
1133test "deleteTree on a symlink" {
1134 var tmp = tmpDir(.{});
1135 defer tmp.cleanup();
1136
1137 // Symlink to a file
1138 try tmp.dir.writeFile(.{ .sub_path = "file", .data = "" });
1139 try setupSymlink(tmp.dir, "file", "filelink", .{});
1140
1141 try tmp.dir.deleteTree("filelink");
1142 try testing.expectError(error.FileNotFound, tmp.dir.access("filelink", .{}));
1143 try tmp.dir.access("file", .{});
1144
1145 // Symlink to a directory
1146 try tmp.dir.makePath("dir");
1147 try setupSymlink(tmp.dir, "dir", "dirlink", .{ .is_directory = true });
1148
1149 try tmp.dir.deleteTree("dirlink");
1150 try testing.expectError(error.FileNotFound, tmp.dir.access("dirlink", .{}));
1151 try tmp.dir.access("dir", .{});
1152}
1153
1154test "makePath, put some files in it, deleteTree" {
1155 try testWithAllSupportedPathTypes(struct {
1156 fn impl(ctx: *TestContext) !void {
1157 const allocator = ctx.arena.allocator();
1158 const dir_path = try ctx.transformPath("os_test_tmp");
1159
1160 try ctx.dir.makePath(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c" }));
1161 try ctx.dir.writeFile(.{
1162 .sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c", "file.txt" }),
1163 .data = "nonsense",
1164 });
1165 try ctx.dir.writeFile(.{
1166 .sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "file2.txt" }),
1167 .data = "blah",
1168 });
1169
1170 try ctx.dir.deleteTree(dir_path);
1171 try testing.expectError(error.FileNotFound, ctx.dir.openDir(dir_path, .{}));
1172 }
1173 }.impl);
1174}
1175
1176test "makePath, put some files in it, deleteTreeMinStackSize" {
1177 try testWithAllSupportedPathTypes(struct {
1178 fn impl(ctx: *TestContext) !void {
1179 const allocator = ctx.arena.allocator();
1180 const dir_path = try ctx.transformPath("os_test_tmp");
1181
1182 try ctx.dir.makePath(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c" }));
1183 try ctx.dir.writeFile(.{
1184 .sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c", "file.txt" }),
1185 .data = "nonsense",
1186 });
1187 try ctx.dir.writeFile(.{
1188 .sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "file2.txt" }),
1189 .data = "blah",
1190 });
1191
1192 try ctx.dir.deleteTreeMinStackSize(dir_path);
1193 try testing.expectError(error.FileNotFound, ctx.dir.openDir(dir_path, .{}));
1194 }
1195 }.impl);
1196}
1197
1198test "makePath in a directory that no longer exists" {
1199 if (native_os == .windows) return error.SkipZigTest; // Windows returns FileBusy if attempting to remove an open dir
1200
1201 var tmp = tmpDir(.{});
1202 defer tmp.cleanup();
1203 try tmp.parent_dir.deleteTree(&tmp.sub_path);
1204
1205 try testing.expectError(error.FileNotFound, tmp.dir.makePath("sub-path"));
1206}
1207
1208test "makePath but sub_path contains pre-existing file" {
1209 var tmp = tmpDir(.{});
1210 defer tmp.cleanup();
1211
1212 try tmp.dir.makeDir("foo");
1213 try tmp.dir.writeFile(.{ .sub_path = "foo/bar", .data = "" });
1214
1215 try testing.expectError(error.NotDir, tmp.dir.makePath("foo/bar/baz"));
1216}
1217
1218fn expectDir(dir: Dir, path: []const u8) !void {
1219 var d = try dir.openDir(path, .{});
1220 d.close();
1221}
1222
1223test "makepath existing directories" {
1224 var tmp = tmpDir(.{});
1225 defer tmp.cleanup();
1226
1227 try tmp.dir.makeDir("A");
1228 var tmpA = try tmp.dir.openDir("A", .{});
1229 defer tmpA.close();
1230 try tmpA.makeDir("B");
1231
1232 const testPath = "A" ++ fs.path.sep_str ++ "B" ++ fs.path.sep_str ++ "C";
1233 try tmp.dir.makePath(testPath);
1234
1235 try expectDir(tmp.dir, testPath);
1236}
1237
1238test "makepath through existing valid symlink" {
1239 var tmp = tmpDir(.{});
1240 defer tmp.cleanup();
1241
1242 try tmp.dir.makeDir("realfolder");
1243 try setupSymlink(tmp.dir, "." ++ fs.path.sep_str ++ "realfolder", "working-symlink", .{});
1244
1245 try tmp.dir.makePath("working-symlink" ++ fs.path.sep_str ++ "in-realfolder");
1246
1247 try expectDir(tmp.dir, "realfolder" ++ fs.path.sep_str ++ "in-realfolder");
1248}
1249
1250test "makepath relative walks" {
1251 var tmp = tmpDir(.{});
1252 defer tmp.cleanup();
1253
1254 const relPath = try fs.path.join(testing.allocator, &.{
1255 "first", "..", "second", "..", "third", "..", "first", "A", "..", "B", "..", "C",
1256 });
1257 defer testing.allocator.free(relPath);
1258
1259 try tmp.dir.makePath(relPath);
1260
1261 // How .. is handled is different on Windows than non-Windows
1262 switch (native_os) {
1263 .windows => {
1264 // On Windows, .. is resolved before passing the path to NtCreateFile,
1265 // meaning everything except `first/C` drops out.
1266 try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "C");
1267 try testing.expectError(error.FileNotFound, tmp.dir.access("second", .{}));
1268 try testing.expectError(error.FileNotFound, tmp.dir.access("third", .{}));
1269 },
1270 else => {
1271 try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "A");
1272 try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "B");
1273 try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "C");
1274 try expectDir(tmp.dir, "second");
1275 try expectDir(tmp.dir, "third");
1276 },
1277 }
1278}
1279
1280test "makepath ignores '.'" {
1281 var tmp = tmpDir(.{});
1282 defer tmp.cleanup();
1283
1284 // Path to create, with "." elements:
1285 const dotPath = try fs.path.join(testing.allocator, &.{
1286 "first", ".", "second", ".", "third",
1287 });
1288 defer testing.allocator.free(dotPath);
1289
1290 // Path to expect to find:
1291 const expectedPath = try fs.path.join(testing.allocator, &.{
1292 "first", "second", "third",
1293 });
1294 defer testing.allocator.free(expectedPath);
1295
1296 try tmp.dir.makePath(dotPath);
1297
1298 try expectDir(tmp.dir, expectedPath);
1299}
1300
1301fn testFilenameLimits(iterable_dir: Dir, maxed_filename: []const u8) !void {
1302 // setup, create a dir and a nested file both with maxed filenames, and walk the dir
1303 {
1304 var maxed_dir = try iterable_dir.makeOpenPath(maxed_filename, .{});
1305 defer maxed_dir.close();
1306
1307 try maxed_dir.writeFile(.{ .sub_path = maxed_filename, .data = "" });
1308
1309 var walker = try iterable_dir.walk(testing.allocator);
1310 defer walker.deinit();
1311
1312 var count: usize = 0;
1313 while (try walker.next()) |entry| {
1314 try testing.expectEqualStrings(maxed_filename, entry.basename);
1315 count += 1;
1316 }
1317 try testing.expectEqual(@as(usize, 2), count);
1318 }
1319
1320 // ensure that we can delete the tree
1321 try iterable_dir.deleteTree(maxed_filename);
1322}
1323
1324test "max file name component lengths" {
1325 var tmp = tmpDir(.{ .iterate = true });
1326 defer tmp.cleanup();
1327
1328 if (native_os == .windows) {
1329 // U+FFFF is the character with the largest code point that is encoded as a single
1330 // UTF-16 code unit, so Windows allows for NAME_MAX of them.
1331 const maxed_windows_filename = ("\u{FFFF}".*) ** windows.NAME_MAX;
1332 try testFilenameLimits(tmp.dir, &maxed_windows_filename);
1333 } else if (native_os == .wasi) {
1334 // On WASI, the maxed filename depends on the host OS, so in order for this test to
1335 // work on any host, we need to use a length that will work for all platforms
1336 // (i.e. the minimum max_name_bytes of all supported platforms).
1337 const maxed_wasi_filename = [_]u8{'1'} ** 255;
1338 try testFilenameLimits(tmp.dir, &maxed_wasi_filename);
1339 } else {
1340 const maxed_ascii_filename = [_]u8{'1'} ** std.fs.max_name_bytes;
1341 try testFilenameLimits(tmp.dir, &maxed_ascii_filename);
1342 }
1343}
1344
1345test "writev, readv" {
1346 const io = testing.io;
1347
1348 var tmp = tmpDir(.{});
1349 defer tmp.cleanup();
1350
1351 const line1 = "line1\n";
1352 const line2 = "line2\n";
1353
1354 var buf1: [line1.len]u8 = undefined;
1355 var buf2: [line2.len]u8 = undefined;
1356 var write_vecs: [2][]const u8 = .{ line1, line2 };
1357 var read_vecs: [2][]u8 = .{ &buf2, &buf1 };
1358
1359 var src_file = try tmp.dir.createFile("test.txt", .{ .read = true });
1360 defer src_file.close();
1361
1362 var writer = src_file.writerStreaming(&.{});
1363
1364 try writer.interface.writeVecAll(&write_vecs);
1365 try writer.interface.flush();
1366 try testing.expectEqual(@as(u64, line1.len + line2.len), try src_file.getEndPos());
1367
1368 var reader = writer.moveToReader(io);
1369 try reader.seekTo(0);
1370 try reader.interface.readVecAll(&read_vecs);
1371 try testing.expectEqualStrings(&buf1, "line2\n");
1372 try testing.expectEqualStrings(&buf2, "line1\n");
1373 try testing.expectError(error.EndOfStream, reader.interface.readSliceAll(&buf1));
1374}
1375
1376test "pwritev, preadv" {
1377 const io = testing.io;
1378
1379 var tmp = tmpDir(.{});
1380 defer tmp.cleanup();
1381
1382 const line1 = "line1\n";
1383 const line2 = "line2\n";
1384 var lines: [2][]const u8 = .{ line1, line2 };
1385 var buf1: [line1.len]u8 = undefined;
1386 var buf2: [line2.len]u8 = undefined;
1387 var read_vecs: [2][]u8 = .{ &buf2, &buf1 };
1388
1389 var src_file = try tmp.dir.createFile("test.txt", .{ .read = true });
1390 defer src_file.close();
1391
1392 var writer = src_file.writer(&.{});
1393
1394 try writer.seekTo(16);
1395 try writer.interface.writeVecAll(&lines);
1396 try writer.interface.flush();
1397 try testing.expectEqual(@as(u64, 16 + line1.len + line2.len), try src_file.getEndPos());
1398
1399 var reader = writer.moveToReader(io);
1400 try reader.seekTo(16);
1401 try reader.interface.readVecAll(&read_vecs);
1402 try testing.expectEqualStrings(&buf1, "line2\n");
1403 try testing.expectEqualStrings(&buf2, "line1\n");
1404 try testing.expectError(error.EndOfStream, reader.interface.readSliceAll(&buf1));
1405}
1406
1407test "setEndPos" {
1408 // https://github.com/ziglang/zig/issues/20747 (open fd does not have write permission)
1409 if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest;
1410 if (builtin.cpu.arch.isMIPS64() and (builtin.abi == .gnuabin32 or builtin.abi == .muslabin32)) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/23806
1411
1412 const io = testing.io;
1413
1414 var tmp = tmpDir(.{});
1415 defer tmp.cleanup();
1416
1417 const file_name = "afile.txt";
1418 try tmp.dir.writeFile(.{ .sub_path = file_name, .data = "ninebytes" });
1419 const f = try tmp.dir.openFile(file_name, .{ .mode = .read_write });
1420 defer f.close();
1421
1422 const initial_size = try f.getEndPos();
1423 var buffer: [32]u8 = undefined;
1424 var reader = f.reader(io, &.{});
1425
1426 {
1427 try f.setEndPos(initial_size);
1428 try testing.expectEqual(initial_size, try f.getEndPos());
1429 try reader.seekTo(0);
1430 try testing.expectEqual(initial_size, try reader.interface.readSliceShort(&buffer));
1431 try testing.expectEqualStrings("ninebytes", buffer[0..@intCast(initial_size)]);
1432 }
1433
1434 {
1435 const larger = initial_size + 4;
1436 try f.setEndPos(larger);
1437 try testing.expectEqual(larger, try f.getEndPos());
1438 try reader.seekTo(0);
1439 try testing.expectEqual(larger, try reader.interface.readSliceShort(&buffer));
1440 try testing.expectEqualStrings("ninebytes\x00\x00\x00\x00", buffer[0..@intCast(larger)]);
1441 }
1442
1443 {
1444 const smaller = initial_size - 5;
1445 try f.setEndPos(smaller);
1446 try testing.expectEqual(smaller, try f.getEndPos());
1447 try reader.seekTo(0);
1448 try testing.expectEqual(smaller, try reader.interface.readSliceShort(&buffer));
1449 try testing.expectEqualStrings("nine", buffer[0..@intCast(smaller)]);
1450 }
1451
1452 try f.setEndPos(0);
1453 try testing.expectEqual(0, try f.getEndPos());
1454 try reader.seekTo(0);
1455 try testing.expectEqual(0, try reader.interface.readSliceShort(&buffer));
1456}
1457
1458test "access file" {
1459 try testWithAllSupportedPathTypes(struct {
1460 fn impl(ctx: *TestContext) !void {
1461 const dir_path = try ctx.transformPath("os_test_tmp");
1462 const file_path = try ctx.transformPath("os_test_tmp" ++ fs.path.sep_str ++ "file.txt");
1463
1464 try ctx.dir.makePath(dir_path);
1465 try testing.expectError(error.FileNotFound, ctx.dir.access(file_path, .{}));
1466
1467 try ctx.dir.writeFile(.{ .sub_path = file_path, .data = "" });
1468 try ctx.dir.access(file_path, .{});
1469 try ctx.dir.deleteTree(dir_path);
1470 }
1471 }.impl);
1472}
1473
1474test "sendfile" {
1475 const io = testing.io;
1476
1477 var tmp = tmpDir(.{});
1478 defer tmp.cleanup();
1479
1480 try tmp.dir.makePath("os_test_tmp");
1481
1482 var dir = try tmp.dir.openDir("os_test_tmp", .{});
1483 defer dir.close();
1484
1485 const line1 = "line1\n";
1486 const line2 = "second line\n";
1487 var vecs = [_][]const u8{ line1, line2 };
1488
1489 var src_file = try dir.createFile("sendfile1.txt", .{ .read = true });
1490 defer src_file.close();
1491 {
1492 var fw = src_file.writer(&.{});
1493 try fw.interface.writeVecAll(&vecs);
1494 }
1495
1496 var dest_file = try dir.createFile("sendfile2.txt", .{ .read = true });
1497 defer dest_file.close();
1498
1499 const header1 = "header1\n";
1500 const header2 = "second header\n";
1501 const trailer1 = "trailer1\n";
1502 const trailer2 = "second trailer\n";
1503 var headers: [2][]const u8 = .{ header1, header2 };
1504 var trailers: [2][]const u8 = .{ trailer1, trailer2 };
1505
1506 var written_buf: [100]u8 = undefined;
1507 var file_reader = src_file.reader(io, &.{});
1508 var fallback_buffer: [50]u8 = undefined;
1509 var file_writer = dest_file.writer(&fallback_buffer);
1510 try file_writer.interface.writeVecAll(&headers);
1511 try file_reader.seekTo(1);
1512 try testing.expectEqual(10, try file_writer.interface.sendFileAll(&file_reader, .limited(10)));
1513 try file_writer.interface.writeVecAll(&trailers);
1514 try file_writer.interface.flush();
1515 var fr = file_writer.moveToReader(io);
1516 try fr.seekTo(0);
1517 const amt = try fr.interface.readSliceShort(&written_buf);
1518 try testing.expectEqualStrings("header1\nsecond header\nine1\nsecontrailer1\nsecond trailer\n", written_buf[0..amt]);
1519}
1520
1521test "sendfile with buffered data" {
1522 const io = testing.io;
1523
1524 var tmp = tmpDir(.{});
1525 defer tmp.cleanup();
1526
1527 try tmp.dir.makePath("os_test_tmp");
1528
1529 var dir = try tmp.dir.openDir("os_test_tmp", .{});
1530 defer dir.close();
1531
1532 var src_file = try dir.createFile("sendfile1.txt", .{ .read = true });
1533 defer src_file.close();
1534
1535 try src_file.writeAll("AAAABBBB");
1536
1537 var dest_file = try dir.createFile("sendfile2.txt", .{ .read = true });
1538 defer dest_file.close();
1539
1540 var src_buffer: [32]u8 = undefined;
1541 var file_reader = src_file.reader(io, &src_buffer);
1542
1543 try file_reader.seekTo(0);
1544 try file_reader.interface.fill(8);
1545
1546 var fallback_buffer: [32]u8 = undefined;
1547 var file_writer = dest_file.writer(&fallback_buffer);
1548
1549 try std.testing.expectEqual(4, try file_writer.interface.sendFileAll(&file_reader, .limited(4)));
1550
1551 var written_buf: [8]u8 = undefined;
1552 var fr = file_writer.moveToReader(io);
1553 try fr.seekTo(0);
1554 const amt = try fr.interface.readSliceShort(&written_buf);
1555
1556 try std.testing.expectEqual(4, amt);
1557 try std.testing.expectEqualSlices(u8, "AAAA", written_buf[0..amt]);
1558}
1559
1560test "copyFile" {
1561 try testWithAllSupportedPathTypes(struct {
1562 fn impl(ctx: *TestContext) !void {
1563 const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP";
1564 const src_file = try ctx.transformPath("tmp_test_copy_file.txt");
1565 const dest_file = try ctx.transformPath("tmp_test_copy_file2.txt");
1566 const dest_file2 = try ctx.transformPath("tmp_test_copy_file3.txt");
1567
1568 try ctx.dir.writeFile(.{ .sub_path = src_file, .data = data });
1569 defer ctx.dir.deleteFile(src_file) catch {};
1570
1571 try ctx.dir.copyFile(src_file, ctx.dir, dest_file, .{});
1572 defer ctx.dir.deleteFile(dest_file) catch {};
1573
1574 try ctx.dir.copyFile(src_file, ctx.dir, dest_file2, .{ .override_mode = File.default_mode });
1575 defer ctx.dir.deleteFile(dest_file2) catch {};
1576
1577 try expectFileContents(ctx.dir, dest_file, data);
1578 try expectFileContents(ctx.dir, dest_file2, data);
1579 }
1580 }.impl);
1581}
1582
1583fn expectFileContents(dir: Dir, file_path: []const u8, data: []const u8) !void {
1584 const contents = try dir.readFileAlloc(file_path, testing.allocator, .limited(1000));
1585 defer testing.allocator.free(contents);
1586
1587 try testing.expectEqualSlices(u8, data, contents);
1588}
1589
1590test "AtomicFile" {
1591 try testWithAllSupportedPathTypes(struct {
1592 fn impl(ctx: *TestContext) !void {
1593 const allocator = ctx.arena.allocator();
1594 const test_out_file = try ctx.transformPath("tmp_atomic_file_test_dest.txt");
1595 const test_content =
1596 \\ hello!
1597 \\ this is a test file
1598 ;
1599
1600 {
1601 var buffer: [100]u8 = undefined;
1602 var af = try ctx.dir.atomicFile(test_out_file, .{ .write_buffer = &buffer });
1603 defer af.deinit();
1604 try af.file_writer.interface.writeAll(test_content);
1605 try af.finish();
1606 }
1607 const content = try ctx.dir.readFileAlloc(test_out_file, allocator, .limited(9999));
1608 try testing.expectEqualStrings(test_content, content);
1609
1610 try ctx.dir.deleteFile(test_out_file);
1611 }
1612 }.impl);
1613}
1614
1615test "open file with exclusive nonblocking lock twice" {
1616 if (native_os == .wasi) return error.SkipZigTest;
1617
1618 try testWithAllSupportedPathTypes(struct {
1619 fn impl(ctx: *TestContext) !void {
1620 const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
1621
1622 const file1 = try ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1623 defer file1.close();
1624
1625 const file2 = ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1626 try testing.expectError(error.WouldBlock, file2);
1627 }
1628 }.impl);
1629}
1630
1631test "open file with shared and exclusive nonblocking lock" {
1632 if (native_os == .wasi) return error.SkipZigTest;
1633
1634 try testWithAllSupportedPathTypes(struct {
1635 fn impl(ctx: *TestContext) !void {
1636 const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
1637
1638 const file1 = try ctx.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });
1639 defer file1.close();
1640
1641 const file2 = ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1642 try testing.expectError(error.WouldBlock, file2);
1643 }
1644 }.impl);
1645}
1646
1647test "open file with exclusive and shared nonblocking lock" {
1648 if (native_os == .wasi) return error.SkipZigTest;
1649
1650 try testWithAllSupportedPathTypes(struct {
1651 fn impl(ctx: *TestContext) !void {
1652 const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
1653
1654 const file1 = try ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
1655 defer file1.close();
1656
1657 const file2 = ctx.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });
1658 try testing.expectError(error.WouldBlock, file2);
1659 }
1660 }.impl);
1661}
1662
1663test "open file with exclusive lock twice, make sure second lock waits" {
1664 if (builtin.single_threaded) return error.SkipZigTest;
1665
1666 try testWithAllSupportedPathTypes(struct {
1667 fn impl(ctx: *TestContext) !void {
1668 const filename = try ctx.transformPath("file_lock_test.txt");
1669
1670 const file = try ctx.dir.createFile(filename, .{ .lock = .exclusive });
1671 errdefer file.close();
1672
1673 const S = struct {
1674 fn checkFn(dir: *fs.Dir, path: []const u8, started: *std.Thread.ResetEvent, locked: *std.Thread.ResetEvent) !void {
1675 started.set();
1676 const file1 = try dir.createFile(path, .{ .lock = .exclusive });
1677
1678 locked.set();
1679 file1.close();
1680 }
1681 };
1682
1683 var started: std.Thread.ResetEvent = .unset;
1684 var locked: std.Thread.ResetEvent = .unset;
1685
1686 const t = try std.Thread.spawn(.{}, S.checkFn, .{
1687 &ctx.dir,
1688 filename,
1689 &started,
1690 &locked,
1691 });
1692 defer t.join();
1693
1694 // Wait for the spawned thread to start trying to acquire the exclusive file lock.
1695 // Then wait a bit to make sure that can't acquire it since we currently hold the file lock.
1696 started.wait();
1697 try testing.expectError(error.Timeout, locked.timedWait(10 * std.time.ns_per_ms));
1698
1699 // Release the file lock which should unlock the thread to lock it and set the locked event.
1700 file.close();
1701 locked.wait();
1702 }
1703 }.impl);
1704}
1705
1706test "open file with exclusive nonblocking lock twice (absolute paths)" {
1707 if (native_os == .wasi) return error.SkipZigTest;
1708
1709 var random_bytes: [12]u8 = undefined;
1710 std.crypto.random.bytes(&random_bytes);
1711
1712 var random_b64: [fs.base64_encoder.calcSize(random_bytes.len)]u8 = undefined;
1713 _ = fs.base64_encoder.encode(&random_b64, &random_bytes);
1714
1715 const sub_path = random_b64 ++ "-zig-test-absolute-paths.txt";
1716
1717 const gpa = testing.allocator;
1718
1719 const cwd = try std.process.getCwdAlloc(gpa);
1720 defer gpa.free(cwd);
1721
1722 const filename = try fs.path.resolve(gpa, &.{ cwd, sub_path });
1723 defer gpa.free(filename);
1724
1725 defer fs.deleteFileAbsolute(filename) catch {}; // createFileAbsolute can leave files on failures
1726 const file1 = try fs.createFileAbsolute(filename, .{
1727 .lock = .exclusive,
1728 .lock_nonblocking = true,
1729 });
1730
1731 const file2 = fs.createFileAbsolute(filename, .{
1732 .lock = .exclusive,
1733 .lock_nonblocking = true,
1734 });
1735 file1.close();
1736 try testing.expectError(error.WouldBlock, file2);
1737}
1738
1739test "read from locked file" {
1740 try testWithAllSupportedPathTypes(struct {
1741 fn impl(ctx: *TestContext) !void {
1742 const filename = try ctx.transformPath("read_lock_file_test.txt");
1743
1744 {
1745 const f = try ctx.dir.createFile(filename, .{ .read = true });
1746 defer f.close();
1747 var buffer: [1]u8 = undefined;
1748 _ = try f.read(&buffer);
1749 }
1750 {
1751 const f = try ctx.dir.createFile(filename, .{
1752 .read = true,
1753 .lock = .exclusive,
1754 });
1755 defer f.close();
1756 const f2 = try ctx.dir.openFile(filename, .{});
1757 defer f2.close();
1758 var buffer: [1]u8 = undefined;
1759 if (builtin.os.tag == .windows) {
1760 try std.testing.expectError(error.LockViolation, f2.read(&buffer));
1761 } else {
1762 try std.testing.expectEqual(0, f2.read(&buffer));
1763 }
1764 }
1765 }
1766 }.impl);
1767}
1768
1769test "walker" {
1770 var tmp = tmpDir(.{ .iterate = true });
1771 defer tmp.cleanup();
1772
1773 // iteration order of walker is undefined, so need lookup maps to check against
1774
1775 const expected_paths = std.StaticStringMap(usize).initComptime(.{
1776 .{ "dir1", 1 },
1777 .{ "dir2", 1 },
1778 .{ "dir3", 1 },
1779 .{ "dir4", 1 },
1780 .{ "dir3" ++ fs.path.sep_str ++ "sub1", 2 },
1781 .{ "dir3" ++ fs.path.sep_str ++ "sub2", 2 },
1782 .{ "dir3" ++ fs.path.sep_str ++ "sub2" ++ fs.path.sep_str ++ "subsub1", 3 },
1783 });
1784
1785 const expected_basenames = std.StaticStringMap(void).initComptime(.{
1786 .{"dir1"},
1787 .{"dir2"},
1788 .{"dir3"},
1789 .{"dir4"},
1790 .{"sub1"},
1791 .{"sub2"},
1792 .{"subsub1"},
1793 });
1794
1795 for (expected_paths.keys()) |key| {
1796 try tmp.dir.makePath(key);
1797 }
1798
1799 var walker = try tmp.dir.walk(testing.allocator);
1800 defer walker.deinit();
1801
1802 var num_walked: usize = 0;
1803 while (try walker.next()) |entry| {
1804 testing.expect(expected_basenames.has(entry.basename)) catch |err| {
1805 std.debug.print("found unexpected basename: {f}\n", .{std.ascii.hexEscape(entry.basename, .lower)});
1806 return err;
1807 };
1808 testing.expect(expected_paths.has(entry.path)) catch |err| {
1809 std.debug.print("found unexpected path: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
1810 return err;
1811 };
1812 testing.expectEqual(expected_paths.get(entry.path).?, entry.depth()) catch |err| {
1813 std.debug.print("path reported unexpected depth: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
1814 return err;
1815 };
1816 // make sure that the entry.dir is the containing dir
1817 var entry_dir = try entry.dir.openDir(entry.basename, .{});
1818 defer entry_dir.close();
1819 num_walked += 1;
1820 }
1821 try testing.expectEqual(expected_paths.kvs.len, num_walked);
1822}
1823
1824test "selective walker, skip entries that start with ." {
1825 var tmp = tmpDir(.{ .iterate = true });
1826 defer tmp.cleanup();
1827
1828 const paths_to_create: []const []const u8 = &.{
1829 "dir1/foo/.git/ignored",
1830 ".hidden/bar",
1831 "a/b/c",
1832 "a/baz",
1833 };
1834
1835 // iteration order of walker is undefined, so need lookup maps to check against
1836
1837 const expected_paths = std.StaticStringMap(usize).initComptime(.{
1838 .{ "dir1", 1 },
1839 .{ "dir1" ++ fs.path.sep_str ++ "foo", 2 },
1840 .{ "a", 1 },
1841 .{ "a" ++ fs.path.sep_str ++ "b", 2 },
1842 .{ "a" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c", 3 },
1843 .{ "a" ++ fs.path.sep_str ++ "baz", 2 },
1844 });
1845
1846 const expected_basenames = std.StaticStringMap(void).initComptime(.{
1847 .{"dir1"},
1848 .{"foo"},
1849 .{"a"},
1850 .{"b"},
1851 .{"c"},
1852 .{"baz"},
1853 });
1854
1855 for (paths_to_create) |path| {
1856 try tmp.dir.makePath(path);
1857 }
1858
1859 var walker = try tmp.dir.walkSelectively(testing.allocator);
1860 defer walker.deinit();
1861
1862 var num_walked: usize = 0;
1863 while (try walker.next()) |entry| {
1864 if (entry.basename[0] == '.') continue;
1865 if (entry.kind == .directory) {
1866 try walker.enter(entry);
1867 }
1868
1869 testing.expect(expected_basenames.has(entry.basename)) catch |err| {
1870 std.debug.print("found unexpected basename: {f}\n", .{std.ascii.hexEscape(entry.basename, .lower)});
1871 return err;
1872 };
1873 testing.expect(expected_paths.has(entry.path)) catch |err| {
1874 std.debug.print("found unexpected path: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
1875 return err;
1876 };
1877 testing.expectEqual(expected_paths.get(entry.path).?, entry.depth()) catch |err| {
1878 std.debug.print("path reported unexpected depth: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
1879 return err;
1880 };
1881
1882 // make sure that the entry.dir is the containing dir
1883 var entry_dir = try entry.dir.openDir(entry.basename, .{});
1884 defer entry_dir.close();
1885 num_walked += 1;
1886 }
1887 try testing.expectEqual(expected_paths.kvs.len, num_walked);
1888}
1889
1890test "walker without fully iterating" {
1891 var tmp = tmpDir(.{ .iterate = true });
1892 defer tmp.cleanup();
1893
1894 var walker = try tmp.dir.walk(testing.allocator);
1895 defer walker.deinit();
1896
1897 // Create 2 directories inside the tmp directory, but then only iterate once before breaking.
1898 // This ensures that walker doesn't try to close the initial directory when not fully iterating.
1899
1900 try tmp.dir.makePath("a");
1901 try tmp.dir.makePath("b");
1902
1903 var num_walked: usize = 0;
1904 while (try walker.next()) |_| {
1905 num_walked += 1;
1906 break;
1907 }
1908 try testing.expectEqual(@as(usize, 1), num_walked);
1909}
1910
1911test "'.' and '..' in fs.Dir functions" {
1912 if (native_os == .windows and builtin.cpu.arch == .aarch64) {
1913 // https://github.com/ziglang/zig/issues/17134
1914 return error.SkipZigTest;
1915 }
1916
1917 try testWithAllSupportedPathTypes(struct {
1918 fn impl(ctx: *TestContext) !void {
1919 const io = ctx.io;
1920 const subdir_path = try ctx.transformPath("./subdir");
1921 const file_path = try ctx.transformPath("./subdir/../file");
1922 const copy_path = try ctx.transformPath("./subdir/../copy");
1923 const rename_path = try ctx.transformPath("./subdir/../rename");
1924 const update_path = try ctx.transformPath("./subdir/../update");
1925
1926 try ctx.dir.makeDir(subdir_path);
1927 try ctx.dir.access(subdir_path, .{});
1928 var created_subdir = try ctx.dir.openDir(subdir_path, .{});
1929 created_subdir.close();
1930
1931 const created_file = try ctx.dir.createFile(file_path, .{});
1932 created_file.close();
1933 try ctx.dir.access(file_path, .{});
1934
1935 try ctx.dir.copyFile(file_path, ctx.dir, copy_path, .{});
1936 try ctx.dir.rename(copy_path, rename_path);
1937 const renamed_file = try ctx.dir.openFile(rename_path, .{});
1938 renamed_file.close();
1939 try ctx.dir.deleteFile(rename_path);
1940
1941 try ctx.dir.writeFile(.{ .sub_path = update_path, .data = "something" });
1942 var dir = ctx.dir.adaptToNewApi();
1943 const prev_status = try dir.updateFile(io, file_path, dir, update_path, .{});
1944 try testing.expectEqual(Io.Dir.PrevStatus.stale, prev_status);
1945
1946 try ctx.dir.deleteDir(subdir_path);
1947 }
1948 }.impl);
1949}
1950
1951test "'.' and '..' in absolute functions" {
1952 if (native_os == .wasi) return error.SkipZigTest;
1953
1954 var tmp = tmpDir(.{});
1955 defer tmp.cleanup();
1956
1957 var arena = ArenaAllocator.init(testing.allocator);
1958 defer arena.deinit();
1959 const allocator = arena.allocator();
1960
1961 const base_path = try tmp.dir.realpathAlloc(allocator, ".");
1962
1963 const subdir_path = try fs.path.join(allocator, &.{ base_path, "./subdir" });
1964 try fs.makeDirAbsolute(subdir_path);
1965 try fs.accessAbsolute(subdir_path, .{});
1966 var created_subdir = try fs.openDirAbsolute(subdir_path, .{});
1967 created_subdir.close();
1968
1969 const created_file_path = try fs.path.join(allocator, &.{ subdir_path, "../file" });
1970 const created_file = try fs.createFileAbsolute(created_file_path, .{});
1971 created_file.close();
1972 try fs.accessAbsolute(created_file_path, .{});
1973
1974 const copied_file_path = try fs.path.join(allocator, &.{ subdir_path, "../copy" });
1975 try fs.copyFileAbsolute(created_file_path, copied_file_path, .{});
1976 const renamed_file_path = try fs.path.join(allocator, &.{ subdir_path, "../rename" });
1977 try fs.renameAbsolute(copied_file_path, renamed_file_path);
1978 const renamed_file = try fs.openFileAbsolute(renamed_file_path, .{});
1979 renamed_file.close();
1980 try fs.deleteFileAbsolute(renamed_file_path);
1981
1982 try fs.deleteDirAbsolute(subdir_path);
1983}
1984
1985test "chmod" {
1986 if (native_os == .windows or native_os == .wasi)
1987 return error.SkipZigTest;
1988
1989 var tmp = tmpDir(.{});
1990 defer tmp.cleanup();
1991
1992 const file = try tmp.dir.createFile("test_file", .{ .mode = 0o600 });
1993 defer file.close();
1994 try testing.expectEqual(@as(File.Mode, 0o600), (try file.stat()).mode & 0o7777);
1995
1996 try file.chmod(0o644);
1997 try testing.expectEqual(@as(File.Mode, 0o644), (try file.stat()).mode & 0o7777);
1998
1999 try tmp.dir.makeDir("test_dir");
2000 var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true });
2001 defer dir.close();
2002
2003 try dir.chmod(0o700);
2004 try testing.expectEqual(@as(File.Mode, 0o700), (try dir.stat()).mode & 0o7777);
2005}
2006
2007test "chown" {
2008 if (native_os == .windows or native_os == .wasi)
2009 return error.SkipZigTest;
2010
2011 var tmp = tmpDir(.{});
2012 defer tmp.cleanup();
2013
2014 const file = try tmp.dir.createFile("test_file", .{});
2015 defer file.close();
2016 try file.chown(null, null);
2017
2018 try tmp.dir.makeDir("test_dir");
2019
2020 var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true });
2021 defer dir.close();
2022 try dir.chown(null, null);
2023}
2024
2025test "delete a setAsCwd directory on Windows" {
2026 if (native_os != .windows) return error.SkipZigTest;
2027
2028 var tmp = tmpDir(.{});
2029 // Set tmp dir as current working directory.
2030 try tmp.dir.setAsCwd();
2031 tmp.dir.close();
2032 try testing.expectError(error.FileBusy, tmp.parent_dir.deleteTree(&tmp.sub_path));
2033 // Now set the parent dir as the current working dir for clean up.
2034 try tmp.parent_dir.setAsCwd();
2035 try tmp.parent_dir.deleteTree(&tmp.sub_path);
2036 // Close the parent "tmp" so we don't leak the HANDLE.
2037 tmp.parent_dir.close();
2038}
2039
2040test "invalid UTF-8/WTF-8 paths" {
2041 const expected_err = switch (native_os) {
2042 .wasi => error.BadPathName,
2043 .windows => error.BadPathName,
2044 else => return error.SkipZigTest,
2045 };
2046
2047 try testWithAllSupportedPathTypes(struct {
2048 fn impl(ctx: *TestContext) !void {
2049 const io = ctx.io;
2050 // This is both invalid UTF-8 and WTF-8, since \xFF is an invalid start byte
2051 const invalid_path = try ctx.transformPath("\xFF");
2052
2053 try testing.expectError(expected_err, ctx.dir.openFile(invalid_path, .{}));
2054
2055 try testing.expectError(expected_err, ctx.dir.createFile(invalid_path, .{}));
2056
2057 try testing.expectError(expected_err, ctx.dir.makeDir(invalid_path));
2058
2059 try testing.expectError(expected_err, ctx.dir.makePath(invalid_path));
2060 try testing.expectError(expected_err, ctx.dir.makeOpenPath(invalid_path, .{}));
2061
2062 try testing.expectError(expected_err, ctx.dir.openDir(invalid_path, .{}));
2063
2064 try testing.expectError(expected_err, ctx.dir.deleteFile(invalid_path));
2065
2066 try testing.expectError(expected_err, ctx.dir.deleteDir(invalid_path));
2067
2068 try testing.expectError(expected_err, ctx.dir.rename(invalid_path, invalid_path));
2069
2070 try testing.expectError(expected_err, ctx.dir.symLink(invalid_path, invalid_path, .{}));
2071 if (native_os == .wasi) {
2072 try testing.expectError(expected_err, ctx.dir.symLinkWasi(invalid_path, invalid_path, .{}));
2073 }
2074
2075 try testing.expectError(expected_err, ctx.dir.readLink(invalid_path, &[_]u8{}));
2076 if (native_os == .wasi) {
2077 try testing.expectError(expected_err, ctx.dir.readLinkWasi(invalid_path, &[_]u8{}));
2078 }
2079
2080 try testing.expectError(expected_err, ctx.dir.readFile(invalid_path, &[_]u8{}));
2081 try testing.expectError(expected_err, ctx.dir.readFileAlloc(invalid_path, testing.allocator, .limited(0)));
2082
2083 try testing.expectError(expected_err, ctx.dir.deleteTree(invalid_path));
2084 try testing.expectError(expected_err, ctx.dir.deleteTreeMinStackSize(invalid_path));
2085
2086 try testing.expectError(expected_err, ctx.dir.writeFile(.{ .sub_path = invalid_path, .data = "" }));
2087
2088 try testing.expectError(expected_err, ctx.dir.access(invalid_path, .{}));
2089
2090 var dir = ctx.dir.adaptToNewApi();
2091 try testing.expectError(expected_err, dir.updateFile(io, invalid_path, dir, invalid_path, .{}));
2092 try testing.expectError(expected_err, ctx.dir.copyFile(invalid_path, ctx.dir, invalid_path, .{}));
2093
2094 try testing.expectError(expected_err, ctx.dir.statFile(invalid_path));
2095
2096 if (native_os != .wasi) {
2097 try testing.expectError(expected_err, ctx.dir.realpath(invalid_path, &[_]u8{}));
2098 try testing.expectError(expected_err, ctx.dir.realpathAlloc(testing.allocator, invalid_path));
2099 }
2100
2101 try testing.expectError(expected_err, fs.rename(ctx.dir, invalid_path, ctx.dir, invalid_path));
2102
2103 if (native_os != .wasi and ctx.path_type != .relative) {
2104 try testing.expectError(expected_err, fs.copyFileAbsolute(invalid_path, invalid_path, .{}));
2105 try testing.expectError(expected_err, fs.makeDirAbsolute(invalid_path));
2106 try testing.expectError(expected_err, fs.deleteDirAbsolute(invalid_path));
2107 try testing.expectError(expected_err, fs.renameAbsolute(invalid_path, invalid_path));
2108 try testing.expectError(expected_err, fs.openDirAbsolute(invalid_path, .{}));
2109 try testing.expectError(expected_err, fs.openFileAbsolute(invalid_path, .{}));
2110 try testing.expectError(expected_err, fs.accessAbsolute(invalid_path, .{}));
2111 try testing.expectError(expected_err, fs.createFileAbsolute(invalid_path, .{}));
2112 try testing.expectError(expected_err, fs.deleteFileAbsolute(invalid_path));
2113 try testing.expectError(expected_err, fs.deleteTreeAbsolute(invalid_path));
2114 var readlink_buf: [fs.max_path_bytes]u8 = undefined;
2115 try testing.expectError(expected_err, fs.readLinkAbsolute(invalid_path, &readlink_buf));
2116 try testing.expectError(expected_err, fs.symLinkAbsolute(invalid_path, invalid_path, .{}));
2117 try testing.expectError(expected_err, fs.realpathAlloc(testing.allocator, invalid_path));
2118 }
2119 }
2120 }.impl);
2121}
2122
2123test "read file non vectored" {
2124 const io = std.testing.io;
2125
2126 var tmp_dir = testing.tmpDir(.{});
2127 defer tmp_dir.cleanup();
2128
2129 const contents = "hello, world!\n";
2130
2131 const file = try tmp_dir.dir.createFile("input.txt", .{ .read = true });
2132 defer file.close();
2133 {
2134 var file_writer: std.fs.File.Writer = .init(file, &.{});
2135 try file_writer.interface.writeAll(contents);
2136 try file_writer.interface.flush();
2137 }
2138
2139 var file_reader: std.Io.File.Reader = .initAdapted(file, io, &.{});
2140
2141 var write_buffer: [100]u8 = undefined;
2142 var w: std.Io.Writer = .fixed(&write_buffer);
2143
2144 var i: usize = 0;
2145 while (true) {
2146 i += file_reader.interface.stream(&w, .limited(3)) catch |err| switch (err) {
2147 error.EndOfStream => break,
2148 else => |e| return e,
2149 };
2150 }
2151 try testing.expectEqualStrings(contents, w.buffered());
2152 try testing.expectEqual(contents.len, i);
2153}
2154
2155test "seek keeping partial buffer" {
2156 const io = std.testing.io;
2157
2158 var tmp_dir = testing.tmpDir(.{});
2159 defer tmp_dir.cleanup();
2160
2161 const contents = "0123456789";
2162
2163 const file = try tmp_dir.dir.createFile("input.txt", .{ .read = true });
2164 defer file.close();
2165 {
2166 var file_writer: std.fs.File.Writer = .init(file, &.{});
2167 try file_writer.interface.writeAll(contents);
2168 try file_writer.interface.flush();
2169 }
2170
2171 var read_buffer: [3]u8 = undefined;
2172 var file_reader: Io.File.Reader = .initAdapted(file, io, &read_buffer);
2173
2174 try testing.expectEqual(0, file_reader.logicalPos());
2175
2176 var buf: [4]u8 = undefined;
2177 try file_reader.interface.readSliceAll(&buf);
2178
2179 if (file_reader.interface.bufferedLen() != 3) {
2180 // Pass the test if the OS doesn't give us vectored reads.
2181 return;
2182 }
2183
2184 try testing.expectEqual(4, file_reader.logicalPos());
2185 try testing.expectEqual(7, file_reader.pos);
2186 try file_reader.seekTo(6);
2187 try testing.expectEqual(6, file_reader.logicalPos());
2188 try testing.expectEqual(7, file_reader.pos);
2189
2190 try testing.expectEqualStrings("0123", &buf);
2191
2192 const n = try file_reader.interface.readSliceShort(&buf);
2193 try testing.expectEqual(4, n);
2194
2195 try testing.expectEqualStrings("6789", &buf);
2196}
2197
2198test "seekBy" {
2199 const io = testing.io;
2200
2201 var tmp_dir = testing.tmpDir(.{});
2202 defer tmp_dir.cleanup();
2203
2204 try tmp_dir.dir.writeFile(.{ .sub_path = "blah.txt", .data = "let's test seekBy" });
2205 const f = try tmp_dir.dir.openFile("blah.txt", .{ .mode = .read_only });
2206 defer f.close();
2207 var reader = f.readerStreaming(io, &.{});
2208 try reader.seekBy(2);
2209
2210 var buffer: [20]u8 = undefined;
2211 const n = try reader.interface.readSliceShort(&buffer);
2212 try testing.expectEqual(15, n);
2213 try testing.expectEqualStrings("t's test seekBy", buffer[0..15]);
2214}
2215
2216test "seekTo flushes buffered data" {
2217 var tmp = std.testing.tmpDir(.{});
2218 defer tmp.cleanup();
2219
2220 const io = std.testing.io;
2221
2222 const contents = "data";
2223
2224 const file = try tmp.dir.createFile("seek.bin", .{ .read = true });
2225 defer file.close();
2226 {
2227 var buf: [16]u8 = undefined;
2228 var file_writer = std.fs.File.writer(file, &buf);
2229
2230 try file_writer.interface.writeAll(contents);
2231 try file_writer.seekTo(8);
2232 try file_writer.interface.flush();
2233 }
2234
2235 var read_buffer: [16]u8 = undefined;
2236 var file_reader: std.Io.File.Reader = .initAdapted(file, io, &read_buffer);
2237
2238 var buf: [4]u8 = undefined;
2239 try file_reader.interface.readSliceAll(&buf);
2240 try std.testing.expectEqualStrings(contents, &buf);
2241}
2242
2243test "File.Writer sendfile with buffered contents" {
2244 const io = testing.io;
2245
2246 var tmp_dir = testing.tmpDir(.{});
2247 defer tmp_dir.cleanup();
2248
2249 {
2250 try tmp_dir.dir.writeFile(.{ .sub_path = "a", .data = "bcd" });
2251 const in = try tmp_dir.dir.openFile("a", .{});
2252 defer in.close();
2253 const out = try tmp_dir.dir.createFile("b", .{});
2254 defer out.close();
2255
2256 var in_buf: [2]u8 = undefined;
2257 var in_r = in.reader(io, &in_buf);
2258 _ = try in_r.getSize(); // Catch seeks past end by populating size
2259 try in_r.interface.fill(2);
2260
2261 var out_buf: [1]u8 = undefined;
2262 var out_w = out.writerStreaming(&out_buf);
2263 try out_w.interface.writeByte('a');
2264 try testing.expectEqual(3, try out_w.interface.sendFileAll(&in_r, .unlimited));
2265 try out_w.interface.flush();
2266 }
2267
2268 var check = try tmp_dir.dir.openFile("b", .{});
2269 defer check.close();
2270 var check_buf: [4]u8 = undefined;
2271 var check_r = check.reader(io, &check_buf);
2272 try testing.expectEqualStrings("abcd", try check_r.interface.take(4));
2273 try testing.expectError(error.EndOfStream, check_r.interface.takeByte());
2274}