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};