std.Io: add dirAccess

This commit is contained in:
Andrew Kelley 2025-10-16 20:53:28 -07:00
parent 3bf0ce65a5
commit f8ea00bd6d
8 changed files with 225 additions and 226 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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