aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/lib/tinyio
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/lib/tinyio
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/lib/tinyio')
-rw-r--r--vendor/libghostty-vt/src/lib/tinyio/posix.zig608
-rw-r--r--vendor/libghostty-vt/src/lib/tinyio/test.zig836
-rw-r--r--vendor/libghostty-vt/src/lib/tinyio/windows.zig745
3 files changed, 2189 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/lib/tinyio/posix.zig b/vendor/libghostty-vt/src/lib/tinyio/posix.zig
new file mode 100644
index 00000000..10b96210
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/tinyio/posix.zig
@@ -0,0 +1,608 @@
+//! POSIX impl of TinyIo: plain blocking syscalls cribbed from
+//! std.posix and the POSIX impls of `std.Io.Threaded`.
+
+const std = @import("std");
+const builtin = @import("builtin");
+const posix = std.posix;
+const Io = std.Io;
+const File = Io.File;
+const Dir = Io.Dir;
+const Threaded = std.Io.Threaded;
+
+// 64-bit offset syscall selection, same as `std.Io.Threaded`.
+const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat;
+const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat;
+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 have_preadv = switch (builtin.os.tag) {
+ .haiku => false,
+ else => true,
+};
+
+pub fn randomSecure(buffer: []u8) Io.RandomSecureError!void {
+ // The same sources as `std.Io.Threaded.randomSecure` minus
+ // cancelation and the /dev/urandom fallback: arc4random_buf where
+ // libc provides it (all the BSDs and Darwin, glibc 2.36+), otherwise
+ // the getrandom syscall on Linux. Anything else has no entropy.
+ if (builtin.link_libc and @TypeOf(posix.system.arc4random_buf) != void) {
+ posix.system.arc4random_buf(buffer.ptr, buffer.len);
+ return;
+ }
+
+ if (builtin.os.tag == .linux) {
+ const linux = std.os.linux;
+ var i: usize = 0;
+ while (i < buffer.len) {
+ const rc = linux.getrandom(buffer[i..].ptr, buffer.len - i, 0);
+ switch (linux.errno(rc)) {
+ .SUCCESS => i += rc,
+ .INTR => continue,
+ else => return error.EntropyUnavailable,
+ }
+ }
+ return;
+ }
+
+ return error.EntropyUnavailable;
+}
+
+pub fn closeFd(fd: posix.fd_t) void {
+ // Never retry close on EINTR: POSIX leaves the fd state unspecified
+ // and Linux always closes it, so retrying risks closing an unrelated
+ // fd opened by another thread.
+ _ = posix.system.close(fd);
+}
+
+pub fn dirOpenFile(
+ _: ?*anyopaque,
+ dir: Dir,
+ sub_path: []const u8,
+ options: Dir.OpenFileOptions,
+) File.OpenError!File {
+ var path_buffer: [posix.PATH_MAX]u8 = undefined;
+ const sub_path_posix = try Threaded.pathToPosix(sub_path, &path_buffer);
+
+ // Nothing in the terminal locks files. Implementing this requires
+ // flock fallbacks (see std.Io.Threaded); report it as unsupported.
+ if (options.lock != .none) return error.FileLocksUnsupported;
+
+ var flags: posix.O = .{
+ .ACCMODE = switch (options.mode) {
+ .read_only => .RDONLY,
+ .write_only => .WRONLY,
+ .read_write => .RDWR,
+ },
+ .NOFOLLOW = !options.follow_symlinks,
+ };
+ if (@hasField(posix.O, "CLOEXEC")) flags.CLOEXEC = true;
+ if (@hasField(posix.O, "LARGEFILE")) flags.LARGEFILE = true;
+ if (@hasField(posix.O, "NOCTTY")) flags.NOCTTY = !options.allow_ctty;
+ if (@hasField(posix.O, "PATH")) flags.PATH = options.path_only;
+ if (@hasField(posix.O, "RESOLVE_BENEATH")) flags.RESOLVE_BENEATH = options.resolve_beneath;
+
+ const mode: posix.mode_t = 0;
+ const fd: posix.fd_t = while (true) {
+ const rc = openat_sym(dir.handle, sub_path_posix, flags, mode);
+ switch (posix.errno(rc)) {
+ .SUCCESS => break @intCast(rc),
+ .INTR => continue,
+ .INVAL => return error.BadPathName,
+ .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.FileNotFound,
+ .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.FileLocksUnsupported,
+ .AGAIN => return error.WouldBlock,
+ .TXTBSY => return error.FileBusy,
+ .NXIO => return error.NoDevice,
+ .ROFS => return error.ReadOnlyFileSystem,
+ .ILSEQ => return error.BadPathName,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ };
+ errdefer closeFd(fd);
+
+ const file: File = .{ .handle = fd, .flags = .{ .nonblocking = false } };
+
+ if (!options.allow_directory) {
+ const is_dir = is_dir: {
+ const stat = fileStat(null, file) catch |err| switch (err) {
+ // Directory-ness is unknown or unknowable.
+ error.Streaming => break :is_dir false,
+ else => |e| return e,
+ };
+ break :is_dir stat.kind == .directory;
+ };
+ if (is_dir) return error.IsDir;
+ }
+
+ return file;
+}
+
+pub fn dirClose(_: ?*anyopaque, dirs: []const Dir) void {
+ for (dirs) |dir| closeFd(dir.handle);
+}
+
+pub fn fileClose(_: ?*anyopaque, files: []const File) void {
+ for (files) |file| closeFd(file.handle);
+}
+
+pub fn fileStat(_: ?*anyopaque, file: File) File.StatError!File.Stat {
+ if (builtin.os.tag == .linux) {
+ const linux = std.os.linux;
+ while (true) {
+ var statx = std.mem.zeroes(linux.Statx);
+ switch (linux.errno(linux.statx(
+ file.handle,
+ "",
+ linux.AT.EMPTY_PATH,
+ Threaded.linux_statx_request,
+ &statx,
+ ))) {
+ .SUCCESS => return Threaded.statFromLinux(&statx),
+ .INTR => continue,
+ .NOMEM => return error.SystemResources,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ }
+ }
+
+ while (true) {
+ var stat = std.mem.zeroes(posix.Stat);
+ switch (posix.errno(fstat_sym(file.handle, &stat))) {
+ .SUCCESS => return Threaded.statFromPosix(&stat),
+ .INTR => continue,
+ .NOMEM => return error.SystemResources,
+ .ACCES => return error.AccessDenied,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ }
+}
+
+pub fn fileLength(userdata: ?*anyopaque, file: File) File.LengthError!u64 {
+ const stat = try fileStat(userdata, file);
+ return stat.size;
+}
+
+/// Gathers non-empty buffers into iovecs. Returns an empty slice if there
+/// is nothing to read into.
+fn buffersToIovecs(
+ data: []const []u8,
+ iovecs_buffer: *[Threaded.max_iovecs_len]posix.iovec,
+) []posix.iovec {
+ var i: usize = 0;
+ for (data) |buf| {
+ if (iovecs_buffer.len - i == 0) break;
+ if (buf.len != 0) {
+ iovecs_buffer[i] = .{ .base = buf.ptr, .len = buf.len };
+ i += 1;
+ }
+ }
+ return iovecs_buffer[0..i];
+}
+
+pub fn fileReadPositional(
+ _: ?*anyopaque,
+ file: File,
+ data: []const []u8,
+ offset: u64,
+) File.ReadPositionalError!usize {
+ var iovecs_buffer: [Threaded.max_iovecs_len]posix.iovec = undefined;
+ const dest = buffersToIovecs(data, &iovecs_buffer);
+ if (dest.len == 0) return 0;
+
+ while (true) {
+ const rc = if (comptime have_preadv)
+ preadv_sym(file.handle, dest.ptr, @intCast(dest.len), @bitCast(offset))
+ else
+ posix.system.pread(file.handle, dest[0].base, dest[0].len, @bitCast(offset));
+ switch (posix.errno(rc)) {
+ .SUCCESS => return @bitCast(rc),
+ .INTR, .TIMEDOUT => continue,
+ .NXIO => return error.Unseekable,
+ .SPIPE => return error.Unseekable,
+ .OVERFLOW => return error.Unseekable,
+ .NOBUFS => return error.SystemResources,
+ .NOMEM => return error.SystemResources,
+ .AGAIN => return error.WouldBlock,
+ .IO => return error.InputOutput,
+ .ISDIR => return error.IsDir,
+ .BADF => return error.NotOpenForReading,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ }
+}
+
+pub fn fileReadStreaming(
+ file: File,
+ data: []const []u8,
+) Io.Operation.FileReadStreaming.Error!usize {
+ var iovecs_buffer: [Threaded.max_iovecs_len]posix.iovec = undefined;
+ const dest = buffersToIovecs(data, &iovecs_buffer);
+ if (dest.len == 0) return 0;
+
+ while (true) {
+ const rc = posix.system.readv(file.handle, dest.ptr, @intCast(dest.len));
+ switch (posix.errno(rc)) {
+ .SUCCESS => {
+ if (rc == 0) return error.EndOfStream;
+ return @intCast(rc);
+ },
+ .INTR, .TIMEDOUT => continue,
+ .AGAIN => return error.WouldBlock,
+ .IO => return error.InputOutput,
+ .ISDIR => return error.IsDir,
+ .NOBUFS => return error.SystemResources,
+ .NOMEM => return error.SystemResources,
+ .NOTCONN => return error.SocketUnconnected,
+ .CONNRESET => return error.ConnectionResetByPeer,
+ .BADF => return error.NotOpenForReading,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ }
+}
+
+pub fn fileSeekBy(_: ?*anyopaque, file: File, offset: i64) File.SeekError!void {
+ while (true) {
+ const rc = lseek_sym(file.handle, offset, posix.SEEK.CUR);
+ switch (posix.errno(rc)) {
+ .SUCCESS => return,
+ .INTR => continue,
+ .INVAL => return error.Unseekable,
+ .OVERFLOW => return error.Unseekable,
+ .SPIPE => return error.Unseekable,
+ .NXIO => return error.Unseekable,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ }
+}
+
+pub fn fileSeekTo(_: ?*anyopaque, file: File, offset: u64) File.SeekError!void {
+ while (true) {
+ const rc = lseek_sym(file.handle, @bitCast(offset), posix.SEEK.SET);
+ switch (posix.errno(rc)) {
+ .SUCCESS => return,
+ .INTR => continue,
+ .INVAL => return error.Unseekable,
+ .OVERFLOW => return error.Unseekable,
+ .SPIPE => return error.Unseekable,
+ .NXIO => return error.Unseekable,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ }
+}
+
+pub fn fileRealPath(
+ _: ?*anyopaque,
+ file: File,
+ out_buffer: []u8,
+) File.RealPathError!usize {
+ return realPathFd(file.handle, out_buffer);
+}
+
+fn realPathFd(fd: posix.fd_t, out_buffer: []u8) File.RealPathError!usize {
+ switch (builtin.os.tag) {
+ .dragonfly, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
+ var sufficient_buffer: [posix.PATH_MAX]u8 = undefined;
+ @memset(&sufficient_buffer, 0);
+ while (true) {
+ switch (posix.errno(posix.system.fcntl(fd, posix.F.GETPATH, &sufficient_buffer))) {
+ .SUCCESS => break,
+ .INTR => continue,
+ .ACCES => return error.AccessDenied,
+ .BADF => return error.FileNotFound,
+ .NOENT => return error.FileNotFound,
+ .NOMEM => return error.SystemResources,
+ .NOSPC => return error.NameTooLong,
+ .RANGE => return error.NameTooLong,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ }
+ const n = std.mem.indexOfScalar(u8, &sufficient_buffer, 0) orelse sufficient_buffer.len;
+ if (n > out_buffer.len) return error.NameTooLong;
+ @memcpy(out_buffer[0..n], sufficient_buffer[0..n]);
+ return n;
+ },
+
+ .linux, .serenity, .illumos => {
+ var procfs_buf: ["/proc/self/path/-2147483648\x00".len]u8 = undefined;
+ const template = if (builtin.os.tag == .illumos) "/proc/self/path/{d}" else "/proc/self/fd/{d}";
+ const proc_path = std.fmt.bufPrintSentinel(&procfs_buf, template, .{fd}, 0) catch unreachable;
+ while (true) {
+ const rc = posix.system.readlink(proc_path, out_buffer.ptr, out_buffer.len);
+ switch (posix.errno(rc)) {
+ .SUCCESS => return @bitCast(rc),
+ .INTR => continue,
+ .ACCES => return error.AccessDenied,
+ .IO => return error.FileSystem,
+ .LOOP => return error.SymLinkLoop,
+ .NAMETOOLONG => return error.NameTooLong,
+ .NOENT => return error.FileNotFound,
+ .NOMEM => return error.SystemResources,
+ .NOTDIR => return error.NotDir,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ }
+ },
+
+ .freebsd => {
+ var k_file: std.c.kinfo_file = undefined;
+ k_file.structsize = std.c.KINFO_FILE_SIZE;
+ while (true) {
+ switch (posix.errno(std.c.fcntl(fd, std.c.F.KINFO, @intFromPtr(&k_file)))) {
+ .SUCCESS => break,
+ .INTR => continue,
+ .BADF => return error.FileNotFound,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ }
+ const len = std.mem.findScalar(u8, &k_file.path, 0) orelse k_file.path.len;
+ if (len == 0) return error.NameTooLong;
+ @memcpy(out_buffer[0..len], k_file.path[0..len]);
+ return len;
+ },
+
+ else => return error.OperationUnsupported,
+ }
+}
+
+pub fn dirRealPathFile(
+ _: ?*anyopaque,
+ dir: Dir,
+ sub_path: []const u8,
+ out_buffer: []u8,
+) Dir.RealPathFileError!usize {
+ var path_buffer: [posix.PATH_MAX]u8 = undefined;
+ const sub_path_posix = try Threaded.pathToPosix(sub_path, &path_buffer);
+
+ if (builtin.link_libc and dir.handle == posix.AT.FDCWD) {
+ if (out_buffer.len < posix.PATH_MAX) return error.NameTooLong;
+ while (true) {
+ if (std.c.realpath(sub_path_posix, out_buffer.ptr)) |redundant_pointer| {
+ std.debug.assert(redundant_pointer == out_buffer.ptr);
+ return std.mem.indexOfScalar(u8, out_buffer, 0) orelse out_buffer.len;
+ }
+ switch (@as(posix.E, @enumFromInt(std.c._errno().*))) {
+ .INTR => continue,
+ .ACCES => return error.AccessDenied,
+ .NOENT => return error.FileNotFound,
+ .OPNOTSUPP => return error.OperationUnsupported,
+ .NOTDIR => return error.NotDir,
+ .NAMETOOLONG => return error.NameTooLong,
+ .LOOP => return error.SymLinkLoop,
+ .IO => return error.InputOutput,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ }
+ }
+
+ // Fallback: open the path and resolve the fd. Used for non-cwd
+ // directory handles (which the terminal itself never passes) and
+ // non-libc builds.
+ var flags: posix.O = .{};
+ if (@hasField(posix.O, "NONBLOCK")) flags.NONBLOCK = true;
+ if (@hasField(posix.O, "CLOEXEC")) flags.CLOEXEC = true;
+ if (@hasField(posix.O, "PATH")) flags.PATH = true;
+
+ const mode: posix.mode_t = 0;
+ const fd: posix.fd_t = while (true) {
+ const rc = openat_sym(dir.handle, sub_path_posix, flags, mode);
+ switch (posix.errno(rc)) {
+ .SUCCESS => break @intCast(rc),
+ .INTR => continue,
+ .INVAL => return error.BadPathName,
+ .ACCES => return error.AccessDenied,
+ .LOOP => return error.SymLinkLoop,
+ .NAMETOOLONG => return error.NameTooLong,
+ .NOENT => return error.FileNotFound,
+ .NOMEM => return error.SystemResources,
+ .NOTDIR => return error.NotDir,
+ .ILSEQ => return error.BadPathName,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ };
+ defer closeFd(fd);
+
+ return realPathFd(fd, out_buffer);
+}
+
+pub fn dirDeleteFile(
+ _: ?*anyopaque,
+ dir: Dir,
+ sub_path: []const u8,
+) Dir.DeleteFileError!void {
+ var path_buffer: [posix.PATH_MAX]u8 = undefined;
+ const sub_path_posix = try Threaded.pathToPosix(sub_path, &path_buffer);
+
+ while (true) {
+ switch (posix.errno(posix.system.unlinkat(dir.handle, sub_path_posix, 0))) {
+ .SUCCESS => return,
+ .INTR => continue,
+ // Some systems return EPERM when trying to delete a directory;
+ // stat to disambiguate from a real permission error.
+ .PERM => switch (builtin.os.tag) {
+ .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .dragonfly, .openbsd, .illumos => {
+ var st = std.mem.zeroes(posix.Stat);
+ while (true) {
+ switch (posix.errno(fstatat_sym(
+ dir.handle,
+ sub_path_posix,
+ &st,
+ posix.AT.SYMLINK_NOFOLLOW,
+ ))) {
+ .SUCCESS => break,
+ .INTR => continue,
+ else => return error.PermissionDenied,
+ }
+ }
+ const is_dir = st.mode & posix.S.IFMT == posix.S.IFDIR;
+ return if (is_dir) error.IsDir else error.PermissionDenied;
+ },
+ else => return error.PermissionDenied,
+ },
+ .ACCES => return error.AccessDenied,
+ .BUSY => return error.FileBusy,
+ .IO => return error.FileSystem,
+ .ISDIR => return error.IsDir,
+ .LOOP => return error.SymLinkLoop,
+ .NAMETOOLONG => return error.NameTooLong,
+ .NOENT => return error.FileNotFound,
+ .NOTDIR => return error.NotDir,
+ .NOMEM => return error.SystemResources,
+ .ROFS => return error.ReadOnlyFileSystem,
+ .ILSEQ => return error.BadPathName,
+ else => |err| return posix.unexpectedErrno(err),
+ }
+ }
+}
+
+pub fn futexWaitInner(ptr: *const u32, expected: u32, timeout_ns: ?u64) void {
+ @branchHint(.cold);
+
+ if (builtin.single_threaded) unreachable; // nobody would ever wake us
+
+ switch (builtin.os.tag) {
+ .linux => {
+ const linux = std.os.linux;
+ var ts_buffer: linux.timespec = undefined;
+ const ts: ?*linux.timespec = if (timeout_ns) |ns| ts: {
+ ts_buffer = .{
+ .sec = @intCast(ns / std.time.ns_per_s),
+ .nsec = @intCast(ns % std.time.ns_per_s),
+ };
+ break :ts &ts_buffer;
+ } else null;
+ const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expected, ts);
+ switch (linux.errno(rc)) {
+ .SUCCESS => {}, // notified by wake
+ .INTR => {}, // caller's responsibility to retry
+ .AGAIN => {}, // ptr.* != expected
+ .INVAL => {}, // possibly timeout overflow
+ .TIMEDOUT => {},
+ else => {}, // spurious wakeup; caller retries
+ }
+ },
+
+ .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
+ const c = std.c;
+ const flags: c.UL = .{
+ .op = .COMPARE_AND_WAIT,
+ .NO_ERRNO = true,
+ };
+ const us: u32 = us: {
+ const ns = timeout_ns orelse break :us 0; // 0 means infinite
+ const us = std.math.lossyCast(u32, ns / std.time.ns_per_us);
+ break :us if (us == 0) 1 else us;
+ };
+ const status = c.__ulock_wait(flags, ptr, expected, us);
+ if (status >= 0) return;
+ switch (@as(c.E, @enumFromInt(-status))) {
+ .INTR => {}, // spurious wake
+ .FAULT => {}, // futex address paged out; caller retries
+ .TIMEDOUT => {},
+ else => {}, // spurious wakeup; caller retries
+ }
+ },
+
+ .freebsd => {
+ const flags = @intFromEnum(std.c.UMTX_OP.WAIT_UINT_PRIVATE);
+ var tm_size: usize = 0;
+ var tm: std.c._umtx_time = undefined;
+ var tm_ptr: ?*const std.c._umtx_time = null;
+ if (timeout_ns) |ns| {
+ tm_ptr = &tm;
+ tm_size = @sizeOf(@TypeOf(tm));
+ tm.flags = 0; // relative time
+ tm.clockid = .MONOTONIC;
+ tm.timeout = .{
+ .sec = @intCast(ns / std.time.ns_per_s),
+ .nsec = @intCast(ns % std.time.ns_per_s),
+ };
+ }
+ _ = std.c._umtx_op(
+ @intFromPtr(ptr),
+ flags,
+ @as(c_ulong, expected),
+ tm_size,
+ @intFromPtr(tm_ptr),
+ );
+ },
+
+ else => {
+ // Portable fallback: futex waits may wake spuriously, so a
+ // bounded sleep is a valid (if inefficient) implementation.
+ // Contention is not expected in libghostty-vt's threading
+ // model, so this is effectively never reached.
+ if (@atomicLoad(u32, ptr, .seq_cst) != expected) return;
+ const ns = @min(timeout_ns orelse std.time.ns_per_ms, std.time.ns_per_ms);
+ const ts: posix.timespec = .{
+ .sec = 0,
+ .nsec = @intCast(ns),
+ };
+ _ = posix.system.nanosleep(&ts, null);
+ },
+ }
+}
+
+pub fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void {
+ @branchHint(.cold);
+ _ = userdata;
+
+ if (builtin.single_threaded) return; // nothing to wake up
+
+ switch (builtin.os.tag) {
+ .linux => {
+ const linux = std.os.linux;
+ _ = linux.futex_3arg(
+ ptr,
+ .{ .cmd = .WAKE, .private = true },
+ @min(max_waiters, std.math.maxInt(i32)),
+ );
+ },
+
+ .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
+ const c = std.c;
+ const flags: c.UL = .{
+ .op = .COMPARE_AND_WAIT,
+ .NO_ERRNO = true,
+ .WAKE_ALL = max_waiters > 1,
+ };
+ while (true) {
+ const status = c.__ulock_wake(flags, ptr, 0);
+ if (status >= 0) return;
+ switch (@as(c.E, @enumFromInt(-status))) {
+ .INTR, .CANCELED => continue, // spurious wake
+ else => return,
+ }
+ }
+ },
+
+ .freebsd => {
+ _ = std.c._umtx_op(
+ @intFromPtr(ptr),
+ @intFromEnum(std.c.UMTX_OP.WAKE_PRIVATE),
+ @min(max_waiters, std.math.maxInt(c_ulong)),
+ 0,
+ 0,
+ );
+ },
+
+ // Portable fallback waiters poll with a timeout; nothing to do.
+ else => {},
+ }
+}
diff --git a/vendor/libghostty-vt/src/lib/tinyio/test.zig b/vendor/libghostty-vt/src/lib/tinyio/test.zig
new file mode 100644
index 00000000..c6b4d96f
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/tinyio/test.zig
@@ -0,0 +1,836 @@
+//! Tests for TinyIo that only touch its public surface, so they run on
+//! every supported platform. Platform-specific tests live next to the
+//! platform arm they exercise.
+
+const std = @import("std");
+const builtin = @import("builtin");
+const posix = std.posix;
+const Io = std.Io;
+const File = Io.File;
+const Dir = Io.Dir;
+const Threaded = std.Io.Threaded;
+const TinyIo = @import("../TinyIo.zig");
+const supported = TinyIo.supported;
+
+const is_windows = builtin.os.tag == .windows;
+const windows = std.os.windows;
+const os_windows = @import("../../os/windows.zig");
+const ntdll = os_windows.exp.ntdll;
+const kernel32 = os_windows.exp.kernel32;
+const HANDLE = windows.HANDLE;
+
+/// Reads until every buffer is full or the stream ends. POSIX readv fills
+/// all buffers in one call; NT reads one buffer per call.
+fn readStreamingAll(file: File, io_: Io, buffers: []const []u8) !usize {
+ var remaining: [8][]u8 = undefined;
+ std.debug.assert(buffers.len <= remaining.len);
+ @memcpy(remaining[0..buffers.len], buffers);
+ var list: [][]u8 = remaining[0..buffers.len];
+
+ var total: usize = 0;
+ while (list.len > 0) {
+ var n = file.readStreaming(io_, list) catch |err| switch (err) {
+ error.EndOfStream => break,
+ else => return err,
+ };
+ if (n == 0) break;
+ total += n;
+ while (list.len > 0 and n >= list[0].len) {
+ n -= list[0].len;
+ list = list[1..];
+ }
+ if (list.len > 0) list[0] = list[0][n..];
+ }
+ return total;
+}
+
+test "read a file through File.Reader" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ const contents = "hello minimal test_io\n" ** 100;
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "test.txt",
+ .data = contents,
+ });
+
+ // Open through our Io. The Dir handle is a plain fd, so it is usable
+ // across Io implementations.
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "test.txt", .{});
+ defer file.close(test_io);
+
+ // Stat through our Io.
+ const stat = try file.stat(test_io);
+ try testing.expectEqual(@as(u64, contents.len), stat.size);
+ try testing.expectEqual(File.Kind.file, stat.kind);
+
+ // fileLength.
+ try testing.expectEqual(@as(u64, contents.len), try file.length(test_io));
+
+ // Read it all back through File.Reader (exercises positional reads
+ // and streaming fallbacks).
+ var buf: [64]u8 = undefined;
+ var reader = file.reader(test_io, &buf);
+ var list: std.ArrayList(u8) = .empty;
+ defer list.deinit(testing.allocator);
+ try reader.interface.appendRemaining(testing.allocator, &list, .unlimited);
+ try testing.expectEqualStrings(contents, list.items);
+}
+
+test "seek" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "seek.txt",
+ .data = "0123456789",
+ });
+
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "seek.txt", .{});
+ defer file.close(test_io);
+
+ // Exercise our seek and streaming read implementations directly at
+ // the vtable level; the higher-level File.Reader seek plumbing has
+ // its own buffering behaviors that are independent of the Io
+ // implementation.
+ var out: [4]u8 = undefined;
+ var slices = [_][]u8{&out};
+
+ try test_io.vtable.fileSeekTo(test_io.userdata, file, 6);
+ var n = try file.readStreaming(test_io, &slices);
+ try testing.expectEqualStrings("6789", out[0..n]);
+
+ // Seek back relative and re-read.
+ try test_io.vtable.fileSeekBy(test_io.userdata, file, -8);
+ n = try file.readStreaming(test_io, &slices);
+ try testing.expectEqualStrings("2345", out[0..n]);
+
+ // Positional reads are independent of the seek position.
+ var pslices = [_][]u8{&out};
+ n = try test_io.vtable.fileReadPositional(test_io.userdata, file, &pslices, 1);
+ try testing.expectEqualStrings("1234", out[0..n]);
+}
+
+test "realPath and deleteFile" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ // Only platforms with a real implementation.
+ switch (builtin.os.tag) {
+ .macos, .ios, .linux, .freebsd, .windows => {},
+ else => return error.SkipZigTest,
+ }
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "real.txt",
+ .data = "x",
+ });
+
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "real.txt", .{});
+ var path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = path_buf[0..try file.realPath(test_io, &path_buf)];
+ try testing.expect(std.mem.endsWith(u8, path, "real.txt"));
+ file.close(test_io);
+
+ // dirRealPathFile via absolute path from cwd.
+ var path_buf2: [std.fs.max_path_bytes]u8 = undefined;
+ const path2 = path_buf2[0..try Dir.cwd().realPathFile(test_io, path, &path_buf2)];
+ try testing.expectEqualStrings(path, path2);
+
+ // Delete it through our Io, verify it is gone.
+ try dir.deleteFile(test_io, "real.txt");
+ try testing.expectError(error.FileNotFound, dir.openFile(test_io, "real.txt", .{}));
+}
+
+test "openFile of a directory returns IsDir" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ try tmp_dir.dir.createDir(testing.io, "sub", .default_dir);
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ try testing.expectError(error.IsDir, dir.openFile(test_io, "sub", .{
+ .allow_directory = false,
+ }));
+}
+
+test "Io.Mutex through TinyIo" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ // Contended and uncontended lock/unlock; exercises the futex ops.
+ var mutex: Io.Mutex = .init;
+ mutex.lockUncancelable(test_io);
+ mutex.unlock(test_io);
+
+ // Wake with no waiters must be a no-op.
+ var word: u32 = 0;
+ test_io.vtable.futexWake(test_io.userdata, &word, 1);
+
+ // Wait with a non-matching expected value must return immediately.
+ test_io.vtable.futexWaitUncancelable(test_io.userdata, &word, 1);
+}
+
+test "randomSecure fills with fresh entropy" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var a: [32]u8 = @splat(0);
+ var b: [32]u8 = @splat(0);
+ try test_io.randomSecure(&a);
+ try test_io.randomSecure(&b);
+
+ // Non-zero and non-repeating. A zero fill is what `random` does
+ // without a source, which would make every one-time password the
+ // same; identical draws would mean the same thing.
+ try testing.expect(!std.mem.allEqual(u8, &a, 0));
+ try testing.expect(!std.mem.allEqual(u8, &b, 0));
+ try testing.expect(!std.mem.eql(u8, &a, &b));
+
+ // Zero-length is a no-op.
+ try test_io.randomSecure(a[0..0]);
+}
+
+test "unused operations fail gracefully" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ try testing.expectError(
+ error.NoSpaceLeft,
+ dir.createFile(test_io, "nope.txt", .{}),
+ );
+}
+
+test "openFile edge cases" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "edge.txt",
+ .data = "edge",
+ });
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+
+ // File locking is unimplemented and must be reported, not ignored.
+ try testing.expectError(error.FileLocksUnsupported, dir.openFile(
+ test_io,
+ "edge.txt",
+ .{ .lock = .shared },
+ ));
+
+ // NUL bytes never form a valid path.
+ try testing.expectError(error.BadPathName, dir.openFile(
+ test_io,
+ "bad\x00path",
+ .{},
+ ));
+
+ // Paths that can't fit in PATH_MAX must not be silently truncated.
+ const long_name = "a" ** (std.fs.max_path_bytes + 1);
+ try testing.expectError(error.NameTooLong, dir.openFile(
+ test_io,
+ long_name,
+ .{},
+ ));
+
+ // A path component that is a file, not a directory. NT reports this
+ // as the path not existing (Threaded does too) rather than ENOTDIR.
+ try testing.expectError(
+ if (comptime is_windows) error.FileNotFound else error.NotDir,
+ dir.openFile(test_io, "edge.txt/child", .{}),
+ );
+
+ // Directories may be opened when allowed (the default) and stat
+ // reports their kind.
+ try tmp_dir.dir.createDir(testing.io, "subdir", .default_dir);
+ var dir_file = try dir.openFile(test_io, "subdir", .{});
+ const dir_stat = try dir_file.stat(test_io);
+ try testing.expectEqual(File.Kind.directory, dir_stat.kind);
+ dir_file.close(test_io);
+
+ // Write modes translate to the right ACCMODE flags; TinyIo can't
+ // write but opening for write must succeed.
+ var wfile = try dir.openFile(test_io, "edge.txt", .{ .mode = .write_only });
+ wfile.close(test_io);
+ var rwfile = try dir.openFile(test_io, "edge.txt", .{ .mode = .read_write });
+ rwfile.close(test_io);
+}
+
+test "openFile symlink handling" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ // Platforms where we know both symlink creation (via the testing Io)
+ // and O_NOFOLLOW behave as expected.
+ switch (builtin.os.tag) {
+ .macos, .ios, .linux, .freebsd => {},
+ else => return error.SkipZigTest,
+ }
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "target.txt",
+ .data = "target",
+ });
+ try tmp_dir.dir.symLink(testing.io, "target.txt", "link.txt", .{});
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+
+ // Following symlinks (the default) opens the target...
+ var file = try dir.openFile(test_io, "link.txt", .{});
+ try testing.expectEqual(@as(u64, "target".len), try file.length(test_io));
+
+ // ...and realPath resolves through the link to the target. This is
+ // the property the Kitty graphics path validation relies on.
+ var path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = path_buf[0..try file.realPath(test_io, &path_buf)];
+ try testing.expect(std.mem.endsWith(u8, path, "target.txt"));
+ file.close(test_io);
+
+ // Refusing to follow symlinks fails with SymLinkLoop.
+ try testing.expectError(error.SymLinkLoop, dir.openFile(
+ test_io,
+ "link.txt",
+ .{ .follow_symlinks = false },
+ ));
+}
+
+test "positional reads at and beyond EOF return zero" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "eof.txt",
+ .data = "0123456789",
+ });
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "eof.txt", .{});
+ defer file.close(test_io);
+
+ var out: [4]u8 = undefined;
+ var slices = [_][]u8{&out};
+
+ // At EOF and past EOF: the vtable contract is "returns 0 if reading
+ // at or past the end".
+ try testing.expectEqual(@as(usize, 0), try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ file,
+ &slices,
+ 10,
+ ));
+ try testing.expectEqual(@as(usize, 0), try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ file,
+ &slices,
+ 9999,
+ ));
+
+ // No buffers and only-empty buffers read nothing without a syscall.
+ try testing.expectEqual(@as(usize, 0), try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ file,
+ &.{},
+ 0,
+ ));
+ var empty = [_][]u8{ &.{}, &.{} };
+ try testing.expectEqual(@as(usize, 0), try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ file,
+ &empty,
+ 0,
+ ));
+}
+
+test "vectored reads scatter across buffers" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "vec.txt",
+ .data = "0123456789abcdef",
+ });
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "vec.txt", .{});
+ defer file.close(test_io);
+
+ // Scatter a positional read across multiple buffers, with empty
+ // buffers interleaved (they must be skipped).
+ var a: [4]u8 = undefined;
+ var b: [2]u8 = undefined;
+ var c: [6]u8 = undefined;
+ var slices = [_][]u8{ &a, &.{}, &b, &c };
+ const n = try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ file,
+ &slices,
+ 0,
+ );
+ try testing.expectEqual(@as(usize, 12), n);
+ try testing.expectEqualStrings("0123", &a);
+ try testing.expectEqualStrings("45", &b);
+ try testing.expectEqualStrings("6789ab", &c);
+
+ // More buffers than max_iovecs_len: reads are truncated to the
+ // first max_iovecs_len non-empty buffers (partial reads are allowed
+ // by the vtable contract; callers retry).
+ comptime std.debug.assert(Threaded.max_iovecs_len < 16);
+ var bytes: [16][1]u8 = undefined;
+ var many: [16][]u8 = undefined;
+ for (&many, &bytes) |*s, *byte| s.* = byte;
+ const n2 = try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ file,
+ &many,
+ 0,
+ );
+ try testing.expectEqual(@as(usize, Threaded.max_iovecs_len), n2);
+ for (bytes[0..n2], "0123456789abcdef"[0..n2]) |got, want| {
+ try testing.expectEqual(want, got[0]);
+ }
+}
+
+test "streaming reads: EndOfStream and scatter" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "stream.txt",
+ .data = "streaming!",
+ });
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "stream.txt", .{});
+ defer file.close(test_io);
+
+ // Scatter a streaming read (one readv on POSIX, one NtReadFile per
+ // buffer on Windows).
+ var a: [6]u8 = undefined;
+ var b: [4]u8 = undefined;
+ var slices = [_][]u8{ &a, &b };
+ try testing.expectEqual(@as(usize, 10), try readStreamingAll(file, test_io, &slices));
+ try testing.expectEqualStrings("stream", &a);
+ try testing.expectEqualStrings("ing!", &b);
+
+ // Reading again at EOF is a stream end, not a zero-length success.
+ try testing.expectError(error.EndOfStream, file.readStreaming(test_io, &slices));
+
+ // Empty destinations read nothing.
+ try testing.expectEqual(@as(usize, 0), try file.readStreaming(test_io, &.{}));
+
+ // Seeking beyond EOF is legal; the next streaming read hits EOF.
+ try test_io.vtable.fileSeekTo(test_io.userdata, file, 9999);
+ try testing.expectError(error.EndOfStream, file.readStreaming(test_io, &slices));
+
+ // And seeking back to zero re-reads from the start.
+ try test_io.vtable.fileSeekTo(test_io.userdata, file, 0);
+ try testing.expectEqual(@as(usize, 10), try readStreamingAll(file, test_io, &slices));
+ try testing.expectEqualStrings("stream", &a);
+}
+
+test "pipes: streaming works; positional and seek are Unseekable or ignored" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ const msg = "through the pipe";
+ const read_end: File = if (comptime is_windows) pipe: {
+ var read_h: HANDLE = undefined;
+ var write_h: HANDLE = undefined;
+ if (!kernel32.CreatePipe(&read_h, &write_h, null, 0).toBool()) {
+ return error.SkipZigTest;
+ }
+ defer _ = ntdll.NtClose(write_h);
+ // Twice, so a second read after the streaming one has data.
+ for (0..2) |_| {
+ var iosb: os_windows.IO_STATUS_BLOCK = undefined;
+ try testing.expectEqual(.SUCCESS, windows.ntdll.NtWriteFile(
+ write_h,
+ null,
+ null,
+ null,
+ &iosb,
+ msg.ptr,
+ msg.len,
+ null,
+ null,
+ ));
+ }
+ break :pipe .{ .handle = read_h, .flags = .{ .nonblocking = false } };
+ } else pipe: {
+ var fds: [2]posix.fd_t = undefined;
+ switch (posix.errno(posix.system.pipe(&fds))) {
+ .SUCCESS => {},
+ else => return error.SkipZigTest,
+ }
+ defer _ = posix.system.close(fds[1]);
+ try testing.expectEqual(
+ @as(isize, msg.len),
+ @as(isize, @intCast(posix.system.write(fds[1], msg, msg.len))),
+ );
+ break :pipe .{ .handle = fds[0], .flags = .{ .nonblocking = false } };
+ };
+ defer read_end.close(test_io);
+
+ // Streaming reads work on unseekable files; this is the fallback
+ // File.Reader depends on when positional reads report Unseekable.
+ var buf: [msg.len]u8 = undefined;
+ var slices = [_][]u8{&buf};
+ try testing.expectEqual(@as(usize, msg.len), try read_end.readStreaming(test_io, &slices));
+ try testing.expectEqualStrings(msg, &buf);
+
+ if (comptime is_windows) {
+ // NT pipes accept a byte offset and a file position but ignore
+ // both: reads stay sequential and seeks succeed without effect.
+ // Threaded behaves the same way, so TinyIo mirrors it rather than
+ // spending a syscall per read to detect pipes.
+ @memset(&buf, 0);
+ try testing.expectEqual(@as(usize, msg.len), try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ read_end,
+ &slices,
+ 8,
+ ));
+ try testing.expectEqualStrings(msg, &buf);
+ try test_io.vtable.fileSeekTo(test_io.userdata, read_end, 0);
+ try test_io.vtable.fileSeekBy(test_io.userdata, read_end, 1);
+
+ // Drained with the writer closed is the end of the stream.
+ try testing.expectError(error.EndOfStream, read_end.readStreaming(test_io, &slices));
+ return;
+ }
+
+ // Positional reads and seeks must report Unseekable so callers can
+ // fall back to streaming.
+ try testing.expectError(error.Unseekable, test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ read_end,
+ &slices,
+ 0,
+ ));
+ try testing.expectError(error.Unseekable, test_io.vtable.fileSeekTo(
+ test_io.userdata,
+ read_end,
+ 0,
+ ));
+ try testing.expectError(error.Unseekable, test_io.vtable.fileSeekBy(
+ test_io.userdata,
+ read_end,
+ 1,
+ ));
+}
+
+test "operate delegates non-read operations to failing stubs" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "w.txt",
+ .data = "x",
+ });
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "w.txt", .{ .mode = .write_only });
+ defer file.close(test_io);
+
+ const result = try test_io.vtable.operate(test_io.userdata, .{
+ .file_write_streaming = .{
+ .file = file,
+ .data = &.{"nope"},
+ },
+ });
+ try testing.expectError(error.InputOutput, result.file_write_streaming);
+}
+
+test "dirRealPathFile edge cases" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ switch (builtin.os.tag) {
+ .macos, .ios, .linux, .freebsd, .windows => {},
+ else => return error.SkipZigTest,
+ }
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "real.txt",
+ .data = "x",
+ });
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+
+ // Resolve the canonical path through an open file for reference.
+ var file = try dir.openFile(test_io, "real.txt", .{});
+ var want_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const want = want_buf[0..try file.realPath(test_io, &want_buf)];
+ file.close(test_io);
+
+ // A non-cwd directory handle exercises the open-then-resolve
+ // fallback branch rather than libc realpath.
+ var got_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const got = got_buf[0..try test_io.vtable.dirRealPathFile(
+ test_io.userdata,
+ dir,
+ "real.txt",
+ &got_buf,
+ )];
+ try testing.expectEqualStrings(want, got);
+
+ // Missing paths report FileNotFound (libc realpath branch, via an
+ // absolute path anchored at cwd).
+ var missing_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const missing = std.fmt.bufPrint(&missing_buf, "{s}.missing", .{want}) catch
+ return error.SkipZigTest;
+ var out_buf: [std.fs.max_path_bytes]u8 = undefined;
+ try testing.expectError(error.FileNotFound, Dir.cwd().realPathFile(
+ test_io,
+ missing,
+ &out_buf,
+ ));
+
+ // libc realpath requires a PATH_MAX-sized output buffer; smaller
+ // buffers must error rather than risk truncation.
+ var small_buf: [8]u8 = undefined;
+ try testing.expectError(error.NameTooLong, Dir.cwd().realPathFile(
+ test_io,
+ want,
+ &small_buf,
+ ));
+}
+
+test "deleteFile edge cases" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+
+ // Nonexistent files.
+ try testing.expectError(error.FileNotFound, dir.deleteFile(test_io, "missing.txt"));
+
+ // NUL bytes never form a valid POSIX path.
+ try testing.expectError(error.BadPathName, dir.deleteFile(test_io, "bad\x00path"));
+
+ // Deleting a directory reports IsDir. On BSD-derived systems
+ // (including macOS) unlink returns EPERM for directories, which
+ // exercises the stat-based disambiguation path.
+ try tmp_dir.dir.createDir(testing.io, "subdir", .default_dir);
+ try testing.expectError(error.IsDir, dir.deleteFile(test_io, "subdir"));
+
+ // Deleting a symlink removes the link, not its target. Creating one
+ // on Windows needs a privilege that plain users and CI lack.
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "target.txt",
+ .data = "x",
+ });
+ tmp_dir.dir.symLink(testing.io, "target.txt", "link.txt", .{}) catch |err| switch (err) {
+ error.AccessDenied => if (comptime is_windows) return error.SkipZigTest else return err,
+ else => return err,
+ };
+ try dir.deleteFile(test_io, "link.txt");
+ var file = try dir.openFile(test_io, "target.txt", .{});
+ file.close(test_io);
+ try testing.expectError(error.FileNotFound, dir.openFile(test_io, "link.txt", .{}));
+}
+
+test "dirClose closes the descriptor" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.createDir(testing.io, "subdir", .default_dir);
+ const opened = try tmp_dir.dir.openDir(testing.io, "subdir", .{});
+
+ const dir: Dir = .{ .handle = opened.handle };
+ test_io.vtable.dirClose(test_io.userdata, &.{dir});
+
+ if (comptime is_windows) {
+ // A query on a closed handle fails without raising anything.
+ var iosb: os_windows.IO_STATUS_BLOCK = undefined;
+ var info: os_windows.FILE_STANDARD_INFORMATION = undefined;
+ try testing.expectEqual(.INVALID_HANDLE, ntdll.NtQueryInformationFile(
+ dir.handle,
+ &iosb,
+ &info,
+ @sizeOf(os_windows.FILE_STANDARD_INFORMATION),
+ .Standard,
+ ));
+ return;
+ }
+
+ // Verify with a raw dup that the fd is gone. We check the errno
+ // directly rather than going through our Io so no error path prints
+ // "unexpected errno" diagnostics in debug test builds. `dup` rather
+ // than `fstat` because glibc has no LFS64 `fstat` symbol, so
+ // `fstat_sym` doesn't exist on Linux.
+ try testing.expectEqual(posix.E.BADF, posix.errno(posix.system.dup(dir.handle)));
+}
+
+test "cancel protection operations are benign" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ // There is no async, so cancelation can never be requested.
+ try test_io.vtable.checkCancel(test_io.userdata);
+
+ // Swap and restore roundtrip like generic std code does around
+ // uninterruptible sections.
+ const prev = test_io.vtable.swapCancelProtection(test_io.userdata, .blocked);
+ _ = test_io.vtable.swapCancelProtection(test_io.userdata, prev);
+ test_io.vtable.recancel(test_io.userdata);
+
+ _ = testing;
+}
+
+test "async runs inline; concurrency is unavailable" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ const S = struct {
+ fn work(x: *u32) u32 {
+ x.* += 1;
+ return x.*;
+ }
+ };
+
+ // The task must have executed synchronously, before await.
+ var state: u32 = 41;
+ var future = test_io.async(S.work, .{&state});
+ try testing.expectEqual(@as(u32, 42), state);
+ try testing.expectEqual(@as(u32, 42), future.await(test_io));
+
+ // Concurrency must be reported as unavailable, not silently run.
+ try testing.expectError(
+ error.ConcurrencyUnavailable,
+ test_io.concurrent(S.work, .{&state}),
+ );
+ try testing.expectEqual(@as(u32, 42), state);
+}
+
+test "futex timed waits return" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+
+ // Nobody wakes this futex, so returning at all proves the timeout
+ // (or the spurious-wakeup contract) works.
+ var word: u32 = 1;
+ try test_io.vtable.futexWait(test_io.userdata, &word, 1, .{
+ .duration = .{ .raw = .fromNanoseconds(5 * std.time.ns_per_ms), .clock = .awake },
+ });
+
+ // Deadlines can't be resolved without a clock; they degrade to a
+ // short poll which must also return.
+ try test_io.vtable.futexWait(test_io.userdata, &word, 1, .{
+ .deadline = .{ .raw = .fromNanoseconds(1), .clock = .awake },
+ });
+
+ // A mismatched expected value returns immediately even with no
+ // timeout.
+ try test_io.vtable.futexWait(test_io.userdata, &word, 2, .none);
+
+ // Waking more than one waiter takes the wake-all path.
+ test_io.vtable.futexWake(test_io.userdata, &word, 2);
+ test_io.vtable.futexWake(test_io.userdata, &word, std.math.maxInt(u32));
+}
+
+test "Io.Mutex under real thread contention" {
+ if (comptime !supported) return error.SkipZigTest;
+ if (comptime builtin.single_threaded) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ // Hammer a mutex from several threads so waiters actually park in
+ // futexWait and get released by futexWake.
+ const S = struct {
+ const iterations = 10_000;
+
+ fn worker(m: *Io.Mutex, io_: Io, counter: *u64) void {
+ for (0..iterations) |_| {
+ m.lockUncancelable(io_);
+ defer m.unlock(io_);
+ counter.* += 1;
+ }
+ }
+ };
+
+ var mutex: Io.Mutex = .init;
+ var counter: u64 = 0;
+
+ const thread_count = 4;
+ var threads: [thread_count]std.Thread = undefined;
+ var spawned: usize = 0;
+ defer for (threads[0..spawned]) |t| t.join();
+ for (&threads) |*t| {
+ t.* = std.Thread.spawn(.{}, S.worker, .{ &mutex, test_io, &counter }) catch
+ break;
+ spawned += 1;
+ }
+ for (threads[0..spawned]) |t| t.join();
+ spawned = 0;
+
+ try testing.expectEqual(@as(u64, S.iterations * thread_count), counter);
+}
diff --git a/vendor/libghostty-vt/src/lib/tinyio/windows.zig b/vendor/libghostty-vt/src/lib/tinyio/windows.zig
new file mode 100644
index 00000000..a815d266
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/tinyio/windows.zig
@@ -0,0 +1,745 @@
+//! Windows (NT) impl of TinyIo.
+//!
+//! Modeled on the Windows impl of `std.Io.Threaded`, minus cancelation and
+//! the sleep-and-retry loops it wraps around two kernel quirks (TinyIo
+//! cannot sleep, so those statuses surface as `error.FileBusy`). Everything
+//! goes through ntdll and kernel32, which static consumers already link.
+//! Paths arrive as WTF-8 and are converted to the NT-prefixed WTF-16 form
+//! `NtCreateFile` expects, the same way Threaded converts them.
+
+const std = @import("std");
+const builtin = @import("builtin");
+const Io = std.Io;
+const File = Io.File;
+const Dir = Io.Dir;
+const Threaded = std.Io.Threaded;
+const TinyIo = @import("../TinyIo.zig");
+const windows = std.os.windows;
+const os_windows = @import("../../os/windows.zig");
+const ntdll = os_windows.exp.ntdll;
+const kernel32 = os_windows.exp.kernel32;
+const HANDLE = windows.HANDLE;
+
+const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
+const unc_nt_prefix = [_]u16{ '\\', '?', '?', '\\', 'U', 'N', 'C', '\\' };
+const share_all = os_windows.FILE_SHARE_READ |
+ os_windows.FILE_SHARE_WRITE |
+ os_windows.FILE_SHARE_DELETE;
+
+/// A WTF-16 path buffer sized for the longest path NT accepts, plus one
+/// slot for the terminator the Rtl path functions want. The array is
+/// deliberately not a sentinel type: `undefined` sentinel arrays make the
+/// compiler materialize a 64 KiB constant image of the buffer (to place
+/// the sentinel) in .rdata, which cost ~200 KB across three buffers.
+const WindowsPath = struct {
+ data: [windows.PATH_MAX_WIDE + 1]u16,
+ len: usize,
+
+ fn span(self: *const WindowsPath) [:0]const u16 {
+ return self.data[0..self.len :0];
+ }
+
+ fn setLen(self: *WindowsPath, len: usize) void {
+ self.len = len;
+ self.data[len] = 0;
+ }
+
+ /// True for absolute NT paths (`\??\...`). These must be opened with
+ /// a null root directory; anything else is relative to a handle.
+ fn isNt(self: *const WindowsPath) bool {
+ return windows.hasCommonNtPrefix(u16, self.span());
+ }
+
+ /// Rewrites the Win32 path produced by RtlGetFullPathName_U into NT
+ /// form in place: `\\?\X` and `\\.\X` become `\??\X`, `\\server\share`
+ /// becomes `\??\UNC\server\share`, and `C:\...` gets `\??\` in front.
+ fn win32ToNt(self: *WindowsPath) error{NameTooLong}!void {
+ const p = self.data[0..self.len];
+ const sep: u16 = '\\';
+ if (p.len >= 4 and p[0] == sep and p[1] == sep and
+ (p[2] == '?' or p[2] == '.') and p[3] == sep)
+ {
+ self.data[0..nt_prefix.len].* = nt_prefix;
+ return;
+ }
+ if (p.len >= 2 and p[0] == sep and p[1] == sep) {
+ const rest_len = p.len - 2;
+ if (unc_nt_prefix.len + rest_len > windows.PATH_MAX_WIDE) return error.NameTooLong;
+ @memmove(self.data[unc_nt_prefix.len..][0..rest_len], p[2..]);
+ self.data[0..unc_nt_prefix.len].* = unc_nt_prefix;
+ self.setLen(unc_nt_prefix.len + rest_len);
+ return;
+ }
+ if (nt_prefix.len + p.len > windows.PATH_MAX_WIDE) return error.NameTooLong;
+ @memmove(self.data[nt_prefix.len..][0..p.len], p);
+ self.data[0..nt_prefix.len].* = nt_prefix;
+ self.setLen(nt_prefix.len + p.len);
+ }
+};
+
+const PathError = Dir.PathNameError || Io.UnexpectedError;
+
+/// Converts a WTF-8 path into what `NtCreateFile` accepts, the way
+/// `std.Io.Threaded.sliceToPrefixedFileW` does: NT paths (`\??\...`) pass
+/// through, relative paths are normalized and stay relative to `dir`,
+/// and everything else (drive-absolute, drive-relative, rooted, UNC,
+/// `\\.\` and `\\?\` device paths, plus relative paths with more `..`
+/// components than can be removed) is resolved to an absolute `\??\`
+/// path through RtlGetFullPathName_U. `dir` matters only for that last
+/// case; the process working directory is what Rtl resolves against.
+///
+/// The result is written to `out`, which is 64 KiB, so callers keep it
+/// rather than having it returned by value.
+fn pathToNt(dir: ?HANDLE, path: []const u8, out: *WindowsPath) PathError!void {
+ out.setLen(try windows.wtf8ToWtf16Le(out.data[0..windows.PATH_MAX_WIDE], path));
+ if (out.isNt()) return;
+
+ const path_type = std.fs.path.getWin32PathType(u16, out.span());
+ switch (path_type) {
+ .relative => {
+ if (windows.normalizePath(u16, out.data[0..out.len])) |len| {
+ out.setLen(len);
+ return;
+ } else |err| switch (err) {
+ // Escapes the directory; resolved to an absolute path below.
+ error.TooManyParentDirs => {},
+ }
+ },
+ .root_local_device => {
+ // `\\.` and `\\?` are the NT prefix and nothing else.
+ out.data[0..nt_prefix.len].* = nt_prefix;
+ out.setLen(nt_prefix.len);
+ return;
+ },
+ else => {},
+ }
+
+ // RtlGetFullPathName_U resolves against the process working directory,
+ // so a relative path against any other directory handle is anchored to
+ // that directory's final path first.
+ var full: [windows.PATH_MAX_WIDE + 1]u16 = undefined;
+ var full_len: usize = 0;
+ if (path_type == .relative) anchor: {
+ const dir_handle = dir orelse break :anchor;
+ if (dir_handle == Dir.cwd().handle) break :anchor;
+ const dir_path = finalPath(dir_handle, &full) catch |err| switch (err) {
+ error.NameTooLong => return error.NameTooLong,
+ else => return error.Unexpected,
+ };
+ full_len = dir_path.len;
+ full[full_len] = '\\';
+ full_len += 1;
+ }
+ if (full_len + out.len > windows.PATH_MAX_WIDE) return error.NameTooLong;
+ @memcpy(full[full_len..][0..out.len], out.span());
+ full_len += out.len;
+ full[full_len] = 0;
+
+ const byte_len = ntdll.RtlGetFullPathName_U(
+ full[0..full_len :0].ptr,
+ @intCast(windows.PATH_MAX_WIDE * 2),
+ &out.data,
+ null,
+ );
+ if (byte_len == 0) return error.BadPathName;
+ if (byte_len / 2 > windows.PATH_MAX_WIDE) return error.NameTooLong;
+ out.setLen(byte_len / 2);
+ try out.win32ToNt();
+}
+
+const FinalPathError = error{
+ FileNotFound,
+ AccessDenied,
+ NameTooLong,
+ SystemResources,
+} || Io.UnexpectedError;
+
+/// The canonical Win32 path of an open handle from GetFinalPathNameByHandleW
+/// with the `\\?\` prefix removed, which is the form Threaded's realpath
+/// returns and the Kitty graphics path validation expects: `\\?\C:\x`
+/// becomes `C:\x` and `\\?\UNC\srv\share\x` becomes `\\srv\share\x`.
+/// The result aliases `buf`.
+fn finalPath(handle: HANDLE, buf: *[windows.PATH_MAX_WIDE + 1]u16) FinalPathError![]u16 {
+ const len = kernel32.GetFinalPathNameByHandleW(
+ handle,
+ buf,
+ @intCast(buf.len),
+ os_windows.FILE_NAME_NORMALIZED | os_windows.VOLUME_NAME_DOS,
+ );
+ if (len == 0) return switch (os_windows.GetLastError()) {
+ .FILE_NOT_FOUND, .PATH_NOT_FOUND, .INVALID_HANDLE => error.FileNotFound,
+ .ACCESS_DENIED => error.AccessDenied,
+ .NOT_ENOUGH_MEMORY => error.SystemResources,
+ else => |err| os_windows.unexpectedError(err),
+ };
+ // A too-small buffer reports the size needed, terminator included.
+ if (len > windows.PATH_MAX_WIDE) return error.NameTooLong;
+
+ const path = buf[0..len];
+ if (path.len >= 4 and path[0] == '\\' and path[1] == '\\' and
+ path[2] == '?' and path[3] == '\\')
+ {
+ // `\\?\` is the Win32 spelling of `\??\`; rewrite it so the std
+ // helper strips it and folds `UNC\` back into `\\`.
+ path[1] = '?';
+ return windows.ntToWin32Namespace(path, path) catch |err| switch (err) {
+ error.NotNtPath => unreachable,
+ error.NameTooLong => error.NameTooLong,
+ };
+ }
+ return path;
+}
+
+fn realPathHandle(handle: HANDLE, out_buffer: []u8) File.RealPathError!usize {
+ var wide: [windows.PATH_MAX_WIDE + 1]u16 = undefined;
+ const path = try finalPath(handle, &wide);
+ if (std.unicode.calcWtf8Len(path) > out_buffer.len) return error.NameTooLong;
+ return std.unicode.wtf16LeToWtf8(out_buffer, path);
+}
+
+const OpenNtError = error{
+ BadPathName,
+ FileNotFound,
+ NetworkNotFound,
+ NoDevice,
+ AccessDenied,
+ PipeBusy,
+ PathAlreadyExists,
+ IsDir,
+ NotDir,
+ AntivirusInterference,
+ FileBusy,
+} || Io.UnexpectedError;
+
+/// Opens an existing file or directory. `path` is relative to `dir`
+/// unless it is an absolute NT path, in which case `dir` is ignored
+/// (NtCreateFile rejects a root directory paired with an absolute name).
+fn openNt(
+ dir: HANDLE,
+ path: *const WindowsPath,
+ access: windows.ACCESS_MASK,
+ options: u32,
+) OpenNtError!HANDLE {
+ var attr: os_windows.OBJECT_ATTRIBUTES = .{
+ .RootDirectory = if (path.isNt()) null else dir,
+ .ObjectName = @constCast(&windows.UNICODE_STRING.init(path.span())),
+ };
+ var iosb: os_windows.IO_STATUS_BLOCK = undefined;
+ var handle: HANDLE = undefined;
+ return switch (ntdll.NtCreateFile(
+ &handle,
+ access,
+ &attr,
+ &iosb,
+ null,
+ os_windows.FILE_ATTRIBUTE_NORMAL,
+ share_all,
+ os_windows.FILE_OPEN,
+ options,
+ null,
+ 0,
+ )) {
+ .SUCCESS => handle,
+ .OBJECT_NAME_INVALID, .OBJECT_PATH_SYNTAX_BAD => error.BadPathName,
+ .OBJECT_NAME_NOT_FOUND, .OBJECT_PATH_NOT_FOUND => error.FileNotFound,
+ .BAD_NETWORK_PATH, .BAD_NETWORK_NAME => error.NetworkNotFound,
+ .NO_MEDIA_IN_DEVICE, .PIPE_NOT_AVAILABLE => error.NoDevice,
+ .ACCESS_DENIED, .USER_MAPPED_FILE => error.AccessDenied,
+ .PIPE_BUSY => error.PipeBusy,
+ .OBJECT_NAME_COLLISION => error.PathAlreadyExists,
+ .FILE_IS_A_DIRECTORY => error.IsDir,
+ .NOT_A_DIRECTORY => error.NotDir,
+ .VIRUS_INFECTED, .VIRUS_DELETED => error.AntivirusInterference,
+ // Threaded sleeps and retries these (a kernel bug with recently
+ // closed executables, and deletes still in progress).
+ .SHARING_VIOLATION, .DELETE_PENDING => error.FileBusy,
+ else => |status| os_windows.unexpectedStatus(status),
+ };
+}
+
+pub fn dirOpenFile(
+ _: ?*anyopaque,
+ dir: Dir,
+ sub_path: []const u8,
+ options: Dir.OpenFileOptions,
+) File.OpenError!File {
+ // Same as the POSIX impl: nothing in the terminal locks files.
+ if (options.lock != .none) return error.FileLocksUnsupported;
+ var path: WindowsPath = undefined;
+ try pathToNt(dir.handle, sub_path, &path);
+
+ // Directories can never be opened for writing, and `.` and `..`
+ // always name a directory.
+ const allow_directory = options.allow_directory and !options.isWrite();
+ if (!allow_directory and (std.mem.eql(u16, path.span(), &.{'.'}) or
+ std.mem.eql(u16, path.span(), &.{ '.', '.' })))
+ {
+ return error.IsDir;
+ }
+
+ var flags: u32 = os_windows.FILE_SYNCHRONOUS_IO_NONALERT;
+ if (!allow_directory) flags |= os_windows.FILE_NON_DIRECTORY_FILE;
+ if (!options.follow_symlinks) flags |= os_windows.FILE_OPEN_REPARSE_POINT;
+ const handle = try openNt(dir.handle, &path, .{
+ .STANDARD = .{ .SYNCHRONIZE = true },
+ .GENERIC = .{ .READ = options.isRead(), .WRITE = options.isWrite() },
+ }, flags);
+ return .{ .handle = handle, .flags = .{ .nonblocking = false } };
+}
+
+pub fn dirClose(_: ?*anyopaque, dirs: []const Dir) void {
+ for (dirs) |dir| _ = ntdll.NtClose(dir.handle);
+}
+
+pub fn fileClose(_: ?*anyopaque, files: []const File) void {
+ for (files) |file| _ = ntdll.NtClose(file.handle);
+}
+
+pub fn fileStat(_: ?*anyopaque, file: File) File.StatError!File.Stat {
+ var iosb: os_windows.IO_STATUS_BLOCK = undefined;
+ var info: os_windows.FILE_ALL_INFORMATION = undefined;
+ switch (ntdll.NtQueryInformationFile(
+ file.handle,
+ &iosb,
+ &info,
+ @sizeOf(os_windows.FILE_ALL_INFORMATION),
+ .All,
+ )) {
+ // The trailing name is variable length and unused, so an
+ // overflow only means it was truncated.
+ .SUCCESS, .BUFFER_OVERFLOW => {},
+ .ACCESS_DENIED => return error.AccessDenied,
+ else => |status| return os_windows.unexpectedStatus(status),
+ }
+
+ const kind: File.Kind = kind: {
+ if (info.BasicInformation.FileAttributes.REPARSE_POINT) {
+ var tag: os_windows.FILE_ATTRIBUTE_TAG_INFORMATION = undefined;
+ switch (ntdll.NtQueryInformationFile(
+ file.handle,
+ &iosb,
+ &tag,
+ @sizeOf(os_windows.FILE_ATTRIBUTE_TAG_INFORMATION),
+ .AttributeTag,
+ )) {
+ .SUCCESS => {},
+ .ACCESS_DENIED => return error.AccessDenied,
+ else => |status| return os_windows.unexpectedStatus(status),
+ }
+ break :kind if (tag.ReparseTag.IsSurrogate) .sym_link else .unknown;
+ }
+ break :kind if (info.BasicInformation.FileAttributes.DIRECTORY)
+ .directory
+ else
+ .file;
+ };
+
+ return .{
+ .inode = info.InternalInformation.IndexNumber,
+ .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
+ .permissions = .default_file,
+ .kind = kind,
+ .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
+ .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
+ .ctime = windows.fromSysTime(info.BasicInformation.ChangeTime),
+ .nlink = info.StandardInformation.NumberOfLinks,
+ .block_size = @intCast(std.heap.page_size_max),
+ };
+}
+
+pub fn fileLength(_: ?*anyopaque, file: File) File.LengthError!u64 {
+ var iosb: os_windows.IO_STATUS_BLOCK = undefined;
+ var info: os_windows.FILE_STANDARD_INFORMATION = undefined;
+ return switch (ntdll.NtQueryInformationFile(
+ file.handle,
+ &iosb,
+ &info,
+ @sizeOf(os_windows.FILE_STANDARD_INFORMATION),
+ .Standard,
+ )) {
+ .SUCCESS => @as(u64, @bitCast(info.EndOfFile)),
+ .ACCESS_DENIED => error.AccessDenied,
+ else => |status| os_windows.unexpectedStatus(status),
+ };
+}
+
+/// One NtReadFile into one buffer. A positional read supplies an explicit
+/// byte offset; a streaming read uses and advances the handle's own file
+/// position (TinyIo only opens synchronous handles, which track it).
+fn ntRead(comptime positional: bool, handle: HANDLE, buffer: []u8, offset: u64) !usize {
+ var iosb: os_windows.IO_STATUS_BLOCK = undefined;
+ var signed_offset: windows.LARGE_INTEGER = undefined;
+ const offset_ptr: ?*const windows.LARGE_INTEGER = if (positional) o: {
+ signed_offset = std.math.cast(i64, offset) orelse return error.Unseekable;
+ break :o &signed_offset;
+ } else null;
+ return switch (ntdll.NtReadFile(
+ handle,
+ null,
+ null,
+ null,
+ &iosb,
+ buffer.ptr,
+ std.math.lossyCast(u32, buffer.len),
+ offset_ptr,
+ null,
+ )) {
+ .SUCCESS => iosb.Information,
+ .END_OF_FILE, .PIPE_BROKEN => error.EndOfStream,
+ .INVALID_HANDLE => error.NotOpenForReading,
+ .INVALID_DEVICE_REQUEST => error.IsDir,
+ .FILE_LOCK_CONFLICT => error.LockViolation,
+ .ACCESS_DENIED => error.AccessDenied,
+ // Pipes and devices reject explicit offsets.
+ .INVALID_PARAMETER => |status| if (positional)
+ error.Unseekable
+ else
+ os_windows.unexpectedStatus(status),
+ // Only asynchronous handles complete later, and TinyIo never
+ // creates one; returning here would hand the kernel a dead buffer.
+ .PENDING => unreachable,
+ else => |status| os_windows.unexpectedStatus(status),
+ };
+}
+
+pub fn fileReadPositional(
+ _: ?*anyopaque,
+ file: File,
+ data: []const []u8,
+ offset: u64,
+) File.ReadPositionalError!usize {
+ // NtReadFile takes one buffer, so a scatter read is issued one buffer
+ // at a time, bounded like the POSIX iovec path, stopping at the first
+ // short read. Positional reads only make sense on seekable files, so
+ // the extra calls cannot block.
+ var total: usize = 0;
+ var count: usize = 0;
+ for (data) |buffer| {
+ if (buffer.len == 0) continue;
+ if (count == Threaded.max_iovecs_len) break;
+ count += 1;
+ const n = ntRead(true, file.handle, buffer, offset + total) catch |err| switch (err) {
+ error.EndOfStream => break,
+ else => |e| return e,
+ };
+ total += n;
+ if (n < buffer.len) break;
+ }
+ return total;
+}
+
+pub fn fileReadStreaming(
+ file: File,
+ data: []const []u8,
+) Io.Operation.FileReadStreaming.Error!usize {
+ // Like Threaded, a streaming read fills one buffer per call: a second
+ // NtReadFile could block on a pipe that already delivered data.
+ for (data) |buffer| {
+ if (buffer.len == 0) continue;
+ return ntRead(false, file.handle, buffer, 0);
+ }
+ return 0;
+}
+
+fn setFilePosition(
+ handle: HANDLE,
+ info: *os_windows.FILE_POSITION_INFORMATION,
+) File.SeekError!void {
+ var iosb: os_windows.IO_STATUS_BLOCK = undefined;
+ return switch (ntdll.NtSetInformationFile(
+ handle,
+ &iosb,
+ info,
+ @sizeOf(os_windows.FILE_POSITION_INFORMATION),
+ .Position,
+ )) {
+ .SUCCESS => {},
+ .ACCESS_DENIED => error.AccessDenied,
+ .PIPE_NOT_AVAILABLE, .INVALID_PARAMETER, .INVALID_DEVICE_REQUEST => error.Unseekable,
+ else => |status| os_windows.unexpectedStatus(status),
+ };
+}
+
+pub fn fileSeekBy(_: ?*anyopaque, file: File, offset: i64) File.SeekError!void {
+ var iosb: os_windows.IO_STATUS_BLOCK = undefined;
+ var info: os_windows.FILE_POSITION_INFORMATION = undefined;
+ switch (ntdll.NtQueryInformationFile(
+ file.handle,
+ &iosb,
+ &info,
+ @sizeOf(os_windows.FILE_POSITION_INFORMATION),
+ .Position,
+ )) {
+ .SUCCESS => {},
+ .ACCESS_DENIED => return error.AccessDenied,
+ .PIPE_NOT_AVAILABLE, .INVALID_PARAMETER, .INVALID_DEVICE_REQUEST => return error.Unseekable,
+ else => |status| return os_windows.unexpectedStatus(status),
+ }
+ const current: u64 = @bitCast(info.CurrentByteOffset);
+ const target = if (offset >= 0)
+ std.math.add(u64, current, @intCast(offset))
+ else
+ std.math.sub(u64, current, @abs(offset));
+ info.CurrentByteOffset = @bitCast(target catch return error.Unseekable);
+ return setFilePosition(file.handle, &info);
+}
+
+pub fn fileSeekTo(_: ?*anyopaque, file: File, offset: u64) File.SeekError!void {
+ var info: os_windows.FILE_POSITION_INFORMATION = .{
+ .CurrentByteOffset = @bitCast(offset),
+ };
+ return setFilePosition(file.handle, &info);
+}
+
+pub fn fileRealPath(
+ _: ?*anyopaque,
+ file: File,
+ out_buffer: []u8,
+) File.RealPathError!usize {
+ return realPathHandle(file.handle, out_buffer);
+}
+
+pub fn dirRealPathFile(
+ _: ?*anyopaque,
+ dir: Dir,
+ sub_path: []const u8,
+ out_buffer: []u8,
+) Dir.RealPathFileError!usize {
+ var path: WindowsPath = undefined;
+ try pathToNt(dir.handle, sub_path, &path);
+ const handle = try openNt(dir.handle, &path, .{
+ .STANDARD = .{ .SYNCHRONIZE = true },
+ .GENERIC = .{ .READ = true },
+ }, os_windows.FILE_SYNCHRONOUS_IO_NONALERT);
+ defer _ = ntdll.NtClose(handle);
+
+ // The path buffer is free again, so it holds the wide result.
+ const final = try finalPath(handle, &path.data);
+ if (std.unicode.calcWtf8Len(final) > out_buffer.len) return error.NameTooLong;
+ return std.unicode.wtf16LeToWtf8(out_buffer, final);
+}
+
+pub fn dirDeleteFile(
+ _: ?*anyopaque,
+ dir: Dir,
+ sub_path: []const u8,
+) Dir.DeleteFileError!void {
+ // The parent cannot be removed through a handle inside it.
+ if (std.mem.eql(u8, sub_path, "..")) return error.FileBusy;
+ var path: WindowsPath = undefined;
+ try pathToNt(dir.handle, sub_path, &path);
+ // NT has no `.`, but an empty name reopens `dir` itself.
+ if (std.mem.eql(u8, sub_path, ".")) path.setLen(0);
+
+ const handle = openNt(dir.handle, &path, .{
+ .STANDARD = .{ .RIGHTS = .{ .DELETE = true }, .SYNCHRONIZE = true },
+ }, os_windows.FILE_SYNCHRONOUS_IO_NONALERT |
+ os_windows.FILE_NON_DIRECTORY_FILE |
+ os_windows.FILE_OPEN_REPARSE_POINT) catch |err| switch (err) {
+ // Not produced by a plain file open, and not in DeleteFileError.
+ error.PipeBusy,
+ error.NoDevice,
+ error.PathAlreadyExists,
+ error.AntivirusInterference,
+ => return error.Unexpected,
+ else => |e| return e,
+ };
+ defer _ = ntdll.NtClose(handle);
+
+ // Prefer POSIX delete semantics (the name is gone immediately, even
+ // with other handles open, e.g. an antivirus scan of a temp file) and
+ // fall back to delete-on-close where the kernel or file system does
+ // not support them.
+ var iosb: os_windows.IO_STATUS_BLOCK = undefined;
+ var ex: os_windows.FILE_DISPOSITION_INFORMATION_EX = .{ .Flags = .{
+ .DELETE = true,
+ .POSIX_SEMANTICS = true,
+ .IGNORE_READONLY_ATTRIBUTE = true,
+ } };
+ const status = switch (ntdll.NtSetInformationFile(
+ handle,
+ &iosb,
+ &ex,
+ @sizeOf(os_windows.FILE_DISPOSITION_INFORMATION_EX),
+ .DispositionEx,
+ )) {
+ .INVALID_PARAMETER, .INVALID_INFO_CLASS, .NOT_SUPPORTED => fallback: {
+ var info: os_windows.FILE_DISPOSITION_INFORMATION = .{ .DeleteFile = .TRUE };
+ break :fallback ntdll.NtSetInformationFile(
+ handle,
+ &iosb,
+ &info,
+ @sizeOf(os_windows.FILE_DISPOSITION_INFORMATION),
+ .Disposition,
+ );
+ },
+ else => |status| status,
+ };
+ switch (status) {
+ .SUCCESS => {},
+ .CANNOT_DELETE, .MEDIA_WRITE_PROTECTED, .ACCESS_DENIED => return error.AccessDenied,
+ else => |s| return os_windows.unexpectedStatus(s),
+ }
+}
+
+pub fn randomSecure(buffer: []u8) Io.RandomSecureError!void {
+ // Read the kernel CSPRNG device directly, which is what ProcessPrng
+ // and BCryptGenRandom draw from and what Threaded reads through a
+ // cached handle. TinyIo is stateless, so the device is opened per
+ // call: three syscalls, fine for one-time password generation, and
+ // no bcryptprimitives or advapi32 import for static consumers.
+ var name: windows.UNICODE_STRING = .init(
+ std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\CNG"),
+ );
+ var attr: os_windows.OBJECT_ATTRIBUTES = .{ .ObjectName = &name };
+ var iosb: os_windows.IO_STATUS_BLOCK = undefined;
+ var handle: HANDLE = undefined;
+ switch (ntdll.NtOpenFile(
+ &handle,
+ .{
+ .STANDARD = .{ .SYNCHRONIZE = true },
+ .SPECIFIC = .{ .FILE = .{ .READ_DATA = true } },
+ },
+ &attr,
+ &iosb,
+ share_all,
+ os_windows.FILE_SYNCHRONOUS_IO_NONALERT,
+ )) {
+ .SUCCESS => {},
+ else => return error.EntropyUnavailable,
+ }
+ defer _ = ntdll.NtClose(handle);
+
+ var i: usize = 0;
+ while (i < buffer.len) {
+ const len = std.math.lossyCast(u32, buffer.len - i);
+ switch (ntdll.NtDeviceIoControlFile(
+ handle,
+ null,
+ null,
+ null,
+ &iosb,
+ os_windows.IOCTL_KSEC_GEN_RANDOM,
+ null,
+ 0,
+ buffer[i..].ptr,
+ len,
+ )) {
+ .SUCCESS => i += len,
+ else => return error.EntropyUnavailable,
+ }
+ }
+}
+
+pub fn futexWaitInner(ptr: *const u32, expected: u32, timeout_ns: ?u64) void {
+ @branchHint(.cold);
+ if (builtin.single_threaded) unreachable; // nobody would ever wake us
+
+ // RtlWaitOnAddress is what kernel32's WaitOnAddress forwards
+ // to; going through ntdll keeps static consumers off
+ // synchronization.lib. A negative timeout is relative in
+ // 100ns units and null waits forever. It returns SUCCESS on a
+ // wake or when the value already differs and TIMEOUT
+ // otherwise; either way the caller re-checks its condition.
+ var interval: windows.LARGE_INTEGER = undefined;
+ const timeout: ?*const windows.LARGE_INTEGER = if (timeout_ns) |ns| t: {
+ interval = -@as(i64, @intCast(@min(ns / 100, std.math.maxInt(i64))));
+ break :t &interval;
+ } else null;
+ _ = ntdll.RtlWaitOnAddress(ptr, &expected, @sizeOf(u32), timeout);
+}
+
+pub fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void {
+ @branchHint(.cold);
+ _ = userdata;
+ if (builtin.single_threaded) return; // nothing to wake up
+ if (max_waiters > 1) ntdll.RtlWakeAddressAll(ptr) else ntdll.RtlWakeAddressSingle(ptr);
+}
+
+test "windows: path conversion" {
+ const testing = std.testing;
+ const L = std.unicode.utf8ToUtf16LeStringLiteral;
+ const cwd = Dir.cwd().handle;
+
+ // Absolute Win32 paths are canonicalized and NT-prefixed, with either
+ // separator and `..` resolved.
+ var p: WindowsPath = undefined;
+ try pathToNt(cwd, "C:/foo/../bar", &p);
+ try testing.expectEqualSlices(u16, L("\\??\\C:\\bar"), p.span());
+ try pathToNt(cwd, "\\\\server\\share\\x\\..\\y", &p);
+ try testing.expectEqualSlices(u16, L("\\??\\UNC\\server\\share\\y"), p.span());
+ try pathToNt(cwd, "\\\\?\\C:\\x", &p);
+ try testing.expectEqualSlices(u16, L("\\??\\C:\\x"), p.span());
+ try pathToNt(cwd, "\\\\.\\pipe\\x", &p);
+ try testing.expectEqualSlices(u16, L("\\??\\pipe\\x"), p.span());
+
+ // NT paths pass through untouched.
+ try pathToNt(cwd, "\\??\\C:\\x", &p);
+ try testing.expectEqualSlices(u16, L("\\??\\C:\\x"), p.span());
+
+ // Relative paths stay relative to the directory handle, normalized.
+ try pathToNt(cwd, "a/../b\\c", &p);
+ try testing.expectEqualSlices(u16, L("b\\c"), p.span());
+ try testing.expect(!p.isNt());
+
+ // Relative paths that climb out of the directory resolve against the
+ // handle's real path (or the working directory for cwd).
+ try pathToNt(cwd, "..\\up.txt", &p);
+ try testing.expect(p.isNt());
+ try testing.expect(std.mem.endsWith(u16, p.span(), L("\\up.txt")));
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ var dir_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const dir_path = dir_buf[0..try tmp_dir.dir.realPath(testing.io, &dir_buf)];
+ const parent = std.fs.path.dirname(dir_path).?;
+ try pathToNt(tmp_dir.dir.handle, "..\\up.txt", &p);
+ var got_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const got = got_buf[0..std.unicode.wtf16LeToWtf8(&got_buf, p.span())];
+ try testing.expect(std.mem.startsWith(u8, got, "\\??\\"));
+ try testing.expectEqualStrings(parent, got[4 .. 4 + parent.len]);
+ try testing.expectEqualStrings("\\up.txt", got[4 + parent.len ..]);
+
+ // Encoding errors are path errors.
+ try testing.expectError(error.BadPathName, pathToNt(cwd, "bad\xff", &p));
+ try testing.expectError(error.NameTooLong, pathToNt(cwd, "a" ** (windows.PATH_MAX_WIDE + 1), &p));
+}
+
+test "windows: realPath resolves through symlinks" {
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "target.txt",
+ .data = "target",
+ });
+ // Needs SeCreateSymbolicLinkPrivilege, which plain users and CI lack.
+ tmp_dir.dir.symLink(testing.io, "target.txt", "link.txt", .{}) catch |err| switch (err) {
+ error.AccessDenied => return error.SkipZigTest,
+ else => return err,
+ };
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+
+ // Following the link (the default) opens the target and realPath
+ // resolves to it, which is what the Kitty path validation relies on.
+ var file = try dir.openFile(test_io, "link.txt", .{});
+ defer file.close(test_io);
+ try testing.expectEqual(@as(u64, "target".len), try file.length(test_io));
+ try testing.expectEqual(File.Kind.file, (try file.stat(test_io)).kind);
+ var path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = path_buf[0..try file.realPath(test_io, &path_buf)];
+ try testing.expect(std.mem.endsWith(u8, path, "target.txt"));
+
+ // Opening the link itself reports a symlink.
+ var link = try dir.openFile(test_io, "link.txt", .{ .follow_symlinks = false });
+ defer link.close(test_io);
+ try testing.expectEqual(File.Kind.sym_link, (try link.stat(test_io)).kind);
+
+ // dirRealPathFile through the link resolves the target too.
+ var path_buf2: [std.fs.max_path_bytes]u8 = undefined;
+ const path2 = path_buf2[0..try dir.realPathFile(test_io, "link.txt", &path_buf2)];
+ try testing.expectEqualStrings(path, path2);
+}