From 0a536a7c9020c891001be053a4c0b354961cc46f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Nov 2023 14:12:53 -0700 Subject: [PATCH] std.fs.File: flatten struct --- CMakeLists.txt | 2 +- lib/std/fs.zig | 12 +- lib/std/fs/AtomicFile.zig | 11 +- lib/std/fs/Dir.zig | 1 + lib/std/fs/File.zig | 1624 +++++++++++++++++++++++++++++++++++++ lib/std/fs/file.zig | 1622 ------------------------------------ 6 files changed, 1639 insertions(+), 1633 deletions(-) create mode 100644 lib/std/fs/File.zig delete mode 100644 lib/std/fs/file.zig diff --git a/CMakeLists.txt b/CMakeLists.txt index 4481dfb7f372c574c56425af89e0af908f87b5f9..393113e336e1099bebc5b2fb1e2909357c6cad39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -249,7 +249,7 @@ set(ZIG_STAGE2_SOURCES "${CMAKE_SOURCE_DIR}/lib/std/fs.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/AtomicFile.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/Dir.zig" - "${CMAKE_SOURCE_DIR}/lib/std/fs/file.zig" + "${CMAKE_SOURCE_DIR}/lib/std/fs/File.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/get_app_data_dir.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/path.zig" "${CMAKE_SOURCE_DIR}/lib/std/hash.zig" diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 1aad6883956ecf96de087ca53bd1f6d83bae63a7..93125c2530b17dcb2626d489ef6f84a8efe8be05 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -10,16 +10,16 @@ const assert = std.debug.assert; const is_darwin = builtin.os.tag.isDarwin(); -pub const Dir = @import("fs/Dir.zig"); pub const AtomicFile = @import("fs/AtomicFile.zig"); +pub const Dir = @import("fs/Dir.zig"); +pub const File = @import("fs/File.zig"); +pub const path = @import("fs/path.zig"); pub const has_executable_bit = switch (builtin.os.tag) { .windows, .wasi => false, else => true, }; -pub const path = @import("fs/path.zig"); -pub const File = @import("fs/file.zig").File; pub const wasi = @import("fs/wasi.zig"); // TODO audit these APIs with respect to Dir and absolute paths @@ -94,6 +94,7 @@ pub const need_async_thread = std.io.is_async and switch (builtin.os.tag) { }; /// TODO remove the allocator requirement from this API +/// TODO move to Dir pub fn atomicSymLink(allocator: Allocator, existing_path: []const u8, new_path: []const u8) !void { if (cwd().symLink(existing_path, new_path, .{})) { return; @@ -104,7 +105,7 @@ pub fn atomicSymLink(allocator: Allocator, existing_path: []const u8, new_path: const dirname = path.dirname(new_path) orelse "."; - var rand_buf: [AtomicFile.RANDOM_BYTES]u8 = undefined; + var rand_buf: [AtomicFile.random_bytes_len]u8 = undefined; const tmp_path = try allocator.alloc(u8, dirname.len + 1 + base64_encoder.calcSize(rand_buf.len)); defer allocator.free(tmp_path); @memcpy(tmp_path[0..dirname.len], dirname); @@ -634,8 +635,9 @@ test { _ = ©FileAbsolute; _ = &updateFileAbsolute; } - _ = &File; + _ = &AtomicFile; _ = &Dir; + _ = &File; _ = &path; _ = @import("fs/test.zig"); _ = @import("fs/get_app_data_dir.zig"); diff --git a/lib/std/fs/AtomicFile.zig b/lib/std/fs/AtomicFile.zig index 3e3ab92d08904c9f9d07ae6f042bd7f077854e17..b090bc95828ec693369ee73a3ec91fc5cc057c96 100644 --- a/lib/std/fs/AtomicFile.zig +++ b/lib/std/fs/AtomicFile.zig @@ -1,6 +1,6 @@ file: File, // TODO either replace this with rand_buf or use []u16 on Windows -tmp_path_buf: [TMP_PATH_LEN:0]u8, +tmp_path_buf: [tmp_path_len:0]u8, dest_basename: []const u8, file_open: bool, file_exists: bool, @@ -9,8 +9,8 @@ dir: Dir, pub const InitError = File.OpenError; -const RANDOM_BYTES = 12; -const TMP_PATH_LEN = fs.base64_encoder.calcSize(RANDOM_BYTES); +pub const random_bytes_len = 12; +const tmp_path_len = fs.base64_encoder.calcSize(random_bytes_len); /// Note that the `Dir.atomicFile` API may be more handy than this lower-level function. pub fn init( @@ -19,8 +19,8 @@ pub fn init( dir: Dir, close_dir_on_deinit: bool, ) InitError!AtomicFile { - var rand_buf: [RANDOM_BYTES]u8 = undefined; - var tmp_path_buf: [TMP_PATH_LEN:0]u8 = undefined; + var rand_buf: [random_bytes_len]u8 = undefined; + var tmp_path_buf: [tmp_path_len:0]u8 = undefined; while (true) { std.crypto.random.bytes(rand_buf[0..]); @@ -81,4 +81,5 @@ const File = std.fs.File; const Dir = std.fs.Dir; const fs = std.fs; const assert = std.debug.assert; +// https://github.com/ziglang/zig/issues/5019 const posix = std.os; diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 1d59615c0709e5ef9a5a863160ee0e45c5f22da9..b637e664f1ee577afcd9cb5b07adb64b315a2fd5 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -2527,6 +2527,7 @@ const builtin = @import("builtin"); const std = @import("../std.zig"); const File = std.fs.File; const AtomicFile = std.fs.AtomicFile; +// https://github.com/ziglang/zig/issues/5019 const posix = std.os; const mem = std.mem; const fs = std.fs; diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig new file mode 100644 index 0000000000000000000000000000000000000000..464e7207dcab4f0d531b55e46158e9eaacad8354 --- /dev/null +++ b/lib/std/fs/File.zig @@ -0,0 +1,1624 @@ +/// The OS-specific file descriptor or file handle. +handle: Handle, + +/// On some systems, such as Linux, file system file descriptors are incapable +/// of non-blocking I/O. This forces us to perform asynchronous I/O on a dedicated thread, +/// to achieve non-blocking file-system I/O. To do this, `File` must be aware of whether +/// it is a file system file descriptor, or, more specifically, whether the I/O is always +/// blocking. +capable_io_mode: io.ModeOverride = io.default_mode, + +/// Furthermore, even when `std.options.io_mode` is async, it is still sometimes desirable +/// to perform blocking I/O, although not by default. For example, when printing a +/// stack trace to stderr. This field tracks both by acting as an overriding I/O mode. +/// When not building in async I/O mode, the type only has the `.blocking` tag, making +/// it a zero-bit type. +intended_io_mode: io.ModeOverride = io.default_mode, + +pub const Handle = posix.fd_t; +pub const Mode = posix.mode_t; +pub const INode = posix.ino_t; +pub const Uid = posix.uid_t; +pub const Gid = posix.gid_t; + +pub const Kind = enum { + block_device, + character_device, + directory, + named_pipe, + sym_link, + file, + unix_domain_socket, + whiteout, + door, + event_port, + unknown, +}; + +/// This is the default mode given to POSIX operating systems for creating +/// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first, +/// since most people would expect "-rw-r--r--", for example, when using +/// the `touch` command, which would correspond to `0o644`. However, POSIX +/// libc implementations use `0o666` inside `fopen` and then rely on the +/// process-scoped "umask" setting to adjust this number for file creation. +pub const default_mode = switch (builtin.os.tag) { + .windows => 0, + .wasi => 0, + else => 0o666, +}; + +pub const OpenError = error{ + SharingViolation, + PathAlreadyExists, + FileNotFound, + AccessDenied, + PipeBusy, + NameTooLong, + /// On Windows, file paths must be valid Unicode. + InvalidUtf8, + /// On Windows, file paths cannot contain these characters: + /// '/', '*', '?', '"', '<', '>', '|' + BadPathName, + Unexpected, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, +} || posix.OpenError || posix.FlockError; + +pub const OpenMode = enum { + read_only, + write_only, + read_write, +}; + +pub const Lock = enum { + none, + shared, + exclusive, +}; + +pub const OpenFlags = struct { + mode: OpenMode = .read_only, + + /// Open the file with an advisory lock to coordinate with other processes + /// accessing it at the same time. An exclusive lock will prevent other + /// processes from acquiring a lock. A shared lock will prevent other + /// processes from acquiring a exclusive lock, but does not prevent + /// other process from getting their own shared locks. + /// + /// The lock is advisory, except on Linux in very specific circumstances[1]. + /// This means that a process that does not respect the locking API can still get access + /// to the file, despite the lock. + /// + /// On these operating systems, the lock is acquired atomically with + /// opening the file: + /// * Darwin + /// * DragonFlyBSD + /// * FreeBSD + /// * Haiku + /// * NetBSD + /// * OpenBSD + /// On these operating systems, the lock is acquired via a separate syscall + /// after opening the file: + /// * Linux + /// * Windows + /// + /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt + lock: Lock = .none, + + /// Sets whether or not to wait until the file is locked to return. If set to true, + /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file + /// is available to proceed. + /// In async I/O mode, non-blocking at the OS level is + /// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned, + /// and `false` means `error.WouldBlock` is handled by the event loop. + lock_nonblocking: bool = false, + + /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even + /// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions + /// related to opening the file, reading, writing, and locking. + intended_io_mode: io.ModeOverride = io.default_mode, + + /// Set this to allow the opened file to automatically become the + /// controlling TTY for the current process. + allow_ctty: bool = false, + + pub fn isRead(self: OpenFlags) bool { + return self.mode != .write_only; + } + + pub fn isWrite(self: OpenFlags) bool { + return self.mode != .read_only; + } +}; + +pub const CreateFlags = struct { + /// Whether the file will be created with read access. + read: bool = false, + + /// If the file already exists, and is a regular file, and the access + /// mode allows writing, it will be truncated to length 0. + truncate: bool = true, + + /// Ensures that this open call creates the file, otherwise causes + /// `error.PathAlreadyExists` to be returned. + exclusive: bool = false, + + /// Open the file with an advisory lock to coordinate with other processes + /// accessing it at the same time. An exclusive lock will prevent other + /// processes from acquiring a lock. A shared lock will prevent other + /// processes from acquiring a exclusive lock, but does not prevent + /// other process from getting their own shared locks. + /// + /// The lock is advisory, except on Linux in very specific circumstances[1]. + /// This means that a process that does not respect the locking API can still get access + /// to the file, despite the lock. + /// + /// On these operating systems, the lock is acquired atomically with + /// opening the file: + /// * Darwin + /// * DragonFlyBSD + /// * FreeBSD + /// * Haiku + /// * NetBSD + /// * OpenBSD + /// On these operating systems, the lock is acquired via a separate syscall + /// after opening the file: + /// * Linux + /// * Windows + /// + /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt + lock: Lock = .none, + + /// Sets whether or not to wait until the file is locked to return. If set to true, + /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file + /// is available to proceed. + /// In async I/O mode, non-blocking at the OS level is + /// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned, + /// and `false` means `error.WouldBlock` is handled by the event loop. + lock_nonblocking: bool = false, + + /// For POSIX systems this is the file system mode the file will + /// be created with. On other systems this is always 0. + mode: Mode = default_mode, + + /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even + /// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions + /// related to opening the file, reading, writing, and locking. + intended_io_mode: io.ModeOverride = io.default_mode, +}; + +/// Upon success, the stream is in an uninitialized state. To continue using it, +/// you must use the open() function. +pub fn close(self: File) void { + if (is_windows) { + windows.CloseHandle(self.handle); + } else if (self.capable_io_mode != self.intended_io_mode) { + std.event.Loop.instance.?.close(self.handle); + } else { + posix.close(self.handle); + } +} + +pub const SyncError = posix.SyncError; + +/// Blocks until all pending file contents and metadata modifications +/// for the file have been synchronized with the underlying filesystem. +/// +/// Note that this does not ensure that metadata for the +/// directory containing the file has also reached disk. +pub fn sync(self: File) SyncError!void { + return posix.fsync(self.handle); +} + +/// Test whether the file refers to a terminal. +/// See also `supportsAnsiEscapeCodes`. +pub fn isTty(self: File) bool { + return posix.isatty(self.handle); +} + +/// Test whether ANSI escape codes will be treated as such. +pub fn supportsAnsiEscapeCodes(self: File) bool { + if (builtin.os.tag == .windows) { + var console_mode: windows.DWORD = 0; + if (windows.kernel32.GetConsoleMode(self.handle, &console_mode) != 0) { + if (console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true; + } + + return posix.isCygwinPty(self.handle); + } + if (builtin.os.tag == .wasi) { + // WASI sanitizes stdout when fd is a tty so ANSI escape codes + // will not be interpreted as actual cursor commands, and + // stderr is always sanitized. + return false; + } + if (self.isTty()) { + if (self.handle == posix.STDOUT_FILENO or self.handle == posix.STDERR_FILENO) { + if (posix.getenvZ("TERM")) |term| { + if (std.mem.eql(u8, term, "dumb")) + return false; + } + } + return true; + } + return false; +} + +pub const SetEndPosError = posix.TruncateError; + +/// Shrinks or expands the file. +/// The file offset after this call is left unchanged. +pub fn setEndPos(self: File, length: u64) SetEndPosError!void { + try posix.ftruncate(self.handle, length); +} + +pub const SeekError = posix.SeekError; + +/// Repositions read/write file offset relative to the current offset. +/// TODO: integrate with async I/O +pub fn seekBy(self: File, offset: i64) SeekError!void { + return posix.lseek_CUR(self.handle, offset); +} + +/// Repositions read/write file offset relative to the end. +/// TODO: integrate with async I/O +pub fn seekFromEnd(self: File, offset: i64) SeekError!void { + return posix.lseek_END(self.handle, offset); +} + +/// Repositions read/write file offset relative to the beginning. +/// TODO: integrate with async I/O +pub fn seekTo(self: File, offset: u64) SeekError!void { + return posix.lseek_SET(self.handle, offset); +} + +pub const GetSeekPosError = posix.SeekError || posix.FStatError; + +/// TODO: integrate with async I/O +pub fn getPos(self: File) GetSeekPosError!u64 { + return posix.lseek_CUR_get(self.handle); +} + +/// TODO: integrate with async I/O +pub fn getEndPos(self: File) GetSeekPosError!u64 { + if (builtin.os.tag == .windows) { + return windows.GetFileSizeEx(self.handle); + } + return (try self.stat()).size; +} + +pub const ModeError = posix.FStatError; + +/// TODO: integrate with async I/O +pub fn mode(self: File) ModeError!Mode { + if (builtin.os.tag == .windows) { + return 0; + } + return (try self.stat()).mode; +} + +pub const Stat = struct { + /// A number that the system uses to point to the file metadata. This + /// number is not guaranteed to be unique across time, as some file + /// systems may reuse an inode after its file has been deleted. Some + /// systems may change the inode of a file over time. + /// + /// On Linux, the inode is a structure that stores the metadata, and + /// the inode _number_ is what you see here: the index number of the + /// inode. + /// + /// The FileIndex on Windows is similar. It is a number for a file that + /// is unique to each filesystem. + inode: INode, + size: u64, + /// This is available on POSIX systems and is always 0 otherwise. + mode: Mode, + kind: Kind, + + /// Access time in nanoseconds, relative to UTC 1970-01-01. + atime: i128, + /// Last modification time in nanoseconds, relative to UTC 1970-01-01. + mtime: i128, + /// Creation time in nanoseconds, relative to UTC 1970-01-01. + ctime: i128, + + pub fn fromSystem(st: posix.system.Stat) Stat { + const atime = st.atime(); + const mtime = st.mtime(); + const ctime = st.ctime(); + const kind: Kind = if (builtin.os.tag == .wasi and !builtin.link_libc) switch (st.filetype) { + .BLOCK_DEVICE => .block_device, + .CHARACTER_DEVICE => .character_device, + .DIRECTORY => .directory, + .SYMBOLIC_LINK => .sym_link, + .REGULAR_FILE => .file, + .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, + else => .unknown, + } else blk: { + const m = st.mode & posix.S.IFMT; + switch (m) { + posix.S.IFBLK => break :blk .block_device, + posix.S.IFCHR => break :blk .character_device, + posix.S.IFDIR => break :blk .directory, + posix.S.IFIFO => break :blk .named_pipe, + posix.S.IFLNK => break :blk .sym_link, + posix.S.IFREG => break :blk .file, + posix.S.IFSOCK => break :blk .unix_domain_socket, + else => {}, + } + if (builtin.os.tag.isSolarish()) switch (m) { + posix.S.IFDOOR => break :blk .door, + posix.S.IFPORT => break :blk .event_port, + else => {}, + }; + + break :blk .unknown; + }; + + return Stat{ + .inode = st.ino, + .size = @as(u64, @bitCast(st.size)), + .mode = st.mode, + .kind = kind, + .atime = @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec, + .mtime = @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec, + .ctime = @as(i128, ctime.tv_sec) * std.time.ns_per_s + ctime.tv_nsec, + }; + } +}; + +pub const StatError = posix.FStatError; + +/// TODO: integrate with async I/O +pub fn stat(self: File) StatError!Stat { + if (builtin.os.tag == .windows) { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + var info: windows.FILE_ALL_INFORMATION = undefined; + const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); + switch (rc) { + .SUCCESS => {}, + // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer + // size provided. This is treated as success because the type of variable-length information that this would be relevant for + // (name, volume name, etc) we don't care about. + .BUFFER_OVERFLOW => {}, + .INVALID_PARAMETER => unreachable, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + return Stat{ + .inode = info.InternalInformation.IndexNumber, + .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), + .mode = 0, + .kind = if (info.StandardInformation.Directory == 0) .file else .directory, + .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), + .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), + .ctime = windows.fromSysTime(info.BasicInformation.CreationTime), + }; + } + + const st = try posix.fstat(self.handle); + return Stat.fromSystem(st); +} + +pub const ChmodError = posix.FChmodError; + +/// Changes the mode of the file. +/// The process must have the correct privileges in order to do this +/// successfully, or must have the effective user ID matching the owner +/// of the file. +pub fn chmod(self: File, new_mode: Mode) ChmodError!void { + try posix.fchmod(self.handle, new_mode); +} + +pub const ChownError = posix.FChownError; + +/// Changes the owner and group of the file. +/// The process must have the correct privileges in order to do this +/// successfully. The group may be changed by the owner of the file to +/// any group of which the owner is a member. If the owner or group is +/// specified as `null`, the ID is not changed. +pub fn chown(self: File, owner: ?Uid, group: ?Gid) ChownError!void { + try posix.fchown(self.handle, owner, group); +} + +/// Cross-platform representation of permissions on a file. +/// The `readonly` and `setReadonly` are the only methods available across all platforms. +/// Platform-specific functionality is available through the `inner` field. +pub const Permissions = struct { + /// You may use the `inner` field to use platform-specific functionality + inner: switch (builtin.os.tag) { + .windows => PermissionsWindows, + else => PermissionsUnix, + }, + + const Self = @This(); + + /// Returns `true` if permissions represent an unwritable file. + /// On Unix, `true` is returned only if no class has write permissions. + pub fn readOnly(self: Self) bool { + return self.inner.readOnly(); + } + + /// Sets whether write permissions are provided. + /// On Unix, this affects *all* classes. If this is undesired, use `unixSet`. + /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` + pub fn setReadOnly(self: *Self, read_only: bool) void { + self.inner.setReadOnly(read_only); + } +}; + +pub const PermissionsWindows = struct { + attributes: windows.DWORD, + + const Self = @This(); + + /// Returns `true` if permissions represent an unwritable file. + pub fn readOnly(self: Self) bool { + return self.attributes & windows.FILE_ATTRIBUTE_READONLY != 0; + } + + /// Sets whether write permissions are provided. + /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` + pub fn setReadOnly(self: *Self, read_only: bool) void { + if (read_only) { + self.attributes |= windows.FILE_ATTRIBUTE_READONLY; + } else { + self.attributes &= ~@as(windows.DWORD, windows.FILE_ATTRIBUTE_READONLY); + } + } +}; + +pub const PermissionsUnix = struct { + mode: Mode, + + const Self = @This(); + + /// Returns `true` if permissions represent an unwritable file. + /// `true` is returned only if no class has write permissions. + pub fn readOnly(self: Self) bool { + return self.mode & 0o222 == 0; + } + + /// Sets whether write permissions are provided. + /// This affects *all* classes. If this is undesired, use `unixSet`. + /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` + pub fn setReadOnly(self: *Self, read_only: bool) void { + if (read_only) { + self.mode &= ~@as(Mode, 0o222); + } else { + self.mode |= @as(Mode, 0o222); + } + } + + pub const Class = enum(u2) { + user = 2, + group = 1, + other = 0, + }; + + pub const Permission = enum(u3) { + read = 0o4, + write = 0o2, + execute = 0o1, + }; + + /// Returns `true` if the chosen class has the selected permission. + /// This method is only available on Unix platforms. + pub fn unixHas(self: Self, class: Class, permission: Permission) bool { + const mask = @as(Mode, @intFromEnum(permission)) << @as(u3, @intFromEnum(class)) * 3; + return self.mode & mask != 0; + } + + /// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged. + /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` + pub fn unixSet(self: *Self, class: Class, permissions: struct { + read: ?bool = null, + write: ?bool = null, + execute: ?bool = null, + }) void { + const shift = @as(u3, @intFromEnum(class)) * 3; + if (permissions.read) |r| { + if (r) { + self.mode |= @as(Mode, 0o4) << shift; + } else { + self.mode &= ~(@as(Mode, 0o4) << shift); + } + } + if (permissions.write) |w| { + if (w) { + self.mode |= @as(Mode, 0o2) << shift; + } else { + self.mode &= ~(@as(Mode, 0o2) << shift); + } + } + if (permissions.execute) |x| { + if (x) { + self.mode |= @as(Mode, 0o1) << shift; + } else { + self.mode &= ~(@as(Mode, 0o1) << shift); + } + } + } + + /// Returns a `Permissions` struct representing the permissions from the passed mode. + pub fn unixNew(new_mode: Mode) Self { + return Self{ + .mode = new_mode, + }; + } +}; + +pub const SetPermissionsError = ChmodError; + +/// Sets permissions according to the provided `Permissions` struct. +/// This method is *NOT* available on WASI +pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void { + switch (builtin.os.tag) { + .windows => { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + var info = windows.FILE_BASIC_INFORMATION{ + .CreationTime = 0, + .LastAccessTime = 0, + .LastWriteTime = 0, + .ChangeTime = 0, + .FileAttributes = permissions.inner.attributes, + }; + const rc = windows.ntdll.NtSetInformationFile( + self.handle, + &io_status_block, + &info, + @sizeOf(windows.FILE_BASIC_INFORMATION), + .FileBasicInformation, + ); + switch (rc) { + .SUCCESS => return, + .INVALID_HANDLE => unreachable, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + }, + .wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod + else => { + try self.chmod(permissions.inner.mode); + }, + } +} + +/// Cross-platform representation of file metadata. +/// Platform-specific functionality is available through the `inner` field. +pub const Metadata = struct { + /// You may use the `inner` field to use platform-specific functionality + inner: switch (builtin.os.tag) { + .windows => MetadataWindows, + .linux => MetadataLinux, + else => MetadataUnix, + }, + + const Self = @This(); + + /// Returns the size of the file + pub fn size(self: Self) u64 { + return self.inner.size(); + } + + /// Returns a `Permissions` struct, representing the permissions on the file + pub fn permissions(self: Self) Permissions { + return self.inner.permissions(); + } + + /// Returns the `Kind` of file. + /// On Windows, can only return: `.file`, `.directory`, `.sym_link` or `.unknown` + pub fn kind(self: Self) Kind { + return self.inner.kind(); + } + + /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 + pub fn accessed(self: Self) i128 { + return self.inner.accessed(); + } + + /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01 + pub fn modified(self: Self) i128 { + return self.inner.modified(); + } + + /// Returns the time the file was created in nanoseconds since UTC 1970-01-01 + /// On Windows, this cannot return null + /// On Linux, this returns null if the filesystem does not support creation times, or if the kernel is older than 4.11 + /// On Unices, this returns null if the filesystem or OS does not support creation times + /// On MacOS, this returns the ctime if the filesystem does not support creation times; this is insanity, and yet another reason to hate on Apple + pub fn created(self: Self) ?i128 { + return self.inner.created(); + } +}; + +pub const MetadataUnix = struct { + stat: posix.Stat, + + const Self = @This(); + + /// Returns the size of the file + pub fn size(self: Self) u64 { + return @as(u64, @intCast(self.stat.size)); + } + + /// Returns a `Permissions` struct, representing the permissions on the file + pub fn permissions(self: Self) Permissions { + return Permissions{ .inner = PermissionsUnix{ .mode = self.stat.mode } }; + } + + /// Returns the `Kind` of the file + pub fn kind(self: Self) Kind { + if (builtin.os.tag == .wasi and !builtin.link_libc) return switch (self.stat.filetype) { + .BLOCK_DEVICE => .block_device, + .CHARACTER_DEVICE => .character_device, + .DIRECTORY => .directory, + .SYMBOLIC_LINK => .sym_link, + .REGULAR_FILE => .file, + .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, + else => .unknown, + }; + + const m = self.stat.mode & posix.S.IFMT; + + switch (m) { + posix.S.IFBLK => return .block_device, + posix.S.IFCHR => return .character_device, + posix.S.IFDIR => return .directory, + posix.S.IFIFO => return .named_pipe, + posix.S.IFLNK => return .sym_link, + posix.S.IFREG => return .file, + posix.S.IFSOCK => return .unix_domain_socket, + else => {}, + } + + if (builtin.os.tag.isSolarish()) switch (m) { + posix.S.IFDOOR => return .door, + posix.S.IFPORT => return .event_port, + else => {}, + }; + + return .unknown; + } + + /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 + pub fn accessed(self: Self) i128 { + const atime = self.stat.atime(); + return @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec; + } + + /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01 + pub fn modified(self: Self) i128 { + const mtime = self.stat.mtime(); + return @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec; + } + + /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. + /// Returns null if this is not supported by the OS or filesystem + pub fn created(self: Self) ?i128 { + if (!@hasDecl(@TypeOf(self.stat), "birthtime")) return null; + const birthtime = self.stat.birthtime(); + + // If the filesystem doesn't support this the value *should* be: + // On FreeBSD: tv_nsec = 0, tv_sec = -1 + // On NetBSD and OpenBSD: tv_nsec = 0, tv_sec = 0 + // On MacOS, it is set to ctime -- we cannot detect this!! + switch (builtin.os.tag) { + .freebsd => if (birthtime.tv_sec == -1 and birthtime.tv_nsec == 0) return null, + .netbsd, .openbsd => if (birthtime.tv_sec == 0 and birthtime.tv_nsec == 0) return null, + .macos => {}, + else => @compileError("Creation time detection not implemented for OS"), + } + + return @as(i128, birthtime.tv_sec) * std.time.ns_per_s + birthtime.tv_nsec; + } +}; + +/// `MetadataUnix`, but using Linux's `statx` syscall. +/// On Linux versions below 4.11, `statx` will be filled with data from stat. +pub const MetadataLinux = struct { + statx: std.os.linux.Statx, + + const Self = @This(); + + /// Returns the size of the file + pub fn size(self: Self) u64 { + return self.statx.size; + } + + /// Returns a `Permissions` struct, representing the permissions on the file + pub fn permissions(self: Self) Permissions { + return Permissions{ .inner = PermissionsUnix{ .mode = self.statx.mode } }; + } + + /// Returns the `Kind` of the file + pub fn kind(self: Self) Kind { + const m = self.statx.mode & posix.S.IFMT; + + switch (m) { + posix.S.IFBLK => return .block_device, + posix.S.IFCHR => return .character_device, + posix.S.IFDIR => return .directory, + posix.S.IFIFO => return .named_pipe, + posix.S.IFLNK => return .sym_link, + posix.S.IFREG => return .file, + posix.S.IFSOCK => return .unix_domain_socket, + else => {}, + } + + return .unknown; + } + + /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 + pub fn accessed(self: Self) i128 { + return @as(i128, self.statx.atime.tv_sec) * std.time.ns_per_s + self.statx.atime.tv_nsec; + } + + /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01 + pub fn modified(self: Self) i128 { + return @as(i128, self.statx.mtime.tv_sec) * std.time.ns_per_s + self.statx.mtime.tv_nsec; + } + + /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. + /// Returns null if this is not supported by the filesystem, or on kernels before than version 4.11 + pub fn created(self: Self) ?i128 { + if (self.statx.mask & std.os.linux.STATX_BTIME == 0) return null; + return @as(i128, self.statx.btime.tv_sec) * std.time.ns_per_s + self.statx.btime.tv_nsec; + } +}; + +pub const MetadataWindows = struct { + attributes: windows.DWORD, + reparse_tag: windows.DWORD, + _size: u64, + access_time: i128, + modified_time: i128, + creation_time: i128, + + const Self = @This(); + + /// Returns the size of the file + pub fn size(self: Self) u64 { + return self._size; + } + + /// Returns a `Permissions` struct, representing the permissions on the file + pub fn permissions(self: Self) Permissions { + return Permissions{ .inner = PermissionsWindows{ .attributes = self.attributes } }; + } + + /// Returns the `Kind` of the file. + /// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown` + pub fn kind(self: Self) Kind { + if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { + if (self.reparse_tag & 0x20000000 != 0) { + return .sym_link; + } + } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) { + return .directory; + } else { + return .file; + } + return .unknown; + } + + /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 + pub fn accessed(self: Self) i128 { + return self.access_time; + } + + /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01 + pub fn modified(self: Self) i128 { + return self.modified_time; + } + + /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. + /// This never returns null, only returning an optional for compatibility with other OSes + pub fn created(self: Self) ?i128 { + return self.creation_time; + } +}; + +pub const MetadataError = posix.FStatError; + +pub fn metadata(self: File) MetadataError!Metadata { + return Metadata{ + .inner = switch (builtin.os.tag) { + .windows => blk: { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + var info: windows.FILE_ALL_INFORMATION = undefined; + + const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); + switch (rc) { + .SUCCESS => {}, + // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer + // size provided. This is treated as success because the type of variable-length information that this would be relevant for + // (name, volume name, etc) we don't care about. + .BUFFER_OVERFLOW => {}, + .INVALID_PARAMETER => unreachable, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + + const reparse_tag: windows.DWORD = reparse_blk: { + if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { + var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; + try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]); + const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0])); + break :reparse_blk reparse_struct.ReparseTag; + } + break :reparse_blk 0; + }; + + break :blk MetadataWindows{ + .attributes = info.BasicInformation.FileAttributes, + .reparse_tag = reparse_tag, + ._size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), + .access_time = windows.fromSysTime(info.BasicInformation.LastAccessTime), + .modified_time = windows.fromSysTime(info.BasicInformation.LastWriteTime), + .creation_time = windows.fromSysTime(info.BasicInformation.CreationTime), + }; + }, + .linux => blk: { + const l = std.os.linux; + var stx = std.mem.zeroes(l.Statx); + const rcx = l.statx(self.handle, "\x00", l.AT.EMPTY_PATH, l.STATX_TYPE | + l.STATX_MODE | l.STATX_ATIME | l.STATX_MTIME | l.STATX_BTIME, &stx); + + switch (posix.errno(rcx)) { + .SUCCESS => {}, + // NOSYS happens when `statx` is unsupported, which is the case on kernel versions before 4.11 + // Here, we call `fstat` and fill `stx` with the data we need + .NOSYS => { + const st = try posix.fstat(self.handle); + + stx.mode = @as(u16, @intCast(st.mode)); + + // Hacky conversion from timespec to statx_timestamp + stx.atime = std.mem.zeroes(l.statx_timestamp); + stx.atime.tv_sec = st.atim.tv_sec; + stx.atime.tv_nsec = @as(u32, @intCast(st.atim.tv_nsec)); // Guaranteed to succeed (tv_nsec is always below 10^9) + + stx.mtime = std.mem.zeroes(l.statx_timestamp); + stx.mtime.tv_sec = st.mtim.tv_sec; + stx.mtime.tv_nsec = @as(u32, @intCast(st.mtim.tv_nsec)); + + stx.mask = l.STATX_BASIC_STATS | l.STATX_MTIME; + }, + .BADF => unreachable, + .FAULT => unreachable, + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + + break :blk MetadataLinux{ + .statx = stx, + }; + }, + else => blk: { + const st = try posix.fstat(self.handle); + break :blk MetadataUnix{ + .stat = st, + }; + }, + }, + }; +} + +pub const UpdateTimesError = posix.FutimensError || windows.SetFileTimeError; + +/// The underlying file system may have a different granularity than nanoseconds, +/// and therefore this function cannot guarantee any precision will be stored. +/// Further, the maximum value is limited by the system ABI. When a value is provided +/// that exceeds this range, the value is clamped to the maximum. +/// TODO: integrate with async I/O +pub fn updateTimes( + self: File, + /// access timestamp in nanoseconds + atime: i128, + /// last modification timestamp in nanoseconds + mtime: i128, +) UpdateTimesError!void { + if (builtin.os.tag == .windows) { + const atime_ft = windows.nanoSecondsToFileTime(atime); + const mtime_ft = windows.nanoSecondsToFileTime(mtime); + return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft); + } + const times = [2]posix.timespec{ + posix.timespec{ + .tv_sec = math.cast(isize, @divFloor(atime, std.time.ns_per_s)) orelse maxInt(isize), + .tv_nsec = math.cast(isize, @mod(atime, std.time.ns_per_s)) orelse maxInt(isize), + }, + posix.timespec{ + .tv_sec = math.cast(isize, @divFloor(mtime, std.time.ns_per_s)) orelse maxInt(isize), + .tv_nsec = math.cast(isize, @mod(mtime, std.time.ns_per_s)) orelse maxInt(isize), + }, + }; + try posix.futimens(self.handle, ×); +} + +/// Reads all the bytes from the current position to the end of the file. +/// On success, caller owns returned buffer. +/// If the file is larger than `max_bytes`, returns `error.FileTooBig`. +pub fn readToEndAlloc(self: File, allocator: Allocator, max_bytes: usize) ![]u8 { + return self.readToEndAllocOptions(allocator, max_bytes, null, @alignOf(u8), null); +} + +/// Reads all the bytes from the current position to the end of the file. +/// On success, caller owns returned buffer. +/// If the file is larger than `max_bytes`, returns `error.FileTooBig`. +/// If `size_hint` is specified the initial buffer size is calculated using +/// that value, otherwise an arbitrary value is used instead. +/// Allows specifying alignment and a sentinel value. +pub fn readToEndAllocOptions( + self: File, + allocator: Allocator, + max_bytes: usize, + size_hint: ?usize, + comptime alignment: u29, + comptime optional_sentinel: ?u8, +) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) { + // If no size hint is provided fall back to the size=0 code path + const size = size_hint orelse 0; + + // The file size returned by stat is used as hint to set the buffer + // size. If the reported size is zero, as it happens on Linux for files + // in /proc, a small buffer is allocated instead. + const initial_cap = (if (size > 0) size else 1024) + @intFromBool(optional_sentinel != null); + var array_list = try std.ArrayListAligned(u8, alignment).initCapacity(allocator, initial_cap); + defer array_list.deinit(); + + self.reader().readAllArrayListAligned(alignment, &array_list, max_bytes) catch |err| switch (err) { + error.StreamTooLong => return error.FileTooBig, + else => |e| return e, + }; + + if (optional_sentinel) |sentinel| { + return try array_list.toOwnedSliceSentinel(sentinel); + } else { + return try array_list.toOwnedSlice(); + } +} + +pub const ReadError = posix.ReadError; +pub const PReadError = posix.PReadError; + +pub fn read(self: File, buffer: []u8) ReadError!usize { + if (is_windows) { + return windows.ReadFile(self.handle, buffer, null, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.read(self.handle, buffer); + } else { + return std.event.Loop.instance.?.read(self.handle, buffer, self.capable_io_mode != self.intended_io_mode); + } +} + +/// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it +/// means the file reached the end. Reaching the end of a file is not an error condition. +pub fn readAll(self: File, buffer: []u8) ReadError!usize { + var index: usize = 0; + while (index != buffer.len) { + const amt = try self.read(buffer[index..]); + if (amt == 0) break; + index += amt; + } + return index; +} + +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize { + if (is_windows) { + return windows.ReadFile(self.handle, buffer, offset, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.pread(self.handle, buffer, offset); + } else { + return std.event.Loop.instance.?.pread(self.handle, buffer, offset, self.capable_io_mode != self.intended_io_mode); + } +} + +/// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it +/// means the file reached the end. Reaching the end of a file is not an error condition. +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize { + var index: usize = 0; + while (index != buffer.len) { + const amt = try self.pread(buffer[index..], offset + index); + if (amt == 0) break; + index += amt; + } + return index; +} + +/// See https://github.com/ziglang/zig/issues/7699 +pub fn readv(self: File, iovecs: []const posix.iovec) ReadError!usize { + if (is_windows) { + // TODO improve this to use ReadFileScatter + if (iovecs.len == 0) return @as(usize, 0); + const first = iovecs[0]; + return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.readv(self.handle, iovecs); + } else { + return std.event.Loop.instance.?.readv(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode); + } +} + +/// Returns the number of bytes read. If the number read is smaller than the total bytes +/// from all the buffers, it means the file reached the end. Reaching the end of a file +/// is not an error condition. +/// +/// The `iovecs` parameter is mutable because: +/// * This function needs to mutate the fields in order to handle partial +/// reads from the underlying OS layer. +/// * The OS layer expects pointer addresses to be inside the application's address space +/// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer +/// addresses when the length is zero. So this function modifies the iov_base fields +/// when the length is zero. +/// +/// Related open issue: https://github.com/ziglang/zig/issues/7699 +pub fn readvAll(self: File, iovecs: []posix.iovec) ReadError!usize { + if (iovecs.len == 0) return 0; + + // We use the address of this local variable for all zero-length + // vectors so that the OS does not complain that we are giving it + // addresses outside the application's address space. + var garbage: [1]u8 = undefined; + for (iovecs) |*v| { + if (v.iov_len == 0) v.iov_base = &garbage; + } + + var i: usize = 0; + var off: usize = 0; + while (true) { + var amt = try self.readv(iovecs[i..]); + var eof = amt == 0; + off += amt; + while (amt >= iovecs[i].iov_len) { + amt -= iovecs[i].iov_len; + i += 1; + if (i >= iovecs.len) return off; + eof = false; + } + if (eof) return off; + iovecs[i].iov_base += amt; + iovecs[i].iov_len -= amt; + } +} + +/// See https://github.com/ziglang/zig/issues/7699 +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn preadv(self: File, iovecs: []const posix.iovec, offset: u64) PReadError!usize { + if (is_windows) { + // TODO improve this to use ReadFileScatter + if (iovecs.len == 0) return @as(usize, 0); + const first = iovecs[0]; + return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.preadv(self.handle, iovecs, offset); + } else { + return std.event.Loop.instance.?.preadv(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode); + } +} + +/// Returns the number of bytes read. If the number read is smaller than the total bytes +/// from all the buffers, it means the file reached the end. Reaching the end of a file +/// is not an error condition. +/// The `iovecs` parameter is mutable because this function needs to mutate the fields in +/// order to handle partial reads from the underlying OS layer. +/// See https://github.com/ziglang/zig/issues/7699 +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn preadvAll(self: File, iovecs: []posix.iovec, offset: u64) PReadError!usize { + if (iovecs.len == 0) return 0; + + var i: usize = 0; + var off: usize = 0; + while (true) { + var amt = try self.preadv(iovecs[i..], offset + off); + var eof = amt == 0; + off += amt; + while (amt >= iovecs[i].iov_len) { + amt -= iovecs[i].iov_len; + i += 1; + if (i >= iovecs.len) return off; + eof = false; + } + if (eof) return off; + iovecs[i].iov_base += amt; + iovecs[i].iov_len -= amt; + } +} + +pub const WriteError = posix.WriteError; +pub const PWriteError = posix.PWriteError; + +pub fn write(self: File, bytes: []const u8) WriteError!usize { + if (is_windows) { + return windows.WriteFile(self.handle, bytes, null, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.write(self.handle, bytes); + } else { + return std.event.Loop.instance.?.write(self.handle, bytes, self.capable_io_mode != self.intended_io_mode); + } +} + +pub fn writeAll(self: File, bytes: []const u8) WriteError!void { + var index: usize = 0; + while (index < bytes.len) { + index += try self.write(bytes[index..]); + } +} + +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize { + if (is_windows) { + return windows.WriteFile(self.handle, bytes, offset, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.pwrite(self.handle, bytes, offset); + } else { + return std.event.Loop.instance.?.pwrite(self.handle, bytes, offset, self.capable_io_mode != self.intended_io_mode); + } +} + +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void { + var index: usize = 0; + while (index < bytes.len) { + index += try self.pwrite(bytes[index..], offset + index); + } +} + +/// See https://github.com/ziglang/zig/issues/7699 +/// See equivalent function: `std.net.Stream.writev`. +pub fn writev(self: File, iovecs: []const posix.iovec_const) WriteError!usize { + if (is_windows) { + // TODO improve this to use WriteFileScatter + if (iovecs.len == 0) return @as(usize, 0); + const first = iovecs[0]; + return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.writev(self.handle, iovecs); + } else { + return std.event.Loop.instance.?.writev(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode); + } +} + +/// The `iovecs` parameter is mutable because: +/// * This function needs to mutate the fields in order to handle partial +/// writes from the underlying OS layer. +/// * The OS layer expects pointer addresses to be inside the application's address space +/// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer +/// addresses when the length is zero. So this function modifies the iov_base fields +/// when the length is zero. +/// See https://github.com/ziglang/zig/issues/7699 +/// See equivalent function: `std.net.Stream.writevAll`. +pub fn writevAll(self: File, iovecs: []posix.iovec_const) WriteError!void { + if (iovecs.len == 0) return; + + // We use the address of this local variable for all zero-length + // vectors so that the OS does not complain that we are giving it + // addresses outside the application's address space. + var garbage: [1]u8 = undefined; + for (iovecs) |*v| { + if (v.iov_len == 0) v.iov_base = &garbage; + } + + var i: usize = 0; + while (true) { + var amt = try self.writev(iovecs[i..]); + while (amt >= iovecs[i].iov_len) { + amt -= iovecs[i].iov_len; + i += 1; + if (i >= iovecs.len) return; + } + iovecs[i].iov_base += amt; + iovecs[i].iov_len -= amt; + } +} + +/// See https://github.com/ziglang/zig/issues/7699 +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn pwritev(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError!usize { + if (is_windows) { + // TODO improve this to use WriteFileScatter + if (iovecs.len == 0) return @as(usize, 0); + const first = iovecs[0]; + return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode); + } + + if (self.intended_io_mode == .blocking) { + return posix.pwritev(self.handle, iovecs, offset); + } else { + return std.event.Loop.instance.?.pwritev(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode); + } +} + +/// The `iovecs` parameter is mutable because this function needs to mutate the fields in +/// order to handle partial writes from the underlying OS layer. +/// See https://github.com/ziglang/zig/issues/7699 +/// On Windows, this function currently does alter the file pointer. +/// https://github.com/ziglang/zig/issues/12783 +pub fn pwritevAll(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError!void { + if (iovecs.len == 0) return; + + var i: usize = 0; + var off: u64 = 0; + while (true) { + var amt = try self.pwritev(iovecs[i..], offset + off); + off += amt; + while (amt >= iovecs[i].iov_len) { + amt -= iovecs[i].iov_len; + i += 1; + if (i >= iovecs.len) return; + } + iovecs[i].iov_base += amt; + iovecs[i].iov_len -= amt; + } +} + +pub const CopyRangeError = posix.CopyFileRangeError; + +pub fn copyRange(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { + const adjusted_len = math.cast(usize, len) orelse maxInt(usize); + const result = try posix.copy_file_range(in.handle, in_offset, out.handle, out_offset, adjusted_len, 0); + return result; +} + +/// Returns the number of bytes copied. If the number read is smaller than `buffer.len`, it +/// means the in file reached the end. Reaching the end of a file is not an error condition. +pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { + var total_bytes_copied: u64 = 0; + var in_off = in_offset; + var out_off = out_offset; + while (total_bytes_copied < len) { + const amt_copied = try copyRange(in, in_off, out, out_off, len - total_bytes_copied); + if (amt_copied == 0) return total_bytes_copied; + total_bytes_copied += amt_copied; + in_off += amt_copied; + out_off += amt_copied; + } + return total_bytes_copied; +} + +pub const WriteFileOptions = struct { + in_offset: u64 = 0, + + /// `null` means the entire file. `0` means no bytes from the file. + /// When this is `null`, trailers must be sent in a separate writev() call + /// due to a flaw in the BSD sendfile API. Other operating systems, such as + /// Linux, already do this anyway due to API limitations. + /// If the size of the source file is known, passing the size here will save one syscall. + in_len: ?u64 = null, + + headers_and_trailers: []posix.iovec_const = &[0]posix.iovec_const{}, + + /// The trailer count is inferred from `headers_and_trailers.len - header_count` + header_count: usize = 0, +}; + +pub const WriteFileError = ReadError || error{EndOfStream} || WriteError; + +pub fn writeFileAll(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void { + return self.writeFileAllSendfile(in_file, args) catch |err| switch (err) { + error.Unseekable, + error.FastOpenAlreadyInProgress, + error.MessageTooBig, + error.FileDescriptorNotASocket, + error.NetworkUnreachable, + error.NetworkSubsystemFailed, + => return self.writeFileAllUnseekable(in_file, args), + + else => |e| return e, + }; +} + +/// Does not try seeking in either of the File parameters. +/// See `writeFileAll` as an alternative to calling this. +pub fn writeFileAllUnseekable(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void { + const headers = args.headers_and_trailers[0..args.header_count]; + const trailers = args.headers_and_trailers[args.header_count..]; + + try self.writevAll(headers); + + try in_file.reader().skipBytes(args.in_offset, .{ .buf_size = 4096 }); + + var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init(); + if (args.in_len) |len| { + var stream = std.io.limitedReader(in_file.reader(), len); + try fifo.pump(stream.reader(), self.writer()); + } else { + try fifo.pump(in_file.reader(), self.writer()); + } + + try self.writevAll(trailers); +} + +/// Low level function which can fail for OS-specific reasons. +/// See `writeFileAll` as an alternative to calling this. +/// TODO integrate with async I/O +fn writeFileAllSendfile(self: File, in_file: File, args: WriteFileOptions) posix.SendFileError!void { + const count = blk: { + if (args.in_len) |l| { + if (l == 0) { + return self.writevAll(args.headers_and_trailers); + } else { + break :blk l; + } + } else { + break :blk 0; + } + }; + const headers = args.headers_and_trailers[0..args.header_count]; + const trailers = args.headers_and_trailers[args.header_count..]; + const zero_iovec = &[0]posix.iovec_const{}; + // When reading the whole file, we cannot put the trailers in the sendfile() syscall, + // because we have no way to determine whether a partial write is past the end of the file or not. + const trls = if (count == 0) zero_iovec else trailers; + const offset = args.in_offset; + const out_fd = self.handle; + const in_fd = in_file.handle; + const flags = 0; + var amt: usize = 0; + hdrs: { + var i: usize = 0; + while (i < headers.len) { + amt = try posix.sendfile(out_fd, in_fd, offset, count, headers[i..], trls, flags); + while (amt >= headers[i].iov_len) { + amt -= headers[i].iov_len; + i += 1; + if (i >= headers.len) break :hdrs; + } + headers[i].iov_base += amt; + headers[i].iov_len -= amt; + } + } + if (count == 0) { + var off: u64 = amt; + while (true) { + amt = try posix.sendfile(out_fd, in_fd, offset + off, 0, zero_iovec, zero_iovec, flags); + if (amt == 0) break; + off += amt; + } + } else { + var off: u64 = amt; + while (off < count) { + amt = try posix.sendfile(out_fd, in_fd, offset + off, count - off, zero_iovec, trailers, flags); + off += amt; + } + amt = @as(usize, @intCast(off - count)); + } + var i: usize = 0; + while (i < trailers.len) { + while (amt >= trailers[i].iov_len) { + amt -= trailers[i].iov_len; + i += 1; + if (i >= trailers.len) return; + } + trailers[i].iov_base += amt; + trailers[i].iov_len -= amt; + amt = try posix.writev(self.handle, trailers[i..]); + } +} + +pub const Reader = io.Reader(File, ReadError, read); + +pub fn reader(file: File) Reader { + return .{ .context = file }; +} + +pub const Writer = io.Writer(File, WriteError, write); + +pub fn writer(file: File) Writer { + return .{ .context = file }; +} + +pub const SeekableStream = io.SeekableStream( + File, + SeekError, + GetSeekPosError, + seekTo, + seekBy, + getPos, + getEndPos, +); + +pub fn seekableStream(file: File) SeekableStream { + return .{ .context = file }; +} + +const range_off: windows.LARGE_INTEGER = 0; +const range_len: windows.LARGE_INTEGER = 1; + +pub const LockError = error{ + SystemResources, + FileLocksNotSupported, +} || posix.UnexpectedError; + +/// Blocks when an incompatible lock is held by another process. +/// A process may hold only one type of lock (shared or exclusive) on +/// a file. When a process terminates in any way, the lock is released. +/// +/// Assumes the file is unlocked. +/// +/// TODO: integrate with async I/O +pub fn lock(file: File, l: Lock) LockError!void { + if (is_windows) { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + const exclusive = switch (l) { + .none => return, + .shared => false, + .exclusive => true, + }; + return windows.LockFile( + file.handle, + null, + null, + null, + &io_status_block, + &range_off, + &range_len, + null, + windows.FALSE, // non-blocking=false + @intFromBool(exclusive), + ) catch |err| switch (err) { + error.WouldBlock => unreachable, // non-blocking=false + else => |e| return e, + }; + } else { + return posix.flock(file.handle, switch (l) { + .none => posix.LOCK.UN, + .shared => posix.LOCK.SH, + .exclusive => posix.LOCK.EX, + }) catch |err| switch (err) { + error.WouldBlock => unreachable, // non-blocking=false + else => |e| return e, + }; + } +} + +/// Assumes the file is locked. +pub fn unlock(file: File) void { + if (is_windows) { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + return windows.UnlockFile( + file.handle, + &io_status_block, + &range_off, + &range_len, + null, + ) catch |err| switch (err) { + error.RangeNotLocked => unreachable, // Function assumes unlocked. + error.Unexpected => unreachable, // Resource deallocation must succeed. + }; + } else { + return posix.flock(file.handle, posix.LOCK.UN) catch |err| switch (err) { + error.WouldBlock => unreachable, // unlocking can't block + error.SystemResources => unreachable, // We are deallocating resources. + error.FileLocksNotSupported => unreachable, // We already got the lock. + error.Unexpected => unreachable, // Resource deallocation must succeed. + }; + } +} + +/// Attempts to obtain a lock, returning `true` if the lock is +/// obtained, and `false` if there was an existing incompatible lock held. +/// A process may hold only one type of lock (shared or exclusive) on +/// a file. When a process terminates in any way, the lock is released. +/// +/// Assumes the file is unlocked. +/// +/// TODO: integrate with async I/O +pub fn tryLock(file: File, l: Lock) LockError!bool { + if (is_windows) { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + const exclusive = switch (l) { + .none => return, + .shared => false, + .exclusive => true, + }; + windows.LockFile( + file.handle, + null, + null, + null, + &io_status_block, + &range_off, + &range_len, + null, + windows.TRUE, // non-blocking=true + @intFromBool(exclusive), + ) catch |err| switch (err) { + error.WouldBlock => return false, + else => |e| return e, + }; + } else { + posix.flock(file.handle, switch (l) { + .none => posix.LOCK.UN, + .shared => posix.LOCK.SH | posix.LOCK.NB, + .exclusive => posix.LOCK.EX | posix.LOCK.NB, + }) catch |err| switch (err) { + error.WouldBlock => return false, + else => |e| return e, + }; + } + return true; +} + +/// Assumes the file is already locked in exclusive mode. +/// Atomically modifies the lock to be in shared mode, without releasing it. +/// +/// TODO: integrate with async I/O +pub fn downgradeLock(file: File) LockError!void { + if (is_windows) { + // On Windows it works like a semaphore + exclusivity flag. To implement this + // function, we first obtain another lock in shared mode. This changes the + // exclusivity flag, but increments the semaphore to 2. So we follow up with + // an NtUnlockFile which decrements the semaphore but does not modify the + // exclusivity flag. + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + windows.LockFile( + file.handle, + null, + null, + null, + &io_status_block, + &range_off, + &range_len, + null, + windows.TRUE, // non-blocking=true + windows.FALSE, // exclusive=false + ) catch |err| switch (err) { + error.WouldBlock => unreachable, // File was not locked in exclusive mode. + else => |e| return e, + }; + return windows.UnlockFile( + file.handle, + &io_status_block, + &range_off, + &range_len, + null, + ) catch |err| switch (err) { + error.RangeNotLocked => unreachable, // File was not locked. + error.Unexpected => unreachable, // Resource deallocation must succeed. + }; + } else { + return posix.flock(file.handle, posix.LOCK.SH | posix.LOCK.NB) catch |err| switch (err) { + error.WouldBlock => unreachable, // File was not locked in exclusive mode. + else => |e| return e, + }; + } +} + +const File = @This(); +const std = @import("../std.zig"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; +// https://github.com/ziglang/zig/issues/5019 +const posix = std.os; +const io = std.io; +const math = std.math; +const assert = std.debug.assert; +const windows = std.os.windows; +const Os = std.builtin.Os; +const maxInt = std.math.maxInt; +const is_windows = builtin.os.tag == .windows; diff --git a/lib/std/fs/file.zig b/lib/std/fs/file.zig deleted file mode 100644 index 3436aa4b330358e123a35a18aa1cab0c8852ee5c..0000000000000000000000000000000000000000 --- a/lib/std/fs/file.zig +++ /dev/null @@ -1,1622 +0,0 @@ -const std = @import("../std.zig"); -const builtin = @import("builtin"); -const os = std.os; -const io = std.io; -const mem = std.mem; -const math = std.math; -const assert = std.debug.assert; -const windows = os.windows; -const Os = std.builtin.Os; -const maxInt = std.math.maxInt; -const is_windows = builtin.os.tag == .windows; - -pub const File = struct { - /// The OS-specific file descriptor or file handle. - handle: Handle, - - /// On some systems, such as Linux, file system file descriptors are incapable - /// of non-blocking I/O. This forces us to perform asynchronous I/O on a dedicated thread, - /// to achieve non-blocking file-system I/O. To do this, `File` must be aware of whether - /// it is a file system file descriptor, or, more specifically, whether the I/O is always - /// blocking. - capable_io_mode: io.ModeOverride = io.default_mode, - - /// Furthermore, even when `std.options.io_mode` is async, it is still sometimes desirable - /// to perform blocking I/O, although not by default. For example, when printing a - /// stack trace to stderr. This field tracks both by acting as an overriding I/O mode. - /// When not building in async I/O mode, the type only has the `.blocking` tag, making - /// it a zero-bit type. - intended_io_mode: io.ModeOverride = io.default_mode, - - pub const Handle = os.fd_t; - pub const Mode = os.mode_t; - pub const INode = os.ino_t; - pub const Uid = os.uid_t; - pub const Gid = os.gid_t; - - pub const Kind = enum { - block_device, - character_device, - directory, - named_pipe, - sym_link, - file, - unix_domain_socket, - whiteout, - door, - event_port, - unknown, - }; - - /// This is the default mode given to POSIX operating systems for creating - /// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first, - /// since most people would expect "-rw-r--r--", for example, when using - /// the `touch` command, which would correspond to `0o644`. However, POSIX - /// libc implementations use `0o666` inside `fopen` and then rely on the - /// process-scoped "umask" setting to adjust this number for file creation. - pub const default_mode = switch (builtin.os.tag) { - .windows => 0, - .wasi => 0, - else => 0o666, - }; - - pub const OpenError = error{ - SharingViolation, - PathAlreadyExists, - FileNotFound, - AccessDenied, - PipeBusy, - NameTooLong, - /// On Windows, file paths must be valid Unicode. - InvalidUtf8, - /// On Windows, file paths cannot contain these characters: - /// '/', '*', '?', '"', '<', '>', '|' - BadPathName, - Unexpected, - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - } || os.OpenError || os.FlockError; - - pub const OpenMode = enum { - read_only, - write_only, - read_write, - }; - - pub const Lock = enum { - none, - shared, - exclusive, - }; - - pub const OpenFlags = struct { - mode: OpenMode = .read_only, - - /// Open the file with an advisory lock to coordinate with other processes - /// accessing it at the same time. An exclusive lock will prevent other - /// processes from acquiring a lock. A shared lock will prevent other - /// processes from acquiring a exclusive lock, but does not prevent - /// other process from getting their own shared locks. - /// - /// The lock is advisory, except on Linux in very specific circumstances[1]. - /// This means that a process that does not respect the locking API can still get access - /// to the file, despite the lock. - /// - /// On these operating systems, the lock is acquired atomically with - /// opening the file: - /// * Darwin - /// * DragonFlyBSD - /// * FreeBSD - /// * Haiku - /// * NetBSD - /// * OpenBSD - /// On these operating systems, the lock is acquired via a separate syscall - /// after opening the file: - /// * Linux - /// * Windows - /// - /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt - lock: Lock = .none, - - /// Sets whether or not to wait until the file is locked to return. If set to true, - /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file - /// is available to proceed. - /// In async I/O mode, non-blocking at the OS level is - /// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned, - /// and `false` means `error.WouldBlock` is handled by the event loop. - lock_nonblocking: bool = false, - - /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even - /// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions - /// related to opening the file, reading, writing, and locking. - intended_io_mode: io.ModeOverride = io.default_mode, - - /// Set this to allow the opened file to automatically become the - /// controlling TTY for the current process. - allow_ctty: bool = false, - - pub fn isRead(self: OpenFlags) bool { - return self.mode != .write_only; - } - - pub fn isWrite(self: OpenFlags) bool { - return self.mode != .read_only; - } - }; - - pub const CreateFlags = struct { - /// Whether the file will be created with read access. - read: bool = false, - - /// If the file already exists, and is a regular file, and the access - /// mode allows writing, it will be truncated to length 0. - truncate: bool = true, - - /// Ensures that this open call creates the file, otherwise causes - /// `error.PathAlreadyExists` to be returned. - exclusive: bool = false, - - /// Open the file with an advisory lock to coordinate with other processes - /// accessing it at the same time. An exclusive lock will prevent other - /// processes from acquiring a lock. A shared lock will prevent other - /// processes from acquiring a exclusive lock, but does not prevent - /// other process from getting their own shared locks. - /// - /// The lock is advisory, except on Linux in very specific circumstances[1]. - /// This means that a process that does not respect the locking API can still get access - /// to the file, despite the lock. - /// - /// On these operating systems, the lock is acquired atomically with - /// opening the file: - /// * Darwin - /// * DragonFlyBSD - /// * FreeBSD - /// * Haiku - /// * NetBSD - /// * OpenBSD - /// On these operating systems, the lock is acquired via a separate syscall - /// after opening the file: - /// * Linux - /// * Windows - /// - /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt - lock: Lock = .none, - - /// Sets whether or not to wait until the file is locked to return. If set to true, - /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file - /// is available to proceed. - /// In async I/O mode, non-blocking at the OS level is - /// determined by `intended_io_mode`, and `true` means `error.WouldBlock` is returned, - /// and `false` means `error.WouldBlock` is handled by the event loop. - lock_nonblocking: bool = false, - - /// For POSIX systems this is the file system mode the file will - /// be created with. On other systems this is always 0. - mode: Mode = default_mode, - - /// Setting this to `.blocking` prevents `O.NONBLOCK` from being passed even - /// if `std.io.is_async`. It allows the use of `nosuspend` when calling functions - /// related to opening the file, reading, writing, and locking. - intended_io_mode: io.ModeOverride = io.default_mode, - }; - - /// Upon success, the stream is in an uninitialized state. To continue using it, - /// you must use the open() function. - pub fn close(self: File) void { - if (is_windows) { - windows.CloseHandle(self.handle); - } else if (self.capable_io_mode != self.intended_io_mode) { - std.event.Loop.instance.?.close(self.handle); - } else { - os.close(self.handle); - } - } - - pub const SyncError = os.SyncError; - - /// Blocks until all pending file contents and metadata modifications - /// for the file have been synchronized with the underlying filesystem. - /// - /// Note that this does not ensure that metadata for the - /// directory containing the file has also reached disk. - pub fn sync(self: File) SyncError!void { - return os.fsync(self.handle); - } - - /// Test whether the file refers to a terminal. - /// See also `supportsAnsiEscapeCodes`. - pub fn isTty(self: File) bool { - return os.isatty(self.handle); - } - - /// Test whether ANSI escape codes will be treated as such. - pub fn supportsAnsiEscapeCodes(self: File) bool { - if (builtin.os.tag == .windows) { - var console_mode: os.windows.DWORD = 0; - if (os.windows.kernel32.GetConsoleMode(self.handle, &console_mode) != 0) { - if (console_mode & os.windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true; - } - - return os.isCygwinPty(self.handle); - } - if (builtin.os.tag == .wasi) { - // WASI sanitizes stdout when fd is a tty so ANSI escape codes - // will not be interpreted as actual cursor commands, and - // stderr is always sanitized. - return false; - } - if (self.isTty()) { - if (self.handle == os.STDOUT_FILENO or self.handle == os.STDERR_FILENO) { - if (os.getenvZ("TERM")) |term| { - if (std.mem.eql(u8, term, "dumb")) - return false; - } - } - return true; - } - return false; - } - - pub const SetEndPosError = os.TruncateError; - - /// Shrinks or expands the file. - /// The file offset after this call is left unchanged. - pub fn setEndPos(self: File, length: u64) SetEndPosError!void { - try os.ftruncate(self.handle, length); - } - - pub const SeekError = os.SeekError; - - /// Repositions read/write file offset relative to the current offset. - /// TODO: integrate with async I/O - pub fn seekBy(self: File, offset: i64) SeekError!void { - return os.lseek_CUR(self.handle, offset); - } - - /// Repositions read/write file offset relative to the end. - /// TODO: integrate with async I/O - pub fn seekFromEnd(self: File, offset: i64) SeekError!void { - return os.lseek_END(self.handle, offset); - } - - /// Repositions read/write file offset relative to the beginning. - /// TODO: integrate with async I/O - pub fn seekTo(self: File, offset: u64) SeekError!void { - return os.lseek_SET(self.handle, offset); - } - - pub const GetSeekPosError = os.SeekError || os.FStatError; - - /// TODO: integrate with async I/O - pub fn getPos(self: File) GetSeekPosError!u64 { - return os.lseek_CUR_get(self.handle); - } - - /// TODO: integrate with async I/O - pub fn getEndPos(self: File) GetSeekPosError!u64 { - if (builtin.os.tag == .windows) { - return windows.GetFileSizeEx(self.handle); - } - return (try self.stat()).size; - } - - pub const ModeError = os.FStatError; - - /// TODO: integrate with async I/O - pub fn mode(self: File) ModeError!Mode { - if (builtin.os.tag == .windows) { - return 0; - } - return (try self.stat()).mode; - } - - pub const Stat = struct { - /// A number that the system uses to point to the file metadata. This - /// number is not guaranteed to be unique across time, as some file - /// systems may reuse an inode after its file has been deleted. Some - /// systems may change the inode of a file over time. - /// - /// On Linux, the inode is a structure that stores the metadata, and - /// the inode _number_ is what you see here: the index number of the - /// inode. - /// - /// The FileIndex on Windows is similar. It is a number for a file that - /// is unique to each filesystem. - inode: INode, - size: u64, - /// This is available on POSIX systems and is always 0 otherwise. - mode: Mode, - kind: Kind, - - /// Access time in nanoseconds, relative to UTC 1970-01-01. - atime: i128, - /// Last modification time in nanoseconds, relative to UTC 1970-01-01. - mtime: i128, - /// Creation time in nanoseconds, relative to UTC 1970-01-01. - ctime: i128, - - pub fn fromSystem(st: os.system.Stat) Stat { - const atime = st.atime(); - const mtime = st.mtime(); - const ctime = st.ctime(); - const kind: Kind = if (builtin.os.tag == .wasi and !builtin.link_libc) switch (st.filetype) { - .BLOCK_DEVICE => .block_device, - .CHARACTER_DEVICE => .character_device, - .DIRECTORY => .directory, - .SYMBOLIC_LINK => .sym_link, - .REGULAR_FILE => .file, - .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, - else => .unknown, - } else blk: { - const m = st.mode & os.S.IFMT; - switch (m) { - os.S.IFBLK => break :blk .block_device, - os.S.IFCHR => break :blk .character_device, - os.S.IFDIR => break :blk .directory, - os.S.IFIFO => break :blk .named_pipe, - os.S.IFLNK => break :blk .sym_link, - os.S.IFREG => break :blk .file, - os.S.IFSOCK => break :blk .unix_domain_socket, - else => {}, - } - if (builtin.os.tag.isSolarish()) switch (m) { - os.S.IFDOOR => break :blk .door, - os.S.IFPORT => break :blk .event_port, - else => {}, - }; - - break :blk .unknown; - }; - - return Stat{ - .inode = st.ino, - .size = @as(u64, @bitCast(st.size)), - .mode = st.mode, - .kind = kind, - .atime = @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec, - .mtime = @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec, - .ctime = @as(i128, ctime.tv_sec) * std.time.ns_per_s + ctime.tv_nsec, - }; - } - }; - - pub const StatError = os.FStatError; - - /// TODO: integrate with async I/O - pub fn stat(self: File) StatError!Stat { - if (builtin.os.tag == .windows) { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - var info: windows.FILE_ALL_INFORMATION = undefined; - const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); - switch (rc) { - .SUCCESS => {}, - // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer - // size provided. This is treated as success because the type of variable-length information that this would be relevant for - // (name, volume name, etc) we don't care about. - .BUFFER_OVERFLOW => {}, - .INVALID_PARAMETER => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - return Stat{ - .inode = info.InternalInformation.IndexNumber, - .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), - .mode = 0, - .kind = if (info.StandardInformation.Directory == 0) .file else .directory, - .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), - .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), - .ctime = windows.fromSysTime(info.BasicInformation.CreationTime), - }; - } - - const st = try os.fstat(self.handle); - return Stat.fromSystem(st); - } - - pub const ChmodError = std.os.FChmodError; - - /// Changes the mode of the file. - /// The process must have the correct privileges in order to do this - /// successfully, or must have the effective user ID matching the owner - /// of the file. - pub fn chmod(self: File, new_mode: Mode) ChmodError!void { - try os.fchmod(self.handle, new_mode); - } - - pub const ChownError = std.os.FChownError; - - /// Changes the owner and group of the file. - /// The process must have the correct privileges in order to do this - /// successfully. The group may be changed by the owner of the file to - /// any group of which the owner is a member. If the owner or group is - /// specified as `null`, the ID is not changed. - pub fn chown(self: File, owner: ?Uid, group: ?Gid) ChownError!void { - try os.fchown(self.handle, owner, group); - } - - /// Cross-platform representation of permissions on a file. - /// The `readonly` and `setReadonly` are the only methods available across all platforms. - /// Platform-specific functionality is available through the `inner` field. - pub const Permissions = struct { - /// You may use the `inner` field to use platform-specific functionality - inner: switch (builtin.os.tag) { - .windows => PermissionsWindows, - else => PermissionsUnix, - }, - - const Self = @This(); - - /// Returns `true` if permissions represent an unwritable file. - /// On Unix, `true` is returned only if no class has write permissions. - pub fn readOnly(self: Self) bool { - return self.inner.readOnly(); - } - - /// Sets whether write permissions are provided. - /// On Unix, this affects *all* classes. If this is undesired, use `unixSet`. - /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` - pub fn setReadOnly(self: *Self, read_only: bool) void { - self.inner.setReadOnly(read_only); - } - }; - - pub const PermissionsWindows = struct { - attributes: os.windows.DWORD, - - const Self = @This(); - - /// Returns `true` if permissions represent an unwritable file. - pub fn readOnly(self: Self) bool { - return self.attributes & os.windows.FILE_ATTRIBUTE_READONLY != 0; - } - - /// Sets whether write permissions are provided. - /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` - pub fn setReadOnly(self: *Self, read_only: bool) void { - if (read_only) { - self.attributes |= os.windows.FILE_ATTRIBUTE_READONLY; - } else { - self.attributes &= ~@as(os.windows.DWORD, os.windows.FILE_ATTRIBUTE_READONLY); - } - } - }; - - pub const PermissionsUnix = struct { - mode: Mode, - - const Self = @This(); - - /// Returns `true` if permissions represent an unwritable file. - /// `true` is returned only if no class has write permissions. - pub fn readOnly(self: Self) bool { - return self.mode & 0o222 == 0; - } - - /// Sets whether write permissions are provided. - /// This affects *all* classes. If this is undesired, use `unixSet`. - /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` - pub fn setReadOnly(self: *Self, read_only: bool) void { - if (read_only) { - self.mode &= ~@as(Mode, 0o222); - } else { - self.mode |= @as(Mode, 0o222); - } - } - - pub const Class = enum(u2) { - user = 2, - group = 1, - other = 0, - }; - - pub const Permission = enum(u3) { - read = 0o4, - write = 0o2, - execute = 0o1, - }; - - /// Returns `true` if the chosen class has the selected permission. - /// This method is only available on Unix platforms. - pub fn unixHas(self: Self, class: Class, permission: Permission) bool { - const mask = @as(Mode, @intFromEnum(permission)) << @as(u3, @intFromEnum(class)) * 3; - return self.mode & mask != 0; - } - - /// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged. - /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)` - pub fn unixSet(self: *Self, class: Class, permissions: struct { - read: ?bool = null, - write: ?bool = null, - execute: ?bool = null, - }) void { - const shift = @as(u3, @intFromEnum(class)) * 3; - if (permissions.read) |r| { - if (r) { - self.mode |= @as(Mode, 0o4) << shift; - } else { - self.mode &= ~(@as(Mode, 0o4) << shift); - } - } - if (permissions.write) |w| { - if (w) { - self.mode |= @as(Mode, 0o2) << shift; - } else { - self.mode &= ~(@as(Mode, 0o2) << shift); - } - } - if (permissions.execute) |x| { - if (x) { - self.mode |= @as(Mode, 0o1) << shift; - } else { - self.mode &= ~(@as(Mode, 0o1) << shift); - } - } - } - - /// Returns a `Permissions` struct representing the permissions from the passed mode. - pub fn unixNew(new_mode: Mode) Self { - return Self{ - .mode = new_mode, - }; - } - }; - - pub const SetPermissionsError = ChmodError; - - /// Sets permissions according to the provided `Permissions` struct. - /// This method is *NOT* available on WASI - pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void { - switch (builtin.os.tag) { - .windows => { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - var info = windows.FILE_BASIC_INFORMATION{ - .CreationTime = 0, - .LastAccessTime = 0, - .LastWriteTime = 0, - .ChangeTime = 0, - .FileAttributes = permissions.inner.attributes, - }; - const rc = windows.ntdll.NtSetInformationFile( - self.handle, - &io_status_block, - &info, - @sizeOf(windows.FILE_BASIC_INFORMATION), - .FileBasicInformation, - ); - switch (rc) { - .SUCCESS => return, - .INVALID_HANDLE => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - }, - .wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod - else => { - try self.chmod(permissions.inner.mode); - }, - } - } - - /// Cross-platform representation of file metadata. - /// Platform-specific functionality is available through the `inner` field. - pub const Metadata = struct { - /// You may use the `inner` field to use platform-specific functionality - inner: switch (builtin.os.tag) { - .windows => MetadataWindows, - .linux => MetadataLinux, - else => MetadataUnix, - }, - - const Self = @This(); - - /// Returns the size of the file - pub fn size(self: Self) u64 { - return self.inner.size(); - } - - /// Returns a `Permissions` struct, representing the permissions on the file - pub fn permissions(self: Self) Permissions { - return self.inner.permissions(); - } - - /// Returns the `Kind` of file. - /// On Windows, can only return: `.file`, `.directory`, `.sym_link` or `.unknown` - pub fn kind(self: Self) Kind { - return self.inner.kind(); - } - - /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 - pub fn accessed(self: Self) i128 { - return self.inner.accessed(); - } - - /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01 - pub fn modified(self: Self) i128 { - return self.inner.modified(); - } - - /// Returns the time the file was created in nanoseconds since UTC 1970-01-01 - /// On Windows, this cannot return null - /// On Linux, this returns null if the filesystem does not support creation times, or if the kernel is older than 4.11 - /// On Unices, this returns null if the filesystem or OS does not support creation times - /// On MacOS, this returns the ctime if the filesystem does not support creation times; this is insanity, and yet another reason to hate on Apple - pub fn created(self: Self) ?i128 { - return self.inner.created(); - } - }; - - pub const MetadataUnix = struct { - stat: os.Stat, - - const Self = @This(); - - /// Returns the size of the file - pub fn size(self: Self) u64 { - return @as(u64, @intCast(self.stat.size)); - } - - /// Returns a `Permissions` struct, representing the permissions on the file - pub fn permissions(self: Self) Permissions { - return Permissions{ .inner = PermissionsUnix{ .mode = self.stat.mode } }; - } - - /// Returns the `Kind` of the file - pub fn kind(self: Self) Kind { - if (builtin.os.tag == .wasi and !builtin.link_libc) return switch (self.stat.filetype) { - .BLOCK_DEVICE => .block_device, - .CHARACTER_DEVICE => .character_device, - .DIRECTORY => .directory, - .SYMBOLIC_LINK => .sym_link, - .REGULAR_FILE => .file, - .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, - else => .unknown, - }; - - const m = self.stat.mode & os.S.IFMT; - - switch (m) { - os.S.IFBLK => return .block_device, - os.S.IFCHR => return .character_device, - os.S.IFDIR => return .directory, - os.S.IFIFO => return .named_pipe, - os.S.IFLNK => return .sym_link, - os.S.IFREG => return .file, - os.S.IFSOCK => return .unix_domain_socket, - else => {}, - } - - if (builtin.os.tag.isSolarish()) switch (m) { - os.S.IFDOOR => return .door, - os.S.IFPORT => return .event_port, - else => {}, - }; - - return .unknown; - } - - /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 - pub fn accessed(self: Self) i128 { - const atime = self.stat.atime(); - return @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec; - } - - /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01 - pub fn modified(self: Self) i128 { - const mtime = self.stat.mtime(); - return @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec; - } - - /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. - /// Returns null if this is not supported by the OS or filesystem - pub fn created(self: Self) ?i128 { - if (!@hasDecl(@TypeOf(self.stat), "birthtime")) return null; - const birthtime = self.stat.birthtime(); - - // If the filesystem doesn't support this the value *should* be: - // On FreeBSD: tv_nsec = 0, tv_sec = -1 - // On NetBSD and OpenBSD: tv_nsec = 0, tv_sec = 0 - // On MacOS, it is set to ctime -- we cannot detect this!! - switch (builtin.os.tag) { - .freebsd => if (birthtime.tv_sec == -1 and birthtime.tv_nsec == 0) return null, - .netbsd, .openbsd => if (birthtime.tv_sec == 0 and birthtime.tv_nsec == 0) return null, - .macos => {}, - else => @compileError("Creation time detection not implemented for OS"), - } - - return @as(i128, birthtime.tv_sec) * std.time.ns_per_s + birthtime.tv_nsec; - } - }; - - /// `MetadataUnix`, but using Linux's `statx` syscall. - /// On Linux versions below 4.11, `statx` will be filled with data from stat. - pub const MetadataLinux = struct { - statx: os.linux.Statx, - - const Self = @This(); - - /// Returns the size of the file - pub fn size(self: Self) u64 { - return self.statx.size; - } - - /// Returns a `Permissions` struct, representing the permissions on the file - pub fn permissions(self: Self) Permissions { - return Permissions{ .inner = PermissionsUnix{ .mode = self.statx.mode } }; - } - - /// Returns the `Kind` of the file - pub fn kind(self: Self) Kind { - const m = self.statx.mode & os.S.IFMT; - - switch (m) { - os.S.IFBLK => return .block_device, - os.S.IFCHR => return .character_device, - os.S.IFDIR => return .directory, - os.S.IFIFO => return .named_pipe, - os.S.IFLNK => return .sym_link, - os.S.IFREG => return .file, - os.S.IFSOCK => return .unix_domain_socket, - else => {}, - } - - return .unknown; - } - - /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 - pub fn accessed(self: Self) i128 { - return @as(i128, self.statx.atime.tv_sec) * std.time.ns_per_s + self.statx.atime.tv_nsec; - } - - /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01 - pub fn modified(self: Self) i128 { - return @as(i128, self.statx.mtime.tv_sec) * std.time.ns_per_s + self.statx.mtime.tv_nsec; - } - - /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. - /// Returns null if this is not supported by the filesystem, or on kernels before than version 4.11 - pub fn created(self: Self) ?i128 { - if (self.statx.mask & os.linux.STATX_BTIME == 0) return null; - return @as(i128, self.statx.btime.tv_sec) * std.time.ns_per_s + self.statx.btime.tv_nsec; - } - }; - - pub const MetadataWindows = struct { - attributes: windows.DWORD, - reparse_tag: windows.DWORD, - _size: u64, - access_time: i128, - modified_time: i128, - creation_time: i128, - - const Self = @This(); - - /// Returns the size of the file - pub fn size(self: Self) u64 { - return self._size; - } - - /// Returns a `Permissions` struct, representing the permissions on the file - pub fn permissions(self: Self) Permissions { - return Permissions{ .inner = PermissionsWindows{ .attributes = self.attributes } }; - } - - /// Returns the `Kind` of the file. - /// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown` - pub fn kind(self: Self) Kind { - if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { - if (self.reparse_tag & 0x20000000 != 0) { - return .sym_link; - } - } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) { - return .directory; - } else { - return .file; - } - return .unknown; - } - - /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01 - pub fn accessed(self: Self) i128 { - return self.access_time; - } - - /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01 - pub fn modified(self: Self) i128 { - return self.modified_time; - } - - /// Returns the time the file was created in nanoseconds since UTC 1970-01-01. - /// This never returns null, only returning an optional for compatibility with other OSes - pub fn created(self: Self) ?i128 { - return self.creation_time; - } - }; - - pub const MetadataError = os.FStatError; - - pub fn metadata(self: File) MetadataError!Metadata { - return Metadata{ - .inner = switch (builtin.os.tag) { - .windows => blk: { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - var info: windows.FILE_ALL_INFORMATION = undefined; - - const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); - switch (rc) { - .SUCCESS => {}, - // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer - // size provided. This is treated as success because the type of variable-length information that this would be relevant for - // (name, volume name, etc) we don't care about. - .BUFFER_OVERFLOW => {}, - .INVALID_PARAMETER => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - - const reparse_tag: windows.DWORD = reparse_blk: { - if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { - var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; - try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]); - const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0])); - break :reparse_blk reparse_struct.ReparseTag; - } - break :reparse_blk 0; - }; - - break :blk MetadataWindows{ - .attributes = info.BasicInformation.FileAttributes, - .reparse_tag = reparse_tag, - ._size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), - .access_time = windows.fromSysTime(info.BasicInformation.LastAccessTime), - .modified_time = windows.fromSysTime(info.BasicInformation.LastWriteTime), - .creation_time = windows.fromSysTime(info.BasicInformation.CreationTime), - }; - }, - .linux => blk: { - var stx = mem.zeroes(os.linux.Statx); - const rcx = os.linux.statx(self.handle, "\x00", os.linux.AT.EMPTY_PATH, os.linux.STATX_TYPE | os.linux.STATX_MODE | os.linux.STATX_ATIME | os.linux.STATX_MTIME | os.linux.STATX_BTIME, &stx); - - switch (os.errno(rcx)) { - .SUCCESS => {}, - // NOSYS happens when `statx` is unsupported, which is the case on kernel versions before 4.11 - // Here, we call `fstat` and fill `stx` with the data we need - .NOSYS => { - const st = try os.fstat(self.handle); - - stx.mode = @as(u16, @intCast(st.mode)); - - // Hacky conversion from timespec to statx_timestamp - stx.atime = std.mem.zeroes(os.linux.statx_timestamp); - stx.atime.tv_sec = st.atim.tv_sec; - stx.atime.tv_nsec = @as(u32, @intCast(st.atim.tv_nsec)); // Guaranteed to succeed (tv_nsec is always below 10^9) - - stx.mtime = std.mem.zeroes(os.linux.statx_timestamp); - stx.mtime.tv_sec = st.mtim.tv_sec; - stx.mtime.tv_nsec = @as(u32, @intCast(st.mtim.tv_nsec)); - - stx.mask = os.linux.STATX_BASIC_STATS | os.linux.STATX_MTIME; - }, - .BADF => unreachable, - .FAULT => unreachable, - .NOMEM => return error.SystemResources, - else => |err| return os.unexpectedErrno(err), - } - - break :blk MetadataLinux{ - .statx = stx, - }; - }, - else => blk: { - const st = try os.fstat(self.handle); - break :blk MetadataUnix{ - .stat = st, - }; - }, - }, - }; - } - - pub const UpdateTimesError = os.FutimensError || windows.SetFileTimeError; - - /// The underlying file system may have a different granularity than nanoseconds, - /// and therefore this function cannot guarantee any precision will be stored. - /// Further, the maximum value is limited by the system ABI. When a value is provided - /// that exceeds this range, the value is clamped to the maximum. - /// TODO: integrate with async I/O - pub fn updateTimes( - self: File, - /// access timestamp in nanoseconds - atime: i128, - /// last modification timestamp in nanoseconds - mtime: i128, - ) UpdateTimesError!void { - if (builtin.os.tag == .windows) { - const atime_ft = windows.nanoSecondsToFileTime(atime); - const mtime_ft = windows.nanoSecondsToFileTime(mtime); - return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft); - } - const times = [2]os.timespec{ - os.timespec{ - .tv_sec = math.cast(isize, @divFloor(atime, std.time.ns_per_s)) orelse maxInt(isize), - .tv_nsec = math.cast(isize, @mod(atime, std.time.ns_per_s)) orelse maxInt(isize), - }, - os.timespec{ - .tv_sec = math.cast(isize, @divFloor(mtime, std.time.ns_per_s)) orelse maxInt(isize), - .tv_nsec = math.cast(isize, @mod(mtime, std.time.ns_per_s)) orelse maxInt(isize), - }, - }; - try os.futimens(self.handle, ×); - } - - /// Reads all the bytes from the current position to the end of the file. - /// On success, caller owns returned buffer. - /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. - pub fn readToEndAlloc(self: File, allocator: mem.Allocator, max_bytes: usize) ![]u8 { - return self.readToEndAllocOptions(allocator, max_bytes, null, @alignOf(u8), null); - } - - /// Reads all the bytes from the current position to the end of the file. - /// On success, caller owns returned buffer. - /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. - /// If `size_hint` is specified the initial buffer size is calculated using - /// that value, otherwise an arbitrary value is used instead. - /// Allows specifying alignment and a sentinel value. - pub fn readToEndAllocOptions( - self: File, - allocator: mem.Allocator, - max_bytes: usize, - size_hint: ?usize, - comptime alignment: u29, - comptime optional_sentinel: ?u8, - ) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) { - // If no size hint is provided fall back to the size=0 code path - const size = size_hint orelse 0; - - // The file size returned by stat is used as hint to set the buffer - // size. If the reported size is zero, as it happens on Linux for files - // in /proc, a small buffer is allocated instead. - const initial_cap = (if (size > 0) size else 1024) + @intFromBool(optional_sentinel != null); - var array_list = try std.ArrayListAligned(u8, alignment).initCapacity(allocator, initial_cap); - defer array_list.deinit(); - - self.reader().readAllArrayListAligned(alignment, &array_list, max_bytes) catch |err| switch (err) { - error.StreamTooLong => return error.FileTooBig, - else => |e| return e, - }; - - if (optional_sentinel) |sentinel| { - return try array_list.toOwnedSliceSentinel(sentinel); - } else { - return try array_list.toOwnedSlice(); - } - } - - pub const ReadError = os.ReadError; - pub const PReadError = os.PReadError; - - pub fn read(self: File, buffer: []u8) ReadError!usize { - if (is_windows) { - return windows.ReadFile(self.handle, buffer, null, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.read(self.handle, buffer); - } else { - return std.event.Loop.instance.?.read(self.handle, buffer, self.capable_io_mode != self.intended_io_mode); - } - } - - /// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it - /// means the file reached the end. Reaching the end of a file is not an error condition. - pub fn readAll(self: File, buffer: []u8) ReadError!usize { - var index: usize = 0; - while (index != buffer.len) { - const amt = try self.read(buffer[index..]); - if (amt == 0) break; - index += amt; - } - return index; - } - - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize { - if (is_windows) { - return windows.ReadFile(self.handle, buffer, offset, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.pread(self.handle, buffer, offset); - } else { - return std.event.Loop.instance.?.pread(self.handle, buffer, offset, self.capable_io_mode != self.intended_io_mode); - } - } - - /// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it - /// means the file reached the end. Reaching the end of a file is not an error condition. - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize { - var index: usize = 0; - while (index != buffer.len) { - const amt = try self.pread(buffer[index..], offset + index); - if (amt == 0) break; - index += amt; - } - return index; - } - - /// See https://github.com/ziglang/zig/issues/7699 - pub fn readv(self: File, iovecs: []const os.iovec) ReadError!usize { - if (is_windows) { - // TODO improve this to use ReadFileScatter - if (iovecs.len == 0) return @as(usize, 0); - const first = iovecs[0]; - return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.readv(self.handle, iovecs); - } else { - return std.event.Loop.instance.?.readv(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode); - } - } - - /// Returns the number of bytes read. If the number read is smaller than the total bytes - /// from all the buffers, it means the file reached the end. Reaching the end of a file - /// is not an error condition. - /// - /// The `iovecs` parameter is mutable because: - /// * This function needs to mutate the fields in order to handle partial - /// reads from the underlying OS layer. - /// * The OS layer expects pointer addresses to be inside the application's address space - /// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer - /// addresses when the length is zero. So this function modifies the iov_base fields - /// when the length is zero. - /// - /// Related open issue: https://github.com/ziglang/zig/issues/7699 - pub fn readvAll(self: File, iovecs: []os.iovec) ReadError!usize { - if (iovecs.len == 0) return 0; - - // We use the address of this local variable for all zero-length - // vectors so that the OS does not complain that we are giving it - // addresses outside the application's address space. - var garbage: [1]u8 = undefined; - for (iovecs) |*v| { - if (v.iov_len == 0) v.iov_base = &garbage; - } - - var i: usize = 0; - var off: usize = 0; - while (true) { - var amt = try self.readv(iovecs[i..]); - var eof = amt == 0; - off += amt; - while (amt >= iovecs[i].iov_len) { - amt -= iovecs[i].iov_len; - i += 1; - if (i >= iovecs.len) return off; - eof = false; - } - if (eof) return off; - iovecs[i].iov_base += amt; - iovecs[i].iov_len -= amt; - } - } - - /// See https://github.com/ziglang/zig/issues/7699 - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn preadv(self: File, iovecs: []const os.iovec, offset: u64) PReadError!usize { - if (is_windows) { - // TODO improve this to use ReadFileScatter - if (iovecs.len == 0) return @as(usize, 0); - const first = iovecs[0]; - return windows.ReadFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.preadv(self.handle, iovecs, offset); - } else { - return std.event.Loop.instance.?.preadv(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode); - } - } - - /// Returns the number of bytes read. If the number read is smaller than the total bytes - /// from all the buffers, it means the file reached the end. Reaching the end of a file - /// is not an error condition. - /// The `iovecs` parameter is mutable because this function needs to mutate the fields in - /// order to handle partial reads from the underlying OS layer. - /// See https://github.com/ziglang/zig/issues/7699 - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn preadvAll(self: File, iovecs: []os.iovec, offset: u64) PReadError!usize { - if (iovecs.len == 0) return 0; - - var i: usize = 0; - var off: usize = 0; - while (true) { - var amt = try self.preadv(iovecs[i..], offset + off); - var eof = amt == 0; - off += amt; - while (amt >= iovecs[i].iov_len) { - amt -= iovecs[i].iov_len; - i += 1; - if (i >= iovecs.len) return off; - eof = false; - } - if (eof) return off; - iovecs[i].iov_base += amt; - iovecs[i].iov_len -= amt; - } - } - - pub const WriteError = os.WriteError; - pub const PWriteError = os.PWriteError; - - pub fn write(self: File, bytes: []const u8) WriteError!usize { - if (is_windows) { - return windows.WriteFile(self.handle, bytes, null, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.write(self.handle, bytes); - } else { - return std.event.Loop.instance.?.write(self.handle, bytes, self.capable_io_mode != self.intended_io_mode); - } - } - - pub fn writeAll(self: File, bytes: []const u8) WriteError!void { - var index: usize = 0; - while (index < bytes.len) { - index += try self.write(bytes[index..]); - } - } - - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize { - if (is_windows) { - return windows.WriteFile(self.handle, bytes, offset, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.pwrite(self.handle, bytes, offset); - } else { - return std.event.Loop.instance.?.pwrite(self.handle, bytes, offset, self.capable_io_mode != self.intended_io_mode); - } - } - - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void { - var index: usize = 0; - while (index < bytes.len) { - index += try self.pwrite(bytes[index..], offset + index); - } - } - - /// See https://github.com/ziglang/zig/issues/7699 - /// See equivalent function: `std.net.Stream.writev`. - pub fn writev(self: File, iovecs: []const os.iovec_const) WriteError!usize { - if (is_windows) { - // TODO improve this to use WriteFileScatter - if (iovecs.len == 0) return @as(usize, 0); - const first = iovecs[0]; - return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], null, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.writev(self.handle, iovecs); - } else { - return std.event.Loop.instance.?.writev(self.handle, iovecs, self.capable_io_mode != self.intended_io_mode); - } - } - - /// The `iovecs` parameter is mutable because: - /// * This function needs to mutate the fields in order to handle partial - /// writes from the underlying OS layer. - /// * The OS layer expects pointer addresses to be inside the application's address space - /// even if the length is zero. Meanwhile, in Zig, slices may have undefined pointer - /// addresses when the length is zero. So this function modifies the iov_base fields - /// when the length is zero. - /// See https://github.com/ziglang/zig/issues/7699 - /// See equivalent function: `std.net.Stream.writevAll`. - pub fn writevAll(self: File, iovecs: []os.iovec_const) WriteError!void { - if (iovecs.len == 0) return; - - // We use the address of this local variable for all zero-length - // vectors so that the OS does not complain that we are giving it - // addresses outside the application's address space. - var garbage: [1]u8 = undefined; - for (iovecs) |*v| { - if (v.iov_len == 0) v.iov_base = &garbage; - } - - var i: usize = 0; - while (true) { - var amt = try self.writev(iovecs[i..]); - while (amt >= iovecs[i].iov_len) { - amt -= iovecs[i].iov_len; - i += 1; - if (i >= iovecs.len) return; - } - iovecs[i].iov_base += amt; - iovecs[i].iov_len -= amt; - } - } - - /// See https://github.com/ziglang/zig/issues/7699 - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn pwritev(self: File, iovecs: []os.iovec_const, offset: u64) PWriteError!usize { - if (is_windows) { - // TODO improve this to use WriteFileScatter - if (iovecs.len == 0) return @as(usize, 0); - const first = iovecs[0]; - return windows.WriteFile(self.handle, first.iov_base[0..first.iov_len], offset, self.intended_io_mode); - } - - if (self.intended_io_mode == .blocking) { - return os.pwritev(self.handle, iovecs, offset); - } else { - return std.event.Loop.instance.?.pwritev(self.handle, iovecs, offset, self.capable_io_mode != self.intended_io_mode); - } - } - - /// The `iovecs` parameter is mutable because this function needs to mutate the fields in - /// order to handle partial writes from the underlying OS layer. - /// See https://github.com/ziglang/zig/issues/7699 - /// On Windows, this function currently does alter the file pointer. - /// https://github.com/ziglang/zig/issues/12783 - pub fn pwritevAll(self: File, iovecs: []os.iovec_const, offset: u64) PWriteError!void { - if (iovecs.len == 0) return; - - var i: usize = 0; - var off: u64 = 0; - while (true) { - var amt = try self.pwritev(iovecs[i..], offset + off); - off += amt; - while (amt >= iovecs[i].iov_len) { - amt -= iovecs[i].iov_len; - i += 1; - if (i >= iovecs.len) return; - } - iovecs[i].iov_base += amt; - iovecs[i].iov_len -= amt; - } - } - - pub const CopyRangeError = os.CopyFileRangeError; - - pub fn copyRange(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { - const adjusted_len = math.cast(usize, len) orelse math.maxInt(usize); - const result = try os.copy_file_range(in.handle, in_offset, out.handle, out_offset, adjusted_len, 0); - return result; - } - - /// Returns the number of bytes copied. If the number read is smaller than `buffer.len`, it - /// means the in file reached the end. Reaching the end of a file is not an error condition. - pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { - var total_bytes_copied: u64 = 0; - var in_off = in_offset; - var out_off = out_offset; - while (total_bytes_copied < len) { - const amt_copied = try copyRange(in, in_off, out, out_off, len - total_bytes_copied); - if (amt_copied == 0) return total_bytes_copied; - total_bytes_copied += amt_copied; - in_off += amt_copied; - out_off += amt_copied; - } - return total_bytes_copied; - } - - pub const WriteFileOptions = struct { - in_offset: u64 = 0, - - /// `null` means the entire file. `0` means no bytes from the file. - /// When this is `null`, trailers must be sent in a separate writev() call - /// due to a flaw in the BSD sendfile API. Other operating systems, such as - /// Linux, already do this anyway due to API limitations. - /// If the size of the source file is known, passing the size here will save one syscall. - in_len: ?u64 = null, - - headers_and_trailers: []os.iovec_const = &[0]os.iovec_const{}, - - /// The trailer count is inferred from `headers_and_trailers.len - header_count` - header_count: usize = 0, - }; - - pub const WriteFileError = ReadError || error{EndOfStream} || WriteError; - - pub fn writeFileAll(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void { - return self.writeFileAllSendfile(in_file, args) catch |err| switch (err) { - error.Unseekable, - error.FastOpenAlreadyInProgress, - error.MessageTooBig, - error.FileDescriptorNotASocket, - error.NetworkUnreachable, - error.NetworkSubsystemFailed, - => return self.writeFileAllUnseekable(in_file, args), - - else => |e| return e, - }; - } - - /// Does not try seeking in either of the File parameters. - /// See `writeFileAll` as an alternative to calling this. - pub fn writeFileAllUnseekable(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void { - const headers = args.headers_and_trailers[0..args.header_count]; - const trailers = args.headers_and_trailers[args.header_count..]; - - try self.writevAll(headers); - - try in_file.reader().skipBytes(args.in_offset, .{ .buf_size = 4096 }); - - var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init(); - if (args.in_len) |len| { - var stream = std.io.limitedReader(in_file.reader(), len); - try fifo.pump(stream.reader(), self.writer()); - } else { - try fifo.pump(in_file.reader(), self.writer()); - } - - try self.writevAll(trailers); - } - - /// Low level function which can fail for OS-specific reasons. - /// See `writeFileAll` as an alternative to calling this. - /// TODO integrate with async I/O - fn writeFileAllSendfile(self: File, in_file: File, args: WriteFileOptions) os.SendFileError!void { - const count = blk: { - if (args.in_len) |l| { - if (l == 0) { - return self.writevAll(args.headers_and_trailers); - } else { - break :blk l; - } - } else { - break :blk 0; - } - }; - const headers = args.headers_and_trailers[0..args.header_count]; - const trailers = args.headers_and_trailers[args.header_count..]; - const zero_iovec = &[0]os.iovec_const{}; - // When reading the whole file, we cannot put the trailers in the sendfile() syscall, - // because we have no way to determine whether a partial write is past the end of the file or not. - const trls = if (count == 0) zero_iovec else trailers; - const offset = args.in_offset; - const out_fd = self.handle; - const in_fd = in_file.handle; - const flags = 0; - var amt: usize = 0; - hdrs: { - var i: usize = 0; - while (i < headers.len) { - amt = try os.sendfile(out_fd, in_fd, offset, count, headers[i..], trls, flags); - while (amt >= headers[i].iov_len) { - amt -= headers[i].iov_len; - i += 1; - if (i >= headers.len) break :hdrs; - } - headers[i].iov_base += amt; - headers[i].iov_len -= amt; - } - } - if (count == 0) { - var off: u64 = amt; - while (true) { - amt = try os.sendfile(out_fd, in_fd, offset + off, 0, zero_iovec, zero_iovec, flags); - if (amt == 0) break; - off += amt; - } - } else { - var off: u64 = amt; - while (off < count) { - amt = try os.sendfile(out_fd, in_fd, offset + off, count - off, zero_iovec, trailers, flags); - off += amt; - } - amt = @as(usize, @intCast(off - count)); - } - var i: usize = 0; - while (i < trailers.len) { - while (amt >= trailers[i].iov_len) { - amt -= trailers[i].iov_len; - i += 1; - if (i >= trailers.len) return; - } - trailers[i].iov_base += amt; - trailers[i].iov_len -= amt; - amt = try os.writev(self.handle, trailers[i..]); - } - } - - pub const Reader = io.Reader(File, ReadError, read); - - pub fn reader(file: File) Reader { - return .{ .context = file }; - } - - pub const Writer = io.Writer(File, WriteError, write); - - pub fn writer(file: File) Writer { - return .{ .context = file }; - } - - pub const SeekableStream = io.SeekableStream( - File, - SeekError, - GetSeekPosError, - seekTo, - seekBy, - getPos, - getEndPos, - ); - - pub fn seekableStream(file: File) SeekableStream { - return .{ .context = file }; - } - - const range_off: windows.LARGE_INTEGER = 0; - const range_len: windows.LARGE_INTEGER = 1; - - pub const LockError = error{ - SystemResources, - FileLocksNotSupported, - } || os.UnexpectedError; - - /// Blocks when an incompatible lock is held by another process. - /// A process may hold only one type of lock (shared or exclusive) on - /// a file. When a process terminates in any way, the lock is released. - /// - /// Assumes the file is unlocked. - /// - /// TODO: integrate with async I/O - pub fn lock(file: File, l: Lock) LockError!void { - if (is_windows) { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - const exclusive = switch (l) { - .none => return, - .shared => false, - .exclusive => true, - }; - return windows.LockFile( - file.handle, - null, - null, - null, - &io_status_block, - &range_off, - &range_len, - null, - windows.FALSE, // non-blocking=false - @intFromBool(exclusive), - ) catch |err| switch (err) { - error.WouldBlock => unreachable, // non-blocking=false - else => |e| return e, - }; - } else { - return os.flock(file.handle, switch (l) { - .none => os.LOCK.UN, - .shared => os.LOCK.SH, - .exclusive => os.LOCK.EX, - }) catch |err| switch (err) { - error.WouldBlock => unreachable, // non-blocking=false - else => |e| return e, - }; - } - } - - /// Assumes the file is locked. - pub fn unlock(file: File) void { - if (is_windows) { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - return windows.UnlockFile( - file.handle, - &io_status_block, - &range_off, - &range_len, - null, - ) catch |err| switch (err) { - error.RangeNotLocked => unreachable, // Function assumes unlocked. - error.Unexpected => unreachable, // Resource deallocation must succeed. - }; - } else { - return os.flock(file.handle, os.LOCK.UN) catch |err| switch (err) { - error.WouldBlock => unreachable, // unlocking can't block - error.SystemResources => unreachable, // We are deallocating resources. - error.FileLocksNotSupported => unreachable, // We already got the lock. - error.Unexpected => unreachable, // Resource deallocation must succeed. - }; - } - } - - /// Attempts to obtain a lock, returning `true` if the lock is - /// obtained, and `false` if there was an existing incompatible lock held. - /// A process may hold only one type of lock (shared or exclusive) on - /// a file. When a process terminates in any way, the lock is released. - /// - /// Assumes the file is unlocked. - /// - /// TODO: integrate with async I/O - pub fn tryLock(file: File, l: Lock) LockError!bool { - if (is_windows) { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - const exclusive = switch (l) { - .none => return, - .shared => false, - .exclusive => true, - }; - windows.LockFile( - file.handle, - null, - null, - null, - &io_status_block, - &range_off, - &range_len, - null, - windows.TRUE, // non-blocking=true - @intFromBool(exclusive), - ) catch |err| switch (err) { - error.WouldBlock => return false, - else => |e| return e, - }; - } else { - os.flock(file.handle, switch (l) { - .none => os.LOCK.UN, - .shared => os.LOCK.SH | os.LOCK.NB, - .exclusive => os.LOCK.EX | os.LOCK.NB, - }) catch |err| switch (err) { - error.WouldBlock => return false, - else => |e| return e, - }; - } - return true; - } - - /// Assumes the file is already locked in exclusive mode. - /// Atomically modifies the lock to be in shared mode, without releasing it. - /// - /// TODO: integrate with async I/O - pub fn downgradeLock(file: File) LockError!void { - if (is_windows) { - // On Windows it works like a semaphore + exclusivity flag. To implement this - // function, we first obtain another lock in shared mode. This changes the - // exclusivity flag, but increments the semaphore to 2. So we follow up with - // an NtUnlockFile which decrements the semaphore but does not modify the - // exclusivity flag. - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - windows.LockFile( - file.handle, - null, - null, - null, - &io_status_block, - &range_off, - &range_len, - null, - windows.TRUE, // non-blocking=true - windows.FALSE, // exclusive=false - ) catch |err| switch (err) { - error.WouldBlock => unreachable, // File was not locked in exclusive mode. - else => |e| return e, - }; - return windows.UnlockFile( - file.handle, - &io_status_block, - &range_off, - &range_len, - null, - ) catch |err| switch (err) { - error.RangeNotLocked => unreachable, // File was not locked. - error.Unexpected => unreachable, // Resource deallocation must succeed. - }; - } else { - return os.flock(file.handle, os.LOCK.SH | os.LOCK.NB) catch |err| switch (err) { - error.WouldBlock => unreachable, // File was not locked in exclusive mode. - else => |e| return e, - }; - } - } -}; -- 2.54.0