std: all File functions moved to std.Io

This commit is contained in:
Andrew Kelley 2025-12-04 16:03:33 -08:00
parent 48d70cfc38
commit 4f6658a67b
17 changed files with 2367 additions and 2325 deletions

View file

@ -437,7 +437,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

View file

@ -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,

View file

@ -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 fn setPermissions(dir: Dir, io: Io, permissions: Permissions) SetPermissionsError!void {
return io.vtable.dirSetPermissions(io.userdata, dir, permissions);
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);
}
/// 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);
}

View file

@ -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.
pub fn writer(file: File, io: Io, buffer: []u8) Writer {
return .init(file, 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 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,
};
/// 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 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;
}
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();
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();
}
};
pub const SetModeError = error{
AccessDenied,
PermissionDenied,
InputOutput,
SymLinkLoop,
FileNotFound,
pub const LockError = error{
SystemResources,
ReadOnlyFileSystem,
FileLocksUnsupported,
} || Io.Cancelable || Io.UnexpectedError;
pub const SetOwnerError = error{
AccessDenied,
PermissionDenied,
InputOutput,
SymLinkLoop,
FileNotFound,
SystemResources,
ReadOnlyFileSystem,
} || Io.Cancelable || Io.UnexpectedError;
/// Blocks when an incompatible lock is held by another process. A process may
/// hold only one type of lock (shared or exclusive) on a file. When a process
/// terminates in any way, the lock is released.
///
/// Assumes the file is unlocked.
pub fn lock(file: File, io: Io, l: Lock) LockError!void {
return io.vtable.fileLock(io.userdata, file, l);
}
/// Assumes the file is locked.
pub fn unlock(file: File, io: Io) void {
return io.vtable.fileUnlock(io.userdata, file);
}
/// 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 DowngradeLockError = 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);
}

View file

@ -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();
}

395
lib/std/Io/File/Reader.zig Normal file
View file

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

566
lib/std/Io/File/Writer.zig Normal file
View file

@ -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,
=> {},
}
}

View file

@ -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,

File diff suppressed because it is too large Load diff

View file

@ -343,7 +343,7 @@ const Module = struct {
error.AntivirusInterference,
error.ProcessFdQuotaExceeded,
error.SystemFdQuotaExceeded,
error.FileLocksNotSupported,
error.FileLocksUnsupported,
error.FileBusy,
=> return error.ReadFailed,
};

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -1994,80 +1994,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: ?*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;

View file

@ -298,17 +298,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,
@ -324,7 +314,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
@ -952,74 +941,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;
var 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),
.FileEndOfFileInformation,
);
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.
@ -1587,7 +1508,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,
@ -2295,47 +2216,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
/// protocol is denied.
@ -3910,34 +3790,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{
@ -4202,74 +4054,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 {
@ -5091,12 +4875,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 {
@ -5116,38 +4895,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,

View file

@ -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

View file

@ -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

View file

@ -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