1const Writer = @This();
2const builtin = @import("builtin");
3const is_windows = builtin.os.tag == .windows;
4
5const std = @import("../../std.zig");
6const Io = std.Io;
7const File = std.Io.File;
8const assert = std.debug.assert;
9
10io: Io,
11file: File,
12err: ?Error = null,
13mode: Mode = .positional,
14/// Tracks the true seek position in the file. To obtain the logical position,
15/// use `logicalPos`.
16pos: u64 = 0,
17write_file_err: ?WriteFileError = null,
18seek_err: ?SeekError = null,
19interface: Io.Writer,
20
21pub const Mode = File.Reader.Mode;
22
23pub const Error = Io.Operation.FileWriteStreaming.Error || Io.Cancelable;
24
25pub const WriteFileError = Error || error{
26 /// Descriptor is not valid or locked, or an mmap(2)-like operation is not available for in_fd.
27 Unimplemented,
28 /// Can happen on FreeBSD when using copy_file_range.
29 CorruptedData,
30 EndOfStream,
31 ReadFailed,
32};
33
34pub const SeekError = Io.File.SeekError;
35
36pub fn init(file: File, io: Io, buffer: []u8) Writer {
37 return .{
38 .io = io,
39 .file = file,
40 .interface = initInterface(buffer),
41 .mode = .positional,
42 };
43}
44
45/// Positional is more threadsafe, since the global seek position is not
46/// affected, but when such syscalls are not available, preemptively
47/// initializing in streaming mode will skip a failed syscall.
48pub fn initStreaming(file: File, io: Io, buffer: []u8) Writer {
49 return .{
50 .io = io,
51 .file = file,
52 .interface = initInterface(buffer),
53 .mode = .streaming,
54 };
55}
56
57/// Detects if `file` is terminal and sets the mode accordingly.
58pub fn initDetect(file: File, io: Io, buffer: []u8) Io.Cancelable!Writer {
59 return .{
60 .io = io,
61 .file = file,
62 .interface = initInterface(buffer),
63 .mode = try .detect(io, file, true, .positional),
64 };
65}
66
67pub fn initInterface(buffer: []u8) Io.Writer {
68 return .{
69 .vtable = &.{
70 .drain = drain,
71 .sendFile = sendFile,
72 },
73 .buffer = buffer,
74 };
75}
76
77pub fn moveToReader(w: *Writer) File.Reader {
78 defer w.* = undefined;
79 return .{
80 .io = w.io,
81 .file = w.file,
82 .mode = w.mode,
83 .pos = w.pos,
84 .interface = File.Reader.initInterface(w.interface.buffer),
85 .seek_err = w.seek_err,
86 };
87}
88
89pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
90 const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
91 switch (w.mode) {
92 .positional, .positional_simple => return drainPositional(w, data, splat),
93 .streaming, .streaming_simple => return drainStreaming(w, data, splat),
94 .failure => return error.WriteFailed,
95 }
96}
97
98fn drainPositional(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
99 const io = w.io;
100 const header = w.interface.buffered();
101 const n = io.vtable.fileWritePositional(io.userdata, w.file, header, data, splat, w.pos) catch |err| switch (err) {
102 error.Unseekable => {
103 w.mode = w.mode.toStreaming();
104 const pos = w.pos;
105 if (pos != 0) {
106 w.pos = 0;
107 w.seekTo(@intCast(pos)) catch {
108 w.mode = .failure;
109 return error.WriteFailed;
110 };
111 }
112 return 0;
113 },
114 else => |e| {
115 w.err = e;
116 return error.WriteFailed;
117 },
118 };
119 w.pos += n;
120 return w.interface.consume(n);
121}
122
123fn drainStreaming(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
124 const io = w.io;
125 const header = w.interface.buffered();
126 const n = w.file.writeStreaming(io, header, data, splat) catch |err| {
127 w.err = err;
128 return error.WriteFailed;
129 };
130 w.pos += n;
131 return w.interface.consume(n);
132}
133
134pub fn sendFile(io_w: *Io.Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
135 const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
136 switch (w.mode) {
137 .positional => return sendFilePositional(w, file_reader, limit),
138 .positional_simple => return error.Unimplemented,
139 .streaming => return sendFileStreaming(w, file_reader, limit),
140 .streaming_simple => return error.Unimplemented,
141 .failure => return error.WriteFailed,
142 }
143}
144
145fn sendFilePositional(w: *Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
146 const io = w.io;
147 const header = w.interface.buffered();
148 const n = io.vtable.fileWriteFilePositional(io.userdata, w.file, header, file_reader, limit, w.pos) catch |err| switch (err) {
149 error.Unseekable => {
150 w.mode = w.mode.toStreaming();
151 const pos = w.pos;
152 if (pos != 0) {
153 w.pos = 0;
154 w.seekTo(@intCast(pos)) catch {
155 w.mode = .failure;
156 return error.WriteFailed;
157 };
158 }
159 return 0;
160 },
161 error.Canceled => {
162 w.err = error.Canceled;
163 return error.WriteFailed;
164 },
165 error.EndOfStream,
166 error.Unimplemented,
167 error.ReadFailed,
168 => |e| return e,
169 else => |e| {
170 w.write_file_err = e;
171 return error.WriteFailed;
172 },
173 };
174 w.pos += n;
175 return w.interface.consume(n);
176}
177
178fn sendFileStreaming(w: *Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
179 const io = w.io;
180 const header = w.interface.buffered();
181 const n = io.vtable.fileWriteFileStreaming(io.userdata, w.file, header, file_reader, limit) catch |err| switch (err) {
182 error.Canceled => {
183 w.err = error.Canceled;
184 return error.WriteFailed;
185 },
186 error.EndOfStream,
187 error.Unimplemented,
188 error.ReadFailed,
189 => |e| return e,
190 else => |e| {
191 w.write_file_err = e;
192 return error.WriteFailed;
193 },
194 };
195 w.pos += n;
196 return w.interface.consume(n);
197}
198
199pub fn seekTo(w: *Writer, offset: u64) (SeekError || Io.Writer.Error)!void {
200 try w.interface.flush();
201 try seekToUnbuffered(w, offset);
202}
203
204pub fn logicalPos(w: *const Writer) u64 {
205 return w.pos + w.interface.end;
206}
207
208/// Asserts that no data is currently buffered.
209pub fn seekToUnbuffered(w: *Writer, offset: u64) SeekError!void {
210 assert(w.interface.buffered().len == 0);
211 const io = w.io;
212 switch (w.mode) {
213 .positional, .positional_simple => {
214 w.pos = offset;
215 },
216 .streaming, .streaming_simple => {
217 if (w.seek_err) |err| return err;
218 io.vtable.fileSeekTo(io.userdata, w.file, offset) catch |err| {
219 w.seek_err = err;
220 return err;
221 };
222 w.pos = offset;
223 },
224 .failure => return w.seek_err.?,
225 }
226}
227
228pub const EndError = File.SetLengthError || Io.Writer.Error;
229
230/// Flushes any buffered data and sets the end position of the file.
231///
232/// If not overwriting existing contents, then calling `interface.flush`
233/// directly is sufficient.
234///
235/// Flush failure is handled by setting `err` so that it can be handled
236/// along with other write failures.
237pub fn end(w: *Writer) EndError!void {
238 const io = w.io;
239 try w.interface.flush();
240 switch (w.mode) {
241 .positional,
242 .positional_simple,
243 => w.file.setLength(io, w.pos) catch |err| switch (err) {
244 error.NonResizable => return,
245 else => |e| return e,
246 },
247
248 .streaming,
249 .streaming_simple,
250 .failure,
251 => {},
252 }
253}
254
255/// Convenience method for calling `Io.Writer.flush` and returning the
256/// underlying error.
257pub fn flush(w: *Writer) Error!void {
258 w.interface.flush() catch |err| switch (err) {
259 error.WriteFailed => return w.err.?,
260 };
261}