diff --git a/CMakeLists.txt b/CMakeLists.txt index 87424ab522..5edea2036a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/lib/std/Io.zig b/lib/std/Io.zig index e3826ee50f..e0e0e4254c 100644 --- 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 index c743ba7cca..51fe6bbb5f 100644 --- 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 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); } diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index ee81f9fcaa..2c1b292b62 100644 --- 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, +/// 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 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; +/// 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 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; - } - - 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); +} diff --git a/lib/std/Io/File/Atomic.zig b/lib/std/Io/File/Atomic.zig new file mode 100644 index 0000000000..ccb81815ed --- /dev/null +++ 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 new file mode 100644 index 0000000000..4af77604fa --- /dev/null +++ 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 new file mode 100644 index 0000000000..13cee3e41b --- /dev/null +++ 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 index 5561cdebd2..81cdc24201 100644 --- 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 index a31f2e1a24..ba5ca6caeb 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -427,9 +427,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, @@ -440,6 +441,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, @@ -533,9 +547,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, @@ -546,6 +561,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, @@ -590,6 +618,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 @@ -1791,7 +1820,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), } @@ -1806,7 +1835,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), } @@ -2116,7 +2145,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, @@ -2155,7 +2184,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), } }, @@ -2420,7 +2449,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, @@ -2458,7 +2487,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), } }, @@ -3363,12 +3392,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, @@ -3440,7 +3469,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, @@ -3654,9 +3683,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, @@ -4139,50 +4168,23 @@ 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 dirSetModeUnsupported(userdata: ?*anyopaque, dir: Io.Dir, new_mode: Io.Dir.Mode) Io.Dir.SetModeError!void { +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; - _ = new_mode; + _ = permissions; return error.Unexpected; } -fn dirSetModePosix(userdata: ?*anyopaque, dir: Io.Dir, new_mode: Io.Dir.Mode) Io.Dir.SetModeError!void { +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); - - try current_thread.beginSyscall(); - while (true) { - switch (posix.errno(posix.system.fchmod(dir.handle, new_mode))) { - .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), - .FAULT => |err| return errnoBug(err), - .INVAL => |err| return errnoBug(err), - .ACCES => return error.AccessDenied, - .IO => return error.InputOutput, - .LOOP => return error.SymLinkLoop, - .NOENT => return error.FileNotFound, - .NOMEM => return error.SystemResources, - .NOTDIR => return error.FileNotFound, - .PERM => return error.PermissionDenied, - .ROFS => return error.ReadOnlyFileSystem, - else => |err| return posix.unexpectedErrno(err), - } - }, - } - } + return setPermissionsPosix(current_thread, dir.handle, permissions.toMode()); } const dirSetOwner = switch (native_os) { @@ -4203,10 +4205,13 @@ fn dirSetOwnerPosix(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, gro 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.fchown(dir.handle, uid, gid))) { + switch (posix.errno(posix.system.fchown(fd, uid, gid))) { .SUCCESS => return current_thread.endSyscall(), .CANCELED => return current_thread.endSyscallCanceled(), .INTR => { @@ -4234,31 +4239,916 @@ fn dirSetOwnerPosix(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, gro } } -const dirSetPermissions = switch (native_os) { - .windows => dirSetPermissionsWindows, - else => dirSetPermissionsPosix, +const fileSync = switch (native_os) { + .windows => fileSyncWindows, + else => fileSyncPosix, }; -fn dirSetPermissionsWindows( - userdata: ?*anyopaque, - dir: Io.Dir, - permissions: Io.Dir.Permissions, -) Io.Dir.SetPermissionsError!void { - _ = userdata; - _ = dir; - _ = permissions; - @panic("TODO"); +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 dirSetPermissionsPosix( +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.fchmod(fd, mode))) { + .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), + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .ACCES => return error.AccessDenied, + .IO => return error.InputOutput, + .LOOP => return error.SymLinkLoop, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOTDIR => return error.FileNotFound, + .PERM => return error.PermissionDenied, + .ROFS => return error.ReadOnlyFileSystem, + else => |err| return posix.unexpectedErrno(err), + } + }, + } + } +} + +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 dirSetTimestampsNow( + userdata: ?*anyopaque, + dir: Io.Dir, + 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 index 03fc7e2811..8feab7649a 100644 --- 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 index 7ee6421b68..67cbc5cbbd 100644 --- 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 deleted file mode 100644 index fda8b5b490..0000000000 --- a/lib/std/fs/File.zig +++ /dev/null @@ -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), .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; - 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; -} - -/// 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), - .FileBasicInformation, - ); - switch (rc) { - .SUCCESS => return, - .INVALID_HANDLE => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - }, - .wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod - else => { - try self.chmod(permissions.inner.mode); - }, - } -} - -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, - null, - ) catch |err| switch (err) { - error.RangeNotLocked => unreachable, // Function assumes unlocked. - error.Unexpected => unreachable, // Resource deallocation must succeed. - }; - } else { - return posix.flock(file.handle, posix.LOCK.UN) catch |err| switch (err) { - error.WouldBlock => unreachable, // unlocking can't block - error.SystemResources => unreachable, // We are deallocating resources. - error.FileLocksNotSupported => unreachable, // We already got the lock. - error.Unexpected => unreachable, // Resource deallocation must succeed. - }; - } -} - -/// Attempts to obtain a lock, returning `true` if the lock is -/// obtained, and `false` if there was an existing incompatible lock held. -/// A process may hold only one type of lock (shared or exclusive) on -/// a file. When a process terminates in any way, the lock is released. -/// -/// Assumes the file is unlocked. -/// -/// TODO: integrate with async I/O -pub fn tryLock(file: File, l: Lock) LockError!bool { - if (is_windows) { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - const exclusive = switch (l) { - .none => return, - .shared => false, - .exclusive => true, - }; - windows.LockFile( - file.handle, - null, - null, - null, - &io_status_block, - &range_off, - &range_len, - null, - windows.TRUE, // non-blocking=true - @intFromBool(exclusive), - ) catch |err| switch (err) { - error.WouldBlock => return false, - else => |e| return e, - }; - } else { - posix.flock(file.handle, switch (l) { - .none => posix.LOCK.UN, - .shared => posix.LOCK.SH | posix.LOCK.NB, - .exclusive => posix.LOCK.EX | posix.LOCK.NB, - }) catch |err| switch (err) { - error.WouldBlock => return false, - else => |e| return e, - }; - } - return true; -} - -/// Assumes the file is already locked in exclusive mode. -/// Atomically modifies the lock to be in shared mode, without releasing it. -/// -/// TODO: integrate with async I/O -pub fn downgradeLock(file: File) LockError!void { - if (is_windows) { - // On Windows it works like a semaphore + exclusivity flag. To implement this - // function, we first obtain another lock in shared mode. This changes the - // exclusivity flag, but increments the semaphore to 2. So we follow up with - // an NtUnlockFile which decrements the semaphore but does not modify the - // exclusivity flag. - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - windows.LockFile( - file.handle, - null, - null, - null, - &io_status_block, - &range_off, - &range_len, - null, - windows.TRUE, // non-blocking=true - windows.FALSE, // exclusive=false - ) catch |err| switch (err) { - error.WouldBlock => unreachable, // File was not locked in exclusive mode. - else => |e| return e, - }; - return windows.UnlockFile( - file.handle, - &io_status_block, - &range_off, - &range_len, - null, - ) catch |err| switch (err) { - error.RangeNotLocked => unreachable, // File was not locked. - error.Unexpected => unreachable, // Resource deallocation must succeed. - }; - } else { - return posix.flock(file.handle, posix.LOCK.SH | posix.LOCK.NB) catch |err| switch (err) { - error.WouldBlock => unreachable, // File was not locked in exclusive mode. - else => |e| return e, - }; - } -} - -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 index ba05ff6057..00ba334d9e 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -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; diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 9df8319d5a..bbb0da0ec8 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -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 /// pro‐tocol 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, diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index 7df2d89798..b169c983d0 100644 --- 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 index 5c500d3f55..84e402f52b 100644 --- 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 index a0c47c3072..ec5f1deef9 100644 --- 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