diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 6ae4c3346c..a20868afe2 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -663,6 +663,7 @@ pub const VTable = struct { dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void, dirStat: *const fn (?*anyopaque, Dir) Dir.StatError!Dir.Stat, dirStatPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.StatPathOptions) Dir.StatPathError!File.Stat, + dirAccess: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.AccessOptions) Dir.AccessError!void, dirCreateFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File, dirOpenFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File, fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index f07b8f64a8..634c1f9fff 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -23,6 +23,37 @@ pub const PathNameError = error{ BadPathName, }; +pub const AccessError = error{ + AccessDenied, + PermissionDenied, + FileNotFound, + InputOutput, + SystemResources, + FileBusy, + SymLinkLoop, + ReadOnlyFileSystem, +} || PathNameError || Io.Cancelable || Io.UnexpectedError; + +pub const AccessOptions = packed struct { + follow_symlinks: bool = true, + read: bool = false, + write: bool = false, + execute: bool = false, +}; + +/// Test accessing `sub_path`. +/// +/// 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. +/// +/// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this +/// function. For example, instead of testing if a file exists and then opening +/// it, just open it and handle the error for file not found. +pub fn access(dir: Dir, io: Io, sub_path: []const u8, options: AccessOptions) AccessError!void { + return io.vtable.dirAccess(io.userdata, dir, sub_path, options); +} + pub const OpenError = error{ FileNotFound, NotDir, diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 715667b0d4..51f3a08df7 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -80,7 +80,67 @@ pub fn stat(file: File, io: Io) StatError!Stat { return io.vtable.fileStat(io.userdata, file); } -pub const OpenFlags = std.fs.File.OpenFlags; +pub const OpenMode = enum { + read_only, + write_only, + read_write, +}; + +pub const Lock = enum { + none, + shared, + exclusive, +}; + +pub const OpenFlags = struct { + mode: OpenMode = .read_only, + + /// Open the file with an advisory lock to coordinate with other processes + /// accessing it at the same time. An exclusive lock will prevent other + /// processes from acquiring a lock. A shared lock will prevent other + /// processes from acquiring a exclusive lock, but does not prevent + /// other process from getting their own shared locks. + /// + /// The lock is advisory, except on Linux in very specific circumstances[1]. + /// This means that a process that does not respect the locking API can still get access + /// to the file, despite the lock. + /// + /// On these operating systems, the lock is acquired atomically with + /// opening the file: + /// * Darwin + /// * DragonFlyBSD + /// * FreeBSD + /// * Haiku + /// * NetBSD + /// * OpenBSD + /// On these operating systems, the lock is acquired via a separate syscall + /// after opening the file: + /// * Linux + /// * Windows + /// + /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt + lock: Lock = .none, + + /// Sets whether or not to wait until the file is locked to return. If set to true, + /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file + /// is available to proceed. + lock_nonblocking: bool = false, + + /// Set this to allow the opened file to automatically become the + /// controlling TTY for the current process. + allow_ctty: bool = false, + + follow_symlinks: bool = true, + + pub fn isRead(self: OpenFlags) bool { + return self.mode != .write_only; + } + + pub fn isWrite(self: OpenFlags) bool { + return self.mode != .read_only; + } +}; + pub const CreateFlags = std.fs.File.CreateFlags; pub const OpenError = error{ diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 57578628fc..fb9319ef26 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -183,6 +183,11 @@ pub fn io(t: *Threaded) Io { .wasi => fileStatWasi, else => fileStatPosix, }, + .dirAccess = switch (builtin.os.tag) { + .windows => @panic("TODO"), + .wasi => dirAccessWasi, + else => dirAccessPosix, + }, .dirCreateFile = switch (builtin.os.tag) { .windows => @panic("TODO"), .wasi => @panic("TODO"), @@ -992,7 +997,6 @@ fn dirStatPathWasi( ) Io.Dir.StatPathError!Io.File.Stat { if (builtin.link_libc) return dirStatPathPosix(userdata, dir, sub_path, options); const t: *Threaded = @ptrCast(@alignCast(userdata)); - const dir_fd = dir.handle; const wasi = std.os.wasi; const flags: wasi.lookupflags_t = .{ .SYMLINK_FOLLOW = @intFromBool(options.follow_symlinks), @@ -1000,16 +1004,16 @@ fn dirStatPathWasi( var stat: wasi.filestat_t = undefined; while (true) { try t.checkCancel(); - switch (wasi.path_filestat_get(dir_fd, flags, sub_path.ptr, sub_path.len, &stat)) { + switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) { .SUCCESS => return statFromWasi(stat), .INTR => continue, .CANCELED => return error.Canceled, - .INVAL => |err| errnoBug(err), - .BADF => |err| errnoBug(err), // Always a race condition. + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // Always a race condition. .NOMEM => return error.SystemResources, .ACCES => return error.AccessDenied, - .FAULT => |err| errnoBug(err), + .FAULT => |err| return errnoBug(err), .NAMETOOLONG => return error.NameTooLong, .NOENT => return error.FileNotFound, .NOTDIR => return error.FileNotFound, @@ -1103,6 +1107,110 @@ const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.syste 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; +fn dirAccessPosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.AccessOptions, +) Io.Dir.AccessError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + const flags: u32 = @as(u32, if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0); + + const mode: u32 = + @as(u32, if (options.read) posix.R_OK else 0) | + @as(u32, if (options.write) posix.W_OK else 0) | + @as(u32, if (options.execute) posix.X_OK else 0); + + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.faccessat(dir.handle, sub_path_posix, mode, flags))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => return error.AccessDenied, + .PERM => return error.PermissionDenied, + .ROFS => return error.ReadOnlyFileSystem, + .LOOP => return error.SymLinkLoop, + .TXTBSY => return error.FileBusy, + .NOTDIR => return error.FileNotFound, + .NOENT => return error.FileNotFound, + .NAMETOOLONG => return error.NameTooLong, + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .IO => return error.InputOutput, + .NOMEM => return error.SystemResources, + .ILSEQ => return error.BadPathName, // TODO move to wasi + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn dirAccessWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.File.OpenFlags, +) Io.File.AccessError!void { + if (builtin.link_libc) return dirAccessPosix(userdata, dir, sub_path, options); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const wasi = std.os.wasi; + const flags: wasi.lookupflags_t = .{ + .SYMLINK_FOLLOW = @intFromBool(options.follow_symlinks), + }; + const stat = while (true) { + var stat: wasi.filestat_t = undefined; + try t.checkCancel(); + switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) { + .SUCCESS => break statFromWasi(stat), + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // Always a race condition. + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + .FAULT => |err| return errnoBug(err), + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.FileNotFound, + .NOTCAPABLE => return error.AccessDenied, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + }; + + if (!options.mode.read and !options.mode.write and !options.mode.execute) + return; + + var directory: wasi.fdstat_t = undefined; + if (wasi.fd_fdstat_get(dir.handle, &directory) != .SUCCESS) + return error.AccessDenied; + + var rights: wasi.rights_t = .{}; + if (options.mode.read) { + if (stat.filetype == .DIRECTORY) { + rights.FD_READDIR = true; + } else { + rights.FD_READ = true; + } + } + if (options.mode.write) + rights.FD_WRITE = true; + + // No validation for execution. + + // https://github.com/ziglang/zig/issues/18882 + const rights_int: u64 = @bitCast(rights); + const inheriting_int: u64 = @bitCast(directory.fs_rights_inheriting); + if ((rights_int & inheriting_int) != rights_int) + return error.AccessDenied; +} + fn dirCreateFilePosix( userdata: ?*anyopaque, dir: Io.Dir, diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 96aa962e67..395e18e6e5 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1,14 +1,15 @@ //! File System. +const builtin = @import("builtin"); +const native_os = builtin.os.tag; const std = @import("std.zig"); -const builtin = @import("builtin"); +const Io = std.Io; const root = @import("root"); const mem = std.mem; const base64 = std.base64; const crypto = std.crypto; const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const native_os = builtin.os.tag; const posix = std.posix; const windows = std.os.windows; @@ -274,7 +275,7 @@ pub fn openFileAbsoluteW(absolute_path_w: []const u16, flags: File.OpenFlags) Fi /// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On WASI, `absolute_path` should be encoded as valid UTF-8. /// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding. -pub fn accessAbsolute(absolute_path: []const u8, flags: File.OpenFlags) Dir.AccessError!void { +pub fn accessAbsolute(absolute_path: []const u8, flags: Io.Dir.AccessOptions) Dir.AccessError!void { assert(path.isAbsolute(absolute_path)); try cwd().access(absolute_path, flags); } diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 3565d2fc2c..c7699a83cf 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -2353,47 +2353,14 @@ pub fn writeFile(self: Dir, options: WriteFileOptions) WriteFileError!void { try file.writeAll(options.data); } -pub const AccessError = posix.AccessError; +/// Deprecated in favor of `Io.Dir.AccessError`. +pub const AccessError = Io.Dir.AccessError; -/// Test accessing `sub_path`. -/// 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. -/// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function. -/// For example, instead of testing if a file exists and then opening it, just -/// open it and handle the error for file not found. -pub fn access(self: Dir, sub_path: []const u8, flags: File.OpenFlags) AccessError!void { - if (native_os == .windows) { - const sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.accessW(sub_path_w.span().ptr, flags); - } - const path_c = try posix.toPosixPath(sub_path); - return self.accessZ(&path_c, flags); -} - -/// Same as `access` except the path parameter is null-terminated. -pub fn accessZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) AccessError!void { - if (native_os == .windows) { - const sub_path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path); - return self.accessW(sub_path_w.span().ptr, flags); - } - const os_mode = switch (flags.mode) { - .read_only => @as(u32, posix.F_OK), - .write_only => @as(u32, posix.W_OK), - .read_write => @as(u32, posix.R_OK | posix.W_OK), - }; - const result = posix.faccessatZ(self.fd, sub_path, os_mode, 0); - return result; -} - -/// Same as `access` except asserts the target OS is Windows and the path parameter is -/// * WTF-16 LE encoded -/// * null-terminated -/// * relative or has the NT namespace prefix -/// TODO currently this ignores `flags`. -pub fn accessW(self: Dir, sub_path_w: [*:0]const u16, flags: File.OpenFlags) AccessError!void { - _ = flags; - return posix.faccessatW(self.fd, sub_path_w); +/// Deprecated in favor of `Io.Dir.access`. +pub fn access(self: Dir, sub_path: []const u8, options: Io.Dir.AccessOptions) AccessError!void { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return Io.Dir.access(self.adaptToNewApi(), io, sub_path, options); } pub const CopyFileOptions = struct { diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index 1299a2c2c9..ca3fb47a5a 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -40,65 +40,12 @@ pub const default_mode = switch (builtin.os.tag) { /// Deprecated in favor of `Io.File.OpenError`. pub const OpenError = Io.File.OpenError || error{WouldBlock}; - -pub const OpenMode = enum { - read_only, - write_only, - read_write, -}; - -pub const Lock = enum { - none, - shared, - exclusive, -}; - -pub const OpenFlags = struct { - mode: OpenMode = .read_only, - - /// Open the file with an advisory lock to coordinate with other processes - /// accessing it at the same time. An exclusive lock will prevent other - /// processes from acquiring a lock. A shared lock will prevent other - /// processes from acquiring a exclusive lock, but does not prevent - /// other process from getting their own shared locks. - /// - /// The lock is advisory, except on Linux in very specific circumstances[1]. - /// This means that a process that does not respect the locking API can still get access - /// to the file, despite the lock. - /// - /// On these operating systems, the lock is acquired atomically with - /// opening the file: - /// * Darwin - /// * DragonFlyBSD - /// * FreeBSD - /// * Haiku - /// * NetBSD - /// * OpenBSD - /// On these operating systems, the lock is acquired via a separate syscall - /// after opening the file: - /// * Linux - /// * Windows - /// - /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt - lock: Lock = .none, - - /// Sets whether or not to wait until the file is locked to return. If set to true, - /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file - /// is available to proceed. - lock_nonblocking: bool = false, - - /// Set this to allow the opened file to automatically become the - /// controlling TTY for the current process. - allow_ctty: bool = false, - - pub fn isRead(self: OpenFlags) bool { - return self.mode != .write_only; - } - - pub fn isWrite(self: OpenFlags) bool { - return self.mode != .read_only; - } -}; +/// 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 = struct { /// Whether the file will be created with read access. diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 651f6eb117..999fe5e50a 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -4360,8 +4360,7 @@ pub const FStatAtError = FStatError || error{ NameTooLong, FileNotFound, SymLinkLoop, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, + BadPathName, }; /// Similar to `fstat`, but returns stat of a resource pointed to by `pathname` @@ -4900,7 +4899,7 @@ pub fn access(path: []const u8, mode: u32) AccessError!void { _ = try windows.GetFileAttributesW(path_w.span().ptr); return; } else if (native_os == .wasi and !builtin.link_libc) { - return faccessat(AT.FDCWD, path, mode, 0); + @compileError("wasi doesn't support absolute paths"); } const path_c = try toPosixPath(path); return accessZ(&path_c, mode); @@ -4934,121 +4933,6 @@ pub fn accessZ(path: [*:0]const u8, mode: u32) AccessError!void { } } -/// Check user's permissions for a file, based on an open directory handle. -/// -/// * On Windows, asserts `path` is valid [WTF-8](https://wtf-8.codeberg.page/). -/// * On WASI, invalid UTF-8 passed to `path` causes `error.InvalidUtf8`. -/// * On other platforms, `path` is an opaque sequence of bytes with no particular encoding. -/// -/// On Windows, `mode` is ignored. This is a POSIX API that is only partially supported by -/// Windows. See `fs` for the cross-platform file system API. -pub fn faccessat(dirfd: fd_t, path: []const u8, mode: u32, flags: u32) AccessError!void { - if (native_os == .windows) { - const path_w = try windows.sliceToPrefixedFileW(dirfd, path); - return faccessatW(dirfd, path_w.span().ptr); - } else if (native_os == .wasi and !builtin.link_libc) { - const resolved: RelativePathWasi = .{ .dir_fd = dirfd, .relative_path = path }; - - const st = try std.os.fstatat_wasi(dirfd, path, .{ - .SYMLINK_FOLLOW = (flags & AT.SYMLINK_NOFOLLOW) == 0, - }); - - if (mode != F_OK) { - var directory: wasi.fdstat_t = undefined; - if (wasi.fd_fdstat_get(resolved.dir_fd, &directory) != .SUCCESS) { - return error.AccessDenied; - } - - var rights: wasi.rights_t = .{}; - if (mode & R_OK != 0) { - if (st.filetype == .DIRECTORY) { - rights.FD_READDIR = true; - } else { - rights.FD_READ = true; - } - } - if (mode & W_OK != 0) { - rights.FD_WRITE = true; - } - // No validation for X_OK - - // https://github.com/ziglang/zig/issues/18882 - const rights_int: u64 = @bitCast(rights); - const inheriting_int: u64 = @bitCast(directory.fs_rights_inheriting); - if ((rights_int & inheriting_int) != rights_int) { - return error.AccessDenied; - } - } - return; - } - const path_c = try toPosixPath(path); - return faccessatZ(dirfd, &path_c, mode, flags); -} - -/// Same as `faccessat` except the path parameter is null-terminated. -pub fn faccessatZ(dirfd: fd_t, path: [*:0]const u8, mode: u32, flags: u32) AccessError!void { - if (native_os == .windows) { - const path_w = try windows.cStrToPrefixedFileW(dirfd, path); - return faccessatW(dirfd, path_w.span().ptr); - } else if (native_os == .wasi and !builtin.link_libc) { - return faccessat(dirfd, mem.sliceTo(path, 0), mode, flags); - } - switch (errno(system.faccessat(dirfd, path, mode, flags))) { - .SUCCESS => return, - .ACCES => return error.AccessDenied, - .PERM => return error.PermissionDenied, - .ROFS => return error.ReadOnlyFileSystem, - .LOOP => return error.SymLinkLoop, - .TXTBSY => return error.FileBusy, - .NOTDIR => return error.FileNotFound, - .NOENT => return error.FileNotFound, - .NAMETOOLONG => return error.NameTooLong, - .INVAL => unreachable, - .FAULT => unreachable, - .IO => return error.InputOutput, - .NOMEM => return error.SystemResources, - .ILSEQ => return error.BadPathName, - else => |err| return unexpectedErrno(err), - } -} - -/// Same as `faccessat` except asserts the target is Windows and the path parameter -/// is NtDll-prefixed, null-terminated, WTF-16 encoded. -pub fn faccessatW(dirfd: fd_t, sub_path_w: [*:0]const u16) AccessError!void { - if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { - return; - } - if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) { - return; - } - - const path_len_bytes = cast(u16, mem.sliceTo(sub_path_w, 0).len * 2) orelse return error.NameTooLong; - var nt_name = windows.UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - .Buffer = @constCast(sub_path_w), - }; - var attr = windows.OBJECT_ATTRIBUTES{ - .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), - .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w)) null else dirfd, - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - var basic_info: windows.FILE_BASIC_INFORMATION = undefined; - switch (windows.ntdll.NtQueryAttributesFile(&attr, &basic_info)) { - .SUCCESS => return, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .OBJECT_NAME_INVALID => unreachable, - .INVALID_PARAMETER => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - .OBJECT_PATH_SYNTAX_BAD => unreachable, - else => |rc| return windows.unexpectedStatus(rc), - } -} - pub const PipeError = error{ SystemFdQuotaExceeded, ProcessFdQuotaExceeded,