aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/lib/tinyio/windows.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/lib/tinyio/windows.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/lib/tinyio/windows.zig')
-rw-r--r--vendor/libghostty-vt/src/lib/tinyio/windows.zig745
1 files changed, 745 insertions, 0 deletions
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);
+}