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 }