const Writer = @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, add the buffer size to this value. 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, InvalidArgument, /// File descriptor does not hold the required rights to write to it. AccessDenied, PermissionDenied, BrokenPipe, 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, } || Io.Cancelable || Io.UnexpectedError; pub const WriteFileError = error{ /// `out_fd` is an unconnected socket, or out_fd closed its read end. BrokenPipe, /// Descriptor is not valid or locked, or an mmap(2)-like operation is not available for in_fd. UnsupportedOperation, /// Nonblocking I/O has been selected but the write would block. WouldBlock, /// Unspecified error while reading from in_fd. InputOutput, /// Insufficient kernel memory to read from in_fd. SystemResources, } || Io.Cancelable || Io.UnexpectedError; pub const SeekError = Io.File.SeekError; pub fn init(file: File, buffer: []u8) Writer { return .{ .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, buffer: []u8) Writer { return .{ .file = file, .interface = initInterface(buffer), .mode = .streaming, }; } 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_reading => return drainPositional(w, data, splat), .streaming, .streaming_reading => 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_reading => return error.Unimplemented, .streaming => return sendFileStreaming(w, file_reader, limit), .streaming_reading => 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.fileSendFilePositional(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; }, 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.fileSendFileStreaming(io.userdata, w.file, header, file_reader, limit) catch |err| switch (err) { error.Canceled => { w.err = error.Canceled; return error.WriteFailed; }, 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); } /// 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_reading => { w.pos = offset; }, .streaming, .streaming_reading => { 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.SetEndPosError || 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 { try w.interface.flush(); switch (w.mode) { .positional, .positional_reading, => w.file.setLength(w.pos) catch |err| switch (err) { error.NonResizable => return, else => |e| return e, }, .streaming, .streaming_reading, .failure, => {}, } }