aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/lib
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
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/lib')
-rw-r--r--vendor/libghostty-vt/src/lib/TinyIo.zig269
-rw-r--r--vendor/libghostty-vt/src/lib/allocator.zig8
-rw-r--r--vendor/libghostty-vt/src/lib/allocator/convenience.zig59
-rw-r--r--vendor/libghostty-vt/src/lib/allocator/wasm.zig98
-rw-r--r--vendor/libghostty-vt/src/lib/c_abi.zig28
-rw-r--r--vendor/libghostty-vt/src/lib/compat/README.md37
-rw-r--r--vendor/libghostty-vt/src/lib/compat/fd.zig83
-rw-r--r--vendor/libghostty-vt/src/lib/compat/file.zig56
-rw-r--r--vendor/libghostty-vt/src/lib/compat/reader.zig64
-rw-r--r--vendor/libghostty-vt/src/lib/compat/testing.zig19
-rw-r--r--vendor/libghostty-vt/src/lib/compat/thread.zig75
-rw-r--r--vendor/libghostty-vt/src/lib/enum.zig99
-rw-r--r--vendor/libghostty-vt/src/lib/main.zig5
-rw-r--r--vendor/libghostty-vt/src/lib/packed.zig290
-rw-r--r--vendor/libghostty-vt/src/lib/string.zig15
-rw-r--r--vendor/libghostty-vt/src/lib/struct.zig24
-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
-rw-r--r--vendor/libghostty-vt/src/lib/types.zig17
-rw-r--r--vendor/libghostty-vt/src/lib/union.zig126
21 files changed, 3394 insertions, 167 deletions
diff --git a/vendor/libghostty-vt/src/lib/TinyIo.zig b/vendor/libghostty-vt/src/lib/TinyIo.zig
new file mode 100644
index 00000000..118af197
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/TinyIo.zig
@@ -0,0 +1,269 @@
+//! TinyIo: a tiny, blocking `std.Io` implementation optimized for
+//! binary size.
+//!
+//! Compared to `std.Io.Threaded`, the binary cost is roughly ~100KB to
+//! ~200KB (macOS vs Linux) smaller, and the runtime cost is ~300KB (256KB
+//! of TLS plus the ~20KB threaded structure) smaller. On Windows the
+//! DLL is ~370KB smaller (ReleaseFast) because Threaded's Windows logic
+//! has Winsock, AFD and process creation.
+//!
+//! Utilizing the built-in Zig `std.Io.Threaded` is easy but due to its
+//! vtable architecture, the linker can't prune uncalled functions, meaning
+//! you have to pay for all the code for every op. Plus, at runtime, Threaded
+//! requires 256KB of thread-local storage, plus the structure itself is
+//! very large.
+//!
+//! TinyIo implements exactly the operations we need as plain blocking
+//! syscalls. It doesn't support cancelation. It doesn't support concurrency
+//! operations, but all our APIs are direct syscalls so it supports the
+//! concurrency of calls that those syscalls support (which is usually
+//! safe on POSIX systems).
+//!
+//! The platform arms live in `tinyio/`: `posix.zig` is direct syscalls
+//! cribbed from Zig's std.posix, and `windows.zig` is direct ntdll and
+//! kernel32 calls modeled on the Windows arms of `std.Io.Threaded`, so
+//! the library never links Threaded's vtable there either. Shared tests
+//! are in `tinyio/test.zig`.
+
+const TinyIo = @This();
+
+const std = @import("std");
+const builtin = @import("builtin");
+const Io = std.Io;
+
+/// True if this platform has a real TinyIo implementation. On
+/// unsupported platforms `io()` still works but every operation fails
+/// like `std.Io.failing`.
+pub const supported: bool = switch (builtin.os.tag) {
+ .wasi, .freestanding, .other, .uefi => false,
+ else => true,
+};
+
+/// Initialize a TinyIo. TinyIo is stateless (zero-sized), so this exists
+/// purely to mirror the `std.Io.Threaded` initialization and usage shape,
+/// making the two easy to swap.
+pub const init: TinyIo = .{};
+
+/// Returns the `std.Io` interface for this implementation.
+pub fn io(self: TinyIo) Io {
+ _ = self;
+ return .{
+ .userdata = null,
+ .vtable = &vtable,
+ };
+}
+
+const vtable: Io.VTable = if (!supported) std.Io.failing.vtable.* else .{
+ .crashHandler = Io.noCrashHandler,
+
+ // `noAsync` runs the task synchronously and returns null, which means
+ // `await`/`cancel` can never be called. `concurrent` reports that
+ // concurrency is unavailable, which callers must handle anyway.
+ .async = Io.noAsync,
+ .concurrent = Io.failingConcurrent,
+ .await = Io.unreachableAwait,
+ .cancel = Io.unreachableCancel,
+
+ .groupAsync = Io.noGroupAsync,
+ .groupConcurrent = Io.failingGroupConcurrent,
+ .groupAwait = Io.unreachableGroupAwait,
+ .groupCancel = Io.unreachableGroupCancel,
+
+ // Cancelation is never requested (there is no async), but these are
+ // benign no-ops instead of `unreachable` since generic std code may
+ // toggle cancel protection around operations.
+ .recancel = recancel,
+ .swapCancelProtection = swapCancelProtection,
+ .checkCancel = checkCancel,
+
+ // Real futexes: `std.Io.Mutex` (used by kitty graphics storage) parks
+ // on these when contended.
+ .futexWait = futexWait,
+ .futexWaitUncancelable = futexWaitUncancelable,
+ .futexWake = futexWake,
+
+ // `operate` carries the streaming file reads used by `File.Reader`.
+ .operate = operate,
+ .batchAwaitAsync = Io.unreachableBatchAwaitAsync,
+ .batchAwaitConcurrent = Io.unreachableBatchAwaitConcurrent,
+ .batchCancel = Io.unreachableBatchCancel,
+
+ .dirCreateDir = Io.failingDirCreateDir,
+ .dirCreateDirPath = Io.failingDirCreateDirPath,
+ .dirCreateDirPathOpen = Io.failingDirCreateDirPathOpen,
+ .dirOpenDir = Io.failingDirOpenDir,
+ .dirStat = Io.failingDirStat,
+ .dirStatFile = Io.failingDirStatFile,
+ .dirAccess = Io.failingDirAccess,
+ .dirCreateFile = Io.failingDirCreateFile,
+ .dirCreateFileAtomic = Io.failingDirCreateFileAtomic,
+ .dirOpenFile = impl.dirOpenFile,
+ .dirClose = impl.dirClose,
+ .dirRead = Io.noDirRead,
+ .dirRealPath = Io.failingDirRealPath,
+ .dirRealPathFile = impl.dirRealPathFile,
+ .dirDeleteFile = impl.dirDeleteFile,
+ .dirDeleteDir = Io.failingDirDeleteDir,
+ .dirRename = Io.failingDirRename,
+ .dirRenamePreserve = Io.failingDirRenamePreserve,
+ .dirSymLink = Io.failingDirSymLink,
+ .dirReadLink = Io.failingDirReadLink,
+ .dirSetOwner = Io.failingDirSetOwner,
+ .dirSetFileOwner = Io.failingDirSetFileOwner,
+ .dirSetPermissions = Io.failingDirSetPermissions,
+ .dirSetFilePermissions = Io.failingDirSetFilePermissions,
+ .dirSetTimestamps = Io.noDirSetTimestamps,
+ .dirHardLink = Io.failingDirHardLink,
+
+ .fileStat = impl.fileStat,
+ .fileLength = impl.fileLength,
+ .fileClose = impl.fileClose,
+ .fileWritePositional = Io.failingFileWritePositional,
+ .fileWriteFileStreaming = Io.noFileWriteFileStreaming,
+ .fileWriteFilePositional = Io.noFileWriteFilePositional,
+ .fileReadPositional = impl.fileReadPositional,
+ .fileSeekBy = impl.fileSeekBy,
+ .fileSeekTo = impl.fileSeekTo,
+ .fileSync = Io.failingFileSync,
+ .fileIsTty = Io.unreachableFileIsTty,
+ .fileEnableAnsiEscapeCodes = Io.unreachableFileEnableAnsiEscapeCodes,
+ .fileSupportsAnsiEscapeCodes = Io.unreachableFileSupportsAnsiEscapeCodes,
+ .fileSetLength = Io.failingFileSetLength,
+ .fileSetOwner = Io.failingFileSetOwner,
+ .fileSetPermissions = Io.failingFileSetPermissions,
+ .fileSetTimestamps = Io.noFileSetTimestamps,
+ .fileLock = Io.failingFileLock,
+ .fileTryLock = Io.failingFileTryLock,
+ .fileUnlock = Io.unreachableFileUnlock,
+ .fileDowngradeLock = Io.failingFileDowngradeLock,
+ .fileRealPath = impl.fileRealPath,
+ .fileHardLink = Io.failingFileHardLink,
+
+ .fileMemoryMapCreate = Io.failingFileMemoryMapCreate,
+ .fileMemoryMapDestroy = Io.unreachableFileMemoryMapDestroy,
+ .fileMemoryMapSetLength = Io.unreachableFileMemoryMapSetLength,
+ .fileMemoryMapRead = Io.unreachableFileMemoryMapRead,
+ .fileMemoryMapWrite = Io.unreachableFileMemoryMapWrite,
+
+ .processExecutableOpen = Io.failingProcessExecutableOpen,
+ .processExecutablePath = Io.failingProcessExecutablePath,
+ .lockStderr = Io.unreachableLockStderr,
+ .tryLockStderr = Io.noTryLockStderr,
+ .unlockStderr = Io.unreachableUnlockStderr,
+ .processCurrentPath = Io.failingProcessCurrentPath,
+ .processSetCurrentDir = Io.failingProcessSetCurrentDir,
+ .processSetCurrentPath = Io.failingProcessSetCurrentPath,
+ .processReplace = Io.failingProcessReplace,
+ .processReplacePath = Io.failingProcessReplacePath,
+ .processSpawn = Io.failingProcessSpawn,
+ .processSpawnPath = Io.failingProcessSpawnPath,
+ .childWait = Io.unreachableChildWait,
+ .childKill = Io.unreachableChildKill,
+
+ .progressParentFile = Io.failingProgressParentFile,
+
+ .random = Io.noRandom,
+ .randomSecure = randomSecure,
+
+ .now = Io.noNow,
+ .clockResolution = Io.failingClockResolution,
+ .sleep = Io.noSleep,
+
+ .netListenIp = Io.failingNetListenIp,
+ .netAccept = Io.failingNetAccept,
+ .netBindIp = Io.failingNetBindIp,
+ .netConnectIp = Io.failingNetConnectIp,
+ .netListenUnix = Io.failingNetListenUnix,
+ .netConnectUnix = Io.failingNetConnectUnix,
+ .netSocketCreatePair = Io.failingNetSocketCreatePair,
+ .netSend = Io.failingNetSend,
+ .netRead = Io.failingNetRead,
+ .netWrite = Io.failingNetWrite,
+ .netWriteFile = Io.failingNetWriteFile,
+ .netClose = Io.unreachableNetClose,
+ .netShutdown = Io.failingNetShutdown,
+ .netInterfaceNameResolve = Io.failingNetInterfaceNameResolve,
+ .netInterfaceName = Io.unreachableNetInterfaceName,
+ .netLookup = Io.failingNetLookup,
+};
+
+/// The platform arm behind the vtable. Each arm exports the same set of
+/// operations; only the selected one is ever analyzed, and `supported`
+/// gates the vtable so unsupported targets never resolve either.
+const impl = switch (builtin.os.tag) {
+ .windows => @import("tinyio/windows.zig"),
+ else => @import("tinyio/posix.zig"),
+};
+
+fn recancel(_: ?*anyopaque) void {}
+
+fn swapCancelProtection(
+ _: ?*anyopaque,
+ new: Io.CancelProtection,
+) Io.CancelProtection {
+ // Stateless: there is no async, so cancelation can never be requested
+ // and the protection state is meaningless. Callers save this return
+ // value only to restore it via another call to this function.
+ _ = new;
+ return .blocked;
+}
+
+fn checkCancel(_: ?*anyopaque) Io.Cancelable!void {}
+
+fn randomSecure(_: ?*anyopaque, buffer: []u8) Io.RandomSecureError!void {
+ if (buffer.len == 0) return;
+ return impl.randomSecure(buffer);
+}
+
+fn operate(
+ userdata: ?*anyopaque,
+ operation: Io.Operation,
+) Io.Cancelable!Io.Operation.Result {
+ switch (operation) {
+ // Streaming file reads are how `File.Reader` consumes files that
+ // don't support positional reads (and the fallback path in
+ // general).
+ .file_read_streaming => |o| return .{
+ .file_read_streaming = impl.fileReadStreaming(o.file, o.data),
+ },
+
+ // Everything else (streaming writes, ioctls, socket receives) is
+ // unused by the terminal; fail like `Io.failing` does.
+ else => return Io.failingOperate(userdata, operation),
+ }
+}
+
+/// Convert an `Io.Timeout` to relative nanoseconds without consulting a
+/// clock. Deadlines can't be resolved without `now` support; treat them
+/// as a short poll, which is valid because futex waits are allowed to
+/// wake spuriously (callers must re-check their condition and retry).
+fn timeoutToNs(timeout: Io.Timeout) ?u64 {
+ return switch (timeout) {
+ .none => null,
+ .duration => |d| @intCast(@max(0, d.raw.toNanoseconds())),
+ .deadline => 10 * std.time.ns_per_ms,
+ };
+}
+
+fn futexWait(
+ userdata: ?*anyopaque,
+ ptr: *const u32,
+ expected: u32,
+ timeout: Io.Timeout,
+) Io.Cancelable!void {
+ _ = userdata;
+ impl.futexWaitInner(ptr, expected, timeoutToNs(timeout));
+}
+
+fn futexWaitUncancelable(userdata: ?*anyopaque, ptr: *const u32, expected: u32) void {
+ _ = userdata;
+ impl.futexWaitInner(ptr, expected, null);
+}
+
+fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void {
+ return impl.futexWake(userdata, ptr, max_waiters);
+}
+
+test {
+ _ = @import("tinyio/test.zig");
+}
diff --git a/vendor/libghostty-vt/src/lib/allocator.zig b/vendor/libghostty-vt/src/lib/allocator.zig
index fc56033a..de343dab 100644
--- a/vendor/libghostty-vt/src/lib/allocator.zig
+++ b/vendor/libghostty-vt/src/lib/allocator.zig
@@ -2,8 +2,8 @@ const std = @import("std");
const builtin = @import("builtin");
const testing = std.testing;
-/// Convenience functions
-pub const convenience = @import("allocator/convenience.zig");
+/// Wasm-specific allocation helpers.
+pub const wasm = @import("allocator/wasm.zig");
/// Useful alias since they're required to create Zig allocators
pub const ZigVTable = std.mem.Allocator.VTable;
@@ -34,6 +34,10 @@ pub fn default(c_alloc_: ?*const Allocator) std.mem.Allocator {
// Wasm
if (comptime builtin.target.cpu.arch.isWasm()) return std.heap.wasm_allocator;
+ // Freestanding targets don't have a default heap. Using the failing
+ // allocator makes a missing allocator show up as out-of-memory.
+ if (comptime builtin.os.tag == .freestanding) return std.mem.Allocator.failing;
+
// No libc, use the preferred allocator for releases which is the
// Zig SMP allocator.
return std.heap.smp_allocator;
diff --git a/vendor/libghostty-vt/src/lib/allocator/convenience.zig b/vendor/libghostty-vt/src/lib/allocator/convenience.zig
deleted file mode 100644
index 0f5f88d2..00000000
--- a/vendor/libghostty-vt/src/lib/allocator/convenience.zig
+++ /dev/null
@@ -1,59 +0,0 @@
-//! This contains convenience functions for allocating various types.
-//!
-//! The primary use case for this is Wasm builds. Ghostty relies a lot on
-//! pointers to various types for ABI compatibility and creating those pointers
-//! in Wasm is tedious. This file contains a purely additive set of functions
-//! that can be exposed to the Wasm module without changing the API from the
-//! C library.
-//!
-//! Given these are convenience methods, they always use the default allocator.
-//! If a caller is using a custom allocator, they have the expertise to
-//! allocate these types manually using their custom allocator.
-
-// Get our default allocator at comptime since it is known.
-const default = @import("../allocator.zig").default;
-const alloc = default(null);
-
-pub const Opaque = *anyopaque;
-
-pub fn allocOpaque() callconv(.c) ?*Opaque {
- return alloc.create(*anyopaque) catch return null;
-}
-
-pub fn freeOpaque(ptr: ?*Opaque) callconv(.c) void {
- if (ptr) |p| alloc.destroy(p);
-}
-
-pub fn allocU8Array(len: usize) callconv(.c) ?[*]u8 {
- const slice = alloc.alloc(u8, len) catch return null;
- return slice.ptr;
-}
-
-pub fn freeU8Array(ptr: ?[*]u8, len: usize) callconv(.c) void {
- if (ptr) |p| alloc.free(p[0..len]);
-}
-
-pub fn allocU16Array(len: usize) callconv(.c) ?[*]u16 {
- const slice = alloc.alloc(u16, len) catch return null;
- return slice.ptr;
-}
-
-pub fn freeU16Array(ptr: ?[*]u16, len: usize) callconv(.c) void {
- if (ptr) |p| alloc.free(p[0..len]);
-}
-
-pub fn allocU8() callconv(.c) ?*u8 {
- return alloc.create(u8) catch return null;
-}
-
-pub fn freeU8(ptr: ?*u8) callconv(.c) void {
- if (ptr) |p| alloc.destroy(p);
-}
-
-pub fn allocUsize() callconv(.c) ?*usize {
- return alloc.create(usize) catch return null;
-}
-
-pub fn freeUsize(ptr: ?*usize) callconv(.c) void {
- if (ptr) |p| alloc.destroy(p);
-}
diff --git a/vendor/libghostty-vt/src/lib/allocator/wasm.zig b/vendor/libghostty-vt/src/lib/allocator/wasm.zig
new file mode 100644
index 00000000..22439103
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/allocator/wasm.zig
@@ -0,0 +1,98 @@
+//! Wasm allocation conveniences for caller-owned storage and opaque out slots.
+//!
+//! The primary use case for this is Wasm builds. Ghostty relies a lot on
+//! pointers to various types for ABI compatibility and creating those pointers
+//! in Wasm is tedious. This file contains the small set of functions exposed by
+//! the Wasm module without changing the API from the C library.
+//!
+//! Given these are convenience methods, they always use the default allocator.
+//! If a caller is using a custom allocator, they have the expertise to
+//! allocate these types manually using their custom allocator.
+
+const std = @import("std");
+const c_abi = @import("../c_abi.zig");
+
+// Get our default allocator at comptime since it is known.
+const default = @import("../allocator.zig").default;
+const alloc = default(null);
+const wasm_alignment: std.mem.Alignment = .fromByteUnits(c_abi.max_alignment);
+
+/// A nullable opaque C handle stored in a constructor out-parameter slot.
+pub const Opaque = ?*anyopaque;
+
+/// Allocate `len` bytes of uninitialized, caller-owned Wasm ABI storage.
+///
+/// The returned pointer is aligned for any fundamental C ABI type and must be
+/// released with `freeBytes` using the same `len`. Returns null when `len` is
+/// zero or allocation fails.
+pub fn allocBytes(len: usize) callconv(.c) ?[*]u8 {
+ if (len == 0) return null;
+ return alloc.rawAlloc(len, wasm_alignment, @returnAddress());
+}
+
+/// Release storage returned by `allocBytes`.
+///
+/// `len` must exactly match the allocation length. A null pointer is ignored.
+pub fn freeBytes(ptr: ?[*]u8, len: usize) callconv(.c) void {
+ const p = ptr orelse return;
+ if (len == 0) return;
+ alloc.rawFree(p[0..len], wasm_alignment, @returnAddress());
+}
+
+/// Allocate a null-initialized slot for an opaque constructor out-parameter.
+///
+/// The slot may be reused after each value is removed with `takeOpaque`. It
+/// must eventually be released with `freeOpaque`.
+pub fn allocOpaque() callconv(.c) ?*Opaque {
+ const ptr = allocBytes(@sizeOf(Opaque)) orelse return null;
+ const result: *Opaque = @ptrCast(@alignCast(ptr));
+ result.* = null;
+ return result;
+}
+
+/// Release a slot returned by `allocOpaque` without freeing its stored handle.
+///
+/// Call the handle's type-specific destructor before freeing a populated slot.
+/// A null slot pointer is ignored.
+pub fn freeOpaque(ptr: ?*Opaque) callconv(.c) void {
+ freeBytes(@ptrCast(ptr), @sizeOf(Opaque));
+}
+
+/// Remove and return the handle in an opaque out-parameter slot.
+///
+/// The slot is reset to null so it can be safely reused for another
+/// constructor. This does not free either the handle or the slot. Returns null
+/// when the slot pointer is null or the slot is empty.
+pub fn takeOpaque(ptr: ?*Opaque) callconv(.c) Opaque {
+ const p = ptr orelse return null;
+ const result = p.*;
+ p.* = null;
+ return result;
+}
+
+test "Wasm allocation" {
+ const ptr = allocBytes(1) orelse return error.OutOfMemory;
+ defer freeBytes(ptr, 1);
+
+ try std.testing.expectEqual(
+ @as(usize, 0),
+ @intFromPtr(ptr) % c_abi.max_alignment,
+ );
+ ptr[0] = 42;
+ try std.testing.expectEqual(@as(u8, 42), ptr[0]);
+
+ try std.testing.expect(allocBytes(0) == null);
+ freeBytes(null, 0);
+}
+
+test "opaque slots are initialized and reusable" {
+ const slot = allocOpaque() orelse return error.OutOfMemory;
+ defer freeOpaque(slot);
+
+ try std.testing.expect(takeOpaque(slot) == null);
+
+ var value: u8 = 42;
+ slot.* = &value;
+ try std.testing.expectEqual(@as(?*anyopaque, &value), takeOpaque(slot));
+ try std.testing.expect(takeOpaque(slot) == null);
+}
diff --git a/vendor/libghostty-vt/src/lib/c_abi.zig b/vendor/libghostty-vt/src/lib/c_abi.zig
new file mode 100644
index 00000000..3ce34754
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/c_abi.zig
@@ -0,0 +1,28 @@
+//! Memory-layout properties shared with C API callers.
+//!
+//! The C ABI defines how values are represented in memory so code written in
+//! different languages can safely exchange them. One part of that contract is
+//! alignment: some values must begin at an address divisible by 2, 4, 8, or
+//! another power of two. Reading a value from a less-aligned address can be
+//! slow on some CPUs and invalid on others.
+//!
+//! This module derives those properties from Zig's compile-time target data.
+//! It does not call or link libc. Keeping the calculation here gives allocators
+//! and ABI metadata one source of truth.
+
+const std = @import("std");
+const builtin = @import("builtin");
+
+/// Largest address alignment required by a fundamental C ABI type, in bytes.
+///
+/// For example, a value of 16 means storage intended to hold an arbitrary C
+/// value must begin at an address evenly divisible by 16. The Wasm allocator
+/// uses this value for caller-owned ABI storage, and `ghostty_type_json`
+/// publishes it so hosts know the guarantee made by that allocator.
+pub const max_alignment: u16 = max: {
+ var result: u16 = @alignOf(*anyopaque);
+ for (std.enums.values(std.Target.CType)) |c_type| {
+ result = @max(result, builtin.target.cTypeAlignment(c_type));
+ }
+ break :max result;
+};
diff --git a/vendor/libghostty-vt/src/lib/compat/README.md b/vendor/libghostty-vt/src/lib/compat/README.md
new file mode 100644
index 00000000..22f58c10
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/compat/README.md
@@ -0,0 +1,37 @@
+# Zig Compatibility Libraries
+
+The following directory contains code that either used to exist in the Zig
+standard library, or currently exists in Zig master that has been deemed
+useful to have ahead of a full release.
+
+Code located here should likely be eventually phased out, or moved to a more
+logical location if it's decided it would be useful to keep.
+
+## Licenses
+
+Zig code taken from the standard library is used under the terms of the MIT
+license. The license body follows.
+
+```
+The MIT License (Expat)
+
+Copyright (c) Zig contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
diff --git a/vendor/libghostty-vt/src/lib/compat/fd.zig b/vendor/libghostty-vt/src/lib/compat/fd.zig
new file mode 100644
index 00000000..3bef7414
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/compat/fd.zig
@@ -0,0 +1,83 @@
+//! Code taken from 0.15.2 and `std.fs.file`. See README.md for license and
+//! details.
+const std = @import("std");
+
+pub const PipeError = error{
+ SystemFdQuotaExceeded,
+ ProcessFdQuotaExceeded,
+} || std.posix.UnexpectedError;
+
+pub fn close(fd: std.posix.fd_t) void {
+ switch (std.posix.errno(std.posix.system.close(fd))) {
+ .BADF => unreachable, // Always a race condition
+ else => {}, // Includes EINTR, see std.posix source in 0.15.2
+ }
+}
+
+pub fn pipe() PipeError![2]std.posix.fd_t {
+ var fds: [2]std.posix.fd_t = undefined;
+ switch (std.posix.errno(std.posix.system.pipe(&fds))) {
+ .SUCCESS => return fds,
+ .INVAL => unreachable, // Invalid parameters to pipe()
+ .FAULT => unreachable, // Invalid fds pointer
+ .NFILE => return error.SystemFdQuotaExceeded,
+ .MFILE => return error.ProcessFdQuotaExceeded,
+ else => |err| return std.posix.unexpectedErrno(err),
+ }
+}
+
+pub fn pipe2(flags: std.posix.O) PipeError![2]std.posix.fd_t {
+ if (@TypeOf(std.posix.system.pipe2) != void) {
+ var fds: [2]std.posix.fd_t = undefined;
+ switch (std.posix.errno(std.posix.system.pipe2(&fds, flags))) {
+ .SUCCESS => return fds,
+ .INVAL => unreachable, // Invalid flags
+ .FAULT => unreachable, // Invalid fds pointer
+ .NFILE => return error.SystemFdQuotaExceeded,
+ .MFILE => return error.ProcessFdQuotaExceeded,
+ else => |err| return std.posix.unexpectedErrno(err),
+ }
+ }
+
+ const fds: [2]std.posix.fd_t = try pipe();
+ errdefer {
+ close(fds[0]);
+ close(fds[1]);
+ }
+
+ // https://github.com/ziglang/zig/issues/18882
+ if (@as(u32, @bitCast(flags)) == 0)
+ return fds;
+
+ // CLOEXEC is special, it's a file descriptor flag and must be set using
+ // F.SETFD.
+ if (flags.CLOEXEC) {
+ for (fds) |fd| {
+ switch (std.posix.errno(std.posix.system.fcntl(fd, std.posix.F.SETFD, @as(u32, std.posix.FD_CLOEXEC)))) {
+ .SUCCESS => {},
+ .INVAL => unreachable, // Invalid flags
+ .BADF => unreachable, // Always a race condition
+ else => |err| return std.posix.unexpectedErrno(err),
+ }
+ }
+ }
+
+ const new_flags: u32 = f: {
+ var new_flags = flags;
+ new_flags.CLOEXEC = false;
+ break :f @bitCast(new_flags);
+ };
+ // Set every other flag affecting the file status using F.SETFL.
+ if (new_flags != 0) {
+ for (fds) |fd| {
+ switch (std.posix.errno(std.posix.system.fcntl(fd, std.posix.F.SETFL, new_flags))) {
+ .SUCCESS => {},
+ .INVAL => unreachable, // Invalid flags
+ .BADF => unreachable, // Always a race condition
+ else => |err| return std.posix.unexpectedErrno(err),
+ }
+ }
+ }
+
+ return fds;
+}
diff --git a/vendor/libghostty-vt/src/lib/compat/file.zig b/vendor/libghostty-vt/src/lib/compat/file.zig
new file mode 100644
index 00000000..efb31a52
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/compat/file.zig
@@ -0,0 +1,56 @@
+//! Code taken from 0.15.2 and `std.fs.file`. See README.md for license and
+//! details.
+const builtin = @import("builtin");
+const std = @import("std");
+const global = @import("../../global.zig");
+
+pub const ReadToEndAllocError = error{FileTooBig} ||
+ std.Io.File.ReadStreamingError ||
+ std.mem.Allocator.Error;
+
+/// Read the file from its current position through end-of-stream, returning
+/// `error.FileTooBig` if the result exceeds `max_bytes`.
+///
+/// Caller owns the memory.
+pub fn readToEndAlloc(file: std.Io.File, alloc: std.mem.Allocator, max_bytes: usize) ReadToEndAllocError![]u8 {
+ var read_buf: [4096]u8 = undefined;
+ var result: std.ArrayList(u8) = .empty;
+ defer result.deinit(alloc);
+
+ while (true) {
+ const n = file.readStreaming(global.io(), &.{&read_buf}) catch |err| switch (err) {
+ error.EndOfStream => break,
+ else => |e| return e,
+ };
+ if (n == 0) continue;
+ if (n > max_bytes - result.items.len) return error.FileTooBig;
+ try result.appendSlice(alloc, read_buf[0..n]);
+ }
+
+ return result.toOwnedSlice(alloc);
+}
+
+test "readToEndAlloc reads through EOF and permits exact limit" {
+ const testing = std.testing;
+ const contents = "hello, world";
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "data",
+ .data = contents,
+ });
+
+ const file = try tmp_dir.dir.openFile(testing.io, "data", .{});
+ defer file.close(testing.io);
+ const result = try readToEndAlloc(file, testing.allocator, contents.len);
+ defer testing.allocator.free(result);
+ try testing.expectEqualStrings(contents, result);
+
+ const too_big_file = try tmp_dir.dir.openFile(testing.io, "data", .{});
+ defer too_big_file.close(testing.io);
+ try testing.expectError(
+ error.FileTooBig,
+ readToEndAlloc(too_big_file, testing.allocator, contents.len - 1),
+ );
+}
diff --git a/vendor/libghostty-vt/src/lib/compat/reader.zig b/vendor/libghostty-vt/src/lib/compat/reader.zig
new file mode 100644
index 00000000..28813440
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/compat/reader.zig
@@ -0,0 +1,64 @@
+//! Code taken from older reader implementations, still may be useful to keep
+//! around in general. See README.md for license and details.
+const std = @import("std");
+
+/// Pulled from old stdlib as this function has been removed.
+pub fn readSkipUntilDelimiterOrEof(reader: *std.Io.Reader, delimiter: u8) std.Io.Reader.Error!void {
+ while (true) {
+ const byte = readByte(reader) catch |err| switch (err) {
+ error.EndOfStream => return,
+ else => |e| return e,
+ };
+ if (byte == delimiter) return;
+ }
+}
+
+/// Pulled from old stdlib as this function has been removed.
+pub fn readStructEndian(
+ reader: *std.Io.Reader,
+ comptime T: type,
+ endian: std.builtin.Endian,
+) std.Io.Reader.Error!T {
+ var result: [1]T = undefined;
+ try reader.readSliceEndian(T, &result, endian);
+ return result[0];
+}
+
+/// Pulled from old stdlib as this function has been removed.
+pub fn readerInt(reader: *std.Io.Reader, comptime T: type, endian: std.builtin.Endian) std.Io.Reader.Error!T {
+ const bytes = try readBytesNoEof(reader, @divExact(@typeInfo(T).int.bits, 8));
+ return std.mem.readInt(T, &bytes, endian);
+}
+
+/// Pulled from old stdlib as this function has been removed.
+pub fn readByteSigned(reader: *std.Io.Reader) std.Io.Reader.Error!i8 {
+ return @as(i8, @bitCast(try readByte(reader)));
+}
+
+/// Pulled from old stdlib as this function has been removed.
+pub fn readByte(reader: *std.Io.Reader) std.Io.Reader.Error!u8 {
+ var result: [1]u8 = undefined;
+ try reader.readSliceAll(&result);
+ return result[0];
+}
+
+/// Pulled from old stdlib as this function has been removed.
+pub fn readSkipBytes(reader: *std.Io.Reader, num_bytes: u64) std.Io.Reader.Error!void {
+ const buf_size = 512;
+
+ var buf: [buf_size]u8 = undefined;
+ var remaining = num_bytes;
+
+ while (remaining > 0) {
+ const amt = @min(remaining, buf_size);
+ try reader.readSliceAll(buf[0..amt]);
+ remaining -= amt;
+ }
+}
+
+/// Pulled from old stdlib as this function has been removed.
+fn readBytesNoEof(reader: *std.Io.Reader, comptime num_bytes: usize) std.Io.Reader.Error![num_bytes]u8 {
+ var bytes: [num_bytes]u8 = undefined;
+ try reader.readSliceAll(&bytes);
+ return bytes;
+}
diff --git a/vendor/libghostty-vt/src/lib/compat/testing.zig b/vendor/libghostty-vt/src/lib/compat/testing.zig
new file mode 100644
index 00000000..e826eb40
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/compat/testing.zig
@@ -0,0 +1,19 @@
+//! Code taken from 0.15.2 `std.testing`. See README.md for license and
+//! details.
+const builtin = @import("builtin");
+const std = @import("std");
+
+/// Given a type, recursively references all the declarations inside, so that the semantic analyzer sees them.
+/// For deep types, you may use `@setEvalBranchQuota`.
+pub fn refAllDeclsRecursive(comptime T: type) void {
+ if (!builtin.is_test) return;
+ inline for (comptime std.meta.declarations(T)) |decl| {
+ if (@TypeOf(@field(T, decl.name)) == type) {
+ switch (@typeInfo(@field(T, decl.name))) {
+ .@"struct", .@"enum", .@"union", .@"opaque" => refAllDeclsRecursive(@field(T, decl.name)),
+ else => {},
+ }
+ }
+ _ = &@field(T, decl.name);
+ }
+}
diff --git a/vendor/libghostty-vt/src/lib/compat/thread.zig b/vendor/libghostty-vt/src/lib/compat/thread.zig
new file mode 100644
index 00000000..890c02f0
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/compat/thread.zig
@@ -0,0 +1,75 @@
+//! Code taken from 0.17.0 `std.Io.Condition`. See README.md for license and
+//! details.
+//!
+//! FIXME: Should be able to remove after 0.17.0, see:
+//! https://codeberg.org/ziglang/zig/pulls/31278
+const builtin = @import("builtin");
+const std = @import("std");
+const assert = @import("../../quirks.zig").inlineAssert;
+
+pub const WaitTimeoutError = std.Io.Cancelable || std.Io.Timeout.Error;
+
+/// Blocks until the condition is signaled, canceled, or the provided
+/// timeout expires.
+///
+/// See also:
+/// * `wait`
+/// * `waitUncancelable`
+pub fn waitTimeout(
+ cond: *std.Io.Condition,
+ io: std.Io,
+ mutex: *std.Io.Mutex,
+ timeout: std.Io.Timeout,
+) WaitTimeoutError!void {
+ const deadline = timeout.toDeadline(io);
+
+ var epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before state load
+
+ {
+ const prev_state = cond.state.fetchAdd(.{ .waiters = 1, .signals = 0 }, .monotonic);
+ assert(prev_state.waiters < std.math.maxInt(u16)); // overflow caused by too many waiters
+ }
+
+ mutex.unlock(io);
+ defer mutex.lockUncancelable(io);
+
+ while (true) {
+ const result = io.futexWaitTimeout(u32, &cond.epoch.raw, epoch, deadline);
+
+ epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before `state` load
+
+ // Even on error, try to consume a pending signal first. Otherwise a race might
+ // cause a signal to get stuck in the state with no corresponding waiter.
+ {
+ var prev_state = cond.state.load(.monotonic);
+ while (prev_state.signals > 0) {
+ prev_state = cond.state.cmpxchgWeak(prev_state, .{
+ .waiters = prev_state.waiters - 1,
+ .signals = prev_state.signals - 1,
+ }, .acquire, .monotonic) orelse {
+ // We successfully consumed a signal.
+ return;
+ };
+ }
+ }
+
+ // There are no more signals available; this was a spurious wakeup or an error. If it
+ // was an error, we will remove ourselves as a waiter and return that error. If a
+ // timeout was specified and the deadline has passed, we remove ourselves as a waiter
+ // and return `error.Timeout`. Otherwise, we'll loop back to the futex wait.
+ result catch |err| {
+ const prev_state = cond.state.fetchSub(.{ .waiters = 1, .signals = 0 }, .monotonic);
+ assert(prev_state.waiters > 0); // underflow caused by illegal state
+ return err;
+ };
+ switch (deadline) {
+ .none => {},
+ .deadline => |d| if (d.untilNow(io).raw.nanoseconds >= 0) {
+ const prev_state = cond.state.fetchSub(.{ .waiters = 1, .signals = 0 }, .monotonic);
+ assert(prev_state.waiters > 0); // underflow caused by illegal state
+ return error.Timeout;
+ },
+ .duration => unreachable,
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/lib/enum.zig b/vendor/libghostty-vt/src/lib/enum.zig
index 76e05fa9..3c7a3a41 100644
--- a/vendor/libghostty-vt/src/lib/enum.zig
+++ b/vendor/libghostty-vt/src/lib/enum.zig
@@ -5,58 +5,50 @@ const Target = @import("target.zig").Target;
/// if we're targeting C, otherwise a Zig enum with smallest possible
/// backing type.
///
-/// In all cases, the enum keys will be created in the order given.
-/// For C ABI, this means that the order MUST NOT be changed in order
-/// to preserve ABI compatibility. You can set a key to null to
-/// remove it from the Zig enum while keeping the "hole" in the C enum
-/// to preserve ABI compatibility.
+/// In all cases, each enum value is its index in `keys`. The order MUST NOT
+/// be changed when the integer values are part of an ABI or serialized format.
+/// A null key removes that field while preserving its integer hole.
///
/// C detection is up to the caller, since there are multiple ways
/// to do that. We rely on the `target` parameter to determine whether we
/// should create a C compatible enum or a Zig enum.
///
-/// For the Zig enum, the enum value is not guaranteed to be stable, so
-/// it shouldn't be relied for things like serialization.
+/// C enums use `c_int`. Zig enums use the smallest unsigned integer that can
+/// represent every key index, including holes.
pub fn Enum(
target: Target,
keys: []const ?[:0]const u8,
) type {
- var fields: [keys.len]std.builtin.Type.EnumField = undefined;
- var fields_i: usize = 0;
- var holes: usize = 0;
- for (keys) |key_| {
- const key: [:0]const u8 = key_ orelse {
- switch (target) {
- // For Zig we don't track holes because the enum value
- // isn't guaranteed to be stable and we want to use the
- // smallest possible backing type.
- .zig => {},
-
- // For C we must track holes to preserve ABI compatibility
- // with subsequent values.
- .c => holes += 1,
+ var names_raw: [keys.len][]const u8 = undefined;
+ var values_raw: [keys.len]comptime_int = undefined;
+ const names_actual, const values_actual = kv: {
+ // Remove null fields while preserving their positions as integer holes.
+ var to: comptime_int = 0;
+ for (0..keys.len) |from| {
+ if (keys[from]) |key| {
+ names_raw[to] = key;
+ values_raw[to] = from;
+ to += 1;
}
- continue;
- };
+ }
- fields[fields_i] = .{
- .name = key,
- .value = fields_i + holes,
- };
- fields_i += 1;
- }
+ break :kv .{ names_raw[0..to], values_raw[0..to] };
+ };
- // Assigned to var so that the type name is nicer in stack traces.
- const Result = @Type(.{ .@"enum" = .{
- .tag_type = switch (target) {
- .c => c_int,
- .zig => std.math.IntFittingRange(0, fields_i - 1),
- },
- .fields = fields[0..fields_i],
- .decls = &.{},
- .is_exhaustive = true,
- } });
- return Result;
+ const TagInt = switch (target) {
+ .c => c_int,
+ .zig => std.math.IntFittingRange(0, keys.len - 1),
+ };
+
+ return @Enum(TagInt, .exhaustive, names_actual, &(values: {
+ // We have to transform our comptime_int values into the actual int
+ // we're creating the enum as.
+ var result_int_values: [values_actual.len]TagInt = undefined;
+ for (0..values_actual.len) |idx| {
+ result_int_values[idx] = values_actual[idx];
+ }
+ break :values result_int_values;
+ }));
}
test "zig" {
@@ -73,7 +65,7 @@ test "c" {
try testing.expectEqual(c_int, info.tag_type);
}
-test "abi by removing a key" {
+test "stable values when removing a key" {
const testing = std.testing;
// C
{
@@ -88,10 +80,28 @@ test "abi by removing a key" {
const T = Enum(.zig, &.{ "a", "b", null, "d" });
const info = @typeInfo(T).@"enum";
try testing.expectEqual(u2, info.tag_type);
- try testing.expectEqual(2, @intFromEnum(T.d));
+ try testing.expectEqual(3, @intFromEnum(T.d));
}
}
+test "zig backing integer includes trailing holes" {
+ const testing = std.testing;
+ const T = Enum(.zig, &.{ "a", null, null, null, null });
+ const info = @typeInfo(T).@"enum";
+ try testing.expectEqual(u3, info.tag_type);
+ try testing.expectEqual(0, @intFromEnum(T.a));
+}
+
+test "zig values remain stable across multiple holes" {
+ const testing = std.testing;
+ const T = Enum(.zig, &.{ null, "b", null, "d", null, "f" });
+ const info = @typeInfo(T).@"enum";
+ try testing.expectEqual(u3, info.tag_type);
+ try testing.expectEqual(1, @intFromEnum(T.b));
+ try testing.expectEqual(3, @intFromEnum(T.d));
+ try testing.expectEqual(5, @intFromEnum(T.f));
+}
+
/// Verify that for every key in enum T, there is a matching declaration in
/// `ghostty.h` with the correct value. This should only ever be called inside a `test`
/// because the `ghostty.h` module is only available then.
@@ -125,7 +135,10 @@ pub fn checkGhosttyHEnum(
if (@hasDecl(c, expected_name)) {
std.testing.expectEqual(field.value, @field(c, expected_name)) catch |e| {
- std.log.err(@typeName(T) ++ " key " ++ field.name ++ " does not have the same backing int as " ++ expected_name, .{});
+ std.log.err(
+ "{s} key {s} does not have the same backing int as " ++ expected_name,
+ .{ @typeName(T), field.name },
+ );
return e;
};
diff --git a/vendor/libghostty-vt/src/lib/main.zig b/vendor/libghostty-vt/src/lib/main.zig
index b22b3d98..573007bd 100644
--- a/vendor/libghostty-vt/src/lib/main.zig
+++ b/vendor/libghostty-vt/src/lib/main.zig
@@ -1,19 +1,22 @@
const std = @import("std");
const enumpkg = @import("enum.zig");
+const packedpkg = @import("packed.zig");
const structpkg = @import("struct.zig");
const types = @import("types.zig");
const unionpkg = @import("union.zig");
pub const allocator = @import("allocator.zig");
+pub const TinyIo = @import("TinyIo.zig");
pub const Buffer = types.Buffer;
pub const Enum = enumpkg.Enum;
+pub const Packed = packedpkg.Packed;
+pub const PackedTaggedUnion = packedpkg.PackedTaggedUnion;
pub const checkGhosttyHEnum = enumpkg.checkGhosttyHEnum;
pub const String = types.String;
pub const Struct = structpkg.Struct;
pub const structSizedFieldFits = structpkg.sizedFieldFits;
pub const Target = @import("target.zig").Target;
pub const TaggedUnion = unionpkg.TaggedUnion;
-pub const cutPrefix = @import("string.zig").cutPrefix;
test {
std.testing.refAllDecls(@This());
diff --git a/vendor/libghostty-vt/src/lib/packed.zig b/vendor/libghostty-vt/src/lib/packed.zig
new file mode 100644
index 00000000..b210242d
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/packed.zig
@@ -0,0 +1,290 @@
+const std = @import("std");
+const testing = std.testing;
+
+/// Metadata for a field in a packed layout.
+pub const FieldOptions = struct {
+ /// Public name for this field. The Zig field name is used by default.
+ name: ?[]const u8 = null,
+
+ /// Public type name for scalar fields. The Zig type is used by default.
+ type_name: ?[]const u8 = null,
+
+ /// Omit this field from public metadata. This is intended for padding.
+ omit: bool = false,
+
+ /// How the field is represented in public metadata.
+ encoding: Encoding = .scalar,
+
+ pub const Encoding = union(enum) {
+ scalar,
+ @"packed": type,
+ tagged_union: type,
+ };
+};
+
+/// Create metadata for a packed struct. Physical layout information is always
+/// reflected from `T`; options only provide public names and relationships.
+pub fn Packed(
+ comptime T: type,
+ comptime options: PackedOptions(T),
+) type {
+ const info = packedStructInfo(T);
+ const FieldT = std.meta.FieldEnum(T);
+
+ return struct {
+ pub const Zig = T;
+
+ // I know this is an insane amount of validation but getting
+ // this correct is critical to C APIs working so we go overboard.
+ comptime {
+ for (info.fields, 0..) |field, i| {
+ const field_options = @field(options.fields, field.name);
+ if (field_options.omit) {
+ if (field_options.name != null or
+ field_options.type_name != null or
+ field_options.encoding != .scalar)
+ {
+ @compileError("omitted packed field has other options: " ++ field.name);
+ }
+ continue;
+ }
+
+ if (field_options.name) |name| {
+ if (name.len == 0)
+ @compileError("packed field name cannot be empty: " ++ field.name);
+ }
+
+ switch (field_options.encoding) {
+ .scalar => switch (@typeInfo(field.type)) {
+ .bool, .int, .@"enum" => {},
+ else => @compileError("packed field requires an explicit encoding: " ++ field.name),
+ },
+ .@"packed" => |Layout| {
+ if (Layout.Zig != field.type)
+ @compileError("nested packed layout has the wrong Zig type: " ++ field.name);
+ if (field_options.type_name != null)
+ @compileError("nested packed field cannot have a scalar type name: " ++ field.name);
+ },
+ .tagged_union => |Layout| {
+ if (Layout.Owner != T or Layout.Union != field.type or
+ Layout.union_field != @field(FieldT, field.name))
+ {
+ @compileError("tagged union layout does not match packed field: " ++ field.name);
+ }
+ if (field_options.type_name != null)
+ @compileError("tagged union field cannot have a scalar type name: " ++ field.name);
+ },
+ }
+
+ const public_name = field_options.name orelse field.name;
+ for (info.fields[0..i]) |previous| {
+ const previous_options = @field(options.fields, previous.name);
+ if (previous_options.omit) continue;
+ const previous_name = previous_options.name orelse previous.name;
+ if (std.mem.eql(u8, public_name, previous_name))
+ @compileError("duplicate public packed field name: " ++ public_name);
+ }
+ }
+ }
+
+ pub const Backing = info.backing_integer.?;
+ pub const Field = std.meta.FieldEnum(T);
+
+ pub fn fieldOptions(comptime field: Field) FieldOptions {
+ return @field(options.fields, @tagName(field));
+ }
+
+ pub fn fieldName(comptime field: Field) ?[]const u8 {
+ const field_options = fieldOptions(field);
+ if (field_options.omit) return null;
+ return field_options.name orelse @tagName(field);
+ }
+
+ pub fn bitOffset(comptime field: Field) usize {
+ return @bitOffsetOf(T, @tagName(field));
+ }
+
+ pub fn bitWidth(comptime field: Field) usize {
+ return @bitSizeOf(@FieldType(T, @tagName(field)));
+ }
+ };
+}
+
+/// Options for a packed struct. A field is generated for every field in `T`
+/// so unknown field names are rejected by normal Zig type checking.
+pub fn PackedOptions(comptime T: type) type {
+ const fields = packedStructInfo(T).fields;
+ const default_options: FieldOptions = .{};
+
+ var names: [fields.len][]const u8 = undefined;
+ var types: [fields.len]type = undefined;
+ var attrs: [fields.len]std.builtin.Type.StructField.Attributes = undefined;
+
+ for (fields, 0..) |field, i| {
+ names[i] = field.name;
+ types[i] = FieldOptions;
+ attrs[i] = .{ .default_value_ptr = &default_options };
+ }
+
+ const Fields = @Struct(.auto, null, &names, &types, &attrs);
+ return struct {
+ fields: Fields = .{},
+ };
+}
+
+/// Describe a packed union field whose active arm is selected by another
+/// field in the containing packed struct. Each tag maps to a reflected packed
+/// arm layout. Multiple tags may map to the same source union field.
+pub fn PackedTaggedUnion(
+ comptime OwnerT: type,
+ comptime union_field_value: std.meta.FieldEnum(OwnerT),
+ comptime tag_field_value: std.meta.FieldEnum(OwnerT),
+ comptime options: PackedTaggedUnionOptions(
+ @FieldType(OwnerT, @tagName(union_field_value)),
+ @FieldType(OwnerT, @tagName(tag_field_value)),
+ ),
+) type {
+ _ = packedStructInfo(OwnerT);
+ const UnionT = @FieldType(OwnerT, @tagName(union_field_value));
+ const union_info = @typeInfo(UnionT).@"union";
+ if (union_info.layout != .@"packed")
+ @compileError("packed tagged union value must be a packed union");
+
+ const TagT = @FieldType(OwnerT, @tagName(tag_field_value));
+ const tag_info = @typeInfo(TagT).@"enum";
+ const ArmT = PackedTaggedUnionArm(UnionT);
+
+ return struct {
+ pub const Owner = OwnerT;
+ pub const Union = UnionT;
+ pub const Tag = TagT;
+ pub const Arm = ArmT;
+ pub const union_field = union_field_value;
+ pub const tag_field = tag_field_value;
+
+ comptime {
+ for (tag_info.fields) |tag| {
+ const arm_value = @field(options.arms, tag.name) orelse continue;
+ switch (arm_value) {
+ inline else => |Layout, source| {
+ const source_name = @tagName(source);
+ if (Layout.Zig != @FieldType(UnionT, source_name))
+ @compileError("packed union arm layout has the wrong Zig type for tag: " ++ tag.name);
+ if (@bitSizeOf(Layout.Zig) > @bitSizeOf(UnionT))
+ @compileError("packed union arm is wider than its union: " ++ tag.name);
+ },
+ }
+ }
+ }
+
+ pub fn arm(comptime tag: Tag) ?Arm {
+ return @field(options.arms, @tagName(tag));
+ }
+ };
+}
+
+/// Options for mapping tag values to packed union arms.
+pub fn PackedTaggedUnionOptions(comptime Union: type, comptime Tag: type) type {
+ const union_info = @typeInfo(Union).@"union";
+ if (union_info.layout != .@"packed")
+ @compileError("packed tagged union value must be a packed union");
+ const tag_fields = @typeInfo(Tag).@"enum".fields;
+ const Arm = PackedTaggedUnionArm(Union);
+ const default_arm: ?Arm = null;
+
+ var names: [tag_fields.len][]const u8 = undefined;
+ var types: [tag_fields.len]type = undefined;
+ var attrs: [tag_fields.len]std.builtin.Type.StructField.Attributes = undefined;
+
+ for (tag_fields, 0..) |field, i| {
+ names[i] = field.name;
+ types[i] = ?Arm;
+ attrs[i] = .{ .default_value_ptr = &default_arm };
+ }
+
+ const Arms = @Struct(.auto, null, &names, &types, &attrs);
+ return struct {
+ arms: Arms = .{},
+ };
+}
+
+fn PackedTaggedUnionArm(comptime Union: type) type {
+ const fields = @typeInfo(Union).@"union".fields;
+ const Tag = std.meta.FieldEnum(Union);
+
+ var names: [fields.len][]const u8 = undefined;
+ var types: [fields.len]type = undefined;
+ var attrs: [fields.len]std.builtin.Type.UnionField.Attributes = undefined;
+
+ for (fields, 0..) |field, i| {
+ names[i] = field.name;
+ types[i] = type;
+ attrs[i] = .{ .@"align" = 1 };
+ }
+
+ return @Union(.auto, Tag, &names, &types, &attrs);
+}
+
+fn packedStructInfo(comptime T: type) std.builtin.Type.Struct {
+ const info = @typeInfo(T).@"struct";
+ if (info.layout != .@"packed")
+ @compileError("Packed requires a packed struct");
+ if (info.backing_integer == null)
+ @compileError("Packed requires an integer-backed packed struct");
+ return info;
+}
+
+test "Packed: reflected fields and nested tagged union" {
+ const Tag = enum(u2) { first, second, third };
+ const Value = packed union {
+ number: packed struct(u4) {
+ data: u3,
+ _pad: u1,
+ },
+ flags: packed struct(u4) {
+ a: bool,
+ b: bool,
+ _pad: u2,
+ },
+ };
+ const Nested = packed struct(u1) { enabled: bool };
+ const Subject = packed struct(u8) {
+ tag: Tag,
+ value: Value,
+ nested: Nested,
+ _padding: u1,
+ };
+
+ const Number = Packed(@FieldType(Value, "number"), .{ .fields = .{
+ .data = .{ .name = "number" },
+ ._pad = .{ .omit = true },
+ } });
+ const Flags = Packed(@FieldType(Value, "flags"), .{ .fields = .{
+ ._pad = .{ .omit = true },
+ } });
+ const NestedLayout = Packed(Nested, .{});
+ const ValueLayout = PackedTaggedUnion(Subject, .value, .tag, .{ .arms = .{
+ .first = .{ .number = Number },
+ .second = .{ .number = Number },
+ .third = .{ .flags = Flags },
+ } });
+ const Layout = Packed(Subject, .{ .fields = .{
+ .value = .{ .encoding = .{ .tagged_union = ValueLayout } },
+ .nested = .{ .encoding = .{ .@"packed" = NestedLayout } },
+ ._padding = .{ .omit = true },
+ } });
+
+ try testing.expectEqual(@bitOffsetOf(Subject, "value"), Layout.bitOffset(.value));
+ try testing.expectEqual(@bitSizeOf(Value), Layout.bitWidth(.value));
+ try testing.expectEqualStrings("number", Number.fieldName(.data).?);
+ try testing.expect(Number.fieldName(._pad) == null);
+ switch (ValueLayout.arm(.first).?) {
+ .number => |ArmLayout| try testing.expect(ArmLayout == Number),
+ else => return error.TestUnexpectedResult,
+ }
+ switch (ValueLayout.arm(.second).?) {
+ .number => |ArmLayout| try testing.expect(ArmLayout == Number),
+ else => return error.TestUnexpectedResult,
+ }
+ try testing.expectEqual(@as(usize, 1), NestedLayout.bitWidth(.enabled));
+}
diff --git a/vendor/libghostty-vt/src/lib/string.zig b/vendor/libghostty-vt/src/lib/string.zig
deleted file mode 100644
index 795823c2..00000000
--- a/vendor/libghostty-vt/src/lib/string.zig
+++ /dev/null
@@ -1,15 +0,0 @@
-const std = @import("std");
-
-// This is a copy of std.mem.cutPrefix from 0.16. Once Ghostty has been ported
-// to 0.16 this can be removed.
-
-/// If slice starts with prefix, returns the rest of slice starting at
-/// prefix.len.
-pub fn cutPrefix(comptime T: type, slice: []const T, prefix: []const T) ?[]const T {
- return if (std.mem.startsWith(T, slice, prefix)) slice[prefix.len..] else null;
-}
-
-test cutPrefix {
- try std.testing.expectEqualStrings("foo", cutPrefix(u8, "--example=foo", "--example=").?);
- try std.testing.expectEqual(null, cutPrefix(u8, "--example=foo", "-example="));
-}
diff --git a/vendor/libghostty-vt/src/lib/struct.zig b/vendor/libghostty-vt/src/lib/struct.zig
index bbf07ece..262e037f 100644
--- a/vendor/libghostty-vt/src/lib/struct.zig
+++ b/vendor/libghostty-vt/src/lib/struct.zig
@@ -20,23 +20,21 @@ pub fn Struct(
.zig => Zig,
.c => c: {
const info = @typeInfo(Zig).@"struct";
- var fields: [info.fields.len]std.builtin.Type.StructField = undefined;
- for (info.fields, 0..) |field, i| {
- fields[i] = .{
- .name = field.name,
- .type = field.type,
+ var names: [info.fields.len][]const u8 = undefined;
+ var types: [info.fields.len]type = undefined;
+ var attrs: [info.fields.len]std.builtin.Type.StructField.Attributes = undefined;
+
+ for (info.fields, &names, &types, &attrs) |field, *name, *ty, *attr| {
+ name.* = field.name;
+ ty.* = field.type;
+ attr.* = .{
+ .@"align" = field.alignment,
+ .@"comptime" = field.is_comptime,
.default_value_ptr = field.default_value_ptr,
- .is_comptime = field.is_comptime,
- .alignment = if (field.alignment > 0) field.alignment else @alignOf(field.type),
};
}
- break :c @Type(.{ .@"struct" = .{
- .layout = .@"extern",
- .fields = &fields,
- .decls = &.{},
- .is_tuple = info.is_tuple,
- } });
+ break :c @Struct(.@"extern", null, &names, &types, &attrs);
},
};
}
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);
+}
diff --git a/vendor/libghostty-vt/src/lib/types.zig b/vendor/libghostty-vt/src/lib/types.zig
index 655fdc22..fbb50af8 100644
--- a/vendor/libghostty-vt/src/lib/types.zig
+++ b/vendor/libghostty-vt/src/lib/types.zig
@@ -5,9 +5,13 @@ pub const String = extern struct {
pub fn init(zig: anytype) String {
return switch (@TypeOf(zig)) {
[]u8, []const u8 => .{
- .ptr = zig.ptr,
+ // Borrowed strings keep a non-null pointer, but it must point
+ // to real storage: empty Zig slices can use invalid sentinels
+ // that foreign runtimes reject even without dereferencing.
+ .ptr = if (zig.len == 0) "" else zig.ptr,
.len = zig.len,
},
+ else => @compileError("unsupported String.init type: " ++ @typeName(@TypeOf(zig))),
};
}
};
@@ -17,3 +21,14 @@ pub const Buffer = extern struct {
cap: usize = 0,
len: usize = 0,
};
+
+test "String.init empty output" {
+ const std = @import("std");
+ const empty = try std.testing.allocator.alloc(u8, 0);
+ defer std.testing.allocator.free(empty);
+ const str = String.init(empty);
+ try std.testing.expectEqual(@as(usize, 0), str.len);
+ // Empty borrowed strings use the same real storage as an empty literal,
+ // never Zig's zero-length allocation sentinel.
+ try std.testing.expectEqual(@as([*]const u8, ""), str.ptr);
+}
diff --git a/vendor/libghostty-vt/src/lib/union.zig b/vendor/libghostty-vt/src/lib/union.zig
index 924d0e86..7ed3fc79 100644
--- a/vendor/libghostty-vt/src/lib/union.zig
+++ b/vendor/libghostty-vt/src/lib/union.zig
@@ -15,14 +15,18 @@ const Target = @import("target.zig").Target;
/// be an enum created with the `Enum` function in this library, so that
/// automatic C ABI compatibility is ensured.
///
-/// The `Padding` type is a type that is always added to the C union
-/// with the key `_padding`. This should be set to a type that has the size
-/// and alignment needed to pad the C union to the expected size. This
-/// should never change to ensure ABI compatibility.
+/// `options.padding` is a type that is always added to the C union with the key
+/// `_padding`. It should have the size and alignment needed to pad the C union
+/// to the expected size and should never change to ensure ABI compatibility.
+///
+/// Each tag has an optional field in `options.field_renames` that may rename its
+/// C value field. Multiple tags may map to the same field when their C value
+/// types are identical. Tags with no payload are omitted. This is used
+/// for metadata and should match the C headers directly.
pub fn TaggedUnion(
comptime target: Target,
comptime Union: type,
- comptime Padding: type,
+ comptime options: TaggedUnionOptions(Union),
) type {
return struct {
comptime {
@@ -30,7 +34,7 @@ pub fn TaggedUnion(
.zig => {},
// For ABI compatibility, we expect that this is our union size.
- .c => if (@sizeOf(CValue) != @sizeOf(Padding)) {
+ .c => if (@sizeOf(CValue) != @sizeOf(options.padding)) {
@compileLog(@sizeOf(CValue));
@compileError("TaggedUnion CValue size does not match expected fixed size");
},
@@ -49,18 +53,35 @@ pub fn TaggedUnion(
.c => extern struct {
tag: Tag,
value: CValue,
+
+ /// Returns the public C value-union field name for `tag`, or
+ /// null when the tag has no value field. This is metadata only;
+ /// it does not rename fields in `CValue`.
+ pub fn cFieldRename(comptime tag: Tag) ?[]const u8 {
+ const tag_name = @tagName(tag);
+ const value = @field(@unionInit(Union, tag_name, undefined), tag_name);
+
+ if (@field(options.field_renames, tag_name)) |name| return name;
+ if (@sizeOf(@TypeOf(value)) == 0) return null;
+
+ return tag_name;
+ }
},
};
/// The C ABI compatible union value type.
pub const CValue = cvalue: {
+ @setEvalBranchQuota(10_000);
switch (target) {
.zig => break :cvalue extern struct {},
.c => {},
}
const tag_fields = @typeInfo(Tag).@"enum".fields;
- var union_fields: [tag_fields.len + 1]std.builtin.Type.UnionField = undefined;
+ var names: [tag_fields.len + 1][]const u8 = undefined;
+ var types: [tag_fields.len + 1]type = undefined;
+ var attrs: [tag_fields.len + 1]std.builtin.Type.UnionField.Attributes = undefined;
+
for (tag_fields, 0..) |field, i| {
const action = @unionInit(Union, field.name, undefined);
const Type = t: {
@@ -74,25 +95,16 @@ pub fn TaggedUnion(
break :t Type;
};
- union_fields[i] = .{
- .name = field.name,
- .type = Type,
- .alignment = @alignOf(Type),
- };
+ names[i] = field.name;
+ types[i] = Type;
+ attrs[i] = .{ .@"align" = @alignOf(Type) };
}
- union_fields[tag_fields.len] = .{
- .name = "_padding",
- .type = Padding,
- .alignment = @alignOf(Padding),
- };
+ names[tag_fields.len] = "_padding";
+ types[tag_fields.len] = options.padding;
+ attrs[tag_fields.len] = .{ .@"align" = @alignOf(options.padding) };
- break :cvalue @Type(.{ .@"union" = .{
- .layout = .@"extern",
- .tag_type = null,
- .fields = &union_fields,
- .decls = &.{},
- } });
+ break :cvalue @Union(.@"extern", null, &names, &types, &attrs);
};
/// Convert to C union.
@@ -120,17 +132,35 @@ pub fn TaggedUnion(
/// Returns the value type for the given tag.
pub fn Value(comptime tag: Tag) type {
- @setEvalBranchQuota(10000);
- inline for (@typeInfo(Union).@"union".fields) |field| {
- const field_tag = @field(Tag, field.name);
- if (field_tag == tag) return field.type;
- }
-
- unreachable;
+ return @FieldType(Union, @tagName(tag));
}
};
}
+/// Options for generating the C representation of a tagged union.
+pub fn TaggedUnionOptions(comptime Union: type) type {
+ const Tag = @typeInfo(Union).@"union".tag_type.?;
+ const tag_fields = @typeInfo(Tag).@"enum".fields;
+ const FieldRenames: type = field_renames: {
+ const default_rename: ?[]const u8 = null;
+
+ var names: [tag_fields.len][]const u8 = undefined;
+ var types: [tag_fields.len]type = undefined;
+ var attrs: [tag_fields.len]std.builtin.Type.StructField.Attributes = undefined;
+
+ for (tag_fields, 0..) |field, i| {
+ names[i] = field.name;
+ types[i] = ?[]const u8;
+ attrs[i] = .{ .default_value_ptr = &default_rename };
+ }
+
+ break :field_renames @Struct(.auto, null, &names, &types, &attrs);
+ };
+ return struct {
+ padding: type,
+ field_renames: FieldRenames = .{},
+ };
+}
test "TaggedUnion: matching size" {
const Tag = enum(c_int) { a, b };
const U = TaggedUnion(
@@ -139,7 +169,7 @@ test "TaggedUnion: matching size" {
a: u32,
b: u64,
},
- u64,
+ .{ .padding = u64 },
);
try testing.expectEqual(8, @sizeOf(U.CValue));
@@ -152,7 +182,7 @@ test "TaggedUnion: padded size" {
union(Tag) {
a: u32,
},
- u64,
+ .{ .padding = u64 },
);
try testing.expectEqual(8, @sizeOf(U.CValue));
@@ -163,9 +193,39 @@ test "TaggedUnion: c conversion" {
const U = TaggedUnion(.c, union(Tag) {
a: u32,
b: u64,
- }, u64);
+ }, .{ .padding = u64 });
const c = U.cval(.{ .a = 42 });
try testing.expectEqual(Tag.a, c.tag);
try testing.expectEqual(42, c.value.a);
}
+
+test "TaggedUnion: custom C value fields" {
+ const Tag = enum(c_int) { a, b, none };
+ const Union = union(Tag) {
+ a: u32,
+ b: u32,
+ none,
+ };
+ const U = TaggedUnion(.c, Union, .{
+ .padding = u64,
+ .field_renames = .{
+ .a = "value",
+ .b = "value",
+ },
+ });
+
+ try testing.expect(!@hasField(U.CValue, "value"));
+ try testing.expect(@hasField(U.CValue, "a"));
+ try testing.expect(@hasField(U.CValue, "b"));
+ try testing.expect(@hasField(U.CValue, "none"));
+ try testing.expectEqualStrings("value", U.C.cFieldRename(.a).?);
+ try testing.expect(U.C.cFieldRename(.none) == null);
+
+ const a = U.cval(.{ .a = 42 });
+ try testing.expectEqual(Tag.a, a.tag);
+ try testing.expectEqual(42, a.value.a);
+
+ const none = U.cval(.none);
+ try testing.expectEqual(Tag.none, none.tag);
+}