commit 814480db7cd0ab1d8441919223a88ee411f54ea8 (tree)
parent d1d2c37af26902f953b2b72335b326c4b01e3bb2
Author: Andrew Kelley <andrew@ziglang.org>
Date: Thu, 4 Dec 2025 16:03:33 -0800
std: all File functions moved to std.Io
Diffstat:
17 files changed, 2349 insertions(+), 2305 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
@@ -436,7 +436,6 @@ set(ZIG_STAGE2_SOURCES
lib/std/fmt.zig
lib/std/fmt/parse_float.zig
lib/std/fs.zig
- lib/std/fs/File.zig
lib/std/fs/get_app_data_dir.zig
lib/std/fs/path.zig
lib/std/hash.zig
diff --git a/lib/std/Io.zig b/lib/std/Io.zig
@@ -679,11 +679,13 @@ pub const VTable = struct {
dirRename: *const fn (?*anyopaque, old_dir: Dir, old_sub_path: []const u8, new_dir: Dir, new_sub_path: []const u8) Dir.RenameError!void,
dirSymLink: *const fn (?*anyopaque, Dir, target_path: []const u8, sym_link_path: []const u8, Dir.SymLinkFlags) Dir.RenameError!void,
dirReadLink: *const fn (?*anyopaque, Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize,
- dirSetMode: *const fn (?*anyopaque, Dir, File.Mode) Dir.SetModeError!void,
dirSetOwner: *const fn (?*anyopaque, Dir, ?File.Uid, ?File.Gid) Dir.SetOwnerError!void,
dirSetPermissions: *const fn (?*anyopaque, Dir, Dir.Permissions) Dir.SetPermissionsError!void,
+ dirSetTimestamps: *const fn (?*anyopaque, Dir, []const u8, last_accessed: Timestamp, last_modified: Timestamp, Dir.SetTimestampsOptions) Dir.SetTimestampsError!void,
+ dirSetTimestampsNow: *const fn (?*anyopaque, Dir, []const u8, Dir.SetTimestampsOptions) Dir.SetTimestampsError!void,
fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat,
+ fileLength: *const fn (?*anyopaque, File) File.LengthError!u64,
fileClose: *const fn (?*anyopaque, File) void,
fileWriteStreaming: *const fn (?*anyopaque, File, buffer: [][]const u8) File.WriteStreamingError!usize,
fileWritePositional: *const fn (?*anyopaque, File, buffer: [][]const u8, offset: u64) File.WritePositionalError!usize,
@@ -694,6 +696,19 @@ pub const VTable = struct {
fileSeekBy: *const fn (?*anyopaque, File, relative_offset: i64) File.SeekError!void,
fileSeekTo: *const fn (?*anyopaque, File, absolute_offset: u64) File.SeekError!void,
openSelfExe: *const fn (?*anyopaque, File.OpenFlags) File.OpenSelfExeError!File,
+ fileSync: *const fn (?*anyopaque, File) File.SyncError!void,
+ fileIsTty: *const fn (?*anyopaque, File) Cancelable!bool,
+ fileEnableAnsiEscapeCodes: *const fn (?*anyopaque, File) File.EnableAnsiEscapeCodesError!void,
+ fileSupportsAnsiEscapeCodes: *const fn (?*anyopaque, File) Cancelable!bool,
+ fileSetLength: *const fn (?*anyopaque, File, u64) File.SetLengthError!void,
+ fileSetOwner: *const fn (?*anyopaque, File, ?File.Uid, ?File.Gid) File.SetOwnerError!void,
+ fileSetPermissions: *const fn (?*anyopaque, File, File.Permissions) File.SetPermissionsError!void,
+ fileSetTimestamps: *const fn (?*anyopaque, File, last_accessed: Timestamp, last_modified: Timestamp, File.SetTimestampsOptions) File.SetTimestampsError!void,
+ fileSetTimestampsNow: *const fn (?*anyopaque, File, File.SetTimestampsOptions) File.SetTimestampsError!void,
+ fileLock: *const fn (?*anyopaque, File, File.Lock) File.LockError!void,
+ fileTryLock: *const fn (?*anyopaque, File, File.Lock) File.LockError!bool,
+ fileUnlock: *const fn (?*anyopaque, File) void,
+ fileDowngradeLock: *const fn (?*anyopaque, File) File.DowngradeLockError!void,
now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp,
sleep: *const fn (?*anyopaque, Timeout) SleepError!void,
diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig
@@ -11,9 +11,6 @@ const Allocator = std.mem.Allocator;
handle: Handle,
-pub const Mode = Io.File.Mode;
-pub const default_mode: Mode = 0o755;
-
pub const Entry = struct {
name: []const u8,
kind: File.Kind,
@@ -435,10 +432,10 @@ pub const PrevStatus = enum {
pub const UpdateFileError = File.OpenError;
-/// Check the file size, mtime, and mode of `source_path` and `dest_path`. If
+/// Check the file size, mtime, and permissions of `source_path` and `dest_path`. If
/// they are equal, does nothing. Otherwise, atomically copies `source_path` to
/// `dest_path`, creating the parent directory hierarchy as needed. The
-/// destination file gains the mtime, atime, and mode of the source file so
+/// destination file gains the mtime, atime, and permissions of the source file so
/// that the next call to `updateFile` will not need a copy.
///
/// Returns the previous status of the file before updating.
@@ -459,7 +456,7 @@ pub fn updateFile(
defer src_file.close(io);
const src_stat = try src_file.stat(io);
- const actual_mode = options.override_mode orelse src_stat.mode;
+ const actual_permissions = options.override_permissions orelse src_stat.permissions;
check_dest_stat: {
const dest_stat = blk: {
var dest_file = dest_dir.openFile(io, dest_path, .{}) catch |err| switch (err) {
@@ -473,19 +470,19 @@ pub fn updateFile(
if (src_stat.size == dest_stat.size and
src_stat.mtime.nanoseconds == dest_stat.mtime.nanoseconds and
- actual_mode == dest_stat.mode)
+ actual_permissions == dest_stat.permissions)
{
return .fresh;
}
}
if (std.fs.path.dirname(dest_path)) |dirname| {
- try dest_dir.makePathMode(io, dirname, default_mode);
+ try dest_dir.makePath(io, dirname, .default_dir);
}
var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available.
var atomic_file = try std.fs.Dir.atomicFile(.adaptFromNewApi(dest_dir), dest_path, .{
- .mode = actual_mode,
+ .permissions = actual_permissions,
.write_buffer = &buffer,
});
defer atomic_file.deinit();
@@ -555,17 +552,12 @@ pub const MakeError = error{
/// Related:
/// * `makePath`
/// * `makeDirAbsolute`
-pub fn makeDir(dir: Dir, io: Io, sub_path: []const u8) MakeError!void {
- return io.vtable.dirMake(io.userdata, dir, sub_path, default_mode);
+pub fn makeDir(dir: Dir, io: Io, sub_path: []const u8, permissions: Permissions) MakeError!void {
+ return io.vtable.dirMake(io.userdata, dir, sub_path, permissions);
}
pub const MakePathError = MakeError || StatPathError;
-/// Same as `makePathMode` but passes `default_mode`.
-pub fn makePath(dir: Dir, io: Io, sub_path: []const u8) MakePathError!void {
- _ = try io.vtable.dirMakePath(io.userdata, dir, sub_path, default_mode);
-}
-
/// Creates parent directories as necessary to ensure `sub_path` exists as a directory.
///
/// Returns success if the path already exists and is a directory.
@@ -587,16 +579,16 @@ pub fn makePath(dir: Dir, io: Io, sub_path: []const u8) MakePathError!void {
/// - On other platforms, `..` are not resolved before the path is passed to `mkdirat`,
/// meaning a `sub_path` like "first/../second" will create both a `./first`
/// and a `./second` directory.
-pub fn makePathMode(dir: Dir, io: Io, sub_path: []const u8, mode: Mode) MakePathError!void {
- _ = try io.vtable.dirMakePath(io.userdata, dir, sub_path, mode);
+pub fn makePath(dir: Dir, io: Io, sub_path: []const u8, permissions: Permissions) MakePathError!void {
+ _ = try io.vtable.dirMakePath(io.userdata, dir, sub_path, permissions);
}
pub const MakePathStatus = enum { existed, created };
/// Same as `makePath` except returns whether the path already existed or was
/// successfully created.
-pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!MakePathStatus {
- return io.vtable.dirMakePath(io.userdata, dir, sub_path, default_mode);
+pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8, permissions: Permissions) MakePathError!MakePathStatus {
+ return io.vtable.dirMakePath(io.userdata, dir, sub_path, permissions);
}
pub const MakeOpenPathError = MakeError || OpenError || StatPathError;
@@ -609,8 +601,8 @@ pub const MakeOpenPathError = MakeError || OpenError || StatPathError;
/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
/// On WASI, `sub_path` should be encoded as valid UTF-8.
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
-pub fn makeOpenPath(dir: Dir, io: Io, sub_path: []const u8, options: OpenOptions) MakeOpenPathError!Dir {
- return io.vtable.dirMakeOpenPath(io.userdata, dir, sub_path, options);
+pub fn makeOpenPath(dir: Dir, io: Io, sub_path: []const u8, permissions: Permissions, options: OpenOptions) MakeOpenPathError!Dir {
+ return io.vtable.dirMakeOpenPath(io.userdata, dir, sub_path, permissions, options);
}
pub const Stat = File.Stat;
@@ -1453,8 +1445,8 @@ fn deleteTreeOpenInitialSubpath(dir: Dir, sub_path: []const u8, kind_hint: File.
}
pub const CopyFileOptions = struct {
- /// When this is `null` the mode is copied from the source file.
- override_mode: ?File.Mode = null,
+ /// When this is `null` the permissions are copied from the source file.
+ override_permissions: ?File.Permissions = null,
};
pub const CopyFileError = File.OpenError || File.StatError ||
@@ -1486,15 +1478,15 @@ pub fn copyFile(
var file_reader: File.Reader = .init(.{ .handle = file.handle }, io, &.{});
defer file_reader.file.close(io);
- const mode = options.override_mode orelse blk: {
+ const permissions = options.override_permissions orelse blk: {
const st = try file_reader.file.stat(io);
file_reader.size = st.size;
- break :blk st.mode;
+ break :blk st.permissions;
};
var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available.
var atomic_file = try dest_dir.atomicFile(io, dest_path, .{
- .mode = mode,
+ .permissions = permissions,
.write_buffer = &buffer,
});
defer atomic_file.deinit(io);
@@ -1508,7 +1500,7 @@ pub fn copyFile(
}
pub const AtomicFileOptions = struct {
- mode: File.Mode = File.default_mode,
+ permissions: File.Permissions = .default_file,
make_path: bool = false,
write_buffer: []u8,
};
@@ -1530,13 +1522,14 @@ pub fn atomicFile(parent: Dir, io: Io, dest_path: []const u8, options: AtomicFil
else
try parent.openDir(io, dirname, .{});
- return .init(std.fs.path.basename(dest_path), options.mode, dir, true, options.write_buffer);
+ return .init(std.fs.path.basename(dest_path), options.permissions, dir, true, options.write_buffer);
} else {
- return .init(dest_path, options.mode, parent, false, options.write_buffer);
+ return .init(dest_path, options.permissions, parent, false, options.write_buffer);
}
}
-pub const SetModeError = File.SetModeError;
+pub const SetPermissionsError = File.SetPermissionsError;
+pub const Permissions = File.Permissions;
/// Also known as "chmod".
///
@@ -1544,8 +1537,8 @@ pub const SetModeError = File.SetModeError;
/// successfully, or must have the effective user ID matching the owner
/// of the directory. Additionally, the directory must have been opened
/// with `OpenOptions.iterate` set to `true`.
-pub fn setMode(dir: Dir, io: Io, new_mode: File.Mode) SetModeError!void {
- return io.vtable.dirSetMode(io.userdata, dir, new_mode);
+pub fn setPermissions(dir: Dir, io: Io, new_permissions: File.Permissions) SetPermissionsError!void {
+ return io.vtable.dirSetPermissions(io.userdata, dir, new_permissions);
}
pub const SetOwnerError = File.SetOwnerError;
@@ -1561,9 +1554,31 @@ pub fn setOwner(dir: Dir, io: Io, owner: ?File.Uid, group: ?File.Gid) SetOwnerEr
return io.vtable.dirSetOwner(io.userdata, dir, owner, group);
}
-pub const SetPermissionsError = File.SetPermissionsError;
-pub const Permissions = File.Permissions;
+pub const SetTimestampsError = File.SetTimestampsError;
+
+pub const SetTimestampsOptions = struct {
+ follow_symlinks: bool = true,
+};
+
+/// The granularity that ultimately is stored depends on the combination of
+/// operating system and file system. When a value as provided that exceeds
+/// this range, the value is clamped to the maximum.
+pub fn setTimestamps(
+ dir: Dir,
+ io: Io,
+ sub_path: []const u8,
+ last_accessed: Io.Timestamp,
+ last_modified: Io.Timestamp,
+ options: SetTimestampsOptions,
+) SetTimestampsError!void {
+ return io.vtable.dirSetTimestamps(io.userdata, dir, sub_path, last_accessed, last_modified, options);
+}
-pub fn setPermissions(dir: Dir, io: Io, permissions: Permissions) SetPermissionsError!void {
- return io.vtable.dirSetPermissions(io.userdata, dir, permissions);
+/// Sets the accessed and modification timestamps of the provided path to the
+/// current wall clock time.
+///
+/// The granularity that ultimately is stored depends on the combination of
+/// operating system and file system.
+pub fn setTimestampsNow(dir: Dir, io: Io, sub_path: []const u8, options: SetTimestampsOptions) SetTimestampsError!void {
+ return io.vtable.fileSetTimestampsNow(io.userdata, dir, sub_path, options);
}
diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig
@@ -11,20 +11,15 @@ const Dir = std.Io.Dir;
handle: Handle,
+pub const Reader = @import("File/Reader.zig");
+pub const Writer = @import("File/Writer.zig");
+pub const Atomic = @import("File/Atomic.zig");
+
pub const Handle = std.posix.fd_t;
-pub const Mode = std.posix.mode_t;
pub const INode = std.posix.ino_t;
pub const Uid = std.posix.uid_t;
pub const Gid = std.posix.gid_t;
-/// 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: Mode = if (Mode == u0) 0 else 0o666;
-
pub const Kind = enum {
block_device,
character_device,
@@ -53,8 +48,7 @@ pub const Stat = struct {
/// is unique to each filesystem.
inode: INode,
size: u64,
- /// This is available on POSIX systems and is always 0 otherwise.
- mode: Mode,
+ permissions: Permissions,
kind: Kind,
/// Last access time in nanoseconds, relative to UTC 1970-01-01.
atime: Io.Timestamp,
@@ -103,11 +97,6 @@ pub const Lock = enum {
exclusive,
};
-pub const LockError = error{
- SystemResources,
- FileLocksNotSupported,
-} || Io.UnexpectedError;
-
pub const OpenFlags = struct {
mode: OpenMode = .read_only,
@@ -200,9 +189,7 @@ pub const CreateFlags = struct {
/// is available to proceed.
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,
+ permissions: Permissions = .default_file,
};
pub const OpenError = error{
@@ -251,7 +238,7 @@ pub const OpenError = error{
/// The path already exists and the `CREAT` and `EXCL` flags were provided.
PathAlreadyExists,
DeviceBusy,
- FileLocksNotSupported,
+ FileLocksUnsupported,
/// One of these three things:
/// * pathname refers to an executable image which is currently being
/// executed and write access was requested.
@@ -269,7 +256,216 @@ pub fn close(file: File, io: Io) void {
return io.vtable.fileClose(io.userdata, file);
}
-pub const OpenSelfExeError = OpenError || std.fs.SelfExePathError || std.posix.FlockError;
+pub const SyncError = error{
+ InputOutput,
+ NoSpaceLeft,
+ DiskQuota,
+ AccessDenied,
+} || Io.Cancelable || Io.UnexpectedError;
+
+/// Blocks until all pending file contents and metadata modifications for the
+/// file have been synchronized with the underlying filesystem.
+///
+/// This does not ensure that metadata for the directory containing the file
+/// has also reached disk.
+pub fn sync(file: File, io: Io) SyncError!void {
+ return io.vtable.fileSync(io.userdata, file);
+}
+
+/// Test whether the file refers to a terminal.
+///
+/// See also:
+/// * `getOrEnableAnsiEscapeSupport`
+/// * `supportsAnsiEscapeCodes`.
+pub fn isTty(file: File, io: Io) bool {
+ return io.vtable.fileIsTty(io.userdata, file);
+}
+
+pub const EnableAnsiEscapeCodesError = error{} || Io.Cancelable || Io.UnexpectedError;
+
+pub fn enableAnsiEscapeCodes(file: File, io: Io) EnableAnsiEscapeCodesError {
+ return io.vtable.fileEnableAnsiEscapeCodes(io.userdata, file);
+}
+
+/// Test whether ANSI escape codes will be treated as such without
+/// attempting to enable support for ANSI escape codes.
+pub fn supportsAnsiEscapeCodes(file: File, io: Io) Io.Cancelable!bool {
+ return io.vtable.fileSupportsAnsiEscapeCodes(io.userdata, file);
+}
+
+pub const SetLengthError = error{
+ FileTooBig,
+ InputOutput,
+ FileBusy,
+ AccessDenied,
+ PermissionDenied,
+ NonResizable,
+} || Io.Cancelable || Io.UnexpectedError;
+
+/// Truncates or expands the file, populating any new data with zeroes.
+///
+/// The file offset after this call is left unchanged.
+pub fn setLength(file: File, io: Io, new_length: u64) SetLengthError!void {
+ return io.vtable.fileSetLength(io.userdata, file, new_length);
+}
+
+pub const LengthError = StatError;
+
+/// Retrieve the ending byte index of the file.
+///
+/// Sometimes cheaper than `stat` if only the length is needed.
+pub fn length(file: File, io: Io) LengthError!u64 {
+ return io.vtable.fileLength(io.userdata, file);
+}
+
+pub const SetPermissionsError = error{
+ AccessDenied,
+ PermissionDenied,
+ InputOutput,
+ SymLinkLoop,
+ FileNotFound,
+ SystemResources,
+ ReadOnlyFileSystem,
+} || Io.Cancelable || Io.UnexpectedError;
+
+/// Also known as "chmod".
+///
+/// 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 setPermissions(file: File, io: Io, new_permissions: Permissions) SetPermissionsError!void {
+ return io.vtable.fileSetPermissions(io.userdata, file, new_permissions);
+}
+
+pub const SetOwnerError = error{
+ AccessDenied,
+ PermissionDenied,
+ InputOutput,
+ SymLinkLoop,
+ FileNotFound,
+ SystemResources,
+ ReadOnlyFileSystem,
+} || Io.Cancelable || Io.UnexpectedError;
+
+/// Also known as "chown".
+///
+/// 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 setOwner(file: File, io: Io, owner: ?Uid, group: ?Gid) SetOwnerError!void {
+ return io.vtable.fileSetOwner(io.userdata, file, owner, group);
+}
+
+/// Cross-platform representation of permissions on a file.
+///
+/// On POSIX systems this corresponds to "mode" and on Windows this corresponds to "attributes".
+///
+/// Overridable via `std.options`.
+pub const Permissions = std.options.FilePermissions orelse if (is_windows) enum(std.os.windows.DWORD) {
+ default_file = 0,
+ _,
+
+ pub const default_dir: @This() = .default_file;
+ pub const has_executable_bit = false;
+
+ const windows = std.os.windows;
+
+ pub fn toAttributes(self: @This()) windows.DWORD {
+ return @intFromEnum(self);
+ }
+
+ pub fn readOnly(self: @This()) bool {
+ const attributes = toAttributes(self);
+ return attributes & windows.FILE_ATTRIBUTE_READONLY != 0;
+ }
+
+ pub fn setReadOnly(self: @This(), read_only: bool) @This() {
+ const attributes = toAttributes(self);
+ return @enumFromInt(if (read_only)
+ attributes | windows.FILE_ATTRIBUTE_READONLY
+ else
+ attributes & ~@as(windows.DWORD, windows.FILE_ATTRIBUTE_READONLY));
+ }
+} else if (std.posix.mode_t != u0) enum(std.posix.mode_t) {
+ /// 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.
+ default_file = 0o666,
+ default_dir = 0o755,
+ _,
+
+ pub const has_executable_bit = true;
+
+ pub fn toMode(self: @This()) std.posix.mode_t {
+ return @intFromEnum(self);
+ }
+
+ /// Returns `true` if and only if no class has write permissions.
+ pub fn readOnly(self: @This()) bool {
+ const mode = toMode(self);
+ return mode & 0o222 == 0;
+ }
+
+ /// Enables write permission for all classes.
+ pub fn setReadOnly(self: @This(), read_only: bool) @This() {
+ const mode = toMode(self);
+ const o222 = @as(std.posix.mode_t, 0o222);
+ return @enumFromInt(if (read_only) mode & ~o222 else mode | o222);
+ }
+} else enum(u0) {
+ default_file = 0,
+ pub const default_dir: @This() = .default_file;
+ pub const has_executable_bit = false;
+};
+
+pub const SetTimestampsError = error{
+ /// times is NULL, or both nsec values are UTIME_NOW, and either:
+ /// * the effective user ID of the caller does not match the owner
+ /// of the file, the caller does not have write access to the
+ /// file, and the caller is not privileged (Linux: does not have
+ /// either the CAP_FOWNER or the CAP_DAC_OVERRIDE capability);
+ /// or,
+ /// * the file is marked immutable (see chattr(1)).
+ AccessDenied,
+ /// The caller attempted to change one or both timestamps to a value
+ /// other than the current time, or to change one of the timestamps
+ /// to the current time while leaving the other timestamp unchanged,
+ /// (i.e., times is not NULL, neither nsec field is UTIME_NOW,
+ /// and neither nsec field is UTIME_OMIT) and either:
+ /// * the caller's effective user ID does not match the owner of
+ /// file, and the caller is not privileged (Linux: does not have
+ /// the CAP_FOWNER capability); or,
+ /// * the file is marked append-only or immutable (see chattr(1)).
+ PermissionDenied,
+ ReadOnlyFileSystem,
+} || Io.Cancelable || Io.UnexpectedError;
+
+/// The granularity that ultimately is stored depends on the combination of
+/// operating system and file system. When a value as provided that exceeds
+/// this range, the value is clamped to the maximum.
+pub fn setTimestamps(
+ file: File,
+ io: Io,
+ last_accessed: Io.Timestamp,
+ last_modified: Io.Timestamp,
+) SetTimestampsError!void {
+ return io.vtable.fileUpdateTimes(io.userdata, file, last_accessed, last_modified);
+}
+
+/// Sets the accessed and modification timestamps of `file` to the current wall
+/// clock time.
+///
+/// The granularity that ultimately is stored depends on the combination of
+/// operating system and file system.
+pub fn setTimestampsNow(file: File, io: Io) SetTimestampsError!void {
+ return io.vtable.fileSetTimestampsNow(io.userdata, file);
+}
+
+pub const OpenSelfExeError = OpenError || std.fs.SelfExePathError || LockError;
pub fn openSelfExe(io: Io, flags: OpenFlags) OpenSelfExeError!File {
return io.vtable.openSelfExe(io.userdata, flags);
@@ -309,6 +505,12 @@ pub fn openAbsolute(io: Io, absolute_path: []const u8, flags: OpenFlags) OpenErr
return Io.Dir.cwd().openFile(io, absolute_path, flags);
}
+pub const SeekError = error{
+ Unseekable,
+ /// The file descriptor does not hold the required rights to seek on it.
+ AccessDenied,
+} || Io.Cancelable || Io.UnexpectedError;
+
/// Defaults to positional reading; falls back to streaming.
///
/// Positional is more threadsafe, since the global seek position is not
@@ -324,509 +526,54 @@ pub fn readerStreaming(file: File, io: Io, buffer: []u8) Reader {
return .initStreaming(file, io, buffer);
}
-pub const SeekError = error{
- Unseekable,
- /// The file descriptor does not hold the required rights to seek on it.
- AccessDenied,
-} || Io.Cancelable || Io.UnexpectedError;
-
-/// Memoizes key information about a file handle such as:
-/// * The size from calling stat, or the error that occurred therein.
-/// * The current seek position.
-/// * The error that occurred when trying to seek.
-/// * Whether reading should be done positionally or streaming.
-/// * Whether reading should be done via fd-to-fd syscalls (e.g. `sendfile`)
-/// versus plain variants (e.g. `read`).
+/// Defaults to positional reading; falls back to streaming.
///
-/// Fulfills the `Io.Reader` interface.
-pub const Reader = struct {
- io: Io,
- file: File,
- err: ?Error = null,
- mode: Reader.Mode = .positional,
- /// Tracks the true seek position in the file. To obtain the logical
- /// position, use `logicalPos`.
- pos: u64 = 0,
- size: ?u64 = null,
- size_err: ?SizeError = null,
- seek_err: ?Reader.SeekError = null,
- interface: Io.Reader,
-
- pub const Error = error{
- InputOutput,
- SystemResources,
- IsDir,
- BrokenPipe,
- ConnectionResetByPeer,
- Timeout,
- /// In WASI, EBADF is mapped to this error because it is returned when
- /// trying to read a directory file descriptor as if it were a file.
- NotOpenForReading,
- SocketUnconnected,
- /// This error occurs when no global event loop is configured,
- /// and reading from the file descriptor would block.
- WouldBlock,
- /// In WASI, this error occurs when the file descriptor does
- /// not hold the required rights to read from it.
- AccessDenied,
- /// This error occurs in Linux if the process to be read from
- /// no longer exists.
- ProcessNotFound,
- /// Unable to read file due to lock.
- LockViolation,
- } || Io.Cancelable || Io.UnexpectedError;
-
- pub const SizeError = std.os.windows.GetFileSizeError || StatError || error{
- /// Occurs if, for example, the file handle is a network socket and therefore does not have a size.
- Streaming,
- };
-
- pub const SeekError = File.SeekError || error{
- /// Seeking fell back to reading, and reached the end before the requested seek position.
- /// `pos` remains at the end of the file.
- EndOfStream,
- /// Seeking fell back to reading, which failed.
- ReadFailed,
- };
-
- pub const Mode = enum {
- streaming,
- positional,
- /// Avoid syscalls other than `read` and `readv`.
- streaming_reading,
- /// Avoid syscalls other than `pread` and `preadv`.
- positional_reading,
- /// Indicates reading cannot continue because of a seek failure.
- failure,
-
- pub fn toStreaming(m: @This()) @This() {
- return switch (m) {
- .positional, .streaming => .streaming,
- .positional_reading, .streaming_reading => .streaming_reading,
- .failure => .failure,
- };
- }
-
- pub fn toReading(m: @This()) @This() {
- return switch (m) {
- .positional, .positional_reading => .positional_reading,
- .streaming, .streaming_reading => .streaming_reading,
- .failure => .failure,
- };
- }
- };
-
- pub fn initInterface(buffer: []u8) Io.Reader {
- return .{
- .vtable = &.{
- .stream = Reader.stream,
- .discard = Reader.discard,
- .readVec = Reader.readVec,
- },
- .buffer = buffer,
- .seek = 0,
- .end = 0,
- };
- }
-
- pub fn init(file: File, io: Io, buffer: []u8) Reader {
- return .{
- .io = io,
- .file = file,
- .interface = initInterface(buffer),
- };
- }
-
- pub fn initSize(file: File, io: Io, buffer: []u8, size: ?u64) Reader {
- return .{
- .io = io,
- .file = file,
- .interface = initInterface(buffer),
- .size = size,
- };
- }
-
- /// Positional is more threadsafe, since the global seek position is not
- /// affected, but when such syscalls are not available, preemptively
- /// initializing in streaming mode skips a failed syscall.
- pub fn initStreaming(file: File, io: Io, buffer: []u8) Reader {
- return .{
- .io = io,
- .file = file,
- .interface = Reader.initInterface(buffer),
- .mode = .streaming,
- .seek_err = error.Unseekable,
- .size_err = error.Streaming,
- };
- }
-
- pub fn getSize(r: *Reader) SizeError!u64 {
- return r.size orelse {
- if (r.size_err) |err| return err;
- if (stat(r.file, r.io)) |st| {
- if (st.kind == .file) {
- r.size = st.size;
- return st.size;
- } else {
- r.mode = r.mode.toStreaming();
- r.size_err = error.Streaming;
- return error.Streaming;
- }
- } else |err| {
- r.size_err = err;
- return err;
- }
- };
- }
-
- pub fn seekBy(r: *Reader, offset: i64) Reader.SeekError!void {
- const io = r.io;
- switch (r.mode) {
- .positional, .positional_reading => {
- setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset));
- },
- .streaming, .streaming_reading => {
- const seek_err = r.seek_err orelse e: {
- if (io.vtable.fileSeekBy(io.userdata, r.file, offset)) {
- setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset));
- return;
- } else |err| {
- r.seek_err = err;
- break :e err;
- }
- };
- var remaining = std.math.cast(u64, offset) orelse return seek_err;
- while (remaining > 0) {
- remaining -= discard(&r.interface, .limited64(remaining)) catch |err| {
- r.seek_err = err;
- return err;
- };
- }
- r.interface.tossBuffered();
- },
- .failure => return r.seek_err.?,
- }
- }
-
- /// Repositions logical read offset relative to the beginning of the file.
- pub fn seekTo(r: *Reader, offset: u64) Reader.SeekError!void {
- const io = r.io;
- switch (r.mode) {
- .positional, .positional_reading => {
- setLogicalPos(r, offset);
- },
- .streaming, .streaming_reading => {
- const logical_pos = logicalPos(r);
- if (offset >= logical_pos) return Reader.seekBy(r, @intCast(offset - logical_pos));
- if (r.seek_err) |err| return err;
- io.vtable.fileSeekTo(io.userdata, r.file, offset) catch |err| {
- r.seek_err = err;
- return err;
- };
- setLogicalPos(r, offset);
- },
- .failure => return r.seek_err.?,
- }
- }
-
- pub fn logicalPos(r: *const Reader) u64 {
- return r.pos - r.interface.bufferedLen();
- }
-
- fn setLogicalPos(r: *Reader, offset: u64) void {
- const logical_pos = r.logicalPos();
- if (offset < logical_pos or offset >= r.pos) {
- r.interface.tossBuffered();
- r.pos = offset;
- } else r.interface.toss(@intCast(offset - logical_pos));
- }
-
- /// Number of slices to store on the stack, when trying to send as many byte
- /// vectors through the underlying read calls as possible.
- const max_buffers_len = 16;
-
- fn stream(io_reader: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize {
- const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
- return streamMode(r, w, limit, r.mode);
- }
-
- pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Reader.Mode) Io.Reader.StreamError!usize {
- switch (mode) {
- .positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) {
- error.Unimplemented => {
- r.mode = r.mode.toReading();
- return 0;
- },
- else => |e| return e,
- },
- .positional_reading => {
- const dest = limit.slice(try w.writableSliceGreedy(1));
- var data: [1][]u8 = .{dest};
- const n = try readVecPositional(r, &data);
- w.advance(n);
- return n;
- },
- .streaming_reading => {
- const dest = limit.slice(try w.writableSliceGreedy(1));
- var data: [1][]u8 = .{dest};
- const n = try readVecStreaming(r, &data);
- w.advance(n);
- return n;
- },
- .failure => return error.ReadFailed,
- }
- }
-
- fn readVec(io_reader: *Io.Reader, data: [][]u8) Io.Reader.Error!usize {
- const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
- switch (r.mode) {
- .positional, .positional_reading => return readVecPositional(r, data),
- .streaming, .streaming_reading => return readVecStreaming(r, data),
- .failure => return error.ReadFailed,
- }
- }
-
- fn readVecPositional(r: *Reader, data: [][]u8) Io.Reader.Error!usize {
- const io = r.io;
- var iovecs_buffer: [max_buffers_len][]u8 = undefined;
- const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data);
- const dest = iovecs_buffer[0..dest_n];
- assert(dest[0].len > 0);
- const n = io.vtable.fileReadPositional(io.userdata, r.file, dest, r.pos) catch |err| switch (err) {
- error.Unseekable => {
- r.mode = r.mode.toStreaming();
- const pos = r.pos;
- if (pos != 0) {
- r.pos = 0;
- r.seekBy(@intCast(pos)) catch {
- r.mode = .failure;
- return error.ReadFailed;
- };
- }
- return 0;
- },
- else => |e| {
- r.err = e;
- return error.ReadFailed;
- },
- };
- if (n == 0) {
- r.size = r.pos;
- return error.EndOfStream;
- }
- r.pos += n;
- if (n > data_size) {
- r.interface.end += n - data_size;
- return data_size;
- }
- return n;
- }
-
- fn readVecStreaming(r: *Reader, data: [][]u8) Io.Reader.Error!usize {
- const io = r.io;
- var iovecs_buffer: [max_buffers_len][]u8 = undefined;
- const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data);
- const dest = iovecs_buffer[0..dest_n];
- assert(dest[0].len > 0);
- const n = io.vtable.fileReadStreaming(io.userdata, r.file, dest) catch |err| {
- r.err = err;
- return error.ReadFailed;
- };
- if (n == 0) {
- r.size = r.pos;
- return error.EndOfStream;
- }
- r.pos += n;
- if (n > data_size) {
- r.interface.end += n - data_size;
- return data_size;
- }
- return n;
- }
-
- fn discard(io_reader: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize {
- const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
- const io = r.io;
- const file = r.file;
- switch (r.mode) {
- .positional, .positional_reading => {
- const size = r.getSize() catch {
- r.mode = r.mode.toStreaming();
- return 0;
- };
- const logical_pos = logicalPos(r);
- const delta = @min(@intFromEnum(limit), size - logical_pos);
- setLogicalPos(r, logical_pos + delta);
- return delta;
- },
- .streaming, .streaming_reading => {
- // Unfortunately we can't seek forward without knowing the
- // size because the seek syscalls provided to us will not
- // return the true end position if a seek would exceed the
- // end.
- fallback: {
- if (r.size_err == null and r.seek_err == null) break :fallback;
-
- const buffered_len = r.interface.bufferedLen();
- var remaining = @intFromEnum(limit);
- if (remaining <= buffered_len) {
- r.interface.seek += remaining;
- return remaining;
- }
- remaining -= buffered_len;
- r.interface.seek = 0;
- r.interface.end = 0;
-
- var trash_buffer: [128]u8 = undefined;
- var data: [1][]u8 = .{trash_buffer[0..@min(trash_buffer.len, remaining)]};
- var iovecs_buffer: [max_buffers_len][]u8 = undefined;
- const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, &data);
- const dest = iovecs_buffer[0..dest_n];
- assert(dest[0].len > 0);
- const n = io.vtable.fileReadStreaming(io.userdata, file, dest) catch |err| {
- r.err = err;
- return error.ReadFailed;
- };
- if (n == 0) {
- r.size = r.pos;
- return error.EndOfStream;
- }
- r.pos += n;
- if (n > data_size) {
- r.interface.end += n - data_size;
- remaining -= data_size;
- } else {
- remaining -= n;
- }
- return @intFromEnum(limit) - remaining;
- }
- const size = r.getSize() catch return 0;
- const n = @min(size - r.pos, std.math.maxInt(i64), @intFromEnum(limit));
- io.vtable.fileSeekBy(io.userdata, file, n) catch |err| {
- r.seek_err = err;
- return 0;
- };
- r.pos += n;
- return n;
- },
- .failure => return error.ReadFailed,
- }
- }
-
- /// Returns whether the stream is at the logical end.
- pub fn atEnd(r: *Reader) bool {
- // Even if stat fails, size is set when end is encountered.
- const size = r.size orelse return false;
- return size - logicalPos(r) == 0;
- }
-};
-
-pub const Atomic = struct {
- file_writer: File.Writer,
- random_integer: u64,
- dest_basename: []const u8,
- file_open: bool,
- file_exists: bool,
- close_dir_on_deinit: bool,
- dir: Dir,
-
- pub const InitError = File.OpenError;
-
- /// Note that the `Dir.atomicFile` API may be more handy than this lower-level function.
- pub fn init(
- dest_basename: []const u8,
- mode: File.Mode,
- dir: Dir,
- close_dir_on_deinit: bool,
- write_buffer: []u8,
- ) InitError!Atomic {
- while (true) {
- const random_integer = std.crypto.random.int(u64);
- const tmp_sub_path = std.fmt.hex(random_integer);
- const file = dir.createFile(&tmp_sub_path, .{ .mode = mode, .exclusive = true }) catch |err| switch (err) {
- error.PathAlreadyExists => continue,
- else => |e| return e,
- };
- return .{
- .file_writer = file.writer(write_buffer),
- .random_integer = random_integer,
- .dest_basename = dest_basename,
- .file_open = true,
- .file_exists = true,
- .close_dir_on_deinit = close_dir_on_deinit,
- .dir = dir,
- };
- }
- }
-
- /// Always call deinit, even after a successful finish().
- pub fn deinit(af: *Atomic) void {
- if (af.file_open) {
- af.file_writer.file.close();
- af.file_open = false;
- }
- if (af.file_exists) {
- const tmp_sub_path = std.fmt.hex(af.random_integer);
- af.dir.deleteFile(&tmp_sub_path) catch {};
- af.file_exists = false;
- }
- if (af.close_dir_on_deinit) {
- af.dir.close();
- }
- af.* = undefined;
- }
+/// Positional is more threadsafe, since the global seek position is not
+/// affected.
+pub fn writer(file: File, io: Io, buffer: []u8) Writer {
+ return .init(file, io, buffer);
+}
- pub const FlushError = File.WriteError;
+/// Positional is more threadsafe, since the global seek position is not
+/// affected, but when such syscalls are not available, preemptively
+/// initializing in streaming mode will skip a failed syscall.
+pub fn writerStreaming(file: File, io: Io, buffer: []u8) Writer {
+ return .initStreaming(file, io, buffer);
+}
- pub fn flush(af: *Atomic) FlushError!void {
- af.file_writer.interface.flush() catch |err| switch (err) {
- error.WriteFailed => return af.file_writer.err.?,
- };
- }
+pub const LockError = error{
+ SystemResources,
+ FileLocksUnsupported,
+} || Io.Cancelable || Io.UnexpectedError;
- pub const RenameIntoPlaceError = Dir.RenameError;
-
- /// On Windows, this function introduces a period of time where some file
- /// system operations on the destination file will result in
- /// `error.AccessDenied`, including rename operations (such as the one used in
- /// this function).
- pub fn renameIntoPlace(af: *Atomic) RenameIntoPlaceError!void {
- const io = af.file_writer.io;
- assert(af.file_exists);
- if (af.file_open) {
- af.file_writer.file.close();
- af.file_open = false;
- }
- const tmp_sub_path = std.fmt.hex(af.random_integer);
- try af.dir.rename(&tmp_sub_path, af.dir, af.dest_basename, io);
- af.file_exists = false;
- }
+/// 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.
+pub fn lock(file: File, io: Io, l: Lock) LockError!void {
+ return io.vtable.fileLock(io.userdata, file, l);
+}
- pub const FinishError = FlushError || RenameIntoPlaceError;
+/// Assumes the file is locked.
+pub fn unlock(file: File, io: Io) void {
+ return io.vtable.fileUnlock(io.userdata, file);
+}
- /// Combination of `flush` followed by `renameIntoPlace`.
- pub fn finish(af: *Atomic) FinishError!void {
- try af.flush();
- try af.renameIntoPlace();
- }
-};
+/// 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.
+pub fn tryLock(file: File, io: Io, l: Lock) LockError!bool {
+ return io.vtable.fileTryLock(io.userdata, file, l);
+}
-pub const SetModeError = error{
- AccessDenied,
- PermissionDenied,
- InputOutput,
- SymLinkLoop,
- FileNotFound,
- SystemResources,
- ReadOnlyFileSystem,
-} || Io.Cancelable || Io.UnexpectedError;
+pub const DowngradeLockError = Io.Cancelable || Io.UnexpectedError;
-pub const SetOwnerError = error{
- AccessDenied,
- PermissionDenied,
- InputOutput,
- SymLinkLoop,
- FileNotFound,
- SystemResources,
- ReadOnlyFileSystem,
-} || Io.Cancelable || Io.UnexpectedError;
+/// Assumes the file is already locked in exclusive mode.
+/// Atomically modifies the lock to be in shared mode, without releasing it.
+pub fn downgradeLock(file: File, io: Io) LockError!void {
+ return io.vtable.fileDowngradeLock(io.userdata, file);
+}
diff --git a/lib/std/Io/File/Atomic.zig b/lib/std/Io/File/Atomic.zig
@@ -0,0 +1,99 @@
+const Atomic = @This();
+
+const std = @import("../../std.zig");
+const Io = std.Io;
+const File = std.Io.File;
+const Dir = std.Io.Dir;
+const assert = std.debug.assert;
+
+file_writer: File.Writer,
+random_integer: u64,
+dest_basename: []const u8,
+file_open: bool,
+file_exists: bool,
+close_dir_on_deinit: bool,
+dir: Dir,
+
+pub const InitError = File.OpenError;
+
+/// Note that the `Dir.atomicFile` API may be more handy than this lower-level function.
+pub fn init(
+ io: Io,
+ dest_basename: []const u8,
+ mode: File.Mode,
+ dir: Dir,
+ close_dir_on_deinit: bool,
+ write_buffer: []u8,
+) InitError!Atomic {
+ while (true) {
+ const random_integer = std.crypto.random.int(u64);
+ const tmp_sub_path = std.fmt.hex(random_integer);
+ const file = dir.createFile(io, &tmp_sub_path, .{ .mode = mode, .exclusive = true }) catch |err| switch (err) {
+ error.PathAlreadyExists => continue,
+ else => |e| return e,
+ };
+ return .{
+ .file_writer = file.writer(io, write_buffer),
+ .random_integer = random_integer,
+ .dest_basename = dest_basename,
+ .file_open = true,
+ .file_exists = true,
+ .close_dir_on_deinit = close_dir_on_deinit,
+ .dir = dir,
+ };
+ }
+}
+
+/// Always call deinit, even after a successful finish().
+pub fn deinit(af: *Atomic) void {
+ const io = af.file_writer.io;
+
+ if (af.file_open) {
+ af.file_writer.file.close(io);
+ af.file_open = false;
+ }
+ if (af.file_exists) {
+ const tmp_sub_path = std.fmt.hex(af.random_integer);
+ af.dir.deleteFile(io, &tmp_sub_path) catch {};
+ af.file_exists = false;
+ }
+ if (af.close_dir_on_deinit) {
+ af.dir.close(io);
+ }
+ af.* = undefined;
+}
+
+pub const FlushError = File.WriteError;
+
+pub fn flush(af: *Atomic) FlushError!void {
+ af.file_writer.interface.flush() catch |err| switch (err) {
+ error.WriteFailed => return af.file_writer.err.?,
+ };
+}
+
+pub const RenameIntoPlaceError = Dir.RenameError;
+
+/// On Windows, this function introduces a period of time where some file
+/// system operations on the destination file will result in
+/// `error.AccessDenied`, including rename operations (such as the one used in
+/// this function).
+pub fn renameIntoPlace(af: *Atomic) RenameIntoPlaceError!void {
+ const io = af.file_writer.io;
+
+ assert(af.file_exists);
+ if (af.file_open) {
+ af.file_writer.file.close(io);
+ af.file_open = false;
+ }
+ const tmp_sub_path = std.fmt.hex(af.random_integer);
+ try af.dir.rename(&tmp_sub_path, af.dir, af.dest_basename, io);
+ af.file_exists = false;
+}
+
+pub const FinishError = FlushError || RenameIntoPlaceError;
+
+/// Combination of `flush` followed by `renameIntoPlace`.
+pub fn finish(af: *Atomic) FinishError!void {
+ try af.flush();
+ try af.renameIntoPlace();
+}
diff --git a/lib/std/Io/File/Reader.zig b/lib/std/Io/File/Reader.zig
@@ -0,0 +1,395 @@
+//! Memoizes key information about a file handle such as:
+//! * The size from calling stat, or the error that occurred therein.
+//! * The current seek position.
+//! * The error that occurred when trying to seek.
+//! * Whether reading should be done positionally or streaming.
+//! * Whether reading should be done via fd-to-fd syscalls (e.g. `sendfile`)
+//! versus plain variants (e.g. `read`).
+//!
+//! Fulfills the `Io.Reader` interface.
+const Reader = @This();
+
+const std = @import("../../std.zig");
+const Io = std.Io;
+const File = std.Io.File;
+const assert = std.debug.assert;
+
+io: Io,
+file: File,
+err: ?Error = null,
+mode: Reader.Mode = .positional,
+/// Tracks the true seek position in the file. To obtain the logical
+/// position, use `logicalPos`.
+pos: u64 = 0,
+size: ?u64 = null,
+size_err: ?SizeError = null,
+seek_err: ?Reader.SeekError = null,
+interface: Io.Reader,
+
+pub const Error = error{
+ InputOutput,
+ SystemResources,
+ IsDir,
+ BrokenPipe,
+ ConnectionResetByPeer,
+ Timeout,
+ /// In WASI, EBADF is mapped to this error because it is returned when
+ /// trying to read a directory file descriptor as if it were a file.
+ NotOpenForReading,
+ SocketUnconnected,
+ /// This error occurs when no global event loop is configured,
+ /// and reading from the file descriptor would block.
+ WouldBlock,
+ /// In WASI, this error occurs when the file descriptor does
+ /// not hold the required rights to read from it.
+ AccessDenied,
+ /// This error occurs in Linux if the process to be read from
+ /// no longer exists.
+ ProcessNotFound,
+ /// Unable to read file due to lock.
+ LockViolation,
+} || Io.Cancelable || Io.UnexpectedError;
+
+pub const SizeError = std.os.windows.GetFileSizeError || File.StatError || error{
+ /// Occurs if, for example, the file handle is a network socket and therefore does not have a size.
+ Streaming,
+};
+
+pub const SeekError = File.SeekError || error{
+ /// Seeking fell back to reading, and reached the end before the requested seek position.
+ /// `pos` remains at the end of the file.
+ EndOfStream,
+ /// Seeking fell back to reading, which failed.
+ ReadFailed,
+};
+
+pub const Mode = enum {
+ streaming,
+ positional,
+ /// Avoid syscalls other than `read` and `readv`.
+ streaming_reading,
+ /// Avoid syscalls other than `pread` and `preadv`.
+ positional_reading,
+ /// Indicates reading cannot continue because of a seek failure.
+ failure,
+
+ pub fn toStreaming(m: @This()) @This() {
+ return switch (m) {
+ .positional, .streaming => .streaming,
+ .positional_reading, .streaming_reading => .streaming_reading,
+ .failure => .failure,
+ };
+ }
+
+ pub fn toReading(m: @This()) @This() {
+ return switch (m) {
+ .positional, .positional_reading => .positional_reading,
+ .streaming, .streaming_reading => .streaming_reading,
+ .failure => .failure,
+ };
+ }
+};
+
+pub fn initInterface(buffer: []u8) Io.Reader {
+ return .{
+ .vtable = &.{
+ .stream = Reader.stream,
+ .discard = Reader.discard,
+ .readVec = Reader.readVec,
+ },
+ .buffer = buffer,
+ .seek = 0,
+ .end = 0,
+ };
+}
+
+pub fn init(file: File, io: Io, buffer: []u8) Reader {
+ return .{
+ .io = io,
+ .file = file,
+ .interface = initInterface(buffer),
+ };
+}
+
+pub fn initSize(file: File, io: Io, buffer: []u8, size: ?u64) Reader {
+ return .{
+ .io = io,
+ .file = file,
+ .interface = initInterface(buffer),
+ .size = size,
+ };
+}
+
+/// Positional is more threadsafe, since the global seek position is not
+/// affected, but when such syscalls are not available, preemptively
+/// initializing in streaming mode skips a failed syscall.
+pub fn initStreaming(file: File, io: Io, buffer: []u8) Reader {
+ return .{
+ .io = io,
+ .file = file,
+ .interface = Reader.initInterface(buffer),
+ .mode = .streaming,
+ .seek_err = error.Unseekable,
+ .size_err = error.Streaming,
+ };
+}
+
+pub fn getSize(r: *Reader) SizeError!u64 {
+ return r.size orelse {
+ if (r.size_err) |err| return err;
+ if (r.file.stat(r.io)) |st| {
+ if (st.kind == .file) {
+ r.size = st.size;
+ return st.size;
+ } else {
+ r.mode = r.mode.toStreaming();
+ r.size_err = error.Streaming;
+ return error.Streaming;
+ }
+ } else |err| {
+ r.size_err = err;
+ return err;
+ }
+ };
+}
+
+pub fn seekBy(r: *Reader, offset: i64) Reader.SeekError!void {
+ const io = r.io;
+ switch (r.mode) {
+ .positional, .positional_reading => {
+ setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset));
+ },
+ .streaming, .streaming_reading => {
+ const seek_err = r.seek_err orelse e: {
+ if (io.vtable.fileSeekBy(io.userdata, r.file, offset)) |_| {
+ setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset));
+ return;
+ } else |err| {
+ r.seek_err = err;
+ break :e err;
+ }
+ };
+ var remaining = std.math.cast(u64, offset) orelse return seek_err;
+ while (remaining > 0) {
+ remaining -= discard(&r.interface, .limited64(remaining)) catch |err| {
+ r.seek_err = err;
+ return err;
+ };
+ }
+ r.interface.tossBuffered();
+ },
+ .failure => return r.seek_err.?,
+ }
+}
+
+/// Repositions logical read offset relative to the beginning of the file.
+pub fn seekTo(r: *Reader, offset: u64) Reader.SeekError!void {
+ const io = r.io;
+ switch (r.mode) {
+ .positional, .positional_reading => {
+ setLogicalPos(r, offset);
+ },
+ .streaming, .streaming_reading => {
+ const logical_pos = logicalPos(r);
+ if (offset >= logical_pos) return Reader.seekBy(r, @intCast(offset - logical_pos));
+ if (r.seek_err) |err| return err;
+ io.vtable.fileSeekTo(io.userdata, r.file, offset) catch |err| {
+ r.seek_err = err;
+ return err;
+ };
+ setLogicalPos(r, offset);
+ },
+ .failure => return r.seek_err.?,
+ }
+}
+
+pub fn logicalPos(r: *const Reader) u64 {
+ return r.pos - r.interface.bufferedLen();
+}
+
+fn setLogicalPos(r: *Reader, offset: u64) void {
+ const logical_pos = r.logicalPos();
+ if (offset < logical_pos or offset >= r.pos) {
+ r.interface.tossBuffered();
+ r.pos = offset;
+ } else r.interface.toss(@intCast(offset - logical_pos));
+}
+
+/// Number of slices to store on the stack, when trying to send as many byte
+/// vectors through the underlying read calls as possible.
+const max_buffers_len = 16;
+
+fn stream(io_reader: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize {
+ const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
+ return streamMode(r, w, limit, r.mode);
+}
+
+pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Reader.Mode) Io.Reader.StreamError!usize {
+ switch (mode) {
+ .positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) {
+ error.Unimplemented => {
+ r.mode = r.mode.toReading();
+ return 0;
+ },
+ else => |e| return e,
+ },
+ .positional_reading => {
+ const dest = limit.slice(try w.writableSliceGreedy(1));
+ var data: [1][]u8 = .{dest};
+ const n = try readVecPositional(r, &data);
+ w.advance(n);
+ return n;
+ },
+ .streaming_reading => {
+ const dest = limit.slice(try w.writableSliceGreedy(1));
+ var data: [1][]u8 = .{dest};
+ const n = try readVecStreaming(r, &data);
+ w.advance(n);
+ return n;
+ },
+ .failure => return error.ReadFailed,
+ }
+}
+
+fn readVec(io_reader: *Io.Reader, data: [][]u8) Io.Reader.Error!usize {
+ const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
+ switch (r.mode) {
+ .positional, .positional_reading => return readVecPositional(r, data),
+ .streaming, .streaming_reading => return readVecStreaming(r, data),
+ .failure => return error.ReadFailed,
+ }
+}
+
+fn readVecPositional(r: *Reader, data: [][]u8) Io.Reader.Error!usize {
+ const io = r.io;
+ var iovecs_buffer: [max_buffers_len][]u8 = undefined;
+ const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data);
+ const dest = iovecs_buffer[0..dest_n];
+ assert(dest[0].len > 0);
+ const n = io.vtable.fileReadPositional(io.userdata, r.file, dest, r.pos) catch |err| switch (err) {
+ error.Unseekable => {
+ r.mode = r.mode.toStreaming();
+ const pos = r.pos;
+ if (pos != 0) {
+ r.pos = 0;
+ r.seekBy(@intCast(pos)) catch {
+ r.mode = .failure;
+ return error.ReadFailed;
+ };
+ }
+ return 0;
+ },
+ else => |e| {
+ r.err = e;
+ return error.ReadFailed;
+ },
+ };
+ if (n == 0) {
+ r.size = r.pos;
+ return error.EndOfStream;
+ }
+ r.pos += n;
+ if (n > data_size) {
+ r.interface.end += n - data_size;
+ return data_size;
+ }
+ return n;
+}
+
+fn readVecStreaming(r: *Reader, data: [][]u8) Io.Reader.Error!usize {
+ const io = r.io;
+ var iovecs_buffer: [max_buffers_len][]u8 = undefined;
+ const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data);
+ const dest = iovecs_buffer[0..dest_n];
+ assert(dest[0].len > 0);
+ const n = io.vtable.fileReadStreaming(io.userdata, r.file, dest) catch |err| {
+ r.err = err;
+ return error.ReadFailed;
+ };
+ if (n == 0) {
+ r.size = r.pos;
+ return error.EndOfStream;
+ }
+ r.pos += n;
+ if (n > data_size) {
+ r.interface.end += n - data_size;
+ return data_size;
+ }
+ return n;
+}
+
+fn discard(io_reader: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize {
+ const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
+ const io = r.io;
+ const file = r.file;
+ switch (r.mode) {
+ .positional, .positional_reading => {
+ const size = r.getSize() catch {
+ r.mode = r.mode.toStreaming();
+ return 0;
+ };
+ const logical_pos = logicalPos(r);
+ const delta = @min(@intFromEnum(limit), size - logical_pos);
+ setLogicalPos(r, logical_pos + delta);
+ return delta;
+ },
+ .streaming, .streaming_reading => {
+ // Unfortunately we can't seek forward without knowing the
+ // size because the seek syscalls provided to us will not
+ // return the true end position if a seek would exceed the
+ // end.
+ fallback: {
+ if (r.size_err == null and r.seek_err == null) break :fallback;
+
+ const buffered_len = r.interface.bufferedLen();
+ var remaining = @intFromEnum(limit);
+ if (remaining <= buffered_len) {
+ r.interface.seek += remaining;
+ return remaining;
+ }
+ remaining -= buffered_len;
+ r.interface.seek = 0;
+ r.interface.end = 0;
+
+ var trash_buffer: [128]u8 = undefined;
+ var data: [1][]u8 = .{trash_buffer[0..@min(trash_buffer.len, remaining)]};
+ var iovecs_buffer: [max_buffers_len][]u8 = undefined;
+ const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, &data);
+ const dest = iovecs_buffer[0..dest_n];
+ assert(dest[0].len > 0);
+ const n = io.vtable.fileReadStreaming(io.userdata, file, dest) catch |err| {
+ r.err = err;
+ return error.ReadFailed;
+ };
+ if (n == 0) {
+ r.size = r.pos;
+ return error.EndOfStream;
+ }
+ r.pos += n;
+ if (n > data_size) {
+ r.interface.end += n - data_size;
+ remaining -= data_size;
+ } else {
+ remaining -= n;
+ }
+ return @intFromEnum(limit) - remaining;
+ }
+ const size = r.getSize() catch return 0;
+ const n = @min(size - r.pos, std.math.maxInt(i64), @intFromEnum(limit));
+ io.vtable.fileSeekBy(io.userdata, file, n) catch |err| {
+ r.seek_err = err;
+ return 0;
+ };
+ r.pos += n;
+ return n;
+ },
+ .failure => return error.ReadFailed,
+ }
+}
+
+/// Returns whether the stream is at the logical end.
+pub fn atEnd(r: *Reader) bool {
+ // Even if stat fails, size is set when end is encountered.
+ const size = r.size orelse return false;
+ return size - logicalPos(r) == 0;
+}
diff --git a/lib/std/Io/File/Writer.zig b/lib/std/Io/File/Writer.zig
@@ -0,0 +1,566 @@
+const Writer = @This();
+
+const builtin = @import("builtin");
+const native_os = builtin.os.tag;
+const is_windows = native_os == .windows;
+
+const std = @import("../../std.zig");
+const Io = std.Io;
+const File = std.Io.File;
+const assert = std.debug.assert;
+const windows = std.os.windows;
+const posix = std.posix;
+
+file: File,
+err: ?File.WriteError = null,
+mode: Writer.Mode = .positional,
+/// Tracks the true seek position in the file. To obtain the logical
+/// position, add the buffer size to this value.
+pos: u64 = 0,
+sendfile_err: ?SendfileError = null,
+copy_file_range_err: ?CopyFileRangeError = null,
+fcopyfile_err: ?FcopyfileError = null,
+seek_err: ?Writer.SeekError = null,
+interface: Io.Writer,
+
+pub const Mode = File.Reader.Mode;
+
+pub const SendfileError = error{
+ UnsupportedOperation,
+ SystemResources,
+ InputOutput,
+ BrokenPipe,
+ WouldBlock,
+ Unexpected,
+};
+
+pub const CopyFileRangeError = std.os.freebsd.CopyFileRangeError || std.os.linux.wrapped.CopyFileRangeError;
+
+pub const FcopyfileError = error{
+ OperationNotSupported,
+ OutOfMemory,
+ Unexpected,
+};
+
+pub const SeekError = Io.File.SeekError;
+
+/// Number of slices to store on the stack, when trying to send as many byte
+/// vectors through the underlying write calls as possible.
+const max_buffers_len = 16;
+
+pub fn init(file: File, buffer: []u8) Writer {
+ return .{
+ .file = file,
+ .interface = initInterface(buffer),
+ .mode = .positional,
+ };
+}
+
+/// Positional is more threadsafe, since the global seek position is not
+/// affected, but when such syscalls are not available, preemptively
+/// initializing in streaming mode will skip a failed syscall.
+pub fn initStreaming(file: File, buffer: []u8) Writer {
+ return .{
+ .file = file,
+ .interface = initInterface(buffer),
+ .mode = .streaming,
+ };
+}
+
+pub fn initInterface(buffer: []u8) Io.Writer {
+ return .{
+ .vtable = &.{
+ .drain = drain,
+ .sendFile = sendFile,
+ },
+ .buffer = buffer,
+ };
+}
+
+pub fn moveToReader(w: *Writer) File.Reader {
+ defer w.* = undefined;
+ return .{
+ .io = w.io,
+ .file = .{ .handle = w.file.handle },
+ .mode = w.mode,
+ .pos = w.pos,
+ .interface = File.Reader.initInterface(w.interface.buffer),
+ .seek_err = w.seek_err,
+ };
+}
+
+pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
+ const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
+ const handle = w.file.handle;
+ const buffered = io_w.buffered();
+ if (is_windows) switch (w.mode) {
+ .positional, .positional_reading => {
+ if (buffered.len != 0) {
+ const n = windows.WriteFile(handle, buffered, w.pos) catch |err| {
+ w.err = err;
+ return error.WriteFailed;
+ };
+ w.pos += n;
+ return io_w.consume(n);
+ }
+ for (data[0 .. data.len - 1]) |buf| {
+ if (buf.len == 0) continue;
+ const n = windows.WriteFile(handle, buf, w.pos) catch |err| {
+ w.err = err;
+ return error.WriteFailed;
+ };
+ w.pos += n;
+ return io_w.consume(n);
+ }
+ const pattern = data[data.len - 1];
+ if (pattern.len == 0 or splat == 0) return 0;
+ const n = windows.WriteFile(handle, pattern, w.pos) catch |err| {
+ w.err = err;
+ return error.WriteFailed;
+ };
+ w.pos += n;
+ return io_w.consume(n);
+ },
+ .streaming, .streaming_reading => {
+ if (buffered.len != 0) {
+ const n = windows.WriteFile(handle, buffered, null) catch |err| {
+ w.err = err;
+ return error.WriteFailed;
+ };
+ w.pos += n;
+ return io_w.consume(n);
+ }
+ for (data[0 .. data.len - 1]) |buf| {
+ if (buf.len == 0) continue;
+ const n = windows.WriteFile(handle, buf, null) catch |err| {
+ w.err = err;
+ return error.WriteFailed;
+ };
+ w.pos += n;
+ return io_w.consume(n);
+ }
+ const pattern = data[data.len - 1];
+ if (pattern.len == 0 or splat == 0) return 0;
+ const n = windows.WriteFile(handle, pattern, null) catch |err| {
+ w.err = err;
+ return error.WriteFailed;
+ };
+ w.pos += n;
+ return io_w.consume(n);
+ },
+ .failure => return error.WriteFailed,
+ };
+ var iovecs: [max_buffers_len]posix.iovec_const = undefined;
+ var len: usize = 0;
+ if (buffered.len > 0) {
+ iovecs[len] = .{ .base = buffered.ptr, .len = buffered.len };
+ len += 1;
+ }
+ for (data[0 .. data.len - 1]) |d| {
+ if (d.len == 0) continue;
+ iovecs[len] = .{ .base = d.ptr, .len = d.len };
+ len += 1;
+ if (iovecs.len - len == 0) break;
+ }
+ const pattern = data[data.len - 1];
+ if (iovecs.len - len != 0) switch (splat) {
+ 0 => {},
+ 1 => if (pattern.len != 0) {
+ iovecs[len] = .{ .base = pattern.ptr, .len = pattern.len };
+ len += 1;
+ },
+ else => switch (pattern.len) {
+ 0 => {},
+ 1 => {
+ const splat_buffer_candidate = io_w.buffer[io_w.end..];
+ var backup_buffer: [64]u8 = undefined;
+ const splat_buffer = if (splat_buffer_candidate.len >= backup_buffer.len)
+ splat_buffer_candidate
+ else
+ &backup_buffer;
+ const memset_len = @min(splat_buffer.len, splat);
+ const buf = splat_buffer[0..memset_len];
+ @memset(buf, pattern[0]);
+ iovecs[len] = .{ .base = buf.ptr, .len = buf.len };
+ len += 1;
+ var remaining_splat = splat - buf.len;
+ while (remaining_splat > splat_buffer.len and iovecs.len - len != 0) {
+ assert(buf.len == splat_buffer.len);
+ iovecs[len] = .{ .base = splat_buffer.ptr, .len = splat_buffer.len };
+ len += 1;
+ remaining_splat -= splat_buffer.len;
+ }
+ if (remaining_splat > 0 and iovecs.len - len != 0) {
+ iovecs[len] = .{ .base = splat_buffer.ptr, .len = remaining_splat };
+ len += 1;
+ }
+ },
+ else => for (0..splat) |_| {
+ iovecs[len] = .{ .base = pattern.ptr, .len = pattern.len };
+ len += 1;
+ if (iovecs.len - len == 0) break;
+ },
+ },
+ };
+ if (len == 0) return 0;
+ switch (w.mode) {
+ .positional, .positional_reading => {
+ const n = posix.pwritev(handle, iovecs[0..len], w.pos) catch |err| switch (err) {
+ error.Unseekable => {
+ w.mode = w.mode.toStreaming();
+ const pos = w.pos;
+ if (pos != 0) {
+ w.pos = 0;
+ w.seekTo(@intCast(pos)) catch {
+ w.mode = .failure;
+ return error.WriteFailed;
+ };
+ }
+ return 0;
+ },
+ else => |e| {
+ w.err = e;
+ return error.WriteFailed;
+ },
+ };
+ w.pos += n;
+ return io_w.consume(n);
+ },
+ .streaming, .streaming_reading => {
+ const n = posix.writev(handle, iovecs[0..len]) catch |err| {
+ w.err = err;
+ return error.WriteFailed;
+ };
+ w.pos += n;
+ return io_w.consume(n);
+ },
+ .failure => return error.WriteFailed,
+ }
+}
+
+pub fn sendFile(
+ io_w: *Io.Writer,
+ file_reader: *Io.File.Reader,
+ limit: Io.Limit,
+) Io.Writer.FileError!usize {
+ const reader_buffered = file_reader.interface.buffered();
+ if (reader_buffered.len >= @intFromEnum(limit))
+ return sendFileBuffered(io_w, file_reader, limit.slice(reader_buffered));
+ const writer_buffered = io_w.buffered();
+ const file_limit = @intFromEnum(limit) - reader_buffered.len;
+ const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
+ const out_fd = w.file.handle;
+ const in_fd = file_reader.file.handle;
+
+ if (file_reader.size) |size| {
+ if (size - file_reader.pos == 0) {
+ if (reader_buffered.len != 0) {
+ return sendFileBuffered(io_w, file_reader, reader_buffered);
+ } else {
+ return error.EndOfStream;
+ }
+ }
+ }
+
+ if (native_os == .freebsd and w.mode == .streaming) sf: {
+ // Try using sendfile on FreeBSD.
+ if (w.sendfile_err != null) break :sf;
+ const offset = std.math.cast(std.c.off_t, file_reader.pos) orelse break :sf;
+ var hdtr_data: std.c.sf_hdtr = undefined;
+ var headers: [2]posix.iovec_const = undefined;
+ var headers_i: u8 = 0;
+ if (writer_buffered.len != 0) {
+ headers[headers_i] = .{ .base = writer_buffered.ptr, .len = writer_buffered.len };
+ headers_i += 1;
+ }
+ if (reader_buffered.len != 0) {
+ headers[headers_i] = .{ .base = reader_buffered.ptr, .len = reader_buffered.len };
+ headers_i += 1;
+ }
+ const hdtr: ?*std.c.sf_hdtr = if (headers_i == 0) null else b: {
+ hdtr_data = .{
+ .headers = &headers,
+ .hdr_cnt = headers_i,
+ .trailers = null,
+ .trl_cnt = 0,
+ };
+ break :b &hdtr_data;
+ };
+ var sbytes: std.c.off_t = undefined;
+ const nbytes: usize = @min(file_limit, std.math.maxInt(usize));
+ const flags = 0;
+ switch (posix.errno(std.c.sendfile(in_fd, out_fd, offset, nbytes, hdtr, &sbytes, flags))) {
+ .SUCCESS, .INTR => {},
+ .INVAL, .OPNOTSUPP, .NOTSOCK, .NOSYS => w.sendfile_err = error.UnsupportedOperation,
+ .BADF => if (builtin.mode == .Debug) @panic("race condition") else {
+ w.sendfile_err = error.Unexpected;
+ },
+ .FAULT => if (builtin.mode == .Debug) @panic("segmentation fault") else {
+ w.sendfile_err = error.Unexpected;
+ },
+ .NOTCONN => w.sendfile_err = error.BrokenPipe,
+ .AGAIN, .BUSY => if (sbytes == 0) {
+ w.sendfile_err = error.WouldBlock;
+ },
+ .IO => w.sendfile_err = error.InputOutput,
+ .PIPE => w.sendfile_err = error.BrokenPipe,
+ .NOBUFS => w.sendfile_err = error.SystemResources,
+ else => |err| w.sendfile_err = posix.unexpectedErrno(err),
+ }
+ if (w.sendfile_err != null) {
+ // Give calling code chance to observe the error before trying
+ // something else.
+ return 0;
+ }
+ if (sbytes == 0) {
+ file_reader.size = file_reader.pos;
+ return error.EndOfStream;
+ }
+ const consumed = io_w.consume(@intCast(sbytes));
+ file_reader.seekBy(@intCast(consumed)) catch return error.ReadFailed;
+ return consumed;
+ }
+
+ if (native_os.isDarwin() and w.mode == .streaming) sf: {
+ // Try using sendfile on macOS.
+ if (w.sendfile_err != null) break :sf;
+ const offset = std.math.cast(std.c.off_t, file_reader.pos) orelse break :sf;
+ var hdtr_data: std.c.sf_hdtr = undefined;
+ var headers: [2]posix.iovec_const = undefined;
+ var headers_i: u8 = 0;
+ if (writer_buffered.len != 0) {
+ headers[headers_i] = .{ .base = writer_buffered.ptr, .len = writer_buffered.len };
+ headers_i += 1;
+ }
+ if (reader_buffered.len != 0) {
+ headers[headers_i] = .{ .base = reader_buffered.ptr, .len = reader_buffered.len };
+ headers_i += 1;
+ }
+ const hdtr: ?*std.c.sf_hdtr = if (headers_i == 0) null else b: {
+ hdtr_data = .{
+ .headers = &headers,
+ .hdr_cnt = headers_i,
+ .trailers = null,
+ .trl_cnt = 0,
+ };
+ break :b &hdtr_data;
+ };
+ const max_count = std.math.maxInt(i32); // Avoid EINVAL.
+ var len: std.c.off_t = @min(file_limit, max_count);
+ const flags = 0;
+ switch (posix.errno(std.c.sendfile(in_fd, out_fd, offset, &len, hdtr, flags))) {
+ .SUCCESS, .INTR => {},
+ .OPNOTSUPP, .NOTSOCK, .NOSYS => w.sendfile_err = error.UnsupportedOperation,
+ .BADF => if (builtin.mode == .Debug) @panic("race condition") else {
+ w.sendfile_err = error.Unexpected;
+ },
+ .FAULT => if (builtin.mode == .Debug) @panic("segmentation fault") else {
+ w.sendfile_err = error.Unexpected;
+ },
+ .INVAL => if (builtin.mode == .Debug) @panic("invalid API usage") else {
+ w.sendfile_err = error.Unexpected;
+ },
+ .NOTCONN => w.sendfile_err = error.BrokenPipe,
+ .AGAIN => if (len == 0) {
+ w.sendfile_err = error.WouldBlock;
+ },
+ .IO => w.sendfile_err = error.InputOutput,
+ .PIPE => w.sendfile_err = error.BrokenPipe,
+ else => |err| w.sendfile_err = posix.unexpectedErrno(err),
+ }
+ if (w.sendfile_err != null) {
+ // Give calling code chance to observe the error before trying
+ // something else.
+ return 0;
+ }
+ if (len == 0) {
+ file_reader.size = file_reader.pos;
+ return error.EndOfStream;
+ }
+ const consumed = io_w.consume(@bitCast(len));
+ file_reader.seekBy(@intCast(consumed)) catch return error.ReadFailed;
+ return consumed;
+ }
+
+ if (native_os == .linux and w.mode == .streaming) sf: {
+ // Try using sendfile on Linux.
+ if (w.sendfile_err != null) break :sf;
+ // Linux sendfile does not support headers.
+ if (writer_buffered.len != 0 or reader_buffered.len != 0)
+ return sendFileBuffered(io_w, file_reader, reader_buffered);
+ const max_count = 0x7ffff000; // Avoid EINVAL.
+ var off: std.os.linux.off_t = undefined;
+ const off_ptr: ?*std.os.linux.off_t, const count: usize = switch (file_reader.mode) {
+ .positional => o: {
+ const size = file_reader.getSize() catch return 0;
+ off = std.math.cast(std.os.linux.off_t, file_reader.pos) orelse return error.ReadFailed;
+ break :o .{ &off, @min(@intFromEnum(limit), size - file_reader.pos, max_count) };
+ },
+ .streaming => .{ null, limit.minInt(max_count) },
+ .streaming_reading, .positional_reading => break :sf,
+ .failure => return error.ReadFailed,
+ };
+ const n = std.os.linux.wrapped.sendfile(out_fd, in_fd, off_ptr, count) catch |err| switch (err) {
+ error.Unseekable => {
+ file_reader.mode = file_reader.mode.toStreaming();
+ const pos = file_reader.pos;
+ if (pos != 0) {
+ file_reader.pos = 0;
+ file_reader.seekBy(@intCast(pos)) catch {
+ file_reader.mode = .failure;
+ return error.ReadFailed;
+ };
+ }
+ return 0;
+ },
+ else => |e| {
+ w.sendfile_err = e;
+ return 0;
+ },
+ };
+ if (n == 0) {
+ file_reader.size = file_reader.pos;
+ return error.EndOfStream;
+ }
+ file_reader.pos += n;
+ w.pos += n;
+ return n;
+ }
+
+ const copy_file_range = switch (native_os) {
+ .freebsd => std.os.freebsd.copy_file_range,
+ .linux => std.os.linux.wrapped.copy_file_range,
+ else => {},
+ };
+ if (@TypeOf(copy_file_range) != void) cfr: {
+ if (w.copy_file_range_err != null) break :cfr;
+ if (writer_buffered.len != 0 or reader_buffered.len != 0)
+ return sendFileBuffered(io_w, file_reader, reader_buffered);
+ var off_in: i64 = undefined;
+ var off_out: i64 = undefined;
+ const off_in_ptr: ?*i64 = switch (file_reader.mode) {
+ .positional_reading, .streaming_reading => return error.Unimplemented,
+ .positional => p: {
+ off_in = @intCast(file_reader.pos);
+ break :p &off_in;
+ },
+ .streaming => null,
+ .failure => return error.WriteFailed,
+ };
+ const off_out_ptr: ?*i64 = switch (w.mode) {
+ .positional_reading, .streaming_reading => return error.Unimplemented,
+ .positional => p: {
+ off_out = @intCast(w.pos);
+ break :p &off_out;
+ },
+ .streaming => null,
+ .failure => return error.WriteFailed,
+ };
+ const n = copy_file_range(in_fd, off_in_ptr, out_fd, off_out_ptr, @intFromEnum(limit), 0) catch |err| {
+ w.copy_file_range_err = err;
+ return 0;
+ };
+ if (n == 0) {
+ file_reader.size = file_reader.pos;
+ return error.EndOfStream;
+ }
+ file_reader.pos += n;
+ w.pos += n;
+ return n;
+ }
+
+ if (builtin.os.tag.isDarwin()) fcf: {
+ if (w.fcopyfile_err != null) break :fcf;
+ if (file_reader.pos != 0) break :fcf;
+ if (w.pos != 0) break :fcf;
+ if (limit != .unlimited) break :fcf;
+ const size = file_reader.getSize() catch break :fcf;
+ if (writer_buffered.len != 0 or reader_buffered.len != 0)
+ return sendFileBuffered(io_w, file_reader, reader_buffered);
+ const rc = std.c.fcopyfile(in_fd, out_fd, null, .{ .DATA = true });
+ switch (posix.errno(rc)) {
+ .SUCCESS => {},
+ .INVAL => if (builtin.mode == .Debug) @panic("invalid API usage") else {
+ w.fcopyfile_err = error.Unexpected;
+ return 0;
+ },
+ .NOMEM => {
+ w.fcopyfile_err = error.OutOfMemory;
+ return 0;
+ },
+ .OPNOTSUPP => {
+ w.fcopyfile_err = error.OperationNotSupported;
+ return 0;
+ },
+ else => |err| {
+ w.fcopyfile_err = posix.unexpectedErrno(err);
+ return 0;
+ },
+ }
+ file_reader.pos = size;
+ w.pos = size;
+ return size;
+ }
+
+ return error.Unimplemented;
+}
+
+fn sendFileBuffered(
+ io_w: *Io.Writer,
+ file_reader: *Io.File.Reader,
+ reader_buffered: []const u8,
+) Io.Writer.FileError!usize {
+ const n = try drain(io_w, &.{reader_buffered}, 1);
+ file_reader.seekBy(@intCast(n)) catch return error.ReadFailed;
+ return n;
+}
+
+pub fn seekTo(w: *Writer, offset: u64) (Writer.SeekError || Io.Writer.Error)!void {
+ try w.interface.flush();
+ try seekToUnbuffered(w, offset);
+}
+
+/// Asserts that no data is currently buffered.
+pub fn seekToUnbuffered(w: *Writer, offset: u64) Writer.SeekError!void {
+ assert(w.interface.buffered().len == 0);
+ switch (w.mode) {
+ .positional, .positional_reading => {
+ w.pos = offset;
+ },
+ .streaming, .streaming_reading => {
+ if (w.seek_err) |err| return err;
+ posix.lseek_SET(w.file.handle, offset) catch |err| {
+ w.seek_err = err;
+ return err;
+ };
+ w.pos = offset;
+ },
+ .failure => return w.seek_err.?,
+ }
+}
+
+pub const EndError = File.SetEndPosError || Io.Writer.Error;
+
+/// Flushes any buffered data and sets the end position of the file.
+///
+/// If not overwriting existing contents, then calling `interface.flush`
+/// directly is sufficient.
+///
+/// Flush failure is handled by setting `err` so that it can be handled
+/// along with other write failures.
+pub fn end(w: *Writer) EndError!void {
+ try w.interface.flush();
+ switch (w.mode) {
+ .positional,
+ .positional_reading,
+ => w.file.setEndPos(w.pos) catch |err| switch (err) {
+ error.NonResizable => return,
+ else => |e| return e,
+ },
+
+ .streaming,
+ .streaming_reading,
+ .failure,
+ => {},
+ }
+}
diff --git a/lib/std/Io/IoUring.zig b/lib/std/Io/IoUring.zig
@@ -1093,7 +1093,7 @@ fn createFile(
.PERM => return error.PermissionDenied,
.EXIST => return error.PathAlreadyExists,
.BUSY => return error.DeviceBusy,
- .OPNOTSUPP => return error.FileLocksNotSupported,
+ .OPNOTSUPP => return error.FileLocksUnsupported,
.AGAIN => return error.WouldBlock,
.TXTBSY => return error.FileBusy,
.NXIO => return error.NoDevice,
@@ -1201,7 +1201,7 @@ fn fileOpen(
.PERM => return error.PermissionDenied,
.EXIST => return error.PathAlreadyExists,
.BUSY => return error.DeviceBusy,
- .OPNOTSUPP => return error.FileLocksNotSupported,
+ .OPNOTSUPP => return error.FileLocksUnsupported,
.AGAIN => return error.WouldBlock,
.TXTBSY => return error.FileBusy,
.NXIO => return error.NoDevice,
diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig
@@ -668,9 +668,10 @@ pub fn io(t: *Threaded) Io {
.dirRename = dirRename,
.dirSymLink = dirSymLink,
.dirReadLink = dirReadLink,
- .dirSetMode = dirSetMode,
.dirSetOwner = dirSetOwner,
.dirSetPermissions = dirSetPermissions,
+ .dirSetTimestamps = dirSetTimestamps,
+ .dirSetTimestampsNow = dirSetTimestampsNow,
.fileStat = fileStat,
.fileClose = fileClose,
@@ -681,6 +682,19 @@ pub fn io(t: *Threaded) Io {
.fileSeekBy = fileSeekBy,
.fileSeekTo = fileSeekTo,
.openSelfExe = openSelfExe,
+ .fileSync = fileSync,
+ .fileIsTty = fileIsTty,
+ .fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes,
+ .fileSupportsAnsiEscapeCodes = fileSupportsAnsiEscapeCodes,
+ .fileSetLength = fileSetLength,
+ .fileSetOwner = fileSetOwner,
+ .fileSetPermissions = fileSetPermissions,
+ .fileSetTimestamps = fileSetTimestamps,
+ .fileSetTimestampsNow = fileSetTimestampsNow,
+ .fileLock = fileLock,
+ .fileTryLock = fileTryLock,
+ .fileUnlock = fileUnlock,
+ .fileDowngradeLock = fileDowngradeLock,
.now = now,
.sleep = sleep,
@@ -774,9 +788,10 @@ pub fn ioBasic(t: *Threaded) Io {
.dirRename = dirRename,
.dirSymLink = dirSymLink,
.dirReadLink = dirReadLink,
- .dirSetMode = dirSetMode,
.dirSetOwner = dirSetOwner,
.dirSetPermissions = dirSetPermissions,
+ .dirSetTimestamps = dirSetTimestamps,
+ .dirSetTimestampsNow = dirSetTimestampsNow,
.fileStat = fileStat,
.fileClose = fileClose,
@@ -787,6 +802,19 @@ pub fn ioBasic(t: *Threaded) Io {
.fileSeekBy = fileSeekBy,
.fileSeekTo = fileSeekTo,
.openSelfExe = openSelfExe,
+ .fileSync = fileSync,
+ .fileIsTty = fileIsTty,
+ .fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes,
+ .fileSupportsAnsiEscapeCodes = fileSupportsAnsiEscapeCodes,
+ .fileSetLength = fileSetLength,
+ .fileSetOwner = fileSetOwner,
+ .fileSetPermissions = fileSetPermissions,
+ .fileSetTimestamps = fileSetTimestamps,
+ .fileSetTimestampsNow = fileSetTimestampsNow,
+ .fileLock = fileLock,
+ .fileTryLock = fileTryLock,
+ .fileUnlock = fileUnlock,
+ .fileDowngradeLock = fileDowngradeLock,
.now = now,
.sleep = sleep,
@@ -831,6 +859,7 @@ const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fs
const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat;
const lseek_sym = if (posix.lfs64_abi) posix.system.lseek64 else posix.system.lseek;
const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv;
+const ftruncate_sym = if (posix.lfs64_abi) posix.system.ftruncate64 else posix.system.ftruncate;
/// Trailing data:
/// 1. context
@@ -1956,7 +1985,7 @@ fn fileStatWindows(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.Fi
// 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,
+ .INVALID_PARAMETER => |err| return windows.statusBug(err),
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
}
@@ -1971,7 +2000,7 @@ fn fileStatWindows(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.Fi
.SUCCESS => {},
// INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e
- .INFO_LENGTH_MISMATCH => unreachable,
+ .INFO_LENGTH_MISMATCH => |err| return windows.statusBug(err),
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
}
@@ -2279,7 +2308,7 @@ fn dirCreateFilePosix(
.PERM => return error.PermissionDenied,
.EXIST => return error.PathAlreadyExists,
.BUSY => return error.DeviceBusy,
- .OPNOTSUPP => return error.FileLocksNotSupported,
+ .OPNOTSUPP => return error.FileLocksUnsupported,
.AGAIN => return error.WouldBlock,
.TXTBSY => return error.FileBusy,
.NXIO => return error.NoDevice,
@@ -2318,7 +2347,7 @@ fn dirCreateFilePosix(
.INVAL => |err| return errnoBug(err), // invalid parameters
.NOLCK => return error.SystemResources,
.AGAIN => return error.WouldBlock,
- .OPNOTSUPP => return error.FileLocksNotSupported,
+ .OPNOTSUPP => return error.FileLocksUnsupported,
else => |err| return posix.unexpectedErrno(err),
}
},
@@ -2588,7 +2617,7 @@ fn dirOpenFilePosix(
.PERM => return error.PermissionDenied,
.EXIST => return error.PathAlreadyExists,
.BUSY => return error.DeviceBusy,
- .OPNOTSUPP => return error.FileLocksNotSupported,
+ .OPNOTSUPP => return error.FileLocksUnsupported,
.AGAIN => return error.WouldBlock,
.TXTBSY => return error.FileBusy,
.NXIO => return error.NoDevice,
@@ -2626,7 +2655,7 @@ fn dirOpenFilePosix(
.INVAL => |err| return errnoBug(err), // invalid parameters
.NOLCK => return error.SystemResources,
.AGAIN => return error.WouldBlock,
- .OPNOTSUPP => return error.FileLocksNotSupported,
+ .OPNOTSUPP => return error.FileLocksUnsupported,
else => |err| return posix.unexpectedErrno(err),
}
},
@@ -3544,12 +3573,12 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, re
);
switch (rc) {
.SUCCESS => {},
- .OBJECT_NAME_INVALID => unreachable,
+ .OBJECT_NAME_INVALID => |err| return w.statusBug(err),
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
.BAD_NETWORK_PATH => return error.NetworkNotFound, // \\server was not found
.BAD_NETWORK_NAME => return error.NetworkNotFound, // \\server was found but \\server\share wasn't
- .INVALID_PARAMETER => unreachable,
+ .INVALID_PARAMETER => |err| return w.statusBug(err),
.FILE_IS_A_DIRECTORY => return error.IsDir,
.NOT_A_DIRECTORY => return error.NotDir,
.SHARING_VIOLATION => return error.FileBusy,
@@ -3621,7 +3650,7 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, re
switch (rc) {
.SUCCESS => {},
.DIRECTORY_NOT_EMPTY => return error.DirNotEmpty,
- .INVALID_PARAMETER => unreachable,
+ .INVALID_PARAMETER => |err| return w.statusBug(err),
.CANNOT_DELETE => return error.AccessDenied,
.MEDIA_WRITE_PROTECTED => return error.AccessDenied,
.ACCESS_DENIED => return error.AccessDenied,
@@ -3835,9 +3864,9 @@ fn dirRenameWindows(
switch (rc) {
.SUCCESS => {},
- .INVALID_HANDLE => unreachable,
- .INVALID_PARAMETER => unreachable,
- .OBJECT_PATH_SYNTAX_BAD => unreachable,
+ .INVALID_HANDLE => |err| return w.statusBug(err),
+ .INVALID_PARAMETER => |err| return w.statusBug(err),
+ .OBJECT_PATH_SYNTAX_BAD => |err| return w.statusBug(err),
.ACCESS_DENIED => return error.AccessDenied,
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
@@ -4320,25 +4349,50 @@ fn dirReadLinkPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, bu
}
}
-const dirSetMode = switch (native_os) {
- .windows => dirSetModeUnsupported,
- else => dirSetModePosix,
+const dirSetPermissions = switch (native_os) {
+ .windows => dirSetPermissionsWindows,
+ else => dirSetPermissionsPosix,
+};
+
+fn dirSetPermissionsWindows(userdata: ?*anyopaque, dir: Io.Dir, permissions: Io.Dir.Permissions) Io.Dir.SetPermissionsError!void {
+ // TODO I think we can actually set permissions on a dir on windows?
+ _ = userdata;
+ _ = dir;
+ _ = permissions;
+ return error.Unexpected;
+}
+
+fn dirSetPermissionsPosix(userdata: ?*anyopaque, dir: Io.Dir, permissions: Io.Dir.Permissions) Io.Dir.SetPermissionsError!void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+ return setPermissionsPosix(current_thread, dir.handle, permissions.toMode());
+}
+
+const dirSetOwner = switch (native_os) {
+ .windows => dirSetOwnerUnsupported,
+ else => dirSetOwnerPosix,
};
-fn dirSetModeUnsupported(userdata: ?*anyopaque, dir: Io.Dir, new_mode: Io.Dir.Mode) Io.Dir.SetModeError!void {
+fn dirSetOwnerUnsupported(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.Dir.SetOwnerError!void {
_ = userdata;
_ = dir;
- _ = new_mode;
+ _ = owner;
+ _ = group;
return error.Unexpected;
}
-fn dirSetModePosix(userdata: ?*anyopaque, dir: Io.Dir, new_mode: Io.Dir.Mode) Io.Dir.SetModeError!void {
+fn dirSetOwnerPosix(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.Dir.SetOwnerError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
+ const uid = owner orelse ~@as(posix.uid_t, 0);
+ const gid = group orelse ~@as(posix.gid_t, 0);
+ return setOwnerPosix(current_thread, dir.handle, uid, gid);
+}
+fn setOwnerPosix(current_thread: *Thread, fd: posix.fd_t, uid: posix.uid_t, gid: posix.gid_t) Io.File.SetOwnerError!void {
try current_thread.beginSyscall();
while (true) {
- switch (posix.errno(posix.system.fchmod(dir.handle, new_mode))) {
+ switch (posix.errno(posix.system.fchown(fd, uid, gid))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
@@ -4348,7 +4402,7 @@ fn dirSetModePosix(userdata: ?*anyopaque, dir: Io.Dir, new_mode: Io.Dir.Mode) Io
else => |e| {
current_thread.endSyscall();
switch (e) {
- .BADF => |err| return errnoBug(err),
+ .BADF => |err| return errnoBug(err), // likely fd refers to directory opened without `Io.Dir.OpenOptions.iterate`
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ACCES => return error.AccessDenied,
@@ -4366,28 +4420,386 @@ fn dirSetModePosix(userdata: ?*anyopaque, dir: Io.Dir, new_mode: Io.Dir.Mode) Io
}
}
-const dirSetOwner = switch (native_os) {
- .windows => dirSetOwnerUnsupported,
- else => dirSetOwnerPosix,
+const fileSync = switch (native_os) {
+ .windows => fileSyncWindows,
+ else => fileSyncPosix,
};
-fn dirSetOwnerUnsupported(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.Dir.SetOwnerError!void {
- _ = userdata;
- _ = dir;
- _ = owner;
- _ = group;
- return error.Unexpected;
+fn fileSyncWindows(userdata: ?*anyopaque, file: Io.File) Io.File.SyncError!void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+
+ try current_thread.checkCancel();
+
+ if (windows.kernel32.FlushFileBuffers(file.handle) != 0)
+ return;
+
+ switch (windows.GetLastError()) {
+ .SUCCESS => return,
+ .INVALID_HANDLE => unreachable,
+ .ACCESS_DENIED => return error.AccessDenied, // a sync was performed but the system couldn't update the access time
+ .UNEXP_NET_ERR => return error.InputOutput,
+ else => |err| return windows.unexpectedError(err),
+ }
}
-fn dirSetOwnerPosix(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.Dir.SetOwnerError!void {
+fn fileSyncPosix(userdata: ?*anyopaque, file: Io.File) Io.File.SyncError!void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (posix.system.fsync(file.handle)) {
+ .SUCCESS => return current_thread.endSyscall(),
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .BADF => |err| return errnoBug(err),
+ .INVAL => |err| return errnoBug(err),
+ .ROFS => |err| return errnoBug(err),
+ .IO => return error.InputOutput,
+ .NOSPC => return error.NoSpaceLeft,
+ .DQUOT => return error.DiskQuota,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
+}
+
+fn fileIsTty(userdata: ?*anyopaque, file: Io.File) Io.Cancelable!bool {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+ return isTty(current_thread, file);
+}
+
+fn isTty(current_thread: *Thread, file: Io.File) Io.Cancelable!bool {
+ if (is_windows) {
+ if (try isCygwinPty(current_thread, file)) return true;
+ try current_thread.checkCancel();
+ var out: windows.DWORD = undefined;
+ return windows.kernel32.GetConsoleMode(file.handle, &out) != 0;
+ }
+
+ if (builtin.link_libc) {
+ try current_thread.beginSyscall();
+ while (true) {
+ const rc = posix.system.isatty(file.handle);
+ switch (posix.errno(rc - 1)) {
+ .SUCCESS => {
+ current_thread.endSyscall();
+ return true;
+ },
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => {
+ current_thread.endSyscall();
+ return false;
+ },
+ }
+ }
+ }
+
+ if (native_os == .wasi) {
+ var statbuf: std.os.wasi.fdstat_t = undefined;
+ const err = std.os.wasi.fd_fdstat_get(file.handle, &statbuf);
+ if (err != .SUCCESS)
+ return false;
+
+ // A tty is a character device that we can't seek or tell on.
+ if (statbuf.fs_filetype != .CHARACTER_DEVICE)
+ return false;
+ if (statbuf.fs_rights_base.FD_SEEK or statbuf.fs_rights_base.FD_TELL)
+ return false;
+
+ return true;
+ }
+
+ if (native_os == .linux) {
+ const linux = std.os.linux;
+ try current_thread.beginSyscall();
+ while (true) {
+ var wsz: linux.winsize = undefined;
+ const fd: usize = @bitCast(@as(isize, file.handle));
+ const rc = linux.syscall3(.ioctl, fd, linux.T.IOCGWINSZ, @intFromPtr(&wsz));
+ switch (linux.errno(rc)) {
+ .SUCCESS => {
+ current_thread.endSyscall();
+ return true;
+ },
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => {
+ current_thread.endSyscall();
+ return false;
+ },
+ }
+ }
+ }
+
+ @compileError("unimplemented");
+}
+
+fn fileEnableAnsiEscapeCodes(userdata: ?*anyopaque, file: Io.File) Io.File.EnableAnsiEscapeCodesError!void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+
+ if (is_windows) {
+ try current_thread.checkCancel();
+
+ // For Windows Terminal, VT Sequences processing is enabled by default.
+ var original_console_mode: windows.DWORD = 0;
+ if (windows.kernel32.GetConsoleMode(file.handle, &original_console_mode) != 0) {
+ if (original_console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return;
+
+ // For Windows Console, VT Sequences processing support was added in Windows 10 build 14361, but disabled by default.
+ // https://devblogs.microsoft.com/commandline/tmux-support-arrives-for-bash-on-ubuntu-on-windows/
+ //
+ // Note: In Microsoft's example for enabling virtual terminal processing, it
+ // shows attempting to enable `DISABLE_NEWLINE_AUTO_RETURN` as well:
+ // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#example-of-enabling-virtual-terminal-processing
+ // This is avoided because in the old Windows Console, that flag causes \n (as opposed to \r\n)
+ // to behave unexpectedly (the cursor moves down 1 row but remains on the same column).
+ // Additionally, the default console mode in Windows Terminal does not have
+ // `DISABLE_NEWLINE_AUTO_RETURN` set, so by only enabling `ENABLE_VIRTUAL_TERMINAL_PROCESSING`
+ // we end up matching the mode of Windows Terminal.
+ const requested_console_modes = windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
+ const console_mode = original_console_mode | requested_console_modes;
+ try current_thread.checkCancel();
+ if (windows.kernel32.SetConsoleMode(file.handle, console_mode) != 0) return;
+ }
+ if (try isCygwinPty(current_thread, file)) return;
+ } else {
+ if (try supportsAnsiEscapeCodes(current_thread, file)) return;
+ }
+ return error.NotTerminalDevice;
+}
+
+fn fileSupportsAnsiEscapeCodes(userdata: ?*anyopaque, file: Io.File) Io.Cancelable!bool {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+ return supportsAnsiEscapeCodes(current_thread, file);
+}
+
+fn supportsAnsiEscapeCodes(current_thread: *Thread, file: Io.File) Io.Cancelable!bool {
+ if (is_windows) {
+ try current_thread.checkCancel();
+ var console_mode: windows.DWORD = 0;
+ if (windows.kernel32.GetConsoleMode(file.handle, &console_mode) != 0) {
+ if (console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true;
+ }
+ return isCygwinPty(current_thread, file);
+ }
+
+ if (native_os == .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 (try isTty(current_thread, file)) return true;
+
+ return false;
+}
+
+fn isCygwinPty(current_thread: *Thread, file: Io.File) Io.Cancelable!bool {
+ if (!is_windows) return false;
+
+ const handle = file.handle;
+
+ // If this is a MSYS2/cygwin pty, then it will be a named pipe with a name in one of these formats:
+ // msys-[...]-ptyN-[...]
+ // cygwin-[...]-ptyN-[...]
+ //
+ // Example: msys-1888ae32e00d56aa-pty0-to-master
+
+ // First, just check that the handle is a named pipe.
+ // This allows us to avoid the more costly NtQueryInformationFile call
+ // for handles that aren't named pipes.
+ {
+ try current_thread.checkCancel();
+ var io_status: windows.IO_STATUS_BLOCK = undefined;
+ var device_info: windows.FILE_FS_DEVICE_INFORMATION = undefined;
+ const rc = windows.ntdll.NtQueryVolumeInformationFile(handle, &io_status, &device_info, @sizeOf(windows.FILE_FS_DEVICE_INFORMATION), .FileFsDeviceInformation);
+ switch (rc) {
+ .SUCCESS => {},
+ else => return false,
+ }
+ if (device_info.DeviceType != windows.FILE_DEVICE_NAMED_PIPE) return false;
+ }
+
+ const name_bytes_offset = @offsetOf(windows.FILE_NAME_INFO, "FileName");
+ // `NAME_MAX` UTF-16 code units (2 bytes each)
+ // This buffer may not be long enough to handle *all* possible paths
+ // (PATH_MAX_WIDE would be necessary for that), but because we only care
+ // about certain paths and we know they must be within a reasonable length,
+ // we can use this smaller buffer and just return false on any error from
+ // NtQueryInformationFile.
+ const num_name_bytes = windows.MAX_PATH * 2;
+ var name_info_bytes align(@alignOf(windows.FILE_NAME_INFO)) = [_]u8{0} ** (name_bytes_offset + num_name_bytes);
+
+ var io_status_block: windows.IO_STATUS_BLOCK = undefined;
+ try current_thread.checkCancel();
+ const rc = windows.ntdll.NtQueryInformationFile(handle, &io_status_block, &name_info_bytes, @intCast(name_info_bytes.len), .FileNameInformation);
+ switch (rc) {
+ .SUCCESS => {},
+ .INVALID_PARAMETER => unreachable,
+ else => return false,
+ }
+
+ const name_info: *const windows.FILE_NAME_INFO = @ptrCast(&name_info_bytes);
+ const name_bytes = name_info_bytes[name_bytes_offset .. name_bytes_offset + name_info.FileNameLength];
+ const name_wide = std.mem.bytesAsSlice(u16, name_bytes);
+ // The name we get from NtQueryInformationFile will be prefixed with a '\', e.g. \msys-1888ae32e00d56aa-pty0-to-master
+ return (std.mem.startsWith(u16, name_wide, &[_]u16{ '\\', 'm', 's', 'y', 's', '-' }) or
+ std.mem.startsWith(u16, name_wide, &[_]u16{ '\\', 'c', 'y', 'g', 'w', 'i', 'n', '-' })) and
+ std.mem.indexOf(u16, name_wide, &[_]u16{ '-', 'p', 't', 'y' }) != null;
+}
+
+fn fileSetLength(userdata: ?*anyopaque, file: Io.File, length: u64) Io.File.SetLengthError!void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+
+ const signed_len: i64 = @bitCast(length);
+ if (signed_len < 0) return error.FileTooBig; // Avoid ambiguous EINVAL errors.
+
+ if (is_windows) {
+ try current_thread.checkCancel();
+
+ var io_status_block: windows.IO_STATUS_BLOCK = undefined;
+ var eof_info: windows.FILE_END_OF_FILE_INFORMATION = .{
+ .EndOfFile = signed_len,
+ };
+
+ const status = windows.ntdll.NtSetInformationFile(
+ file.handle,
+ &io_status_block,
+ &eof_info,
+ @sizeOf(windows.FILE_END_OF_FILE_INFORMATION),
+ .FileEndOfFileInformation,
+ );
+ switch (status) {
+ .SUCCESS => return,
+ .INVALID_HANDLE => |err| return windows.statusBug(err), // Handle not open for writing.
+ .ACCESS_DENIED => return error.AccessDenied,
+ .USER_MAPPED_FILE => return error.AccessDenied,
+ .INVALID_PARAMETER => return error.FileTooBig,
+ else => return windows.unexpectedStatus(status),
+ }
+ }
+
+ if (native_os == .wasi and !builtin.link_libc) {
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (std.os.wasi.fd_filestat_set_size(file.handle, length)) {
+ .SUCCESS => return current_thread.endSyscall(),
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .FBIG => return error.FileTooBig,
+ .IO => return error.InputOutput,
+ .PERM => return error.PermissionDenied,
+ .TXTBSY => return error.FileBusy,
+ .BADF => |err| return errnoBug(err), // Handle not open for writing
+ .INVAL => return error.NonResizable,
+ .NOTCAPABLE => return error.AccessDenied,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
+ }
+
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (posix.errno(ftruncate_sym(file.handle, signed_len))) {
+ .SUCCESS => return current_thread.endSyscall(),
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .FBIG => return error.FileTooBig,
+ .IO => return error.InputOutput,
+ .PERM => return error.PermissionDenied,
+ .TXTBSY => return error.FileBusy,
+ .BADF => |err| return errnoBug(err), // Handle not open for writing.
+ .INVAL => return error.NonResizable, // This is returned for /dev/null for example.
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
+}
+
+fn fileSetOwner(userdata: ?*anyopaque, file: Io.File, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.File.SetOwnerError!void {
+ switch (native_os) {
+ .windows, .wasi => return error.Unexpected,
+ else => {},
+ }
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const uid = owner orelse ~@as(posix.uid_t, 0);
const gid = group orelse ~@as(posix.gid_t, 0);
+ return setOwnerPosix(current_thread, file.handle, uid, gid);
+}
+
+fn fileSetPermissions(userdata: ?*anyopaque, file: Io.File, permissions: Io.File.Permissions) Io.File.SetPermissionsError!void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+ switch (native_os) {
+ .windows => {
+ try current_thread.checkCancel();
+ 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 status = windows.ntdll.NtSetInformationFile(
+ file.handle,
+ &io_status_block,
+ &info,
+ @sizeOf(windows.FILE_BASIC_INFORMATION),
+ .FileBasicInformation,
+ );
+ switch (status) {
+ .SUCCESS => return,
+ .INVALID_HANDLE => |err| return windows.statusBug(err),
+ .ACCESS_DENIED => return error.AccessDenied,
+ else => return windows.unexpectedStatus(status),
+ }
+ },
+ .wasi => return error.Unexpected, // Unsupported OS.
+ else => return setPermissionsPosix(current_thread, file.handle, permissions.toMode()),
+ }
+}
+fn setPermissionsPosix(current_thread: *Thread, fd: posix.fd_t, mode: posix.mode_t) Io.File.SetPermissionsError!void {
try current_thread.beginSyscall();
while (true) {
- switch (posix.errno(posix.system.fchown(dir.handle, uid, gid))) {
+ switch (posix.errno(posix.system.fchmod(fd, mode))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
@@ -4397,7 +4809,7 @@ fn dirSetOwnerPosix(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, gro
else => |e| {
current_thread.endSyscall();
switch (e) {
- .BADF => |err| return errnoBug(err), // likely fd refers to directory opened without `Io.Dir.OpenOptions.iterate`
+ .BADF => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ACCES => return error.AccessDenied,
@@ -4415,31 +4827,509 @@ fn dirSetOwnerPosix(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, gro
}
}
-const dirSetPermissions = switch (native_os) {
- .windows => dirSetPermissionsWindows,
- else => dirSetPermissionsPosix,
-};
-
-fn dirSetPermissionsWindows(
+fn dirSetTimestamps(
userdata: ?*anyopaque,
dir: Io.Dir,
- permissions: Io.Dir.Permissions,
-) Io.Dir.SetPermissionsError!void {
- _ = userdata;
- _ = dir;
- _ = permissions;
- @panic("TODO");
+ sub_path: []const u8,
+ last_accessed: Io.Timestamp,
+ last_modified: Io.Timestamp,
+ options: Io.File.SetTimestampsOptions,
+) Io.File.SetTimestampsError!void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+
+ if (is_windows) {
+ @panic("TODO");
+ }
+
+ if (native_os == .wasi and !builtin.link_libc) {
+ @panic("TODO");
+ }
+
+ const times: [2]posix.timespec = .{
+ timestampToPosix(last_accessed),
+ timestampToPosix(last_modified),
+ };
+
+ const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0;
+
+ var path_buffer: [posix.PATH_MAX]u8 = undefined;
+ const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
+
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (posix.errno(posix.system.utimensat(dir.handle, sub_path_posix, ×, flags))) {
+ .SUCCESS => return current_thread.endSyscall(),
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .ACCES => return error.AccessDenied,
+ .PERM => return error.PermissionDenied,
+ .BADF => |err| return errnoBug(err), // always a race condition
+ .FAULT => |err| return errnoBug(err),
+ .INVAL => |err| return errnoBug(err),
+ .ROFS => return error.ReadOnlyFileSystem,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
}
-fn dirSetPermissionsPosix(
+fn dirSetTimestampsNow(
userdata: ?*anyopaque,
dir: Io.Dir,
- permissions: Io.Dir.Permissions,
-) Io.Dir.SetPermissionsError!void {
- _ = userdata;
- _ = dir;
- _ = permissions;
- @panic("TODO");
+ sub_path: []const u8,
+ options: Io.File.SetTimestampsOptions,
+) Io.File.SetTimestampsError!void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+
+ if (is_windows) {
+ @panic("TODO");
+ }
+
+ if (native_os == .wasi and !builtin.link_libc) {
+ @panic("TODO");
+ }
+
+ const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0;
+
+ var path_buffer: [posix.PATH_MAX]u8 = undefined;
+ const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
+
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (posix.errno(posix.system.utimensat(dir.handle, sub_path_posix, null, flags))) {
+ .SUCCESS => return current_thread.endSyscall(),
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .ACCES => return error.AccessDenied,
+ .PERM => return error.PermissionDenied,
+ .BADF => |err| return errnoBug(err), // always a race condition
+ .FAULT => |err| return errnoBug(err),
+ .INVAL => |err| return errnoBug(err),
+ .ROFS => return error.ReadOnlyFileSystem,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
+}
+
+fn fileSetTimestamps(
+ userdata: ?*anyopaque,
+ file: Io.File,
+ last_accessed: Io.Timestamp,
+ last_modified: Io.Timestamp,
+) Io.File.SetTimestampsError!void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+
+ if (is_windows) {
+ try current_thread.checkCancel();
+
+ const atime_ft = windows.nanoSecondsToFileTime(last_accessed.toNanoseconds());
+ const mtime_ft = windows.nanoSecondsToFileTime(last_modified.toNanoseconds());
+
+ // https://github.com/ziglang/zig/issues/1840
+ const rc = windows.kernel32.SetFileTime(file.handle, null, &atime_ft, &mtime_ft);
+ if (rc == 0) {
+ switch (windows.GetLastError()) {
+ else => |err| return windows.unexpectedError(err),
+ }
+ }
+ return;
+ }
+
+ const times: [2]posix.timespec = .{
+ timestampToPosix(last_accessed),
+ timestampToPosix(last_modified),
+ };
+
+ if (native_os == .wasi and !builtin.link_libc) {
+ const atim = times[0].toTimestamp();
+ const mtim = times[1].toTimestamp();
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (std.os.wasi.fd_filestat_set_times(file.handle, atim, mtim, .{
+ .ATIM = true,
+ .MTIM = true,
+ })) {
+ .SUCCESS => return current_thread.endSyscall(),
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .ACCES => return error.AccessDenied,
+ .PERM => return error.PermissionDenied,
+ .BADF => |err| return errnoBug(err), // File descriptor use-after-free.
+ .FAULT => |err| return errnoBug(err),
+ .INVAL => |err| return errnoBug(err),
+ .ROFS => return error.ReadOnlyFileSystem,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
+ }
+
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (posix.errno(posix.system.futimens(file.handle, ×))) {
+ .SUCCESS => return current_thread.endSyscall(),
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .ACCES => return error.AccessDenied,
+ .PERM => return error.PermissionDenied,
+ .BADF => |err| return errnoBug(err), // always a race condition
+ .FAULT => |err| return errnoBug(err),
+ .INVAL => |err| return errnoBug(err),
+ .ROFS => return error.ReadOnlyFileSystem,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
+}
+
+fn fileSetTimestampsNow(userdata: ?*anyopaque, file: Io.File) Io.File.SetTimestampsError!void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+
+ if (is_windows) {
+ @panic("TODO");
+ }
+
+ if (native_os == .wasi and !builtin.link_libc) {
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (std.os.wasi.fd_filestat_set_times(file.handle, 0, 0, .{
+ .ATIM_NOW = true,
+ .MTIM_NOW = true,
+ })) {
+ .SUCCESS => return current_thread.endSyscall(),
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .ACCES => return error.AccessDenied,
+ .PERM => return error.PermissionDenied,
+ .BADF => |err| return errnoBug(err), // always a race condition
+ .FAULT => |err| return errnoBug(err),
+ .INVAL => |err| return errnoBug(err),
+ .ROFS => return error.ReadOnlyFileSystem,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
+ }
+
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (posix.errno(posix.system.futimens(file.handle, null))) {
+ .SUCCESS => return current_thread.endSyscall(),
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .ACCES => return error.AccessDenied,
+ .PERM => return error.PermissionDenied,
+ .BADF => |err| return errnoBug(err), // always a race condition
+ .FAULT => |err| return errnoBug(err),
+ .INVAL => |err| return errnoBug(err),
+ .ROFS => return error.ReadOnlyFileSystem,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
+}
+
+const windows_lock_range_off: windows.LARGE_INTEGER = 0;
+const windows_lock_range_len: windows.LARGE_INTEGER = 1;
+
+fn fileLock(userdata: ?*anyopaque, file: Io.File, lock: Io.File.Lock) Io.File.LockError!void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+
+ if (is_windows) {
+ const exclusive = switch (lock) {
+ .none => return,
+ .shared => false,
+ .exclusive => true,
+ };
+ try current_thread.checkCancel();
+ var io_status_block: windows.IO_STATUS_BLOCK = undefined;
+ const status = windows.ntdll.NtLockFile(
+ file.handle,
+ null,
+ null,
+ null,
+ &io_status_block,
+ &windows_lock_range_off,
+ &windows_lock_range_len,
+ null,
+ windows.FALSE,
+ @intFromBool(exclusive),
+ );
+ switch (status) {
+ .SUCCESS => return,
+ .INSUFFICIENT_RESOURCES => return error.SystemResources,
+ .LOCK_NOT_GRANTED => |err| return windows.statusBug(err), // passed FailImmediately=false
+ .ACCESS_VIOLATION => |err| return windows.statusBug(err), // bad io_status_block pointer
+ else => return windows.unexpectedStatus(status),
+ }
+ }
+
+ const operation = switch (lock) {
+ .none => posix.LOCK.UN,
+ .shared => posix.LOCK.SH,
+ .exclusive => posix.LOCK.EX,
+ };
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (posix.errno(posix.system.flock(file.handle, operation))) {
+ .SUCCESS => return current_thread.endSyscall(),
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .BADF => |err| return errnoBug(err),
+ .INVAL => |err| return errnoBug(err), // invalid parameters
+ .NOLCK => return error.SystemResources,
+ .AGAIN => |err| return errnoBug(err),
+ .OPNOTSUPP => return error.FileLocksUnsupported,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
+}
+
+fn fileTryLock(userdata: ?*anyopaque, file: Io.File, lock: Io.File.Lock) Io.File.LockError!bool {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+
+ if (is_windows) {
+ const exclusive = switch (lock) {
+ .none => return,
+ .shared => false,
+ .exclusive => true,
+ };
+ try current_thread.checkCancel();
+ var io_status_block: windows.IO_STATUS_BLOCK = undefined;
+ const status = windows.ntdll.NtLockFile(
+ file.handle,
+ null,
+ null,
+ null,
+ &io_status_block,
+ &windows_lock_range_off,
+ &windows_lock_range_len,
+ null,
+ windows.TRUE,
+ @intFromBool(exclusive),
+ );
+ switch (status) {
+ .SUCCESS => return true,
+ .INSUFFICIENT_RESOURCES => return error.SystemResources,
+ .LOCK_NOT_GRANTED => return false,
+ .ACCESS_VIOLATION => |err| return windows.statusBug(err), // bad io_status_block pointer
+ else => return windows.unexpectedStatus(status),
+ }
+ }
+
+ const operation = switch (lock) {
+ .none => posix.LOCK.UN,
+ .shared => posix.LOCK.SH | posix.LOCK.NB,
+ .exclusive => posix.LOCK.EX | posix.LOCK.NB,
+ };
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (posix.errno(posix.system.flock(file.handle, operation))) {
+ .SUCCESS => {
+ current_thread.endSyscall();
+ return true;
+ },
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ .AGAIN => {
+ current_thread.endSyscall();
+ return false;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .BADF => |err| return errnoBug(err),
+ .INVAL => |err| return errnoBug(err), // invalid parameters
+ .NOLCK => return error.SystemResources,
+ .OPNOTSUPP => return error.FileLocksUnsupported,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
+}
+
+fn fileUnlock(userdata: ?*anyopaque, file: Io.File) void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+
+ if (is_windows) {
+ try current_thread.checkCancel();
+ var io_status_block: windows.IO_STATUS_BLOCK = undefined;
+ const status = windows.ntdll.NtUnlockFile(
+ file.handle,
+ &io_status_block,
+ &windows_lock_range_off,
+ &windows_lock_range_len,
+ null,
+ );
+ if (is_debug) switch (status) {
+ .SUCCESS => {},
+ .RANGE_NOT_LOCKED => unreachable, // Function asserts unlocked.
+ .ACCESS_VIOLATION => unreachable, // bad io_status_block pointer
+ else => unreachable, // Resource deallocation must succeed.
+ };
+ return;
+ }
+
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (posix.errno(posix.system.flock(file.handle, posix.LOCK.UN))) {
+ .SUCCESS => return current_thread.endSyscall(),
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ if (is_debug) switch (e) {
+ .AGAIN => unreachable, // unlocking can't block
+ .BADF => unreachable, // File descriptor used after closed.
+ .INVAL => unreachable, // invalid parameters
+ .NOLCK => unreachable, // Resource deallocation.
+ .OPNOTSUPP => unreachable, // We already got the lock.
+ else => unreachable, // Resource deallocation must succeed.
+ };
+ return;
+ },
+ }
+ }
+}
+
+fn fileDowngradeLock(userdata: ?*anyopaque, file: Io.File) Io.File.DowngradeLockError!void {
+ const t: *Threaded = @ptrCast(@alignCast(userdata));
+ const current_thread = Thread.getCurrent(t);
+
+ if (is_windows) {
+ try current_thread.checkCancel();
+ // 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;
+ switch (windows.ntdll.NtLockFile(
+ file.handle,
+ null,
+ null,
+ null,
+ &io_status_block,
+ &windows_lock_range_off,
+ &windows_lock_range_len,
+ null,
+ windows.TRUE,
+ windows.FALSE,
+ )) {
+ .SUCCESS => {},
+ .INSUFFICIENT_RESOURCES => |err| return windows.statusBug(err),
+ .LOCK_NOT_GRANTED => |err| return windows.statusBug(err), // File was not locked in exclusive mode.
+ .ACCESS_VIOLATION => |err| return windows.statusBug(err), // bad io_status_block pointer
+ else => |status| return windows.unexpectedStatus(status),
+ }
+ const status = windows.ntdll.NtUnlockFile(
+ file.handle,
+ &io_status_block,
+ &windows_lock_range_off,
+ &windows_lock_range_len,
+ null,
+ );
+ if (is_debug) switch (status) {
+ .SUCCESS => {},
+ .RANGE_NOT_LOCKED => unreachable, // File was not locked.
+ .ACCESS_VIOLATION => unreachable, // bad io_status_block pointer
+ else => unreachable, // Resource deallocation must succeed.
+ };
+ return;
+ }
+
+ const operation = posix.LOCK.SH | posix.LOCK.NB;
+
+ try current_thread.beginSyscall();
+ while (true) {
+ switch (posix.errno(posix.system.flock(file.handle, operation))) {
+ .SUCCESS => {
+ current_thread.endSyscall();
+ return true;
+ },
+ .CANCELED => return current_thread.endSyscallCanceled(),
+ .INTR => {
+ try current_thread.checkCancel();
+ continue;
+ },
+ else => |e| {
+ current_thread.endSyscall();
+ switch (e) {
+ .AGAIN => |err| return errnoBug(err), // File was not locked in exclusive mode.
+ .BADF => |err| return errnoBug(err),
+ .INVAL => |err| return errnoBug(err), // invalid parameters
+ .NOLCK => |err| return errnoBug(err), // Lock already obtained.
+ .OPNOTSUPP => |err| return errnoBug(err), // Lock already obtained.
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ },
+ }
+ }
}
fn dirOpenDirWasi(
diff --git a/lib/std/debug/SelfInfo/Windows.zig b/lib/std/debug/SelfInfo/Windows.zig
@@ -343,7 +343,7 @@ const Module = struct {
error.AntivirusInterference,
error.ProcessFdQuotaExceeded,
error.SystemFdQuotaExceeded,
- error.FileLocksNotSupported,
+ error.FileLocksUnsupported,
error.FileBusy,
=> return error.ReadFailed,
};
diff --git a/lib/std/fs.zig b/lib/std/fs.zig
@@ -23,12 +23,6 @@ pub const Dir = std.Io.Dir;
pub const File = std.Io.File;
pub const path = @import("fs/path.zig");
-
-pub const has_executable_bit = switch (native_os) {
- .windows, .wasi => false,
- else => true,
-};
-
pub const wasi = @import("fs/wasi.zig");
// TODO audit these APIs with respect to Dir and absolute paths
diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig
@@ -1,1356 +0,0 @@
-const File = @This();
-
-const builtin = @import("builtin");
-const native_os = builtin.os.tag;
-const is_windows = native_os == .windows;
-
-const std = @import("../std.zig");
-const Io = std.Io;
-const Os = std.builtin.Os;
-const Allocator = std.mem.Allocator;
-const posix = std.posix;
-const math = std.math;
-const assert = std.debug.assert;
-const linux = std.os.linux;
-const windows = std.os.windows;
-const maxInt = std.math.maxInt;
-const Alignment = std.mem.Alignment;
-
-/// The OS-specific file descriptor or file handle.
-handle: Handle,
-
-pub const Handle = Io.File.Handle;
-pub const Mode = Io.File.Mode;
-pub const INode = Io.File.INode;
-pub const Uid = Io.File.Uid;
-pub const Gid = Io.File.Gid;
-pub const Kind = Io.File.Kind;
-
-/// Deprecated in favor of `Io.File.OpenError`.
-pub const OpenError = Io.File.OpenError || error{WouldBlock};
-/// Deprecated in favor of `Io.File.OpenMode`.
-pub const OpenMode = Io.File.OpenMode;
-/// Deprecated in favor of `Io.File.Lock`.
-pub const Lock = Io.File.Lock;
-/// Deprecated in favor of `Io.File.OpenFlags`.
-pub const OpenFlags = Io.File.OpenFlags;
-
-pub const CreateFlags = std.Io.File.CreateFlags;
-
-pub fn stdout() File {
- return .{ .handle = if (is_windows) windows.peb().ProcessParameters.hStdOutput else posix.STDOUT_FILENO };
-}
-
-pub fn stderr() File {
- return .{ .handle = if (is_windows) windows.peb().ProcessParameters.hStdError else posix.STDERR_FILENO };
-}
-
-pub fn stdin() File {
- return .{ .handle = if (is_windows) windows.peb().ProcessParameters.hStdInput else posix.STDIN_FILENO };
-}
-
-/// 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 {
- 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 `getOrEnableAnsiEscapeSupport` and `supportsAnsiEscapeCodes`.
-pub fn isTty(self: File) bool {
- return posix.isatty(self.handle);
-}
-
-pub fn isCygwinPty(file: File) bool {
- if (builtin.os.tag != .windows) return false;
-
- const handle = file.handle;
-
- // If this is a MSYS2/cygwin pty, then it will be a named pipe with a name in one of these formats:
- // msys-[...]-ptyN-[...]
- // cygwin-[...]-ptyN-[...]
- //
- // Example: msys-1888ae32e00d56aa-pty0-to-master
-
- // First, just check that the handle is a named pipe.
- // This allows us to avoid the more costly NtQueryInformationFile call
- // for handles that aren't named pipes.
- {
- var io_status: windows.IO_STATUS_BLOCK = undefined;
- var device_info: windows.FILE.FS_DEVICE_INFORMATION = undefined;
- const rc = windows.ntdll.NtQueryVolumeInformationFile(handle, &io_status, &device_info, @sizeOf(windows.FILE.FS_DEVICE_INFORMATION), .Device);
- switch (rc) {
- .SUCCESS => {},
- else => return false,
- }
- if (device_info.DeviceType.FileDevice != .NAMED_PIPE) return false;
- }
-
- const name_bytes_offset = @offsetOf(windows.FILE_NAME_INFO, "FileName");
- // `NAME_MAX` UTF-16 code units (2 bytes each)
- // This buffer may not be long enough to handle *all* possible paths
- // (PATH_MAX_WIDE would be necessary for that), but because we only care
- // about certain paths and we know they must be within a reasonable length,
- // we can use this smaller buffer and just return false on any error from
- // NtQueryInformationFile.
- const num_name_bytes = windows.MAX_PATH * 2;
- var name_info_bytes align(@alignOf(windows.FILE_NAME_INFO)) = [_]u8{0} ** (name_bytes_offset + num_name_bytes);
-
- var io_status_block: windows.IO_STATUS_BLOCK = undefined;
- const rc = windows.ntdll.NtQueryInformationFile(handle, &io_status_block, &name_info_bytes, @intCast(name_info_bytes.len), .Name);
- switch (rc) {
- .SUCCESS => {},
- .INVALID_PARAMETER => unreachable,
- else => return false,
- }
-
- const name_info: *const windows.FILE_NAME_INFO = @ptrCast(&name_info_bytes);
- const name_bytes = name_info_bytes[name_bytes_offset .. name_bytes_offset + name_info.FileNameLength];
- const name_wide = std.mem.bytesAsSlice(u16, name_bytes);
- // The name we get from NtQueryInformationFile will be prefixed with a '\', e.g. \msys-1888ae32e00d56aa-pty0-to-master
- return (std.mem.startsWith(u16, name_wide, &[_]u16{ '\\', 'm', 's', 'y', 's', '-' }) or
- std.mem.startsWith(u16, name_wide, &[_]u16{ '\\', 'c', 'y', 'g', 'w', 'i', 'n', '-' })) and
- std.mem.find(u16, name_wide, &[_]u16{ '-', 'p', 't', 'y' }) != null;
-}
-
-/// Returns whether or not ANSI escape codes will be treated as such,
-/// and attempts to enable support for ANSI escape codes if necessary
-/// (on Windows).
-///
-/// Returns `true` if ANSI escape codes are supported or support was
-/// successfully enabled. Returns false if ANSI escape codes are not
-/// supported or support was unable to be enabled.
-///
-/// See also `supportsAnsiEscapeCodes`.
-pub fn getOrEnableAnsiEscapeSupport(self: File) bool {
- if (builtin.os.tag == .windows) {
- var original_console_mode: windows.DWORD = 0;
-
- // For Windows Terminal, VT Sequences processing is enabled by default.
- if (windows.kernel32.GetConsoleMode(self.handle, &original_console_mode) != 0) {
- if (original_console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true;
-
- // For Windows Console, VT Sequences processing support was added in Windows 10 build 14361, but disabled by default.
- // https://devblogs.microsoft.com/commandline/tmux-support-arrives-for-bash-on-ubuntu-on-windows/
- //
- // Note: In Microsoft's example for enabling virtual terminal processing, it
- // shows attempting to enable `DISABLE_NEWLINE_AUTO_RETURN` as well:
- // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#example-of-enabling-virtual-terminal-processing
- // This is avoided because in the old Windows Console, that flag causes \n (as opposed to \r\n)
- // to behave unexpectedly (the cursor moves down 1 row but remains on the same column).
- // Additionally, the default console mode in Windows Terminal does not have
- // `DISABLE_NEWLINE_AUTO_RETURN` set, so by only enabling `ENABLE_VIRTUAL_TERMINAL_PROCESSING`
- // we end up matching the mode of Windows Terminal.
- const requested_console_modes = windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
- const console_mode = original_console_mode | requested_console_modes;
- if (windows.kernel32.SetConsoleMode(self.handle, console_mode) != 0) return true;
- }
-
- return self.isCygwinPty();
- }
- return self.supportsAnsiEscapeCodes();
-}
-
-/// Test whether ANSI escape codes will be treated as such without
-/// attempting to enable support for ANSI escape codes.
-///
-/// See also `getOrEnableAnsiEscapeSupport`.
-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 self.isCygwinPty();
- }
- 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 GetEndPosError = std.os.windows.GetFileSizeError || StatError;
-
-/// TODO: integrate with async I/O
-pub fn getEndPos(self: File) GetEndPosError!u64 {
- if (builtin.os.tag == .windows) {
- return windows.GetFileSizeEx(self.handle);
- }
- return (try self.stat()).size;
-}
-
-pub const ModeError = StatError;
-
-/// 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;
-}
-
-/// Deprecated in favor of `Io.File.Stat`.
-pub const Stat = Io.File.Stat;
-
-/// Deprecated in favor of `Io.File.StatError`.
-pub const StatError = posix.FStatError;
-
-/// Deprecated in favor of `Io.File.stat`.
-pub fn stat(self: File) StatError!Stat {
- var threaded: Io.Threaded = .init_single_threaded;
- const io = threaded.ioBasic();
- return Io.File.stat(.{ .handle = self.handle }, io);
-}
-
-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),
- .Basic,
- );
- 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);
- },
- }
-}
-
-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: Io.Timestamp,
- /// last modification timestamp in nanoseconds
- mtime: Io.Timestamp,
-) 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{
- .sec = math.cast(isize, @divFloor(atime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize),
- .nsec = math.cast(isize, @mod(atime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize),
- },
- posix.timespec{
- .sec = math.cast(isize, @divFloor(mtime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize),
- .nsec = math.cast(isize, @mod(mtime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize),
- },
- };
- try posix.futimens(self.handle, ×);
-}
-
-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);
- }
-
- return posix.read(self.handle, buffer);
-}
-
-/// 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);
- }
-
- return posix.pread(self.handle, buffer, offset);
-}
-
-/// Deprecated in favor of `Reader`.
-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) {
- if (iovecs.len == 0) return 0;
- const first = iovecs[0];
- return windows.ReadFile(self.handle, first.base[0..first.len], null);
- }
-
- return posix.readv(self.handle, iovecs);
-}
-
-/// 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) {
- if (iovecs.len == 0) return 0;
- const first = iovecs[0];
- return windows.ReadFile(self.handle, first.base[0..first.len], offset);
- }
-
- return posix.preadv(self.handle, iovecs, offset);
-}
-
-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);
- }
-
- return posix.write(self.handle, bytes);
-}
-
-pub fn writeAll(self: File, bytes: []const u8) WriteError!void {
- var index: usize = 0;
- while (index < bytes.len) {
- index += try self.write(bytes[index..]);
- }
-}
-
-/// Deprecated in favor of `Writer`.
-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);
- }
-}
-
-/// 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);
- }
-
- return posix.pwrite(self.handle, bytes, offset);
-}
-
-/// See https://github.com/ziglang/zig/issues/7699
-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 0;
- const first = iovecs[0];
- return windows.WriteFile(self.handle, first.base[0..first.len], null);
- }
-
- return posix.writev(self.handle, iovecs);
-}
-
-/// 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) {
- if (iovecs.len == 0) return 0;
- const first = iovecs[0];
- return windows.WriteFile(self.handle, first.base[0..first.len], offset);
- }
-
- return posix.pwritev(self.handle, iovecs, offset);
-}
-
-/// Deprecated in favor of `Writer`.
-pub const CopyRangeError = posix.CopyFileRangeError;
-
-/// Deprecated in favor of `Writer`.
-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;
-}
-
-/// Deprecated in favor of `Writer`.
-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;
-}
-
-/// Deprecated in favor of `Io.File.Reader`.
-pub const Reader = Io.File.Reader;
-
-pub const Writer = struct {
- file: File,
- err: ?WriteError = null,
- mode: Writer.Mode = .positional,
- /// Tracks the true seek position in the file. To obtain the logical
- /// position, add the buffer size to this value.
- pos: u64 = 0,
- sendfile_err: ?SendfileError = null,
- copy_file_range_err: ?CopyFileRangeError = null,
- fcopyfile_err: ?FcopyfileError = null,
- seek_err: ?Writer.SeekError = null,
- interface: Io.Writer,
-
- pub const Mode = Reader.Mode;
-
- pub const SendfileError = error{
- UnsupportedOperation,
- SystemResources,
- InputOutput,
- BrokenPipe,
- WouldBlock,
- Unexpected,
- };
-
- pub const CopyFileRangeError = std.os.freebsd.CopyFileRangeError || std.os.linux.wrapped.CopyFileRangeError;
-
- pub const FcopyfileError = error{
- OperationNotSupported,
- OutOfMemory,
- Unexpected,
- };
-
- pub const SeekError = Io.File.SeekError;
-
- /// Number of slices to store on the stack, when trying to send as many byte
- /// vectors through the underlying write calls as possible.
- const max_buffers_len = 16;
-
- pub fn init(file: File, buffer: []u8) Writer {
- return .{
- .file = file,
- .interface = initInterface(buffer),
- .mode = .positional,
- };
- }
-
- /// Positional is more threadsafe, since the global seek position is not
- /// affected, but when such syscalls are not available, preemptively
- /// initializing in streaming mode will skip a failed syscall.
- pub fn initStreaming(file: File, buffer: []u8) Writer {
- return .{
- .file = file,
- .interface = initInterface(buffer),
- .mode = .streaming,
- };
- }
-
- pub fn initInterface(buffer: []u8) Io.Writer {
- return .{
- .vtable = &.{
- .drain = drain,
- .sendFile = sendFile,
- },
- .buffer = buffer,
- };
- }
-
- /// TODO when this logic moves from fs.File to Io.File the io parameter should be deleted
- pub fn moveToReader(w: *Writer, io: Io) Reader {
- defer w.* = undefined;
- return .{
- .io = io,
- .file = .{ .handle = w.file.handle },
- .mode = w.mode,
- .pos = w.pos,
- .interface = Reader.initInterface(w.interface.buffer),
- .seek_err = w.seek_err,
- };
- }
-
- pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
- const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
- const handle = w.file.handle;
- const buffered = io_w.buffered();
- if (is_windows) switch (w.mode) {
- .positional, .positional_reading => {
- if (buffered.len != 0) {
- const n = windows.WriteFile(handle, buffered, w.pos) catch |err| {
- w.err = err;
- return error.WriteFailed;
- };
- w.pos += n;
- return io_w.consume(n);
- }
- for (data[0 .. data.len - 1]) |buf| {
- if (buf.len == 0) continue;
- const n = windows.WriteFile(handle, buf, w.pos) catch |err| {
- w.err = err;
- return error.WriteFailed;
- };
- w.pos += n;
- return io_w.consume(n);
- }
- const pattern = data[data.len - 1];
- if (pattern.len == 0 or splat == 0) return 0;
- const n = windows.WriteFile(handle, pattern, w.pos) catch |err| {
- w.err = err;
- return error.WriteFailed;
- };
- w.pos += n;
- return io_w.consume(n);
- },
- .streaming, .streaming_reading => {
- if (buffered.len != 0) {
- const n = windows.WriteFile(handle, buffered, null) catch |err| {
- w.err = err;
- return error.WriteFailed;
- };
- w.pos += n;
- return io_w.consume(n);
- }
- for (data[0 .. data.len - 1]) |buf| {
- if (buf.len == 0) continue;
- const n = windows.WriteFile(handle, buf, null) catch |err| {
- w.err = err;
- return error.WriteFailed;
- };
- w.pos += n;
- return io_w.consume(n);
- }
- const pattern = data[data.len - 1];
- if (pattern.len == 0 or splat == 0) return 0;
- const n = windows.WriteFile(handle, pattern, null) catch |err| {
- w.err = err;
- return error.WriteFailed;
- };
- w.pos += n;
- return io_w.consume(n);
- },
- .failure => return error.WriteFailed,
- };
- var iovecs: [max_buffers_len]std.posix.iovec_const = undefined;
- var len: usize = 0;
- if (buffered.len > 0) {
- iovecs[len] = .{ .base = buffered.ptr, .len = buffered.len };
- len += 1;
- }
- for (data[0 .. data.len - 1]) |d| {
- if (d.len == 0) continue;
- iovecs[len] = .{ .base = d.ptr, .len = d.len };
- len += 1;
- if (iovecs.len - len == 0) break;
- }
- const pattern = data[data.len - 1];
- if (iovecs.len - len != 0) switch (splat) {
- 0 => {},
- 1 => if (pattern.len != 0) {
- iovecs[len] = .{ .base = pattern.ptr, .len = pattern.len };
- len += 1;
- },
- else => switch (pattern.len) {
- 0 => {},
- 1 => {
- const splat_buffer_candidate = io_w.buffer[io_w.end..];
- var backup_buffer: [64]u8 = undefined;
- const splat_buffer = if (splat_buffer_candidate.len >= backup_buffer.len)
- splat_buffer_candidate
- else
- &backup_buffer;
- const memset_len = @min(splat_buffer.len, splat);
- const buf = splat_buffer[0..memset_len];
- @memset(buf, pattern[0]);
- iovecs[len] = .{ .base = buf.ptr, .len = buf.len };
- len += 1;
- var remaining_splat = splat - buf.len;
- while (remaining_splat > splat_buffer.len and iovecs.len - len != 0) {
- assert(buf.len == splat_buffer.len);
- iovecs[len] = .{ .base = splat_buffer.ptr, .len = splat_buffer.len };
- len += 1;
- remaining_splat -= splat_buffer.len;
- }
- if (remaining_splat > 0 and iovecs.len - len != 0) {
- iovecs[len] = .{ .base = splat_buffer.ptr, .len = remaining_splat };
- len += 1;
- }
- },
- else => for (0..splat) |_| {
- iovecs[len] = .{ .base = pattern.ptr, .len = pattern.len };
- len += 1;
- if (iovecs.len - len == 0) break;
- },
- },
- };
- if (len == 0) return 0;
- switch (w.mode) {
- .positional, .positional_reading => {
- const n = std.posix.pwritev(handle, iovecs[0..len], w.pos) catch |err| switch (err) {
- error.Unseekable => {
- w.mode = w.mode.toStreaming();
- const pos = w.pos;
- if (pos != 0) {
- w.pos = 0;
- w.seekTo(@intCast(pos)) catch {
- w.mode = .failure;
- return error.WriteFailed;
- };
- }
- return 0;
- },
- else => |e| {
- w.err = e;
- return error.WriteFailed;
- },
- };
- w.pos += n;
- return io_w.consume(n);
- },
- .streaming, .streaming_reading => {
- const n = std.posix.writev(handle, iovecs[0..len]) catch |err| {
- w.err = err;
- return error.WriteFailed;
- };
- w.pos += n;
- return io_w.consume(n);
- },
- .failure => return error.WriteFailed,
- }
- }
-
- pub fn sendFile(
- io_w: *Io.Writer,
- file_reader: *Io.File.Reader,
- limit: Io.Limit,
- ) Io.Writer.FileError!usize {
- const reader_buffered = file_reader.interface.buffered();
- if (reader_buffered.len >= @intFromEnum(limit))
- return sendFileBuffered(io_w, file_reader, limit.slice(reader_buffered));
- const writer_buffered = io_w.buffered();
- const file_limit = @intFromEnum(limit) - reader_buffered.len;
- const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
- const out_fd = w.file.handle;
- const in_fd = file_reader.file.handle;
-
- if (file_reader.size) |size| {
- if (size - file_reader.pos == 0) {
- if (reader_buffered.len != 0) {
- return sendFileBuffered(io_w, file_reader, reader_buffered);
- } else {
- return error.EndOfStream;
- }
- }
- }
-
- if (native_os == .freebsd and w.mode == .streaming) sf: {
- // Try using sendfile on FreeBSD.
- if (w.sendfile_err != null) break :sf;
- const offset = std.math.cast(std.c.off_t, file_reader.pos) orelse break :sf;
- var hdtr_data: std.c.sf_hdtr = undefined;
- var headers: [2]posix.iovec_const = undefined;
- var headers_i: u8 = 0;
- if (writer_buffered.len != 0) {
- headers[headers_i] = .{ .base = writer_buffered.ptr, .len = writer_buffered.len };
- headers_i += 1;
- }
- if (reader_buffered.len != 0) {
- headers[headers_i] = .{ .base = reader_buffered.ptr, .len = reader_buffered.len };
- headers_i += 1;
- }
- const hdtr: ?*std.c.sf_hdtr = if (headers_i == 0) null else b: {
- hdtr_data = .{
- .headers = &headers,
- .hdr_cnt = headers_i,
- .trailers = null,
- .trl_cnt = 0,
- };
- break :b &hdtr_data;
- };
- var sbytes: std.c.off_t = undefined;
- const nbytes: usize = @min(file_limit, maxInt(usize));
- const flags = 0;
- switch (posix.errno(std.c.sendfile(in_fd, out_fd, offset, nbytes, hdtr, &sbytes, flags))) {
- .SUCCESS, .INTR => {},
- .INVAL, .OPNOTSUPP, .NOTSOCK, .NOSYS => w.sendfile_err = error.UnsupportedOperation,
- .BADF => if (builtin.mode == .Debug) @panic("race condition") else {
- w.sendfile_err = error.Unexpected;
- },
- .FAULT => if (builtin.mode == .Debug) @panic("segmentation fault") else {
- w.sendfile_err = error.Unexpected;
- },
- .NOTCONN => w.sendfile_err = error.BrokenPipe,
- .AGAIN, .BUSY => if (sbytes == 0) {
- w.sendfile_err = error.WouldBlock;
- },
- .IO => w.sendfile_err = error.InputOutput,
- .PIPE => w.sendfile_err = error.BrokenPipe,
- .NOBUFS => w.sendfile_err = error.SystemResources,
- else => |err| w.sendfile_err = posix.unexpectedErrno(err),
- }
- if (w.sendfile_err != null) {
- // Give calling code chance to observe the error before trying
- // something else.
- return 0;
- }
- if (sbytes == 0) {
- file_reader.size = file_reader.pos;
- return error.EndOfStream;
- }
- const consumed = io_w.consume(@intCast(sbytes));
- file_reader.seekBy(@intCast(consumed)) catch return error.ReadFailed;
- return consumed;
- }
-
- if (native_os.isDarwin() and w.mode == .streaming) sf: {
- // Try using sendfile on macOS.
- if (w.sendfile_err != null) break :sf;
- const offset = std.math.cast(std.c.off_t, file_reader.pos) orelse break :sf;
- var hdtr_data: std.c.sf_hdtr = undefined;
- var headers: [2]posix.iovec_const = undefined;
- var headers_i: u8 = 0;
- if (writer_buffered.len != 0) {
- headers[headers_i] = .{ .base = writer_buffered.ptr, .len = writer_buffered.len };
- headers_i += 1;
- }
- if (reader_buffered.len != 0) {
- headers[headers_i] = .{ .base = reader_buffered.ptr, .len = reader_buffered.len };
- headers_i += 1;
- }
- const hdtr: ?*std.c.sf_hdtr = if (headers_i == 0) null else b: {
- hdtr_data = .{
- .headers = &headers,
- .hdr_cnt = headers_i,
- .trailers = null,
- .trl_cnt = 0,
- };
- break :b &hdtr_data;
- };
- const max_count = maxInt(i32); // Avoid EINVAL.
- var len: std.c.off_t = @min(file_limit, max_count);
- const flags = 0;
- switch (posix.errno(std.c.sendfile(in_fd, out_fd, offset, &len, hdtr, flags))) {
- .SUCCESS, .INTR => {},
- .OPNOTSUPP, .NOTSOCK, .NOSYS => w.sendfile_err = error.UnsupportedOperation,
- .BADF => if (builtin.mode == .Debug) @panic("race condition") else {
- w.sendfile_err = error.Unexpected;
- },
- .FAULT => if (builtin.mode == .Debug) @panic("segmentation fault") else {
- w.sendfile_err = error.Unexpected;
- },
- .INVAL => if (builtin.mode == .Debug) @panic("invalid API usage") else {
- w.sendfile_err = error.Unexpected;
- },
- .NOTCONN => w.sendfile_err = error.BrokenPipe,
- .AGAIN => if (len == 0) {
- w.sendfile_err = error.WouldBlock;
- },
- .IO => w.sendfile_err = error.InputOutput,
- .PIPE => w.sendfile_err = error.BrokenPipe,
- else => |err| w.sendfile_err = posix.unexpectedErrno(err),
- }
- if (w.sendfile_err != null) {
- // Give calling code chance to observe the error before trying
- // something else.
- return 0;
- }
- if (len == 0) {
- file_reader.size = file_reader.pos;
- return error.EndOfStream;
- }
- const consumed = io_w.consume(@bitCast(len));
- file_reader.seekBy(@intCast(consumed)) catch return error.ReadFailed;
- return consumed;
- }
-
- if (native_os == .linux and w.mode == .streaming) sf: {
- // Try using sendfile on Linux.
- if (w.sendfile_err != null) break :sf;
- // Linux sendfile does not support headers.
- if (writer_buffered.len != 0 or reader_buffered.len != 0)
- return sendFileBuffered(io_w, file_reader, reader_buffered);
- const max_count = 0x7ffff000; // Avoid EINVAL.
- var off: std.os.linux.off_t = undefined;
- const off_ptr: ?*std.os.linux.off_t, const count: usize = switch (file_reader.mode) {
- .positional => o: {
- const size = file_reader.getSize() catch return 0;
- off = std.math.cast(std.os.linux.off_t, file_reader.pos) orelse return error.ReadFailed;
- break :o .{ &off, @min(@intFromEnum(limit), size - file_reader.pos, max_count) };
- },
- .streaming => .{ null, limit.minInt(max_count) },
- .streaming_reading, .positional_reading => break :sf,
- .failure => return error.ReadFailed,
- };
- const n = std.os.linux.wrapped.sendfile(out_fd, in_fd, off_ptr, count) catch |err| switch (err) {
- error.Unseekable => {
- file_reader.mode = file_reader.mode.toStreaming();
- const pos = file_reader.pos;
- if (pos != 0) {
- file_reader.pos = 0;
- file_reader.seekBy(@intCast(pos)) catch {
- file_reader.mode = .failure;
- return error.ReadFailed;
- };
- }
- return 0;
- },
- else => |e| {
- w.sendfile_err = e;
- return 0;
- },
- };
- if (n == 0) {
- file_reader.size = file_reader.pos;
- return error.EndOfStream;
- }
- file_reader.pos += n;
- w.pos += n;
- return n;
- }
-
- const copy_file_range = switch (native_os) {
- .freebsd => std.os.freebsd.copy_file_range,
- .linux => std.os.linux.wrapped.copy_file_range,
- else => {},
- };
- if (@TypeOf(copy_file_range) != void) cfr: {
- if (w.copy_file_range_err != null) break :cfr;
- if (writer_buffered.len != 0 or reader_buffered.len != 0)
- return sendFileBuffered(io_w, file_reader, reader_buffered);
- var off_in: i64 = undefined;
- var off_out: i64 = undefined;
- const off_in_ptr: ?*i64 = switch (file_reader.mode) {
- .positional_reading, .streaming_reading => return error.Unimplemented,
- .positional => p: {
- off_in = @intCast(file_reader.pos);
- break :p &off_in;
- },
- .streaming => null,
- .failure => return error.WriteFailed,
- };
- const off_out_ptr: ?*i64 = switch (w.mode) {
- .positional_reading, .streaming_reading => return error.Unimplemented,
- .positional => p: {
- off_out = @intCast(w.pos);
- break :p &off_out;
- },
- .streaming => null,
- .failure => return error.WriteFailed,
- };
- const n = copy_file_range(in_fd, off_in_ptr, out_fd, off_out_ptr, @intFromEnum(limit), 0) catch |err| {
- w.copy_file_range_err = err;
- return 0;
- };
- if (n == 0) {
- file_reader.size = file_reader.pos;
- return error.EndOfStream;
- }
- file_reader.pos += n;
- w.pos += n;
- return n;
- }
-
- if (builtin.os.tag.isDarwin()) fcf: {
- if (w.fcopyfile_err != null) break :fcf;
- if (file_reader.pos != 0) break :fcf;
- if (w.pos != 0) break :fcf;
- if (limit != .unlimited) break :fcf;
- const size = file_reader.getSize() catch break :fcf;
- if (writer_buffered.len != 0 or reader_buffered.len != 0)
- return sendFileBuffered(io_w, file_reader, reader_buffered);
- const rc = std.c.fcopyfile(in_fd, out_fd, null, .{ .DATA = true });
- switch (posix.errno(rc)) {
- .SUCCESS => {},
- .INVAL => if (builtin.mode == .Debug) @panic("invalid API usage") else {
- w.fcopyfile_err = error.Unexpected;
- return 0;
- },
- .NOMEM => {
- w.fcopyfile_err = error.OutOfMemory;
- return 0;
- },
- .OPNOTSUPP => {
- w.fcopyfile_err = error.OperationNotSupported;
- return 0;
- },
- else => |err| {
- w.fcopyfile_err = posix.unexpectedErrno(err);
- return 0;
- },
- }
- file_reader.pos = size;
- w.pos = size;
- return size;
- }
-
- return error.Unimplemented;
- }
-
- fn sendFileBuffered(
- io_w: *Io.Writer,
- file_reader: *Io.File.Reader,
- reader_buffered: []const u8,
- ) Io.Writer.FileError!usize {
- const n = try drain(io_w, &.{reader_buffered}, 1);
- file_reader.seekBy(@intCast(n)) catch return error.ReadFailed;
- return n;
- }
-
- pub fn seekTo(w: *Writer, offset: u64) (Writer.SeekError || Io.Writer.Error)!void {
- try w.interface.flush();
- try seekToUnbuffered(w, offset);
- }
-
- /// Asserts that no data is currently buffered.
- pub fn seekToUnbuffered(w: *Writer, offset: u64) Writer.SeekError!void {
- assert(w.interface.buffered().len == 0);
- switch (w.mode) {
- .positional, .positional_reading => {
- w.pos = offset;
- },
- .streaming, .streaming_reading => {
- if (w.seek_err) |err| return err;
- posix.lseek_SET(w.file.handle, offset) catch |err| {
- w.seek_err = err;
- return err;
- };
- w.pos = offset;
- },
- .failure => return w.seek_err.?,
- }
- }
-
- pub const EndError = SetEndPosError || Io.Writer.Error;
-
- /// Flushes any buffered data and sets the end position of the file.
- ///
- /// If not overwriting existing contents, then calling `interface.flush`
- /// directly is sufficient.
- ///
- /// Flush failure is handled by setting `err` so that it can be handled
- /// along with other write failures.
- pub fn end(w: *Writer) EndError!void {
- try w.interface.flush();
- switch (w.mode) {
- .positional,
- .positional_reading,
- => w.file.setEndPos(w.pos) catch |err| switch (err) {
- error.NonResizable => return,
- else => |e| return e,
- },
-
- .streaming,
- .streaming_reading,
- .failure,
- => {},
- }
- }
-};
-
-/// Defaults to positional reading; falls back to streaming.
-///
-/// Positional is more threadsafe, since the global seek position is not
-/// affected.
-pub fn reader(file: File, io: Io, buffer: []u8) Reader {
- return .init(.{ .handle = file.handle }, io, buffer);
-}
-
-/// Positional is more threadsafe, since the global seek position is not
-/// affected, but when such syscalls are not available, preemptively
-/// initializing in streaming mode skips a failed syscall.
-pub fn readerStreaming(file: File, io: Io, buffer: []u8) Reader {
- return .initStreaming(.{ .handle = file.handle }, io, buffer);
-}
-
-/// Defaults to positional reading; falls back to streaming.
-///
-/// Positional is more threadsafe, since the global seek position is not
-/// affected.
-pub fn writer(file: File, buffer: []u8) Writer {
- return .init(file, buffer);
-}
-
-/// Positional is more threadsafe, since the global seek position is not
-/// affected, but when such syscalls are not available, preemptively
-/// initializing in streaming mode will skip a failed syscall.
-pub fn writerStreaming(file: File, buffer: []u8) Writer {
- return .initStreaming(file, buffer);
-}
-
-const range_off: windows.LARGE_INTEGER = 0;
-const range_len: windows.LARGE_INTEGER = 1;
-
-/// Deprecated
-pub const LockError = Io.File.LockError;
-
-/// 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,
- 0,
- ) 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,
- 0,
- ) 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,
- };
- }
-}
-
-pub fn adaptToNewApi(file: File) Io.File {
- return .{ .handle = file.handle };
-}
-
-pub fn adaptFromNewApi(file: Io.File) File {
- return .{ .handle = file.handle };
-}
diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig
@@ -4248,80 +4248,6 @@ pub fn InitOnceExecuteOnce(InitOnce: *INIT_ONCE, InitFn: INIT_ONCE_FN, Parameter
assert(kernel32.InitOnceExecuteOnce(InitOnce, InitFn, Parameter, Context) != 0);
}
-pub const SetFileTimeError = error{Unexpected};
-
-pub fn SetFileTime(
- hFile: HANDLE,
- lpCreationTime: ?*const FILETIME,
- lpLastAccessTime: ?*const FILETIME,
- lpLastWriteTime: ?*const FILETIME,
-) SetFileTimeError!void {
- const rc = kernel32.SetFileTime(hFile, lpCreationTime, lpLastAccessTime, lpLastWriteTime);
- if (rc == 0) {
- switch (GetLastError()) {
- else => |err| return unexpectedError(err),
- }
- }
-}
-
-pub const LockFileError = error{
- SystemResources,
- WouldBlock,
-} || UnexpectedError;
-
-pub fn LockFile(
- FileHandle: HANDLE,
- Event: ?HANDLE,
- ApcRoutine: ?*const IO_APC_ROUTINE,
- ApcContext: ?*anyopaque,
- IoStatusBlock: *IO_STATUS_BLOCK,
- ByteOffset: *const LARGE_INTEGER,
- Length: *const LARGE_INTEGER,
- Key: ?*ULONG,
- FailImmediately: BOOLEAN,
- ExclusiveLock: BOOLEAN,
-) !void {
- const rc = ntdll.NtLockFile(
- FileHandle,
- Event,
- ApcRoutine,
- ApcContext,
- IoStatusBlock,
- ByteOffset,
- Length,
- Key,
- FailImmediately,
- ExclusiveLock,
- );
- switch (rc) {
- .SUCCESS => return,
- .INSUFFICIENT_RESOURCES => return error.SystemResources,
- .LOCK_NOT_GRANTED => return error.WouldBlock,
- .ACCESS_VIOLATION => unreachable, // bad io_status_block pointer
- else => return unexpectedStatus(rc),
- }
-}
-
-pub const UnlockFileError = error{
- RangeNotLocked,
-} || UnexpectedError;
-
-pub fn UnlockFile(
- FileHandle: HANDLE,
- IoStatusBlock: *IO_STATUS_BLOCK,
- ByteOffset: *const LARGE_INTEGER,
- Length: *const LARGE_INTEGER,
- Key: ULONG,
-) !void {
- const rc = ntdll.NtUnlockFile(FileHandle, IoStatusBlock, ByteOffset, Length, Key);
- switch (rc) {
- .SUCCESS => return,
- .RANGE_NOT_LOCKED => return error.RangeNotLocked,
- .ACCESS_VIOLATION => unreachable, // bad io_status_block pointer
- else => return unexpectedStatus(rc),
- }
-}
-
/// This is a workaround for the C backend until zig has the ability to put
/// C code in inline assembly.
extern fn zig_thumb_windows_teb() callconv(.c) *anyopaque;
diff --git a/lib/std/posix.zig b/lib/std/posix.zig
@@ -309,17 +309,7 @@ pub fn close(fd: fd_t) void {
}
}
-pub const FChmodError = error{
- AccessDenied,
- PermissionDenied,
- InputOutput,
- SymLinkLoop,
- FileNotFound,
- SystemResources,
- ReadOnlyFileSystem,
-} || UnexpectedError;
-
-pub const FChmodAtError = FChmodError || error{
+pub const FChmodAtError = std.Io.File.SetPermissionsError || error{
/// A component of `path` exceeded `NAME_MAX`, or the entire path exceeded
/// `PATH_MAX`.
NameTooLong,
@@ -335,7 +325,6 @@ pub const FChmodAtError = FChmodError || error{
ProcessFdQuotaExceeded,
/// The procfs fallback was used but the system exceeded it open file limit.
SystemFdQuotaExceeded,
- Canceled,
};
/// Changes the `mode` of `path` relative to the directory referred to by
@@ -969,72 +958,6 @@ pub fn pread(fd: fd_t, buf: []u8, offset: u64) PReadError!usize {
}
}
-pub const TruncateError = error{
- FileTooBig,
- InputOutput,
- FileBusy,
- AccessDenied,
- PermissionDenied,
- NonResizable,
-} || UnexpectedError;
-
-/// Length must be positive when treated as an i64.
-pub fn ftruncate(fd: fd_t, length: u64) TruncateError!void {
- const signed_len: i64 = @bitCast(length);
- if (signed_len < 0) return error.FileTooBig; // avoid ambiguous EINVAL errors
-
- if (native_os == .windows) {
- var io_status_block: windows.IO_STATUS_BLOCK = undefined;
- const eof_info: windows.FILE.END_OF_FILE_INFORMATION = .{
- .EndOfFile = signed_len,
- };
- const rc = windows.ntdll.NtSetInformationFile(
- fd,
- &io_status_block,
- &eof_info,
- @sizeOf(windows.FILE.END_OF_FILE_INFORMATION),
- .EndOfFile,
- );
- switch (rc) {
- .SUCCESS => return,
- .INVALID_HANDLE => unreachable, // Handle not open for writing
- .ACCESS_DENIED => return error.AccessDenied,
- .USER_MAPPED_FILE => return error.AccessDenied,
- .INVALID_PARAMETER => return error.FileTooBig,
- else => return windows.unexpectedStatus(rc),
- }
- }
- if (native_os == .wasi and !builtin.link_libc) {
- switch (wasi.fd_filestat_set_size(fd, length)) {
- .SUCCESS => return,
- .INTR => unreachable,
- .FBIG => return error.FileTooBig,
- .IO => return error.InputOutput,
- .PERM => return error.PermissionDenied,
- .TXTBSY => return error.FileBusy,
- .BADF => unreachable, // Handle not open for writing
- .INVAL => return error.NonResizable,
- .NOTCAPABLE => return error.AccessDenied,
- else => |err| return unexpectedErrno(err),
- }
- }
-
- const ftruncate_sym = if (lfs64_abi) system.ftruncate64 else system.ftruncate;
- while (true) {
- switch (errno(ftruncate_sym(fd, signed_len))) {
- .SUCCESS => return,
- .INTR => continue,
- .FBIG => return error.FileTooBig,
- .IO => return error.InputOutput,
- .PERM => return error.PermissionDenied,
- .TXTBSY => return error.FileBusy,
- .BADF => unreachable, // Handle not open for writing
- .INVAL => return error.NonResizable, // This is returned for /dev/null for example.
- else => |err| return unexpectedErrno(err),
- }
- }
-}
-
/// Number of bytes read is returned. Upon reading end-of-file, zero is returned.
///
/// Retries when interrupted by a signal.
@@ -1602,7 +1525,7 @@ pub fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: O, mode: mode_t) O
.PERM => return error.PermissionDenied,
.EXIST => return error.PathAlreadyExists,
.BUSY => return error.DeviceBusy,
- .OPNOTSUPP => return error.FileLocksNotSupported,
+ .OPNOTSUPP => return error.FileLocksUnsupported,
.AGAIN => return error.WouldBlock,
.TXTBSY => return error.FileBusy,
.NXIO => return error.NoDevice,
@@ -2313,47 +2236,6 @@ pub fn getegid() gid_t {
return system.getegid();
}
-/// Test whether a file descriptor refers to a terminal.
-pub fn isatty(handle: fd_t) bool {
- if (native_os == .windows) {
- if (fs.File.isCygwinPty(.{ .handle = handle }))
- return true;
-
- var out: windows.DWORD = undefined;
- return windows.kernel32.GetConsoleMode(handle, &out) != 0;
- }
- if (builtin.link_libc) {
- return system.isatty(handle) != 0;
- }
- if (native_os == .wasi) {
- var statbuf: wasi.fdstat_t = undefined;
- const err = wasi.fd_fdstat_get(handle, &statbuf);
- if (err != .SUCCESS)
- return false;
-
- // A tty is a character device that we can't seek or tell on.
- if (statbuf.fs_filetype != .CHARACTER_DEVICE)
- return false;
- if (statbuf.fs_rights_base.FD_SEEK or statbuf.fs_rights_base.FD_TELL)
- return false;
-
- return true;
- }
- if (native_os == .linux) {
- while (true) {
- var wsz: winsize = undefined;
- const fd: usize = @bitCast(@as(isize, handle));
- const rc = linux.syscall3(.ioctl, fd, linux.T.IOCGWINSZ, @intFromPtr(&wsz));
- switch (linux.errno(rc)) {
- .SUCCESS => return true,
- .INTR => continue,
- else => return false,
- }
- }
- }
- return system.isatty(handle) != 0;
-}
-
pub const SocketError = error{
/// Permission to create a socket of the specified type and/or
/// pro‐tocol is denied.
@@ -3923,34 +3805,6 @@ pub fn fcntl(fd: fd_t, cmd: i32, arg: usize) FcntlError!usize {
}
}
-pub const FlockError = error{
- WouldBlock,
-
- /// The kernel ran out of memory for allocating file locks
- SystemResources,
-
- /// The underlying filesystem does not support file locks
- FileLocksNotSupported,
-} || UnexpectedError;
-
-/// Depending on the operating system `flock` may or may not interact with
-/// `fcntl` locks made by other processes.
-pub fn flock(fd: fd_t, operation: i32) FlockError!void {
- while (true) {
- const rc = system.flock(fd, operation);
- switch (errno(rc)) {
- .SUCCESS => return,
- .BADF => unreachable,
- .INTR => continue,
- .INVAL => unreachable, // invalid parameters
- .NOLCK => return error.SystemResources,
- .AGAIN => return error.WouldBlock, // TODO: integrate with async instead of just returning an error
- .OPNOTSUPP => return error.FileLocksNotSupported,
- else => |err| return unexpectedErrno(err),
- }
- }
-}
-
/// Spurious wakeups are possible and no precision of timing is guaranteed.
pub fn nanosleep(seconds: u64, nanoseconds: u64) void {
var req = timespec{
@@ -4215,74 +4069,6 @@ pub fn sigprocmask(flags: u32, noalias set: ?*const sigset_t, noalias oldset: ?*
}
}
-pub const FutimensError = error{
- /// times is NULL, or both nsec values are UTIME_NOW, and either:
- /// * the effective user ID of the caller does not match the owner
- /// of the file, the caller does not have write access to the
- /// file, and the caller is not privileged (Linux: does not have
- /// either the CAP_FOWNER or the CAP_DAC_OVERRIDE capability);
- /// or,
- /// * the file is marked immutable (see chattr(1)).
- AccessDenied,
-
- /// The caller attempted to change one or both timestamps to a value
- /// other than the current time, or to change one of the timestamps
- /// to the current time while leaving the other timestamp unchanged,
- /// (i.e., times is not NULL, neither nsec field is UTIME_NOW,
- /// and neither nsec field is UTIME_OMIT) and either:
- /// * the caller's effective user ID does not match the owner of
- /// file, and the caller is not privileged (Linux: does not have
- /// the CAP_FOWNER capability); or,
- /// * the file is marked append-only or immutable (see chattr(1)).
- PermissionDenied,
-
- ReadOnlyFileSystem,
-} || UnexpectedError;
-
-pub fn futimens(fd: fd_t, times: ?*const [2]timespec) FutimensError!void {
- if (native_os == .wasi and !builtin.link_libc) {
- // TODO WASI encodes `wasi.fstflags` to signify magic values
- // similar to UTIME_NOW and UTIME_OMIT. Currently, we ignore
- // this here, but we should really handle it somehow.
- const error_code = blk: {
- if (times) |times_arr| {
- const atim = times_arr[0].toTimestamp();
- const mtim = times_arr[1].toTimestamp();
- break :blk wasi.fd_filestat_set_times(fd, atim, mtim, .{
- .ATIM = true,
- .MTIM = true,
- });
- }
-
- break :blk wasi.fd_filestat_set_times(fd, 0, 0, .{
- .ATIM_NOW = true,
- .MTIM_NOW = true,
- });
- };
- switch (error_code) {
- .SUCCESS => return,
- .ACCES => return error.AccessDenied,
- .PERM => return error.PermissionDenied,
- .BADF => unreachable, // always a race condition
- .FAULT => unreachable,
- .INVAL => unreachable,
- .ROFS => return error.ReadOnlyFileSystem,
- else => |err| return unexpectedErrno(err),
- }
- }
-
- switch (errno(system.futimens(fd, times))) {
- .SUCCESS => return,
- .ACCES => return error.AccessDenied,
- .PERM => return error.PermissionDenied,
- .BADF => unreachable, // always a race condition
- .FAULT => unreachable,
- .INVAL => unreachable,
- .ROFS => return error.ReadOnlyFileSystem,
- else => |err| return unexpectedErrno(err),
- }
-}
-
pub const GetHostNameError = error{PermissionDenied} || UnexpectedError;
pub fn gethostname(name_buffer: *[HOST_NAME_MAX]u8) GetHostNameError![]u8 {
@@ -5104,12 +4890,7 @@ pub fn signalfd(fd: fd_t, mask: *const sigset_t, flags: u32) !fd_t {
}
}
-pub const SyncError = error{
- InputOutput,
- NoSpaceLeft,
- DiskQuota,
- AccessDenied,
-} || UnexpectedError;
+pub const SyncError = std.Io.File.SyncError;
/// Write all pending file contents and metadata modifications to all filesystems.
pub fn sync() void {
@@ -5129,38 +4910,8 @@ pub fn syncfs(fd: fd_t) SyncError!void {
}
}
-/// Write all pending file contents and metadata modifications for the specified file descriptor to the underlying filesystem.
-pub fn fsync(fd: fd_t) SyncError!void {
- if (native_os == .windows) {
- if (windows.kernel32.FlushFileBuffers(fd) != 0)
- return;
- switch (windows.GetLastError()) {
- .SUCCESS => return,
- .INVALID_HANDLE => unreachable,
- .ACCESS_DENIED => return error.AccessDenied, // a sync was performed but the system couldn't update the access time
- .UNEXP_NET_ERR => return error.InputOutput,
- else => return error.InputOutput,
- }
- }
- const rc = system.fsync(fd);
- switch (errno(rc)) {
- .SUCCESS => return,
- .BADF, .INVAL, .ROFS => unreachable,
- .IO => return error.InputOutput,
- .NOSPC => return error.NoSpaceLeft,
- .DQUOT => return error.DiskQuota,
- else => |err| return unexpectedErrno(err),
- }
-}
-
/// Write all pending file contents for the specified file descriptor to the underlying filesystem, but not necessarily the metadata.
pub fn fdatasync(fd: fd_t) SyncError!void {
- if (native_os == .windows) {
- return fsync(fd) catch |err| switch (err) {
- SyncError.AccessDenied => return, // fdatasync doesn't promise that the access time was synced
- else => return err,
- };
- }
const rc = system.fdatasync(fd);
switch (errno(rc)) {
.SUCCESS => return,
diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig
@@ -596,7 +596,7 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void {
error.NoSpaceLeft => unreachable,
error.FileTooBig => unreachable,
error.DeviceBusy => unreachable,
- error.FileLocksNotSupported => unreachable,
+ error.FileLocksUnsupported => unreachable,
error.BadPathName => unreachable, // Windows-only
error.WouldBlock => unreachable,
error.NetworkNotFound => unreachable, // Windows-only
diff --git a/lib/std/std.zig b/lib/std/std.zig
@@ -173,6 +173,9 @@ pub const Options = struct {
/// If this is `false`, then captured stack traces will always be empty, and attempts to write
/// stack traces will just print an error to the relevant `Io.Writer` and return.
allow_stack_tracing: bool = !@import("builtin").strip_debug_info,
+
+ /// Overrides `std.Io.File.Permissions`.
+ FilePermissions: ?type = null,
};
// This forces the start.zig file to be imported, and the comptime logic inside that
diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig
@@ -824,7 +824,7 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion {
error.SharingViolation => return error.Unexpected, // Windows-only
error.NetworkNotFound => return error.Unexpected, // Windows-only
error.AntivirusInterference => return error.Unexpected, // Windows-only
- error.FileLocksNotSupported => return error.Unexpected, // No lock requested.
+ error.FileLocksUnsupported => return error.Unexpected, // No lock requested.
error.NoSpaceLeft => return error.Unexpected, // read-only
error.PathAlreadyExists => return error.Unexpected, // read-only
error.DeviceBusy => return error.Unexpected, // read-only
@@ -1033,7 +1033,7 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ
error.SharingViolation => return error.Unexpected,
error.BadPathName => return error.Unexpected,
error.PipeBusy => return error.Unexpected,
- error.FileLocksNotSupported => return error.Unexpected,
+ error.FileLocksUnsupported => return error.Unexpected,
error.FileBusy => return error.Unexpected, // opened without write permissions
error.AntivirusInterference => return error.Unexpected, // Windows-only error