zig/lib/std/Io/Threaded.zig
2025-12-05 17:29:03 -08:00

9425 lines
364 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const Threaded = @This();
const builtin = @import("builtin");
const native_os = builtin.os.tag;
const is_windows = native_os == .windows;
const windows = std.os.windows;
const ws2_32 = std.os.windows.ws2_32;
const is_debug = builtin.mode == .Debug;
const std = @import("../std.zig");
const Io = std.Io;
const net = std.Io.net;
const HostName = std.Io.net.HostName;
const IpAddress = std.Io.net.IpAddress;
const Allocator = std.mem.Allocator;
const Alignment = std.mem.Alignment;
const assert = std.debug.assert;
const posix = std.posix;
/// Thread-safe.
allocator: Allocator,
mutex: std.Thread.Mutex = .{},
cond: std.Thread.Condition = .{},
run_queue: std.SinglyLinkedList = .{},
join_requested: bool = false,
stack_size: usize,
/// All threads are spawned detached; this is how we wait until they all exit.
wait_group: std.Thread.WaitGroup = .{},
/// Maximum thread pool size (excluding main thread) when dispatching async
/// tasks. Until this limit, calls to `Io.async` when all threads are busy will
/// cause a new thread to be spawned and permanently added to the pool. After
/// this limit, calls to `Io.async` when all threads are busy run the task
/// immediately.
///
/// Defaults to a number equal to logical CPU cores.
///
/// Protected by `mutex` once the I/O instance is already in use. See
/// `setAsyncLimit`.
async_limit: Io.Limit,
/// Maximum thread pool size (excluding main thread) for dispatching concurrent
/// tasks. Until this limit, calls to `Io.concurrent` will increase the thread
/// pool size.
///
/// concurrent tasks. After this number, calls to `Io.concurrent` return
/// `error.ConcurrencyUnavailable`.
concurrent_limit: Io.Limit = .unlimited,
/// Error from calling `std.Thread.getCpuCount` in `init`.
cpu_count_error: ?std.Thread.CpuCountError,
/// Number of threads that are unavailable to take tasks. To calculate
/// available count, subtract this from either `async_limit` or
/// `concurrent_limit`.
busy_count: usize = 0,
main_thread: Thread,
pid: Pid = .unknown,
/// When a cancel request is made, blocking syscalls can be unblocked by
/// issuing a signal. However, if the signal arrives after the check and before
/// the syscall instruction, it is missed.
///
/// This option solves the race condition by retrying the signal delivery
/// until it is acknowledged, with an exponential backoff.
///
/// Unfortunately, trying again until the cancellation request is acknowledged
/// has been observed to be relatively slow, and usually strong cancellation
/// guarantees are not needed, so this defaults to off.
robust_cancel: RobustCancel = .disabled,
wsa: if (is_windows) Wsa else struct {} = .{},
have_signal_handler: bool,
old_sig_io: if (have_sig_io) posix.Sigaction else void,
old_sig_pipe: if (have_sig_pipe) posix.Sigaction else void,
pub const RobustCancel = if (std.Thread.use_pthreads or native_os == .linux) enum {
enabled,
disabled,
} else enum {
disabled,
};
pub const Pid = if (native_os == .linux) enum(posix.pid_t) {
unknown = 0,
_,
} else enum(u0) { unknown = 0 };
const Thread = struct {
/// The value that needs to be passed to pthread_kill or tgkill in order to
/// send a signal.
signal_id: SignaleeId,
current_closure: ?*Closure = null,
const SignaleeId = if (std.Thread.use_pthreads) std.c.pthread_t else std.Thread.Id;
threadlocal var current: ?*Thread = null;
fn getCurrent(t: *Threaded) *Thread {
return current orelse return &t.main_thread;
}
fn checkCancel(thread: *Thread) error{Canceled}!void {
const closure = thread.current_closure orelse return;
switch (@cmpxchgStrong(
CancelStatus,
&closure.cancel_status,
.requested,
.acknowledged,
.acq_rel,
.acquire,
) orelse return error.Canceled) {
.requested => unreachable,
.acknowledged => unreachable,
.none, _ => {},
}
}
fn beginSyscall(thread: *Thread) error{Canceled}!void {
const closure = thread.current_closure orelse return;
switch (@cmpxchgStrong(
CancelStatus,
&closure.cancel_status,
.none,
.fromSignaleeId(thread.signal_id),
.acq_rel,
.acquire,
) orelse return) {
.none => unreachable,
.requested => {
@atomicStore(CancelStatus, &closure.cancel_status, .acknowledged, .release);
return error.Canceled;
},
.acknowledged => return,
_ => unreachable,
}
}
fn endSyscall(thread: *Thread) void {
const closure = thread.current_closure orelse return;
_ = @cmpxchgStrong(
CancelStatus,
&closure.cancel_status,
.fromSignaleeId(thread.signal_id),
.none,
.acq_rel,
.acquire,
) orelse return;
}
fn endSyscallCanceled(thread: *Thread) Io.Cancelable {
if (thread.current_closure) |closure| {
@atomicStore(CancelStatus, &closure.cancel_status, .acknowledged, .release);
}
return error.Canceled;
}
fn currentSignalId() SignaleeId {
return if (std.Thread.use_pthreads) std.c.pthread_self() else std.Thread.getCurrentId();
}
};
const max_iovecs_len = 8;
const splat_buffer_size = 64;
comptime {
if (@TypeOf(posix.IOV_MAX) != void) assert(max_iovecs_len <= posix.IOV_MAX);
}
const CancelStatus = enum(usize) {
/// Cancellation has neither been requested, nor checked. The async
/// operation will check status before entering a blocking syscall.
/// This is also the status used for uninteruptible tasks.
none = 0,
/// Cancellation has been requested and the status will be checked before
/// entering a blocking syscall.
requested = std.math.maxInt(usize) - 1,
/// Cancellation has been acknowledged and is in progress. Signals should
/// not be sent.
acknowledged = std.math.maxInt(usize),
/// Stores a `Thread.SignaleeId` and indicates that sending a signal to this thread
/// is needed in order to cancel. This state is set before going into
/// a blocking operation that needs to get unblocked via signal.
_,
const Unpacked = union(enum) {
none,
requested,
acknowledged,
signal_id: Thread.SignaleeId,
};
fn unpack(cs: CancelStatus) Unpacked {
return switch (cs) {
.none => .none,
.requested => .requested,
.acknowledged => .acknowledged,
_ => |signal_id| .{
.signal_id = if (std.Thread.use_pthreads)
@ptrFromInt(@intFromEnum(signal_id))
else
@truncate(@intFromEnum(signal_id)),
},
};
}
fn fromSignaleeId(signal_id: Thread.SignaleeId) CancelStatus {
return if (std.Thread.use_pthreads)
@enumFromInt(@intFromPtr(signal_id))
else
@enumFromInt(signal_id);
}
};
const Closure = struct {
start: Start,
node: std.SinglyLinkedList.Node = .{},
cancel_status: CancelStatus,
const Start = *const fn (*Closure, *Threaded) void;
fn requestCancel(closure: *Closure, t: *Threaded) void {
var signal_id = switch (@atomicRmw(CancelStatus, &closure.cancel_status, .Xchg, .requested, .monotonic).unpack()) {
.none, .acknowledged, .requested => return,
.signal_id => |signal_id| signal_id,
};
// The task will enter a blocking syscall before checking for cancellation again.
// We can send a signal to interrupt the syscall, but if it arrives before
// the syscall instruction, it will be missed. Therefore, this code tries
// again until the cancellation request is acknowledged.
// 1 << 10 ns is about 1 microsecond, approximately syscall overhead.
// 1 << 20 ns is about 1 millisecond.
// 1 << 30 ns is about 1 second.
//
// On a heavily loaded Linux 6.17.5, I observed a maximum of 20
// attempts not acknowledged before the timeout (including exponential
// backoff) was sufficient, despite the heavy load.
const max_attempts = 22;
for (0..max_attempts) |attempt_index| {
if (std.Thread.use_pthreads) {
if (std.c.pthread_kill(signal_id, .IO) != 0) return;
} else if (native_os == .linux) {
const pid: posix.pid_t = p: {
const cached_pid = @atomicLoad(Pid, &t.pid, .monotonic);
if (cached_pid != .unknown) break :p @intFromEnum(cached_pid);
const pid = std.os.linux.getpid();
@atomicStore(Pid, &t.pid, @enumFromInt(pid), .monotonic);
break :p pid;
};
if (std.os.linux.tgkill(pid, @bitCast(signal_id), .IO) != 0) return;
} else {
return;
}
if (t.robust_cancel != .enabled) return;
var timespec: posix.timespec = .{
.sec = 0,
.nsec = @as(isize, 1) << @intCast(attempt_index),
};
if (native_os == .linux) {
_ = std.os.linux.clock_nanosleep(posix.CLOCK.MONOTONIC, .{ .ABSTIME = false }, &timespec, &timespec);
} else {
_ = posix.system.nanosleep(&timespec, &timespec);
}
switch (@atomicRmw(CancelStatus, &closure.cancel_status, .Xchg, .requested, .monotonic).unpack()) {
.requested => continue, // Retry needed in case other thread hasn't yet entered the syscall.
.none, .acknowledged => return,
.signal_id => |new_signal_id| signal_id = new_signal_id,
}
}
}
};
/// Related:
/// * `init_single_threaded`
pub fn init(
/// Must be threadsafe. Only used for the following functions:
/// * `Io.VTable.async`
/// * `Io.VTable.concurrent`
/// * `Io.VTable.groupAsync`
/// * `Io.VTable.groupConcurrent`
/// If these functions are avoided, then `Allocator.failing` may be passed
/// here.
gpa: Allocator,
) Threaded {
if (builtin.single_threaded) return .init_single_threaded;
const cpu_count = std.Thread.getCpuCount();
var t: Threaded = .{
.allocator = gpa,
.stack_size = std.Thread.SpawnConfig.default_stack_size,
.async_limit = if (cpu_count) |n| .limited(n - 1) else |_| .nothing,
.cpu_count_error = if (cpu_count) |_| null else |e| e,
.old_sig_io = undefined,
.old_sig_pipe = undefined,
.have_signal_handler = false,
.main_thread = .{
.signal_id = Thread.currentSignalId(),
},
};
if (posix.Sigaction != void) {
// This causes sending `posix.SIG.IO` to thread to interrupt blocking
// syscalls, returning `posix.E.INTR`.
const act: posix.Sigaction = .{
.handler = .{ .handler = doNothingSignalHandler },
.mask = posix.sigemptyset(),
.flags = 0,
};
if (have_sig_io) posix.sigaction(.IO, &act, &t.old_sig_io);
if (have_sig_pipe) posix.sigaction(.PIPE, &act, &t.old_sig_pipe);
t.have_signal_handler = true;
}
return t;
}
/// Statically initialize such that calls to `Io.VTable.concurrent` will fail
/// with `error.ConcurrencyUnavailable`.
///
/// When initialized this way:
/// * cancel requests have no effect.
/// * `deinit` is safe, but unnecessary to call.
pub const init_single_threaded: Threaded = .{
.allocator = .failing,
.stack_size = std.Thread.SpawnConfig.default_stack_size,
.async_limit = .nothing,
.cpu_count_error = null,
.concurrent_limit = .nothing,
.old_sig_io = undefined,
.old_sig_pipe = undefined,
.have_signal_handler = false,
.main_thread = .{ .signal_id = undefined },
};
pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {
t.mutex.lock();
defer t.mutex.unlock();
t.async_limit = new_limit;
}
pub fn deinit(t: *Threaded) void {
t.join();
if (is_windows and t.wsa.status == .initialized) {
if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
}
if (posix.Sigaction != void and t.have_signal_handler) {
if (have_sig_io) posix.sigaction(.IO, &t.old_sig_io, null);
if (have_sig_pipe) posix.sigaction(.PIPE, &t.old_sig_pipe, null);
}
t.* = undefined;
}
fn join(t: *Threaded) void {
if (builtin.single_threaded) return;
{
t.mutex.lock();
defer t.mutex.unlock();
t.join_requested = true;
}
t.cond.broadcast();
t.wait_group.wait();
}
fn worker(t: *Threaded) void {
var thread: Thread = .{
.signal_id = Thread.currentSignalId(),
};
Thread.current = &thread;
defer t.wait_group.finish();
t.mutex.lock();
defer t.mutex.unlock();
while (true) {
while (t.run_queue.popFirst()) |closure_node| {
t.mutex.unlock();
const closure: *Closure = @fieldParentPtr("node", closure_node);
closure.start(closure, t);
t.mutex.lock();
t.busy_count -= 1;
}
if (t.join_requested) break;
t.cond.wait(&t.mutex);
}
}
pub fn io(t: *Threaded) Io {
return .{
.userdata = t,
.vtable = &.{
.async = async,
.concurrent = concurrent,
.await = await,
.cancel = cancel,
.select = select,
.groupAsync = groupAsync,
.groupConcurrent = groupConcurrent,
.groupWait = groupWait,
.groupCancel = groupCancel,
.mutexLock = mutexLock,
.mutexLockUncancelable = mutexLockUncancelable,
.mutexUnlock = mutexUnlock,
.conditionWait = conditionWait,
.conditionWaitUncancelable = conditionWaitUncancelable,
.conditionWake = conditionWake,
.dirMake = dirMake,
.dirMakePath = dirMakePath,
.dirMakeOpenPath = dirMakeOpenPath,
.dirStat = dirStat,
.dirStatPath = dirStatPath,
.dirAccess = dirAccess,
.dirCreateFile = dirCreateFile,
.dirOpenFile = dirOpenFile,
.dirOpenDir = dirOpenDir,
.dirClose = dirClose,
.dirRealPath = dirRealPath,
.dirDeleteFile = dirDeleteFile,
.dirDeleteDir = dirDeleteDir,
.dirRename = dirRename,
.dirSymLink = dirSymLink,
.dirReadLink = dirReadLink,
.dirSetOwner = dirSetOwner,
.dirSetPermissions = dirSetPermissions,
.dirSetTimestamps = dirSetTimestamps,
.dirSetTimestampsNow = dirSetTimestampsNow,
.fileStat = fileStat,
.fileClose = fileClose,
.fileWriteStreaming = fileWriteStreaming,
.fileWritePositional = fileWritePositional,
.fileReadStreaming = fileReadStreaming,
.fileReadPositional = fileReadPositional,
.fileSeekBy = fileSeekBy,
.fileSeekTo = fileSeekTo,
.openSelfExe = openSelfExe,
.fileSync = fileSync,
.fileIsTty = fileIsTty,
.fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes,
.fileSupportsAnsiEscapeCodes = fileSupportsAnsiEscapeCodes,
.fileSetLength = fileSetLength,
.fileSetOwner = fileSetOwner,
.fileSetPermissions = fileSetPermissions,
.fileSetTimestamps = fileSetTimestamps,
.fileSetTimestampsNow = fileSetTimestampsNow,
.fileLock = fileLock,
.fileTryLock = fileTryLock,
.fileUnlock = fileUnlock,
.fileDowngradeLock = fileDowngradeLock,
.now = now,
.sleep = sleep,
.netListenIp = switch (native_os) {
.windows => netListenIpWindows,
else => netListenIpPosix,
},
.netListenUnix = switch (native_os) {
.windows => netListenUnixWindows,
else => netListenUnixPosix,
},
.netAccept = switch (native_os) {
.windows => netAcceptWindows,
else => netAcceptPosix,
},
.netBindIp = switch (native_os) {
.windows => netBindIpWindows,
else => netBindIpPosix,
},
.netConnectIp = switch (native_os) {
.windows => netConnectIpWindows,
else => netConnectIpPosix,
},
.netConnectUnix = switch (native_os) {
.windows => netConnectUnixWindows,
else => netConnectUnixPosix,
},
.netClose = netClose,
.netRead = switch (native_os) {
.windows => netReadWindows,
else => netReadPosix,
},
.netWrite = switch (native_os) {
.windows => netWriteWindows,
else => netWritePosix,
},
.netSend = switch (native_os) {
.windows => netSendWindows,
else => netSendPosix,
},
.netReceive = switch (native_os) {
.windows => netReceiveWindows,
else => netReceivePosix,
},
.netInterfaceNameResolve = netInterfaceNameResolve,
.netInterfaceName = netInterfaceName,
.netLookup = netLookup,
},
};
}
/// Same as `io` but disables all networking functionality, which has
/// an additional dependency on Windows (ws2_32).
pub fn ioBasic(t: *Threaded) Io {
return .{
.userdata = t,
.vtable = &.{
.async = async,
.concurrent = concurrent,
.await = await,
.cancel = cancel,
.select = select,
.groupAsync = groupAsync,
.groupConcurrent = groupConcurrent,
.groupWait = groupWait,
.groupCancel = groupCancel,
.mutexLock = mutexLock,
.mutexLockUncancelable = mutexLockUncancelable,
.mutexUnlock = mutexUnlock,
.conditionWait = conditionWait,
.conditionWaitUncancelable = conditionWaitUncancelable,
.conditionWake = conditionWake,
.dirMake = dirMake,
.dirMakePath = dirMakePath,
.dirMakeOpenPath = dirMakeOpenPath,
.dirStat = dirStat,
.dirStatPath = dirStatPath,
.dirAccess = dirAccess,
.dirCreateFile = dirCreateFile,
.dirOpenFile = dirOpenFile,
.dirOpenDir = dirOpenDir,
.dirClose = dirClose,
.dirRealPath = dirRealPath,
.dirDeleteFile = dirDeleteFile,
.dirDeleteDir = dirDeleteDir,
.dirRename = dirRename,
.dirSymLink = dirSymLink,
.dirReadLink = dirReadLink,
.dirSetOwner = dirSetOwner,
.dirSetPermissions = dirSetPermissions,
.dirSetTimestamps = dirSetTimestamps,
.dirSetTimestampsNow = dirSetTimestampsNow,
.fileStat = fileStat,
.fileClose = fileClose,
.fileWriteStreaming = fileWriteStreaming,
.fileWritePositional = fileWritePositional,
.fileReadStreaming = fileReadStreaming,
.fileReadPositional = fileReadPositional,
.fileSeekBy = fileSeekBy,
.fileSeekTo = fileSeekTo,
.openSelfExe = openSelfExe,
.fileSync = fileSync,
.fileIsTty = fileIsTty,
.fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes,
.fileSupportsAnsiEscapeCodes = fileSupportsAnsiEscapeCodes,
.fileSetLength = fileSetLength,
.fileSetOwner = fileSetOwner,
.fileSetPermissions = fileSetPermissions,
.fileSetTimestamps = fileSetTimestamps,
.fileSetTimestampsNow = fileSetTimestampsNow,
.fileLock = fileLock,
.fileTryLock = fileTryLock,
.fileUnlock = fileUnlock,
.fileDowngradeLock = fileDowngradeLock,
.now = now,
.sleep = sleep,
.netListenIp = netListenIpUnavailable,
.netListenUnix = netListenUnixUnavailable,
.netAccept = netAcceptUnavailable,
.netBindIp = netBindIpUnavailable,
.netConnectIp = netConnectIpUnavailable,
.netConnectUnix = netConnectUnixUnavailable,
.netClose = netCloseUnavailable,
.netRead = netReadUnavailable,
.netWrite = netWriteUnavailable,
.netSend = netSendUnavailable,
.netReceive = netReceiveUnavailable,
.netInterfaceNameResolve = netInterfaceNameResolveUnavailable,
.netInterfaceName = netInterfaceNameUnavailable,
.netLookup = netLookupUnavailable,
},
};
}
pub const socket_flags_unsupported = native_os.isDarwin() or native_os == .haiku;
const have_accept4 = !socket_flags_unsupported;
const have_flock_open_flags = @hasField(posix.O, "EXLOCK");
const have_networking = native_os != .wasi;
const have_flock = @TypeOf(posix.system.flock) != void;
const have_sendmmsg = native_os == .linux;
const have_futex = switch (builtin.cpu.arch) {
.wasm32, .wasm64 => builtin.cpu.has(.wasm, .atomics),
else => true,
};
const have_preadv = switch (native_os) {
.windows, .haiku => false,
else => true,
};
const have_sig_io = posix.SIG != void and @hasField(posix.SIG, "IO");
const have_sig_pipe = posix.SIG != void and @hasField(posix.SIG, "PIPE");
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 ftruncate_sym = if (posix.lfs64_abi) posix.system.ftruncate64 else posix.system.ftruncate;
/// Trailing data:
/// 1. context
/// 2. result
const AsyncClosure = struct {
closure: Closure,
func: *const fn (context: *anyopaque, result: *anyopaque) void,
reset_event: ResetEvent,
select_condition: ?*ResetEvent,
context_alignment: Alignment,
result_offset: usize,
alloc_len: usize,
const done_reset_event: *ResetEvent = @ptrFromInt(@alignOf(ResetEvent));
fn start(closure: *Closure, t: *Threaded) void {
const ac: *AsyncClosure = @alignCast(@fieldParentPtr("closure", closure));
const current_thread = Thread.getCurrent(t);
current_thread.current_closure = closure;
ac.func(ac.contextPointer(), ac.resultPointer());
current_thread.current_closure = null;
if (@atomicRmw(?*ResetEvent, &ac.select_condition, .Xchg, done_reset_event, .release)) |select_reset| {
assert(select_reset != done_reset_event);
select_reset.set();
}
ac.reset_event.set();
}
fn resultPointer(ac: *AsyncClosure) [*]u8 {
const base: [*]u8 = @ptrCast(ac);
return base + ac.result_offset;
}
fn contextPointer(ac: *AsyncClosure) [*]u8 {
const base: [*]u8 = @ptrCast(ac);
const context_offset = ac.context_alignment.forward(@intFromPtr(ac) + @sizeOf(AsyncClosure)) - @intFromPtr(ac);
return base + context_offset;
}
fn init(
gpa: Allocator,
result_len: usize,
result_alignment: Alignment,
context: []const u8,
context_alignment: Alignment,
func: *const fn (context: *const anyopaque, result: *anyopaque) void,
) Allocator.Error!*AsyncClosure {
const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(AsyncClosure);
const worst_case_context_offset = context_alignment.forward(@sizeOf(AsyncClosure) + max_context_misalignment);
const worst_case_result_offset = result_alignment.forward(worst_case_context_offset + context.len);
const alloc_len = worst_case_result_offset + result_len;
const ac: *AsyncClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(AsyncClosure), alloc_len)));
errdefer comptime unreachable;
const actual_context_addr = context_alignment.forward(@intFromPtr(ac) + @sizeOf(AsyncClosure));
const actual_result_addr = result_alignment.forward(actual_context_addr + context.len);
const actual_result_offset = actual_result_addr - @intFromPtr(ac);
ac.* = .{
.closure = .{
.cancel_status = .none,
.start = start,
},
.func = func,
.context_alignment = context_alignment,
.result_offset = actual_result_offset,
.alloc_len = alloc_len,
.reset_event = .unset,
.select_condition = null,
};
@memcpy(ac.contextPointer()[0..context.len], context);
return ac;
}
fn waitAndDeinit(ac: *AsyncClosure, t: *Threaded, result: []u8) void {
ac.reset_event.wait(t) catch |err| switch (err) {
error.Canceled => {
ac.closure.requestCancel(t);
ac.reset_event.waitUncancelable();
},
};
@memcpy(result, ac.resultPointer()[0..result.len]);
ac.deinit(t.allocator);
}
fn deinit(ac: *AsyncClosure, gpa: Allocator) void {
const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(ac);
gpa.free(base[0..ac.alloc_len]);
}
};
fn async(
userdata: ?*anyopaque,
result: []u8,
result_alignment: Alignment,
context: []const u8,
context_alignment: Alignment,
start: *const fn (context: *const anyopaque, result: *anyopaque) void,
) ?*Io.AnyFuture {
const t: *Threaded = @ptrCast(@alignCast(userdata));
if (builtin.single_threaded) {
start(context.ptr, result.ptr);
return null;
}
const gpa = t.allocator;
const ac = AsyncClosure.init(gpa, result.len, result_alignment, context, context_alignment, start) catch {
start(context.ptr, result.ptr);
return null;
};
t.mutex.lock();
const busy_count = t.busy_count;
if (busy_count >= @intFromEnum(t.async_limit)) {
t.mutex.unlock();
ac.deinit(gpa);
start(context.ptr, result.ptr);
return null;
}
t.busy_count = busy_count + 1;
const pool_size = t.wait_group.value();
if (pool_size - busy_count == 0) {
t.wait_group.start();
const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {
t.wait_group.finish();
t.busy_count = busy_count;
t.mutex.unlock();
ac.deinit(gpa);
start(context.ptr, result.ptr);
return null;
};
thread.detach();
}
t.run_queue.prepend(&ac.closure.node);
t.mutex.unlock();
t.cond.signal();
return @ptrCast(ac);
}
fn concurrent(
userdata: ?*anyopaque,
result_len: usize,
result_alignment: Alignment,
context: []const u8,
context_alignment: Alignment,
start: *const fn (context: *const anyopaque, result: *anyopaque) void,
) Io.ConcurrentError!*Io.AnyFuture {
if (builtin.single_threaded) return error.ConcurrencyUnavailable;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const gpa = t.allocator;
const ac = AsyncClosure.init(gpa, result_len, result_alignment, context, context_alignment, start) catch
return error.ConcurrencyUnavailable;
errdefer ac.deinit(gpa);
t.mutex.lock();
defer t.mutex.unlock();
const busy_count = t.busy_count;
if (busy_count >= @intFromEnum(t.concurrent_limit))
return error.ConcurrencyUnavailable;
t.busy_count = busy_count + 1;
errdefer t.busy_count = busy_count;
const pool_size = t.wait_group.value();
if (pool_size - busy_count == 0) {
t.wait_group.start();
errdefer t.wait_group.finish();
const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch
return error.ConcurrencyUnavailable;
thread.detach();
}
t.run_queue.prepend(&ac.closure.node);
t.cond.signal();
return @ptrCast(ac);
}
const GroupClosure = struct {
closure: Closure,
group: *Io.Group,
/// Points to sibling `GroupClosure`. Used for walking the group to cancel all.
node: std.SinglyLinkedList.Node,
func: *const fn (*Io.Group, context: *anyopaque) void,
context_alignment: Alignment,
alloc_len: usize,
fn start(closure: *Closure, t: *Threaded) void {
const gc: *GroupClosure = @alignCast(@fieldParentPtr("closure", closure));
const current_thread = Thread.getCurrent(t);
const group = gc.group;
const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
const reset_event: *ResetEvent = @ptrCast(&group.context);
current_thread.current_closure = closure;
gc.func(group, gc.contextPointer());
current_thread.current_closure = null;
const prev_state = group_state.fetchSub(sync_one_pending, .acq_rel);
assert((prev_state / sync_one_pending) > 0);
if (prev_state == (sync_one_pending | sync_is_waiting)) reset_event.set();
}
fn contextPointer(gc: *GroupClosure) [*]u8 {
const base: [*]u8 = @ptrCast(gc);
const context_offset = gc.context_alignment.forward(@intFromPtr(gc) + @sizeOf(GroupClosure)) - @intFromPtr(gc);
return base + context_offset;
}
/// Does not initialize the `node` field.
fn init(
gpa: Allocator,
group: *Io.Group,
context: []const u8,
context_alignment: Alignment,
func: *const fn (*Io.Group, context: *const anyopaque) void,
) Allocator.Error!*GroupClosure {
const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(GroupClosure);
const worst_case_context_offset = context_alignment.forward(@sizeOf(GroupClosure) + max_context_misalignment);
const alloc_len = worst_case_context_offset + context.len;
const gc: *GroupClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(GroupClosure), alloc_len)));
errdefer comptime unreachable;
gc.* = .{
.closure = .{
.cancel_status = .none,
.start = start,
},
.group = group,
.node = undefined,
.func = func,
.context_alignment = context_alignment,
.alloc_len = alloc_len,
};
@memcpy(gc.contextPointer()[0..context.len], context);
return gc;
}
fn deinit(gc: *GroupClosure, gpa: Allocator) void {
const base: [*]align(@alignOf(GroupClosure)) u8 = @ptrCast(gc);
gpa.free(base[0..gc.alloc_len]);
}
const sync_is_waiting: usize = 1 << 0;
const sync_one_pending: usize = 1 << 1;
};
fn groupAsync(
userdata: ?*anyopaque,
group: *Io.Group,
context: []const u8,
context_alignment: Alignment,
start: *const fn (*Io.Group, context: *const anyopaque) void,
) void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
if (builtin.single_threaded) return start(group, context.ptr);
const gpa = t.allocator;
const gc = GroupClosure.init(gpa, group, context, context_alignment, start) catch
return start(group, context.ptr);
t.mutex.lock();
const busy_count = t.busy_count;
if (busy_count >= @intFromEnum(t.async_limit)) {
t.mutex.unlock();
gc.deinit(gpa);
return start(group, context.ptr);
}
t.busy_count = busy_count + 1;
const pool_size = t.wait_group.value();
if (pool_size - busy_count == 0) {
t.wait_group.start();
const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {
t.wait_group.finish();
t.busy_count = busy_count;
t.mutex.unlock();
gc.deinit(gpa);
return start(group, context.ptr);
};
thread.detach();
}
// Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe.
gc.node = .{ .next = @ptrCast(@alignCast(group.token)) };
group.token = &gc.node;
t.run_queue.prepend(&gc.closure.node);
// This needs to be done before unlocking the mutex to avoid a race with
// the associated task finishing.
const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
const prev_state = group_state.fetchAdd(GroupClosure.sync_one_pending, .monotonic);
assert((prev_state / GroupClosure.sync_one_pending) < (std.math.maxInt(usize) / GroupClosure.sync_one_pending));
t.mutex.unlock();
t.cond.signal();
}
fn groupConcurrent(
userdata: ?*anyopaque,
group: *Io.Group,
context: []const u8,
context_alignment: Alignment,
start: *const fn (*Io.Group, context: *const anyopaque) void,
) Io.ConcurrentError!void {
if (builtin.single_threaded) return error.ConcurrencyUnavailable;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const gpa = t.allocator;
const gc = GroupClosure.init(gpa, group, context, context_alignment, start) catch
return error.ConcurrencyUnavailable;
t.mutex.lock();
defer t.mutex.unlock();
const busy_count = t.busy_count;
if (busy_count >= @intFromEnum(t.concurrent_limit))
return error.ConcurrencyUnavailable;
t.busy_count = busy_count + 1;
errdefer t.busy_count = busy_count;
const pool_size = t.wait_group.value();
if (pool_size - busy_count == 0) {
t.wait_group.start();
errdefer t.wait_group.finish();
const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch
return error.ConcurrencyUnavailable;
thread.detach();
}
// Append to the group linked list inside the mutex to make `Io.Group.concurrent` thread-safe.
gc.node = .{ .next = @ptrCast(@alignCast(group.token)) };
group.token = &gc.node;
t.run_queue.prepend(&gc.closure.node);
// This needs to be done before unlocking the mutex to avoid a race with
// the associated task finishing.
const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
const prev_state = group_state.fetchAdd(GroupClosure.sync_one_pending, .monotonic);
assert((prev_state / GroupClosure.sync_one_pending) < (std.math.maxInt(usize) / GroupClosure.sync_one_pending));
t.cond.signal();
}
fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const gpa = t.allocator;
if (builtin.single_threaded) return;
const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
const reset_event: *ResetEvent = @ptrCast(&group.context);
const prev_state = group_state.fetchAdd(GroupClosure.sync_is_waiting, .acquire);
assert(prev_state & GroupClosure.sync_is_waiting == 0);
if ((prev_state / GroupClosure.sync_one_pending) > 0) reset_event.wait(t) catch |err| switch (err) {
error.Canceled => {
var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));
while (true) {
const gc: *GroupClosure = @fieldParentPtr("node", node);
gc.closure.requestCancel(t);
node = node.next orelse break;
}
reset_event.waitUncancelable();
},
};
var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));
while (true) {
const gc: *GroupClosure = @fieldParentPtr("node", node);
const node_next = node.next;
gc.deinit(gpa);
node = node_next orelse break;
}
}
fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const gpa = t.allocator;
if (builtin.single_threaded) return;
{
var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));
while (true) {
const gc: *GroupClosure = @fieldParentPtr("node", node);
gc.closure.requestCancel(t);
node = node.next orelse break;
}
}
const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
const reset_event: *ResetEvent = @ptrCast(&group.context);
const prev_state = group_state.fetchAdd(GroupClosure.sync_is_waiting, .acquire);
assert(prev_state & GroupClosure.sync_is_waiting == 0);
if ((prev_state / GroupClosure.sync_one_pending) > 0) reset_event.waitUncancelable();
{
var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));
while (true) {
const gc: *GroupClosure = @fieldParentPtr("node", node);
const node_next = node.next;
gc.deinit(gpa);
node = node_next orelse break;
}
}
}
fn await(
userdata: ?*anyopaque,
any_future: *Io.AnyFuture,
result: []u8,
result_alignment: Alignment,
) void {
_ = result_alignment;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const closure: *AsyncClosure = @ptrCast(@alignCast(any_future));
closure.waitAndDeinit(t, result);
}
fn cancel(
userdata: ?*anyopaque,
any_future: *Io.AnyFuture,
result: []u8,
result_alignment: Alignment,
) void {
_ = result_alignment;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const ac: *AsyncClosure = @ptrCast(@alignCast(any_future));
ac.closure.requestCancel(t);
ac.waitAndDeinit(t, result);
}
fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) Io.Cancelable!void {
if (builtin.single_threaded) unreachable; // Interface should have prevented this.
if (native_os == .netbsd) @panic("TODO");
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (prev_state == .contended) {
try futexWait(current_thread, @ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
}
while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) {
try futexWait(current_thread, @ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
}
}
fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void {
if (builtin.single_threaded) unreachable; // Interface should have prevented this.
if (native_os == .netbsd) @panic("TODO");
_ = userdata;
if (prev_state == .contended) {
futexWaitUncancelable(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
}
while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) {
futexWaitUncancelable(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
}
}
fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void {
if (builtin.single_threaded) unreachable; // Interface should have prevented this.
if (native_os == .netbsd) @panic("TODO");
_ = userdata;
_ = prev_state;
if (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .unlocked, .release) == .contended) {
futexWake(@ptrCast(&mutex.state), 1);
}
}
fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void {
if (builtin.single_threaded) unreachable; // Deadlock.
if (native_os == .netbsd) @panic("TODO");
const t: *Threaded = @ptrCast(@alignCast(userdata));
const t_io = ioBasic(t);
comptime assert(@TypeOf(cond.state) == u64);
const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state);
const cond_state = &ints[0];
const cond_epoch = &ints[1];
const one_waiter = 1;
const waiter_mask = 0xffff;
const one_signal = 1 << 16;
const signal_mask = 0xffff << 16;
var epoch = cond_epoch.load(.acquire);
var state = cond_state.fetchAdd(one_waiter, .monotonic);
assert(state & waiter_mask != waiter_mask);
state += one_waiter;
mutex.unlock(t_io);
defer mutex.lockUncancelable(t_io);
while (true) {
futexWaitUncancelable(cond_epoch, epoch);
epoch = cond_epoch.load(.acquire);
state = cond_state.load(.monotonic);
while (state & signal_mask != 0) {
const new_state = state - one_waiter - one_signal;
state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return;
}
}
}
fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void {
if (builtin.single_threaded) unreachable; // Deadlock.
if (native_os == .netbsd) @panic("TODO");
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const t_io = ioBasic(t);
comptime assert(@TypeOf(cond.state) == u64);
const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state);
const cond_state = &ints[0];
const cond_epoch = &ints[1];
const one_waiter = 1;
const waiter_mask = 0xffff;
const one_signal = 1 << 16;
const signal_mask = 0xffff << 16;
// Observe the epoch, then check the state again to see if we should wake up.
// The epoch must be observed before we check the state or we could potentially miss a wake() and deadlock:
//
// - T1: s = LOAD(&state)
// - T2: UPDATE(&s, signal)
// - T2: UPDATE(&epoch, 1) + FUTEX_WAKE(&epoch)
// - T1: e = LOAD(&epoch) (was reordered after the state load)
// - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed the state update + the epoch change)
//
// Acquire barrier to ensure the epoch load happens before the state load.
var epoch = cond_epoch.load(.acquire);
var state = cond_state.fetchAdd(one_waiter, .monotonic);
assert(state & waiter_mask != waiter_mask);
state += one_waiter;
mutex.unlock(t_io);
defer mutex.lockUncancelable(t_io);
while (true) {
try futexWait(current_thread, cond_epoch, epoch);
epoch = cond_epoch.load(.acquire);
state = cond_state.load(.monotonic);
// Try to wake up by consuming a signal and decremented the waiter we
// added previously. Acquire barrier ensures code before the wake()
// which added the signal happens before we decrement it and return.
while (state & signal_mask != 0) {
const new_state = state - one_waiter - one_signal;
state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return;
}
}
}
fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void {
if (builtin.single_threaded) unreachable; // Nothing to wake up.
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
comptime assert(@TypeOf(cond.state) == u64);
const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state);
const cond_state = &ints[0];
const cond_epoch = &ints[1];
const one_waiter = 1;
const waiter_mask = 0xffff;
const one_signal = 1 << 16;
const signal_mask = 0xffff << 16;
var state = cond_state.load(.monotonic);
while (true) {
const waiters = (state & waiter_mask) / one_waiter;
const signals = (state & signal_mask) / one_signal;
// Reserves which waiters to wake up by incrementing the signals count.
// Therefore, the signals count is always less than or equal to the
// waiters count. We don't need to Futex.wake if there's nothing to
// wake up or if other wake() threads have reserved to wake up the
// current waiters.
const wakeable = waiters - signals;
if (wakeable == 0) {
return;
}
const to_wake = switch (wake) {
.one => 1,
.all => wakeable,
};
// Reserve the amount of waiters to wake by incrementing the signals
// count. Release barrier ensures code before the wake() happens before
// the signal it posted and consumed by the wait() threads.
const new_state = state + (one_signal * to_wake);
state = cond_state.cmpxchgWeak(state, new_state, .release, .monotonic) orelse {
// Wake up the waiting threads we reserved above by changing the epoch value.
//
// A waiting thread could miss a wake up if *exactly* ((1<<32)-1)
// wake()s happen between it observing the epoch and sleeping on
// it. This is very unlikely due to how many precise amount of
// Futex.wake() calls that would be between the waiting thread's
// potential preemption.
//
// Release barrier ensures the signal being added to the state
// happens before the epoch is changed. If not, the waiting thread
// could potentially deadlock from missing both the state and epoch
// change:
//
// - T2: UPDATE(&epoch, 1) (reordered before the state change)
// - T1: e = LOAD(&epoch)
// - T1: s = LOAD(&state)
// - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch)
// - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change)
_ = cond_epoch.fetchAdd(1, .release);
if (native_os == .netbsd) @panic("TODO");
futexWake(cond_epoch, to_wake);
return;
};
}
}
const dirMake = switch (native_os) {
.windows => dirMakeWindows,
.wasi => dirMakeWasi,
else => dirMakePosix,
};
fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.mkdirat(dir.handle, sub_path_posix, mode))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.PERM => return error.PermissionDenied,
.DQUOT => return error.DiskQuota,
.EXIST => return error.PathAlreadyExists,
.FAULT => |err| return errnoBug(err),
.LOOP => return error.SymLinkLoop,
.MLINK => return error.LinkQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.NOTDIR => return error.NotDir,
.ROFS => return error.ReadOnlyFileSystem,
// dragonfly: when dir_fd is unlinked from filesystem
.NOTCONN => return error.FileNotFound,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirMakeWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void {
if (builtin.link_libc) return dirMakePosix(userdata, dir, sub_path, mode);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.path_create_directory(dir.handle, sub_path.ptr, sub_path.len)) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.PERM => return error.PermissionDenied,
.DQUOT => return error.DiskQuota,
.EXIST => return error.PathAlreadyExists,
.FAULT => |err| return errnoBug(err),
.LOOP => return error.SymLinkLoop,
.MLINK => return error.LinkQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.NOTDIR => return error.NotDir,
.ROFS => return error.ReadOnlyFileSystem,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirMakeWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.checkCancel();
const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
_ = mode;
const sub_dir_handle = windows.OpenFile(sub_path_w.span(), .{
.dir = dir.handle,
.access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE,
.creation = windows.FILE_CREATE,
.filter = .dir_only,
}) catch |err| switch (err) {
error.IsDir => return error.Unexpected,
error.PipeBusy => return error.Unexpected,
error.NoDevice => return error.Unexpected,
error.WouldBlock => return error.Unexpected,
error.AntivirusInterference => return error.Unexpected,
else => |e| return e,
};
windows.CloseHandle(sub_dir_handle);
}
fn dirMakePath(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
mode: Io.Dir.Mode,
) Io.Dir.MakePathError!Io.Dir.MakePathStatus {
const t: *Threaded = @ptrCast(@alignCast(userdata));
var it = std.fs.path.componentIterator(sub_path);
var status: Io.Dir.MakePathStatus = .existed;
var component = it.last() orelse return error.BadPathName;
while (true) {
if (dirMake(t, dir, component.path, mode)) |_| {
status = .created;
} else |err| switch (err) {
error.PathAlreadyExists => {
// stat the file and return an error if it's not a directory
// this is important because otherwise a dangling symlink
// could cause an infinite loop
check_dir: {
// workaround for windows, see https://github.com/ziglang/zig/issues/16738
const fstat = dirStatPath(t, dir, component.path, .{}) catch |stat_err| switch (stat_err) {
error.IsDir => break :check_dir,
else => |e| return e,
};
if (fstat.kind != .directory) return error.NotDir;
}
},
error.FileNotFound => |e| {
component = it.previous() orelse return e;
continue;
},
else => |e| return e,
}
component = it.next() orelse return status;
}
}
const dirMakeOpenPath = switch (native_os) {
.windows => dirMakeOpenPathWindows,
.wasi => dirMakeOpenPathWasi,
else => dirMakeOpenPathPosix,
};
fn dirMakeOpenPathPosix(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.OpenOptions,
) Io.Dir.MakeOpenPathError!Io.Dir {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const t_io = ioBasic(t);
return dirOpenDirPosix(t, dir, sub_path, options) catch |err| switch (err) {
error.FileNotFound => {
try dir.makePath(t_io, sub_path);
return dirOpenDirPosix(t, dir, sub_path, options);
},
else => |e| return e,
};
}
fn dirMakeOpenPathWindows(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.OpenOptions,
) Io.Dir.MakeOpenPathError!Io.Dir {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const w = windows;
const access_mask = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
w.SYNCHRONIZE | w.FILE_TRAVERSE |
(if (options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0));
var it = std.fs.path.componentIterator(sub_path);
// If there are no components in the path, then create a dummy component with the full path.
var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{
.name = "",
.path = sub_path,
};
while (true) {
try current_thread.checkCancel();
const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, component.path);
const sub_path_w = sub_path_w_array.span();
const is_last = it.peekNext() == null;
const create_disposition: u32 = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE;
var result: Io.Dir = .{ .handle = undefined };
const path_len_bytes: u16 = @intCast(sub_path_w.len * 2);
var nt_name: w.UNICODE_STRING = .{
.Length = path_len_bytes,
.MaximumLength = path_len_bytes,
.Buffer = @constCast(sub_path_w.ptr),
};
var attr: w.OBJECT_ATTRIBUTES = .{
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
.Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
.ObjectName = &nt_name,
.SecurityDescriptor = null,
.SecurityQualityOfService = null,
};
const open_reparse_point: w.DWORD = if (!options.follow_symlinks) w.FILE_OPEN_REPARSE_POINT else 0x0;
var io_status_block: w.IO_STATUS_BLOCK = undefined;
const rc = w.ntdll.NtCreateFile(
&result.handle,
access_mask,
&attr,
&io_status_block,
null,
w.FILE_ATTRIBUTE_NORMAL,
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE,
create_disposition,
w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point,
null,
0,
);
switch (rc) {
.SUCCESS => {
component = it.next() orelse return result;
w.CloseHandle(result.handle);
continue;
},
.OBJECT_NAME_INVALID => return error.BadPathName,
.OBJECT_NAME_COLLISION => {
assert(!is_last);
// stat the file and return an error if it's not a directory
// this is important because otherwise a dangling symlink
// could cause an infinite loop
check_dir: {
// workaround for windows, see https://github.com/ziglang/zig/issues/16738
const fstat = dirStatPathWindows(t, dir, component.path, .{
.follow_symlinks = options.follow_symlinks,
}) catch |stat_err| switch (stat_err) {
error.IsDir => break :check_dir,
else => |e| return e,
};
if (fstat.kind != .directory) return error.NotDir;
}
component = it.next().?;
continue;
},
.OBJECT_NAME_NOT_FOUND,
.OBJECT_PATH_NOT_FOUND,
=> {
component = it.previous() orelse return error.FileNotFound;
continue;
},
.NOT_A_DIRECTORY => return error.NotDir,
// This can happen if the directory has 'List folder contents' permission set to 'Deny'
// and the directory is trying to be opened for iteration.
.ACCESS_DENIED => return error.AccessDenied,
.INVALID_PARAMETER => |err| return w.statusBug(err),
else => return w.unexpectedStatus(rc),
}
}
}
fn dirMakeOpenPathWasi(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.OpenOptions,
) Io.Dir.MakeOpenPathError!Io.Dir {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const t_io = ioBasic(t);
return dirOpenDirWasi(t, dir, sub_path, options) catch |err| switch (err) {
error.FileNotFound => {
try dir.makePath(t_io, sub_path);
return dirOpenDirWasi(t, dir, sub_path, options);
},
else => |e| return e,
};
}
fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const file: Io.File = .{ .handle = dir.handle };
return fileStat(t, file);
}
const dirStatPath = switch (native_os) {
.linux => dirStatPathLinux,
.windows => dirStatPathWindows,
.wasi => dirStatPathWasi,
else => dirStatPathPosix,
};
fn dirStatPathLinux(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.StatPathOptions,
) Io.Dir.StatPathError!Io.File.Stat {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const linux = std.os.linux;
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
const flags: u32 = linux.AT.NO_AUTOMOUNT |
@as(u32, if (!options.follow_symlinks) linux.AT.SYMLINK_NOFOLLOW else 0);
try current_thread.beginSyscall();
while (true) {
var statx = std.mem.zeroes(linux.Statx);
const rc = linux.statx(
dir.handle,
sub_path_posix,
flags,
linux.STATX_INO | linux.STATX_SIZE | linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME,
&statx,
);
switch (linux.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
return statFromLinux(&statx);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => |err| return errnoBug(err), // Handled by pathToPosix() above.
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirStatPathPosix(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.StatPathOptions,
) Io.Dir.StatPathError!Io.File.Stat {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0;
try current_thread.beginSyscall();
while (true) {
var stat = std.mem.zeroes(posix.Stat);
switch (posix.errno(fstatat_sym(dir.handle, sub_path_posix, &stat, flags))) {
.SUCCESS => {
current_thread.endSyscall();
return statFromPosix(&stat);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.NOMEM => return error.SystemResources,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.FAULT => |err| return errnoBug(err),
.NAMETOOLONG => return error.NameTooLong,
.LOOP => return error.SymLinkLoop,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.FileNotFound,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirStatPathWindows(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.StatPathOptions,
) Io.Dir.StatPathError!Io.File.Stat {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const file = try dirOpenFileWindows(t, dir, sub_path, .{
.follow_symlinks = options.follow_symlinks,
});
defer windows.CloseHandle(file.handle);
return fileStatWindows(t, file);
}
fn dirStatPathWasi(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.StatPathOptions,
) Io.Dir.StatPathError!Io.File.Stat {
if (builtin.link_libc) return dirStatPathPosix(userdata, dir, sub_path, options);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const wasi = std.os.wasi;
const flags: wasi.lookupflags_t = .{
.SYMLINK_FOLLOW = options.follow_symlinks,
};
var stat: wasi.filestat_t = undefined;
try current_thread.beginSyscall();
while (true) {
switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) {
.SUCCESS => {
current_thread.endSyscall();
return statFromWasi(&stat);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.NOMEM => return error.SystemResources,
.ACCES => return error.AccessDenied,
.FAULT => |err| return errnoBug(err),
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.FileNotFound,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const fileStat = switch (native_os) {
.linux => fileStatLinux,
.windows => fileStatWindows,
.wasi => fileStatWasi,
else => fileStatPosix,
};
fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (posix.Stat == void) return error.Streaming;
try current_thread.beginSyscall();
while (true) {
var stat = std.mem.zeroes(posix.Stat);
switch (posix.errno(fstat_sym(file.handle, &stat))) {
.SUCCESS => {
current_thread.endSyscall();
return statFromPosix(&stat);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.NOMEM => return error.SystemResources,
.ACCES => return error.AccessDenied,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileStatLinux(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const linux = std.os.linux;
try current_thread.beginSyscall();
while (true) {
var statx = std.mem.zeroes(linux.Statx);
const rc = linux.statx(
file.handle,
"",
linux.AT.EMPTY_PATH,
linux.STATX_INO | linux.STATX_SIZE | linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME,
&statx,
);
switch (linux.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
return statFromLinux(&statx);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.LOOP => |err| return errnoBug(err),
.NAMETOOLONG => |err| return errnoBug(err),
.NOENT => |err| return errnoBug(err),
.NOMEM => return error.SystemResources,
.NOTDIR => |err| return errnoBug(err),
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileStatWindows(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.checkCancel();
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
var info: windows.FILE_ALL_INFORMATION = undefined;
const rc = windows.ntdll.NtQueryInformationFile(file.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation);
switch (rc) {
.SUCCESS => {},
// Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
// size provided. This is treated as success because the type of variable-length information that this would be relevant for
// (name, volume name, etc) we don't care about.
.BUFFER_OVERFLOW => {},
.INVALID_PARAMETER => |err| return windows.statusBug(err),
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
}
return .{
.inode = info.InternalInformation.IndexNumber,
.size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
.mode = 0,
.kind = if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) reparse_point: {
var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined;
const tag_rc = windows.ntdll.NtQueryInformationFile(file.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation);
switch (tag_rc) {
.SUCCESS => {},
// INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e
.INFO_LENGTH_MISMATCH => |err| return windows.statusBug(err),
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
}
if (tag_info.ReparseTag & windows.reparse_tag_name_surrogate_bit != 0) {
break :reparse_point .sym_link;
}
// Unknown reparse point
break :reparse_point .unknown;
} else if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0)
.directory
else
.file,
.atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
.mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
.ctime = windows.fromSysTime(info.BasicInformation.ChangeTime),
};
}
fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat {
if (builtin.link_libc) return fileStatPosix(userdata, file);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
var stat: std.os.wasi.filestat_t = undefined;
switch (std.os.wasi.fd_filestat_get(file.handle, &stat)) {
.SUCCESS => {
current_thread.endSyscall();
return statFromWasi(&stat);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.NOMEM => return error.SystemResources,
.ACCES => return error.AccessDenied,
.NOTCAPABLE => return error.AccessDenied,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirAccess = switch (native_os) {
.windows => dirAccessWindows,
.wasi => dirAccessWasi,
else => dirAccessPosix,
};
fn dirAccessPosix(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.AccessOptions,
) Io.Dir.AccessError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
const flags: u32 = @as(u32, if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0);
const mode: u32 =
@as(u32, if (options.read) posix.R_OK else 0) |
@as(u32, if (options.write) posix.W_OK else 0) |
@as(u32, if (options.execute) posix.X_OK else 0);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.faccessat(dir.handle, sub_path_posix, mode, flags))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.ROFS => return error.ReadOnlyFileSystem,
.LOOP => return error.SymLinkLoop,
.TXTBSY => return error.FileBusy,
.NOTDIR => return error.FileNotFound,
.NOENT => return error.FileNotFound,
.NAMETOOLONG => return error.NameTooLong,
.INVAL => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.IO => return error.InputOutput,
.NOMEM => return error.SystemResources,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirAccessWasi(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.AccessOptions,
) Io.Dir.AccessError!void {
if (builtin.link_libc) return dirAccessPosix(userdata, dir, sub_path, options);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const wasi = std.os.wasi;
const flags: wasi.lookupflags_t = .{
.SYMLINK_FOLLOW = options.follow_symlinks,
};
var stat: wasi.filestat_t = undefined;
try current_thread.beginSyscall();
while (true) {
switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.NOMEM => return error.SystemResources,
.ACCES => return error.AccessDenied,
.FAULT => |err| return errnoBug(err),
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.FileNotFound,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
if (!options.read and !options.write and !options.execute)
return;
var directory: wasi.fdstat_t = undefined;
if (wasi.fd_fdstat_get(dir.handle, &directory) != .SUCCESS)
return error.AccessDenied;
var rights: wasi.rights_t = .{};
if (options.read) {
if (stat.filetype == .DIRECTORY) {
rights.FD_READDIR = true;
} else {
rights.FD_READ = true;
}
}
if (options.write)
rights.FD_WRITE = true;
// No validation for execution.
// https://github.com/ziglang/zig/issues/18882
const rights_int: u64 = @bitCast(rights);
const inheriting_int: u64 = @bitCast(directory.fs_rights_inheriting);
if ((rights_int & inheriting_int) != rights_int)
return error.AccessDenied;
}
fn dirAccessWindows(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.AccessOptions,
) Io.Dir.AccessError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.checkCancel();
_ = options; // TODO
const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w = sub_path_w_array.span();
if (sub_path_w[0] == '.' and sub_path_w[1] == 0) return;
if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) return;
const path_len_bytes = std.math.cast(u16, std.mem.sliceTo(sub_path_w, 0).len * 2) orelse
return error.NameTooLong;
var nt_name: windows.UNICODE_STRING = .{
.Length = path_len_bytes,
.MaximumLength = path_len_bytes,
.Buffer = @constCast(sub_path_w.ptr),
};
var attr = windows.OBJECT_ATTRIBUTES{
.Length = @sizeOf(windows.OBJECT_ATTRIBUTES),
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
.Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
.ObjectName = &nt_name,
.SecurityDescriptor = null,
.SecurityQualityOfService = null,
};
var basic_info: windows.FILE_BASIC_INFORMATION = undefined;
switch (windows.ntdll.NtQueryAttributesFile(&attr, &basic_info)) {
.SUCCESS => return,
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
.OBJECT_NAME_INVALID => |err| return windows.statusBug(err),
.INVALID_PARAMETER => |err| return windows.statusBug(err),
.ACCESS_DENIED => return error.AccessDenied,
.OBJECT_PATH_SYNTAX_BAD => |err| return windows.statusBug(err),
else => |rc| return windows.unexpectedStatus(rc),
}
}
const dirCreateFile = switch (native_os) {
.windows => dirCreateFileWindows,
.wasi => dirCreateFileWasi,
else => dirCreateFilePosix,
};
fn dirCreateFilePosix(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
flags: Io.File.CreateFlags,
) Io.File.OpenError!Io.File {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
var os_flags: posix.O = .{
.ACCMODE = if (flags.read) .RDWR else .WRONLY,
.CREAT = true,
.TRUNC = flags.truncate,
.EXCL = flags.exclusive,
};
if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true;
if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true;
// Use the O locking flags if the os supports them to acquire the lock
// atomically. Note that the NONBLOCK flag is removed after the openat()
// call is successful.
if (have_flock_open_flags) switch (flags.lock) {
.none => {},
.shared => {
os_flags.SHLOCK = true;
os_flags.NONBLOCK = flags.lock_nonblocking;
},
.exclusive => {
os_flags.EXLOCK = true;
os_flags.NONBLOCK = flags.lock_nonblocking;
},
};
try current_thread.beginSyscall();
const fd: posix.fd_t = while (true) {
const rc = openat_sym(dir.handle, sub_path_posix, os_flags, flags.mode);
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
break @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.INVAL => return error.BadPathName,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.ACCES => return error.AccessDenied,
.FBIG => return error.FileTooBig,
.OVERFLOW => return error.FileTooBig,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MFILE => return error.ProcessFdQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NFILE => return error.SystemFdQuotaExceeded,
.NODEV => return error.NoDevice,
.NOENT => return error.FileNotFound,
.SRCH => return error.ProcessNotFound,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
.EXIST => return error.PathAlreadyExists,
.BUSY => return error.DeviceBusy,
.OPNOTSUPP => return error.FileLocksUnsupported,
.AGAIN => return error.WouldBlock,
.TXTBSY => return error.FileBusy,
.NXIO => return error.NoDevice,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
};
errdefer posix.close(fd);
if (have_flock and !have_flock_open_flags and flags.lock != .none) {
const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0;
const lock_flags = switch (flags.lock) {
.none => unreachable,
.shared => posix.LOCK.SH | lock_nonblocking,
.exclusive => posix.LOCK.EX | lock_nonblocking,
};
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.flock(fd, lock_flags))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => |err| return errnoBug(err), // invalid parameters
.NOLCK => return error.SystemResources,
.AGAIN => return error.WouldBlock,
.OPNOTSUPP => return error.FileLocksUnsupported,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
if (have_flock_open_flags and flags.lock_nonblocking) {
try current_thread.beginSyscall();
var fl_flags: usize = while (true) {
const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0));
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
break @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |err| {
current_thread.endSyscall();
return posix.unexpectedErrno(err);
},
}
};
fl_flags |= @as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK"));
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |err| {
current_thread.endSyscall();
return posix.unexpectedErrno(err);
},
}
}
}
return .{ .handle = fd };
}
fn dirCreateFileWindows(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
flags: Io.File.CreateFlags,
) Io.File.OpenError!Io.File {
const w = windows;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.checkCancel();
const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w = sub_path_w_array.span();
const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0;
const handle = try w.OpenFile(sub_path_w, .{
.dir = dir.handle,
.access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag,
.creation = if (flags.exclusive)
@as(u32, w.FILE_CREATE)
else if (flags.truncate)
@as(u32, w.FILE_OVERWRITE_IF)
else
@as(u32, w.FILE_OPEN_IF),
});
errdefer w.CloseHandle(handle);
var io_status_block: w.IO_STATUS_BLOCK = undefined;
const range_off: w.LARGE_INTEGER = 0;
const range_len: w.LARGE_INTEGER = 1;
const exclusive = switch (flags.lock) {
.none => return .{ .handle = handle },
.shared => false,
.exclusive => true,
};
try w.LockFile(
handle,
null,
null,
null,
&io_status_block,
&range_off,
&range_len,
null,
@intFromBool(flags.lock_nonblocking),
@intFromBool(exclusive),
);
return .{ .handle = handle };
}
fn dirCreateFileWasi(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
flags: Io.File.CreateFlags,
) Io.File.OpenError!Io.File {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const wasi = std.os.wasi;
const lookup_flags: wasi.lookupflags_t = .{};
const oflags: wasi.oflags_t = .{
.CREAT = true,
.TRUNC = flags.truncate,
.EXCL = flags.exclusive,
};
const fdflags: wasi.fdflags_t = .{};
const base: wasi.rights_t = .{
.FD_READ = flags.read,
.FD_WRITE = true,
.FD_DATASYNC = true,
.FD_SEEK = true,
.FD_TELL = true,
.FD_FDSTAT_SET_FLAGS = true,
.FD_SYNC = true,
.FD_ALLOCATE = true,
.FD_ADVISE = true,
.FD_FILESTAT_SET_TIMES = true,
.FD_FILESTAT_SET_SIZE = true,
.FD_FILESTAT_GET = true,
// POLL_FD_READWRITE only grants extra rights if the corresponding FD_READ and/or
// FD_WRITE is also set.
.POLL_FD_READWRITE = true,
};
const inheriting: wasi.rights_t = .{};
var fd: posix.fd_t = undefined;
try current_thread.beginSyscall();
while (true) {
switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) {
.SUCCESS => {
current_thread.endSyscall();
return .{ .handle = fd };
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.INVAL => return error.BadPathName,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.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,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
.EXIST => return error.PathAlreadyExists,
.BUSY => return error.DeviceBusy,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirOpenFile = switch (native_os) {
.windows => dirOpenFileWindows,
.wasi => dirOpenFileWasi,
else => dirOpenFilePosix,
};
fn dirOpenFilePosix(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
flags: Io.File.OpenFlags,
) Io.File.OpenError!Io.File {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
var os_flags: posix.O = switch (native_os) {
.wasi => .{
.read = flags.mode != .write_only,
.write = flags.mode != .read_only,
},
else => .{
.ACCMODE = switch (flags.mode) {
.read_only => .RDONLY,
.write_only => .WRONLY,
.read_write => .RDWR,
},
},
};
if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true;
if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true;
if (@hasField(posix.O, "NOCTTY")) os_flags.NOCTTY = !flags.allow_ctty;
// Use the O locking flags if the os supports them to acquire the lock
// atomically. Note that the NONBLOCK flag is removed after the openat()
// call is successful.
if (have_flock_open_flags) switch (flags.lock) {
.none => {},
.shared => {
os_flags.SHLOCK = true;
os_flags.NONBLOCK = flags.lock_nonblocking;
},
.exclusive => {
os_flags.EXLOCK = true;
os_flags.NONBLOCK = flags.lock_nonblocking;
},
};
try current_thread.beginSyscall();
const fd: posix.fd_t = while (true) {
const rc = openat_sym(dir.handle, sub_path_posix, os_flags, @as(posix.mode_t, 0));
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
break @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.INVAL => return error.BadPathName,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.ACCES => return error.AccessDenied,
.FBIG => return error.FileTooBig,
.OVERFLOW => return error.FileTooBig,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MFILE => return error.ProcessFdQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NFILE => return error.SystemFdQuotaExceeded,
.NODEV => return error.NoDevice,
.NOENT => return error.FileNotFound,
.SRCH => return error.ProcessNotFound,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
.EXIST => return error.PathAlreadyExists,
.BUSY => return error.DeviceBusy,
.OPNOTSUPP => return error.FileLocksUnsupported,
.AGAIN => return error.WouldBlock,
.TXTBSY => return error.FileBusy,
.NXIO => return error.NoDevice,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
};
errdefer posix.close(fd);
if (have_flock and !have_flock_open_flags and flags.lock != .none) {
const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0;
const lock_flags = switch (flags.lock) {
.none => unreachable,
.shared => posix.LOCK.SH | lock_nonblocking,
.exclusive => posix.LOCK.EX | lock_nonblocking,
};
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.flock(fd, lock_flags))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => |err| return errnoBug(err), // invalid parameters
.NOLCK => return error.SystemResources,
.AGAIN => return error.WouldBlock,
.OPNOTSUPP => return error.FileLocksUnsupported,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
if (have_flock_open_flags and flags.lock_nonblocking) {
try current_thread.beginSyscall();
var fl_flags: usize = while (true) {
const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0));
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
break @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |err| {
current_thread.endSyscall();
return posix.unexpectedErrno(err);
},
}
};
fl_flags |= @as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK"));
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |err| {
current_thread.endSyscall();
return posix.unexpectedErrno(err);
},
}
}
}
return .{ .handle = fd };
}
fn dirOpenFileWindows(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
flags: Io.File.OpenFlags,
) Io.File.OpenError!Io.File {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
const sub_path_w = sub_path_w_array.span();
const dir_handle = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle;
return dirOpenFileWtf16(t, dir_handle, sub_path_w, flags);
}
pub fn dirOpenFileWtf16(
t: *Threaded,
dir_handle: ?windows.HANDLE,
sub_path_w: [:0]const u16,
flags: Io.File.OpenFlags,
) Io.File.OpenError!Io.File {
if (std.mem.eql(u16, sub_path_w, &.{'.'})) return error.IsDir;
if (std.mem.eql(u16, sub_path_w, &.{ '.', '.' })) return error.IsDir;
const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
const current_thread = Thread.getCurrent(t);
const w = windows;
var nt_name: w.UNICODE_STRING = .{
.Length = path_len_bytes,
.MaximumLength = path_len_bytes,
.Buffer = @constCast(sub_path_w.ptr),
};
var attr: w.OBJECT_ATTRIBUTES = .{
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
.RootDirectory = dir_handle,
.Attributes = 0,
.ObjectName = &nt_name,
.SecurityDescriptor = null,
.SecurityQualityOfService = null,
};
var io_status_block: w.IO_STATUS_BLOCK = undefined;
const blocking_flag: w.ULONG = w.FILE_SYNCHRONOUS_IO_NONALERT;
const file_or_dir_flag: w.ULONG = w.FILE_NON_DIRECTORY_FILE;
// If we're not following symlinks, we need to ensure we don't pass in any
// synchronization flags such as FILE_SYNCHRONOUS_IO_NONALERT.
const create_file_flags: w.ULONG = file_or_dir_flag |
if (flags.follow_symlinks) blocking_flag else w.FILE_OPEN_REPARSE_POINT;
// There are multiple kernel bugs being worked around with retries.
const max_attempts = 13;
var attempt: u5 = 0;
const handle = while (true) {
try current_thread.checkCancel();
var result: w.HANDLE = undefined;
const rc = w.ntdll.NtCreateFile(
&result,
w.SYNCHRONIZE |
(if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) |
(if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0),
&attr,
&io_status_block,
null,
w.FILE_ATTRIBUTE_NORMAL,
w.FILE_SHARE_WRITE | w.FILE_SHARE_READ | w.FILE_SHARE_DELETE,
w.FILE_OPEN,
create_file_flags,
null,
0,
);
switch (rc) {
.SUCCESS => break result,
.OBJECT_NAME_INVALID => return error.BadPathName,
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
.BAD_NETWORK_PATH => return error.NetworkNotFound, // \\server was not found
.BAD_NETWORK_NAME => return error.NetworkNotFound, // \\server was found but \\server\share wasn't
.NO_MEDIA_IN_DEVICE => return error.NoDevice,
.INVALID_PARAMETER => |err| return w.statusBug(err),
.SHARING_VIOLATION => {
// This occurs if the file attempting to be opened is a running
// executable. However, there's a kernel bug: the error may be
// incorrectly returned for an indeterminate amount of time
// after an executable file is closed. Here we work around the
// kernel bug with retry attempts.
if (max_attempts - attempt == 0) return error.SharingViolation;
_ = w.kernel32.SleepEx((@as(u32, 1) << attempt) >> 1, w.TRUE);
attempt += 1;
continue;
},
.ACCESS_DENIED => return error.AccessDenied,
.PIPE_BUSY => return error.PipeBusy,
.PIPE_NOT_AVAILABLE => return error.NoDevice,
.OBJECT_PATH_SYNTAX_BAD => |err| return w.statusBug(err),
.OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
.FILE_IS_A_DIRECTORY => return error.IsDir,
.NOT_A_DIRECTORY => return error.NotDir,
.USER_MAPPED_FILE => return error.AccessDenied,
.INVALID_HANDLE => |err| return w.statusBug(err),
.DELETE_PENDING => {
// This error means that there *was* a file in this location on
// the file system, but it was deleted. However, the OS is not
// finished with the deletion operation, and so this CreateFile
// call has failed. Here, we simulate the kernel bug being
// fixed by sleeping and retrying until the error goes away.
if (max_attempts - attempt == 0) return error.SharingViolation;
_ = w.kernel32.SleepEx((@as(u32, 1) << attempt) >> 1, w.TRUE);
attempt += 1;
continue;
},
.VIRUS_INFECTED, .VIRUS_DELETED => return error.AntivirusInterference,
else => return w.unexpectedStatus(rc),
}
};
errdefer w.CloseHandle(handle);
const range_off: w.LARGE_INTEGER = 0;
const range_len: w.LARGE_INTEGER = 1;
const exclusive = switch (flags.lock) {
.none => return .{ .handle = handle },
.shared => false,
.exclusive => true,
};
try w.LockFile(
handle,
null,
null,
null,
&io_status_block,
&range_off,
&range_len,
null,
@intFromBool(flags.lock_nonblocking),
@intFromBool(exclusive),
);
return .{ .handle = handle };
}
fn dirOpenFileWasi(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
flags: Io.File.OpenFlags,
) Io.File.OpenError!Io.File {
if (builtin.link_libc) return dirOpenFilePosix(userdata, dir, sub_path, flags);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const wasi = std.os.wasi;
var base: std.os.wasi.rights_t = .{};
// POLL_FD_READWRITE only grants extra rights if the corresponding FD_READ and/or FD_WRITE
// is also set.
if (flags.isRead()) {
base.FD_READ = true;
base.FD_TELL = true;
base.FD_SEEK = true;
base.FD_FILESTAT_GET = true;
base.POLL_FD_READWRITE = true;
}
if (flags.isWrite()) {
base.FD_WRITE = true;
base.FD_TELL = true;
base.FD_SEEK = true;
base.FD_DATASYNC = true;
base.FD_FDSTAT_SET_FLAGS = true;
base.FD_SYNC = true;
base.FD_ALLOCATE = true;
base.FD_ADVISE = true;
base.FD_FILESTAT_SET_TIMES = true;
base.FD_FILESTAT_SET_SIZE = true;
base.POLL_FD_READWRITE = true;
}
const lookup_flags: wasi.lookupflags_t = .{};
const oflags: wasi.oflags_t = .{};
const inheriting: wasi.rights_t = .{};
const fdflags: wasi.fdflags_t = .{};
var fd: posix.fd_t = undefined;
try current_thread.beginSyscall();
while (true) {
switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) {
.SUCCESS => {
errdefer posix.close(fd);
current_thread.endSyscall();
return .{ .handle = fd };
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.ACCES => return error.AccessDenied,
.FBIG => return error.FileTooBig,
.OVERFLOW => return error.FileTooBig,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MFILE => return error.ProcessFdQuotaExceeded,
.NFILE => return error.SystemFdQuotaExceeded,
.NODEV => return error.NoDevice,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
.BUSY => return error.DeviceBusy,
.NOTCAPABLE => return error.AccessDenied,
.NAMETOOLONG => return error.NameTooLong,
.INVAL => return error.BadPathName,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirOpenDir = switch (native_os) {
.wasi => dirOpenDirWasi,
.haiku => dirOpenDirHaiku,
else => dirOpenDirPosix,
};
/// This function is also used for WASI when libc is linked.
fn dirOpenDirPosix(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.OpenOptions,
) Io.Dir.OpenError!Io.Dir {
const t: *Threaded = @ptrCast(@alignCast(userdata));
if (is_windows) {
const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
return dirOpenDirWindows(t, dir, sub_path_w.span(), options);
}
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
var flags: posix.O = switch (native_os) {
.wasi => .{
.read = true,
.NOFOLLOW = !options.follow_symlinks,
.DIRECTORY = true,
},
else => .{
.ACCMODE = .RDONLY,
.NOFOLLOW = !options.follow_symlinks,
.DIRECTORY = true,
.CLOEXEC = true,
},
};
if (@hasField(posix.O, "PATH") and !options.iterate)
flags.PATH = true;
try current_thread.beginSyscall();
while (true) {
const rc = openat_sym(dir.handle, sub_path_posix, flags, @as(usize, 0));
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
return .{ .handle = @intCast(rc) };
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.INVAL => return error.BadPathName,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.ACCES => return error.AccessDenied,
.LOOP => return error.SymLinkLoop,
.MFILE => return error.ProcessFdQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NFILE => return error.SystemFdQuotaExceeded,
.NODEV => return error.NoDevice,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
.BUSY => return error.DeviceBusy,
.NXIO => return error.NoDevice,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirOpenDirHaiku(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.OpenOptions,
) Io.Dir.OpenError!Io.Dir {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
_ = options;
try current_thread.beginSyscall();
while (true) {
const rc = posix.system._kern_open_dir(dir.handle, sub_path_posix);
if (rc >= 0) {
current_thread.endSyscall();
return .{ .handle = rc };
}
switch (@as(posix.E, @enumFromInt(rc))) {
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.ACCES => return error.AccessDenied,
.LOOP => return error.SymLinkLoop,
.MFILE => return error.ProcessFdQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NFILE => return error.SystemFdQuotaExceeded,
.NODEV => return error.NoDevice,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
.BUSY => return error.DeviceBusy,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
pub fn dirOpenDirWindows(
t: *Io.Threaded,
dir: Io.Dir,
sub_path_w: [:0]const u16,
options: Io.Dir.OpenOptions,
) Io.Dir.OpenError!Io.Dir {
const current_thread = Thread.getCurrent(t);
const w = windows;
// TODO remove some of these flags if options.access_sub_paths is false
const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA |
w.SYNCHRONIZE | w.FILE_TRAVERSE;
const access_mask: u32 = if (options.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags;
const path_len_bytes: u16 = @intCast(sub_path_w.len * 2);
var nt_name: w.UNICODE_STRING = .{
.Length = path_len_bytes,
.MaximumLength = path_len_bytes,
.Buffer = @constCast(sub_path_w.ptr),
};
var attr: w.OBJECT_ATTRIBUTES = .{
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
.Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
.ObjectName = &nt_name,
.SecurityDescriptor = null,
.SecurityQualityOfService = null,
};
const open_reparse_point: w.DWORD = if (!options.follow_symlinks) w.FILE_OPEN_REPARSE_POINT else 0x0;
var io_status_block: w.IO_STATUS_BLOCK = undefined;
var result: Io.Dir = .{ .handle = undefined };
try current_thread.checkCancel();
const rc = w.ntdll.NtCreateFile(
&result.handle,
access_mask,
&attr,
&io_status_block,
null,
w.FILE_ATTRIBUTE_NORMAL,
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE,
w.FILE_OPEN,
w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point,
null,
0,
);
switch (rc) {
.SUCCESS => return result,
.OBJECT_NAME_INVALID => return error.BadPathName,
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
.OBJECT_NAME_COLLISION => |err| return w.statusBug(err),
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
.NOT_A_DIRECTORY => return error.NotDir,
// This can happen if the directory has 'List folder contents' permission set to 'Deny'
// and the directory is trying to be opened for iteration.
.ACCESS_DENIED => return error.AccessDenied,
.INVALID_PARAMETER => |err| return w.statusBug(err),
else => return w.unexpectedStatus(rc),
}
}
const MakeOpenDirAccessMaskWOptions = struct {
no_follow: bool,
create_disposition: u32,
};
fn dirClose(userdata: ?*anyopaque, dir: Io.Dir) void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
posix.close(dir.handle);
}
const dirRealPath = switch (native_os) {
.windows => dirRealPathWindows,
else => dirRealPathPosix,
};
fn dirRealPathWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, out_buffer: []u8) Io.Dir.RealPathError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const w = windows;
const current_thread = Thread.getCurrent(t);
try current_thread.checkCancel();
var path_name_w = try w.sliceToPrefixedFileW(dir.handle, sub_path);
const access_mask = w.GENERIC_READ | w.SYNCHRONIZE;
const share_access = w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE;
const creation = w.FILE_OPEN;
const h_file = blk: {
const res = w.OpenFile(path_name_w.span(), .{
.dir = dir.handle,
.access_mask = access_mask,
.share_access = share_access,
.creation = creation,
.filter = .any,
}) catch |err| switch (err) {
error.WouldBlock => unreachable,
else => |e| return e,
};
break :blk res;
};
defer w.CloseHandle(h_file);
const wide_slice = w.GetFinalPathNameByHandle(h_file, .{}, out_buffer);
const len = std.unicode.calcWtf8Len(wide_slice);
if (len > out_buffer.len)
return error.NameTooLong;
return std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
}
fn dirRealPathPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, out_buffer: []u8) Io.Dir.RealPathError!usize {
if (native_os == .wasi) @compileError("unsupported operating system");
const max_path_bytes = std.fs.max_path_bytes;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
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;
try current_thread.beginSyscall();
const fd: posix.fd_t = while (true) {
const rc = openat_sym(dir.handle, sub_path_posix, flags, 0);
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
break @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.INVAL => return error.BadPathName,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.ACCES => return error.AccessDenied,
.FBIG => return error.FileTooBig,
.OVERFLOW => return error.FileTooBig,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MFILE => return error.ProcessFdQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NFILE => return error.SystemFdQuotaExceeded,
.NODEV => return error.NoDevice,
.NOENT => return error.FileNotFound,
.SRCH => return error.ProcessNotFound,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
.EXIST => return error.PathAlreadyExists,
.BUSY => return error.DeviceBusy,
.NXIO => return error.NoDevice,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
};
errdefer posix.close(fd);
switch (native_os) {
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
// On macOS, we can use F.GETPATH fcntl command to query the OS for
// the path to the file descriptor.
@memset(out_buffer, 0);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.fcntl(fd, posix.F.GETPATH, out_buffer))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => return error.FileNotFound,
.NOSPC => return error.NameTooLong,
.NOENT => return error.FileNotFound,
// TODO man pages for fcntl on macOS don't really tell you what
// errno values to expect when command is F.GETPATH...
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
return std.mem.indexOfScalar(u8, &out_buffer, 0) orelse out_buffer.len;
},
.linux, .serenity, .illumos => {
var procfs_buf: ["/proc/self/path/-2147483648\x00".len]u8 = undefined;
const template = if (native_os == .illumos) "/proc/self/path/{d}" else "/proc/self/fd/{d}";
const proc_path = std.fmt.bufPrintSentinel(&procfs_buf, template, .{fd}, 0) catch unreachable;
try current_thread.beginSyscall();
while (true) {
const rc = posix.system.readlink(proc_path, out_buffer.ptr, out_buffer.len);
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
const len: usize = @bitCast(rc);
return len;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.FAULT => |err| return errnoBug(err),
.INVAL => return error.NotLink,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.ILSEQ => |err| return errnoBug(err),
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
},
.freebsd => {
var kfile: std.c.kinfo_file = undefined;
kfile.structsize = std.c.KINFO_FILE_SIZE;
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(std.c.fcntl(fd, std.c.F.KINFO, @intFromPtr(&kfile)))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => return error.FileNotFound,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
const len = std.mem.indexOfScalar(u8, &kfile.path, 0) orelse kfile.path.len;
if (len == 0) return error.NameTooLong;
return len;
},
.netbsd, .dragonfly => {
@memset(out_buffer[0..max_path_bytes], 0);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(std.c.fcntl(fd, posix.F.GETPATH, out_buffer))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.BADF => return error.FileNotFound,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.RANGE => return error.NameTooLong,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
return std.mem.indexOfScalar(u8, &out_buffer, 0) orelse out_buffer.len;
},
else => @compileError("unsupported OS"),
}
comptime unreachable;
}
const dirDeleteFile = switch (native_os) {
.windows => dirDeleteFileWindows,
.wasi => dirDeleteFileWasi,
else => dirDeleteFilePosix,
};
fn dirDeleteFileWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteFileError!void {
return dirDeleteWindows(userdata, dir, sub_path, false);
}
fn dirDeleteFileWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteFileError!void {
if (builtin.link_libc) return dirDeleteFilePosix(userdata, dir, sub_path);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
const res = std.os.wasi.path_unlink_file(dir.handle, sub_path.ptr, sub_path.len);
switch (res) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.FAULT => |err| return errnoBug(err),
.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,
.NOTEMPTY => return error.DirNotEmpty,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
.INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirDeleteFilePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteFileError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.unlinkat(dir.handle, sub_path_posix, 0))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
// Some systems return permission errors when trying to delete a
// directory, so we need to handle that case specifically and
// translate the error.
.PERM => switch (native_os) {
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .dragonfly, .openbsd, .illumos => {
// Don't follow symlinks to match unlinkat (which acts on symlinks rather than follows them).
var st = std.mem.zeroes(posix.Stat);
while (true) {
try current_thread.checkCancel();
switch (posix.errno(fstatat_sym(dir.handle, sub_path_posix, &st, posix.AT.SYMLINK_NOFOLLOW))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => continue,
.CANCELED => return current_thread.endSyscallCanceled(),
else => {
current_thread.endSyscall();
return error.PermissionDenied;
},
}
}
const is_dir = st.mode & posix.S.IFMT == posix.S.IFDIR;
if (is_dir)
return error.IsDir
else
return error.PermissionDenied;
},
else => {
current_thread.endSyscall();
return error.PermissionDenied;
},
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.BUSY => return error.FileBusy,
.FAULT => |err| return errnoBug(err),
.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,
.EXIST => |err| return errnoBug(err),
.NOTEMPTY => |err| return errnoBug(err), // Not passing AT.REMOVEDIR
.ILSEQ => return error.BadPathName,
.INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirDeleteDir = switch (native_os) {
.windows => dirDeleteDirWindows,
.wasi => dirDeleteDirWasi,
else => dirDeleteDirPosix,
};
fn dirDeleteDirWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteDirError!void {
return dirDeleteWindows(userdata, dir, sub_path, true);
}
fn dirDeleteWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, remove_dir: bool) Io.Dir.DeleteFileError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const w = windows;
try current_thread.checkCancel();
const sub_path_w = try w.sliceToPrefixedFileW(dir.handle, sub_path);
const path_len_bytes = @as(u16, @intCast(sub_path_w.len * 2));
var nt_name: w.UNICODE_STRING = .{
.Length = path_len_bytes,
.MaximumLength = path_len_bytes,
// The Windows API makes this mutable, but it will not mutate here.
.Buffer = @constCast(sub_path_w.ptr),
};
if (sub_path_w[0] == '.' and sub_path_w[1] == 0) {
// Windows does not recognize this, but it does work with empty string.
nt_name.Length = 0;
}
if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) {
// Can't remove the parent directory with an open handle.
return error.FileBusy;
}
const create_options_flags: w.ULONG = if (remove_dir)
w.FILE_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT
else
w.FILE_NON_DIRECTORY_FILE | w.FILE_OPEN_REPARSE_POINT;
var attr: w.OBJECT_ATTRIBUTES = .{
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
.Attributes = w.OBJ_CASE_INSENSITIVE,
.ObjectName = &nt_name,
.SecurityDescriptor = null,
.SecurityQualityOfService = null,
};
var io_status_block: w.IO_STATUS_BLOCK = undefined;
var tmp_handle: w.HANDLE = undefined;
var rc = w.ntdll.NtCreateFile(
&tmp_handle,
w.SYNCHRONIZE | w.DELETE,
&attr,
&io_status_block,
null,
0,
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE,
w.FILE_OPEN,
create_options_flags,
null,
0,
);
switch (rc) {
.SUCCESS => {},
.OBJECT_NAME_INVALID => |err| return w.statusBug(err),
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
.BAD_NETWORK_PATH => return error.NetworkNotFound, // \\server was not found
.BAD_NETWORK_NAME => return error.NetworkNotFound, // \\server was found but \\server\share wasn't
.INVALID_PARAMETER => |err| return w.statusBug(err),
.FILE_IS_A_DIRECTORY => return error.IsDir,
.NOT_A_DIRECTORY => return error.NotDir,
.SHARING_VIOLATION => return error.FileBusy,
.ACCESS_DENIED => return error.AccessDenied,
.DELETE_PENDING => return,
else => return w.unexpectedStatus(rc),
}
defer w.CloseHandle(tmp_handle);
// FileDispositionInformationEx has varying levels of support:
// - FILE_DISPOSITION_INFORMATION_EX requires >= win10_rs1
// (INVALID_INFO_CLASS is returned if not supported)
// - Requires the NTFS filesystem
// (on filesystems like FAT32, INVALID_PARAMETER is returned)
// - FILE_DISPOSITION_POSIX_SEMANTICS requires >= win10_rs1
// - FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5
// (NOT_SUPPORTED is returned if a flag is unsupported)
//
// The strategy here is just to try using FileDispositionInformationEx and fall back to
// FileDispositionInformation if the return value lets us know that some aspect of it is not supported.
const need_fallback = need_fallback: {
try current_thread.checkCancel();
// Deletion with posix semantics if the filesystem supports it.
var info: w.FILE_DISPOSITION_INFORMATION_EX = .{
.Flags = w.FILE_DISPOSITION_DELETE |
w.FILE_DISPOSITION_POSIX_SEMANTICS |
w.FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE,
};
rc = w.ntdll.NtSetInformationFile(
tmp_handle,
&io_status_block,
&info,
@sizeOf(w.FILE_DISPOSITION_INFORMATION_EX),
.FileDispositionInformationEx,
);
switch (rc) {
.SUCCESS => return,
// The filesystem does not support FileDispositionInformationEx
.INVALID_PARAMETER,
// The operating system does not support FileDispositionInformationEx
.INVALID_INFO_CLASS,
// The operating system does not support one of the flags
.NOT_SUPPORTED,
=> break :need_fallback true,
// For all other statuses, fall down to the switch below to handle them.
else => break :need_fallback false,
}
};
if (need_fallback) {
try current_thread.checkCancel();
// Deletion with file pending semantics, which requires waiting or moving
// files to get them removed (from here).
var file_dispo: w.FILE_DISPOSITION_INFORMATION = .{
.DeleteFile = w.TRUE,
};
rc = w.ntdll.NtSetInformationFile(
tmp_handle,
&io_status_block,
&file_dispo,
@sizeOf(w.FILE_DISPOSITION_INFORMATION),
.FileDispositionInformation,
);
}
switch (rc) {
.SUCCESS => {},
.DIRECTORY_NOT_EMPTY => return error.DirNotEmpty,
.INVALID_PARAMETER => |err| return w.statusBug(err),
.CANNOT_DELETE => return error.AccessDenied,
.MEDIA_WRITE_PROTECTED => return error.AccessDenied,
.ACCESS_DENIED => return error.AccessDenied,
else => return w.unexpectedStatus(rc),
}
}
fn dirDeleteDirWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteDirError!void {
if (builtin.link_libc) return dirDeleteDirPosix(userdata, dir, sub_path);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
const res = std.os.wasi.path_remove_directory(dir.handle, sub_path.ptr, sub_path.len);
switch (res) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.FAULT => |err| return errnoBug(err),
.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,
.NOTEMPTY => return error.DirNotEmpty,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
.INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirDeleteDirPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.DeleteDirError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.unlinkat(dir.handle, sub_path_posix, posix.AT.REMOVEDIR))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.FAULT => |err| return errnoBug(err),
.IO => return error.FileSystem,
.ISDIR => |err| return errnoBug(err),
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.ROFS => return error.ReadOnlyFileSystem,
.EXIST => |err| return errnoBug(err),
.NOTEMPTY => |err| return errnoBug(err), // Not passing AT.REMOVEDIR
.ILSEQ => return error.BadPathName,
.INVAL => |err| return errnoBug(err), // invalid flags, or pathname has . as last component
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirRename = switch (native_os) {
.windows => dirRenameWindows,
.wasi => dirRenameWasi,
else => dirRenamePosix,
};
fn dirRenameWindows(
userdata: ?*anyopaque,
old_dir: Io.Dir,
old_sub_path: []const u8,
new_dir: Io.Dir,
new_sub_path: []const u8,
) Io.Dir.RenameError!void {
const w = windows;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const old_path_w = try windows.sliceToPrefixedFileW(old_dir.handle, old_sub_path);
const new_path_w = try windows.sliceToPrefixedFileW(new_dir.handle, new_sub_path);
const replace_if_exists = true;
try current_thread.checkCancel();
const src_fd = w.OpenFile(old_path_w, .{
.dir = old_dir.handle,
.access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | w.DELETE,
.creation = w.FILE_OPEN,
.filter = .any, // This function is supposed to rename both files and directories.
.follow_symlinks = false,
}) catch |err| switch (err) {
error.WouldBlock => unreachable, // Not possible without `.share_access_nonblocking = true`.
else => |e| return e,
};
defer w.CloseHandle(src_fd);
var rc: w.NTSTATUS = undefined;
// FileRenameInformationEx has varying levels of support:
// - FILE_RENAME_INFORMATION_EX requires >= win10_rs1
// (INVALID_INFO_CLASS is returned if not supported)
// - Requires the NTFS filesystem
// (on filesystems like FAT32, INVALID_PARAMETER is returned)
// - FILE_RENAME_POSIX_SEMANTICS requires >= win10_rs1
// - FILE_RENAME_IGNORE_READONLY_ATTRIBUTE requires >= win10_rs5
// (NOT_SUPPORTED is returned if a flag is unsupported)
//
// The strategy here is just to try using FileRenameInformationEx and fall back to
// FileRenameInformation if the return value lets us know that some aspect of it is not supported.
const need_fallback = need_fallback: {
const struct_buf_len = @sizeOf(w.FILE_RENAME_INFORMATION_EX) + (w.PATH_MAX_WIDE * 2);
var rename_info_buf: [struct_buf_len]u8 align(@alignOf(w.FILE_RENAME_INFORMATION_EX)) = undefined;
const struct_len = @sizeOf(w.FILE_RENAME_INFORMATION_EX) + new_path_w.len * 2;
if (struct_len > struct_buf_len) return error.NameTooLong;
const rename_info: *w.FILE_RENAME_INFORMATION_EX = @ptrCast(&rename_info_buf);
var io_status_block: w.IO_STATUS_BLOCK = undefined;
var flags: w.ULONG = w.FILE_RENAME_POSIX_SEMANTICS | w.FILE_RENAME_IGNORE_READONLY_ATTRIBUTE;
if (replace_if_exists) flags |= w.FILE_RENAME_REPLACE_IF_EXISTS;
rename_info.* = .{
.Flags = flags,
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle,
.FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong
.FileName = undefined,
};
@memcpy((&rename_info.FileName).ptr, new_path_w);
rc = w.ntdll.NtSetInformationFile(
src_fd,
&io_status_block,
rename_info,
@intCast(struct_len), // already checked for error.NameTooLong
.FileRenameInformationEx,
);
switch (rc) {
.SUCCESS => return,
// The filesystem does not support FileDispositionInformationEx
.INVALID_PARAMETER,
// The operating system does not support FileDispositionInformationEx
.INVALID_INFO_CLASS,
// The operating system does not support one of the flags
.NOT_SUPPORTED,
=> break :need_fallback true,
// For all other statuses, fall down to the switch below to handle them.
else => break :need_fallback false,
}
};
if (need_fallback) {
const struct_buf_len = @sizeOf(w.FILE_RENAME_INFORMATION) + (w.PATH_MAX_WIDE * 2);
var rename_info_buf: [struct_buf_len]u8 align(@alignOf(w.FILE_RENAME_INFORMATION)) = undefined;
const struct_len = @sizeOf(w.FILE_RENAME_INFORMATION) + new_path_w.len * 2;
if (struct_len > struct_buf_len) return error.NameTooLong;
const rename_info: *w.FILE_RENAME_INFORMATION = @ptrCast(&rename_info_buf);
var io_status_block: w.IO_STATUS_BLOCK = undefined;
rename_info.* = .{
.Flags = @intFromBool(replace_if_exists),
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir.handle,
.FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong
.FileName = undefined,
};
@memcpy((&rename_info.FileName).ptr, new_path_w);
rc = w.ntdll.NtSetInformationFile(
src_fd,
&io_status_block,
rename_info,
@intCast(struct_len), // already checked for error.NameTooLong
.FileRenameInformation,
);
}
switch (rc) {
.SUCCESS => {},
.INVALID_HANDLE => |err| return w.statusBug(err),
.INVALID_PARAMETER => |err| return w.statusBug(err),
.OBJECT_PATH_SYNTAX_BAD => |err| return w.statusBug(err),
.ACCESS_DENIED => return error.AccessDenied,
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
.NOT_SAME_DEVICE => return error.RenameAcrossMountPoints,
.OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
.DIRECTORY_NOT_EMPTY => return error.PathAlreadyExists,
.FILE_IS_A_DIRECTORY => return error.IsDir,
.NOT_A_DIRECTORY => return error.NotDir,
else => return w.unexpectedStatus(rc),
}
}
fn dirRenameWasi(
userdata: ?*anyopaque,
old_dir: Io.Dir,
old_sub_path: []const u8,
new_dir: Io.Dir,
new_sub_path: []const u8,
) Io.Dir.RenameError!void {
if (builtin.link_libc) return dirRenamePosix(userdata, old_dir, old_sub_path, new_dir, new_sub_path);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.path_rename(old_dir.handle, old_sub_path.ptr, old_sub_path.len, new_dir.handle, new_sub_path.ptr, new_sub_path.len)) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.DQUOT => return error.DiskQuota,
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MLINK => return error.LinkQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.EXIST => return error.PathAlreadyExists,
.NOTEMPTY => return error.PathAlreadyExists,
.ROFS => return error.ReadOnlyFileSystem,
.XDEV => return error.RenameAcrossMountPoints,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirRenamePosix(
userdata: ?*anyopaque,
old_dir: Io.Dir,
old_sub_path: []const u8,
new_dir: Io.Dir,
new_sub_path: []const u8,
) Io.Dir.RenameError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var old_path_buffer: [posix.PATH_MAX]u8 = undefined;
var new_path_buffer: [posix.PATH_MAX]u8 = undefined;
const old_sub_path_posix = try pathToPosix(old_sub_path, &old_path_buffer);
const new_sub_path_posix = try pathToPosix(new_sub_path, &new_path_buffer);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.renameat(old_dir.handle, old_sub_path_posix, new_dir.handle, new_sub_path_posix))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BUSY => return error.FileBusy,
.DQUOT => return error.DiskQuota,
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MLINK => return error.LinkQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.EXIST => return error.PathAlreadyExists,
.NOTEMPTY => return error.PathAlreadyExists,
.ROFS => return error.ReadOnlyFileSystem,
.XDEV => return error.RenameAcrossMountPoints,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirSymLink = switch (native_os) {
.windows => dirSymLinkWindows,
.wasi => dirSymLinkWasi,
else => dirSymLinkPosix,
};
fn dirSymLinkWindows(
userdata: ?*anyopaque,
dir: Io.Dir,
target_path: []const u8,
sym_link_path: []const u8,
flags: Io.Dir.SymLinkFlags,
) Io.Dir.SymLinkError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const w = windows;
try current_thread.checkCancel();
// Target path does not use sliceToPrefixedFileW because certain paths
// are handled differently when creating a symlink than they would be
// when converting to an NT namespaced path. CreateSymbolicLink in
// symLinkW will handle the necessary conversion.
var target_path_w: w.PathSpace = undefined;
target_path_w.len = try w.wtf8ToWtf16Le(&target_path_w.data, target_path);
target_path_w.data[target_path_w.len] = 0;
// However, we need to canonicalize any path separators to `\`, since if
// the target path is relative, then it must use `\` as the path separator.
std.mem.replaceScalar(
u16,
target_path_w.data[0..target_path_w.len],
std.mem.nativeToLittle(u16, '/'),
std.mem.nativeToLittle(u16, '\\'),
);
const sym_link_path_w = try w.sliceToPrefixedFileW(dir.handle, sym_link_path);
const SYMLINK_DATA = extern struct {
ReparseTag: w.ULONG,
ReparseDataLength: w.USHORT,
Reserved: w.USHORT,
SubstituteNameOffset: w.USHORT,
SubstituteNameLength: w.USHORT,
PrintNameOffset: w.USHORT,
PrintNameLength: w.USHORT,
Flags: w.ULONG,
};
const symlink_handle = w.OpenFile(sym_link_path_w.span(), .{
.access_mask = w.SYNCHRONIZE | w.GENERIC_READ | w.GENERIC_WRITE,
.dir = dir,
.creation = w.FILE_CREATE,
.filter = if (flags.is_directory) .dir_only else .file_only,
}) catch |err| switch (err) {
error.IsDir => return error.PathAlreadyExists,
error.NotDir => return error.Unexpected,
error.WouldBlock => return error.Unexpected,
error.PipeBusy => return error.Unexpected,
error.NoDevice => return error.Unexpected,
error.AntivirusInterference => return error.Unexpected,
else => |e| return e,
};
defer w.CloseHandle(symlink_handle);
// Relevant portions of the documentation:
// > Relative links are specified using the following conventions:
// > - Root relative—for example, "\Windows\System32" resolves to "current drive:\Windows\System32".
// > - Current working directoryrelative—for example, if the current working directory is
// > C:\Windows\System32, "C:File.txt" resolves to "C:\Windows\System32\File.txt".
// > Note: If you specify a current working directoryrelative link, it is created as an absolute
// > link, due to the way the current working directory is processed based on the user and the thread.
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
var is_target_absolute = false;
const final_target_path = target_path: {
if (w.hasCommonNtPrefix(u16, target_path)) {
// Already an NT path, no need to do anything to it
break :target_path target_path;
} else {
switch (w.getWin32PathType(u16, target_path)) {
// Rooted paths need to avoid getting put through wToPrefixedFileW
// (and they are treated as relative in this context)
// Note: It seems that rooted paths in symbolic links are relative to
// the drive that the symbolic exists on, not to the CWD's drive.
// So, if the symlink is on C:\ and the CWD is on D:\,
// it will still resolve the path relative to the root of
// the C:\ drive.
.rooted => break :target_path target_path,
// Keep relative paths relative, but anything else needs to get NT-prefixed.
else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path))
break :target_path target_path,
}
}
var prefixed_target_path = try w.wToPrefixedFileW(dir, target_path);
// We do this after prefixing to ensure that drive-relative paths are treated as absolute
is_target_absolute = std.fs.path.isAbsoluteWindowsWtf16(prefixed_target_path.span());
break :target_path prefixed_target_path.span();
};
// prepare reparse data buffer
var buffer: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4;
const header_len = @sizeOf(w.ULONG) + @sizeOf(w.USHORT) * 2;
const target_is_absolute = std.fs.path.isAbsoluteWindowsWtf16(final_target_path);
const symlink_data = SYMLINK_DATA{
.ReparseTag = w.IO_REPARSE_TAG_SYMLINK,
.ReparseDataLength = @intCast(buf_len - header_len),
.Reserved = 0,
.SubstituteNameOffset = @intCast(final_target_path.len * 2),
.SubstituteNameLength = @intCast(final_target_path.len * 2),
.PrintNameOffset = 0,
.PrintNameLength = @intCast(final_target_path.len * 2),
.Flags = if (!target_is_absolute) w.SYMLINK_FLAG_RELATIVE else 0,
};
@memcpy(buffer[0..@sizeOf(SYMLINK_DATA)], std.mem.asBytes(&symlink_data));
@memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
const paths_start = @sizeOf(SYMLINK_DATA) + final_target_path.len * 2;
@memcpy(buffer[paths_start..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
_ = try w.DeviceIoControl(symlink_handle, w.FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null);
}
fn dirSymLinkWasi(
userdata: ?*anyopaque,
dir: Io.Dir,
target_path: []const u8,
sym_link_path: []const u8,
flags: Io.Dir.SymLinkFlags,
) Io.Dir.SymLinkError!void {
if (builtin.link_libc) return dirSymLinkPosix(dir, target_path, sym_link_path, flags);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.path_symlink(target_path.ptr, target_path.len, dir.handle, sym_link_path.ptr, sym_link_path.len)) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err),
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.DQUOT => return error.DiskQuota,
.EXIST => return error.PathAlreadyExists,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.ROFS => return error.ReadOnlyFileSystem,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirSymLinkPosix(
userdata: ?*anyopaque,
dir: Io.Dir,
target_path: []const u8,
sym_link_path: []const u8,
flags: Io.Dir.SymLinkFlags,
) Io.Dir.SymLinkError!void {
_ = flags;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var target_path_buffer: [posix.PATH_MAX]u8 = undefined;
var sym_link_path_buffer: [posix.PATH_MAX]u8 = undefined;
const target_path_posix = try pathToPosix(target_path, &target_path_buffer);
const sym_link_path_posix = try pathToPosix(sym_link_path, &sym_link_path_buffer);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.symlinkat(target_path_posix, dir.handle, sym_link_path_posix))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.DQUOT => return error.DiskQuota,
.EXIST => return error.PathAlreadyExists,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.ROFS => return error.ReadOnlyFileSystem,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const dirReadLink = switch (native_os) {
.windows => dirReadLinkWindows,
.wasi => dirReadLinkWasi,
else => dirReadLinkPosix,
};
fn dirReadLinkWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, buffer: []u8) Io.Dir.ReadLinkError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const w = windows;
try current_thread.checkCancel();
var sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
const result_handle = w.OpenFile(sub_path_w.span(), .{
.access_mask = w.FILE_READ_ATTRIBUTES | w.SYNCHRONIZE,
.dir = dir,
.creation = w.FILE_OPEN,
.follow_symlinks = false,
.filter = .any,
}) catch |err| switch (err) {
error.IsDir, error.NotDir => return error.Unexpected, // filter = .any
error.PathAlreadyExists => return error.Unexpected, // FILE_OPEN
error.WouldBlock => return error.Unexpected,
error.NoDevice => return error.FileNotFound,
error.PipeBusy => return error.AccessDenied,
else => |e| return e,
};
defer w.CloseHandle(result_handle);
var reparse_buf: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 align(@alignOf(w.REPARSE_DATA_BUFFER)) = undefined;
_ = w.DeviceIoControl(result_handle, w.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]) catch |err| switch (err) {
error.AccessDenied => return error.Unexpected,
error.UnrecognizedVolume => return error.Unexpected,
else => |e| return e,
};
const reparse_struct: *const w.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0]));
const wide_result = switch (reparse_struct.ReparseTag) {
w.IO_REPARSE_TAG_SYMLINK => r: {
const buf: *const w.SYMBOLIC_LINK_REPARSE_BUFFER = @ptrCast(@alignCast(&reparse_struct.DataBuffer[0]));
const offset = buf.SubstituteNameOffset >> 1;
const len = buf.SubstituteNameLength >> 1;
const path_buf: [*]const u16 = &buf.PathBuffer;
const is_relative = buf.Flags & w.SYMLINK_FLAG_RELATIVE != 0;
break :r try w.parseReadLinkPath(path_buf[offset..][0..len], is_relative, buffer);
},
w.IO_REPARSE_TAG_MOUNT_POINT => r: {
const buf: *const w.MOUNT_POINT_REPARSE_BUFFER = @ptrCast(@alignCast(&reparse_struct.DataBuffer[0]));
const offset = buf.SubstituteNameOffset >> 1;
const len = buf.SubstituteNameLength >> 1;
const path_buf: [*]const u16 = &buf.PathBuffer;
break :r try w.parseReadLinkPath(path_buf[offset..][0..len], false, buffer);
},
else => return error.UnsupportedReparsePointType,
};
const len = std.unicode.calcWtf8Len(wide_result);
if (len > buffer.len) return error.NameTooLong;
return std.unicode.wtf16LeToWtf8(buffer, wide_result);
}
fn dirReadLinkWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, buffer: []u8) Io.Dir.ReadLinkError!usize {
if (builtin.link_libc) return dirReadLinkPosix(userdata, dir, sub_path, buffer);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var n: usize = undefined;
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.path_readlink(dir.handle, sub_path.ptr, sub_path.len, buffer.ptr, buffer.len, &n)) {
.SUCCESS => {
current_thread.endSyscall();
return buffer[0..n];
},
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.FAULT => |err| return errnoBug(err),
.INVAL => return error.NotLink,
.IO => return error.FileSystem,
.LOOP => return error.SymLinkLoop,
.NAMETOOLONG => return error.NameTooLong,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirReadLinkPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, buffer: []u8) Io.Dir.ReadLinkError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var sub_path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &sub_path_buffer);
try current_thread.beginSyscall();
while (true) {
const rc = posix.system.readlinkat(dir.handle, sub_path_posix, buffer.ptr, buffer.len);
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
const len: usize = @bitCast(rc);
return len;
},
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.FAULT => |err| return errnoBug(err),
.INVAL => return error.NotLink,
.IO => return error.FileSystem,
.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),
}
},
}
}
}
const dirSetPermissions = switch (native_os) {
.windows => dirSetPermissionsWindows,
else => dirSetPermissionsPosix,
};
fn dirSetPermissionsWindows(userdata: ?*anyopaque, dir: Io.Dir, permissions: Io.Dir.Permissions) Io.Dir.SetPermissionsError!void {
// TODO I think we can actually set permissions on a dir on windows?
_ = userdata;
_ = dir;
_ = permissions;
return error.Unexpected;
}
fn dirSetPermissionsPosix(userdata: ?*anyopaque, dir: Io.Dir, permissions: Io.Dir.Permissions) Io.Dir.SetPermissionsError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
return setPermissionsPosix(current_thread, dir.handle, permissions.toMode());
}
const dirSetOwner = switch (native_os) {
.windows => dirSetOwnerUnsupported,
else => dirSetOwnerPosix,
};
fn dirSetOwnerUnsupported(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.Dir.SetOwnerError!void {
_ = userdata;
_ = dir;
_ = owner;
_ = group;
return error.Unexpected;
}
fn dirSetOwnerPosix(userdata: ?*anyopaque, dir: Io.Dir, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.Dir.SetOwnerError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const uid = owner orelse ~@as(posix.uid_t, 0);
const gid = group orelse ~@as(posix.gid_t, 0);
return setOwnerPosix(current_thread, dir.handle, uid, gid);
}
fn setOwnerPosix(current_thread: *Thread, fd: posix.fd_t, uid: posix.uid_t, gid: posix.gid_t) Io.File.SetOwnerError!void {
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.fchown(fd, uid, gid))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // likely fd refers to directory opened without `Io.Dir.OpenOptions.iterate`
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ACCES => return error.AccessDenied,
.IO => return error.InputOutput,
.LOOP => return error.SymLinkLoop,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.FileNotFound,
.PERM => return error.PermissionDenied,
.ROFS => return error.ReadOnlyFileSystem,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const fileSync = switch (native_os) {
.windows => fileSyncWindows,
else => fileSyncPosix,
};
fn fileSyncWindows(userdata: ?*anyopaque, file: Io.File) Io.File.SyncError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.checkCancel();
if (windows.kernel32.FlushFileBuffers(file.handle) != 0)
return;
switch (windows.GetLastError()) {
.SUCCESS => return,
.INVALID_HANDLE => unreachable,
.ACCESS_DENIED => return error.AccessDenied, // a sync was performed but the system couldn't update the access time
.UNEXP_NET_ERR => return error.InputOutput,
else => |err| return windows.unexpectedError(err),
}
}
fn fileSyncPosix(userdata: ?*anyopaque, file: Io.File) Io.File.SyncError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
switch (posix.system.fsync(file.handle)) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ROFS => |err| return errnoBug(err),
.IO => return error.InputOutput,
.NOSPC => return error.NoSpaceLeft,
.DQUOT => return error.DiskQuota,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileIsTty(userdata: ?*anyopaque, file: Io.File) Io.Cancelable!bool {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
return isTty(current_thread, file);
}
fn isTty(current_thread: *Thread, file: Io.File) Io.Cancelable!bool {
if (is_windows) {
if (try isCygwinPty(current_thread, file)) return true;
try current_thread.checkCancel();
var out: windows.DWORD = undefined;
return windows.kernel32.GetConsoleMode(file.handle, &out) != 0;
}
if (builtin.link_libc) {
try current_thread.beginSyscall();
while (true) {
const rc = posix.system.isatty(file.handle);
switch (posix.errno(rc - 1)) {
.SUCCESS => {
current_thread.endSyscall();
return true;
},
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => {
current_thread.endSyscall();
return false;
},
}
}
}
if (native_os == .wasi) {
var statbuf: std.os.wasi.fdstat_t = undefined;
const err = std.os.wasi.fd_fdstat_get(file.handle, &statbuf);
if (err != .SUCCESS)
return false;
// A tty is a character device that we can't seek or tell on.
if (statbuf.fs_filetype != .CHARACTER_DEVICE)
return false;
if (statbuf.fs_rights_base.FD_SEEK or statbuf.fs_rights_base.FD_TELL)
return false;
return true;
}
if (native_os == .linux) {
const linux = std.os.linux;
try current_thread.beginSyscall();
while (true) {
var wsz: linux.winsize = undefined;
const fd: usize = @bitCast(@as(isize, file.handle));
const rc = linux.syscall3(.ioctl, fd, linux.T.IOCGWINSZ, @intFromPtr(&wsz));
switch (linux.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
return true;
},
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => {
current_thread.endSyscall();
return false;
},
}
}
}
@compileError("unimplemented");
}
fn fileEnableAnsiEscapeCodes(userdata: ?*anyopaque, file: Io.File) Io.File.EnableAnsiEscapeCodesError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (is_windows) {
try current_thread.checkCancel();
// For Windows Terminal, VT Sequences processing is enabled by default.
var original_console_mode: windows.DWORD = 0;
if (windows.kernel32.GetConsoleMode(file.handle, &original_console_mode) != 0) {
if (original_console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return;
// For Windows Console, VT Sequences processing support was added in Windows 10 build 14361, but disabled by default.
// https://devblogs.microsoft.com/commandline/tmux-support-arrives-for-bash-on-ubuntu-on-windows/
//
// Note: In Microsoft's example for enabling virtual terminal processing, it
// shows attempting to enable `DISABLE_NEWLINE_AUTO_RETURN` as well:
// https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#example-of-enabling-virtual-terminal-processing
// This is avoided because in the old Windows Console, that flag causes \n (as opposed to \r\n)
// to behave unexpectedly (the cursor moves down 1 row but remains on the same column).
// Additionally, the default console mode in Windows Terminal does not have
// `DISABLE_NEWLINE_AUTO_RETURN` set, so by only enabling `ENABLE_VIRTUAL_TERMINAL_PROCESSING`
// we end up matching the mode of Windows Terminal.
const requested_console_modes = windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
const console_mode = original_console_mode | requested_console_modes;
try current_thread.checkCancel();
if (windows.kernel32.SetConsoleMode(file.handle, console_mode) != 0) return;
}
if (try isCygwinPty(current_thread, file)) return;
} else {
if (try supportsAnsiEscapeCodes(current_thread, file)) return;
}
return error.NotTerminalDevice;
}
fn fileSupportsAnsiEscapeCodes(userdata: ?*anyopaque, file: Io.File) Io.Cancelable!bool {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
return supportsAnsiEscapeCodes(current_thread, file);
}
fn supportsAnsiEscapeCodes(current_thread: *Thread, file: Io.File) Io.Cancelable!bool {
if (is_windows) {
try current_thread.checkCancel();
var console_mode: windows.DWORD = 0;
if (windows.kernel32.GetConsoleMode(file.handle, &console_mode) != 0) {
if (console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true;
}
return isCygwinPty(current_thread, file);
}
if (native_os == .wasi) {
// WASI sanitizes stdout when fd is a tty so ANSI escape codes will not
// be interpreted as actual cursor commands, and stderr is always
// sanitized.
return false;
}
if (try isTty(current_thread, file)) return true;
return false;
}
fn isCygwinPty(current_thread: *Thread, file: Io.File) Io.Cancelable!bool {
if (!is_windows) return false;
const handle = file.handle;
// If this is a MSYS2/cygwin pty, then it will be a named pipe with a name in one of these formats:
// msys-[...]-ptyN-[...]
// cygwin-[...]-ptyN-[...]
//
// Example: msys-1888ae32e00d56aa-pty0-to-master
// First, just check that the handle is a named pipe.
// This allows us to avoid the more costly NtQueryInformationFile call
// for handles that aren't named pipes.
{
try current_thread.checkCancel();
var io_status: windows.IO_STATUS_BLOCK = undefined;
var device_info: windows.FILE_FS_DEVICE_INFORMATION = undefined;
const rc = windows.ntdll.NtQueryVolumeInformationFile(handle, &io_status, &device_info, @sizeOf(windows.FILE_FS_DEVICE_INFORMATION), .FileFsDeviceInformation);
switch (rc) {
.SUCCESS => {},
else => return false,
}
if (device_info.DeviceType != windows.FILE_DEVICE_NAMED_PIPE) return false;
}
const name_bytes_offset = @offsetOf(windows.FILE_NAME_INFO, "FileName");
// `NAME_MAX` UTF-16 code units (2 bytes each)
// This buffer may not be long enough to handle *all* possible paths
// (PATH_MAX_WIDE would be necessary for that), but because we only care
// about certain paths and we know they must be within a reasonable length,
// we can use this smaller buffer and just return false on any error from
// NtQueryInformationFile.
const num_name_bytes = windows.MAX_PATH * 2;
var name_info_bytes align(@alignOf(windows.FILE_NAME_INFO)) = [_]u8{0} ** (name_bytes_offset + num_name_bytes);
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
try current_thread.checkCancel();
const rc = windows.ntdll.NtQueryInformationFile(handle, &io_status_block, &name_info_bytes, @intCast(name_info_bytes.len), .FileNameInformation);
switch (rc) {
.SUCCESS => {},
.INVALID_PARAMETER => unreachable,
else => return false,
}
const name_info: *const windows.FILE_NAME_INFO = @ptrCast(&name_info_bytes);
const name_bytes = name_info_bytes[name_bytes_offset .. name_bytes_offset + name_info.FileNameLength];
const name_wide = std.mem.bytesAsSlice(u16, name_bytes);
// The name we get from NtQueryInformationFile will be prefixed with a '\', e.g. \msys-1888ae32e00d56aa-pty0-to-master
return (std.mem.startsWith(u16, name_wide, &[_]u16{ '\\', 'm', 's', 'y', 's', '-' }) or
std.mem.startsWith(u16, name_wide, &[_]u16{ '\\', 'c', 'y', 'g', 'w', 'i', 'n', '-' })) and
std.mem.indexOf(u16, name_wide, &[_]u16{ '-', 'p', 't', 'y' }) != null;
}
fn fileSetLength(userdata: ?*anyopaque, file: Io.File, length: u64) Io.File.SetLengthError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const signed_len: i64 = @bitCast(length);
if (signed_len < 0) return error.FileTooBig; // Avoid ambiguous EINVAL errors.
if (is_windows) {
try current_thread.checkCancel();
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
var eof_info: windows.FILE_END_OF_FILE_INFORMATION = .{
.EndOfFile = signed_len,
};
const status = windows.ntdll.NtSetInformationFile(
file.handle,
&io_status_block,
&eof_info,
@sizeOf(windows.FILE_END_OF_FILE_INFORMATION),
.FileEndOfFileInformation,
);
switch (status) {
.SUCCESS => return,
.INVALID_HANDLE => |err| return windows.statusBug(err), // Handle not open for writing.
.ACCESS_DENIED => return error.AccessDenied,
.USER_MAPPED_FILE => return error.AccessDenied,
.INVALID_PARAMETER => return error.FileTooBig,
else => return windows.unexpectedStatus(status),
}
}
if (native_os == .wasi and !builtin.link_libc) {
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.fd_filestat_set_size(file.handle, length)) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.PERM => return error.PermissionDenied,
.TXTBSY => return error.FileBusy,
.BADF => |err| return errnoBug(err), // Handle not open for writing
.INVAL => return error.NonResizable,
.NOTCAPABLE => return error.AccessDenied,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(ftruncate_sym(file.handle, signed_len))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.PERM => return error.PermissionDenied,
.TXTBSY => return error.FileBusy,
.BADF => |err| return errnoBug(err), // Handle not open for writing.
.INVAL => return error.NonResizable, // This is returned for /dev/null for example.
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileSetOwner(userdata: ?*anyopaque, file: Io.File, owner: ?Io.File.Uid, group: ?Io.File.Gid) Io.File.SetOwnerError!void {
switch (native_os) {
.windows, .wasi => return error.Unexpected,
else => {},
}
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const uid = owner orelse ~@as(posix.uid_t, 0);
const gid = group orelse ~@as(posix.gid_t, 0);
return setOwnerPosix(current_thread, file.handle, uid, gid);
}
fn fileSetPermissions(userdata: ?*anyopaque, file: Io.File, permissions: Io.File.Permissions) Io.File.SetPermissionsError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
switch (native_os) {
.windows => {
try current_thread.checkCancel();
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
var info: windows.FILE_BASIC_INFORMATION = .{
.CreationTime = 0,
.LastAccessTime = 0,
.LastWriteTime = 0,
.ChangeTime = 0,
.FileAttributes = permissions.inner.attributes,
};
const status = windows.ntdll.NtSetInformationFile(
file.handle,
&io_status_block,
&info,
@sizeOf(windows.FILE_BASIC_INFORMATION),
.FileBasicInformation,
);
switch (status) {
.SUCCESS => return,
.INVALID_HANDLE => |err| return windows.statusBug(err),
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(status),
}
},
.wasi => return error.Unexpected, // Unsupported OS.
else => return setPermissionsPosix(current_thread, file.handle, permissions.toMode()),
}
}
fn setPermissionsPosix(current_thread: *Thread, fd: posix.fd_t, mode: posix.mode_t) Io.File.SetPermissionsError!void {
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.fchmod(fd, mode))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ACCES => return error.AccessDenied,
.IO => return error.InputOutput,
.LOOP => return error.SymLinkLoop,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.FileNotFound,
.PERM => return error.PermissionDenied,
.ROFS => return error.ReadOnlyFileSystem,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirSetTimestamps(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
last_accessed: Io.Timestamp,
last_modified: Io.Timestamp,
options: Io.File.SetTimestampsOptions,
) Io.File.SetTimestampsError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (is_windows) {
@panic("TODO");
}
if (native_os == .wasi and !builtin.link_libc) {
@panic("TODO");
}
const times: [2]posix.timespec = .{
timestampToPosix(last_accessed),
timestampToPosix(last_modified),
};
const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0;
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.utimensat(dir.handle, sub_path_posix, &times, flags))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BADF => |err| return errnoBug(err), // always a race condition
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ROFS => return error.ReadOnlyFileSystem,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirSetTimestampsNow(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.File.SetTimestampsOptions,
) Io.File.SetTimestampsError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (is_windows) {
@panic("TODO");
}
if (native_os == .wasi and !builtin.link_libc) {
@panic("TODO");
}
const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0;
var path_buffer: [posix.PATH_MAX]u8 = undefined;
const sub_path_posix = try pathToPosix(sub_path, &path_buffer);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.utimensat(dir.handle, sub_path_posix, null, flags))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BADF => |err| return errnoBug(err), // always a race condition
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ROFS => return error.ReadOnlyFileSystem,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileSetTimestamps(
userdata: ?*anyopaque,
file: Io.File,
last_accessed: Io.Timestamp,
last_modified: Io.Timestamp,
) Io.File.SetTimestampsError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (is_windows) {
try current_thread.checkCancel();
const atime_ft = windows.nanoSecondsToFileTime(last_accessed.toNanoseconds());
const mtime_ft = windows.nanoSecondsToFileTime(last_modified.toNanoseconds());
// https://github.com/ziglang/zig/issues/1840
const rc = windows.kernel32.SetFileTime(file.handle, null, &atime_ft, &mtime_ft);
if (rc == 0) {
switch (windows.GetLastError()) {
else => |err| return windows.unexpectedError(err),
}
}
return;
}
const times: [2]posix.timespec = .{
timestampToPosix(last_accessed),
timestampToPosix(last_modified),
};
if (native_os == .wasi and !builtin.link_libc) {
const atim = times[0].toTimestamp();
const mtim = times[1].toTimestamp();
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.fd_filestat_set_times(file.handle, atim, mtim, .{
.ATIM = true,
.MTIM = true,
})) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BADF => |err| return errnoBug(err), // File descriptor use-after-free.
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ROFS => return error.ReadOnlyFileSystem,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.futimens(file.handle, &times))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BADF => |err| return errnoBug(err), // always a race condition
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ROFS => return error.ReadOnlyFileSystem,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileSetTimestampsNow(userdata: ?*anyopaque, file: Io.File) Io.File.SetTimestampsError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (is_windows) {
@panic("TODO");
}
if (native_os == .wasi and !builtin.link_libc) {
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.fd_filestat_set_times(file.handle, 0, 0, .{
.ATIM_NOW = true,
.MTIM_NOW = true,
})) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BADF => |err| return errnoBug(err), // always a race condition
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ROFS => return error.ReadOnlyFileSystem,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.futimens(file.handle, null))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.BADF => |err| return errnoBug(err), // always a race condition
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ROFS => return error.ReadOnlyFileSystem,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const windows_lock_range_off: windows.LARGE_INTEGER = 0;
const windows_lock_range_len: windows.LARGE_INTEGER = 1;
fn fileLock(userdata: ?*anyopaque, file: Io.File, lock: Io.File.Lock) Io.File.LockError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (is_windows) {
const exclusive = switch (lock) {
.none => return,
.shared => false,
.exclusive => true,
};
try current_thread.checkCancel();
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
const status = windows.ntdll.NtLockFile(
file.handle,
null,
null,
null,
&io_status_block,
&windows_lock_range_off,
&windows_lock_range_len,
null,
windows.FALSE,
@intFromBool(exclusive),
);
switch (status) {
.SUCCESS => return,
.INSUFFICIENT_RESOURCES => return error.SystemResources,
.LOCK_NOT_GRANTED => |err| return windows.statusBug(err), // passed FailImmediately=false
.ACCESS_VIOLATION => |err| return windows.statusBug(err), // bad io_status_block pointer
else => return windows.unexpectedStatus(status),
}
}
const operation = switch (lock) {
.none => posix.LOCK.UN,
.shared => posix.LOCK.SH,
.exclusive => posix.LOCK.EX,
};
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.flock(file.handle, operation))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err), // invalid parameters
.NOLCK => return error.SystemResources,
.AGAIN => |err| return errnoBug(err),
.OPNOTSUPP => return error.FileLocksUnsupported,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileTryLock(userdata: ?*anyopaque, file: Io.File, lock: Io.File.Lock) Io.File.LockError!bool {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (is_windows) {
const exclusive = switch (lock) {
.none => return,
.shared => false,
.exclusive => true,
};
try current_thread.checkCancel();
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
const status = windows.ntdll.NtLockFile(
file.handle,
null,
null,
null,
&io_status_block,
&windows_lock_range_off,
&windows_lock_range_len,
null,
windows.TRUE,
@intFromBool(exclusive),
);
switch (status) {
.SUCCESS => return true,
.INSUFFICIENT_RESOURCES => return error.SystemResources,
.LOCK_NOT_GRANTED => return false,
.ACCESS_VIOLATION => |err| return windows.statusBug(err), // bad io_status_block pointer
else => return windows.unexpectedStatus(status),
}
}
const operation = switch (lock) {
.none => posix.LOCK.UN,
.shared => posix.LOCK.SH | posix.LOCK.NB,
.exclusive => posix.LOCK.EX | posix.LOCK.NB,
};
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.flock(file.handle, operation))) {
.SUCCESS => {
current_thread.endSyscall();
return true;
},
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
.AGAIN => {
current_thread.endSyscall();
return false;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err), // invalid parameters
.NOLCK => return error.SystemResources,
.OPNOTSUPP => return error.FileLocksUnsupported,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileUnlock(userdata: ?*anyopaque, file: Io.File) void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (is_windows) {
try current_thread.checkCancel();
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
const status = windows.ntdll.NtUnlockFile(
file.handle,
&io_status_block,
&windows_lock_range_off,
&windows_lock_range_len,
null,
);
if (is_debug) switch (status) {
.SUCCESS => {},
.RANGE_NOT_LOCKED => unreachable, // Function asserts unlocked.
.ACCESS_VIOLATION => unreachable, // bad io_status_block pointer
else => unreachable, // Resource deallocation must succeed.
};
return;
}
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.flock(file.handle, posix.LOCK.UN))) {
.SUCCESS => return current_thread.endSyscall(),
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
if (is_debug) switch (e) {
.AGAIN => unreachable, // unlocking can't block
.BADF => unreachable, // File descriptor used after closed.
.INVAL => unreachable, // invalid parameters
.NOLCK => unreachable, // Resource deallocation.
.OPNOTSUPP => unreachable, // We already got the lock.
else => unreachable, // Resource deallocation must succeed.
};
return;
},
}
}
}
fn fileDowngradeLock(userdata: ?*anyopaque, file: Io.File) Io.File.DowngradeLockError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (is_windows) {
try current_thread.checkCancel();
// On Windows it works like a semaphore + exclusivity flag. To
// implement this function, we first obtain another lock in shared
// mode. This changes the exclusivity flag, but increments the
// semaphore to 2. So we follow up with an NtUnlockFile which
// decrements the semaphore but does not modify the exclusivity flag.
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
switch (windows.ntdll.NtLockFile(
file.handle,
null,
null,
null,
&io_status_block,
&windows_lock_range_off,
&windows_lock_range_len,
null,
windows.TRUE,
windows.FALSE,
)) {
.SUCCESS => {},
.INSUFFICIENT_RESOURCES => |err| return windows.statusBug(err),
.LOCK_NOT_GRANTED => |err| return windows.statusBug(err), // File was not locked in exclusive mode.
.ACCESS_VIOLATION => |err| return windows.statusBug(err), // bad io_status_block pointer
else => |status| return windows.unexpectedStatus(status),
}
const status = windows.ntdll.NtUnlockFile(
file.handle,
&io_status_block,
&windows_lock_range_off,
&windows_lock_range_len,
null,
);
if (is_debug) switch (status) {
.SUCCESS => {},
.RANGE_NOT_LOCKED => unreachable, // File was not locked.
.ACCESS_VIOLATION => unreachable, // bad io_status_block pointer
else => unreachable, // Resource deallocation must succeed.
};
return;
}
const operation = posix.LOCK.SH | posix.LOCK.NB;
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.flock(file.handle, operation))) {
.SUCCESS => {
current_thread.endSyscall();
return true;
},
.CANCELED => return current_thread.endSyscallCanceled(),
.INTR => {
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.AGAIN => |err| return errnoBug(err), // File was not locked in exclusive mode.
.BADF => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err), // invalid parameters
.NOLCK => |err| return errnoBug(err), // Lock already obtained.
.OPNOTSUPP => |err| return errnoBug(err), // Lock already obtained.
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn dirOpenDirWasi(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
options: Io.Dir.OpenOptions,
) Io.Dir.OpenError!Io.Dir {
if (builtin.link_libc) return dirOpenDirPosix(userdata, dir, sub_path, options);
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const wasi = std.os.wasi;
var base: std.os.wasi.rights_t = .{
.FD_FILESTAT_GET = true,
.FD_FDSTAT_SET_FLAGS = true,
.FD_FILESTAT_SET_TIMES = true,
};
if (options.access_sub_paths) {
base.FD_READDIR = true;
base.PATH_CREATE_DIRECTORY = true;
base.PATH_CREATE_FILE = true;
base.PATH_LINK_SOURCE = true;
base.PATH_LINK_TARGET = true;
base.PATH_OPEN = true;
base.PATH_READLINK = true;
base.PATH_RENAME_SOURCE = true;
base.PATH_RENAME_TARGET = true;
base.PATH_FILESTAT_GET = true;
base.PATH_FILESTAT_SET_SIZE = true;
base.PATH_FILESTAT_SET_TIMES = true;
base.PATH_SYMLINK = true;
base.PATH_REMOVE_DIRECTORY = true;
base.PATH_UNLINK_FILE = true;
}
const lookup_flags: wasi.lookupflags_t = .{ .SYMLINK_FOLLOW = options.follow_symlinks };
const oflags: wasi.oflags_t = .{ .DIRECTORY = true };
const fdflags: wasi.fdflags_t = .{};
var fd: posix.fd_t = undefined;
try current_thread.beginSyscall();
while (true) {
switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, base, fdflags, &fd)) {
.SUCCESS => {
current_thread.endSyscall();
return .{ .handle = fd };
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.FAULT => |err| return errnoBug(err),
.INVAL => return error.BadPathName,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.ACCES => return error.AccessDenied,
.LOOP => return error.SymLinkLoop,
.MFILE => return error.ProcessFdQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NFILE => return error.SystemFdQuotaExceeded,
.NODEV => return error.NoDevice,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
.BUSY => return error.DeviceBusy,
.NOTCAPABLE => return error.AccessDenied,
.ILSEQ => return error.BadPathName,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileClose(userdata: ?*anyopaque, file: Io.File) void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
posix.close(file.handle);
}
const fileReadStreaming = switch (native_os) {
.windows => fileReadStreamingWindows,
else => fileReadStreamingPosix,
};
fn fileReadStreamingPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.Reader.Error!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined;
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;
}
}
const dest = iovecs_buffer[0..i];
assert(dest[0].len > 0);
if (native_os == .wasi and !builtin.link_libc) {
try current_thread.beginSyscall();
while (true) {
var nread: usize = undefined;
switch (std.os.wasi.fd_read(file.handle, dest.ptr, dest.len, &nread)) {
.SUCCESS => {
current_thread.endSyscall();
return nread;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.BADF => return error.NotOpenForReading, // File operation on directory.
.IO => return error.InputOutput,
.ISDIR => return error.IsDir,
.NOBUFS => return error.SystemResources,
.NOMEM => return error.SystemResources,
.NOTCONN => return error.SocketUnconnected,
.CONNRESET => return error.ConnectionResetByPeer,
.TIMEDOUT => return error.Timeout,
.NOTCAPABLE => return error.AccessDenied,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
try current_thread.beginSyscall();
while (true) {
const rc = posix.system.readv(file.handle, dest.ptr, @intCast(dest.len));
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
return @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.SRCH => return error.ProcessNotFound,
.AGAIN => return error.WouldBlock,
.BADF => |err| {
if (native_os == .wasi) return error.NotOpenForReading; // File operation on directory.
return errnoBug(err); // File descriptor used after closed.
},
.IO => return error.InputOutput,
.ISDIR => return error.IsDir,
.NOBUFS => return error.SystemResources,
.NOMEM => return error.SystemResources,
.NOTCONN => return error.SocketUnconnected,
.CONNRESET => return error.ConnectionResetByPeer,
.TIMEDOUT => return error.Timeout,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileReadStreamingWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.Reader.Error!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const DWORD = windows.DWORD;
var index: usize = 0;
while (data[index].len == 0) index += 1;
const buffer = data[index];
const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len);
while (true) {
try current_thread.checkCancel();
var n: DWORD = undefined;
if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, null) != 0)
return n;
switch (windows.GetLastError()) {
.IO_PENDING => |err| return windows.errorBug(err),
.OPERATION_ABORTED => continue,
.BROKEN_PIPE => return 0,
.HANDLE_EOF => return 0,
.NETNAME_DELETED => return error.ConnectionResetByPeer,
.LOCK_VIOLATION => return error.LockViolation,
.ACCESS_DENIED => return error.AccessDenied,
.INVALID_HANDLE => return error.NotOpenForReading,
else => |err| return windows.unexpectedError(err),
}
}
}
fn fileReadPositionalPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (!have_preadv) @compileError("TODO");
var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined;
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;
}
}
const dest = iovecs_buffer[0..i];
assert(dest[0].len > 0);
if (native_os == .wasi and !builtin.link_libc) {
try current_thread.beginSyscall();
while (true) {
var nread: usize = undefined;
switch (std.os.wasi.fd_pread(file.handle, dest.ptr, dest.len, offset, &nread)) {
.SUCCESS => {
current_thread.endSyscall();
return nread;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.AGAIN => |err| return errnoBug(err),
.BADF => return error.NotOpenForReading, // File operation on directory.
.IO => return error.InputOutput,
.ISDIR => return error.IsDir,
.NOBUFS => return error.SystemResources,
.NOMEM => return error.SystemResources,
.NOTCONN => return error.SocketUnconnected,
.CONNRESET => return error.ConnectionResetByPeer,
.TIMEDOUT => return error.Timeout,
.NXIO => return error.Unseekable,
.SPIPE => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
.NOTCAPABLE => return error.AccessDenied,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
try current_thread.beginSyscall();
while (true) {
const rc = preadv_sym(file.handle, dest.ptr, @intCast(dest.len), @bitCast(offset));
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
return @bitCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.SRCH => return error.ProcessNotFound,
.AGAIN => return error.WouldBlock,
.BADF => |err| {
if (native_os == .wasi) return error.NotOpenForReading; // File operation on directory.
return errnoBug(err); // File descriptor used after closed.
},
.IO => return error.InputOutput,
.ISDIR => return error.IsDir,
.NOBUFS => return error.SystemResources,
.NOMEM => return error.SystemResources,
.NOTCONN => return error.SocketUnconnected,
.CONNRESET => return error.ConnectionResetByPeer,
.TIMEDOUT => return error.Timeout,
.NXIO => return error.Unseekable,
.SPIPE => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
const fileReadPositional = switch (native_os) {
.windows => fileReadPositionalWindows,
else => fileReadPositionalPosix,
};
fn fileReadPositionalWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const DWORD = windows.DWORD;
var index: usize = 0;
while (data[index].len == 0) index += 1;
const buffer = data[index];
const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len);
var overlapped: windows.OVERLAPPED = .{
.Internal = 0,
.InternalHigh = 0,
.DUMMYUNIONNAME = .{
.DUMMYSTRUCTNAME = .{
.Offset = @truncate(offset),
.OffsetHigh = @truncate(offset >> 32),
},
},
.hEvent = null,
};
while (true) {
try current_thread.checkCancel();
var n: DWORD = undefined;
if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, &overlapped) != 0)
return n;
switch (windows.GetLastError()) {
.IO_PENDING => |err| return windows.errorBug(err),
.OPERATION_ABORTED => continue,
.BROKEN_PIPE => return 0,
.HANDLE_EOF => return 0,
.NETNAME_DELETED => return error.ConnectionResetByPeer,
.LOCK_VIOLATION => return error.LockViolation,
.ACCESS_DENIED => return error.AccessDenied,
.INVALID_HANDLE => return error.NotOpenForReading,
else => |err| return windows.unexpectedError(err),
}
}
}
fn fileSeekBy(userdata: ?*anyopaque, file: Io.File, offset: i64) Io.File.SeekError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const fd = file.handle;
if (native_os == .linux and !builtin.link_libc and @sizeOf(usize) == 4) {
var result: u64 = undefined;
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.llseek(fd, offset, &result, posix.SEEK.CUR))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
.SPIPE => return error.Unseekable,
.NXIO => return error.Unseekable,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
if (native_os == .windows) {
try current_thread.checkCancel();
return windows.SetFilePointerEx_CURRENT(fd, offset);
}
if (native_os == .wasi and !builtin.link_libc) {
var new_offset: std.os.wasi.filesize_t = undefined;
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.fd_seek(fd, offset, .CUR, &new_offset)) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
.SPIPE => return error.Unseekable,
.NXIO => return error.Unseekable,
.NOTCAPABLE => return error.AccessDenied,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
if (posix.SEEK == void) return error.Unseekable;
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(lseek_sym(fd, offset, posix.SEEK.CUR))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
.SPIPE => return error.Unseekable,
.NXIO => return error.Unseekable,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const fd = file.handle;
if (native_os == .linux and !builtin.link_libc and @sizeOf(usize) == 4) {
try current_thread.beginSyscall();
while (true) {
var result: u64 = undefined;
switch (posix.errno(posix.system.llseek(fd, offset, &result, posix.SEEK.SET))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
.SPIPE => return error.Unseekable,
.NXIO => return error.Unseekable,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
if (native_os == .windows) {
try current_thread.checkCancel();
return windows.SetFilePointerEx_BEGIN(fd, offset);
}
if (native_os == .wasi and !builtin.link_libc) while (true) {
var new_offset: std.os.wasi.filesize_t = undefined;
try current_thread.beginSyscall();
switch (std.os.wasi.fd_seek(fd, @bitCast(offset), .SET, &new_offset)) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
.SPIPE => return error.Unseekable,
.NXIO => return error.Unseekable,
.NOTCAPABLE => return error.AccessDenied,
else => |err| return posix.unexpectedErrno(err),
}
},
}
};
if (posix.SEEK == void) return error.Unseekable;
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(lseek_sym(fd, @bitCast(offset), posix.SEEK.SET))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
.SPIPE => return error.Unseekable,
.NXIO => return error.Unseekable,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn openSelfExe(userdata: ?*anyopaque, flags: Io.File.OpenFlags) Io.File.OpenSelfExeError!Io.File {
const t: *Threaded = @ptrCast(@alignCast(userdata));
switch (native_os) {
.linux, .serenity => return dirOpenFilePosix(t, .{ .handle = posix.AT.FDCWD }, "/proc/self/exe", flags),
.windows => {
// If ImagePathName is a symlink, then it will contain the path of the symlink,
// not the path that the symlink points to. However, because we are opening
// the file, we can let the openFileW call follow the symlink for us.
const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName;
const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0];
const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name);
return dirOpenFileWtf16(t, null, prefixed_path_w.span(), flags);
},
else => @panic("TODO implement openSelfExe"),
}
}
fn fileWritePositional(
userdata: ?*anyopaque,
file: Io.File,
buffer: [][]const u8,
offset: u64,
) Io.File.WritePositionalError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
while (true) {
_ = file;
_ = buffer;
_ = offset;
@panic("TODO implement fileWritePositional");
}
}
fn fileWriteStreaming(userdata: ?*anyopaque, file: Io.File, buffer: [][]const u8) Io.File.WriteStreamingError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
while (true) {
_ = file;
_ = buffer;
@panic("TODO implement fileWriteStreaming");
}
}
fn nowPosix(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
const clock_id: posix.clockid_t = clockToPosix(clock);
var tp: posix.timespec = undefined;
switch (posix.errno(posix.system.clock_gettime(clock_id, &tp))) {
.SUCCESS => return timestampFromPosix(&tp),
.INVAL => return error.UnsupportedClock,
else => |err| return posix.unexpectedErrno(err),
}
}
const now = switch (native_os) {
.windows => nowWindows,
.wasi => nowWasi,
else => nowPosix,
};
fn nowWindows(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
switch (clock) {
.real => {
// RtlGetSystemTimePrecise() has a granularity of 100 nanoseconds
// and uses the NTFS/Windows epoch, which is 1601-01-01.
const epoch_ns = std.time.epoch.windows * std.time.ns_per_s;
return .{ .nanoseconds = @as(i96, windows.ntdll.RtlGetSystemTimePrecise()) * 100 + epoch_ns };
},
.awake, .boot => {
// QPC on windows doesn't fail on >= XP/2000 and includes time suspended.
const qpc = windows.QueryPerformanceCounter();
// We don't need to cache QPF as it's internally just a memory read to KUSER_SHARED_DATA
// (a read-only page of info updated and mapped by the kernel to all processes):
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-kuser_shared_data
// https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/ntexapi_x/kuser_shared_data/index.htm
const qpf = windows.QueryPerformanceFrequency();
// 10Mhz (1 qpc tick every 100ns) is a common enough QPF value that we can optimize on it.
// https://github.com/microsoft/STL/blob/785143a0c73f030238ef618890fd4d6ae2b3a3a0/stl/inc/chrono#L694-L701
const common_qpf = 10_000_000;
if (qpf == common_qpf) return .{ .nanoseconds = qpc * (std.time.ns_per_s / common_qpf) };
// Convert to ns using fixed point.
const scale = @as(u64, std.time.ns_per_s << 32) / @as(u32, @intCast(qpf));
const result = (@as(u96, qpc) * scale) >> 32;
return .{ .nanoseconds = @intCast(result) };
},
.cpu_process,
.cpu_thread,
=> return error.UnsupportedClock,
}
}
fn nowWasi(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
var ns: std.os.wasi.timestamp_t = undefined;
const err = std.os.wasi.clock_time_get(clockToWasi(clock), 1, &ns);
if (err != .SUCCESS) return error.Unexpected;
return .fromNanoseconds(ns);
}
const sleep = switch (native_os) {
.windows => sleepWindows,
.wasi => sleepWasi,
.linux => sleepLinux,
else => sleepPosix,
};
fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const clock_id: posix.clockid_t = clockToPosix(switch (timeout) {
.none => .awake,
.duration => |d| d.clock,
.deadline => |d| d.clock,
});
const deadline_nanoseconds: i96 = switch (timeout) {
.none => std.math.maxInt(i96),
.duration => |duration| duration.raw.nanoseconds,
.deadline => |deadline| deadline.raw.nanoseconds,
};
var timespec: posix.timespec = timestampToPosix(deadline_nanoseconds);
try current_thread.beginSyscall();
while (true) {
switch (std.os.linux.errno(std.os.linux.clock_nanosleep(clock_id, .{ .ABSTIME = switch (timeout) {
.none, .duration => false,
.deadline => true,
} }, &timespec, &timespec))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => return error.UnsupportedClock,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const t_io = ioBasic(t);
try current_thread.checkCancel();
const ms = ms: {
const d = (try timeout.toDurationFromNow(t_io)) orelse
break :ms std.math.maxInt(windows.DWORD);
break :ms std.math.lossyCast(windows.DWORD, d.raw.toMilliseconds());
};
// TODO: alertable true with checkCancel in a loop plus deadline
_ = windows.kernel32.SleepEx(ms, windows.FALSE);
}
fn sleepWasi(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const t_io = ioBasic(t);
const w = std.os.wasi;
const clock: w.subscription_clock_t = if (try timeout.toDurationFromNow(t_io)) |d| .{
.id = clockToWasi(d.clock),
.timeout = std.math.lossyCast(u64, d.raw.nanoseconds),
.precision = 0,
.flags = 0,
} else .{
.id = .MONOTONIC,
.timeout = std.math.maxInt(u64),
.precision = 0,
.flags = 0,
};
const in: w.subscription_t = .{
.userdata = 0,
.u = .{
.tag = .CLOCK,
.u = .{ .clock = clock },
},
};
var event: w.event_t = undefined;
var nevents: usize = undefined;
try current_thread.beginSyscall();
_ = w.poll_oneoff(&in, &event, 1, &nevents);
current_thread.endSyscall();
}
fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const t_io = ioBasic(t);
const sec_type = @typeInfo(posix.timespec).@"struct".fields[0].type;
const nsec_type = @typeInfo(posix.timespec).@"struct".fields[1].type;
var timespec: posix.timespec = t: {
const d = (try timeout.toDurationFromNow(t_io)) orelse break :t .{
.sec = std.math.maxInt(sec_type),
.nsec = std.math.maxInt(nsec_type),
};
break :t timestampToPosix(d.raw.toNanoseconds());
};
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.nanosleep(&timespec, &timespec))) {
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
// This prong handles success as well as unexpected errors.
else => return current_thread.endSyscall(),
}
}
}
fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) Io.Cancelable!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
var reset_event: ResetEvent = .unset;
for (futures, 0..) |future, i| {
const closure: *AsyncClosure = @ptrCast(@alignCast(future));
if (@atomicRmw(?*ResetEvent, &closure.select_condition, .Xchg, &reset_event, .seq_cst) == AsyncClosure.done_reset_event) {
for (futures[0..i]) |cleanup_future| {
const cleanup_closure: *AsyncClosure = @ptrCast(@alignCast(cleanup_future));
if (@atomicRmw(?*ResetEvent, &cleanup_closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) {
cleanup_closure.reset_event.waitUncancelable(); // Ensure no reference to our stack-allocated reset_event.
}
}
return i;
}
}
try reset_event.wait(t);
var result: ?usize = null;
for (futures, 0..) |future, i| {
const closure: *AsyncClosure = @ptrCast(@alignCast(future));
if (@atomicRmw(?*ResetEvent, &closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) {
closure.reset_event.waitUncancelable(); // Ensure no reference to our stack-allocated reset_event.
if (result == null) result = i; // In case multiple are ready, return first.
}
}
return result.?;
}
fn netListenIpPosix(
userdata: ?*anyopaque,
address: IpAddress,
options: IpAddress.ListenOptions,
) IpAddress.ListenError!net.Server {
if (!have_networking) return error.NetworkDown;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const family = posixAddressFamily(&address);
const socket_fd = try openSocketPosix(current_thread, family, .{
.mode = options.mode,
.protocol = options.protocol,
});
errdefer posix.close(socket_fd);
if (options.reuse_address) {
try setSocketOption(current_thread, socket_fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1);
if (@hasDecl(posix.SO, "REUSEPORT"))
try setSocketOption(current_thread, socket_fd, posix.SOL.SOCKET, posix.SO.REUSEPORT, 1);
}
var storage: PosixAddress = undefined;
var addr_len = addressToPosix(&address, &storage);
try posixBind(current_thread, socket_fd, &storage.any, addr_len);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.listen(socket_fd, options.kernel_backlog))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ADDRINUSE => return error.AddressInUse,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
try posixGetSockName(current_thread, socket_fd, &storage.any, &addr_len);
return .{
.socket = .{
.handle = socket_fd,
.address = addressFromPosix(&storage),
},
};
}
fn netListenIpWindows(
userdata: ?*anyopaque,
address: IpAddress,
options: IpAddress.ListenOptions,
) IpAddress.ListenError!net.Server {
if (!have_networking) return error.NetworkDown;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const family = posixAddressFamily(&address);
const socket_handle = try openSocketWsa(t, current_thread, family, .{
.mode = options.mode,
.protocol = options.protocol,
});
errdefer closeSocketWindows(socket_handle);
if (options.reuse_address)
try setSocketOptionWsa(t, socket_handle, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1);
var storage: WsaAddress = undefined;
var addr_len = addressToWsa(&address, &storage);
try current_thread.beginSyscall();
while (true) {
const rc = ws2_32.bind(socket_handle, &storage.any, addr_len);
if (rc != ws2_32.SOCKET_ERROR) {
current_thread.endSyscall();
break;
}
switch (ws2_32.WSAGetLastError()) {
.EINTR => {
try current_thread.checkCancel();
continue;
},
.NOTINITIALISED => {
try initializeWsa(t);
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.EADDRINUSE => return error.AddressInUse,
.EADDRNOTAVAIL => return error.AddressUnavailable,
.ENOTSOCK => |err| return wsaErrorBug(err),
.EFAULT => |err| return wsaErrorBug(err),
.EINVAL => |err| return wsaErrorBug(err),
.ENOBUFS => return error.SystemResources,
.ENETDOWN => return error.NetworkDown,
else => |err| return windows.unexpectedWSAError(err),
}
},
}
}
try current_thread.beginSyscall();
while (true) {
const rc = ws2_32.listen(socket_handle, options.kernel_backlog);
if (rc != ws2_32.SOCKET_ERROR) {
current_thread.endSyscall();
break;
}
switch (ws2_32.WSAGetLastError()) {
.EINTR => {
try current_thread.checkCancel();
continue;
},
.NOTINITIALISED => {
try initializeWsa(t);
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.ENETDOWN => return error.NetworkDown,
.EADDRINUSE => return error.AddressInUse,
.EISCONN => |err| return wsaErrorBug(err),
.EINVAL => |err| return wsaErrorBug(err),
.EMFILE, .ENOBUFS => return error.SystemResources,
.ENOTSOCK => |err| return wsaErrorBug(err),
.EOPNOTSUPP => |err| return wsaErrorBug(err),
.EINPROGRESS => |err| return wsaErrorBug(err),
else => |err| return windows.unexpectedWSAError(err),
}
},
}
}
try wsaGetSockName(t, current_thread, socket_handle, &storage.any, &addr_len);
return .{
.socket = .{
.handle = socket_handle,
.address = addressFromWsa(&storage),
},
};
}
fn netListenIpUnavailable(
userdata: ?*anyopaque,
address: IpAddress,
options: IpAddress.ListenOptions,
) IpAddress.ListenError!net.Server {
_ = userdata;
_ = address;
_ = options;
return error.NetworkDown;
}
fn netListenUnixPosix(
userdata: ?*anyopaque,
address: *const net.UnixAddress,
options: net.UnixAddress.ListenOptions,
) net.UnixAddress.ListenError!net.Socket.Handle {
if (!net.has_unix_sockets) return error.AddressFamilyUnsupported;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const socket_fd = openSocketPosix(current_thread, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) {
error.ProtocolUnsupportedBySystem => return error.AddressFamilyUnsupported,
error.ProtocolUnsupportedByAddressFamily => return error.AddressFamilyUnsupported,
error.SocketModeUnsupported => return error.AddressFamilyUnsupported,
error.OptionUnsupported => return error.Unexpected,
else => |e| return e,
};
errdefer posix.close(socket_fd);
var storage: UnixAddress = undefined;
const addr_len = addressUnixToPosix(address, &storage);
try posixBindUnix(current_thread, socket_fd, &storage.any, addr_len);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.listen(socket_fd, options.kernel_backlog))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ADDRINUSE => return error.AddressInUse,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
return socket_fd;
}
fn netListenUnixWindows(
userdata: ?*anyopaque,
address: *const net.UnixAddress,
options: net.UnixAddress.ListenOptions,
) net.UnixAddress.ListenError!net.Socket.Handle {
if (!net.has_unix_sockets) return error.AddressFamilyUnsupported;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const socket_handle = openSocketWsa(t, current_thread, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) {
error.ProtocolUnsupportedByAddressFamily => return error.AddressFamilyUnsupported,
else => |e| return e,
};
errdefer closeSocketWindows(socket_handle);
var storage: WsaAddress = undefined;
const addr_len = addressUnixToWsa(address, &storage);
try current_thread.beginSyscall();
while (true) {
const rc = ws2_32.bind(socket_handle, &storage.any, addr_len);
if (rc != ws2_32.SOCKET_ERROR) break;
switch (ws2_32.WSAGetLastError()) {
.EINTR => {
try current_thread.checkCancel();
continue;
},
.NOTINITIALISED => {
try initializeWsa(t);
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.EADDRINUSE => return error.AddressInUse,
.EADDRNOTAVAIL => return error.AddressUnavailable,
.ENOTSOCK => |err| return wsaErrorBug(err),
.EFAULT => |err| return wsaErrorBug(err),
.EINVAL => |err| return wsaErrorBug(err),
.ENOBUFS => return error.SystemResources,
.ENETDOWN => return error.NetworkDown,
else => |err| return windows.unexpectedWSAError(err),
}
},
}
}
while (true) {
try current_thread.checkCancel();
const rc = ws2_32.listen(socket_handle, options.kernel_backlog);
if (rc != ws2_32.SOCKET_ERROR) {
current_thread.endSyscall();
return socket_handle;
}
switch (ws2_32.WSAGetLastError()) {
.EINTR => continue,
.NOTINITIALISED => {
try initializeWsa(t);
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.ENETDOWN => return error.NetworkDown,
.EADDRINUSE => return error.AddressInUse,
.EISCONN => |err| return wsaErrorBug(err),
.EINVAL => |err| return wsaErrorBug(err),
.EMFILE, .ENOBUFS => return error.SystemResources,
.ENOTSOCK => |err| return wsaErrorBug(err),
.EOPNOTSUPP => |err| return wsaErrorBug(err),
.EINPROGRESS => |err| return wsaErrorBug(err),
else => |err| return windows.unexpectedWSAError(err),
}
},
}
}
}
fn netListenUnixUnavailable(
userdata: ?*anyopaque,
address: *const net.UnixAddress,
options: net.UnixAddress.ListenOptions,
) net.UnixAddress.ListenError!net.Socket.Handle {
_ = userdata;
_ = address;
_ = options;
return error.AddressFamilyUnsupported;
}
fn posixBindUnix(
current_thread: *Thread,
fd: posix.socket_t,
addr: *const posix.sockaddr,
addr_len: posix.socklen_t,
) !void {
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.bind(fd, addr, addr_len))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.ADDRINUSE => return error.AddressInUse,
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
.ADDRNOTAVAIL => return error.AddressUnavailable,
.NOMEM => return error.SystemResources,
.LOOP => return error.SymLinkLoop,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.ROFS => return error.ReadOnlyFileSystem,
.PERM => return error.PermissionDenied,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => |err| return errnoBug(err), // invalid parameters
.NOTSOCK => |err| return errnoBug(err), // invalid `sockfd`
.FAULT => |err| return errnoBug(err), // invalid `addr` pointer
.NAMETOOLONG => |err| return errnoBug(err),
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn posixBind(
current_thread: *Thread,
socket_fd: posix.socket_t,
addr: *const posix.sockaddr,
addr_len: posix.socklen_t,
) !void {
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.bind(socket_fd, addr, addr_len))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ADDRINUSE => return error.AddressInUse,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.INVAL => |err| return errnoBug(err), // invalid parameters
.NOTSOCK => |err| return errnoBug(err), // invalid `sockfd`
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
.ADDRNOTAVAIL => return error.AddressUnavailable,
.FAULT => |err| return errnoBug(err), // invalid `addr` pointer
.NOMEM => return error.SystemResources,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn posixConnect(
current_thread: *Thread,
socket_fd: posix.socket_t,
addr: *const posix.sockaddr,
addr_len: posix.socklen_t,
) !void {
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.connect(socket_fd, addr, addr_len))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ADDRNOTAVAIL => return error.AddressUnavailable,
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
.AGAIN, .INPROGRESS => return error.WouldBlock,
.ALREADY => return error.ConnectionPending,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.CONNREFUSED => return error.ConnectionRefused,
.CONNRESET => return error.ConnectionResetByPeer,
.FAULT => |err| return errnoBug(err),
.ISCONN => |err| return errnoBug(err),
.HOSTUNREACH => return error.HostUnreachable,
.NETUNREACH => return error.NetworkUnreachable,
.NOTSOCK => |err| return errnoBug(err),
.PROTOTYPE => |err| return errnoBug(err),
.TIMEDOUT => return error.Timeout,
.CONNABORTED => |err| return errnoBug(err),
.ACCES => return error.AccessDenied,
.PERM => |err| return errnoBug(err),
.NOENT => |err| return errnoBug(err),
.NETDOWN => return error.NetworkDown,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn posixConnectUnix(
current_thread: *Thread,
fd: posix.socket_t,
addr: *const posix.sockaddr,
addr_len: posix.socklen_t,
) !void {
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.connect(fd, addr, addr_len))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
.AGAIN => return error.WouldBlock,
.INPROGRESS => return error.WouldBlock,
.ACCES => return error.AccessDenied,
.LOOP => return error.SymLinkLoop,
.NOENT => return error.FileNotFound,
.NOTDIR => return error.NotDir,
.ROFS => return error.ReadOnlyFileSystem,
.PERM => return error.PermissionDenied,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.CONNABORTED => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.ISCONN => |err| return errnoBug(err),
.NOTSOCK => |err| return errnoBug(err),
.PROTOTYPE => |err| return errnoBug(err),
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn posixGetSockName(
current_thread: *Thread,
socket_fd: posix.fd_t,
addr: *posix.sockaddr,
addr_len: *posix.socklen_t,
) !void {
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.getsockname(socket_fd, addr, addr_len))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err), // invalid parameters
.NOTSOCK => |err| return errnoBug(err), // always a race condition
.NOBUFS => return error.SystemResources,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn wsaGetSockName(
t: *Threaded,
current_thread: *Thread,
handle: ws2_32.SOCKET,
addr: *ws2_32.sockaddr,
addr_len: *i32,
) !void {
try current_thread.beginSyscall();
while (true) {
const rc = ws2_32.getsockname(handle, addr, addr_len);
if (rc != ws2_32.SOCKET_ERROR) {
current_thread.endSyscall();
return;
}
switch (ws2_32.WSAGetLastError()) {
.EINTR => {
try current_thread.checkCancel();
continue;
},
.NOTINITIALISED => {
try initializeWsa(t);
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.ENETDOWN => return error.NetworkDown,
.EFAULT => |err| return wsaErrorBug(err),
.ENOTSOCK => |err| return wsaErrorBug(err),
.EINVAL => |err| return wsaErrorBug(err),
else => |err| return windows.unexpectedWSAError(err),
}
},
}
}
}
fn setSocketOption(current_thread: *Thread, fd: posix.fd_t, level: i32, opt_name: u32, option: u32) !void {
const o: []const u8 = @ptrCast(&option);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.setsockopt(fd, level, opt_name, o.ptr, @intCast(o.len)))) {
.SUCCESS => {
current_thread.endSyscall();
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.NOTSOCK => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn setSocketOptionWsa(t: *Threaded, socket: Io.net.Socket.Handle, level: i32, opt_name: u32, option: u32) !void {
const o: []const u8 = @ptrCast(&option);
const rc = ws2_32.setsockopt(socket, level, @bitCast(opt_name), o.ptr, @intCast(o.len));
while (true) {
if (rc != ws2_32.SOCKET_ERROR) return;
switch (ws2_32.WSAGetLastError()) {
.EINTR => continue,
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.NOTINITIALISED => {
try initializeWsa(t);
continue;
},
.ENETDOWN => return error.NetworkDown,
.EFAULT => |err| return wsaErrorBug(err),
.ENOTSOCK => |err| return wsaErrorBug(err),
.EINVAL => |err| return wsaErrorBug(err),
else => |err| return windows.unexpectedWSAError(err),
}
}
}
fn netConnectIpPosix(
userdata: ?*anyopaque,
address: *const IpAddress,
options: IpAddress.ConnectOptions,
) IpAddress.ConnectError!net.Stream {
if (!have_networking) return error.NetworkDown;
if (options.timeout != .none) @panic("TODO implement netConnectIpPosix with timeout");
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const family = posixAddressFamily(address);
const socket_fd = try openSocketPosix(current_thread, family, .{
.mode = options.mode,
.protocol = options.protocol,
});
errdefer posix.close(socket_fd);
var storage: PosixAddress = undefined;
var addr_len = addressToPosix(address, &storage);
try posixConnect(current_thread, socket_fd, &storage.any, addr_len);
try posixGetSockName(current_thread, socket_fd, &storage.any, &addr_len);
return .{ .socket = .{
.handle = socket_fd,
.address = addressFromPosix(&storage),
} };
}
fn netConnectIpWindows(
userdata: ?*anyopaque,
address: *const IpAddress,
options: IpAddress.ConnectOptions,
) IpAddress.ConnectError!net.Stream {
if (!have_networking) return error.NetworkDown;
if (options.timeout != .none) @panic("TODO implement netConnectIpWindows with timeout");
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const family = posixAddressFamily(address);
const socket_handle = try openSocketWsa(t, current_thread, family, .{
.mode = options.mode,
.protocol = options.protocol,
});
errdefer closeSocketWindows(socket_handle);
var storage: WsaAddress = undefined;
var addr_len = addressToWsa(address, &storage);
try current_thread.beginSyscall();
while (true) {
const rc = ws2_32.connect(socket_handle, &storage.any, addr_len);
if (rc != ws2_32.SOCKET_ERROR) {
current_thread.endSyscall();
break;
}
switch (ws2_32.WSAGetLastError()) {
.EINTR => {
try current_thread.checkCancel();
continue;
},
.NOTINITIALISED => {
try initializeWsa(t);
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.EADDRNOTAVAIL => return error.AddressUnavailable,
.ECONNREFUSED => return error.ConnectionRefused,
.ECONNRESET => return error.ConnectionResetByPeer,
.ETIMEDOUT => return error.Timeout,
.EHOSTUNREACH => return error.HostUnreachable,
.ENETUNREACH => return error.NetworkUnreachable,
.EFAULT => |err| return wsaErrorBug(err),
.EINVAL => |err| return wsaErrorBug(err),
.EISCONN => |err| return wsaErrorBug(err),
.ENOTSOCK => |err| return wsaErrorBug(err),
.EWOULDBLOCK => return error.WouldBlock,
.EACCES => return error.AccessDenied,
.ENOBUFS => return error.SystemResources,
.EAFNOSUPPORT => return error.AddressFamilyUnsupported,
else => |err| return windows.unexpectedWSAError(err),
}
},
}
}
try wsaGetSockName(t, current_thread, socket_handle, &storage.any, &addr_len);
return .{ .socket = .{
.handle = socket_handle,
.address = addressFromWsa(&storage),
} };
}
fn netConnectIpUnavailable(
userdata: ?*anyopaque,
address: *const IpAddress,
options: IpAddress.ConnectOptions,
) IpAddress.ConnectError!net.Stream {
_ = userdata;
_ = address;
_ = options;
return error.NetworkDown;
}
fn netConnectUnixPosix(
userdata: ?*anyopaque,
address: *const net.UnixAddress,
) net.UnixAddress.ConnectError!net.Socket.Handle {
if (!net.has_unix_sockets) return error.AddressFamilyUnsupported;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const socket_fd = openSocketPosix(current_thread, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) {
error.OptionUnsupported => return error.Unexpected,
else => |e| return e,
};
errdefer posix.close(socket_fd);
var storage: UnixAddress = undefined;
const addr_len = addressUnixToPosix(address, &storage);
try posixConnectUnix(current_thread, socket_fd, &storage.any, addr_len);
return socket_fd;
}
fn netConnectUnixWindows(
userdata: ?*anyopaque,
address: *const net.UnixAddress,
) net.UnixAddress.ConnectError!net.Socket.Handle {
if (!net.has_unix_sockets) return error.AddressFamilyUnsupported;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const socket_handle = try openSocketWsa(t, current_thread, posix.AF.UNIX, .{ .mode = .stream });
errdefer closeSocketWindows(socket_handle);
var storage: WsaAddress = undefined;
const addr_len = addressUnixToWsa(address, &storage);
while (true) {
const rc = ws2_32.connect(socket_handle, &storage.any, addr_len);
if (rc != ws2_32.SOCKET_ERROR) break;
switch (ws2_32.WSAGetLastError()) {
.EINTR => continue,
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.NOTINITIALISED => {
try initializeWsa(t);
continue;
},
.ECONNREFUSED => return error.FileNotFound,
.EFAULT => |err| return wsaErrorBug(err),
.EINVAL => |err| return wsaErrorBug(err),
.EISCONN => |err| return wsaErrorBug(err),
.ENOTSOCK => |err| return wsaErrorBug(err),
.EWOULDBLOCK => return error.WouldBlock,
.EACCES => return error.AccessDenied,
.ENOBUFS => return error.SystemResources,
.EAFNOSUPPORT => return error.AddressFamilyUnsupported,
else => |err| return windows.unexpectedWSAError(err),
}
}
return socket_handle;
}
fn netConnectUnixUnavailable(
userdata: ?*anyopaque,
address: *const net.UnixAddress,
) net.UnixAddress.ConnectError!net.Socket.Handle {
_ = userdata;
_ = address;
return error.AddressFamilyUnsupported;
}
fn netBindIpPosix(
userdata: ?*anyopaque,
address: *const IpAddress,
options: IpAddress.BindOptions,
) IpAddress.BindError!net.Socket {
if (!have_networking) return error.NetworkDown;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const family = posixAddressFamily(address);
const socket_fd = try openSocketPosix(current_thread, family, options);
errdefer posix.close(socket_fd);
var storage: PosixAddress = undefined;
var addr_len = addressToPosix(address, &storage);
try posixBind(current_thread, socket_fd, &storage.any, addr_len);
try posixGetSockName(current_thread, socket_fd, &storage.any, &addr_len);
return .{
.handle = socket_fd,
.address = addressFromPosix(&storage),
};
}
fn netBindIpWindows(
userdata: ?*anyopaque,
address: *const IpAddress,
options: IpAddress.BindOptions,
) IpAddress.BindError!net.Socket {
if (!have_networking) return error.NetworkDown;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const family = posixAddressFamily(address);
const socket_handle = try openSocketWsa(t, current_thread, family, .{
.mode = options.mode,
.protocol = options.protocol,
});
errdefer closeSocketWindows(socket_handle);
var storage: WsaAddress = undefined;
var addr_len = addressToWsa(address, &storage);
try current_thread.beginSyscall();
while (true) {
const rc = ws2_32.bind(socket_handle, &storage.any, addr_len);
if (rc != ws2_32.SOCKET_ERROR) {
current_thread.endSyscall();
break;
}
switch (ws2_32.WSAGetLastError()) {
.EINTR => {
try current_thread.checkCancel();
continue;
},
.NOTINITIALISED => {
try initializeWsa(t);
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.EADDRINUSE => return error.AddressInUse,
.EADDRNOTAVAIL => return error.AddressUnavailable,
.ENOTSOCK => |err| return wsaErrorBug(err),
.EFAULT => |err| return wsaErrorBug(err),
.EINVAL => |err| return wsaErrorBug(err),
.ENOBUFS => return error.SystemResources,
.ENETDOWN => return error.NetworkDown,
else => |err| return windows.unexpectedWSAError(err),
}
},
}
}
try wsaGetSockName(t, current_thread, socket_handle, &storage.any, &addr_len);
return .{
.handle = socket_handle,
.address = addressFromWsa(&storage),
};
}
fn netBindIpUnavailable(
userdata: ?*anyopaque,
address: *const IpAddress,
options: IpAddress.BindOptions,
) IpAddress.BindError!net.Socket {
_ = userdata;
_ = address;
_ = options;
return error.NetworkDown;
}
fn openSocketPosix(
current_thread: *Thread,
family: posix.sa_family_t,
options: IpAddress.BindOptions,
) error{
AddressFamilyUnsupported,
ProtocolUnsupportedBySystem,
ProcessFdQuotaExceeded,
SystemFdQuotaExceeded,
SystemResources,
ProtocolUnsupportedByAddressFamily,
SocketModeUnsupported,
OptionUnsupported,
Unexpected,
Canceled,
}!posix.socket_t {
const mode = posixSocketMode(options.mode);
const protocol = posixProtocol(options.protocol);
try current_thread.beginSyscall();
const socket_fd = while (true) {
const flags: u32 = mode | if (socket_flags_unsupported) 0 else posix.SOCK.CLOEXEC;
const socket_rc = posix.system.socket(family, flags, protocol);
switch (posix.errno(socket_rc)) {
.SUCCESS => {
const fd: posix.fd_t = @intCast(socket_rc);
errdefer posix.close(fd);
if (socket_flags_unsupported) while (true) {
try current_thread.checkCancel();
switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) {
.SUCCESS => break,
.INTR => continue,
.CANCELED => return current_thread.endSyscallCanceled(),
else => |err| {
current_thread.endSyscall();
return posix.unexpectedErrno(err);
},
}
};
current_thread.endSyscall();
break fd;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
.INVAL => return error.ProtocolUnsupportedBySystem,
.MFILE => return error.ProcessFdQuotaExceeded,
.NFILE => return error.SystemFdQuotaExceeded,
.NOBUFS => return error.SystemResources,
.NOMEM => return error.SystemResources,
.PROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily,
.PROTOTYPE => return error.SocketModeUnsupported,
else => |err| return posix.unexpectedErrno(err),
}
},
}
};
errdefer posix.close(socket_fd);
if (options.ip6_only) {
if (posix.IPV6 == void) return error.OptionUnsupported;
try setSocketOption(current_thread, socket_fd, posix.IPPROTO.IPV6, posix.IPV6.V6ONLY, 0);
}
return socket_fd;
}
fn openSocketWsa(
t: *Threaded,
current_thread: *Thread,
family: posix.sa_family_t,
options: IpAddress.BindOptions,
) !ws2_32.SOCKET {
const mode = posixSocketMode(options.mode);
const protocol = posixProtocol(options.protocol);
const flags: u32 = ws2_32.WSA_FLAG_OVERLAPPED | ws2_32.WSA_FLAG_NO_HANDLE_INHERIT;
try current_thread.beginSyscall();
while (true) {
const rc = ws2_32.WSASocketW(family, @bitCast(mode), @bitCast(protocol), null, 0, flags);
if (rc != ws2_32.INVALID_SOCKET) {
current_thread.endSyscall();
return rc;
}
switch (ws2_32.WSAGetLastError()) {
.EINTR => {
try current_thread.checkCancel();
continue;
},
.NOTINITIALISED => {
try initializeWsa(t);
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.EAFNOSUPPORT => return error.AddressFamilyUnsupported,
.EMFILE => return error.ProcessFdQuotaExceeded,
.ENOBUFS => return error.SystemResources,
.EPROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily,
else => |err| return windows.unexpectedWSAError(err),
}
},
}
}
}
fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Server.AcceptError!net.Stream {
if (!have_networking) return error.NetworkDown;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var storage: PosixAddress = undefined;
var addr_len: posix.socklen_t = @sizeOf(PosixAddress);
try current_thread.beginSyscall();
const fd = while (true) {
const rc = if (have_accept4)
posix.system.accept4(listen_fd, &storage.any, &addr_len, posix.SOCK.CLOEXEC)
else
posix.system.accept(listen_fd, &storage.any, &addr_len);
switch (posix.errno(rc)) {
.SUCCESS => {
const fd: posix.fd_t = @intCast(rc);
errdefer posix.close(fd);
if (!have_accept4) while (true) {
try current_thread.checkCancel();
switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) {
.SUCCESS => break,
.INTR => continue,
else => |err| {
current_thread.endSyscall();
return posix.unexpectedErrno(err);
},
}
};
current_thread.endSyscall();
break fd;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.AGAIN => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.CONNABORTED => return error.ConnectionAborted,
.FAULT => |err| return errnoBug(err),
.INVAL => return error.SocketNotListening,
.NOTSOCK => |err| return errnoBug(err),
.MFILE => return error.ProcessFdQuotaExceeded,
.NFILE => return error.SystemFdQuotaExceeded,
.NOBUFS => return error.SystemResources,
.NOMEM => return error.SystemResources,
.OPNOTSUPP => |err| return errnoBug(err),
.PROTO => return error.ProtocolFailure,
.PERM => return error.BlockedByFirewall,
else => |err| return posix.unexpectedErrno(err),
}
},
}
};
return .{ .socket = .{
.handle = fd,
.address = addressFromPosix(&storage),
} };
}
fn netAcceptWindows(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net.Server.AcceptError!net.Stream {
if (!have_networking) return error.NetworkDown;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var storage: WsaAddress = undefined;
var addr_len: i32 = @sizeOf(WsaAddress);
try current_thread.beginSyscall();
while (true) {
const rc = ws2_32.accept(listen_handle, &storage.any, &addr_len);
if (rc != ws2_32.INVALID_SOCKET) {
current_thread.endSyscall();
return .{ .socket = .{
.handle = rc,
.address = addressFromWsa(&storage),
} };
}
switch (ws2_32.WSAGetLastError()) {
.EINTR => {
try current_thread.checkCancel();
continue;
},
.NOTINITIALISED => {
try initializeWsa(t);
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.ECONNRESET => return error.ConnectionAborted,
.EFAULT => |err| return wsaErrorBug(err),
.ENOTSOCK => |err| return wsaErrorBug(err),
.EINVAL => |err| return wsaErrorBug(err),
.EMFILE => return error.ProcessFdQuotaExceeded,
.ENETDOWN => return error.NetworkDown,
.ENOBUFS => return error.SystemResources,
.EOPNOTSUPP => |err| return wsaErrorBug(err),
else => |err| return windows.unexpectedWSAError(err),
}
},
}
}
}
fn netAcceptUnavailable(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net.Server.AcceptError!net.Stream {
_ = userdata;
_ = listen_handle;
return error.NetworkDown;
}
fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize {
if (!have_networking) return error.NetworkDown;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined;
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;
}
}
const dest = iovecs_buffer[0..i];
assert(dest[0].len > 0);
if (native_os == .wasi and !builtin.link_libc) {
try current_thread.beginSyscall();
while (true) {
var n: usize = undefined;
switch (std.os.wasi.fd_read(fd, dest.ptr, dest.len, &n)) {
.SUCCESS => {
current_thread.endSyscall();
return n;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.AGAIN => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.NOBUFS => return error.SystemResources,
.NOMEM => return error.SystemResources,
.NOTCONN => return error.SocketUnconnected,
.CONNRESET => return error.ConnectionResetByPeer,
.TIMEDOUT => return error.Timeout,
.NOTCAPABLE => return error.AccessDenied,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
try current_thread.beginSyscall();
while (true) {
const rc = posix.system.readv(fd, dest.ptr, @intCast(dest.len));
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
return @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.AGAIN => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.NOBUFS => return error.SystemResources,
.NOMEM => return error.SystemResources,
.NOTCONN => return error.SocketUnconnected,
.CONNRESET => return error.ConnectionResetByPeer,
.TIMEDOUT => return error.Timeout,
.PIPE => return error.SocketUnconnected,
.NETDOWN => return error.NetworkDown,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn netReadWindows(userdata: ?*anyopaque, handle: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize {
if (!have_networking) return error.NetworkDown;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const bufs = b: {
var iovec_buffer: [max_iovecs_len]ws2_32.WSABUF = undefined;
var i: usize = 0;
var n: usize = 0;
for (data) |buf| {
if (iovec_buffer.len - i == 0) break;
if (buf.len == 0) continue;
if (std.math.cast(u32, buf.len)) |len| {
iovec_buffer[i] = .{ .buf = buf.ptr, .len = len };
i += 1;
n += len;
continue;
}
iovec_buffer[i] = .{ .buf = buf.ptr, .len = std.math.maxInt(u32) };
i += 1;
n += std.math.maxInt(u32);
break;
}
const bufs = iovec_buffer[0..i];
assert(bufs[0].len != 0);
break :b bufs;
};
while (true) {
try current_thread.checkCancel();
var flags: u32 = 0;
var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED);
var n: u32 = undefined;
const rc = ws2_32.WSARecv(handle, bufs.ptr, @intCast(bufs.len), &n, &flags, &overlapped, null);
if (rc != ws2_32.SOCKET_ERROR) return n;
const wsa_error: ws2_32.WinsockError = switch (ws2_32.WSAGetLastError()) {
.IO_PENDING => e: {
var result_flags: u32 = undefined;
const overlapped_rc = ws2_32.WSAGetOverlappedResult(
handle,
&overlapped,
&n,
windows.TRUE,
&result_flags,
);
if (overlapped_rc == windows.FALSE) {
break :e ws2_32.WSAGetLastError();
} else {
return n;
}
},
else => |err| err,
};
switch (wsa_error) {
.EINTR => continue,
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.NOTINITIALISED => {
try initializeWsa(t);
continue;
},
.ECONNRESET => return error.ConnectionResetByPeer,
.EFAULT => unreachable, // a pointer is not completely contained in user address space.
.EINVAL => |err| return wsaErrorBug(err),
.EMSGSIZE => |err| return wsaErrorBug(err),
.ENETDOWN => return error.NetworkDown,
.ENETRESET => return error.ConnectionResetByPeer,
.ENOTCONN => return error.SocketUnconnected,
else => |err| return windows.unexpectedWSAError(err),
}
}
}
fn netReadUnavailable(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize {
_ = userdata;
_ = fd;
_ = data;
return error.NetworkDown;
}
fn netSendPosix(
userdata: ?*anyopaque,
handle: net.Socket.Handle,
messages: []net.OutgoingMessage,
flags: net.SendFlags,
) struct { ?net.Socket.SendError, usize } {
if (!have_networking) return .{ error.NetworkDown, 0 };
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const posix_flags: u32 =
@as(u32, if (@hasDecl(posix.MSG, "CONFIRM") and flags.confirm) posix.MSG.CONFIRM else 0) |
@as(u32, if (@hasDecl(posix.MSG, "DONTROUTE") and flags.dont_route) posix.MSG.DONTROUTE else 0) |
@as(u32, if (@hasDecl(posix.MSG, "EOR") and flags.eor) posix.MSG.EOR else 0) |
@as(u32, if (@hasDecl(posix.MSG, "OOB") and flags.oob) posix.MSG.OOB else 0) |
@as(u32, if (@hasDecl(posix.MSG, "FASTOPEN") and flags.fastopen) posix.MSG.FASTOPEN else 0) |
posix.MSG.NOSIGNAL;
var i: usize = 0;
while (messages.len - i != 0) {
if (have_sendmmsg) {
i += netSendMany(current_thread, handle, messages[i..], posix_flags) catch |err| return .{ err, i };
continue;
}
netSendOne(t, current_thread, handle, &messages[i], posix_flags) catch |err| return .{ err, i };
i += 1;
}
return .{ null, i };
}
fn netSendWindows(
userdata: ?*anyopaque,
handle: net.Socket.Handle,
messages: []net.OutgoingMessage,
flags: net.SendFlags,
) struct { ?net.Socket.SendError, usize } {
if (!have_networking) return .{ error.NetworkDown, 0 };
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
_ = handle;
_ = messages;
_ = flags;
@panic("TODO netSendWindows");
}
fn netSendUnavailable(
userdata: ?*anyopaque,
handle: net.Socket.Handle,
messages: []net.OutgoingMessage,
flags: net.SendFlags,
) struct { ?net.Socket.SendError, usize } {
_ = userdata;
_ = handle;
_ = messages;
_ = flags;
return .{ error.NetworkDown, 0 };
}
fn netSendOne(
t: *Threaded,
current_thread: *Thread,
handle: net.Socket.Handle,
message: *net.OutgoingMessage,
flags: u32,
) net.Socket.SendError!void {
var addr: PosixAddress = undefined;
var iovec: posix.iovec_const = .{ .base = @constCast(message.data_ptr), .len = message.data_len };
const msg: posix.msghdr_const = .{
.name = &addr.any,
.namelen = addressToPosix(message.address, &addr),
.iov = (&iovec)[0..1],
.iovlen = 1,
// OS returns EINVAL if this pointer is invalid even if controllen is zero.
.control = if (message.control.len == 0) null else @constCast(message.control.ptr),
.controllen = @intCast(message.control.len),
.flags = 0,
};
try current_thread.beginSyscall();
while (true) {
const rc = posix.system.sendmsg(handle, &msg, flags);
if (is_windows) {
if (rc != ws2_32.SOCKET_ERROR) {
current_thread.endSyscall();
message.data_len = @intCast(rc);
return;
}
switch (ws2_32.WSAGetLastError()) {
.EINTR => {
try current_thread.checkCancel();
continue;
},
.NOTINITIALISED => {
try initializeWsa(t);
try current_thread.checkCancel();
continue;
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.EACCES => return error.AccessDenied,
.EADDRNOTAVAIL => return error.AddressUnavailable,
.ECONNRESET => return error.ConnectionResetByPeer,
.EMSGSIZE => return error.MessageOversize,
.ENOBUFS => return error.SystemResources,
.ENOTSOCK => return error.FileDescriptorNotASocket,
.EAFNOSUPPORT => return error.AddressFamilyUnsupported,
.EDESTADDRREQ => unreachable, // A destination address is required.
.EFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small.
.EHOSTUNREACH => return error.NetworkUnreachable,
.EINVAL => unreachable,
.ENETDOWN => return error.NetworkDown,
.ENETRESET => return error.ConnectionResetByPeer,
.ENETUNREACH => return error.NetworkUnreachable,
.ENOTCONN => return error.SocketUnconnected,
.ESHUTDOWN => |err| return wsaErrorBug(err),
else => |err| return windows.unexpectedWSAError(err),
}
},
}
}
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
message.data_len = @intCast(rc);
return;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => return error.AccessDenied,
.ALREADY => return error.FastOpenAlreadyInProgress,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.CONNRESET => return error.ConnectionResetByPeer,
.DESTADDRREQ => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.INVAL => |err| return errnoBug(err),
.ISCONN => |err| return errnoBug(err),
.MSGSIZE => return error.MessageOversize,
.NOBUFS => return error.SystemResources,
.NOMEM => return error.SystemResources,
.NOTSOCK => |err| return errnoBug(err),
.OPNOTSUPP => |err| return errnoBug(err),
.PIPE => return error.SocketUnconnected,
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
.HOSTUNREACH => return error.HostUnreachable,
.NETUNREACH => return error.NetworkUnreachable,
.NOTCONN => return error.SocketUnconnected,
.NETDOWN => return error.NetworkDown,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn netSendMany(
current_thread: *Thread,
handle: net.Socket.Handle,
messages: []net.OutgoingMessage,
flags: u32,
) net.Socket.SendError!usize {
var msg_buffer: [64]posix.system.mmsghdr = undefined;
var addr_buffer: [msg_buffer.len]PosixAddress = undefined;
var iovecs_buffer: [msg_buffer.len]posix.iovec = undefined;
const min_len: usize = @min(messages.len, msg_buffer.len);
const clamped_messages = messages[0..min_len];
const clamped_msgs = (&msg_buffer)[0..min_len];
const clamped_addrs = (&addr_buffer)[0..min_len];
const clamped_iovecs = (&iovecs_buffer)[0..min_len];
for (clamped_messages, clamped_msgs, clamped_addrs, clamped_iovecs) |*message, *msg, *addr, *iovec| {
iovec.* = .{ .base = @constCast(message.data_ptr), .len = message.data_len };
msg.* = .{
.hdr = .{
.name = &addr.any,
.namelen = addressToPosix(message.address, addr),
.iov = iovec[0..1],
.iovlen = 1,
.control = @constCast(message.control.ptr),
.controllen = message.control.len,
.flags = 0,
},
.len = undefined, // Populated by calling sendmmsg below.
};
}
try current_thread.beginSyscall();
while (true) {
const rc = posix.system.sendmmsg(handle, clamped_msgs.ptr, @intCast(clamped_msgs.len), flags);
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
const n: usize = @intCast(rc);
for (clamped_messages[0..n], clamped_msgs[0..n]) |*message, *msg| {
message.data_len = msg.len;
}
return n;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.AGAIN => |err| return errnoBug(err),
.ALREADY => return error.FastOpenAlreadyInProgress,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.CONNRESET => return error.ConnectionResetByPeer,
.DESTADDRREQ => |err| return errnoBug(err), // The socket is not connection-mode, and no peer address is set.
.FAULT => |err| return errnoBug(err), // An invalid user space address was specified for an argument.
.INVAL => |err| return errnoBug(err), // Invalid argument passed.
.ISCONN => |err| return errnoBug(err), // connection-mode socket was connected already but a recipient was specified
.MSGSIZE => return error.MessageOversize,
.NOBUFS => return error.SystemResources,
.NOMEM => return error.SystemResources,
.NOTSOCK => |err| return errnoBug(err), // The file descriptor sockfd does not refer to a socket.
.OPNOTSUPP => |err| return errnoBug(err), // Some bit in the flags argument is inappropriate for the socket type.
.PIPE => return error.SocketUnconnected,
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
.HOSTUNREACH => return error.HostUnreachable,
.NETUNREACH => return error.NetworkUnreachable,
.NOTCONN => return error.SocketUnconnected,
.NETDOWN => return error.NetworkDown,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn netReceivePosix(
userdata: ?*anyopaque,
handle: net.Socket.Handle,
message_buffer: []net.IncomingMessage,
data_buffer: []u8,
flags: net.ReceiveFlags,
timeout: Io.Timeout,
) struct { ?net.Socket.ReceiveTimeoutError, usize } {
if (!have_networking) return .{ error.NetworkDown, 0 };
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const t_io = io(t);
// recvmmsg is useless, here's why:
// * [timeout bug](https://bugzilla.kernel.org/show_bug.cgi?id=75371)
// * it wants iovecs for each message but we have a better API: one data
// buffer to handle all the messages. The better API cannot be lowered to
// the split vectors though because reducing the buffer size might make
// some messages unreceivable.
// So the strategy instead is to use non-blocking recvmsg calls, calling
// poll() with timeout if the first one returns EAGAIN.
const posix_flags: u32 =
@as(u32, if (flags.oob) posix.MSG.OOB else 0) |
@as(u32, if (flags.peek) posix.MSG.PEEK else 0) |
@as(u32, if (flags.trunc) posix.MSG.TRUNC else 0) |
posix.MSG.DONTWAIT | posix.MSG.NOSIGNAL;
var poll_fds: [1]posix.pollfd = .{
.{
.fd = handle,
.events = posix.POLL.IN,
.revents = undefined,
},
};
var message_i: usize = 0;
var data_i: usize = 0;
const deadline = timeout.toDeadline(t_io) catch |err| return .{ err, message_i };
recv: while (true) {
if (message_buffer.len - message_i == 0) return .{ null, message_i };
const message = &message_buffer[message_i];
const remaining_data_buffer = data_buffer[data_i..];
var storage: PosixAddress = undefined;
var iov: posix.iovec = .{ .base = remaining_data_buffer.ptr, .len = remaining_data_buffer.len };
var msg: posix.msghdr = .{
.name = &storage.any,
.namelen = @sizeOf(PosixAddress),
.iov = (&iov)[0..1],
.iovlen = 1,
.control = message.control.ptr,
.controllen = @intCast(message.control.len),
.flags = undefined,
};
current_thread.beginSyscall() catch |err| return .{ err, message_i };
const recv_rc = posix.system.recvmsg(handle, &msg, posix_flags);
current_thread.endSyscall();
switch (posix.errno(recv_rc)) {
.SUCCESS => {
const data = remaining_data_buffer[0..@intCast(recv_rc)];
data_i += data.len;
message.* = .{
.from = addressFromPosix(&storage),
.data = data,
.control = if (msg.control) |ptr| @as([*]u8, @ptrCast(ptr))[0..msg.controllen] else message.control,
.flags = .{
.eor = (msg.flags & posix.MSG.EOR) != 0,
.trunc = (msg.flags & posix.MSG.TRUNC) != 0,
.ctrunc = (msg.flags & posix.MSG.CTRUNC) != 0,
.oob = (msg.flags & posix.MSG.OOB) != 0,
.errqueue = if (@hasDecl(posix.MSG, "ERRQUEUE")) (msg.flags & posix.MSG.ERRQUEUE) != 0 else false,
},
};
message_i += 1;
continue;
},
.AGAIN => while (true) {
if (message_i != 0) return .{ null, message_i };
const max_poll_ms = std.math.maxInt(u31);
const timeout_ms: u31 = if (deadline) |d| t: {
const duration = d.durationFromNow(t_io) catch |err| return .{ err, message_i };
if (duration.raw.nanoseconds <= 0) return .{ error.Timeout, message_i };
break :t @intCast(@min(max_poll_ms, duration.raw.toMilliseconds()));
} else max_poll_ms;
current_thread.beginSyscall() catch |err| return .{ err, message_i };
const poll_rc = posix.system.poll(&poll_fds, poll_fds.len, timeout_ms);
current_thread.endSyscall();
switch (posix.errno(poll_rc)) {
.SUCCESS => {
if (poll_rc == 0) {
// Although spurious timeouts are OK, when no deadline
// is passed we must not return `error.Timeout`.
if (deadline == null) continue;
return .{ error.Timeout, message_i };
}
continue :recv;
},
.INTR => continue,
.CANCELED => return .{ current_thread.endSyscallCanceled(), message_i },
.FAULT => |err| return .{ errnoBug(err), message_i },
.INVAL => |err| return .{ errnoBug(err), message_i },
.NOMEM => return .{ error.SystemResources, message_i },
else => |err| return .{ posix.unexpectedErrno(err), message_i },
}
},
.INTR => continue,
.CANCELED => return .{ current_thread.endSyscallCanceled(), message_i },
.BADF => |err| return .{ errnoBug(err), message_i },
.NFILE => return .{ error.SystemFdQuotaExceeded, message_i },
.MFILE => return .{ error.ProcessFdQuotaExceeded, message_i },
.FAULT => |err| return .{ errnoBug(err), message_i },
.INVAL => |err| return .{ errnoBug(err), message_i },
.NOBUFS => return .{ error.SystemResources, message_i },
.NOMEM => return .{ error.SystemResources, message_i },
.NOTCONN => return .{ error.SocketUnconnected, message_i },
.NOTSOCK => |err| return .{ errnoBug(err), message_i },
.MSGSIZE => return .{ error.MessageOversize, message_i },
.PIPE => return .{ error.SocketUnconnected, message_i },
.OPNOTSUPP => |err| return .{ errnoBug(err), message_i },
.CONNRESET => return .{ error.ConnectionResetByPeer, message_i },
.NETDOWN => return .{ error.NetworkDown, message_i },
else => |err| return .{ posix.unexpectedErrno(err), message_i },
}
}
}
fn netReceiveWindows(
userdata: ?*anyopaque,
handle: net.Socket.Handle,
message_buffer: []net.IncomingMessage,
data_buffer: []u8,
flags: net.ReceiveFlags,
timeout: Io.Timeout,
) struct { ?net.Socket.ReceiveTimeoutError, usize } {
if (!have_networking) return .{ error.NetworkDown, 0 };
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
_ = handle;
_ = message_buffer;
_ = data_buffer;
_ = flags;
_ = timeout;
@panic("TODO implement netReceiveWindows");
}
fn netReceiveUnavailable(
userdata: ?*anyopaque,
handle: net.Socket.Handle,
message_buffer: []net.IncomingMessage,
data_buffer: []u8,
flags: net.ReceiveFlags,
timeout: Io.Timeout,
) struct { ?net.Socket.ReceiveTimeoutError, usize } {
_ = userdata;
_ = handle;
_ = message_buffer;
_ = data_buffer;
_ = flags;
_ = timeout;
return .{ error.NetworkDown, 0 };
}
fn netWritePosix(
userdata: ?*anyopaque,
fd: net.Socket.Handle,
header: []const u8,
data: []const []const u8,
splat: usize,
) net.Stream.Writer.Error!usize {
if (!have_networking) return error.NetworkDown;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
var iovecs: [max_iovecs_len]posix.iovec_const = undefined;
var msg: posix.msghdr_const = .{
.name = null,
.namelen = 0,
.iov = &iovecs,
.iovlen = 0,
.control = null,
.controllen = 0,
.flags = 0,
};
addBuf(&iovecs, &msg.iovlen, header);
for (data[0 .. data.len - 1]) |bytes| addBuf(&iovecs, &msg.iovlen, bytes);
const pattern = data[data.len - 1];
if (iovecs.len - msg.iovlen != 0) switch (splat) {
0 => {},
1 => addBuf(&iovecs, &msg.iovlen, pattern),
else => switch (pattern.len) {
0 => {},
1 => {
var backup_buffer: [splat_buffer_size]u8 = undefined;
const splat_buffer = &backup_buffer;
const memset_len = @min(splat_buffer.len, splat);
const buf = splat_buffer[0..memset_len];
@memset(buf, pattern[0]);
addBuf(&iovecs, &msg.iovlen, buf);
var remaining_splat = splat - buf.len;
while (remaining_splat > splat_buffer.len and iovecs.len - msg.iovlen != 0) {
assert(buf.len == splat_buffer.len);
addBuf(&iovecs, &msg.iovlen, splat_buffer);
remaining_splat -= splat_buffer.len;
}
addBuf(&iovecs, &msg.iovlen, splat_buffer[0..remaining_splat]);
},
else => for (0..@min(splat, iovecs.len - msg.iovlen)) |_| {
addBuf(&iovecs, &msg.iovlen, pattern);
},
},
};
const flags = posix.MSG.NOSIGNAL;
try current_thread.beginSyscall();
while (true) {
const rc = posix.system.sendmsg(fd, &msg, flags);
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
return @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.ACCES => |err| return errnoBug(err),
.AGAIN => |err| return errnoBug(err),
.ALREADY => return error.FastOpenAlreadyInProgress,
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.CONNRESET => return error.ConnectionResetByPeer,
.DESTADDRREQ => |err| return errnoBug(err), // The socket is not connection-mode, and no peer address is set.
.FAULT => |err| return errnoBug(err), // An invalid user space address was specified for an argument.
.INVAL => |err| return errnoBug(err), // Invalid argument passed.
.ISCONN => |err| return errnoBug(err), // connection-mode socket was connected already but a recipient was specified
.MSGSIZE => |err| return errnoBug(err),
.NOBUFS => return error.SystemResources,
.NOMEM => return error.SystemResources,
.NOTSOCK => |err| return errnoBug(err), // The file descriptor sockfd does not refer to a socket.
.OPNOTSUPP => |err| return errnoBug(err), // Some bit in the flags argument is inappropriate for the socket type.
.PIPE => return error.SocketUnconnected,
.AFNOSUPPORT => return error.AddressFamilyUnsupported,
.HOSTUNREACH => return error.HostUnreachable,
.NETUNREACH => return error.NetworkUnreachable,
.NOTCONN => return error.SocketUnconnected,
.NETDOWN => return error.NetworkDown,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn netWriteWindows(
userdata: ?*anyopaque,
handle: net.Socket.Handle,
header: []const u8,
data: []const []const u8,
splat: usize,
) net.Stream.Writer.Error!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
comptime assert(native_os == .windows);
var iovecs: [max_iovecs_len]ws2_32.WSABUF = undefined;
var len: u32 = 0;
addWsaBuf(&iovecs, &len, header);
for (data[0 .. data.len - 1]) |bytes| addWsaBuf(&iovecs, &len, bytes);
const pattern = data[data.len - 1];
if (iovecs.len - len != 0) switch (splat) {
0 => {},
1 => addWsaBuf(&iovecs, &len, pattern),
else => switch (pattern.len) {
0 => {},
1 => {
var backup_buffer: [64]u8 = undefined;
const splat_buffer = &backup_buffer;
const memset_len = @min(splat_buffer.len, splat);
const buf = splat_buffer[0..memset_len];
@memset(buf, pattern[0]);
addWsaBuf(&iovecs, &len, buf);
var remaining_splat = splat - buf.len;
while (remaining_splat > splat_buffer.len and len < iovecs.len) {
addWsaBuf(&iovecs, &len, splat_buffer);
remaining_splat -= splat_buffer.len;
}
addWsaBuf(&iovecs, &len, splat_buffer[0..remaining_splat]);
},
else => for (0..@min(splat, iovecs.len - len)) |_| {
addWsaBuf(&iovecs, &len, pattern);
},
},
};
while (true) {
try current_thread.checkCancel();
var n: u32 = undefined;
var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED);
const rc = ws2_32.WSASend(handle, &iovecs, len, &n, 0, &overlapped, null);
if (rc != ws2_32.SOCKET_ERROR) return n;
const wsa_error: ws2_32.WinsockError = switch (ws2_32.WSAGetLastError()) {
.IO_PENDING => e: {
var result_flags: u32 = undefined;
const overlapped_rc = ws2_32.WSAGetOverlappedResult(
handle,
&overlapped,
&n,
windows.TRUE,
&result_flags,
);
if (overlapped_rc == windows.FALSE) {
break :e ws2_32.WSAGetLastError();
} else {
return n;
}
},
else => |err| err,
};
switch (wsa_error) {
.EINTR => continue,
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return current_thread.endSyscallCanceled(),
.NOTINITIALISED => {
try initializeWsa(t);
continue;
},
.ECONNABORTED => return error.ConnectionResetByPeer,
.ECONNRESET => return error.ConnectionResetByPeer,
.EINVAL => return error.SocketUnconnected,
.ENETDOWN => return error.NetworkDown,
.ENETRESET => return error.ConnectionResetByPeer,
.ENOBUFS => return error.SystemResources,
.ENOTCONN => return error.SocketUnconnected,
.ENOTSOCK => |err| return wsaErrorBug(err),
.EOPNOTSUPP => |err| return wsaErrorBug(err),
.ESHUTDOWN => |err| return wsaErrorBug(err),
else => |err| return windows.unexpectedWSAError(err),
}
}
}
fn addWsaBuf(v: []ws2_32.WSABUF, i: *u32, bytes: []const u8) void {
const cap = std.math.maxInt(u32);
var remaining = bytes;
while (remaining.len > cap) {
if (v.len - i.* == 0) return;
v[i.*] = .{ .buf = @constCast(remaining.ptr), .len = cap };
i.* += 1;
remaining = remaining[cap..];
} else {
@branchHint(.likely);
if (v.len - i.* == 0) return;
v[i.*] = .{ .buf = @constCast(remaining.ptr), .len = @intCast(remaining.len) };
i.* += 1;
}
}
fn netWriteUnavailable(
userdata: ?*anyopaque,
handle: net.Socket.Handle,
header: []const u8,
data: []const []const u8,
splat: usize,
) net.Stream.Writer.Error!usize {
_ = userdata;
_ = handle;
_ = header;
_ = data;
_ = splat;
return error.NetworkDown;
}
fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void {
// OS checks ptr addr before length so zero length vectors must be omitted.
if (bytes.len == 0) return;
if (v.len - i.* == 0) return;
v[i.*] = .{ .base = bytes.ptr, .len = bytes.len };
i.* += 1;
}
fn netClose(userdata: ?*anyopaque, handle: net.Socket.Handle) void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
switch (native_os) {
.windows => closeSocketWindows(handle),
else => posix.close(handle),
}
}
fn netCloseUnavailable(userdata: ?*anyopaque, handle: net.Socket.Handle) void {
_ = userdata;
_ = handle;
unreachable; // How you gonna close something that was impossible to open?
}
fn netInterfaceNameResolve(
userdata: ?*anyopaque,
name: *const net.Interface.Name,
) net.Interface.Name.ResolveError!net.Interface {
if (!have_networking) return error.InterfaceNotFound;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (native_os == .linux) {
const sock_fd = openSocketPosix(current_thread, posix.AF.UNIX, .{ .mode = .dgram }) catch |err| switch (err) {
error.ProcessFdQuotaExceeded => return error.SystemResources,
error.SystemFdQuotaExceeded => return error.SystemResources,
error.AddressFamilyUnsupported => return error.Unexpected,
error.ProtocolUnsupportedBySystem => return error.Unexpected,
error.ProtocolUnsupportedByAddressFamily => return error.Unexpected,
error.SocketModeUnsupported => return error.Unexpected,
error.OptionUnsupported => return error.Unexpected,
else => |e| return e,
};
defer posix.close(sock_fd);
var ifr: posix.ifreq = .{
.ifrn = .{ .name = @bitCast(name.bytes) },
.ifru = undefined,
};
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(posix.system.ioctl(sock_fd, posix.SIOCGIFINDEX, @intFromPtr(&ifr)))) {
.SUCCESS => {
current_thread.endSyscall();
return .{ .index = @bitCast(ifr.ifru.ivalue) };
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err), // Bad parameters.
.NOTTY => |err| return errnoBug(err),
.NXIO => |err| return errnoBug(err),
.BADF => |err| return errnoBug(err), // File descriptor used after closed.
.FAULT => |err| return errnoBug(err), // Bad pointer parameter.
.IO => |err| return errnoBug(err), // sock_fd is not a file descriptor
.NODEV => return error.InterfaceNotFound,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
if (native_os == .windows) {
try current_thread.checkCancel();
@panic("TODO implement netInterfaceNameResolve for Windows");
}
if (builtin.link_libc) {
try current_thread.checkCancel();
const index = std.c.if_nametoindex(&name.bytes);
if (index == 0) return error.InterfaceNotFound;
return .{ .index = @bitCast(index) };
}
@panic("unimplemented");
}
fn netInterfaceNameResolveUnavailable(
userdata: ?*anyopaque,
name: *const net.Interface.Name,
) net.Interface.Name.ResolveError!net.Interface {
_ = userdata;
_ = name;
return error.InterfaceNotFound;
}
fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
try current_thread.checkCancel();
if (native_os == .linux) {
_ = interface;
@panic("TODO implement netInterfaceName for linux");
}
if (native_os == .windows) {
@panic("TODO implement netInterfaceName for windows");
}
if (builtin.link_libc) {
@panic("TODO implement netInterfaceName for libc");
}
@panic("unimplemented");
}
fn netInterfaceNameUnavailable(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name {
_ = userdata;
_ = interface;
return error.Unexpected;
}
fn netLookup(
userdata: ?*anyopaque,
host_name: HostName,
resolved: *Io.Queue(HostName.LookupResult),
options: HostName.LookupOptions,
) void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
const t_io = io(t);
resolved.putOneUncancelable(t_io, .{ .end = netLookupFallible(t, current_thread, host_name, resolved, options) });
}
fn netLookupUnavailable(
userdata: ?*anyopaque,
host_name: HostName,
resolved: *Io.Queue(HostName.LookupResult),
options: HostName.LookupOptions,
) void {
_ = host_name;
_ = options;
const t: *Threaded = @ptrCast(@alignCast(userdata));
const t_io = ioBasic(t);
resolved.putOneUncancelable(t_io, .{ .end = error.NetworkDown });
}
fn netLookupFallible(
t: *Threaded,
current_thread: *Thread,
host_name: HostName,
resolved: *Io.Queue(HostName.LookupResult),
options: HostName.LookupOptions,
) !void {
if (!have_networking) return error.NetworkDown;
const t_io = io(t);
const name = host_name.bytes;
assert(name.len <= HostName.max_len);
if (is_windows) {
var name_buffer: [HostName.max_len + 1]u16 = undefined;
const name_len = std.unicode.wtf8ToWtf16Le(&name_buffer, host_name.bytes) catch
unreachable; // HostName is prevalidated.
name_buffer[name_len] = 0;
const name_w = name_buffer[0..name_len :0];
var port_buffer: [8]u8 = undefined;
var port_buffer_wide: [8]u16 = undefined;
const port = std.fmt.bufPrint(&port_buffer, "{d}", .{options.port}) catch
unreachable; // `port_buffer` is big enough for decimal u16.
for (port, port_buffer_wide[0..port.len]) |byte, *wide|
wide.* = std.mem.nativeToLittle(u16, byte);
port_buffer_wide[port.len] = 0;
const port_w = port_buffer_wide[0..port.len :0];
const hints: ws2_32.ADDRINFOEXW = .{
.flags = .{ .NUMERICSERV = true },
.family = if (options.family) |f| switch (f) {
.ip4 => posix.AF.INET,
.ip6 => posix.AF.INET6,
} else posix.AF.UNSPEC,
.socktype = posix.SOCK.STREAM,
.protocol = posix.IPPROTO.TCP,
.canonname = null,
.addr = null,
.addrlen = 0,
.blob = null,
.bloblen = 0,
.provider = null,
.next = null,
};
const cancel_handle: ?*windows.HANDLE = null;
var res: *ws2_32.ADDRINFOEXW = undefined;
const timeout: ?*ws2_32.timeval = null;
while (true) {
try current_thread.checkCancel(); // TODO make requestCancel call GetAddrInfoExCancel
// TODO make this append to the queue eagerly rather than blocking until
// the whole thing finishes
const rc: ws2_32.WinsockError = @enumFromInt(ws2_32.GetAddrInfoExW(name_w, port_w, .DNS, null, &hints, &res, timeout, null, null, cancel_handle));
switch (rc) {
@as(ws2_32.WinsockError, @enumFromInt(0)) => break,
.EINTR => continue,
.ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled,
.NOTINITIALISED => {
try initializeWsa(t);
continue;
},
.TRY_AGAIN => return error.NameServerFailure,
.EINVAL => |err| return wsaErrorBug(err),
.NO_RECOVERY => return error.NameServerFailure,
.EAFNOSUPPORT => return error.AddressFamilyUnsupported,
.NOT_ENOUGH_MEMORY => return error.SystemResources,
.HOST_NOT_FOUND => return error.UnknownHostName,
.TYPE_NOT_FOUND => return error.ProtocolUnsupportedByAddressFamily,
.ESOCKTNOSUPPORT => return error.ProtocolUnsupportedBySystem,
else => |err| return windows.unexpectedWSAError(err),
}
}
defer ws2_32.FreeAddrInfoExW(res);
var it: ?*ws2_32.ADDRINFOEXW = res;
var canon_name: ?[*:0]const u16 = null;
while (it) |info| : (it = info.next) {
const addr = info.addr orelse continue;
const storage: WsaAddress = .{ .any = addr.* };
try resolved.putOne(t_io, .{ .address = addressFromWsa(&storage) });
if (info.canonname) |n| {
if (canon_name == null) {
canon_name = n;
}
}
}
if (canon_name) |n| {
const len = std.unicode.wtf16LeToWtf8(options.canonical_name_buffer, std.mem.sliceTo(n, 0));
try resolved.putOne(t_io, .{ .canonical_name = .{
.bytes = options.canonical_name_buffer[0..len],
} });
}
return;
}
// On Linux, glibc provides getaddrinfo_a which is capable of supporting our semantics.
// However, musl's POSIX-compliant getaddrinfo is not, so we bypass it.
if (builtin.target.isGnuLibC()) {
// TODO use getaddrinfo_a / gai_cancel
}
if (native_os == .linux) {
if (options.family != .ip4) {
if (IpAddress.parseIp6(name, options.port)) |addr| {
try resolved.putAll(t_io, &.{
.{ .address = addr },
.{ .canonical_name = copyCanon(options.canonical_name_buffer, name) },
});
return;
} else |_| {}
}
if (options.family != .ip6) {
if (IpAddress.parseIp4(name, options.port)) |addr| {
try resolved.putAll(t_io, &.{
.{ .address = addr },
.{ .canonical_name = copyCanon(options.canonical_name_buffer, name) },
});
return;
} else |_| {}
}
lookupHosts(t, host_name, resolved, options) catch |err| switch (err) {
error.UnknownHostName => {},
else => |e| return e,
};
// RFC 6761 Section 6.3.3
// Name resolution APIs and libraries SHOULD recognize
// localhost names as special and SHOULD always return the IP
// loopback address for address queries and negative responses
// for all other query types.
// Check for equal to "localhost(.)" or ends in ".localhost(.)"
const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost";
if (std.mem.endsWith(u8, name, localhost) and
(name.len == localhost.len or name[name.len - localhost.len] == '.'))
{
var results_buffer: [3]HostName.LookupResult = undefined;
var results_index: usize = 0;
if (options.family != .ip4) {
results_buffer[results_index] = .{ .address = .{ .ip6 = .loopback(options.port) } };
results_index += 1;
}
if (options.family != .ip6) {
results_buffer[results_index] = .{ .address = .{ .ip4 = .loopback(options.port) } };
results_index += 1;
}
const canon_name = "localhost";
const canon_name_dest = options.canonical_name_buffer[0..canon_name.len];
canon_name_dest.* = canon_name.*;
results_buffer[results_index] = .{ .canonical_name = .{ .bytes = canon_name_dest } };
results_index += 1;
try resolved.putAll(t_io, results_buffer[0..results_index]);
return;
}
return lookupDnsSearch(t, host_name, resolved, options);
}
if (native_os == .openbsd) {
// TODO use getaddrinfo_async / asr_abort
}
if (native_os == .freebsd) {
// TODO use dnsres_getaddrinfo
}
if (native_os.isDarwin()) {
// TODO use CFHostStartInfoResolution / CFHostCancelInfoResolution
}
if (builtin.link_libc) {
// This operating system lacks a way to resolve asynchronously. We are
// stuck with getaddrinfo.
var name_buffer: [HostName.max_len + 1]u8 = undefined;
@memcpy(name_buffer[0..host_name.bytes.len], host_name.bytes);
name_buffer[host_name.bytes.len] = 0;
const name_c = name_buffer[0..host_name.bytes.len :0];
var port_buffer: [8]u8 = undefined;
const port_c = std.fmt.bufPrintZ(&port_buffer, "{d}", .{options.port}) catch unreachable;
const hints: posix.addrinfo = .{
.flags = .{ .NUMERICSERV = true },
.family = posix.AF.UNSPEC,
.socktype = posix.SOCK.STREAM,
.protocol = posix.IPPROTO.TCP,
.canonname = null,
.addr = null,
.addrlen = 0,
.next = null,
};
var res: ?*posix.addrinfo = null;
try current_thread.beginSyscall();
while (true) {
switch (posix.system.getaddrinfo(name_c.ptr, port_c.ptr, &hints, &res)) {
@as(posix.system.EAI, @enumFromInt(0)) => {
current_thread.endSyscall();
break;
},
.SYSTEM => switch (posix.errno(-1)) {
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
return posix.unexpectedErrno(e);
},
},
else => |e| {
current_thread.endSyscall();
switch (e) {
.ADDRFAMILY => return error.AddressFamilyUnsupported,
.AGAIN => return error.NameServerFailure,
.FAIL => return error.NameServerFailure,
.FAMILY => return error.AddressFamilyUnsupported,
.MEMORY => return error.SystemResources,
.NODATA => return error.UnknownHostName,
.NONAME => return error.UnknownHostName,
else => return error.Unexpected,
}
},
}
}
defer if (res) |some| posix.system.freeaddrinfo(some);
var it = res;
var canon_name: ?[*:0]const u8 = null;
while (it) |info| : (it = info.next) {
const addr = info.addr orelse continue;
const storage: PosixAddress = .{ .any = addr.* };
try resolved.putOne(t_io, .{ .address = addressFromPosix(&storage) });
if (info.canonname) |n| {
if (canon_name == null) {
canon_name = n;
}
}
}
if (canon_name) |n| {
try resolved.putOne(t_io, .{
.canonical_name = copyCanon(options.canonical_name_buffer, std.mem.sliceTo(n, 0)),
});
}
return;
}
return error.OptionUnsupported;
}
pub const PosixAddress = extern union {
any: posix.sockaddr,
in: posix.sockaddr.in,
in6: posix.sockaddr.in6,
};
const UnixAddress = extern union {
any: posix.sockaddr,
un: posix.sockaddr.un,
};
const WsaAddress = extern union {
any: ws2_32.sockaddr,
in: ws2_32.sockaddr.in,
in6: ws2_32.sockaddr.in6,
un: ws2_32.sockaddr.un,
};
pub fn posixAddressFamily(a: *const IpAddress) posix.sa_family_t {
return switch (a.*) {
.ip4 => posix.AF.INET,
.ip6 => posix.AF.INET6,
};
}
pub fn addressFromPosix(posix_address: *const PosixAddress) IpAddress {
return switch (posix_address.any.family) {
posix.AF.INET => .{ .ip4 = address4FromPosix(&posix_address.in) },
posix.AF.INET6 => .{ .ip6 = address6FromPosix(&posix_address.in6) },
else => .{ .ip4 = .loopback(0) },
};
}
fn addressFromWsa(wsa_address: *const WsaAddress) IpAddress {
return switch (wsa_address.any.family) {
posix.AF.INET => .{ .ip4 = address4FromWsa(&wsa_address.in) },
posix.AF.INET6 => .{ .ip6 = address6FromWsa(&wsa_address.in6) },
else => .{ .ip4 = .loopback(0) },
};
}
pub fn addressToPosix(a: *const IpAddress, storage: *PosixAddress) posix.socklen_t {
return switch (a.*) {
.ip4 => |ip4| {
storage.in = address4ToPosix(ip4);
return @sizeOf(posix.sockaddr.in);
},
.ip6 => |*ip6| {
storage.in6 = address6ToPosix(ip6);
return @sizeOf(posix.sockaddr.in6);
},
};
}
fn addressToWsa(a: *const IpAddress, storage: *WsaAddress) i32 {
return switch (a.*) {
.ip4 => |ip4| {
storage.in = address4ToPosix(ip4);
return @sizeOf(posix.sockaddr.in);
},
.ip6 => |*ip6| {
storage.in6 = address6ToPosix(ip6);
return @sizeOf(posix.sockaddr.in6);
},
};
}
fn addressUnixToPosix(a: *const net.UnixAddress, storage: *UnixAddress) posix.socklen_t {
@memcpy(storage.un.path[0..a.path.len], a.path);
storage.un.family = posix.AF.UNIX;
storage.un.path[a.path.len] = 0;
return @sizeOf(posix.sockaddr.un);
}
fn addressUnixToWsa(a: *const net.UnixAddress, storage: *WsaAddress) i32 {
@memcpy(storage.un.path[0..a.path.len], a.path);
storage.un.family = posix.AF.UNIX;
storage.un.path[a.path.len] = 0;
return @sizeOf(posix.sockaddr.un);
}
fn address4FromPosix(in: *const posix.sockaddr.in) net.Ip4Address {
return .{
.port = std.mem.bigToNative(u16, in.port),
.bytes = @bitCast(in.addr),
};
}
fn address6FromPosix(in6: *const posix.sockaddr.in6) net.Ip6Address {
return .{
.port = std.mem.bigToNative(u16, in6.port),
.bytes = in6.addr,
.flow = in6.flowinfo,
.interface = .{ .index = in6.scope_id },
};
}
fn address4FromWsa(in: *const ws2_32.sockaddr.in) net.Ip4Address {
return .{
.port = std.mem.bigToNative(u16, in.port),
.bytes = @bitCast(in.addr),
};
}
fn address6FromWsa(in6: *const ws2_32.sockaddr.in6) net.Ip6Address {
return .{
.port = std.mem.bigToNative(u16, in6.port),
.bytes = in6.addr,
.flow = in6.flowinfo,
.interface = .{ .index = in6.scope_id },
};
}
fn address4ToPosix(a: net.Ip4Address) posix.sockaddr.in {
return .{
.port = std.mem.nativeToBig(u16, a.port),
.addr = @bitCast(a.bytes),
};
}
fn address6ToPosix(a: *const net.Ip6Address) posix.sockaddr.in6 {
return .{
.port = std.mem.nativeToBig(u16, a.port),
.flowinfo = a.flow,
.addr = a.bytes,
.scope_id = a.interface.index,
};
}
pub fn errnoBug(err: posix.E) Io.UnexpectedError {
if (is_debug) std.debug.panic("programmer bug caused syscall error: {t}", .{err});
return error.Unexpected;
}
fn wsaErrorBug(err: ws2_32.WinsockError) Io.UnexpectedError {
if (is_debug) std.debug.panic("programmer bug caused syscall error: {t}", .{err});
return error.Unexpected;
}
pub fn posixSocketMode(mode: net.Socket.Mode) u32 {
return switch (mode) {
.stream => posix.SOCK.STREAM,
.dgram => posix.SOCK.DGRAM,
.seqpacket => posix.SOCK.SEQPACKET,
.raw => posix.SOCK.RAW,
.rdm => posix.SOCK.RDM,
};
}
pub fn posixProtocol(protocol: ?net.Protocol) u32 {
return @intFromEnum(protocol orelse return 0);
}
fn recoverableOsBugDetected() void {
if (is_debug) unreachable;
}
fn clockToPosix(clock: Io.Clock) posix.clockid_t {
return switch (clock) {
.real => posix.CLOCK.REALTIME,
.awake => switch (native_os) {
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => posix.CLOCK.UPTIME_RAW,
else => posix.CLOCK.MONOTONIC,
},
.boot => switch (native_os) {
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => posix.CLOCK.MONOTONIC_RAW,
// On freebsd derivatives, use MONOTONIC_FAST as currently there's
// no precision tradeoff.
.freebsd, .dragonfly => posix.CLOCK.MONOTONIC_FAST,
// On linux, use BOOTTIME instead of MONOTONIC as it ticks while
// suspended.
.linux => posix.CLOCK.BOOTTIME,
// On other posix systems, MONOTONIC is generally the fastest and
// ticks while suspended.
else => posix.CLOCK.MONOTONIC,
},
.cpu_process => posix.CLOCK.PROCESS_CPUTIME_ID,
.cpu_thread => posix.CLOCK.THREAD_CPUTIME_ID,
};
}
fn clockToWasi(clock: Io.Clock) std.os.wasi.clockid_t {
return switch (clock) {
.real => .REALTIME,
.awake => .MONOTONIC,
.boot => .MONOTONIC,
.cpu_process => .PROCESS_CPUTIME_ID,
.cpu_thread => .THREAD_CPUTIME_ID,
};
}
fn statFromLinux(stx: *const std.os.linux.Statx) Io.File.Stat {
const atime = stx.atime;
const mtime = stx.mtime;
const ctime = stx.ctime;
return .{
.inode = stx.ino,
.size = stx.size,
.mode = stx.mode,
.kind = switch (stx.mode & std.os.linux.S.IFMT) {
std.os.linux.S.IFDIR => .directory,
std.os.linux.S.IFCHR => .character_device,
std.os.linux.S.IFBLK => .block_device,
std.os.linux.S.IFREG => .file,
std.os.linux.S.IFIFO => .named_pipe,
std.os.linux.S.IFLNK => .sym_link,
std.os.linux.S.IFSOCK => .unix_domain_socket,
else => .unknown,
},
.atime = .{ .nanoseconds = @intCast(@as(i128, atime.sec) * std.time.ns_per_s + atime.nsec) },
.mtime = .{ .nanoseconds = @intCast(@as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec) },
.ctime = .{ .nanoseconds = @intCast(@as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec) },
};
}
fn statFromPosix(st: *const posix.Stat) Io.File.Stat {
const atime = st.atime();
const mtime = st.mtime();
const ctime = st.ctime();
return .{
.inode = st.ino,
.size = @bitCast(st.size),
.mode = st.mode,
.kind = k: {
const m = st.mode & posix.S.IFMT;
switch (m) {
posix.S.IFBLK => break :k .block_device,
posix.S.IFCHR => break :k .character_device,
posix.S.IFDIR => break :k .directory,
posix.S.IFIFO => break :k .named_pipe,
posix.S.IFLNK => break :k .sym_link,
posix.S.IFREG => break :k .file,
posix.S.IFSOCK => break :k .unix_domain_socket,
else => {},
}
if (native_os == .illumos) switch (m) {
posix.S.IFDOOR => break :k .door,
posix.S.IFPORT => break :k .event_port,
else => {},
};
break :k .unknown;
},
.atime = timestampFromPosix(&atime),
.mtime = timestampFromPosix(&mtime),
.ctime = timestampFromPosix(&ctime),
};
}
fn statFromWasi(st: *const std.os.wasi.filestat_t) Io.File.Stat {
return .{
.inode = st.ino,
.size = @bitCast(st.size),
.mode = 0,
.kind = switch (st.filetype) {
.BLOCK_DEVICE => .block_device,
.CHARACTER_DEVICE => .character_device,
.DIRECTORY => .directory,
.SYMBOLIC_LINK => .sym_link,
.REGULAR_FILE => .file,
.SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket,
else => .unknown,
},
.atime = .fromNanoseconds(st.atim),
.mtime = .fromNanoseconds(st.mtim),
.ctime = .fromNanoseconds(st.ctim),
};
}
fn timestampFromPosix(timespec: *const posix.timespec) Io.Timestamp {
return .{ .nanoseconds = @intCast(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec) };
}
fn timestampToPosix(nanoseconds: i96) posix.timespec {
return .{
.sec = @intCast(@divFloor(nanoseconds, std.time.ns_per_s)),
.nsec = @intCast(@mod(nanoseconds, std.time.ns_per_s)),
};
}
fn pathToPosix(file_path: []const u8, buffer: *[posix.PATH_MAX]u8) Io.Dir.PathNameError![:0]u8 {
if (std.mem.containsAtLeastScalar2(u8, file_path, 0, 1)) return error.BadPathName;
// >= rather than > to make room for the null byte
if (file_path.len >= buffer.len) return error.NameTooLong;
@memcpy(buffer[0..file_path.len], file_path);
buffer[file_path.len] = 0;
return buffer[0..file_path.len :0];
}
fn lookupDnsSearch(
t: *Threaded,
host_name: HostName,
resolved: *Io.Queue(HostName.LookupResult),
options: HostName.LookupOptions,
) HostName.LookupError!void {
const t_io = io(t);
const rc = HostName.ResolvConf.init(t_io) catch return error.ResolvConfParseFailed;
// Count dots, suppress search when >=ndots or name ends in
// a dot, which is an explicit request for global scope.
const dots = std.mem.countScalar(u8, host_name.bytes, '.');
const search_len = if (dots >= rc.ndots or std.mem.endsWith(u8, host_name.bytes, ".")) 0 else rc.search_len;
const search = rc.search_buffer[0..search_len];
var canon_name = host_name.bytes;
// Strip final dot for canon, fail if multiple trailing dots.
if (std.mem.endsWith(u8, canon_name, ".")) canon_name.len -= 1;
if (std.mem.endsWith(u8, canon_name, ".")) return error.UnknownHostName;
// Name with search domain appended is set up in `canon_name`. This
// both provides the desired default canonical name (if the requested
// name is not a CNAME record) and serves as a buffer for passing the
// full requested name to `lookupDns`.
@memcpy(options.canonical_name_buffer[0..canon_name.len], canon_name);
options.canonical_name_buffer[canon_name.len] = '.';
var it = std.mem.tokenizeAny(u8, search, " \t");
while (it.next()) |token| {
@memcpy(options.canonical_name_buffer[canon_name.len + 1 ..][0..token.len], token);
const lookup_canon_name = options.canonical_name_buffer[0 .. canon_name.len + 1 + token.len];
if (lookupDns(t, lookup_canon_name, &rc, resolved, options)) |result| {
return result;
} else |err| switch (err) {
error.UnknownHostName => continue,
else => |e| return e,
}
}
const lookup_canon_name = options.canonical_name_buffer[0..canon_name.len];
return lookupDns(t, lookup_canon_name, &rc, resolved, options);
}
fn lookupDns(
t: *Threaded,
lookup_canon_name: []const u8,
rc: *const HostName.ResolvConf,
resolved: *Io.Queue(HostName.LookupResult),
options: HostName.LookupOptions,
) HostName.LookupError!void {
const t_io = io(t);
const family_records: [2]struct { af: IpAddress.Family, rr: HostName.DnsRecord } = .{
.{ .af = .ip6, .rr = .A },
.{ .af = .ip4, .rr = .AAAA },
};
var query_buffers: [2][280]u8 = undefined;
var answer_buffer: [2 * 512]u8 = undefined;
var queries_buffer: [2][]const u8 = undefined;
var answers_buffer: [2][]const u8 = undefined;
var nq: usize = 0;
var answer_buffer_i: usize = 0;
for (family_records) |fr| {
if (options.family != fr.af) {
const entropy = std.crypto.random.array(u8, 2);
const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr, entropy);
queries_buffer[nq] = query_buffers[nq][0..len];
nq += 1;
}
}
var ip4_mapped_buffer: [HostName.ResolvConf.max_nameservers]IpAddress = undefined;
const ip4_mapped = ip4_mapped_buffer[0..rc.nameservers_len];
var any_ip6 = false;
for (rc.nameservers(), ip4_mapped) |*ns, *m| {
m.* = .{ .ip6 = .fromAny(ns.*) };
any_ip6 = any_ip6 or ns.* == .ip6;
}
var socket = s: {
if (any_ip6) ip6: {
const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) };
const socket = ip6_addr.bind(t_io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) {
error.AddressFamilyUnsupported => break :ip6,
else => |e| return e,
};
break :s socket;
}
any_ip6 = false;
const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) };
const socket = try ip4_addr.bind(t_io, .{ .mode = .dgram });
break :s socket;
};
defer socket.close(t_io);
const mapped_nameservers = if (any_ip6) ip4_mapped else rc.nameservers();
const queries = queries_buffer[0..nq];
const answers = answers_buffer[0..queries.len];
var answers_remaining = answers.len;
for (answers) |*answer| answer.len = 0;
// boot clock is chosen because time the computer is suspended should count
// against time spent waiting for external messages to arrive.
const clock: Io.Clock = .boot;
var now_ts = try clock.now(t_io);
const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds));
const attempt_duration: Io.Duration = .{
.nanoseconds = (std.time.ns_per_s / rc.attempts) * @as(i96, rc.timeout_seconds),
};
send: while (now_ts.nanoseconds < final_ts.nanoseconds) : (now_ts = try clock.now(t_io)) {
const max_messages = queries_buffer.len * HostName.ResolvConf.max_nameservers;
{
var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined;
var message_i: usize = 0;
for (queries, answers) |query, *answer| {
if (answer.len != 0) continue;
for (mapped_nameservers) |*ns| {
message_buffer[message_i] = .{
.address = ns,
.data_ptr = query.ptr,
.data_len = query.len,
};
message_i += 1;
}
}
_ = netSendPosix(t, socket.handle, message_buffer[0..message_i], .{});
}
const timeout: Io.Timeout = .{ .deadline = .{
.raw = now_ts.addDuration(attempt_duration),
.clock = clock,
} };
while (true) {
var message_buffer: [max_messages]Io.net.IncomingMessage = @splat(.init);
const buf = answer_buffer[answer_buffer_i..];
const recv_err, const recv_n = socket.receiveManyTimeout(t_io, &message_buffer, buf, .{}, timeout);
for (message_buffer[0..recv_n]) |*received_message| {
const reply = received_message.data;
// Ignore non-identifiable packets.
if (reply.len < 4) continue;
// Ignore replies from addresses we didn't send to.
const ns = for (mapped_nameservers) |*ns| {
if (received_message.from.eql(ns)) break ns;
} else {
continue;
};
// Find which query this answer goes with, if any.
const query, const answer = for (queries, answers) |query, *answer| {
if (reply[0] == query[0] and reply[1] == query[1]) break .{ query, answer };
} else {
continue;
};
if (answer.len != 0) continue;
// Only accept positive or negative responses; retry immediately on
// server failure, and ignore all other codes such as refusal.
switch (reply[3] & 15) {
0, 3 => {
answer.* = reply;
answer_buffer_i += reply.len;
answers_remaining -= 1;
if (answer_buffer.len - answer_buffer_i == 0) break :send;
if (answers_remaining == 0) break :send;
},
2 => {
var retry_message: Io.net.OutgoingMessage = .{
.address = ns,
.data_ptr = query.ptr,
.data_len = query.len,
};
_ = netSendPosix(t, socket.handle, (&retry_message)[0..1], .{});
continue;
},
else => continue,
}
}
if (recv_err) |err| switch (err) {
error.Canceled => return error.Canceled,
error.Timeout => continue :send,
else => continue,
};
}
} else {
return error.NameServerFailure;
}
var addresses_len: usize = 0;
var canonical_name: ?HostName = null;
for (answers) |answer| {
var it = HostName.DnsResponse.init(answer) catch {
// Here we could potentially add diagnostics to the results queue.
continue;
};
while (it.next() catch {
// Here we could potentially add diagnostics to the results queue.
continue;
}) |record| switch (record.rr) {
.A => {
const data = record.packet[record.data_off..][0..record.data_len];
if (data.len != 4) return error.InvalidDnsARecord;
try resolved.putOne(t_io, .{ .address = .{ .ip4 = .{
.bytes = data[0..4].*,
.port = options.port,
} } });
addresses_len += 1;
},
.AAAA => {
const data = record.packet[record.data_off..][0..record.data_len];
if (data.len != 16) return error.InvalidDnsAAAARecord;
try resolved.putOne(t_io, .{ .address = .{ .ip6 = .{
.bytes = data[0..16].*,
.port = options.port,
} } });
addresses_len += 1;
},
.CNAME => {
_, canonical_name = HostName.expand(record.packet, record.data_off, options.canonical_name_buffer) catch
return error.InvalidDnsCnameRecord;
},
_ => continue,
};
}
try resolved.putOne(t_io, .{ .canonical_name = canonical_name orelse .{ .bytes = lookup_canon_name } });
if (addresses_len == 0) return error.NameServerFailure;
}
fn lookupHosts(
t: *Threaded,
host_name: HostName,
resolved: *Io.Queue(HostName.LookupResult),
options: HostName.LookupOptions,
) !void {
const t_io = io(t);
const file = Io.File.openAbsolute(t_io, "/etc/hosts", .{}) catch |err| switch (err) {
error.FileNotFound,
error.NotDir,
error.AccessDenied,
=> return error.UnknownHostName,
error.Canceled => |e| return e,
else => {
// Here we could add more detailed diagnostics to the results queue.
return error.DetectingNetworkConfigurationFailed;
},
};
defer file.close(t_io);
var line_buf: [512]u8 = undefined;
var file_reader = file.reader(t_io, &line_buf);
return lookupHostsReader(t, host_name, resolved, options, &file_reader.interface) catch |err| switch (err) {
error.ReadFailed => switch (file_reader.err.?) {
error.Canceled => |e| return e,
else => {
// Here we could add more detailed diagnostics to the results queue.
return error.DetectingNetworkConfigurationFailed;
},
},
error.Canceled => |e| return e,
error.UnknownHostName => |e| return e,
};
}
fn lookupHostsReader(
t: *Threaded,
host_name: HostName,
resolved: *Io.Queue(HostName.LookupResult),
options: HostName.LookupOptions,
reader: *Io.Reader,
) error{ ReadFailed, Canceled, UnknownHostName }!void {
const t_io = io(t);
var addresses_len: usize = 0;
var canonical_name: ?HostName = null;
while (true) {
const line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) {
error.StreamTooLong => {
// Skip lines that are too long.
_ = reader.discardDelimiterInclusive('\n') catch |e| switch (e) {
error.EndOfStream => break,
error.ReadFailed => return error.ReadFailed,
};
continue;
},
error.ReadFailed => return error.ReadFailed,
error.EndOfStream => break,
};
reader.toss(1);
var split_it = std.mem.splitScalar(u8, line, '#');
const no_comment_line = split_it.first();
var line_it = std.mem.tokenizeAny(u8, no_comment_line, " \t");
const ip_text = line_it.next() orelse continue;
var first_name_text: ?[]const u8 = null;
while (line_it.next()) |name_text| {
if (std.mem.eql(u8, name_text, host_name.bytes)) {
if (first_name_text == null) first_name_text = name_text;
break;
}
} else continue;
if (canonical_name == null) {
if (HostName.init(first_name_text.?)) |name_text| {
if (name_text.bytes.len <= options.canonical_name_buffer.len) {
const canonical_name_dest = options.canonical_name_buffer[0..name_text.bytes.len];
@memcpy(canonical_name_dest, name_text.bytes);
canonical_name = .{ .bytes = canonical_name_dest };
}
} else |_| {}
}
if (options.family != .ip6) {
if (IpAddress.parseIp4(ip_text, options.port)) |addr| {
try resolved.putOne(t_io, .{ .address = addr });
addresses_len += 1;
} else |_| {}
}
if (options.family != .ip4) {
if (IpAddress.parseIp6(ip_text, options.port)) |addr| {
try resolved.putOne(t_io, .{ .address = addr });
addresses_len += 1;
} else |_| {}
}
}
if (canonical_name) |canon_name| try resolved.putOne(t_io, .{ .canonical_name = canon_name });
if (addresses_len == 0) return error.UnknownHostName;
}
/// Writes DNS resolution query packet data to `w`; at most 280 bytes.
fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: HostName.DnsRecord, entropy: [2]u8) usize {
// This implementation is ported from musl libc.
// A more idiomatic "ziggy" implementation would be welcome.
var name = dname;
if (std.mem.endsWith(u8, name, ".")) name.len -= 1;
assert(name.len <= 253);
const n = 17 + name.len + @intFromBool(name.len != 0);
// Construct query template - ID will be filled later
q[0..2].* = entropy;
@memset(q[2..n], 0);
q[2] = @as(u8, op) * 8 + 1;
q[5] = 1;
@memcpy(q[13..][0..name.len], name);
var i: usize = 13;
var j: usize = undefined;
while (q[i] != 0) : (i = j + 1) {
j = i;
while (q[j] != 0 and q[j] != '.') : (j += 1) {}
// TODO determine the circumstances for this and whether or
// not this should be an error.
if (j - i - 1 > 62) unreachable;
q[i - 1] = @intCast(j - i);
}
q[i + 1] = @intFromEnum(ty);
q[i + 3] = class;
return n;
}
fn copyCanon(canonical_name_buffer: *[HostName.max_len]u8, name: []const u8) HostName {
const dest = canonical_name_buffer[0..name.len];
@memcpy(dest, name);
return .{ .bytes = dest };
}
/// Darwin XNU 7195.50.7.100.1 introduced __ulock_wait2 and migrated code paths (notably pthread_cond_t) towards it:
/// https://github.com/apple/darwin-xnu/commit/d4061fb0260b3ed486147341b72468f836ed6c8f#diff-08f993cc40af475663274687b7c326cc6c3031e0db3ac8de7b24624610616be6
///
/// This XNU version appears to correspond to 11.0.1:
/// https://kernelshaman.blogspot.com/2021/01/building-xnu-for-macos-big-sur-1101.html
///
/// ulock_wait() uses 32-bit micro-second timeouts where 0 = INFINITE or no-timeout
/// ulock_wait2() uses 64-bit nano-second timeouts (with the same convention)
const darwin_supports_ulock_wait2 = builtin.os.version_range.semver.min.major >= 11;
fn futexWait(current_thread: *Thread, ptr: *const std.atomic.Value(u32), expect: u32) Io.Cancelable!void {
@branchHint(.cold);
if (builtin.cpu.arch.isWasm()) {
comptime assert(builtin.cpu.has(.wasm, .atomics));
try current_thread.checkCancel();
const timeout: i64 = -1;
const signed_expect: i32 = @bitCast(expect);
const result = asm volatile (
\\local.get %[ptr]
\\local.get %[expected]
\\local.get %[timeout]
\\memory.atomic.wait32 0
\\local.set %[ret]
: [ret] "=r" (-> u32),
: [ptr] "r" (&ptr.raw),
[expected] "r" (signed_expect),
[timeout] "r" (timeout),
);
switch (result) {
0 => {}, // ok
1 => {}, // expected != loaded
2 => assert(!is_debug), // timeout
else => assert(!is_debug),
}
} else switch (native_os) {
.linux => {
const linux = std.os.linux;
try current_thread.beginSyscall();
const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null);
current_thread.endSyscall();
switch (linux.errno(rc)) {
.SUCCESS => {}, // notified by `wake()`
.INTR => {}, // caller's responsibility to retry
.AGAIN => {}, // ptr.* != expect
.INVAL => {}, // possibly timeout overflow
.TIMEDOUT => recoverableOsBugDetected(),
.FAULT => recoverableOsBugDetected(), // ptr was invalid
else => recoverableOsBugDetected(),
}
},
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
const c = std.c;
const flags: c.UL = .{
.op = .COMPARE_AND_WAIT,
.NO_ERRNO = true,
};
try current_thread.beginSyscall();
const status = if (darwin_supports_ulock_wait2)
c.__ulock_wait2(flags, ptr, expect, 0, 0)
else
c.__ulock_wait(flags, ptr, expect, 0);
current_thread.endSyscall();
if (status >= 0) return;
if (is_debug) switch (@as(c.E, @enumFromInt(-status))) {
.INTR => {}, // spurious wake
// Address of the futex was paged out. This is unlikely, but possible in theory, and
// pthread/libdispatch on darwin bother to handle it. In this case we'll return
// without waiting, but the caller should retry anyway.
.FAULT => {},
.TIMEDOUT => unreachable,
else => unreachable,
};
},
.windows => {
try current_thread.checkCancel();
switch (windows.ntdll.RtlWaitOnAddress(ptr, &expect, @sizeOf(@TypeOf(expect)), null)) {
.SUCCESS => {},
.CANCELLED => return error.Canceled,
else => recoverableOsBugDetected(),
}
},
.freebsd => {
const flags = @intFromEnum(std.c.UMTX_OP.WAIT_UINT_PRIVATE);
try current_thread.beginSyscall();
const rc = std.c._umtx_op(@intFromPtr(&ptr.raw), flags, @as(c_ulong, expect), 0, 0);
current_thread.endSyscall();
if (is_debug) switch (posix.errno(rc)) {
.SUCCESS => {},
.FAULT => unreachable, // one of the args points to invalid memory
.INVAL => unreachable, // arguments should be correct
.TIMEDOUT => unreachable, // no timeout provided
.INTR => {}, // spurious wake
else => unreachable,
};
},
else => @compileError("unimplemented: futexWait"),
}
}
pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) void {
@branchHint(.cold);
if (builtin.cpu.arch.isWasm()) {
comptime assert(builtin.cpu.has(.wasm, .atomics));
const timeout: i64 = -1;
const signed_expect: i32 = @bitCast(expect);
const result = asm volatile (
\\local.get %[ptr]
\\local.get %[expected]
\\local.get %[timeout]
\\memory.atomic.wait32 0
\\local.set %[ret]
: [ret] "=r" (-> u32),
: [ptr] "r" (&ptr.raw),
[expected] "r" (signed_expect),
[timeout] "r" (timeout),
);
switch (result) {
0 => {}, // ok
1 => {}, // expected != loaded
2 => recoverableOsBugDetected(), // timeout
else => recoverableOsBugDetected(),
}
} else switch (native_os) {
.linux => {
const linux = std.os.linux;
const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null);
switch (linux.errno(rc)) {
.SUCCESS => {}, // notified by `wake()`
.INTR => {}, // caller's responsibility to repeat
.AGAIN => {}, // ptr.* != expect
.INVAL => {}, // possibly timeout overflow
.TIMEDOUT => recoverableOsBugDetected(),
.FAULT => recoverableOsBugDetected(), // ptr was invalid
else => recoverableOsBugDetected(),
}
},
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => {
const c = std.c;
const flags: c.UL = .{
.op = .COMPARE_AND_WAIT,
.NO_ERRNO = true,
};
const status = if (darwin_supports_ulock_wait2)
c.__ulock_wait2(flags, ptr, expect, 0, 0)
else
c.__ulock_wait(flags, ptr, expect, 0);
if (status >= 0) return;
switch (@as(c.E, @enumFromInt(-status))) {
// Wait was interrupted by the OS or other spurious signalling.
.INTR => {},
// Address of the futex was paged out. This is unlikely, but possible in theory, and
// pthread/libdispatch on darwin bother to handle it. In this case we'll return
// without waiting, but the caller should retry anyway.
.FAULT => {},
.TIMEDOUT => recoverableOsBugDetected(),
else => recoverableOsBugDetected(),
}
},
.windows => {
switch (windows.ntdll.RtlWaitOnAddress(ptr, &expect, @sizeOf(@TypeOf(expect)), null)) {
.SUCCESS, .CANCELLED => {},
else => recoverableOsBugDetected(),
}
},
.freebsd => {
const flags = @intFromEnum(std.c.UMTX_OP.WAIT_UINT_PRIVATE);
const rc = std.c._umtx_op(@intFromPtr(&ptr.raw), flags, @as(c_ulong, expect), 0, 0);
switch (posix.errno(rc)) {
.SUCCESS => {},
.INTR => {}, // spurious wake
.FAULT => recoverableOsBugDetected(), // one of the args points to invalid memory
.INVAL => recoverableOsBugDetected(), // arguments should be correct
.TIMEDOUT => recoverableOsBugDetected(), // no timeout provided
else => recoverableOsBugDetected(),
}
},
else => @compileError("unimplemented: futexWaitUncancelable"),
}
}
pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void {
@branchHint(.cold);
if (builtin.cpu.arch.isWasm()) {
comptime assert(builtin.cpu.has(.wasm, .atomics));
assert(max_waiters != 0);
const woken_count = asm volatile (
\\local.get %[ptr]
\\local.get %[waiters]
\\memory.atomic.notify 0
\\local.set %[ret]
: [ret] "=r" (-> u32),
: [ptr] "r" (&ptr.raw),
[waiters] "r" (max_waiters),
);
_ = woken_count; // can be 0 when linker flag 'shared-memory' is not enabled
} else switch (native_os) {
.linux => {
const linux = std.os.linux;
switch (linux.errno(linux.futex_3arg(
&ptr.raw,
.{ .cmd = .WAKE, .private = true },
@min(max_waiters, std.math.maxInt(i32)),
))) {
.SUCCESS => return, // successful wake up
.INVAL => return, // invalid futex_wait() on ptr done elsewhere
.FAULT => return, // pointer became invalid while doing the wake
else => return recoverableOsBugDetected(), // deadlock due to operating system bug
}
},
.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()
.FAULT => unreachable, // __ulock_wake doesn't generate EFAULT according to darwin pthread_cond_t
.NOENT => return, // nothing was woken up
.ALREADY => unreachable, // only for UL.Op.WAKE_THREAD
else => unreachable, // deadlock due to operating system bug
}
}
},
.windows => {
assert(max_waiters != 0);
switch (max_waiters) {
1 => windows.ntdll.RtlWakeAddressSingle(ptr),
else => windows.ntdll.RtlWakeAddressAll(ptr),
}
},
.freebsd => {
const rc = std.c._umtx_op(
@intFromPtr(&ptr.raw),
@intFromEnum(std.c.UMTX_OP.WAKE_PRIVATE),
@as(c_ulong, max_waiters),
0, // there is no timeout struct
0, // there is no timeout struct pointer
);
switch (posix.errno(rc)) {
.SUCCESS => {},
.FAULT => {}, // it's ok if the ptr doesn't point to valid memory
.INVAL => unreachable, // arguments should be correct
else => unreachable, // deadlock due to operating system bug
}
},
else => @compileError("unimplemented: futexWake"),
}
}
/// A thread-safe logical boolean value which can be `set` and `unset`.
///
/// It can also block threads until the value is set with cancelation via timed
/// waits. Statically initializable; four bytes on all targets.
pub const ResetEvent = switch (native_os) {
.illumos, .netbsd => ResetEventPosix,
else => ResetEventFutex,
};
/// A `ResetEvent` implementation based on futexes.
const ResetEventFutex = enum(u32) {
unset = 0,
waiting = 1,
is_set = 2,
/// Returns whether the logical boolean is `set`.
///
/// Once `reset` is called, this returns false until the next `set`.
///
/// The memory accesses before the `set` can be said to happen before
/// `isSet` returns true.
pub fn isSet(ref: *const ResetEventFutex) bool {
if (builtin.single_threaded) return switch (ref.*) {
.unset => false,
.waiting => unreachable,
.is_set => true,
};
// Acquire barrier ensures memory accesses before `set` happen before
// returning true.
return @atomicLoad(ResetEventFutex, ref, .acquire) == .is_set;
}
/// Blocks the calling thread until `set` is called.
///
/// This is effectively a more efficient version of `while (!isSet()) {}`.
///
/// The memory accesses before the `set` can be said to happen before `wait` returns.
pub fn wait(ref: *ResetEventFutex, t: *Threaded) Io.Cancelable!void {
if (builtin.single_threaded) switch (ref.*) {
.unset => unreachable, // Deadlock, no other threads to wake us up.
.waiting => unreachable, // Invalid state.
.is_set => return,
};
// Try to set the state from `unset` to `waiting` to indicate to the
// `set` thread that others are blocked on the ResetEventFutex. Avoid using
// any strict barriers until we know the ResetEventFutex is set.
var state = @atomicLoad(ResetEventFutex, ref, .acquire);
if (state == .is_set) {
@branchHint(.likely);
return;
}
if (state == .unset) {
state = @cmpxchgStrong(ResetEventFutex, ref, state, .waiting, .acquire, .acquire) orelse .waiting;
}
const current_thread = Thread.getCurrent(t);
while (state == .waiting) {
try futexWait(current_thread, @ptrCast(ref), @intFromEnum(ResetEventFutex.waiting));
state = @atomicLoad(ResetEventFutex, ref, .acquire);
}
assert(state == .is_set);
}
/// Same as `wait` except uninterruptible.
pub fn waitUncancelable(ref: *ResetEventFutex) void {
if (builtin.single_threaded) switch (ref.*) {
.unset => unreachable, // Deadlock, no other threads to wake us up.
.waiting => unreachable, // Invalid state.
.is_set => return,
};
// Try to set the state from `unset` to `waiting` to indicate to the
// `set` thread that others are blocked on the ResetEventFutex. Avoid using
// any strict barriers until we know the ResetEventFutex is set.
var state = @atomicLoad(ResetEventFutex, ref, .acquire);
if (state == .is_set) {
@branchHint(.likely);
return;
}
if (state == .unset) {
state = @cmpxchgStrong(ResetEventFutex, ref, state, .waiting, .acquire, .acquire) orelse .waiting;
}
while (state == .waiting) {
futexWaitUncancelable(@ptrCast(ref), @intFromEnum(ResetEventFutex.waiting));
state = @atomicLoad(ResetEventFutex, ref, .acquire);
}
assert(state == .is_set);
}
/// Marks the logical boolean as `set` and unblocks any threads in `wait`
/// or `timedWait` to observe the new state.
///
/// The logical boolean stays `set` until `reset` is called, making future
/// `set` calls do nothing semantically.
///
/// The memory accesses before `set` can be said to happen before `isSet`
/// returns true or `wait`/`timedWait` return successfully.
pub fn set(ref: *ResetEventFutex) void {
if (builtin.single_threaded) {
ref.* = .is_set;
return;
}
if (@atomicRmw(ResetEventFutex, ref, .Xchg, .is_set, .release) == .waiting) {
futexWake(@ptrCast(ref), std.math.maxInt(u32));
}
}
/// Unmarks the ResetEventFutex as if `set` was never called.
///
/// Assumes no threads are blocked in `wait` or `timedWait`. Concurrent
/// calls to `set`, `isSet` and `reset` are allowed.
pub fn reset(ref: *ResetEventFutex) void {
if (builtin.single_threaded) {
ref.* = .unset;
return;
}
@atomicStore(ResetEventFutex, ref, .unset, .monotonic);
}
};
/// A `ResetEvent` implementation based on pthreads API.
const ResetEventPosix = struct {
cond: std.c.pthread_cond_t,
mutex: std.c.pthread_mutex_t,
state: ResetEventFutex,
pub const unset: ResetEventPosix = .{
.cond = std.c.PTHREAD_COND_INITIALIZER,
.mutex = std.c.PTHREAD_MUTEX_INITIALIZER,
.state = .unset,
};
pub fn isSet(rep: *const ResetEventPosix) bool {
if (builtin.single_threaded) return switch (rep.state) {
.unset => false,
.waiting => unreachable,
.is_set => true,
};
return @atomicLoad(ResetEventFutex, &rep.state, .acquire) == .is_set;
}
pub fn wait(rep: *ResetEventPosix, t: *Threaded) Io.Cancelable!void {
if (builtin.single_threaded) switch (rep.*) {
.unset => unreachable, // Deadlock, no other threads to wake us up.
.waiting => unreachable, // Invalid state.
.is_set => return,
};
const current_thread = Thread.getCurrent(t);
assert(std.c.pthread_mutex_lock(&rep.mutex) == .SUCCESS);
defer assert(std.c.pthread_mutex_unlock(&rep.mutex) == .SUCCESS);
sw: switch (rep.state) {
.unset => {
rep.state = .waiting;
continue :sw .waiting;
},
.waiting => {
try current_thread.beginSyscall();
assert(std.c.pthread_cond_wait(&rep.cond, &rep.mutex) == .SUCCESS);
current_thread.endSyscall();
continue :sw rep.state;
},
.is_set => return,
}
}
pub fn waitUncancelable(rep: *ResetEventPosix) void {
if (builtin.single_threaded) switch (rep.*) {
.unset => unreachable, // Deadlock, no other threads to wake us up.
.waiting => unreachable, // Invalid state.
.is_set => return,
};
assert(std.c.pthread_mutex_lock(&rep.mutex) == .SUCCESS);
defer assert(std.c.pthread_mutex_unlock(&rep.mutex) == .SUCCESS);
sw: switch (rep.state) {
.unset => {
rep.state = .waiting;
continue :sw .waiting;
},
.waiting => {
assert(std.c.pthread_cond_wait(&rep.cond, &rep.mutex) == .SUCCESS);
continue :sw rep.state;
},
.is_set => return,
}
}
pub fn set(rep: *ResetEventPosix) void {
if (builtin.single_threaded) {
rep.* = .is_set;
return;
}
if (@atomicRmw(ResetEventFutex, &rep.state, .Xchg, .is_set, .release) == .waiting) {
assert(std.c.pthread_cond_broadcast(&rep.cond) == .SUCCESS);
}
}
pub fn reset(rep: *ResetEventPosix) void {
if (builtin.single_threaded) {
rep.* = .unset;
return;
}
@atomicStore(ResetEventFutex, &rep.state, .unset, .monotonic);
}
};
fn closeSocketWindows(s: ws2_32.SOCKET) void {
const rc = ws2_32.closesocket(s);
if (is_debug) switch (rc) {
0 => {},
ws2_32.SOCKET_ERROR => switch (ws2_32.WSAGetLastError()) {
else => recoverableOsBugDetected(),
},
else => recoverableOsBugDetected(),
};
}
const Wsa = struct {
status: Status = .uninitialized,
mutex: Io.Mutex = .init,
init_error: ?Wsa.InitError = null,
const Status = enum { uninitialized, initialized, failure };
const InitError = error{
ProcessFdQuotaExceeded,
NetworkDown,
VersionUnsupported,
BlockingOperationInProgress,
} || Io.UnexpectedError;
};
fn initializeWsa(t: *Threaded) error{ NetworkDown, Canceled }!void {
const t_io = io(t);
const wsa = &t.wsa;
try wsa.mutex.lock(t_io);
defer wsa.mutex.unlock(t_io);
switch (wsa.status) {
.uninitialized => {
var wsa_data: ws2_32.WSADATA = undefined;
const minor_version = 2;
const major_version = 2;
switch (ws2_32.WSAStartup((@as(windows.WORD, minor_version) << 8) | major_version, &wsa_data)) {
0 => {
wsa.status = .initialized;
return;
},
else => |err_int| {
wsa.status = .failure;
wsa.init_error = switch (@as(ws2_32.WinsockError, @enumFromInt(@as(u16, @intCast(err_int))))) {
.SYSNOTREADY => error.NetworkDown,
.VERNOTSUPPORTED => error.VersionUnsupported,
.EINPROGRESS => error.BlockingOperationInProgress,
.EPROCLIM => error.ProcessFdQuotaExceeded,
else => |err| windows.unexpectedWSAError(err),
};
},
}
},
.initialized => return,
.failure => {},
}
return error.NetworkDown;
}
fn doNothingSignalHandler(_: posix.SIG) callconv(.c) void {}
test {
_ = @import("Threaded/test.zig");
}