master
   1const File = @This();
   2
   3const builtin = @import("builtin");
   4const native_os = builtin.os.tag;
   5const is_windows = native_os == .windows;
   6
   7const std = @import("../std.zig");
   8const Io = std.Io;
   9const Os = std.builtin.Os;
  10const Allocator = std.mem.Allocator;
  11const posix = std.posix;
  12const math = std.math;
  13const assert = std.debug.assert;
  14const linux = std.os.linux;
  15const windows = std.os.windows;
  16const maxInt = std.math.maxInt;
  17const Alignment = std.mem.Alignment;
  18
  19/// The OS-specific file descriptor or file handle.
  20handle: Handle,
  21
  22pub const Handle = Io.File.Handle;
  23pub const Mode = Io.File.Mode;
  24pub const INode = Io.File.INode;
  25pub const Uid = posix.uid_t;
  26pub const Gid = posix.gid_t;
  27pub const Kind = Io.File.Kind;
  28
  29/// This is the default mode given to POSIX operating systems for creating
  30/// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first,
  31/// since most people would expect "-rw-r--r--", for example, when using
  32/// the `touch` command, which would correspond to `0o644`. However, POSIX
  33/// libc implementations use `0o666` inside `fopen` and then rely on the
  34/// process-scoped "umask" setting to adjust this number for file creation.
  35pub const default_mode: Mode = if (Mode == u0) 0 else 0o666;
  36
  37/// Deprecated in favor of `Io.File.OpenError`.
  38pub const OpenError = Io.File.OpenError || error{WouldBlock};
  39/// Deprecated in favor of `Io.File.OpenMode`.
  40pub const OpenMode = Io.File.OpenMode;
  41/// Deprecated in favor of `Io.File.Lock`.
  42pub const Lock = Io.File.Lock;
  43/// Deprecated in favor of `Io.File.OpenFlags`.
  44pub const OpenFlags = Io.File.OpenFlags;
  45
  46pub const CreateFlags = struct {
  47    /// Whether the file will be created with read access.
  48    read: bool = false,
  49
  50    /// If the file already exists, and is a regular file, and the access
  51    /// mode allows writing, it will be truncated to length 0.
  52    truncate: bool = true,
  53
  54    /// Ensures that this open call creates the file, otherwise causes
  55    /// `error.PathAlreadyExists` to be returned.
  56    exclusive: bool = false,
  57
  58    /// Open the file with an advisory lock to coordinate with other processes
  59    /// accessing it at the same time. An exclusive lock will prevent other
  60    /// processes from acquiring a lock. A shared lock will prevent other
  61    /// processes from acquiring a exclusive lock, but does not prevent
  62    /// other process from getting their own shared locks.
  63    ///
  64    /// The lock is advisory, except on Linux in very specific circumstances[1].
  65    /// This means that a process that does not respect the locking API can still get access
  66    /// to the file, despite the lock.
  67    ///
  68    /// On these operating systems, the lock is acquired atomically with
  69    /// opening the file:
  70    /// * Darwin
  71    /// * DragonFlyBSD
  72    /// * FreeBSD
  73    /// * Haiku
  74    /// * NetBSD
  75    /// * OpenBSD
  76    /// On these operating systems, the lock is acquired via a separate syscall
  77    /// after opening the file:
  78    /// * Linux
  79    /// * Windows
  80    ///
  81    /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt
  82    lock: Lock = .none,
  83
  84    /// Sets whether or not to wait until the file is locked to return. If set to true,
  85    /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file
  86    /// is available to proceed.
  87    lock_nonblocking: bool = false,
  88
  89    /// For POSIX systems this is the file system mode the file will
  90    /// be created with. On other systems this is always 0.
  91    mode: Mode = default_mode,
  92};
  93
  94pub fn stdout() File {
  95    return .{ .handle = if (is_windows) windows.peb().ProcessParameters.hStdOutput else posix.STDOUT_FILENO };
  96}
  97
  98pub fn stderr() File {
  99    return .{ .handle = if (is_windows) windows.peb().ProcessParameters.hStdError else posix.STDERR_FILENO };
 100}
 101
 102pub fn stdin() File {
 103    return .{ .handle = if (is_windows) windows.peb().ProcessParameters.hStdInput else posix.STDIN_FILENO };
 104}
 105
 106/// Upon success, the stream is in an uninitialized state. To continue using it,
 107/// you must use the open() function.
 108pub fn close(self: File) void {
 109    if (is_windows) {
 110        windows.CloseHandle(self.handle);
 111    } else {
 112        posix.close(self.handle);
 113    }
 114}
 115
 116pub const SyncError = posix.SyncError;
 117
 118/// Blocks until all pending file contents and metadata modifications
 119/// for the file have been synchronized with the underlying filesystem.
 120///
 121/// Note that this does not ensure that metadata for the
 122/// directory containing the file has also reached disk.
 123pub fn sync(self: File) SyncError!void {
 124    return posix.fsync(self.handle);
 125}
 126
 127/// Test whether the file refers to a terminal.
 128/// See also `getOrEnableAnsiEscapeSupport` and `supportsAnsiEscapeCodes`.
 129pub fn isTty(self: File) bool {
 130    return posix.isatty(self.handle);
 131}
 132
 133pub fn isCygwinPty(file: File) bool {
 134    if (builtin.os.tag != .windows) return false;
 135
 136    const handle = file.handle;
 137
 138    // If this is a MSYS2/cygwin pty, then it will be a named pipe with a name in one of these formats:
 139    //   msys-[...]-ptyN-[...]
 140    //   cygwin-[...]-ptyN-[...]
 141    //
 142    // Example: msys-1888ae32e00d56aa-pty0-to-master
 143
 144    // First, just check that the handle is a named pipe.
 145    // This allows us to avoid the more costly NtQueryInformationFile call
 146    // for handles that aren't named pipes.
 147    {
 148        var io_status: windows.IO_STATUS_BLOCK = undefined;
 149        var device_info: windows.FILE_FS_DEVICE_INFORMATION = undefined;
 150        const rc = windows.ntdll.NtQueryVolumeInformationFile(handle, &io_status, &device_info, @sizeOf(windows.FILE_FS_DEVICE_INFORMATION), .FileFsDeviceInformation);
 151        switch (rc) {
 152            .SUCCESS => {},
 153            else => return false,
 154        }
 155        if (device_info.DeviceType != windows.FILE_DEVICE_NAMED_PIPE) return false;
 156    }
 157
 158    const name_bytes_offset = @offsetOf(windows.FILE_NAME_INFO, "FileName");
 159    // `NAME_MAX` UTF-16 code units (2 bytes each)
 160    // This buffer may not be long enough to handle *all* possible paths
 161    // (PATH_MAX_WIDE would be necessary for that), but because we only care
 162    // about certain paths and we know they must be within a reasonable length,
 163    // we can use this smaller buffer and just return false on any error from
 164    // NtQueryInformationFile.
 165    const num_name_bytes = windows.MAX_PATH * 2;
 166    var name_info_bytes align(@alignOf(windows.FILE_NAME_INFO)) = [_]u8{0} ** (name_bytes_offset + num_name_bytes);
 167
 168    var io_status_block: windows.IO_STATUS_BLOCK = undefined;
 169    const rc = windows.ntdll.NtQueryInformationFile(handle, &io_status_block, &name_info_bytes, @intCast(name_info_bytes.len), .FileNameInformation);
 170    switch (rc) {
 171        .SUCCESS => {},
 172        .INVALID_PARAMETER => unreachable,
 173        else => return false,
 174    }
 175
 176    const name_info: *const windows.FILE_NAME_INFO = @ptrCast(&name_info_bytes);
 177    const name_bytes = name_info_bytes[name_bytes_offset .. name_bytes_offset + name_info.FileNameLength];
 178    const name_wide = std.mem.bytesAsSlice(u16, name_bytes);
 179    // The name we get from NtQueryInformationFile will be prefixed with a '\', e.g. \msys-1888ae32e00d56aa-pty0-to-master
 180    return (std.mem.startsWith(u16, name_wide, &[_]u16{ '\\', 'm', 's', 'y', 's', '-' }) or
 181        std.mem.startsWith(u16, name_wide, &[_]u16{ '\\', 'c', 'y', 'g', 'w', 'i', 'n', '-' })) and
 182        std.mem.indexOf(u16, name_wide, &[_]u16{ '-', 'p', 't', 'y' }) != null;
 183}
 184
 185/// Returns whether or not ANSI escape codes will be treated as such,
 186/// and attempts to enable support for ANSI escape codes if necessary
 187/// (on Windows).
 188///
 189/// Returns `true` if ANSI escape codes are supported or support was
 190/// successfully enabled. Returns false if ANSI escape codes are not
 191/// supported or support was unable to be enabled.
 192///
 193/// See also `supportsAnsiEscapeCodes`.
 194pub fn getOrEnableAnsiEscapeSupport(self: File) bool {
 195    if (builtin.os.tag == .windows) {
 196        var original_console_mode: windows.DWORD = 0;
 197
 198        // For Windows Terminal, VT Sequences processing is enabled by default.
 199        if (windows.kernel32.GetConsoleMode(self.handle, &original_console_mode) != 0) {
 200            if (original_console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true;
 201
 202            // For Windows Console, VT Sequences processing support was added in Windows 10 build 14361, but disabled by default.
 203            // https://devblogs.microsoft.com/commandline/tmux-support-arrives-for-bash-on-ubuntu-on-windows/
 204            //
 205            // Note: In Microsoft's example for enabling virtual terminal processing, it
 206            // shows attempting to enable `DISABLE_NEWLINE_AUTO_RETURN` as well:
 207            // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#example-of-enabling-virtual-terminal-processing
 208            // This is avoided because in the old Windows Console, that flag causes \n (as opposed to \r\n)
 209            // to behave unexpectedly (the cursor moves down 1 row but remains on the same column).
 210            // Additionally, the default console mode in Windows Terminal does not have
 211            // `DISABLE_NEWLINE_AUTO_RETURN` set, so by only enabling `ENABLE_VIRTUAL_TERMINAL_PROCESSING`
 212            // we end up matching the mode of Windows Terminal.
 213            const requested_console_modes = windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
 214            const console_mode = original_console_mode | requested_console_modes;
 215            if (windows.kernel32.SetConsoleMode(self.handle, console_mode) != 0) return true;
 216        }
 217
 218        return self.isCygwinPty();
 219    }
 220    return self.supportsAnsiEscapeCodes();
 221}
 222
 223/// Test whether ANSI escape codes will be treated as such without
 224/// attempting to enable support for ANSI escape codes.
 225///
 226/// See also `getOrEnableAnsiEscapeSupport`.
 227pub fn supportsAnsiEscapeCodes(self: File) bool {
 228    if (builtin.os.tag == .windows) {
 229        var console_mode: windows.DWORD = 0;
 230        if (windows.kernel32.GetConsoleMode(self.handle, &console_mode) != 0) {
 231            if (console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true;
 232        }
 233
 234        return self.isCygwinPty();
 235    }
 236    if (builtin.os.tag == .wasi) {
 237        // WASI sanitizes stdout when fd is a tty so ANSI escape codes
 238        // will not be interpreted as actual cursor commands, and
 239        // stderr is always sanitized.
 240        return false;
 241    }
 242    if (self.isTty()) {
 243        if (self.handle == posix.STDOUT_FILENO or self.handle == posix.STDERR_FILENO) {
 244            if (posix.getenvZ("TERM")) |term| {
 245                if (std.mem.eql(u8, term, "dumb"))
 246                    return false;
 247            }
 248        }
 249        return true;
 250    }
 251    return false;
 252}
 253
 254pub const SetEndPosError = posix.TruncateError;
 255
 256/// Shrinks or expands the file.
 257/// The file offset after this call is left unchanged.
 258pub fn setEndPos(self: File, length: u64) SetEndPosError!void {
 259    try posix.ftruncate(self.handle, length);
 260}
 261
 262pub const SeekError = posix.SeekError;
 263
 264/// Repositions read/write file offset relative to the current offset.
 265/// TODO: integrate with async I/O
 266pub fn seekBy(self: File, offset: i64) SeekError!void {
 267    return posix.lseek_CUR(self.handle, offset);
 268}
 269
 270/// Repositions read/write file offset relative to the end.
 271/// TODO: integrate with async I/O
 272pub fn seekFromEnd(self: File, offset: i64) SeekError!void {
 273    return posix.lseek_END(self.handle, offset);
 274}
 275
 276/// Repositions read/write file offset relative to the beginning.
 277/// TODO: integrate with async I/O
 278pub fn seekTo(self: File, offset: u64) SeekError!void {
 279    return posix.lseek_SET(self.handle, offset);
 280}
 281
 282pub const GetSeekPosError = posix.SeekError || StatError;
 283
 284/// TODO: integrate with async I/O
 285pub fn getPos(self: File) GetSeekPosError!u64 {
 286    return posix.lseek_CUR_get(self.handle);
 287}
 288
 289pub const GetEndPosError = std.os.windows.GetFileSizeError || StatError;
 290
 291/// TODO: integrate with async I/O
 292pub fn getEndPos(self: File) GetEndPosError!u64 {
 293    if (builtin.os.tag == .windows) {
 294        return windows.GetFileSizeEx(self.handle);
 295    }
 296    return (try self.stat()).size;
 297}
 298
 299pub const ModeError = StatError;
 300
 301/// TODO: integrate with async I/O
 302pub fn mode(self: File) ModeError!Mode {
 303    if (builtin.os.tag == .windows) {
 304        return 0;
 305    }
 306    return (try self.stat()).mode;
 307}
 308
 309pub const Stat = Io.File.Stat;
 310
 311pub const StatError = posix.FStatError;
 312
 313/// Returns `Stat` containing basic information about the `File`.
 314pub fn stat(self: File) StatError!Stat {
 315    var threaded: Io.Threaded = .init_single_threaded;
 316    const io = threaded.ioBasic();
 317    return Io.File.stat(.{ .handle = self.handle }, io);
 318}
 319
 320pub const ChmodError = posix.FChmodError;
 321
 322/// Changes the mode of the file.
 323/// The process must have the correct privileges in order to do this
 324/// successfully, or must have the effective user ID matching the owner
 325/// of the file.
 326pub fn chmod(self: File, new_mode: Mode) ChmodError!void {
 327    try posix.fchmod(self.handle, new_mode);
 328}
 329
 330pub const ChownError = posix.FChownError;
 331
 332/// Changes the owner and group of the file.
 333/// The process must have the correct privileges in order to do this
 334/// successfully. The group may be changed by the owner of the file to
 335/// any group of which the owner is a member. If the owner or group is
 336/// specified as `null`, the ID is not changed.
 337pub fn chown(self: File, owner: ?Uid, group: ?Gid) ChownError!void {
 338    try posix.fchown(self.handle, owner, group);
 339}
 340
 341/// Cross-platform representation of permissions on a file.
 342/// The `readonly` and `setReadonly` are the only methods available across all platforms.
 343/// Platform-specific functionality is available through the `inner` field.
 344pub const Permissions = struct {
 345    /// You may use the `inner` field to use platform-specific functionality
 346    inner: switch (builtin.os.tag) {
 347        .windows => PermissionsWindows,
 348        else => PermissionsUnix,
 349    },
 350
 351    const Self = @This();
 352
 353    /// Returns `true` if permissions represent an unwritable file.
 354    /// On Unix, `true` is returned only if no class has write permissions.
 355    pub fn readOnly(self: Self) bool {
 356        return self.inner.readOnly();
 357    }
 358
 359    /// Sets whether write permissions are provided.
 360    /// On Unix, this affects *all* classes. If this is undesired, use `unixSet`.
 361    /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
 362    pub fn setReadOnly(self: *Self, read_only: bool) void {
 363        self.inner.setReadOnly(read_only);
 364    }
 365};
 366
 367pub const PermissionsWindows = struct {
 368    attributes: windows.DWORD,
 369
 370    const Self = @This();
 371
 372    /// Returns `true` if permissions represent an unwritable file.
 373    pub fn readOnly(self: Self) bool {
 374        return self.attributes & windows.FILE_ATTRIBUTE_READONLY != 0;
 375    }
 376
 377    /// Sets whether write permissions are provided.
 378    /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
 379    pub fn setReadOnly(self: *Self, read_only: bool) void {
 380        if (read_only) {
 381            self.attributes |= windows.FILE_ATTRIBUTE_READONLY;
 382        } else {
 383            self.attributes &= ~@as(windows.DWORD, windows.FILE_ATTRIBUTE_READONLY);
 384        }
 385    }
 386};
 387
 388pub const PermissionsUnix = struct {
 389    mode: Mode,
 390
 391    const Self = @This();
 392
 393    /// Returns `true` if permissions represent an unwritable file.
 394    /// `true` is returned only if no class has write permissions.
 395    pub fn readOnly(self: Self) bool {
 396        return self.mode & 0o222 == 0;
 397    }
 398
 399    /// Sets whether write permissions are provided.
 400    /// This affects *all* classes. If this is undesired, use `unixSet`.
 401    /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
 402    pub fn setReadOnly(self: *Self, read_only: bool) void {
 403        if (read_only) {
 404            self.mode &= ~@as(Mode, 0o222);
 405        } else {
 406            self.mode |= @as(Mode, 0o222);
 407        }
 408    }
 409
 410    pub const Class = enum(u2) {
 411        user = 2,
 412        group = 1,
 413        other = 0,
 414    };
 415
 416    pub const Permission = enum(u3) {
 417        read = 0o4,
 418        write = 0o2,
 419        execute = 0o1,
 420    };
 421
 422    /// Returns `true` if the chosen class has the selected permission.
 423    /// This method is only available on Unix platforms.
 424    pub fn unixHas(self: Self, class: Class, permission: Permission) bool {
 425        const mask = @as(Mode, @intFromEnum(permission)) << @as(u3, @intFromEnum(class)) * 3;
 426        return self.mode & mask != 0;
 427    }
 428
 429    /// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged.
 430    /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
 431    pub fn unixSet(self: *Self, class: Class, permissions: struct {
 432        read: ?bool = null,
 433        write: ?bool = null,
 434        execute: ?bool = null,
 435    }) void {
 436        const shift = @as(u3, @intFromEnum(class)) * 3;
 437        if (permissions.read) |r| {
 438            if (r) {
 439                self.mode |= @as(Mode, 0o4) << shift;
 440            } else {
 441                self.mode &= ~(@as(Mode, 0o4) << shift);
 442            }
 443        }
 444        if (permissions.write) |w| {
 445            if (w) {
 446                self.mode |= @as(Mode, 0o2) << shift;
 447            } else {
 448                self.mode &= ~(@as(Mode, 0o2) << shift);
 449            }
 450        }
 451        if (permissions.execute) |x| {
 452            if (x) {
 453                self.mode |= @as(Mode, 0o1) << shift;
 454            } else {
 455                self.mode &= ~(@as(Mode, 0o1) << shift);
 456            }
 457        }
 458    }
 459
 460    /// Returns a `Permissions` struct representing the permissions from the passed mode.
 461    pub fn unixNew(new_mode: Mode) Self {
 462        return Self{
 463            .mode = new_mode,
 464        };
 465    }
 466};
 467
 468pub const SetPermissionsError = ChmodError;
 469
 470/// Sets permissions according to the provided `Permissions` struct.
 471/// This method is *NOT* available on WASI
 472pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void {
 473    switch (builtin.os.tag) {
 474        .windows => {
 475            var io_status_block: windows.IO_STATUS_BLOCK = undefined;
 476            var info = windows.FILE_BASIC_INFORMATION{
 477                .CreationTime = 0,
 478                .LastAccessTime = 0,
 479                .LastWriteTime = 0,
 480                .ChangeTime = 0,
 481                .FileAttributes = permissions.inner.attributes,
 482            };
 483            const rc = windows.ntdll.NtSetInformationFile(
 484                self.handle,
 485                &io_status_block,
 486                &info,
 487                @sizeOf(windows.FILE_BASIC_INFORMATION),
 488                .FileBasicInformation,
 489            );
 490            switch (rc) {
 491                .SUCCESS => return,
 492                .INVALID_HANDLE => unreachable,
 493                .ACCESS_DENIED => return error.AccessDenied,
 494                else => return windows.unexpectedStatus(rc),
 495            }
 496        },
 497        .wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod
 498        else => {
 499            try self.chmod(permissions.inner.mode);
 500        },
 501    }
 502}
 503
 504pub const UpdateTimesError = posix.FutimensError || windows.SetFileTimeError;
 505
 506/// The underlying file system may have a different granularity than nanoseconds,
 507/// and therefore this function cannot guarantee any precision will be stored.
 508/// Further, the maximum value is limited by the system ABI. When a value is provided
 509/// that exceeds this range, the value is clamped to the maximum.
 510/// TODO: integrate with async I/O
 511pub fn updateTimes(
 512    self: File,
 513    /// access timestamp in nanoseconds
 514    atime: Io.Timestamp,
 515    /// last modification timestamp in nanoseconds
 516    mtime: Io.Timestamp,
 517) UpdateTimesError!void {
 518    if (builtin.os.tag == .windows) {
 519        const atime_ft = windows.nanoSecondsToFileTime(atime);
 520        const mtime_ft = windows.nanoSecondsToFileTime(mtime);
 521        return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft);
 522    }
 523    const times = [2]posix.timespec{
 524        posix.timespec{
 525            .sec = math.cast(isize, @divFloor(atime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize),
 526            .nsec = math.cast(isize, @mod(atime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize),
 527        },
 528        posix.timespec{
 529            .sec = math.cast(isize, @divFloor(mtime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize),
 530            .nsec = math.cast(isize, @mod(mtime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize),
 531        },
 532    };
 533    try posix.futimens(self.handle, &times);
 534}
 535
 536pub const ReadError = posix.ReadError;
 537pub const PReadError = posix.PReadError;
 538
 539pub fn read(self: File, buffer: []u8) ReadError!usize {
 540    if (is_windows) {
 541        return windows.ReadFile(self.handle, buffer, null);
 542    }
 543
 544    return posix.read(self.handle, buffer);
 545}
 546
 547/// On Windows, this function currently does alter the file pointer.
 548/// https://github.com/ziglang/zig/issues/12783
 549pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize {
 550    if (is_windows) {
 551        return windows.ReadFile(self.handle, buffer, offset);
 552    }
 553
 554    return posix.pread(self.handle, buffer, offset);
 555}
 556
 557/// Deprecated in favor of `Reader`.
 558pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize {
 559    var index: usize = 0;
 560    while (index != buffer.len) {
 561        const amt = try self.pread(buffer[index..], offset + index);
 562        if (amt == 0) break;
 563        index += amt;
 564    }
 565    return index;
 566}
 567
 568/// See https://github.com/ziglang/zig/issues/7699
 569pub fn readv(self: File, iovecs: []const posix.iovec) ReadError!usize {
 570    if (is_windows) {
 571        if (iovecs.len == 0) return 0;
 572        const first = iovecs[0];
 573        return windows.ReadFile(self.handle, first.base[0..first.len], null);
 574    }
 575
 576    return posix.readv(self.handle, iovecs);
 577}
 578
 579/// See https://github.com/ziglang/zig/issues/7699
 580/// On Windows, this function currently does alter the file pointer.
 581/// https://github.com/ziglang/zig/issues/12783
 582pub fn preadv(self: File, iovecs: []const posix.iovec, offset: u64) PReadError!usize {
 583    if (is_windows) {
 584        if (iovecs.len == 0) return 0;
 585        const first = iovecs[0];
 586        return windows.ReadFile(self.handle, first.base[0..first.len], offset);
 587    }
 588
 589    return posix.preadv(self.handle, iovecs, offset);
 590}
 591
 592pub const WriteError = posix.WriteError;
 593pub const PWriteError = posix.PWriteError;
 594
 595pub fn write(self: File, bytes: []const u8) WriteError!usize {
 596    if (is_windows) {
 597        return windows.WriteFile(self.handle, bytes, null);
 598    }
 599
 600    return posix.write(self.handle, bytes);
 601}
 602
 603pub fn writeAll(self: File, bytes: []const u8) WriteError!void {
 604    var index: usize = 0;
 605    while (index < bytes.len) {
 606        index += try self.write(bytes[index..]);
 607    }
 608}
 609
 610/// Deprecated in favor of `Writer`.
 611pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void {
 612    var index: usize = 0;
 613    while (index < bytes.len) {
 614        index += try self.pwrite(bytes[index..], offset + index);
 615    }
 616}
 617
 618/// On Windows, this function currently does alter the file pointer.
 619/// https://github.com/ziglang/zig/issues/12783
 620pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize {
 621    if (is_windows) {
 622        return windows.WriteFile(self.handle, bytes, offset);
 623    }
 624
 625    return posix.pwrite(self.handle, bytes, offset);
 626}
 627
 628/// See https://github.com/ziglang/zig/issues/7699
 629pub fn writev(self: File, iovecs: []const posix.iovec_const) WriteError!usize {
 630    if (is_windows) {
 631        // TODO improve this to use WriteFileScatter
 632        if (iovecs.len == 0) return 0;
 633        const first = iovecs[0];
 634        return windows.WriteFile(self.handle, first.base[0..first.len], null);
 635    }
 636
 637    return posix.writev(self.handle, iovecs);
 638}
 639
 640/// See https://github.com/ziglang/zig/issues/7699
 641/// On Windows, this function currently does alter the file pointer.
 642/// https://github.com/ziglang/zig/issues/12783
 643pub fn pwritev(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError!usize {
 644    if (is_windows) {
 645        if (iovecs.len == 0) return 0;
 646        const first = iovecs[0];
 647        return windows.WriteFile(self.handle, first.base[0..first.len], offset);
 648    }
 649
 650    return posix.pwritev(self.handle, iovecs, offset);
 651}
 652
 653/// Deprecated in favor of `Writer`.
 654pub const CopyRangeError = posix.CopyFileRangeError;
 655
 656/// Deprecated in favor of `Writer`.
 657pub fn copyRange(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 {
 658    const adjusted_len = math.cast(usize, len) orelse maxInt(usize);
 659    const result = try posix.copy_file_range(in.handle, in_offset, out.handle, out_offset, adjusted_len, 0);
 660    return result;
 661}
 662
 663/// Deprecated in favor of `Writer`.
 664pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 {
 665    var total_bytes_copied: u64 = 0;
 666    var in_off = in_offset;
 667    var out_off = out_offset;
 668    while (total_bytes_copied < len) {
 669        const amt_copied = try copyRange(in, in_off, out, out_off, len - total_bytes_copied);
 670        if (amt_copied == 0) return total_bytes_copied;
 671        total_bytes_copied += amt_copied;
 672        in_off += amt_copied;
 673        out_off += amt_copied;
 674    }
 675    return total_bytes_copied;
 676}
 677
 678/// Deprecated in favor of `Io.File.Reader`.
 679pub const Reader = Io.File.Reader;
 680
 681pub const Writer = struct {
 682    file: File,
 683    err: ?WriteError = null,
 684    mode: Writer.Mode = .positional,
 685    /// Tracks the true seek position in the file. To obtain the logical
 686    /// position, add the buffer size to this value.
 687    pos: u64 = 0,
 688    sendfile_err: ?SendfileError = null,
 689    copy_file_range_err: ?CopyFileRangeError = null,
 690    fcopyfile_err: ?FcopyfileError = null,
 691    seek_err: ?Writer.SeekError = null,
 692    interface: Io.Writer,
 693
 694    pub const Mode = Reader.Mode;
 695
 696    pub const SendfileError = error{
 697        UnsupportedOperation,
 698        SystemResources,
 699        InputOutput,
 700        BrokenPipe,
 701        WouldBlock,
 702        Unexpected,
 703    };
 704
 705    pub const CopyFileRangeError = std.os.freebsd.CopyFileRangeError || std.os.linux.wrapped.CopyFileRangeError;
 706
 707    pub const FcopyfileError = error{
 708        OperationNotSupported,
 709        OutOfMemory,
 710        Unexpected,
 711    };
 712
 713    pub const SeekError = File.SeekError;
 714
 715    /// Number of slices to store on the stack, when trying to send as many byte
 716    /// vectors through the underlying write calls as possible.
 717    const max_buffers_len = 16;
 718
 719    pub fn init(file: File, buffer: []u8) Writer {
 720        return .{
 721            .file = file,
 722            .interface = initInterface(buffer),
 723            .mode = .positional,
 724        };
 725    }
 726
 727    /// Positional is more threadsafe, since the global seek position is not
 728    /// affected, but when such syscalls are not available, preemptively
 729    /// initializing in streaming mode will skip a failed syscall.
 730    pub fn initStreaming(file: File, buffer: []u8) Writer {
 731        return .{
 732            .file = file,
 733            .interface = initInterface(buffer),
 734            .mode = .streaming,
 735        };
 736    }
 737
 738    pub fn initInterface(buffer: []u8) Io.Writer {
 739        return .{
 740            .vtable = &.{
 741                .drain = drain,
 742                .sendFile = sendFile,
 743            },
 744            .buffer = buffer,
 745        };
 746    }
 747
 748    /// TODO when this logic moves from fs.File to Io.File the io parameter should be deleted
 749    pub fn moveToReader(w: *Writer, io: Io) Reader {
 750        defer w.* = undefined;
 751        return .{
 752            .io = io,
 753            .file = .{ .handle = w.file.handle },
 754            .mode = w.mode,
 755            .pos = w.pos,
 756            .interface = Reader.initInterface(w.interface.buffer),
 757            .seek_err = w.seek_err,
 758        };
 759    }
 760
 761    pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
 762        const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
 763        const handle = w.file.handle;
 764        const buffered = io_w.buffered();
 765        if (is_windows) switch (w.mode) {
 766            .positional, .positional_reading => {
 767                if (buffered.len != 0) {
 768                    const n = windows.WriteFile(handle, buffered, w.pos) catch |err| {
 769                        w.err = err;
 770                        return error.WriteFailed;
 771                    };
 772                    w.pos += n;
 773                    return io_w.consume(n);
 774                }
 775                for (data[0 .. data.len - 1]) |buf| {
 776                    if (buf.len == 0) continue;
 777                    const n = windows.WriteFile(handle, buf, w.pos) catch |err| {
 778                        w.err = err;
 779                        return error.WriteFailed;
 780                    };
 781                    w.pos += n;
 782                    return io_w.consume(n);
 783                }
 784                const pattern = data[data.len - 1];
 785                if (pattern.len == 0 or splat == 0) return 0;
 786                const n = windows.WriteFile(handle, pattern, w.pos) catch |err| {
 787                    w.err = err;
 788                    return error.WriteFailed;
 789                };
 790                w.pos += n;
 791                return io_w.consume(n);
 792            },
 793            .streaming, .streaming_reading => {
 794                if (buffered.len != 0) {
 795                    const n = windows.WriteFile(handle, buffered, null) catch |err| {
 796                        w.err = err;
 797                        return error.WriteFailed;
 798                    };
 799                    w.pos += n;
 800                    return io_w.consume(n);
 801                }
 802                for (data[0 .. data.len - 1]) |buf| {
 803                    if (buf.len == 0) continue;
 804                    const n = windows.WriteFile(handle, buf, null) catch |err| {
 805                        w.err = err;
 806                        return error.WriteFailed;
 807                    };
 808                    w.pos += n;
 809                    return io_w.consume(n);
 810                }
 811                const pattern = data[data.len - 1];
 812                if (pattern.len == 0 or splat == 0) return 0;
 813                const n = windows.WriteFile(handle, pattern, null) catch |err| {
 814                    w.err = err;
 815                    return error.WriteFailed;
 816                };
 817                w.pos += n;
 818                return io_w.consume(n);
 819            },
 820            .failure => return error.WriteFailed,
 821        };
 822        var iovecs: [max_buffers_len]std.posix.iovec_const = undefined;
 823        var len: usize = 0;
 824        if (buffered.len > 0) {
 825            iovecs[len] = .{ .base = buffered.ptr, .len = buffered.len };
 826            len += 1;
 827        }
 828        for (data[0 .. data.len - 1]) |d| {
 829            if (d.len == 0) continue;
 830            iovecs[len] = .{ .base = d.ptr, .len = d.len };
 831            len += 1;
 832            if (iovecs.len - len == 0) break;
 833        }
 834        const pattern = data[data.len - 1];
 835        if (iovecs.len - len != 0) switch (splat) {
 836            0 => {},
 837            1 => if (pattern.len != 0) {
 838                iovecs[len] = .{ .base = pattern.ptr, .len = pattern.len };
 839                len += 1;
 840            },
 841            else => switch (pattern.len) {
 842                0 => {},
 843                1 => {
 844                    const splat_buffer_candidate = io_w.buffer[io_w.end..];
 845                    var backup_buffer: [64]u8 = undefined;
 846                    const splat_buffer = if (splat_buffer_candidate.len >= backup_buffer.len)
 847                        splat_buffer_candidate
 848                    else
 849                        &backup_buffer;
 850                    const memset_len = @min(splat_buffer.len, splat);
 851                    const buf = splat_buffer[0..memset_len];
 852                    @memset(buf, pattern[0]);
 853                    iovecs[len] = .{ .base = buf.ptr, .len = buf.len };
 854                    len += 1;
 855                    var remaining_splat = splat - buf.len;
 856                    while (remaining_splat > splat_buffer.len and iovecs.len - len != 0) {
 857                        assert(buf.len == splat_buffer.len);
 858                        iovecs[len] = .{ .base = splat_buffer.ptr, .len = splat_buffer.len };
 859                        len += 1;
 860                        remaining_splat -= splat_buffer.len;
 861                    }
 862                    if (remaining_splat > 0 and iovecs.len - len != 0) {
 863                        iovecs[len] = .{ .base = splat_buffer.ptr, .len = remaining_splat };
 864                        len += 1;
 865                    }
 866                },
 867                else => for (0..splat) |_| {
 868                    iovecs[len] = .{ .base = pattern.ptr, .len = pattern.len };
 869                    len += 1;
 870                    if (iovecs.len - len == 0) break;
 871                },
 872            },
 873        };
 874        if (len == 0) return 0;
 875        switch (w.mode) {
 876            .positional, .positional_reading => {
 877                const n = std.posix.pwritev(handle, iovecs[0..len], w.pos) catch |err| switch (err) {
 878                    error.Unseekable => {
 879                        w.mode = w.mode.toStreaming();
 880                        const pos = w.pos;
 881                        if (pos != 0) {
 882                            w.pos = 0;
 883                            w.seekTo(@intCast(pos)) catch {
 884                                w.mode = .failure;
 885                                return error.WriteFailed;
 886                            };
 887                        }
 888                        return 0;
 889                    },
 890                    else => |e| {
 891                        w.err = e;
 892                        return error.WriteFailed;
 893                    },
 894                };
 895                w.pos += n;
 896                return io_w.consume(n);
 897            },
 898            .streaming, .streaming_reading => {
 899                const n = std.posix.writev(handle, iovecs[0..len]) catch |err| {
 900                    w.err = err;
 901                    return error.WriteFailed;
 902                };
 903                w.pos += n;
 904                return io_w.consume(n);
 905            },
 906            .failure => return error.WriteFailed,
 907        }
 908    }
 909
 910    pub fn sendFile(
 911        io_w: *Io.Writer,
 912        file_reader: *Io.File.Reader,
 913        limit: Io.Limit,
 914    ) Io.Writer.FileError!usize {
 915        const reader_buffered = file_reader.interface.buffered();
 916        if (reader_buffered.len >= @intFromEnum(limit))
 917            return sendFileBuffered(io_w, file_reader, limit.slice(reader_buffered));
 918        const writer_buffered = io_w.buffered();
 919        const file_limit = @intFromEnum(limit) - reader_buffered.len;
 920        const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
 921        const out_fd = w.file.handle;
 922        const in_fd = file_reader.file.handle;
 923
 924        if (file_reader.size) |size| {
 925            if (size - file_reader.pos == 0) {
 926                if (reader_buffered.len != 0) {
 927                    return sendFileBuffered(io_w, file_reader, reader_buffered);
 928                } else {
 929                    return error.EndOfStream;
 930                }
 931            }
 932        }
 933
 934        if (native_os == .freebsd and w.mode == .streaming) sf: {
 935            // Try using sendfile on FreeBSD.
 936            if (w.sendfile_err != null) break :sf;
 937            const offset = std.math.cast(std.c.off_t, file_reader.pos) orelse break :sf;
 938            var hdtr_data: std.c.sf_hdtr = undefined;
 939            var headers: [2]posix.iovec_const = undefined;
 940            var headers_i: u8 = 0;
 941            if (writer_buffered.len != 0) {
 942                headers[headers_i] = .{ .base = writer_buffered.ptr, .len = writer_buffered.len };
 943                headers_i += 1;
 944            }
 945            if (reader_buffered.len != 0) {
 946                headers[headers_i] = .{ .base = reader_buffered.ptr, .len = reader_buffered.len };
 947                headers_i += 1;
 948            }
 949            const hdtr: ?*std.c.sf_hdtr = if (headers_i == 0) null else b: {
 950                hdtr_data = .{
 951                    .headers = &headers,
 952                    .hdr_cnt = headers_i,
 953                    .trailers = null,
 954                    .trl_cnt = 0,
 955                };
 956                break :b &hdtr_data;
 957            };
 958            var sbytes: std.c.off_t = undefined;
 959            const nbytes: usize = @min(file_limit, maxInt(usize));
 960            const flags = 0;
 961            switch (posix.errno(std.c.sendfile(in_fd, out_fd, offset, nbytes, hdtr, &sbytes, flags))) {
 962                .SUCCESS, .INTR => {},
 963                .INVAL, .OPNOTSUPP, .NOTSOCK, .NOSYS => w.sendfile_err = error.UnsupportedOperation,
 964                .BADF => if (builtin.mode == .Debug) @panic("race condition") else {
 965                    w.sendfile_err = error.Unexpected;
 966                },
 967                .FAULT => if (builtin.mode == .Debug) @panic("segmentation fault") else {
 968                    w.sendfile_err = error.Unexpected;
 969                },
 970                .NOTCONN => w.sendfile_err = error.BrokenPipe,
 971                .AGAIN, .BUSY => if (sbytes == 0) {
 972                    w.sendfile_err = error.WouldBlock;
 973                },
 974                .IO => w.sendfile_err = error.InputOutput,
 975                .PIPE => w.sendfile_err = error.BrokenPipe,
 976                .NOBUFS => w.sendfile_err = error.SystemResources,
 977                else => |err| w.sendfile_err = posix.unexpectedErrno(err),
 978            }
 979            if (w.sendfile_err != null) {
 980                // Give calling code chance to observe the error before trying
 981                // something else.
 982                return 0;
 983            }
 984            if (sbytes == 0) {
 985                file_reader.size = file_reader.pos;
 986                return error.EndOfStream;
 987            }
 988            const consumed = io_w.consume(@intCast(sbytes));
 989            file_reader.seekBy(@intCast(consumed)) catch return error.ReadFailed;
 990            return consumed;
 991        }
 992
 993        if (native_os.isDarwin() and w.mode == .streaming) sf: {
 994            // Try using sendfile on macOS.
 995            if (w.sendfile_err != null) break :sf;
 996            const offset = std.math.cast(std.c.off_t, file_reader.pos) orelse break :sf;
 997            var hdtr_data: std.c.sf_hdtr = undefined;
 998            var headers: [2]posix.iovec_const = undefined;
 999            var headers_i: u8 = 0;
1000            if (writer_buffered.len != 0) {
1001                headers[headers_i] = .{ .base = writer_buffered.ptr, .len = writer_buffered.len };
1002                headers_i += 1;
1003            }
1004            if (reader_buffered.len != 0) {
1005                headers[headers_i] = .{ .base = reader_buffered.ptr, .len = reader_buffered.len };
1006                headers_i += 1;
1007            }
1008            const hdtr: ?*std.c.sf_hdtr = if (headers_i == 0) null else b: {
1009                hdtr_data = .{
1010                    .headers = &headers,
1011                    .hdr_cnt = headers_i,
1012                    .trailers = null,
1013                    .trl_cnt = 0,
1014                };
1015                break :b &hdtr_data;
1016            };
1017            const max_count = maxInt(i32); // Avoid EINVAL.
1018            var len: std.c.off_t = @min(file_limit, max_count);
1019            const flags = 0;
1020            switch (posix.errno(std.c.sendfile(in_fd, out_fd, offset, &len, hdtr, flags))) {
1021                .SUCCESS, .INTR => {},
1022                .OPNOTSUPP, .NOTSOCK, .NOSYS => w.sendfile_err = error.UnsupportedOperation,
1023                .BADF => if (builtin.mode == .Debug) @panic("race condition") else {
1024                    w.sendfile_err = error.Unexpected;
1025                },
1026                .FAULT => if (builtin.mode == .Debug) @panic("segmentation fault") else {
1027                    w.sendfile_err = error.Unexpected;
1028                },
1029                .INVAL => if (builtin.mode == .Debug) @panic("invalid API usage") else {
1030                    w.sendfile_err = error.Unexpected;
1031                },
1032                .NOTCONN => w.sendfile_err = error.BrokenPipe,
1033                .AGAIN => if (len == 0) {
1034                    w.sendfile_err = error.WouldBlock;
1035                },
1036                .IO => w.sendfile_err = error.InputOutput,
1037                .PIPE => w.sendfile_err = error.BrokenPipe,
1038                else => |err| w.sendfile_err = posix.unexpectedErrno(err),
1039            }
1040            if (w.sendfile_err != null) {
1041                // Give calling code chance to observe the error before trying
1042                // something else.
1043                return 0;
1044            }
1045            if (len == 0) {
1046                file_reader.size = file_reader.pos;
1047                return error.EndOfStream;
1048            }
1049            const consumed = io_w.consume(@bitCast(len));
1050            file_reader.seekBy(@intCast(consumed)) catch return error.ReadFailed;
1051            return consumed;
1052        }
1053
1054        if (native_os == .linux and w.mode == .streaming) sf: {
1055            // Try using sendfile on Linux.
1056            if (w.sendfile_err != null) break :sf;
1057            // Linux sendfile does not support headers.
1058            if (writer_buffered.len != 0 or reader_buffered.len != 0)
1059                return sendFileBuffered(io_w, file_reader, reader_buffered);
1060            const max_count = 0x7ffff000; // Avoid EINVAL.
1061            var off: std.os.linux.off_t = undefined;
1062            const off_ptr: ?*std.os.linux.off_t, const count: usize = switch (file_reader.mode) {
1063                .positional => o: {
1064                    const size = file_reader.getSize() catch return 0;
1065                    off = std.math.cast(std.os.linux.off_t, file_reader.pos) orelse return error.ReadFailed;
1066                    break :o .{ &off, @min(@intFromEnum(limit), size - file_reader.pos, max_count) };
1067                },
1068                .streaming => .{ null, limit.minInt(max_count) },
1069                .streaming_reading, .positional_reading => break :sf,
1070                .failure => return error.ReadFailed,
1071            };
1072            const n = std.os.linux.wrapped.sendfile(out_fd, in_fd, off_ptr, count) catch |err| switch (err) {
1073                error.Unseekable => {
1074                    file_reader.mode = file_reader.mode.toStreaming();
1075                    const pos = file_reader.pos;
1076                    if (pos != 0) {
1077                        file_reader.pos = 0;
1078                        file_reader.seekBy(@intCast(pos)) catch {
1079                            file_reader.mode = .failure;
1080                            return error.ReadFailed;
1081                        };
1082                    }
1083                    return 0;
1084                },
1085                else => |e| {
1086                    w.sendfile_err = e;
1087                    return 0;
1088                },
1089            };
1090            if (n == 0) {
1091                file_reader.size = file_reader.pos;
1092                return error.EndOfStream;
1093            }
1094            file_reader.pos += n;
1095            w.pos += n;
1096            return n;
1097        }
1098
1099        const copy_file_range = switch (native_os) {
1100            .freebsd => std.os.freebsd.copy_file_range,
1101            .linux => std.os.linux.wrapped.copy_file_range,
1102            else => {},
1103        };
1104        if (@TypeOf(copy_file_range) != void) cfr: {
1105            if (w.copy_file_range_err != null) break :cfr;
1106            if (writer_buffered.len != 0 or reader_buffered.len != 0)
1107                return sendFileBuffered(io_w, file_reader, reader_buffered);
1108            var off_in: i64 = undefined;
1109            var off_out: i64 = undefined;
1110            const off_in_ptr: ?*i64 = switch (file_reader.mode) {
1111                .positional_reading, .streaming_reading => return error.Unimplemented,
1112                .positional => p: {
1113                    off_in = @intCast(file_reader.pos);
1114                    break :p &off_in;
1115                },
1116                .streaming => null,
1117                .failure => return error.WriteFailed,
1118            };
1119            const off_out_ptr: ?*i64 = switch (w.mode) {
1120                .positional_reading, .streaming_reading => return error.Unimplemented,
1121                .positional => p: {
1122                    off_out = @intCast(w.pos);
1123                    break :p &off_out;
1124                },
1125                .streaming => null,
1126                .failure => return error.WriteFailed,
1127            };
1128            const n = copy_file_range(in_fd, off_in_ptr, out_fd, off_out_ptr, @intFromEnum(limit), 0) catch |err| {
1129                w.copy_file_range_err = err;
1130                return 0;
1131            };
1132            if (n == 0) {
1133                file_reader.size = file_reader.pos;
1134                return error.EndOfStream;
1135            }
1136            file_reader.pos += n;
1137            w.pos += n;
1138            return n;
1139        }
1140
1141        if (builtin.os.tag.isDarwin()) fcf: {
1142            if (w.fcopyfile_err != null) break :fcf;
1143            if (file_reader.pos != 0) break :fcf;
1144            if (w.pos != 0) break :fcf;
1145            if (limit != .unlimited) break :fcf;
1146            const size = file_reader.getSize() catch break :fcf;
1147            if (writer_buffered.len != 0 or reader_buffered.len != 0)
1148                return sendFileBuffered(io_w, file_reader, reader_buffered);
1149            const rc = std.c.fcopyfile(in_fd, out_fd, null, .{ .DATA = true });
1150            switch (posix.errno(rc)) {
1151                .SUCCESS => {},
1152                .INVAL => if (builtin.mode == .Debug) @panic("invalid API usage") else {
1153                    w.fcopyfile_err = error.Unexpected;
1154                    return 0;
1155                },
1156                .NOMEM => {
1157                    w.fcopyfile_err = error.OutOfMemory;
1158                    return 0;
1159                },
1160                .OPNOTSUPP => {
1161                    w.fcopyfile_err = error.OperationNotSupported;
1162                    return 0;
1163                },
1164                else => |err| {
1165                    w.fcopyfile_err = posix.unexpectedErrno(err);
1166                    return 0;
1167                },
1168            }
1169            file_reader.pos = size;
1170            w.pos = size;
1171            return size;
1172        }
1173
1174        return error.Unimplemented;
1175    }
1176
1177    fn sendFileBuffered(
1178        io_w: *Io.Writer,
1179        file_reader: *Io.File.Reader,
1180        reader_buffered: []const u8,
1181    ) Io.Writer.FileError!usize {
1182        const n = try drain(io_w, &.{reader_buffered}, 1);
1183        file_reader.seekBy(@intCast(n)) catch return error.ReadFailed;
1184        return n;
1185    }
1186
1187    pub fn seekTo(w: *Writer, offset: u64) (Writer.SeekError || Io.Writer.Error)!void {
1188        try w.interface.flush();
1189        try seekToUnbuffered(w, offset);
1190    }
1191
1192    /// Asserts that no data is currently buffered.
1193    pub fn seekToUnbuffered(w: *Writer, offset: u64) Writer.SeekError!void {
1194        assert(w.interface.buffered().len == 0);
1195        switch (w.mode) {
1196            .positional, .positional_reading => {
1197                w.pos = offset;
1198            },
1199            .streaming, .streaming_reading => {
1200                if (w.seek_err) |err| return err;
1201                posix.lseek_SET(w.file.handle, offset) catch |err| {
1202                    w.seek_err = err;
1203                    return err;
1204                };
1205                w.pos = offset;
1206            },
1207            .failure => return w.seek_err.?,
1208        }
1209    }
1210
1211    pub const EndError = SetEndPosError || Io.Writer.Error;
1212
1213    /// Flushes any buffered data and sets the end position of the file.
1214    ///
1215    /// If not overwriting existing contents, then calling `interface.flush`
1216    /// directly is sufficient.
1217    ///
1218    /// Flush failure is handled by setting `err` so that it can be handled
1219    /// along with other write failures.
1220    pub fn end(w: *Writer) EndError!void {
1221        try w.interface.flush();
1222        switch (w.mode) {
1223            .positional,
1224            .positional_reading,
1225            => w.file.setEndPos(w.pos) catch |err| switch (err) {
1226                error.NonResizable => return,
1227                else => |e| return e,
1228            },
1229
1230            .streaming,
1231            .streaming_reading,
1232            .failure,
1233            => {},
1234        }
1235    }
1236};
1237
1238/// Defaults to positional reading; falls back to streaming.
1239///
1240/// Positional is more threadsafe, since the global seek position is not
1241/// affected.
1242pub fn reader(file: File, io: Io, buffer: []u8) Reader {
1243    return .init(.{ .handle = file.handle }, io, buffer);
1244}
1245
1246/// Positional is more threadsafe, since the global seek position is not
1247/// affected, but when such syscalls are not available, preemptively
1248/// initializing in streaming mode skips a failed syscall.
1249pub fn readerStreaming(file: File, io: Io, buffer: []u8) Reader {
1250    return .initStreaming(.{ .handle = file.handle }, io, buffer);
1251}
1252
1253/// Defaults to positional reading; falls back to streaming.
1254///
1255/// Positional is more threadsafe, since the global seek position is not
1256/// affected.
1257pub fn writer(file: File, buffer: []u8) Writer {
1258    return .init(file, buffer);
1259}
1260
1261/// Positional is more threadsafe, since the global seek position is not
1262/// affected, but when such syscalls are not available, preemptively
1263/// initializing in streaming mode will skip a failed syscall.
1264pub fn writerStreaming(file: File, buffer: []u8) Writer {
1265    return .initStreaming(file, buffer);
1266}
1267
1268const range_off: windows.LARGE_INTEGER = 0;
1269const range_len: windows.LARGE_INTEGER = 1;
1270
1271pub const LockError = error{
1272    SystemResources,
1273    FileLocksNotSupported,
1274} || posix.UnexpectedError;
1275
1276/// Blocks when an incompatible lock is held by another process.
1277/// A process may hold only one type of lock (shared or exclusive) on
1278/// a file. When a process terminates in any way, the lock is released.
1279///
1280/// Assumes the file is unlocked.
1281///
1282/// TODO: integrate with async I/O
1283pub fn lock(file: File, l: Lock) LockError!void {
1284    if (is_windows) {
1285        var io_status_block: windows.IO_STATUS_BLOCK = undefined;
1286        const exclusive = switch (l) {
1287            .none => return,
1288            .shared => false,
1289            .exclusive => true,
1290        };
1291        return windows.LockFile(
1292            file.handle,
1293            null,
1294            null,
1295            null,
1296            &io_status_block,
1297            &range_off,
1298            &range_len,
1299            null,
1300            windows.FALSE, // non-blocking=false
1301            @intFromBool(exclusive),
1302        ) catch |err| switch (err) {
1303            error.WouldBlock => unreachable, // non-blocking=false
1304            else => |e| return e,
1305        };
1306    } else {
1307        return posix.flock(file.handle, switch (l) {
1308            .none => posix.LOCK.UN,
1309            .shared => posix.LOCK.SH,
1310            .exclusive => posix.LOCK.EX,
1311        }) catch |err| switch (err) {
1312            error.WouldBlock => unreachable, // non-blocking=false
1313            else => |e| return e,
1314        };
1315    }
1316}
1317
1318/// Assumes the file is locked.
1319pub fn unlock(file: File) void {
1320    if (is_windows) {
1321        var io_status_block: windows.IO_STATUS_BLOCK = undefined;
1322        return windows.UnlockFile(
1323            file.handle,
1324            &io_status_block,
1325            &range_off,
1326            &range_len,
1327            null,
1328        ) catch |err| switch (err) {
1329            error.RangeNotLocked => unreachable, // Function assumes unlocked.
1330            error.Unexpected => unreachable, // Resource deallocation must succeed.
1331        };
1332    } else {
1333        return posix.flock(file.handle, posix.LOCK.UN) catch |err| switch (err) {
1334            error.WouldBlock => unreachable, // unlocking can't block
1335            error.SystemResources => unreachable, // We are deallocating resources.
1336            error.FileLocksNotSupported => unreachable, // We already got the lock.
1337            error.Unexpected => unreachable, // Resource deallocation must succeed.
1338        };
1339    }
1340}
1341
1342/// Attempts to obtain a lock, returning `true` if the lock is
1343/// obtained, and `false` if there was an existing incompatible lock held.
1344/// A process may hold only one type of lock (shared or exclusive) on
1345/// a file. When a process terminates in any way, the lock is released.
1346///
1347/// Assumes the file is unlocked.
1348///
1349/// TODO: integrate with async I/O
1350pub fn tryLock(file: File, l: Lock) LockError!bool {
1351    if (is_windows) {
1352        var io_status_block: windows.IO_STATUS_BLOCK = undefined;
1353        const exclusive = switch (l) {
1354            .none => return,
1355            .shared => false,
1356            .exclusive => true,
1357        };
1358        windows.LockFile(
1359            file.handle,
1360            null,
1361            null,
1362            null,
1363            &io_status_block,
1364            &range_off,
1365            &range_len,
1366            null,
1367            windows.TRUE, // non-blocking=true
1368            @intFromBool(exclusive),
1369        ) catch |err| switch (err) {
1370            error.WouldBlock => return false,
1371            else => |e| return e,
1372        };
1373    } else {
1374        posix.flock(file.handle, switch (l) {
1375            .none => posix.LOCK.UN,
1376            .shared => posix.LOCK.SH | posix.LOCK.NB,
1377            .exclusive => posix.LOCK.EX | posix.LOCK.NB,
1378        }) catch |err| switch (err) {
1379            error.WouldBlock => return false,
1380            else => |e| return e,
1381        };
1382    }
1383    return true;
1384}
1385
1386/// Assumes the file is already locked in exclusive mode.
1387/// Atomically modifies the lock to be in shared mode, without releasing it.
1388///
1389/// TODO: integrate with async I/O
1390pub fn downgradeLock(file: File) LockError!void {
1391    if (is_windows) {
1392        // On Windows it works like a semaphore + exclusivity flag. To implement this
1393        // function, we first obtain another lock in shared mode. This changes the
1394        // exclusivity flag, but increments the semaphore to 2. So we follow up with
1395        // an NtUnlockFile which decrements the semaphore but does not modify the
1396        // exclusivity flag.
1397        var io_status_block: windows.IO_STATUS_BLOCK = undefined;
1398        windows.LockFile(
1399            file.handle,
1400            null,
1401            null,
1402            null,
1403            &io_status_block,
1404            &range_off,
1405            &range_len,
1406            null,
1407            windows.TRUE, // non-blocking=true
1408            windows.FALSE, // exclusive=false
1409        ) catch |err| switch (err) {
1410            error.WouldBlock => unreachable, // File was not locked in exclusive mode.
1411            else => |e| return e,
1412        };
1413        return windows.UnlockFile(
1414            file.handle,
1415            &io_status_block,
1416            &range_off,
1417            &range_len,
1418            null,
1419        ) catch |err| switch (err) {
1420            error.RangeNotLocked => unreachable, // File was not locked.
1421            error.Unexpected => unreachable, // Resource deallocation must succeed.
1422        };
1423    } else {
1424        return posix.flock(file.handle, posix.LOCK.SH | posix.LOCK.NB) catch |err| switch (err) {
1425            error.WouldBlock => unreachable, // File was not locked in exclusive mode.
1426            else => |e| return e,
1427        };
1428    }
1429}
1430
1431pub fn adaptToNewApi(file: File) Io.File {
1432    return .{ .handle = file.handle };
1433}
1434
1435pub fn adaptFromNewApi(file: Io.File) File {
1436    return .{ .handle = file.handle };
1437}