aboutsummaryrefslogtreecommitdiff
path: root/lib/std/Io/File
diff options
context:
space:
mode:
authorAndrew Kelley <andrewrk@noreply.codeberg.org>2025-12-27 14:10:46 +0100
committerAndrew Kelley <andrewrk@noreply.codeberg.org>2025-12-27 14:10:46 +0100
commite55e6b5528bb2f01de242fcf32b172e244e98e74 (patch)
tree3a5eb3193d3d192c54ab0c2b7295a7f21861c27e /lib/std/Io/File
parentc3f2de5e519926eb0029062fe8e782a6f9df9c05 (diff)
parent60a1ba0a8f3517356fa2941462f002a7f580545b (diff)
downloadzig-e55e6b5528bb2f01de242fcf32b172e244e98e74.tar.gz
zig-e55e6b5528bb2f01de242fcf32b172e244e98e74.zip
Merge pull request 'std: migrate all `fs` APIs to `Io`' (#30232) from std.Io-fs into master
Reviewed-on: https://codeberg.org/ziglang/zig/pulls/30232
Diffstat (limited to 'lib/std/Io/File')
-rw-r--r--lib/std/Io/File/Atomic.zig102
-rw-r--r--lib/std/Io/File/Reader.zig394
-rw-r--r--lib/std/Io/File/Writer.zig274
3 files changed, 770 insertions, 0 deletions
diff --git a/lib/std/Io/File/Atomic.zig b/lib/std/Io/File/Atomic.zig
new file mode 100644
index 0000000000..340303ca39
--- /dev/null
+++ b/lib/std/Io/File/Atomic.zig
@@ -0,0 +1,102 @@
+const Atomic = @This();
+
+const std = @import("../../std.zig");
+const Io = std.Io;
+const File = std.Io.File;
+const Dir = std.Io.Dir;
+const assert = std.debug.assert;
+
+file_writer: File.Writer,
+random_integer: u64,
+dest_basename: []const u8,
+file_open: bool,
+file_exists: bool,
+close_dir_on_deinit: bool,
+dir: Dir,
+
+pub const InitError = File.OpenError;
+
+/// Note that the `Dir.atomicFile` API may be more handy than this lower-level function.
+pub fn init(
+ io: Io,
+ dest_basename: []const u8,
+ permissions: File.Permissions,
+ dir: Dir,
+ close_dir_on_deinit: bool,
+ write_buffer: []u8,
+) InitError!Atomic {
+ while (true) {
+ const random_integer = std.crypto.random.int(u64);
+ const tmp_sub_path = std.fmt.hex(random_integer);
+ const file = dir.createFile(io, &tmp_sub_path, .{
+ .permissions = permissions,
+ .exclusive = true,
+ }) catch |err| switch (err) {
+ error.PathAlreadyExists => continue,
+ else => |e| return e,
+ };
+ return .{
+ .file_writer = file.writer(io, write_buffer),
+ .random_integer = random_integer,
+ .dest_basename = dest_basename,
+ .file_open = true,
+ .file_exists = true,
+ .close_dir_on_deinit = close_dir_on_deinit,
+ .dir = dir,
+ };
+ }
+}
+
+/// Always call deinit, even after a successful finish().
+pub fn deinit(af: *Atomic) void {
+ const io = af.file_writer.io;
+
+ if (af.file_open) {
+ af.file_writer.file.close(io);
+ af.file_open = false;
+ }
+ if (af.file_exists) {
+ const tmp_sub_path = std.fmt.hex(af.random_integer);
+ af.dir.deleteFile(io, &tmp_sub_path) catch {};
+ af.file_exists = false;
+ }
+ if (af.close_dir_on_deinit) {
+ af.dir.close(io);
+ }
+ af.* = undefined;
+}
+
+pub const FlushError = File.Writer.Error;
+
+pub fn flush(af: *Atomic) FlushError!void {
+ af.file_writer.interface.flush() catch |err| switch (err) {
+ error.WriteFailed => return af.file_writer.err.?,
+ };
+}
+
+pub const RenameIntoPlaceError = Dir.RenameError;
+
+/// On Windows, this function introduces a period of time where some file
+/// system operations on the destination file will result in
+/// `error.AccessDenied`, including rename operations (such as the one used in
+/// this function).
+pub fn renameIntoPlace(af: *Atomic) RenameIntoPlaceError!void {
+ const io = af.file_writer.io;
+
+ assert(af.file_exists);
+ if (af.file_open) {
+ af.file_writer.file.close(io);
+ af.file_open = false;
+ }
+ const tmp_sub_path = std.fmt.hex(af.random_integer);
+ try af.dir.rename(&tmp_sub_path, af.dir, af.dest_basename, io);
+ af.file_exists = false;
+}
+
+pub const FinishError = FlushError || RenameIntoPlaceError;
+
+/// Combination of `flush` followed by `renameIntoPlace`.
+pub fn finish(af: *Atomic) FinishError!void {
+ try af.flush();
+ try af.renameIntoPlace();
+}
diff --git a/lib/std/Io/File/Reader.zig b/lib/std/Io/File/Reader.zig
new file mode 100644
index 0000000000..0c573c9ae1
--- /dev/null
+++ b/lib/std/Io/File/Reader.zig
@@ -0,0 +1,394 @@
+//! Memoizes key information about a file handle such as:
+//! * The size from calling stat, or the error that occurred therein.
+//! * The current seek position.
+//! * The error that occurred when trying to seek.
+//! * Whether reading should be done positionally or streaming.
+//! * Whether reading should be done via fd-to-fd syscalls (e.g. `sendfile`)
+//! versus plain variants (e.g. `read`).
+//!
+//! Fulfills the `Io.Reader` interface.
+const Reader = @This();
+
+const std = @import("../../std.zig");
+const Io = std.Io;
+const File = std.Io.File;
+const assert = std.debug.assert;
+
+io: Io,
+file: File,
+err: ?Error = null,
+mode: Mode = .positional,
+/// Tracks the true seek position in the file. To obtain the logical position,
+/// use `logicalPos`.
+pos: u64 = 0,
+size: ?u64 = null,
+size_err: ?SizeError = null,
+seek_err: ?SeekError = null,
+interface: Io.Reader,
+
+pub const Error = error{
+ InputOutput,
+ SystemResources,
+ IsDir,
+ BrokenPipe,
+ ConnectionResetByPeer,
+ Timeout,
+ /// In WASI, EBADF is mapped to this error because it is returned when
+ /// trying to read a directory file descriptor as if it were a file.
+ NotOpenForReading,
+ SocketUnconnected,
+ /// Non-blocking has been enabled, and reading from the file descriptor
+ /// would block.
+ WouldBlock,
+ /// In WASI, this error occurs when the file descriptor does
+ /// not hold the required rights to read from it.
+ AccessDenied,
+ /// Unable to read file due to lock. Depending on the `Io` implementation,
+ /// reading from a locked file may return this error, or may ignore the
+ /// lock.
+ LockViolation,
+} || Io.Cancelable || Io.UnexpectedError;
+
+pub const SizeError = std.os.windows.GetFileSizeError || File.StatError || error{
+ /// Occurs if, for example, the file handle is a network socket and therefore does not have a size.
+ Streaming,
+};
+
+pub const SeekError = File.SeekError || error{
+ /// Seeking fell back to reading, and reached the end before the requested seek position.
+ /// `pos` remains at the end of the file.
+ EndOfStream,
+ /// Seeking fell back to reading, which failed.
+ ReadFailed,
+};
+
+pub const Mode = enum {
+ streaming,
+ positional,
+ /// Avoid syscalls other than `read` and `readv`.
+ streaming_simple,
+ /// Avoid syscalls other than `pread` and `preadv`.
+ positional_simple,
+ /// Indicates reading cannot continue because of a seek failure.
+ failure,
+
+ pub fn toStreaming(m: @This()) @This() {
+ return switch (m) {
+ .positional, .streaming => .streaming,
+ .positional_simple, .streaming_simple => .streaming_simple,
+ .failure => .failure,
+ };
+ }
+
+ pub fn toSimple(m: @This()) @This() {
+ return switch (m) {
+ .positional, .positional_simple => .positional_simple,
+ .streaming, .streaming_simple => .streaming_simple,
+ .failure => .failure,
+ };
+ }
+};
+
+pub fn initInterface(buffer: []u8) Io.Reader {
+ return .{
+ .vtable = &.{
+ .stream = stream,
+ .discard = discard,
+ .readVec = readVec,
+ },
+ .buffer = buffer,
+ .seek = 0,
+ .end = 0,
+ };
+}
+
+pub fn init(file: File, io: Io, buffer: []u8) Reader {
+ return .{
+ .io = io,
+ .file = file,
+ .interface = initInterface(buffer),
+ };
+}
+
+pub fn initSize(file: File, io: Io, buffer: []u8, size: ?u64) Reader {
+ return .{
+ .io = io,
+ .file = file,
+ .interface = initInterface(buffer),
+ .size = size,
+ };
+}
+
+/// Positional is more threadsafe, since the global seek position is not
+/// affected, but when such syscalls are not available, preemptively
+/// initializing in streaming mode skips a failed syscall.
+pub fn initStreaming(file: File, io: Io, buffer: []u8) Reader {
+ return .{
+ .io = io,
+ .file = file,
+ .interface = Reader.initInterface(buffer),
+ .mode = .streaming,
+ .seek_err = error.Unseekable,
+ .size_err = error.Streaming,
+ };
+}
+
+pub fn getSize(r: *Reader) SizeError!u64 {
+ return r.size orelse {
+ if (r.size_err) |err| return err;
+ if (r.file.stat(r.io)) |st| {
+ if (st.kind == .file) {
+ r.size = st.size;
+ return st.size;
+ } else {
+ r.mode = r.mode.toStreaming();
+ r.size_err = error.Streaming;
+ return error.Streaming;
+ }
+ } else |err| {
+ r.size_err = err;
+ return err;
+ }
+ };
+}
+
+pub fn seekBy(r: *Reader, offset: i64) SeekError!void {
+ const io = r.io;
+ switch (r.mode) {
+ .positional, .positional_simple => {
+ setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset));
+ },
+ .streaming, .streaming_simple => {
+ const seek_err = r.seek_err orelse e: {
+ if (io.vtable.fileSeekBy(io.userdata, r.file, offset)) |_| {
+ setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset));
+ return;
+ } else |err| {
+ r.seek_err = err;
+ break :e err;
+ }
+ };
+ var remaining = std.math.cast(u64, offset) orelse return seek_err;
+ while (remaining > 0) {
+ remaining -= discard(&r.interface, .limited64(remaining)) catch |err| {
+ r.seek_err = err;
+ return err;
+ };
+ }
+ r.interface.tossBuffered();
+ },
+ .failure => return r.seek_err.?,
+ }
+}
+
+/// Repositions logical read offset relative to the beginning of the file.
+pub fn seekTo(r: *Reader, offset: u64) SeekError!void {
+ const io = r.io;
+ switch (r.mode) {
+ .positional, .positional_simple => {
+ setLogicalPos(r, offset);
+ },
+ .streaming, .streaming_simple => {
+ const logical_pos = logicalPos(r);
+ if (offset >= logical_pos) return seekBy(r, @intCast(offset - logical_pos));
+ if (r.seek_err) |err| return err;
+ io.vtable.fileSeekTo(io.userdata, r.file, offset) catch |err| {
+ r.seek_err = err;
+ return err;
+ };
+ setLogicalPos(r, offset);
+ },
+ .failure => return r.seek_err.?,
+ }
+}
+
+pub fn logicalPos(r: *const Reader) u64 {
+ return r.pos - r.interface.bufferedLen();
+}
+
+fn setLogicalPos(r: *Reader, offset: u64) void {
+ const logical_pos = r.logicalPos();
+ if (offset < logical_pos or offset >= r.pos) {
+ r.interface.tossBuffered();
+ r.pos = offset;
+ } else r.interface.toss(@intCast(offset - logical_pos));
+}
+
+/// Number of slices to store on the stack, when trying to send as many byte
+/// vectors through the underlying read calls as possible.
+const max_buffers_len = 16;
+
+fn stream(io_reader: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize {
+ const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
+ return streamMode(r, w, limit, r.mode);
+}
+
+pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Mode) Io.Reader.StreamError!usize {
+ switch (mode) {
+ .positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) {
+ error.Unimplemented => {
+ r.mode = r.mode.toSimple();
+ return 0;
+ },
+ else => |e| return e,
+ },
+ .positional_simple => {
+ const dest = limit.slice(try w.writableSliceGreedy(1));
+ var data: [1][]u8 = .{dest};
+ const n = try readVecPositional(r, &data);
+ w.advance(n);
+ return n;
+ },
+ .streaming_simple => {
+ const dest = limit.slice(try w.writableSliceGreedy(1));
+ var data: [1][]u8 = .{dest};
+ const n = try readVecStreaming(r, &data);
+ w.advance(n);
+ return n;
+ },
+ .failure => return error.ReadFailed,
+ }
+}
+
+fn readVec(io_reader: *Io.Reader, data: [][]u8) Io.Reader.Error!usize {
+ const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
+ switch (r.mode) {
+ .positional, .positional_simple => return readVecPositional(r, data),
+ .streaming, .streaming_simple => return readVecStreaming(r, data),
+ .failure => return error.ReadFailed,
+ }
+}
+
+fn readVecPositional(r: *Reader, data: [][]u8) Io.Reader.Error!usize {
+ const io = r.io;
+ var iovecs_buffer: [max_buffers_len][]u8 = undefined;
+ const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data);
+ const dest = iovecs_buffer[0..dest_n];
+ assert(dest[0].len > 0);
+ const n = io.vtable.fileReadPositional(io.userdata, r.file, dest, r.pos) catch |err| switch (err) {
+ error.Unseekable => {
+ r.mode = r.mode.toStreaming();
+ const pos = r.pos;
+ if (pos != 0) {
+ r.pos = 0;
+ r.seekBy(@intCast(pos)) catch {
+ r.mode = .failure;
+ return error.ReadFailed;
+ };
+ }
+ return 0;
+ },
+ else => |e| {
+ r.err = e;
+ return error.ReadFailed;
+ },
+ };
+ if (n == 0) {
+ r.size = r.pos;
+ return error.EndOfStream;
+ }
+ r.pos += n;
+ if (n > data_size) {
+ r.interface.end += n - data_size;
+ return data_size;
+ }
+ return n;
+}
+
+fn readVecStreaming(r: *Reader, data: [][]u8) Io.Reader.Error!usize {
+ const io = r.io;
+ var iovecs_buffer: [max_buffers_len][]u8 = undefined;
+ const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data);
+ const dest = iovecs_buffer[0..dest_n];
+ assert(dest[0].len > 0);
+ const n = io.vtable.fileReadStreaming(io.userdata, r.file, dest) catch |err| {
+ r.err = err;
+ return error.ReadFailed;
+ };
+ if (n == 0) {
+ r.size = r.pos;
+ return error.EndOfStream;
+ }
+ r.pos += n;
+ if (n > data_size) {
+ r.interface.end += n - data_size;
+ return data_size;
+ }
+ return n;
+}
+
+fn discard(io_reader: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize {
+ const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
+ const io = r.io;
+ const file = r.file;
+ switch (r.mode) {
+ .positional, .positional_simple => {
+ const size = r.getSize() catch {
+ r.mode = r.mode.toStreaming();
+ return 0;
+ };
+ const logical_pos = logicalPos(r);
+ const delta = @min(@intFromEnum(limit), size - logical_pos);
+ setLogicalPos(r, logical_pos + delta);
+ return delta;
+ },
+ .streaming, .streaming_simple => {
+ // Unfortunately we can't seek forward without knowing the
+ // size because the seek syscalls provided to us will not
+ // return the true end position if a seek would exceed the
+ // end.
+ fallback: {
+ if (r.size_err == null and r.seek_err == null) break :fallback;
+
+ const buffered_len = r.interface.bufferedLen();
+ var remaining = @intFromEnum(limit);
+ if (remaining <= buffered_len) {
+ r.interface.seek += remaining;
+ return remaining;
+ }
+ remaining -= buffered_len;
+ r.interface.seek = 0;
+ r.interface.end = 0;
+
+ var trash_buffer: [128]u8 = undefined;
+ var data: [1][]u8 = .{trash_buffer[0..@min(trash_buffer.len, remaining)]};
+ var iovecs_buffer: [max_buffers_len][]u8 = undefined;
+ const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, &data);
+ const dest = iovecs_buffer[0..dest_n];
+ assert(dest[0].len > 0);
+ const n = io.vtable.fileReadStreaming(io.userdata, file, dest) catch |err| {
+ r.err = err;
+ return error.ReadFailed;
+ };
+ if (n == 0) {
+ r.size = r.pos;
+ return error.EndOfStream;
+ }
+ r.pos += n;
+ if (n > data_size) {
+ r.interface.end += n - data_size;
+ remaining -= data_size;
+ } else {
+ remaining -= n;
+ }
+ return @intFromEnum(limit) - remaining;
+ }
+ const size = r.getSize() catch return 0;
+ const n = @min(size - r.pos, std.math.maxInt(i64), @intFromEnum(limit));
+ io.vtable.fileSeekBy(io.userdata, file, n) catch |err| {
+ r.seek_err = err;
+ return 0;
+ };
+ r.pos += n;
+ return n;
+ },
+ .failure => return error.ReadFailed,
+ }
+}
+
+/// Returns whether the stream is at the logical end.
+pub fn atEnd(r: *Reader) bool {
+ // Even if stat fails, size is set when end is encountered.
+ const size = r.size orelse return false;
+ return size - logicalPos(r) == 0;
+}
diff --git a/lib/std/Io/File/Writer.zig b/lib/std/Io/File/Writer.zig
new file mode 100644
index 0000000000..bf8c0bf289
--- /dev/null
+++ b/lib/std/Io/File/Writer.zig
@@ -0,0 +1,274 @@
+const Writer = @This();
+const builtin = @import("builtin");
+const is_windows = builtin.os.tag == .windows;
+
+const std = @import("../../std.zig");
+const Io = std.Io;
+const File = std.Io.File;
+const assert = std.debug.assert;
+
+io: Io,
+file: File,
+err: ?Error = null,
+mode: Mode = .positional,
+/// Tracks the true seek position in the file. To obtain the logical position,
+/// use `logicalPos`.
+pos: u64 = 0,
+write_file_err: ?WriteFileError = null,
+seek_err: ?SeekError = null,
+interface: Io.Writer,
+
+pub const Mode = File.Reader.Mode;
+
+pub const Error = error{
+ DiskQuota,
+ FileTooBig,
+ InputOutput,
+ NoSpaceLeft,
+ DeviceBusy,
+ /// File descriptor does not hold the required rights to write to it.
+ AccessDenied,
+ PermissionDenied,
+ /// File is an unconnected socket, or closed its read end.
+ BrokenPipe,
+ /// Insufficient kernel memory to read from in_fd.
+ SystemResources,
+ NotOpenForWriting,
+ /// The process cannot access the file because another process has locked
+ /// a portion of the file. Windows-only.
+ LockViolation,
+ /// Non-blocking has been enabled and this operation would block.
+ WouldBlock,
+ /// This error occurs when a device gets disconnected before or mid-flush
+ /// while it's being written to - errno(6): No such device or address.
+ NoDevice,
+ FileBusy,
+} || Io.Cancelable || Io.UnexpectedError;
+
+pub const WriteFileError = Error || error{
+ /// Descriptor is not valid or locked, or an mmap(2)-like operation is not available for in_fd.
+ Unimplemented,
+ /// Can happen on FreeBSD when using copy_file_range.
+ CorruptedData,
+ EndOfStream,
+ ReadFailed,
+};
+
+pub const SeekError = Io.File.SeekError;
+
+pub fn init(file: File, io: Io, buffer: []u8) Writer {
+ return .{
+ .io = io,
+ .file = file,
+ .interface = initInterface(buffer),
+ .mode = .positional,
+ };
+}
+
+/// Positional is more threadsafe, since the global seek position is not
+/// affected, but when such syscalls are not available, preemptively
+/// initializing in streaming mode will skip a failed syscall.
+pub fn initStreaming(file: File, io: Io, buffer: []u8) Writer {
+ return .{
+ .io = io,
+ .file = file,
+ .interface = initInterface(buffer),
+ .mode = .streaming,
+ };
+}
+
+/// Detects if `file` is terminal and sets the mode accordingly.
+pub fn initDetect(file: File, io: Io, buffer: []u8) Io.Cancelable!Writer {
+ return .{
+ .io = io,
+ .file = file,
+ .interface = initInterface(buffer),
+ .mode = try .detect(io, file, true, .positional),
+ };
+}
+
+pub fn initInterface(buffer: []u8) Io.Writer {
+ return .{
+ .vtable = &.{
+ .drain = drain,
+ .sendFile = sendFile,
+ },
+ .buffer = buffer,
+ };
+}
+
+pub fn moveToReader(w: *Writer) File.Reader {
+ defer w.* = undefined;
+ return .{
+ .io = w.io,
+ .file = .{ .handle = w.file.handle },
+ .mode = w.mode,
+ .pos = w.pos,
+ .interface = File.Reader.initInterface(w.interface.buffer),
+ .seek_err = w.seek_err,
+ };
+}
+
+pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
+ const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
+ switch (w.mode) {
+ .positional, .positional_simple => return drainPositional(w, data, splat),
+ .streaming, .streaming_simple => return drainStreaming(w, data, splat),
+ .failure => return error.WriteFailed,
+ }
+}
+
+fn drainPositional(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
+ const io = w.io;
+ const header = w.interface.buffered();
+ const n = io.vtable.fileWritePositional(io.userdata, w.file, header, data, splat, w.pos) catch |err| switch (err) {
+ error.Unseekable => {
+ w.mode = w.mode.toStreaming();
+ const pos = w.pos;
+ if (pos != 0) {
+ w.pos = 0;
+ w.seekTo(@intCast(pos)) catch {
+ w.mode = .failure;
+ return error.WriteFailed;
+ };
+ }
+ return 0;
+ },
+ else => |e| {
+ w.err = e;
+ return error.WriteFailed;
+ },
+ };
+ w.pos += n;
+ return w.interface.consume(n);
+}
+
+fn drainStreaming(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
+ const io = w.io;
+ const header = w.interface.buffered();
+ const n = io.vtable.fileWriteStreaming(io.userdata, w.file, header, data, splat) catch |err| {
+ w.err = err;
+ return error.WriteFailed;
+ };
+ w.pos += n;
+ return w.interface.consume(n);
+}
+
+pub fn sendFile(io_w: *Io.Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
+ const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
+ switch (w.mode) {
+ .positional => return sendFilePositional(w, file_reader, limit),
+ .positional_simple => return error.Unimplemented,
+ .streaming => return sendFileStreaming(w, file_reader, limit),
+ .streaming_simple => return error.Unimplemented,
+ .failure => return error.WriteFailed,
+ }
+}
+
+fn sendFilePositional(w: *Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
+ const io = w.io;
+ const header = w.interface.buffered();
+ const n = io.vtable.fileWriteFilePositional(io.userdata, w.file, header, file_reader, limit, w.pos) catch |err| switch (err) {
+ error.Unseekable => {
+ w.mode = w.mode.toStreaming();
+ const pos = w.pos;
+ if (pos != 0) {
+ w.pos = 0;
+ w.seekTo(@intCast(pos)) catch {
+ w.mode = .failure;
+ return error.WriteFailed;
+ };
+ }
+ return 0;
+ },
+ error.Canceled => {
+ w.err = error.Canceled;
+ return error.WriteFailed;
+ },
+ error.EndOfStream => return error.EndOfStream,
+ error.Unimplemented => return error.Unimplemented,
+ error.ReadFailed => return error.ReadFailed,
+ else => |e| {
+ w.write_file_err = e;
+ return error.WriteFailed;
+ },
+ };
+ w.pos += n;
+ return w.interface.consume(n);
+}
+
+fn sendFileStreaming(w: *Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
+ const io = w.io;
+ const header = w.interface.buffered();
+ const n = io.vtable.fileWriteFileStreaming(io.userdata, w.file, header, file_reader, limit) catch |err| switch (err) {
+ error.Canceled => {
+ w.err = error.Canceled;
+ return error.WriteFailed;
+ },
+ error.EndOfStream => return error.EndOfStream,
+ error.Unimplemented => return error.Unimplemented,
+ error.ReadFailed => return error.ReadFailed,
+ else => |e| {
+ w.write_file_err = e;
+ return error.WriteFailed;
+ },
+ };
+ w.pos += n;
+ return w.interface.consume(n);
+}
+
+pub fn seekTo(w: *Writer, offset: u64) (SeekError || Io.Writer.Error)!void {
+ try w.interface.flush();
+ try seekToUnbuffered(w, offset);
+}
+
+pub fn logicalPos(w: *const Writer) u64 {
+ return w.pos + w.interface.end;
+}
+
+/// Asserts that no data is currently buffered.
+pub fn seekToUnbuffered(w: *Writer, offset: u64) SeekError!void {
+ assert(w.interface.buffered().len == 0);
+ const io = w.io;
+ switch (w.mode) {
+ .positional, .positional_simple => {
+ w.pos = offset;
+ },
+ .streaming, .streaming_simple => {
+ if (w.seek_err) |err| return err;
+ io.vtable.fileSeekTo(io.userdata, w.file, offset) catch |err| {
+ w.seek_err = err;
+ return err;
+ };
+ w.pos = offset;
+ },
+ .failure => return w.seek_err.?,
+ }
+}
+
+pub const EndError = File.SetLengthError || Io.Writer.Error;
+
+/// Flushes any buffered data and sets the end position of the file.
+///
+/// If not overwriting existing contents, then calling `interface.flush`
+/// directly is sufficient.
+///
+/// Flush failure is handled by setting `err` so that it can be handled
+/// along with other write failures.
+pub fn end(w: *Writer) EndError!void {
+ const io = w.io;
+ try w.interface.flush();
+ switch (w.mode) {
+ .positional,
+ .positional_simple,
+ => w.file.setLength(io, w.pos) catch |err| switch (err) {
+ error.NonResizable => return,
+ else => |e| return e,
+ },
+
+ .streaming,
+ .streaming_simple,
+ .failure,
+ => {},
+ }
+}