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 assert = std.debug.assert;
10
11handle: Handle,
12
13pub const Handle = std.posix.fd_t;
14pub const Mode = std.posix.mode_t;
15pub const INode = std.posix.ino_t;
16
17pub const Kind = enum {
18 block_device,
19 character_device,
20 directory,
21 named_pipe,
22 sym_link,
23 file,
24 unix_domain_socket,
25 whiteout,
26 door,
27 event_port,
28 unknown,
29};
30
31pub const Stat = struct {
32 /// A number that the system uses to point to the file metadata. This
33 /// number is not guaranteed to be unique across time, as some file
34 /// systems may reuse an inode after its file has been deleted. Some
35 /// systems may change the inode of a file over time.
36 ///
37 /// On Linux, the inode is a structure that stores the metadata, and
38 /// the inode _number_ is what you see here: the index number of the
39 /// inode.
40 ///
41 /// The FileIndex on Windows is similar. It is a number for a file that
42 /// is unique to each filesystem.
43 inode: INode,
44 size: u64,
45 /// This is available on POSIX systems and is always 0 otherwise.
46 mode: Mode,
47 kind: Kind,
48 /// Last access time in nanoseconds, relative to UTC 1970-01-01.
49 atime: Io.Timestamp,
50 /// Last modification time in nanoseconds, relative to UTC 1970-01-01.
51 mtime: Io.Timestamp,
52 /// Last status/metadata change time in nanoseconds, relative to UTC 1970-01-01.
53 ctime: Io.Timestamp,
54};
55
56pub fn stdout() File {
57 return .{ .handle = if (is_windows) std.os.windows.peb().ProcessParameters.hStdOutput else std.posix.STDOUT_FILENO };
58}
59
60pub fn stderr() File {
61 return .{ .handle = if (is_windows) std.os.windows.peb().ProcessParameters.hStdError else std.posix.STDERR_FILENO };
62}
63
64pub fn stdin() File {
65 return .{ .handle = if (is_windows) std.os.windows.peb().ProcessParameters.hStdInput else std.posix.STDIN_FILENO };
66}
67
68pub const StatError = error{
69 SystemResources,
70 /// In WASI, this error may occur when the file descriptor does
71 /// not hold the required rights to get its filestat information.
72 AccessDenied,
73 PermissionDenied,
74 /// Attempted to stat a non-file stream.
75 Streaming,
76} || Io.Cancelable || Io.UnexpectedError;
77
78/// Returns `Stat` containing basic information about the `File`.
79pub fn stat(file: File, io: Io) StatError!Stat {
80 return io.vtable.fileStat(io.userdata, file);
81}
82
83pub const OpenMode = enum {
84 read_only,
85 write_only,
86 read_write,
87};
88
89pub const Lock = enum {
90 none,
91 shared,
92 exclusive,
93};
94
95pub const OpenFlags = struct {
96 mode: OpenMode = .read_only,
97
98 /// Open the file with an advisory lock to coordinate with other processes
99 /// accessing it at the same time. An exclusive lock will prevent other
100 /// processes from acquiring a lock. A shared lock will prevent other
101 /// processes from acquiring a exclusive lock, but does not prevent
102 /// other process from getting their own shared locks.
103 ///
104 /// The lock is advisory, except on Linux in very specific circumstances[1].
105 /// This means that a process that does not respect the locking API can still get access
106 /// to the file, despite the lock.
107 ///
108 /// On these operating systems, the lock is acquired atomically with
109 /// opening the file:
110 /// * Darwin
111 /// * DragonFlyBSD
112 /// * FreeBSD
113 /// * Haiku
114 /// * NetBSD
115 /// * OpenBSD
116 /// On these operating systems, the lock is acquired via a separate syscall
117 /// after opening the file:
118 /// * Linux
119 /// * Windows
120 ///
121 /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt
122 lock: Lock = .none,
123
124 /// Sets whether or not to wait until the file is locked to return. If set to true,
125 /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file
126 /// is available to proceed.
127 lock_nonblocking: bool = false,
128
129 /// Set this to allow the opened file to automatically become the
130 /// controlling TTY for the current process.
131 allow_ctty: bool = false,
132
133 follow_symlinks: bool = true,
134
135 pub fn isRead(self: OpenFlags) bool {
136 return self.mode != .write_only;
137 }
138
139 pub fn isWrite(self: OpenFlags) bool {
140 return self.mode != .read_only;
141 }
142};
143
144pub const CreateFlags = std.fs.File.CreateFlags;
145
146pub const OpenError = error{
147 SharingViolation,
148 PipeBusy,
149 NoDevice,
150 /// On Windows, `\\server` or `\\server\share` was not found.
151 NetworkNotFound,
152 ProcessNotFound,
153 /// On Windows, antivirus software is enabled by default. It can be
154 /// disabled, but Windows Update sometimes ignores the user's preference
155 /// and re-enables it. When enabled, antivirus software on Windows
156 /// intercepts file system operations and makes them significantly slower
157 /// in addition to possibly failing with this error code.
158 AntivirusInterference,
159 /// In WASI, this error may occur when the file descriptor does
160 /// not hold the required rights to open a new resource relative to it.
161 AccessDenied,
162 PermissionDenied,
163 SymLinkLoop,
164 ProcessFdQuotaExceeded,
165 SystemFdQuotaExceeded,
166 /// Either:
167 /// * One of the path components does not exist.
168 /// * Cwd was used, but cwd has been deleted.
169 /// * The path associated with the open directory handle has been deleted.
170 /// * On macOS, multiple processes or threads raced to create the same file
171 /// with `O.EXCL` set to `false`.
172 FileNotFound,
173 /// The path exceeded `max_path_bytes` bytes.
174 /// Insufficient kernel memory was available, or
175 /// the named file is a FIFO and per-user hard limit on
176 /// memory allocation for pipes has been reached.
177 SystemResources,
178 /// The file is too large to be opened. This error is unreachable
179 /// for 64-bit targets, as well as when opening directories.
180 FileTooBig,
181 /// The path refers to directory but the `DIRECTORY` flag was not provided.
182 IsDir,
183 /// A new path cannot be created because the device has no room for the new file.
184 /// This error is only reachable when the `CREAT` flag is provided.
185 NoSpaceLeft,
186 /// A component used as a directory in the path was not, in fact, a directory, or
187 /// `DIRECTORY` was specified and the path was not a directory.
188 NotDir,
189 /// The path already exists and the `CREAT` and `EXCL` flags were provided.
190 PathAlreadyExists,
191 DeviceBusy,
192 FileLocksNotSupported,
193 /// One of these three things:
194 /// * pathname refers to an executable image which is currently being
195 /// executed and write access was requested.
196 /// * pathname refers to a file that is currently in use as a swap
197 /// file, and the O_TRUNC flag was specified.
198 /// * pathname refers to a file that is currently being read by the
199 /// kernel (e.g., for module/firmware loading), and write access was
200 /// requested.
201 FileBusy,
202 /// Non-blocking was requested and the operation cannot return immediately.
203 WouldBlock,
204} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
205
206pub fn close(file: File, io: Io) void {
207 return io.vtable.fileClose(io.userdata, file);
208}
209
210pub const OpenSelfExeError = OpenError || std.fs.SelfExePathError || std.posix.FlockError;
211
212pub fn openSelfExe(io: Io, flags: OpenFlags) OpenSelfExeError!File {
213 return io.vtable.openSelfExe(io.userdata, flags);
214}
215
216pub const ReadPositionalError = Reader.Error || error{Unseekable};
217
218pub fn readPositional(file: File, io: Io, buffer: [][]u8, offset: u64) ReadPositionalError!usize {
219 return io.vtable.fileReadPositional(io.userdata, file, buffer, offset);
220}
221
222pub const WriteStreamingError = error{} || Io.UnexpectedError || Io.Cancelable;
223
224pub fn writeStreaming(file: File, io: Io, buffer: [][]const u8) WriteStreamingError!usize {
225 return file.fileWriteStreaming(io, buffer);
226}
227
228pub const WritePositionalError = WriteStreamingError || error{Unseekable};
229
230pub fn writePositional(file: File, io: Io, buffer: [][]const u8, offset: u64) WritePositionalError!usize {
231 return io.vtable.fileWritePositional(io.userdata, file, buffer, offset);
232}
233
234pub fn openAbsolute(io: Io, absolute_path: []const u8, flags: OpenFlags) OpenError!File {
235 assert(std.fs.path.isAbsolute(absolute_path));
236 return Io.Dir.cwd().openFile(io, absolute_path, flags);
237}
238
239/// Defaults to positional reading; falls back to streaming.
240///
241/// Positional is more threadsafe, since the global seek position is not
242/// affected.
243pub fn reader(file: File, io: Io, buffer: []u8) Reader {
244 return .init(file, io, buffer);
245}
246
247/// Positional is more threadsafe, since the global seek position is not
248/// affected, but when such syscalls are not available, preemptively
249/// initializing in streaming mode skips a failed syscall.
250pub fn readerStreaming(file: File, io: Io, buffer: []u8) Reader {
251 return .initStreaming(file, io, buffer);
252}
253
254pub const SeekError = error{
255 Unseekable,
256 /// The file descriptor does not hold the required rights to seek on it.
257 AccessDenied,
258} || Io.Cancelable || Io.UnexpectedError;
259
260/// Memoizes key information about a file handle such as:
261/// * The size from calling stat, or the error that occurred therein.
262/// * The current seek position.
263/// * The error that occurred when trying to seek.
264/// * Whether reading should be done positionally or streaming.
265/// * Whether reading should be done via fd-to-fd syscalls (e.g. `sendfile`)
266/// versus plain variants (e.g. `read`).
267///
268/// Fulfills the `Io.Reader` interface.
269pub const Reader = struct {
270 io: Io,
271 file: File,
272 err: ?Error = null,
273 mode: Reader.Mode = .positional,
274 /// Tracks the true seek position in the file. To obtain the logical
275 /// position, use `logicalPos`.
276 pos: u64 = 0,
277 size: ?u64 = null,
278 size_err: ?SizeError = null,
279 seek_err: ?Reader.SeekError = null,
280 interface: Io.Reader,
281
282 pub const Error = error{
283 InputOutput,
284 SystemResources,
285 IsDir,
286 BrokenPipe,
287 ConnectionResetByPeer,
288 Timeout,
289 /// In WASI, EBADF is mapped to this error because it is returned when
290 /// trying to read a directory file descriptor as if it were a file.
291 NotOpenForReading,
292 SocketUnconnected,
293 /// This error occurs when no global event loop is configured,
294 /// and reading from the file descriptor would block.
295 WouldBlock,
296 /// In WASI, this error occurs when the file descriptor does
297 /// not hold the required rights to read from it.
298 AccessDenied,
299 /// This error occurs in Linux if the process to be read from
300 /// no longer exists.
301 ProcessNotFound,
302 /// Unable to read file due to lock.
303 LockViolation,
304 } || Io.Cancelable || Io.UnexpectedError;
305
306 pub const SizeError = std.os.windows.GetFileSizeError || StatError || error{
307 /// Occurs if, for example, the file handle is a network socket and therefore does not have a size.
308 Streaming,
309 };
310
311 pub const SeekError = File.SeekError || error{
312 /// Seeking fell back to reading, and reached the end before the requested seek position.
313 /// `pos` remains at the end of the file.
314 EndOfStream,
315 /// Seeking fell back to reading, which failed.
316 ReadFailed,
317 };
318
319 pub const Mode = enum {
320 streaming,
321 positional,
322 /// Avoid syscalls other than `read` and `readv`.
323 streaming_reading,
324 /// Avoid syscalls other than `pread` and `preadv`.
325 positional_reading,
326 /// Indicates reading cannot continue because of a seek failure.
327 failure,
328
329 pub fn toStreaming(m: @This()) @This() {
330 return switch (m) {
331 .positional, .streaming => .streaming,
332 .positional_reading, .streaming_reading => .streaming_reading,
333 .failure => .failure,
334 };
335 }
336
337 pub fn toReading(m: @This()) @This() {
338 return switch (m) {
339 .positional, .positional_reading => .positional_reading,
340 .streaming, .streaming_reading => .streaming_reading,
341 .failure => .failure,
342 };
343 }
344 };
345
346 pub fn initInterface(buffer: []u8) Io.Reader {
347 return .{
348 .vtable = &.{
349 .stream = Reader.stream,
350 .discard = Reader.discard,
351 .readVec = Reader.readVec,
352 },
353 .buffer = buffer,
354 .seek = 0,
355 .end = 0,
356 };
357 }
358
359 pub fn init(file: File, io: Io, buffer: []u8) Reader {
360 return .{
361 .io = io,
362 .file = file,
363 .interface = initInterface(buffer),
364 };
365 }
366
367 /// Takes a legacy `std.fs.File` to help with upgrading.
368 pub fn initAdapted(file: std.fs.File, io: Io, buffer: []u8) Reader {
369 return .init(.{ .handle = file.handle }, io, buffer);
370 }
371
372 pub fn initSize(file: File, io: Io, buffer: []u8, size: ?u64) Reader {
373 return .{
374 .io = io,
375 .file = file,
376 .interface = initInterface(buffer),
377 .size = size,
378 };
379 }
380
381 /// Positional is more threadsafe, since the global seek position is not
382 /// affected, but when such syscalls are not available, preemptively
383 /// initializing in streaming mode skips a failed syscall.
384 pub fn initStreaming(file: File, io: Io, buffer: []u8) Reader {
385 return .{
386 .io = io,
387 .file = file,
388 .interface = Reader.initInterface(buffer),
389 .mode = .streaming,
390 .seek_err = error.Unseekable,
391 .size_err = error.Streaming,
392 };
393 }
394
395 pub fn getSize(r: *Reader) SizeError!u64 {
396 return r.size orelse {
397 if (r.size_err) |err| return err;
398 if (stat(r.file, r.io)) |st| {
399 if (st.kind == .file) {
400 r.size = st.size;
401 return st.size;
402 } else {
403 r.mode = r.mode.toStreaming();
404 r.size_err = error.Streaming;
405 return error.Streaming;
406 }
407 } else |err| {
408 r.size_err = err;
409 return err;
410 }
411 };
412 }
413
414 pub fn seekBy(r: *Reader, offset: i64) Reader.SeekError!void {
415 const io = r.io;
416 switch (r.mode) {
417 .positional, .positional_reading => {
418 setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset));
419 },
420 .streaming, .streaming_reading => {
421 const seek_err = r.seek_err orelse e: {
422 if (io.vtable.fileSeekBy(io.userdata, r.file, offset)) |_| {
423 setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset));
424 return;
425 } else |err| {
426 r.seek_err = err;
427 break :e err;
428 }
429 };
430 var remaining = std.math.cast(u64, offset) orelse return seek_err;
431 while (remaining > 0) {
432 remaining -= discard(&r.interface, .limited64(remaining)) catch |err| {
433 r.seek_err = err;
434 return err;
435 };
436 }
437 r.interface.tossBuffered();
438 },
439 .failure => return r.seek_err.?,
440 }
441 }
442
443 /// Repositions logical read offset relative to the beginning of the file.
444 pub fn seekTo(r: *Reader, offset: u64) Reader.SeekError!void {
445 const io = r.io;
446 switch (r.mode) {
447 .positional, .positional_reading => {
448 setLogicalPos(r, offset);
449 },
450 .streaming, .streaming_reading => {
451 const logical_pos = logicalPos(r);
452 if (offset >= logical_pos) return Reader.seekBy(r, @intCast(offset - logical_pos));
453 if (r.seek_err) |err| return err;
454 io.vtable.fileSeekTo(io.userdata, r.file, offset) catch |err| {
455 r.seek_err = err;
456 return err;
457 };
458 setLogicalPos(r, offset);
459 },
460 .failure => return r.seek_err.?,
461 }
462 }
463
464 pub fn logicalPos(r: *const Reader) u64 {
465 return r.pos - r.interface.bufferedLen();
466 }
467
468 fn setLogicalPos(r: *Reader, offset: u64) void {
469 const logical_pos = r.logicalPos();
470 if (offset < logical_pos or offset >= r.pos) {
471 r.interface.tossBuffered();
472 r.pos = offset;
473 } else r.interface.toss(@intCast(offset - logical_pos));
474 }
475
476 /// Number of slices to store on the stack, when trying to send as many byte
477 /// vectors through the underlying read calls as possible.
478 const max_buffers_len = 16;
479
480 fn stream(io_reader: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize {
481 const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
482 return streamMode(r, w, limit, r.mode);
483 }
484
485 pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Reader.Mode) Io.Reader.StreamError!usize {
486 switch (mode) {
487 .positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) {
488 error.Unimplemented => {
489 r.mode = r.mode.toReading();
490 return 0;
491 },
492 else => |e| return e,
493 },
494 .positional_reading => {
495 const dest = limit.slice(try w.writableSliceGreedy(1));
496 var data: [1][]u8 = .{dest};
497 const n = try readVecPositional(r, &data);
498 w.advance(n);
499 return n;
500 },
501 .streaming_reading => {
502 const dest = limit.slice(try w.writableSliceGreedy(1));
503 var data: [1][]u8 = .{dest};
504 const n = try readVecStreaming(r, &data);
505 w.advance(n);
506 return n;
507 },
508 .failure => return error.ReadFailed,
509 }
510 }
511
512 fn readVec(io_reader: *Io.Reader, data: [][]u8) Io.Reader.Error!usize {
513 const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
514 switch (r.mode) {
515 .positional, .positional_reading => return readVecPositional(r, data),
516 .streaming, .streaming_reading => return readVecStreaming(r, data),
517 .failure => return error.ReadFailed,
518 }
519 }
520
521 fn readVecPositional(r: *Reader, data: [][]u8) Io.Reader.Error!usize {
522 const io = r.io;
523 var iovecs_buffer: [max_buffers_len][]u8 = undefined;
524 const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data);
525 const dest = iovecs_buffer[0..dest_n];
526 assert(dest[0].len > 0);
527 const n = io.vtable.fileReadPositional(io.userdata, r.file, dest, r.pos) catch |err| switch (err) {
528 error.Unseekable => {
529 r.mode = r.mode.toStreaming();
530 const pos = r.pos;
531 if (pos != 0) {
532 r.pos = 0;
533 r.seekBy(@intCast(pos)) catch {
534 r.mode = .failure;
535 return error.ReadFailed;
536 };
537 }
538 return 0;
539 },
540 else => |e| {
541 r.err = e;
542 return error.ReadFailed;
543 },
544 };
545 if (n == 0) {
546 r.size = r.pos;
547 return error.EndOfStream;
548 }
549 r.pos += n;
550 if (n > data_size) {
551 r.interface.end += n - data_size;
552 return data_size;
553 }
554 return n;
555 }
556
557 fn readVecStreaming(r: *Reader, data: [][]u8) Io.Reader.Error!usize {
558 const io = r.io;
559 var iovecs_buffer: [max_buffers_len][]u8 = undefined;
560 const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data);
561 const dest = iovecs_buffer[0..dest_n];
562 assert(dest[0].len > 0);
563 const n = io.vtable.fileReadStreaming(io.userdata, r.file, dest) catch |err| {
564 r.err = err;
565 return error.ReadFailed;
566 };
567 if (n == 0) {
568 r.size = r.pos;
569 return error.EndOfStream;
570 }
571 r.pos += n;
572 if (n > data_size) {
573 r.interface.end += n - data_size;
574 return data_size;
575 }
576 return n;
577 }
578
579 fn discard(io_reader: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize {
580 const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
581 const io = r.io;
582 const file = r.file;
583 switch (r.mode) {
584 .positional, .positional_reading => {
585 const size = r.getSize() catch {
586 r.mode = r.mode.toStreaming();
587 return 0;
588 };
589 const logical_pos = logicalPos(r);
590 const delta = @min(@intFromEnum(limit), size - logical_pos);
591 setLogicalPos(r, logical_pos + delta);
592 return delta;
593 },
594 .streaming, .streaming_reading => {
595 // Unfortunately we can't seek forward without knowing the
596 // size because the seek syscalls provided to us will not
597 // return the true end position if a seek would exceed the
598 // end.
599 fallback: {
600 if (r.size_err == null and r.seek_err == null) break :fallback;
601
602 const buffered_len = r.interface.bufferedLen();
603 var remaining = @intFromEnum(limit);
604 if (remaining <= buffered_len) {
605 r.interface.seek += remaining;
606 return remaining;
607 }
608 remaining -= buffered_len;
609 r.interface.seek = 0;
610 r.interface.end = 0;
611
612 var trash_buffer: [128]u8 = undefined;
613 var data: [1][]u8 = .{trash_buffer[0..@min(trash_buffer.len, remaining)]};
614 var iovecs_buffer: [max_buffers_len][]u8 = undefined;
615 const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, &data);
616 const dest = iovecs_buffer[0..dest_n];
617 assert(dest[0].len > 0);
618 const n = io.vtable.fileReadStreaming(io.userdata, file, dest) catch |err| {
619 r.err = err;
620 return error.ReadFailed;
621 };
622 if (n == 0) {
623 r.size = r.pos;
624 return error.EndOfStream;
625 }
626 r.pos += n;
627 if (n > data_size) {
628 r.interface.end += n - data_size;
629 remaining -= data_size;
630 } else {
631 remaining -= n;
632 }
633 return @intFromEnum(limit) - remaining;
634 }
635 const size = r.getSize() catch return 0;
636 const n = @min(size - r.pos, std.math.maxInt(i64), @intFromEnum(limit));
637 io.vtable.fileSeekBy(io.userdata, file, n) catch |err| {
638 r.seek_err = err;
639 return 0;
640 };
641 r.pos += n;
642 return n;
643 },
644 .failure => return error.ReadFailed,
645 }
646 }
647
648 /// Returns whether the stream is at the logical end.
649 pub fn atEnd(r: *Reader) bool {
650 // Even if stat fails, size is set when end is encountered.
651 const size = r.size orelse return false;
652 return size - logicalPos(r) == 0;
653 }
654};