lib/sys/src/fs.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const builtin = @import("builtin");
3 const capabilities = @import("capabilities.zig");
4 const fd = @import("fd.zig");
5
6 const debug_io = std.Options.debug_io;
7 const linux = std.os.linux;
8 const native_os = builtin.os.tag;
9 const posix = std.posix;
10
11 pub const required_capabilities = capabilities.host(&.{.filesystem});
12
13 pub const Dir = std.Io.Dir;
14 pub const CwdError = std.process.CurrentPathAllocError;
15 pub const ReadFileError = std.Io.Dir.ReadFileAllocError;
16 pub const WriteFileError = std.Io.Dir.WriteFileError;
17 pub const AppendFileError = std.Io.File.OpenError || std.Io.File.StatError || std.Io.File.Writer.SeekError || std.Io.Writer.Error;
18 pub const AppendFileSyncError = AppendFileError || std.Io.File.SyncError;
19 pub const SyncFilesystemError = std.Io.File.SyncError;
20 pub const ExistsError = std.Io.Dir.StatFileError;
21 pub const AccessOptions = std.Io.Dir.AccessOptions;
22 pub const AccessError = std.Io.Dir.AccessError;
23 pub const OpenFileError = std.Io.File.OpenError;
24 pub const OpenFileOptions = std.Io.Dir.OpenFileOptions;
25 pub const OpenRegularFileError = std.posix.OpenError ||
26 std.Io.File.StatError ||
27 error{ UnsupportedPlatform, NotRegularFile };
28 pub const OpenDirError = std.Io.Dir.OpenError;
29 pub const OpenDirOptions = std.Io.Dir.OpenOptions;
30 pub const ReadHandleError = std.posix.ReadError;
31 pub const CreateFileOptions = std.Io.Dir.CreateFileOptions;
32 pub const CreateDirPathError = std.Io.Dir.CreateDirPathError;
33 pub const DeleteFileError = std.Io.Dir.DeleteFileError;
34 pub const DeleteTreeError = std.Io.Dir.DeleteTreeError;
35 pub const FilePermissions = std.Io.File.Permissions;
36 pub const SetFilePermissionsOptions = std.Io.Dir.SetFilePermissionsOptions;
37 pub const SetFilePermissionsError = std.Io.Dir.SetFilePermissionsError;
38 pub const RenameError = std.Io.Dir.RenameError;
39 pub const ExchangeError = error{
40 FileNotFound,
41 AccessDenied,
42 ExchangeUnsupported,
43 } || posix.UnexpectedError;
44 pub const RealPathError = std.Io.Dir.RealPathFileAllocError;
45 pub const ListDirError = std.Io.Dir.OpenError || std.Io.Dir.Iterator.Error || std.mem.Allocator.Error;
46 pub const FileOwnerUserId = linux.uid_t;
47 pub const FileMode = posix.mode_t;
48 pub const WatchEvent = linux.inotify_event;
49
50 pub const NamedPipeError = error{
51 UnsupportedPlatform,
52 AccessDenied,
53 CreateFailed,
54 };
55 pub const AnonymousFileError = error{
56 UnsupportedPlatform,
57 OutOfMemory,
58 CreateFailed,
59 };
60 pub const TruncateError = error{
61 UnsupportedPlatform,
62 OutOfMemory,
63 TruncateFailed,
64 };
65 pub const WatchError = error{
66 UnsupportedPlatform,
67 WatchFailed,
68 };
69
70 pub const WatchMask = struct {
71 pub const close_write = linux.IN.CLOSE_WRITE;
72 pub const create = linux.IN.CREATE;
73 pub const delete = linux.IN.DELETE;
74 pub const delete_self = linux.IN.DELETE_SELF;
75 pub const ignored = linux.IN.IGNORED;
76 pub const moved_from = linux.IN.MOVED_FROM;
77 pub const moved_to = linux.IN.MOVED_TO;
78 pub const move_self = linux.IN.MOVE_SELF;
79 pub const queue_overflow = linux.IN.Q_OVERFLOW;
80 pub const unmount = linux.IN.UNMOUNT;
81 };
82
83 pub const FileOwnerPolicy = enum {
84 unsupported,
85 linux_statx,
86 };
87
88 pub const AllocatedBlocksUnsupported = enum {
89 platform,
90 statx_unavailable,
91 };
92
93 pub const AllocatedBlocks = union(enum) {
94 supported: u64,
95 unsupported: AllocatedBlocksUnsupported,
96 };
97
98 pub const FileOwnerError = error{
99 UnsupportedPlatform,
100 BadPathName,
101 NameTooLong,
102 AccessDenied,
103 FileNotFound,
104 NotDir,
105 SymLinkLoop,
106 SystemResources,
107 OwnerUnavailable,
108 };
109
110 pub const EntryKind = enum {
111 file,
112 directory,
113 sym_link,
114 other,
115 };
116
117 pub const Entry = struct {
118 name: []u8,
119 kind: EntryKind,
120 path: []u8,
121
122 pub fn deinit(self: Entry, allocator: std.mem.Allocator) void {
123 allocator.free(self.name);
124 allocator.free(self.path);
125 }
126 };
127
128 pub const OpenedRegularFile = struct {
129 file: std.Io.File,
130 stat: std.Io.File.Stat,
131 };
132
133 pub fn debugIo() std.Io {
134 return debug_io;
135 }
136
137 pub fn cwd() Dir {
138 return Dir.cwd();
139 }
140
141 pub fn cwdAlloc(allocator: std.mem.Allocator) CwdError![]u8 {
142 const path_z = try std.process.currentPathAlloc(debug_io, allocator);
143 defer allocator.free(path_z);
144 return try allocator.dupe(u8, path_z);
145 }
146
147 pub fn setCreationMask(mask: FileMode) error{UnsupportedPlatform}!FileMode {
148 if (comptime native_os != .linux) return error.UnsupportedPlatform;
149 return @intCast(linux.syscall1(.umask, mask));
150 }
151
152 pub fn createNamedPipe(
153 directory: Dir,
154 name: [:0]const u8,
155 mode: FileMode,
156 ) NamedPipeError!void {
157 const result = switch (native_os) {
158 .linux => linux.mknodat(directory.handle, name.ptr, linux.S.IFIFO | mode, 0),
159 .macos => darwin.mkfifoat(directory.handle, name.ptr, mode),
160 else => return error.UnsupportedPlatform,
161 };
162 return switch (posix.errno(result)) {
163 .SUCCESS => {},
164 .ACCES, .PERM => error.AccessDenied,
165 else => error.CreateFailed,
166 };
167 }
168
169 pub fn createAnonymousFile(name: []const u8) AnonymousFileError!fd.Descriptor {
170 if (comptime native_os != .linux) return error.UnsupportedPlatform;
171 return posix.memfd_create(name, posix.MFD.CLOEXEC) catch |err| switch (err) {
172 error.OutOfMemory => error.OutOfMemory,
173 else => error.CreateFailed,
174 };
175 }
176
177 /// Creates a Linux memory file that allows seals and carries none yet, for a
178 /// caller that will later seal the file because sealing has to be allowed at
179 /// creation. Bytes go in before the seals, because the immutable check requires
180 /// the seals to be present. The descriptor is close-on-exec, and the caller
181 /// closes it, while a host other than Linux returns `UnsupportedPlatform`.
182 pub fn createSealableFile(name: []const u8) AnonymousFileError!fd.Descriptor {
183 if (comptime native_os != .linux) return error.UnsupportedPlatform;
184 return posix.memfd_create(name, posix.MFD.CLOEXEC | posix.MFD.ALLOW_SEALING) catch |err|
185 switch (err) {
186 error.OutOfMemory => error.OutOfMemory,
187 else => error.CreateFailed,
188 };
189 }
190
191 /// Seals a populated file so that every later mapping of it sees the same
192 /// bytes, adding the write, shrink, and grow seals, which stop mutation of the
193 /// file through any descriptor. The seals are read back before the call
194 /// returns, so a return means they are in place, while every failure, including
195 /// a host other than Linux, reports `UnsupportedImageBacking`.
196 pub fn sealFile(descriptor: fd.Descriptor) error{UnsupportedImageBacking}!void {
197 if (comptime native_os != .linux) return error.UnsupportedImageBacking;
198 const seals = linux.F.SEAL_WRITE | linux.F.SEAL_SHRINK | linux.F.SEAL_GROW;
199 if (posix.errno(linux.fcntl(descriptor, linux.F.ADD_SEALS, seals)) != .SUCCESS) {
200 return error.UnsupportedImageBacking;
201 }
202 try requireImmutableFile(descriptor);
203 }
204
205 /// Admits a descriptor for an immutable mapping only when the Linux write,
206 /// shrink, and grow seals are all present, so a mutable ordinary file returns
207 /// `UnsupportedImageBacking`. Read-only permissions, a matching digest, and a
208 /// private mapping do not qualify a file on their own.
209 pub fn requireImmutableFile(descriptor: fd.Descriptor) error{UnsupportedImageBacking}!void {
210 if (comptime native_os != .linux) return error.UnsupportedImageBacking;
211 const required = linux.F.SEAL_WRITE | linux.F.SEAL_SHRINK | linux.F.SEAL_GROW;
212 const actual = linux.fcntl(descriptor, linux.F.GET_SEALS, 0);
213 if (posix.errno(actual) != .SUCCESS or actual & required != required) {
214 return error.UnsupportedImageBacking;
215 }
216 }
217
218 pub fn truncateDescriptor(
219 descriptor: fd.Descriptor,
220 byte_len: usize,
221 ) TruncateError!void {
222 switch (comptime native_os) {
223 .linux => {
224 var operation: LinuxTruncateOperation = .{};
225 return truncateDescriptorWith(&operation, descriptor, byte_len);
226 },
227 .macos => {
228 var operation: DarwinTruncateOperation = .{};
229 return truncateDescriptorWith(&operation, descriptor, byte_len);
230 },
231 else => return error.UnsupportedPlatform,
232 }
233 }
234
235 pub fn createWatch() WatchError!fd.Descriptor {
236 if (comptime native_os != .linux) return error.UnsupportedPlatform;
237 const result = linux.inotify_init1(linux.IN.CLOEXEC | linux.IN.NONBLOCK);
238 return switch (linux.errno(result)) {
239 .SUCCESS => @intCast(result),
240 else => error.WatchFailed,
241 };
242 }
243
244 pub fn addWatch(
245 descriptor: fd.Descriptor,
246 path: [:0]const u8,
247 mask: u32,
248 ) WatchError!i32 {
249 if (comptime native_os != .linux) return error.UnsupportedPlatform;
250 const result = linux.inotify_add_watch(descriptor, path.ptr, mask);
251 return switch (linux.errno(result)) {
252 .SUCCESS => @intCast(result),
253 else => error.WatchFailed,
254 };
255 }
256
257 pub fn removeWatch(descriptor: fd.Descriptor, watch: i32) WatchError!void {
258 if (comptime native_os != .linux) return error.UnsupportedPlatform;
259 return switch (linux.errno(linux.inotify_rm_watch(descriptor, watch))) {
260 .SUCCESS => {},
261 else => error.WatchFailed,
262 };
263 }
264
265 pub fn readFileAlloc(
266 allocator: std.mem.Allocator,
267 path: []const u8,
268 limit: usize,
269 ) ReadFileError![]u8 {
270 return std.Io.Dir.cwd().readFileAlloc(debug_io, path, allocator, .limited(limit));
271 }
272
273 pub fn writeFile(path: []const u8, contents: []const u8) WriteFileError!void {
274 try std.Io.Dir.cwd().writeFile(debug_io, .{
275 .sub_path = path,
276 .data = contents,
277 .flags = .{ .truncate = true },
278 });
279 }
280
281 pub fn createFile(path: []const u8, options: CreateFileOptions) OpenFileError!std.Io.File {
282 return try std.Io.Dir.cwd().createFile(debug_io, path, options);
283 }
284
285 pub fn openAbsoluteFile(path: []const u8, options: OpenFileOptions) OpenFileError!std.Io.File {
286 return try std.Io.Dir.openFileAbsolute(debug_io, path, options);
287 }
288
289 pub fn openAbsoluteDir(path: []const u8, options: OpenDirOptions) OpenDirError!Dir {
290 return try std.Io.Dir.openDirAbsolute(debug_io, path, options);
291 }
292
293 pub fn openRegularFile(
294 io: std.Io,
295 directory: Dir,
296 path: []const u8,
297 ) OpenRegularFileError!OpenedRegularFile {
298 if (comptime native_os != .linux and native_os != .macos) {
299 return error.UnsupportedPlatform;
300 }
301 const descriptor = std.posix.openat(directory.handle, path, .{
302 .ACCMODE = .RDONLY,
303 .NONBLOCK = true,
304 .CLOEXEC = true,
305 .NOFOLLOW = true,
306 }, 0) catch |err| switch (err) {
307 error.SymLinkLoop => return error.NotRegularFile,
308 else => |other| return other,
309 };
310 var file = std.Io.File{
311 .handle = descriptor,
312 .flags = .{ .nonblocking = true },
313 };
314 errdefer file.close(io);
315 const stat = file.stat(io) catch |err| switch (err) {
316 error.Streaming => return error.NotRegularFile,
317 else => |other| return other,
318 };
319 if (stat.kind != .file) return error.NotRegularFile;
320 return .{ .file = file, .stat = stat };
321 }
322
323 pub fn readHandle(file: std.Io.File, buffer: []u8) ReadHandleError!usize {
324 return try std.posix.read(file.handle, buffer);
325 }
326
327 pub fn readHandleAt(file: std.Io.File, buffer: []u8, offset: u64) !usize {
328 return file.readPositionalAll(debug_io, buffer, offset);
329 }
330
331 pub fn readHandleToEndAlloc(allocator: std.mem.Allocator, file: std.Io.File, limit: usize) ![]u8 {
332 var buffer: [4096]u8 = undefined;
333 var reader = file.reader(debug_io, &buffer);
334 return reader.interface.allocRemaining(allocator, .limited(limit));
335 }
336
337 pub fn writeHandleAll(file: std.Io.File, bytes: []const u8) !void {
338 try file.writeStreamingAll(debug_io, bytes);
339 }
340
341 pub fn writeHandleAt(file: std.Io.File, bytes: []const u8, offset: u64) !void {
342 try file.writePositionalAll(debug_io, bytes, offset);
343 }
344
345 pub fn closeHandle(file: std.Io.File) void {
346 file.close(debug_io);
347 }
348
349 /// Flushes the open file's dirty data pages to the storage device before a
350 /// caller reports written records durable, so that the records can be found
351 /// again after a crash. Surviving a power loss still depends on the device's
352 /// own caches and on its support for the flush command, so a return carries no
353 /// unconditional hardware guarantee. On Linux the call is `fdatasync`, which
354 /// leaves unchanged metadata alone, while a host other than Linux returns
355 /// `error.Unexpected`.
356 pub fn syncData(file: std.Io.File) std.posix.SyncError!void {
357 if (comptime builtin.os.tag == .linux) return std.posix.fdatasync(file.handle);
358 return error.Unexpected;
359 }
360
361 test "Journal data sync persists descriptor writes" {
362 if (comptime builtin.os.tag != .linux) return error.SkipZigTest;
363 var tmp = std.testing.tmpDir(.{});
364 defer tmp.cleanup();
365 const file = try tmp.dir.createFile(debug_io, "journal", .{ .read = true });
366 defer file.close(debug_io);
367 try writeHandleAt(file, "selected Journal bytes", 0);
368 try syncData(file);
369 const reopened = try tmp.dir.openFile(debug_io, "journal", .{});
370 defer reopened.close(debug_io);
371 var bytes: [22]u8 = undefined;
372 const count = try readHandleAt(reopened, &bytes, 0);
373 try std.testing.expectEqualStrings("selected Journal bytes", bytes[0..count]);
374 }
375
376 pub fn syncFilesystem(file: std.Io.File) SyncFilesystemError!void {
377 if (comptime builtin.os.tag == .linux) return std.posix.syncfs(file.handle);
378 if (comptime builtin.os.tag.isDarwin()) return syncDarwinFilesystem(file);
379 if (comptime builtin.os.tag == .windows) return;
380 return file.sync(debug_io);
381 }
382
383 fn syncDarwinFilesystem(file: std.Io.File) SyncFilesystemError!void {
384 while (true) switch (@as(std.posix.E, @fromBackingInt(@intCast(darwin.fsync_volume_np(
385 file.handle,
386 darwin.sync_volume_wait,
387 ))))) {
388 .SUCCESS => return,
389 .INTR => {},
390 .BADF, .INVAL, .ROFS => unreachable,
391 .IO => return error.InputOutput,
392 .NOSPC => return error.NoSpaceLeft,
393 .DQUOT => return error.DiskQuota,
394 .ACCES, .PERM => return error.AccessDenied,
395 else => |err| return std.posix.unexpectedErrno(err),
396 };
397 }
398
399 const darwin = struct {
400 const sync_volume_wait = 0x02;
401
402 extern "c" fn fsync_volume_np(fd: std.posix.fd_t, flags: c_int) c_int;
403 extern "c" fn ftruncate(fd: std.posix.fd_t, length: std.posix.off_t) c_int;
404 extern "c" fn mkfifoat(
405 fd: std.posix.fd_t,
406 path: [*:0]const u8,
407 mode: std.posix.mode_t,
408 ) c_int;
409 };
410
411 pub fn appendFile(path: []const u8, chunks: []const []const u8) AppendFileError!void {
412 var file = try std.Io.Dir.cwd().createFile(debug_io, path, .{
413 .truncate = false,
414 .read = false,
415 });
416 defer file.close(debug_io);
417
418 const stat = try file.stat(debug_io);
419 var write_buffer: [1024]u8 = undefined;
420 var writer = file.writer(debug_io, &write_buffer);
421 try writer.seekTo(stat.size);
422 for (chunks) |chunk| try writer.interface.writeAll(chunk);
423 try writer.interface.flush();
424 }
425
426 pub fn appendFileSync(path: []const u8, chunks: []const []const u8) AppendFileSyncError!void {
427 var file = try std.Io.Dir.cwd().createFile(debug_io, path, .{
428 .truncate = false,
429 .read = false,
430 });
431 defer file.close(debug_io);
432
433 const stat = try file.stat(debug_io);
434 var write_buffer: [1024]u8 = undefined;
435 var writer = file.writer(debug_io, &write_buffer);
436 try writer.seekTo(stat.size);
437 for (chunks) |chunk| try writer.interface.writeAll(chunk);
438 try writer.interface.flush();
439 try file.sync(debug_io);
440 }
441
442 pub fn exists(path: []const u8) bool {
443 _ = statFile(path) catch return false;
444 return true;
445 }
446
447 pub fn accessAbsolute(path: []const u8, options: AccessOptions) AccessError!void {
448 try std.Io.Dir.accessAbsolute(debug_io, path, options);
449 }
450
451 pub fn absolutePathExists(path: []const u8) bool {
452 accessAbsolute(path, .{}) catch return false;
453 return true;
454 }
455
456 pub fn statFile(path: []const u8) ExistsError!std.Io.Dir.Stat {
457 return std.Io.Dir.cwd().statFile(debug_io, path, .{});
458 }
459
460 pub fn allocatedBlocks(file: std.Io.File) AllocatedBlocks {
461 return switch (comptime native_os) {
462 .linux => allocatedBlocksLinux(file),
463 else => .{ .unsupported = .platform },
464 };
465 }
466
467 fn allocatedBlocksLinux(file: std.Io.File) AllocatedBlocks {
468 while (true) {
469 var statx = std.mem.zeroes(linux.Statx);
470 const rc = linux.statx(
471 file.handle,
472 "",
473 linux.AT.EMPTY_PATH,
474 .{ .BLOCKS = true },
475 &statx,
476 );
477 switch (linux.errno(rc)) {
478 .SUCCESS => return if (statx.mask.BLOCKS)
479 .{ .supported = statx.blocks }
480 else
481 .{ .unsupported = .statx_unavailable },
482 .INTR => {},
483 .BADF => unreachable,
484 else => return .{ .unsupported = .statx_unavailable },
485 }
486 }
487 }
488
489 pub fn fileOwnerPolicy() FileOwnerPolicy {
490 return switch (native_os) {
491 .linux => .linux_statx,
492 else => .unsupported,
493 };
494 }
495
496 pub fn fileOwnerUserId(
497 path: []const u8,
498 ) FileOwnerError!FileOwnerUserId {
499 return switch (comptime fileOwnerPolicy()) {
500 .unsupported => error.UnsupportedPlatform,
501 .linux_statx => fileOwnerUserIdLinux(path),
502 };
503 }
504
505 fn fileOwnerUserIdLinux(
506 path: []const u8,
507 ) FileOwnerError!FileOwnerUserId {
508 if (comptime native_os != .linux) {
509 return error.UnsupportedPlatform;
510 }
511 if (std.mem.indexOfScalar(u8, path, 0) != null) {
512 return error.BadPathName;
513 }
514 var path_buffer: [std.posix.PATH_MAX]u8 =
515 undefined;
516 if (path.len >= path_buffer.len) {
517 return error.NameTooLong;
518 }
519 @memcpy(path_buffer[0..path.len], path);
520 path_buffer[path.len] = 0;
521 const path_z = path_buffer[0..path.len :0];
522 while (true) {
523 var statx = std.mem.zeroes(linux.Statx);
524 const rc = linux.statx(
525 linux.AT.FDCWD,
526 path_z,
527 linux.AT.NO_AUTOMOUNT,
528 .{ .UID = true },
529 &statx,
530 );
531 switch (linux.errno(rc)) {
532 .SUCCESS => {
533 if (!statx.mask.UID) {
534 return error.OwnerUnavailable;
535 }
536 return statx.uid;
537 },
538 .INTR => {},
539 .ACCES => return error.AccessDenied,
540 .LOOP => return error.SymLinkLoop,
541 .NAMETOOLONG => return error.NameTooLong,
542 .NOENT => return error.FileNotFound,
543 .NOTDIR => return error.NotDir,
544 .NOMEM => return error.SystemResources,
545 else => return error.OwnerUnavailable,
546 }
547 }
548 }
549
550 pub fn absoluteFileExists(path: []const u8) OpenFileError!bool {
551 var file = std.Io.Dir.openFileAbsolute(debug_io, path, .{}) catch |err| switch (err) {
552 error.FileNotFound => return false,
553 else => return err,
554 };
555 defer file.close(debug_io);
556 return true;
557 }
558
559 pub fn createDirPath(path: []const u8) CreateDirPathError!void {
560 try std.Io.Dir.cwd().createDirPath(debug_io, path);
561 }
562
563 pub fn createAbsoluteDirPath(path: []const u8) CreateDirPathError!void {
564 std.debug.assert(std.fs.path.isAbsolute(path));
565 try createDirPath(path);
566 }
567
568 pub fn deleteFile(path: []const u8) DeleteFileError!void {
569 try std.Io.Dir.cwd().deleteFile(debug_io, path);
570 }
571
572 pub fn deleteAbsoluteFile(path: []const u8) DeleteFileError!void {
573 try std.Io.Dir.deleteFileAbsolute(debug_io, path);
574 }
575
576 pub fn deleteTree(path: []const u8) DeleteTreeError!void {
577 try std.Io.Dir.cwd().deleteTree(debug_io, path);
578 }
579
580 pub fn setFilePermissions(
581 path: []const u8,
582 permissions: FilePermissions,
583 options: SetFilePermissionsOptions,
584 ) SetFilePermissionsError!void {
585 try std.Io.Dir.cwd().setFilePermissions(debug_io, path, permissions, options);
586 }
587
588 pub fn rename(old_path: []const u8, new_path: []const u8) RenameError!void {
589 try std.Io.Dir.cwd().rename(old_path, std.Io.Dir.cwd(), new_path, debug_io);
590 }
591
592 /// Swaps two entries of one directory in a single atomic rename, so every
593 /// observer sees either both old names or both new names. Linux provides it
594 /// through renameat2 RENAME_EXCHANGE. A kernel or filesystem without it
595 /// refuses with ExchangeUnsupported.
596 pub fn exchange(directory: Dir, first: [:0]const u8, second: [:0]const u8) ExchangeError!void {
597 if (comptime native_os != .linux) return error.ExchangeUnsupported;
598 const result = linux.renameat2(directory.handle, first, directory.handle, second, .{ .EXCHANGE = true });
599 switch (linux.errno(result)) {
600 .SUCCESS => {},
601 .NOENT => return error.FileNotFound,
602 .ACCES, .PERM => return error.AccessDenied,
603 .INVAL, .NOSYS, .OPNOTSUPP => return error.ExchangeUnsupported,
604 else => |code| return posix.unexpectedErrno(code),
605 }
606 }
607
608 test "exchange swaps a file and a directory in one rename" {
609 if (comptime native_os != .linux) return error.SkipZigTest;
610 var tmp = std.testing.tmpDir(.{});
611 defer tmp.cleanup();
612 try tmp.dir.writeFile(debug_io, .{ .sub_path = "first", .data = "first bytes" });
613 try tmp.dir.createDir(debug_io, "second", .fromMode(0o700));
614 try exchange(tmp.dir, "first", "second");
615 try std.testing.expectEqual(.directory, (try tmp.dir.statFile(debug_io, "first", .{})).kind);
616 var bytes: [16]u8 = undefined;
617 const contents = try tmp.dir.readFile(debug_io, "second", &bytes);
618 try std.testing.expectEqualStrings("first bytes", contents);
619 try std.testing.expectError(error.FileNotFound, exchange(tmp.dir, "first", "missing"));
620 }
621
622 pub fn realPathAlloc(allocator: std.mem.Allocator, path: []const u8) RealPathError![]u8 {
623 const path_z = try std.Io.Dir.cwd().realPathFileAlloc(debug_io, path, allocator);
624 defer allocator.free(path_z);
625 return try allocator.dupe(u8, path_z);
626 }
627
628 pub fn listDirAlloc(allocator: std.mem.Allocator, path: []const u8) ListDirError![]Entry {
629 var handle = try std.Io.Dir.cwd().openDir(debug_io, path, .{ .iterate = true });
630 defer handle.close(debug_io);
631
632 var entries: std.ArrayListUnmanaged(Entry) = .empty;
633 errdefer {
634 for (entries.items) |entry| entry.deinit(allocator);
635 entries.deinit(allocator);
636 }
637
638 var iterator = handle.iterate();
639 while (try iterator.next(debug_io)) |entry| {
640 const entry_path = try std.fs.path.join(allocator, &.{ path, entry.name });
641 const entry_name = try allocator.dupe(u8, entry.name);
642 var appended = false;
643 errdefer if (!appended) {
644 allocator.free(entry_path);
645 allocator.free(entry_name);
646 };
647 try entries.append(allocator, .{
648 .name = entry_name,
649 .kind = entryKind(entry.kind),
650 .path = entry_path,
651 });
652 appended = true;
653 }
654
655 return try entries.toOwnedSlice(allocator);
656 }
657
658 pub fn freeEntries(allocator: std.mem.Allocator, entries: []Entry) void {
659 for (entries) |entry| entry.deinit(allocator);
660 allocator.free(entries);
661 }
662
663 const TruncateStatus = enum {
664 success,
665 interrupted,
666 out_of_memory,
667 failed,
668 };
669
670 const LinuxTruncateOperation = struct {
671 fn run(
672 _: *@This(),
673 descriptor: fd.Descriptor,
674 byte_len: usize,
675 ) TruncateStatus {
676 return switch (linux.errno(linux.ftruncate(descriptor, @intCast(byte_len)))) {
677 .SUCCESS => .success,
678 .INTR => .interrupted,
679 .NOMEM => .out_of_memory,
680 else => .failed,
681 };
682 }
683 };
684
685 const DarwinTruncateOperation = struct {
686 fn run(
687 _: *@This(),
688 descriptor: fd.Descriptor,
689 byte_len: usize,
690 ) TruncateStatus {
691 return switch (posix.errno(darwin.ftruncate(descriptor, @intCast(byte_len)))) {
692 .SUCCESS => .success,
693 .INTR => .interrupted,
694 .NOMEM => .out_of_memory,
695 else => .failed,
696 };
697 }
698 };
699
700 fn truncateDescriptorWith(
701 operation: anytype,
702 descriptor: fd.Descriptor,
703 byte_len: usize,
704 ) TruncateError!void {
705 while (true) {
706 switch (operation.run(descriptor, byte_len)) {
707 .success => return,
708 .interrupted => continue,
709 .out_of_memory => return error.OutOfMemory,
710 .failed => return error.TruncateFailed,
711 }
712 }
713 }
714
715 fn entryKind(kind: std.Io.File.Kind) EntryKind {
716 return switch (kind) {
717 .file => .file,
718 .directory => .directory,
719 .sym_link => .sym_link,
720 else => .other,
721 };
722 }
723
724 const regular_open_input: [:0]const u8 = ".tiny-regular-open-input";
725 const regular_open_ready: [:0]const u8 = ".tiny-regular-open-ready";
726 const regular_open_go: [:0]const u8 = ".tiny-regular-open-go";
727 const handshake_attempts_max: usize = 500;
728 const handshake_wait_ms: u64 = 10;
729
730 fn supportsRegularFileOpen() bool {
731 return native_os == .linux or native_os == .macos;
732 }
733
734 fn createNamedPipeForTest(
735 directory: Dir,
736 name: [:0]const u8,
737 ) !void {
738 createNamedPipe(directory, name, 0o600) catch |err| switch (err) {
739 error.UnsupportedPlatform, error.AccessDenied => return error.SkipZigTest,
740 error.CreateFailed => return error.CreateNamedPipeFailed,
741 };
742 }
743
744 const InterruptingTruncateOperation = struct {
745 attempts: usize = 0,
746
747 fn run(self: *@This(), _: fd.Descriptor, _: usize) TruncateStatus {
748 self.attempts += 1;
749 return if (self.attempts == 1) .interrupted else .success;
750 }
751 };
752
753 const FixedTruncateOperation = struct {
754 status: TruncateStatus,
755 attempts: usize = 0,
756
757 fn run(self: *@This(), _: fd.Descriptor, _: usize) TruncateStatus {
758 self.attempts += 1;
759 return self.status;
760 }
761 };
762
763 test "descriptor truncate accepts success" {
764 var operation: FixedTruncateOperation = .{ .status = .success };
765 try truncateDescriptorWith(&operation, 7, 4096);
766 try std.testing.expectEqual(@as(usize, 1), operation.attempts);
767 }
768
769 test "descriptor truncate retries interruptions" {
770 var operation: InterruptingTruncateOperation = .{};
771 try truncateDescriptorWith(&operation, 7, 4096);
772 try std.testing.expectEqual(@as(usize, 2), operation.attempts);
773 }
774
775 test "descriptor truncate maps operation errors" {
776 var exhausted: FixedTruncateOperation = .{ .status = .out_of_memory };
777 try std.testing.expectError(
778 error.OutOfMemory,
779 truncateDescriptorWith(&exhausted, 7, 4096),
780 );
781 try std.testing.expectEqual(@as(usize, 1), exhausted.attempts);
782
783 var failed: FixedTruncateOperation = .{ .status = .failed };
784 try std.testing.expectError(
785 error.TruncateFailed,
786 truncateDescriptorWith(&failed, 7, 4096),
787 );
788 try std.testing.expectEqual(@as(usize, 1), failed.attempts);
789 }
790
791 fn openNamedPipeGuardForTest(
792 directory: Dir,
793 name: [:0]const u8,
794 ) !std.Io.File {
795 const descriptor = try std.posix.openat(directory.handle, name, .{
796 .ACCMODE = .RDWR,
797 .NONBLOCK = true,
798 .CLOEXEC = true,
799 .NOFOLLOW = true,
800 }, 0);
801 return .{
802 .handle = descriptor,
803 .flags = .{ .nonblocking = true },
804 };
805 }
806
807 fn expectRegularOpenRejected(directory: Dir, name: []const u8) !void {
808 var opened = openRegularFile(debug_io, directory, name) catch |err| {
809 if (err != error.NotRegularFile) return err;
810 return;
811 };
812 defer opened.file.close(debug_io);
813 return error.ExpectedRegularFileRejection;
814 }
815
816 fn expectRegularOpenFlags(file: std.Io.File) !void {
817 const descriptor_flags = std.posix.system.fcntl(
818 file.handle,
819 std.posix.F.GETFD,
820 @as(usize, 0),
821 );
822 try std.testing.expectEqual(
823 std.posix.E.SUCCESS,
824 std.posix.errno(descriptor_flags),
825 );
826 const descriptor_bits: usize = @intCast(descriptor_flags);
827 try std.testing.expect(
828 descriptor_bits & @as(usize, std.posix.FD_CLOEXEC) != 0,
829 );
830 const status_flags = std.posix.system.fcntl(
831 file.handle,
832 std.posix.F.GETFL,
833 @as(usize, 0),
834 );
835 try std.testing.expectEqual(
836 std.posix.E.SUCCESS,
837 std.posix.errno(status_flags),
838 );
839 const status_bits: usize = @intCast(status_flags);
840 const nonblocking: u32 = @bitCast(std.posix.O{ .NONBLOCK = true });
841 try std.testing.expect(status_bits & nonblocking != 0);
842 }
843
844 fn waitForTestEntry(directory: Dir, name: []const u8) !void {
845 const time = @import("time.zig");
846 for (0..handshake_attempts_max) |_| {
847 if (directory.statFile(
848 debug_io,
849 name,
850 .{ .follow_symlinks = false },
851 )) |_| {
852 return;
853 } else |err| switch (err) {
854 error.FileNotFound => time.sleepMilliseconds(handshake_wait_ms),
855 else => return err,
856 }
857 }
858 return error.TestHandshakeTimedOut;
859 }
860
861 fn waitForReplacementChild(child: *@import("process/root.zig").Child) !void {
862 const process = @import("process/root.zig");
863 const time = @import("time.zig");
864 const child_id = child.id orelse return error.ExpectedChildProcess;
865 for (0..handshake_attempts_max) |_| {
866 if (try process.waitNoHang(child_id)) |outcome| {
867 child.id = null;
868 if (process.exitCode(outcome.term) != 0) {
869 return error.ReplacementChildFailed;
870 }
871 return;
872 }
873 time.sleepMilliseconds(handshake_wait_ms);
874 }
875 return error.ReplacementChildTimedOut;
876 }
877
878 test "regular file opener returns descriptor-bound read-only bytes" {
879 if (!supportsRegularFileOpen()) return error.SkipZigTest;
880 var temporary = std.testing.tmpDir(.{});
881 defer temporary.cleanup();
882 try temporary.dir.writeFile(debug_io, .{
883 .sub_path = "read-only.txt",
884 .data = "descriptor bytes",
885 });
886 try temporary.dir.setFilePermissions(
887 debug_io,
888 "read-only.txt",
889 .fromMode(0o400),
890 .{},
891 );
892 var opened = try openRegularFile(
893 debug_io,
894 temporary.dir,
895 "read-only.txt",
896 );
897 defer opened.file.close(debug_io);
898 try std.testing.expectEqual(std.Io.File.Kind.file, opened.stat.kind);
899 try std.testing.expect(opened.file.flags.nonblocking);
900 try expectRegularOpenFlags(opened.file);
901 var reader = opened.file.reader(debug_io, &.{});
902 const bytes = try reader.interface.allocRemaining(
903 std.testing.allocator,
904 .limited(32),
905 );
906 defer std.testing.allocator.free(bytes);
907 try std.testing.expectEqualStrings("descriptor bytes", bytes);
908 }
909
910 test "regular file opener rejects final symlinks" {
911 if (!supportsRegularFileOpen()) return error.SkipZigTest;
912 var temporary = std.testing.tmpDir(.{});
913 defer temporary.cleanup();
914 try temporary.dir.writeFile(debug_io, .{
915 .sub_path = "target.txt",
916 .data = "target",
917 });
918 temporary.dir.symLink(
919 debug_io,
920 "target.txt",
921 "link.txt",
922 .{},
923 ) catch |err| switch (err) {
924 error.AccessDenied, error.PermissionDenied, error.FileSystem => {
925 return error.SkipZigTest;
926 },
927 else => return err,
928 };
929 try expectRegularOpenRejected(temporary.dir, "link.txt");
930 }
931
932 test "regular file opener rejects named pipes" {
933 if (!supportsRegularFileOpen()) return error.SkipZigTest;
934 var temporary = std.testing.tmpDir(.{});
935 defer temporary.cleanup();
936 try createNamedPipeForTest(temporary.dir, "named-pipe");
937 var guard = try openNamedPipeGuardForTest(
938 temporary.dir,
939 "named-pipe",
940 );
941 defer guard.close(debug_io);
942 try expectRegularOpenRejected(temporary.dir, "named-pipe");
943 }
944
945 test "regular file opener rejects replacement without waiting" {
946 if (!supportsRegularFileOpen()) return error.SkipZigTest;
947 const process = @import("process/root.zig");
948 var io_state = std.Io.Threaded.init(std.testing.allocator, .{});
949 defer io_state.deinit();
950 const process_io = io_state.io();
951 var temporary = std.testing.tmpDir(.{});
952 defer temporary.cleanup();
953 try temporary.dir.writeFile(debug_io, .{
954 .sub_path = regular_open_input,
955 .data = "regular",
956 });
957 const root = try temporary.dir.realPathFileAlloc(
958 debug_io,
959 ".",
960 std.testing.allocator,
961 );
962 defer std.testing.allocator.free(root);
963 const child_path = try std.Io.Dir.cwd().realPathFileAlloc(
964 debug_io,
965 @import("fs_test_options").regular_open_child_path,
966 std.testing.allocator,
967 );
968 defer std.testing.allocator.free(child_path);
969 var child = try process.spawn(process_io, .{
970 .argv = &.{
971 child_path,
972 regular_open_input,
973 regular_open_ready,
974 regular_open_go,
975 },
976 .cwd = .{ .path = root },
977 .stdin = .ignore,
978 .stdout = .ignore,
979 .stderr = .inherit,
980 });
981 defer process.killAndReap(&child, process_io);
982 try waitForTestEntry(temporary.dir, regular_open_ready);
983 try temporary.dir.deleteFile(debug_io, regular_open_input);
984 try createNamedPipeForTest(temporary.dir, regular_open_input);
985 try temporary.dir.writeFile(debug_io, .{
986 .sub_path = regular_open_go,
987 .data = "",
988 });
989 try waitForReplacementChild(&child);
990 }
991
992 test "writeFile and readFileAlloc round-trip through cwd" {
993 var tmp = std.testing.tmpDir(.{});
994 defer tmp.cleanup();
995
996 const allocator = std.testing.allocator;
997 const tmp_root = try tmp.dir.realPathFileAlloc(debug_io, ".", allocator);
998 defer allocator.free(tmp_root);
999
1000 const path = try std.fs.path.join(allocator, &.{ tmp_root, "round-trip.txt" });
1001 defer allocator.free(path);
1002
1003 try writeFile(path, "hello");
1004 try std.testing.expect(exists(path));
1005 try std.testing.expect(try absoluteFileExists(path));
1006
1007 const contents = try readFileAlloc(allocator, path, 1024);
1008 defer allocator.free(contents);
1009 try std.testing.expectEqualStrings("hello", contents);
1010 }
1011
1012 test "cwd handle is available through sys fs" {
1013 _ = cwd();
1014 _ = debugIo();
1015 }
1016
1017 test "file owner policy is explicit" {
1018 const expected: FileOwnerPolicy = switch (native_os) {
1019 .linux => .linux_statx,
1020 else => .unsupported,
1021 };
1022 try std.testing.expectEqual(
1023 expected,
1024 fileOwnerPolicy(),
1025 );
1026 }
1027
1028 test "file owner identity matches the effective user" {
1029 if (comptime fileOwnerPolicy() == .unsupported) {
1030 return error.SkipZigTest;
1031 }
1032 const process = @import("process/root.zig");
1033 var tmp = std.testing.tmpDir(.{});
1034 defer tmp.cleanup();
1035 const allocator = std.testing.allocator;
1036 const tmp_root = try tmp.dir.realPathFileAlloc(
1037 debug_io,
1038 ".",
1039 allocator,
1040 );
1041 defer allocator.free(tmp_root);
1042 try std.testing.expectEqual(
1043 try process.effectiveUserId(),
1044 try fileOwnerUserId(tmp_root),
1045 );
1046 try std.testing.expectError(
1047 error.BadPathName,
1048 fileOwnerUserId("invalid\x00path"),
1049 );
1050 }
1051
1052 test "appendFile appends chunks to cwd file" {
1053 var tmp = std.testing.tmpDir(.{});
1054 defer tmp.cleanup();
1055
1056 const allocator = std.testing.allocator;
1057 const tmp_root = try tmp.dir.realPathFileAlloc(debug_io, ".", allocator);
1058 defer allocator.free(tmp_root);
1059
1060 const path = try std.fs.path.join(allocator, &.{ tmp_root, "append.txt" });
1061 defer allocator.free(path);
1062
1063 try appendFile(path, &.{ "one", "\x00" });
1064 try appendFile(path, &.{ "two", "\x00" });
1065
1066 const contents = try readFileAlloc(allocator, path, 1024);
1067 defer allocator.free(contents);
1068 try std.testing.expectEqualStrings("one\x00two\x00", contents);
1069 }
1070
1071 test "appendFileSync appends chunks and syncs cwd file" {
1072 var tmp = std.testing.tmpDir(.{});
1073 defer tmp.cleanup();
1074
1075 const allocator = std.testing.allocator;
1076 const tmp_root = try tmp.dir.realPathFileAlloc(debug_io, ".", allocator);
1077 defer allocator.free(tmp_root);
1078
1079 const path = try std.fs.path.join(allocator, &.{ tmp_root, "append-sync.txt" });
1080 defer allocator.free(path);
1081
1082 try appendFileSync(path, &.{ "first", "\n" });
1083 try appendFileSync(path, &.{ "second", "\n" });
1084
1085 const contents = try readFileAlloc(allocator, path, 1024);
1086 defer allocator.free(contents);
1087 try std.testing.expectEqualStrings("first\nsecond\n", contents);
1088 }
1089
1090 test "syncFilesystem flushes the filesystem containing a live file" {
1091 var tmp = std.testing.tmpDir(.{});
1092 defer tmp.cleanup();
1093
1094 var file = try tmp.dir.createFile(debug_io, "filesystem-sync.txt", .{});
1095 defer file.close(debug_io);
1096 try file.writeStreamingAll(debug_io, "durable");
1097 try syncFilesystem(file);
1098 }
1099
1100 test "absolute path helpers access create and delete" {
1101 var tmp = std.testing.tmpDir(.{});
1102 defer tmp.cleanup();
1103
1104 const allocator = std.testing.allocator;
1105 const tmp_root = try tmp.dir.realPathFileAlloc(debug_io, ".", allocator);
1106 defer allocator.free(tmp_root);
1107
1108 const nested_dir = try std.fs.path.join(allocator, &.{ tmp_root, "nested", "child" });
1109 defer allocator.free(nested_dir);
1110 try createAbsoluteDirPath(nested_dir);
1111 try accessAbsolute(nested_dir, .{});
1112 try std.testing.expect(absolutePathExists(nested_dir));
1113
1114 const path = try std.fs.path.join(allocator, &.{ nested_dir, "remove.txt" });
1115 defer allocator.free(path);
1116 try writeFile(path, "temporary");
1117 try std.testing.expect(absolutePathExists(path));
1118 try deleteAbsoluteFile(path);
1119 try std.testing.expect(!absolutePathExists(path));
1120 }
1121
1122 test "deleteTree removes cwd directory trees" {
1123 var tmp = std.testing.tmpDir(.{});
1124 defer tmp.cleanup();
1125
1126 const allocator = std.testing.allocator;
1127 const tmp_root = try tmp.dir.realPathFileAlloc(debug_io, ".", allocator);
1128 defer allocator.free(tmp_root);
1129
1130 const nested_dir = try std.fs.path.join(allocator, &.{ tmp_root, "tree", "nested" });
1131 defer allocator.free(nested_dir);
1132 try createDirPath(nested_dir);
1133
1134 const file_path = try std.fs.path.join(allocator, &.{ nested_dir, "child.txt" });
1135 defer allocator.free(file_path);
1136 try writeFile(file_path, "child");
1137
1138 const tree_root = try std.fs.path.join(allocator, &.{ tmp_root, "tree" });
1139 defer allocator.free(tree_root);
1140 try deleteTree(tree_root);
1141 try std.testing.expect(!absolutePathExists(tree_root));
1142 }
1143
1144 test "rename replaces cwd destination" {
1145 var tmp = std.testing.tmpDir(.{});
1146 defer tmp.cleanup();
1147
1148 const allocator = std.testing.allocator;
1149 const tmp_root = try tmp.dir.realPathFileAlloc(debug_io, ".", allocator);
1150 defer allocator.free(tmp_root);
1151
1152 const source_path = try std.fs.path.join(allocator, &.{ tmp_root, "source.txt" });
1153 defer allocator.free(source_path);
1154 const dest_path = try std.fs.path.join(allocator, &.{ tmp_root, "dest.txt" });
1155 defer allocator.free(dest_path);
1156
1157 try writeFile(source_path, "replacement");
1158 try writeFile(dest_path, "old");
1159 try rename(source_path, dest_path);
1160
1161 try std.testing.expect(!exists(source_path));
1162 const contents = try readFileAlloc(allocator, dest_path, 1024);
1163 defer allocator.free(contents);
1164 try std.testing.expectEqualStrings("replacement", contents);
1165 }
1166
1167 test "listDirAlloc returns owned entries" {
1168 var tmp = std.testing.tmpDir(.{});
1169 defer tmp.cleanup();
1170
1171 const allocator = std.testing.allocator;
1172 const tmp_root = try tmp.dir.realPathFileAlloc(debug_io, ".", allocator);
1173 defer allocator.free(tmp_root);
1174
1175 const child_dir = try std.fs.path.join(allocator, &.{ tmp_root, "child" });
1176 defer allocator.free(child_dir);
1177 try createDirPath(child_dir);
1178
1179 const child_file = try std.fs.path.join(allocator, &.{ tmp_root, "file.txt" });
1180 defer allocator.free(child_file);
1181 try writeFile(child_file, "x");
1182
1183 const entries = try listDirAlloc(allocator, tmp_root);
1184 defer freeEntries(allocator, entries);
1185
1186 var saw_dir = false;
1187 var saw_file = false;
1188 for (entries) |entry| {
1189 if (std.mem.eql(u8, entry.name, "child")) {
1190 saw_dir = entry.kind == .directory;
1191 } else if (std.mem.eql(u8, entry.name, "file.txt")) {
1192 saw_file = entry.kind == .file;
1193 }
1194 }
1195
1196 try std.testing.expect(saw_dir);
1197 try std.testing.expect(saw_file);
1198 }
1199
1200 test "handle helpers write, read positionally, and read to end" {
1201 var tmp = std.testing.tmpDir(.{});
1202 defer tmp.cleanup();
1203
1204 const allocator = std.testing.allocator;
1205 var file = try tmp.dir.createFile(debug_io, "handle.txt", .{ .read = true });
1206 defer file.close(debug_io);
1207
1208 try writeHandleAll(file, "positional ");
1209 try writeHandleAll(file, "bytes");
1210
1211 var window: [10]u8 = undefined;
1212 const read_count = try readHandleAt(file, &window, 11);
1213 try std.testing.expectEqualStrings("bytes", window[0..read_count]);
1214
1215 var fresh = try tmp.dir.openFile(debug_io, "handle.txt", .{});
1216 defer fresh.close(debug_io);
1217 const all = try readHandleToEndAlloc(allocator, fresh, 1024);
1218 defer allocator.free(all);
1219 try std.testing.expectEqualStrings("positional bytes", all);
1220
1221 var limited = try tmp.dir.openFile(debug_io, "handle.txt", .{});
1222 defer limited.close(debug_io);
1223 try std.testing.expectError(error.StreamTooLong, readHandleToEndAlloc(allocator, limited, 4));
1224 }
1225
1226 test "allocated blocks report support without inferring file size" {
1227 var tmp = std.testing.tmpDir(.{});
1228 defer tmp.cleanup();
1229 var file = try tmp.dir.createFile(debug_io, "blocks.bin", .{});
1230 defer file.close(debug_io);
1231 var bytes: [4096]u8 = @splat(0xa5);
1232 try writeHandleAll(file, &bytes);
1233 try file.sync(debug_io);
1234 switch (allocatedBlocks(file)) {
1235 .supported => |blocks| {
1236 try std.testing.expectEqual(.linux, native_os);
1237 try std.testing.expect(blocks > 0);
1238 },
1239 .unsupported => |reason| switch (native_os) {
1240 .linux => try std.testing.expectEqual(
1241 AllocatedBlocksUnsupported.statx_unavailable,
1242 reason,
1243 ),
1244 else => try std.testing.expectEqual(
1245 AllocatedBlocksUnsupported.platform,
1246 reason,
1247 ),
1248 },
1249 }
1250 }