From b1733b7bcec44ddd73a210ede09c3c9eeb411d27 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 9 Oct 2025 00:47:04 -0700 Subject: [PATCH] std.Io: implement dirOpenFile --- lib/std/Io.zig | 4 +- lib/std/Io/Dir.zig | 4 +- lib/std/Io/Threaded.zig | 140 +++++++++++++++++++++++++++++--- lib/std/fs.zig | 50 +----------- lib/std/fs/Dir.zig | 171 ++-------------------------------------- 5 files changed, 140 insertions(+), 229 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 320ff3ba2d..9d51c54309 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -657,9 +657,9 @@ 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, + 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, - createFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File, - fileOpen: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File, fileClose: *const fn (?*anyopaque, File) void, pwrite: *const fn (?*anyopaque, File, buffer: []const u8, offset: std.posix.off_t) File.PWriteError!usize, /// Returns 0 on end of stream. diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 5460f6bd60..f07b8f64a8 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -39,11 +39,11 @@ pub const OpenError = error{ } || PathNameError || Io.Cancelable || Io.UnexpectedError; pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { - return io.vtable.fileOpen(io.userdata, dir, sub_path, flags); + return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags); } pub fn createFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { - return io.vtable.createFile(io.userdata, dir, sub_path, flags); + return io.vtable.dirCreateFile(io.userdata, dir, sub_path, flags); } pub const WriteFileOptions = struct { diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 02e68f85bb..fac093909c 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -178,12 +178,12 @@ pub fn io(pool: *Pool) Io { .wasi => fileStatWasi, else => fileStatPosix, }, - .createFile = switch (builtin.os.tag) { + .dirCreateFile = switch (builtin.os.tag) { .windows => @panic("TODO"), .wasi => @panic("TODO"), - else => createFilePosix, + else => dirCreateFilePosix, }, - .fileOpen = fileOpen, + .dirOpenFile = dirOpenFile, .fileClose = fileClose, .pwrite = pwrite, .fileReadStreaming = fileReadStreaming, @@ -966,8 +966,9 @@ fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File. } const have_flock = @TypeOf(posix.system.flock) != void; +const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; -fn createFilePosix( +fn dirCreateFilePosix( userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, @@ -1003,8 +1004,6 @@ fn createFilePosix( }, }; - const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; - const fd: posix.fd_t = while (true) { try pool.checkCancel(); const rc = openat_sym(dir.handle, sub_path_posix, os_flags, flags.mode); @@ -1088,24 +1087,139 @@ fn createFilePosix( return .{ .handle = fd }; } -fn fileOpen( +fn dirOpenFile( userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, flags: Io.File.OpenFlags, ) Io.File.OpenError!Io.File { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); - const fs_dir: std.fs.Dir = .{ .fd = dir.handle }; - const fs_file = try fs_dir.openFile(sub_path, flags); - return .{ .handle = fs_file.handle }; + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + var os_flags: posix.O = switch (native_os) { + .wasi => .{ + .read = flags.mode != .write_only, + .write = flags.mode != .read_only, + }, + else => .{ + .ACCMODE = switch (flags.mode) { + .read_only => .RDONLY, + .write_only => .WRONLY, + .read_write => .RDWR, + }, + }, + }; + if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true; + if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true; + if (@hasField(posix.O, "NOCTTY")) os_flags.NOCTTY = !flags.allow_ctty; + + // Use the O locking flags if the os supports them to acquire the lock + // atomically. + const has_flock_open_flags = @hasField(posix.O, "EXLOCK"); + if (has_flock_open_flags) { + // Note that the NONBLOCK flag is removed after the openat() call + // is successful. + switch (flags.lock) { + .none => {}, + .shared => { + os_flags.SHLOCK = true; + os_flags.NONBLOCK = flags.lock_nonblocking; + }, + .exclusive => { + os_flags.EXLOCK = true; + os_flags.NONBLOCK = flags.lock_nonblocking; + }, + } + } + const fd: posix.fd_t = while (true) { + try pool.checkCancel(); + const rc = openat_sym(dir.handle, sub_path_posix, os_flags, 0); + switch (posix.errno(rc)) { + .SUCCESS => break @intCast(rc), + .INTR => continue, + + .FAULT => |err| return errnoBug(err), + .INVAL => return error.BadPathName, + .BADF => |err| return errnoBug(err), + .ACCES => return error.AccessDenied, + .FBIG => return error.FileTooBig, + .OVERFLOW => return error.FileTooBig, + .ISDIR => return error.IsDir, + .LOOP => return error.SymLinkLoop, + .MFILE => return error.ProcessFdQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.NoDevice, + .NOENT => return error.FileNotFound, + .SRCH => return error.ProcessNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.NoSpaceLeft, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .EXIST => return error.PathAlreadyExists, + .BUSY => return error.DeviceBusy, + .OPNOTSUPP => return error.FileLocksNotSupported, + //.AGAIN => return error.WouldBlock, + .TXTBSY => return error.FileBusy, + .NXIO => return error.NoDevice, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + }; + errdefer posix.close(fd); + + if (have_flock and !has_flock_open_flags and flags.lock != .none) { + const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; + const lock_flags = switch (flags.lock) { + .none => unreachable, + .shared => posix.LOCK.SH | lock_nonblocking, + .exclusive => posix.LOCK.EX | lock_nonblocking, + }; + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.flock(fd, lock_flags))) { + .SUCCESS => break, + .INTR => continue, + + .BADF => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOLCK => return error.SystemResources, + //.AGAIN => return error.WouldBlock, + .OPNOTSUPP => return error.FileLocksNotSupported, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + if (has_flock_open_flags and flags.lock_nonblocking) { + var fl_flags: usize = while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.GETFL, 0))) { + .SUCCESS => break, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + }; + fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.fcntl(fd, posix.F.SETFL, fl_flags))) { + .SUCCESS => break, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + return .{ .handle = fd }; } fn fileClose(userdata: ?*anyopaque, file: Io.File) void { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; - const fs_file: std.fs.File = .{ .handle = file.handle }; - return fs_file.close(); + posix.close(file.handle); } fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.ReadStreamingError!usize { diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 7d5ac75276..e03d426276 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -260,12 +260,6 @@ pub fn openFileAbsolute(absolute_path: []const u8, flags: File.OpenFlags) File.O return cwd().openFile(absolute_path, flags); } -/// Same as `openFileAbsolute` but the path parameter is null-terminated. -pub fn openFileAbsoluteZ(absolute_path_c: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File { - assert(path.isAbsoluteZ(absolute_path_c)); - return cwd().openFileZ(absolute_path_c, flags); -} - /// Same as `openFileAbsolute` but the path parameter is WTF-16-encoded. pub fn openFileAbsoluteW(absolute_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { assert(path.isAbsoluteWindowsWTF16(absolute_path_w)); @@ -284,11 +278,6 @@ pub fn accessAbsolute(absolute_path: []const u8, flags: File.OpenFlags) Dir.Acce assert(path.isAbsolute(absolute_path)); try cwd().access(absolute_path, flags); } -/// Same as `accessAbsolute` but the path parameter is null-terminated. -pub fn accessAbsoluteZ(absolute_path: [*:0]const u8, flags: File.OpenFlags) Dir.AccessError!void { - assert(path.isAbsoluteZ(absolute_path)); - try cwd().accessZ(absolute_path, flags); -} /// Same as `accessAbsolute` but the path parameter is WTF-16 encoded. pub fn accessAbsoluteW(absolute_path: [*:0]const u16, flags: File.OpenFlags) Dir.AccessError!void { assert(path.isAbsoluteWindowsW(absolute_path)); @@ -309,12 +298,6 @@ pub fn createFileAbsolute(absolute_path: []const u8, flags: File.CreateFlags) Fi return cwd().createFile(absolute_path, flags); } -/// Same as `createFileAbsolute` but the path parameter is null-terminated. -pub fn createFileAbsoluteZ(absolute_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File { - assert(path.isAbsoluteZ(absolute_path_c)); - return cwd().createFileZ(absolute_path_c, flags); -} - /// Same as `createFileAbsolute` but the path parameter is WTF-16 encoded. pub fn createFileAbsoluteW(absolute_path_w: [*:0]const u16, flags: File.CreateFlags) File.OpenError!File { assert(path.isAbsoluteWindowsW(absolute_path_w)); @@ -333,12 +316,6 @@ pub fn deleteFileAbsolute(absolute_path: []const u8) Dir.DeleteFileError!void { return cwd().deleteFile(absolute_path); } -/// Same as `deleteFileAbsolute` except the parameter is null-terminated. -pub fn deleteFileAbsoluteZ(absolute_path_c: [*:0]const u8) Dir.DeleteFileError!void { - assert(path.isAbsoluteZ(absolute_path_c)); - return cwd().deleteFileZ(absolute_path_c); -} - /// Same as `deleteFileAbsolute` except the parameter is WTF-16 encoded. pub fn deleteFileAbsoluteW(absolute_path_w: [*:0]const u16) Dir.DeleteFileError!void { assert(path.isAbsoluteWindowsW(absolute_path_w)); @@ -383,12 +360,6 @@ pub fn readlinkAbsoluteW(pathname_w: [*:0]const u16, buffer: *[max_path_bytes]u8 return posix.readlinkW(mem.span(pathname_w), buffer); } -/// Same as `readLink`, except the path parameter is null-terminated. -pub fn readLinkAbsoluteZ(pathname_c: [*:0]const u8, buffer: *[max_path_bytes]u8) ![]u8 { - assert(path.isAbsoluteZ(pathname_c)); - return posix.readlinkZ(pathname_c, buffer); -} - /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent /// one; the latter case is known as a dangling link. @@ -426,28 +397,11 @@ pub fn symLinkAbsoluteW( return windows.CreateSymbolicLink(null, mem.span(sym_link_path_w), mem.span(target_path_w), flags.is_directory); } -/// Same as `symLinkAbsolute` except the parameters are null-terminated pointers. -/// See also `symLinkAbsolute`. -pub fn symLinkAbsoluteZ( - target_path_c: [*:0]const u8, - sym_link_path_c: [*:0]const u8, - flags: Dir.SymLinkFlags, -) !void { - assert(path.isAbsoluteZ(target_path_c)); - assert(path.isAbsoluteZ(sym_link_path_c)); - if (native_os == .windows) { - const target_path_w = try windows.cStrToPrefixedFileW(null, target_path_c); - const sym_link_path_w = try windows.cStrToPrefixedFileW(null, sym_link_path_c); - return windows.CreateSymbolicLink(null, sym_link_path_w.span(), target_path_w.span(), flags.is_directory); - } - return posix.symlinkZ(target_path_c, sym_link_path_c); -} - pub const OpenSelfExeError = posix.OpenError || SelfExePathError || posix.FlockError; pub fn openSelfExe(flags: File.OpenFlags) OpenSelfExeError!File { if (native_os == .linux or native_os == .serenity) { - return openFileAbsoluteZ("/proc/self/exe", flags); + return openFileAbsolute("/proc/self/exe", flags); } if (native_os == .windows) { // If ImagePathName is a symlink, then it will contain the path of the symlink, @@ -463,7 +417,7 @@ pub fn openSelfExe(flags: File.OpenFlags) OpenSelfExeError!File { var buf: [max_path_bytes]u8 = undefined; const self_exe_path = try selfExePath(&buf); buf[self_exe_path.len] = 0; - return openFileAbsoluteZ(buf[0..self_exe_path.len :0].ptr, flags); + return openFileAbsolute(buf[0..self_exe_path.len :0], flags); } // This is `posix.ReadLinkError || posix.RealPathError` with impossible errors excluded diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index f6455215ac..1b5f3808ff 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -885,94 +885,9 @@ pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.Ope const fd = try posix.openatWasi(self.fd, sub_path, .{}, .{}, .{}, base, .{}); return .{ .handle = fd }; } - const path_c = try posix.toPosixPath(sub_path); - return self.openFileZ(&path_c, flags); -} - -/// Same as `openFile` but the path parameter is null-terminated. -pub fn openFileZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File { - switch (native_os) { - .windows => { - const path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path); - return self.openFileW(path_w.span(), flags); - }, - // Use the libc API when libc is linked because it implements things - // such as opening absolute file paths. - .wasi => if (!builtin.link_libc) { - return openFile(self, mem.sliceTo(sub_path, 0), flags); - }, - else => {}, - } - - var os_flags: posix.O = switch (native_os) { - .wasi => .{ - .read = flags.mode != .write_only, - .write = flags.mode != .read_only, - }, - else => .{ - .ACCMODE = switch (flags.mode) { - .read_only => .RDONLY, - .write_only => .WRONLY, - .read_write => .RDWR, - }, - }, - }; - if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true; - if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true; - if (@hasField(posix.O, "NOCTTY")) os_flags.NOCTTY = !flags.allow_ctty; - - // Use the O locking flags if the os supports them to acquire the lock - // atomically. - const has_flock_open_flags = @hasField(posix.O, "EXLOCK"); - if (has_flock_open_flags) { - // Note that the NONBLOCK flag is removed after the openat() call - // is successful. - switch (flags.lock) { - .none => {}, - .shared => { - os_flags.SHLOCK = true; - os_flags.NONBLOCK = flags.lock_nonblocking; - }, - .exclusive => { - os_flags.EXLOCK = true; - os_flags.NONBLOCK = flags.lock_nonblocking; - }, - } - } - const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0); - errdefer posix.close(fd); - - if (have_flock and !has_flock_open_flags and flags.lock != .none) { - // TODO: integrate async I/O - const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; - try posix.flock(fd, switch (flags.lock) { - .none => unreachable, - .shared => posix.LOCK.SH | lock_nonblocking, - .exclusive => posix.LOCK.EX | lock_nonblocking, - }); - } - - if (has_flock_open_flags and flags.lock_nonblocking) { - var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); - _ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - } - - return .{ .handle = fd }; + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return .adaptFromNewApi(try Io.Dir.openFile(self.adaptToNewApi(), io, sub_path, flags)); } /// Same as `openFile` but Windows-only and the path parameter is @@ -1048,82 +963,10 @@ pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File }, .{}), }; } - const path_c = try posix.toPosixPath(sub_path); - return self.createFileZ(&path_c, flags); -} - -/// Same as `createFile` but the path parameter is null-terminated. -pub fn createFileZ(self: Dir, sub_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File { - switch (native_os) { - .windows => { - const path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path_c); - return self.createFileW(path_w.span(), flags); - }, - .wasi => { - return createFile(self, mem.sliceTo(sub_path_c, 0), flags); - }, - else => {}, - } - - var os_flags: posix.O = .{ - .ACCMODE = if (flags.read) .RDWR else .WRONLY, - .CREAT = true, - .TRUNC = flags.truncate, - .EXCL = flags.exclusive, - }; - if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true; - if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true; - - // Use the O locking flags if the os supports them to acquire the lock - // atomically. Note that the NONBLOCK flag is removed after the openat() - // call is successful. - const has_flock_open_flags = @hasField(posix.O, "EXLOCK"); - if (has_flock_open_flags) switch (flags.lock) { - .none => {}, - .shared => { - os_flags.SHLOCK = true; - os_flags.NONBLOCK = flags.lock_nonblocking; - }, - .exclusive => { - os_flags.EXLOCK = true; - os_flags.NONBLOCK = flags.lock_nonblocking; - }, - }; - - const fd = try posix.openatZ(self.fd, sub_path_c, os_flags, flags.mode); - errdefer posix.close(fd); - - if (have_flock and !has_flock_open_flags and flags.lock != .none) { - // TODO: integrate async I/O - const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; - try posix.flock(fd, switch (flags.lock) { - .none => unreachable, - .shared => posix.LOCK.SH | lock_nonblocking, - .exclusive => posix.LOCK.EX | lock_nonblocking, - }); - } - - if (has_flock_open_flags and flags.lock_nonblocking) { - var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); - _ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - } - - return .{ .handle = fd }; + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + const new_file = try Io.Dir.createFile(self.adaptToNewApi(), io, sub_path, flags); + return .adaptFromNewApi(new_file); } /// Same as `createFile` but Windows-only and the path parameter is