From cc913e9158d2fe065d1bc247799140f8c28752c2 Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Wed, 22 Apr 2026 06:28:50 -0600 Subject: [PATCH] 0.16 support --- .github/workflows/build.yml | 7 +- build.zig | 47 +- build.zig.zon | 11 +- examples/FrameTimeGraph.zig | 10 +- examples/dbe.zig | 72 +- examples/draw.zig | 41 +- examples/fontviewer.zig | 39 +- examples/getserverfontnames.zig | 41 +- examples/graphics.zig | 41 +- examples/hello.zig | 41 +- examples/input.zig | 39 +- examples/keys.zig | 39 +- examples/present.zig | 44 +- examples/queryfont.zig | 61 +- examples/runall.zig | 55 +- examples/testexample.zig | 27 +- examples/text.zig | 81 +- examples/transparent.zig | 39 +- src/SocketReader.zig | 107 +++ src/SocketWriter.zig | 81 ++ src/x.zig | 470 +++++++---- src/x/draft.zig | 23 +- src/xauth.zig | 81 +- std16/build.zig | 7 + std16/build.zig.zon | 12 + std16/src/Io.zig | 1072 ++++++++++++++++++++++++ std16/src/Io/Dir.zig | 37 + std16/src/Io/File.zig | 56 ++ std16/src/Io/net.zig | 1350 +++++++++++++++++++++++++++++++ std16/src/process.zig | 4 + std16/src/process/Args.zig | 1 + std16/src/process/Environ.zig | 14 + std16/src/std.zig | 20 + 33 files changed, 3632 insertions(+), 438 deletions(-) create mode 100644 src/SocketReader.zig create mode 100644 src/SocketWriter.zig create mode 100644 std16/build.zig create mode 100644 std16/build.zig.zon create mode 100644 std16/src/Io.zig create mode 100644 std16/src/Io/Dir.zig create mode 100644 std16/src/Io/File.zig create mode 100644 std16/src/Io/net.zig create mode 100644 std16/src/process.zig create mode 100644 std16/src/process/Args.zig create mode 100644 std16/src/process/Environ.zig create mode 100644 std16/src/std.zig diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08c2110..52983ba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,5 +27,8 @@ jobs: - name: Setup Zig uses: marler8997/setup-anyzig@master - - name: Build - run: zig build --summary all install test-non-interactive + - name: Build 0.15.2 + run: zig 0.15.2 build --summary all install test-non-interactive + + - name: Build 0.16.0 + run: zig 0.16.0 build --summary all install test-non-interactive diff --git a/build.zig b/build.zig index 45523bf..de24af3 100644 --- a/build.zig +++ b/build.zig @@ -43,7 +43,6 @@ pub fn build(b: *std.Build) void { const zigversion_mod = b.createModule(.{ .root_source_file = if (zig_atleast_16) b.path("zigversion/atleast16.zig") else b.path("zigversion/before16.zig"), }); - // In almost all cases, Zig programs should only use this module, not the // library defined below, that's for C programs. const x_mod = b.addModule("x11", .{ @@ -52,6 +51,11 @@ pub fn build(b: *std.Build) void { .{ .name = "zigversion", .module = zigversion_mod }, }, }); + if (!zig_atleast_16) { + if (b.lazyDependency("std16", .{})) |std16_dep| { + x_mod.addImport("std16", std16_dep.module("std16")); + } + } const true_type_mod = b.dependency("TrueType", .{}).module("TrueType"); const xtt_mod = b.addModule("xtt", .{ @@ -71,6 +75,11 @@ pub fn build(b: *std.Build) void { .single_threaded = true, }), }); + if (!zig_atleast_16) { + if (b.lazyDependency("std16", .{})) |std16_dep| { + examples_exe.root_module.addImport("std16", std16_dep.module("std16")); + } + } const run_examples = b.addRunArtifact(examples_exe); const test_step = b.step("test", "Run all tests and interactive examples)"); @@ -89,6 +98,16 @@ pub fn build(b: *std.Build) void { .{ .name = "x11", .module = x_mod }, }, }); + if (!zig_atleast_16) { + if (b.lazyDependency("std16", .{})) |std16_dep| { + example_mod.addImport("std16", std16_dep.module("std16")); + } + } else { + if (b.lazyDependency("io", .{})) |io_dep| { + example_mod.addImport("Threaded", io_dep.module("Threaded")); + } + } + if (example.needs_text) { example_mod.addImport("xtt", xtt_mod); @@ -103,6 +122,13 @@ pub fn build(b: *std.Build) void { .root_module = example_mod, }); + const enabled = switch (target.result.os.tag) { + // dbe example not working on 0.16 windows because it uses poll which seems + // to have broken on 0.16? + .windows => !std.mem.eql(u8, example.name, "dbe"), + else => true, + }; + const exe_check = b.addExecutable(.{ .name = b.fmt("{s}_check", .{example.name}), .root_module = example_mod, @@ -111,11 +137,13 @@ pub fn build(b: *std.Build) void { const install = b.addInstallArtifact(exe, .{}); build_examples_step.dependOn(&install.step); - b.getInstallStep().dependOn(&install.step); + if (enabled) b.getInstallStep().dependOn(&install.step); b.step("build-" ++ example.name, "").dependOn(&install.step); - run_examples.addArtifactArg(exe); - run_examples.step.dependOn(&install.step); + if (enabled) { + run_examples.addArtifactArg(exe); + run_examples.step.dependOn(&install.step); + } const run = b.addRunArtifact(exe); run.step.dependOn(&install.step); @@ -191,6 +219,12 @@ pub fn build(b: *std.Build) void { .{ .name = "zigversion", .module = zigversion_mod }, }, }); + if (!zig_atleast_16) { + if (b.lazyDependency("std16", .{})) |std16_dep| { + x_mod_with_target.addImport("std16", std16_dep.module("std16")); + } + } + const unit_tests = b.addTest(.{ .root_module = x_mod_with_target, }); @@ -211,6 +245,11 @@ pub fn build(b: *std.Build) void { }, }), }); + if (!zig_atleast_16) { + if (b.lazyDependency("std16", .{})) |std16_dep| { + xauth_exe.root_module.addImport("std16", std16_dep.module("std16")); + } + } const install = b.addInstallArtifact(xauth_exe, .{}); b.step("install-xauth", "").dependOn(&install.step); test_non_interactive.dependOn(&install.step); diff --git a/build.zig.zon b/build.zig.zon index 2166e78..09c9d7f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,9 +2,18 @@ .name = .x11, .version = "0.0.0", .fingerprint = 0x3220e772e6b06bc9, - .zig_version = "0.15.2", + .zig_version = "0.16.0", .minimum_zig_version = "0.15.2", .dependencies = .{ + .std16 = .{ + .path = "std16", + .lazy = true, + }, + .io = .{ + .url = "git+https://github.com/marler8997/zig-io#d9e63d8b17fb7567ff8c973d6dbea7e8391a465c", + .hash = "io-0.0.0-1IkbBbPlCwCDRE0r-SUWVvR4hUrcA5Rmln2KiKfQqXPN", + .lazy = true, + }, .TrueType = .{ .url = "git+https://codeberg.org/andrewrk/TrueType#b0f9867671a14ce4ed75a05a7e52c927623095e5", .hash = "TrueType-0.0.0-Ne-mWMxxAQDit1oHfxlK2lkZAX8P7Wsa2SBPaR_xSpc8", diff --git a/examples/FrameTimeGraph.zig b/examples/FrameTimeGraph.zig index 1a0c2e4..0d6cfec 100644 --- a/examples/FrameTimeGraph.zig +++ b/examples/FrameTimeGraph.zig @@ -7,7 +7,7 @@ const highlight_color = 0xcccccc; const over_color = 0xcc3333; font_dims: FontDims, -previous_time: ?std.time.Instant = null, +previous_time: ?std16.Io.Timestamp = null, frame_times: [history_len]f32 = [1]f32{0} ** history_len, cursor: u8 = 0, max_ms: f32 = 50, @@ -20,15 +20,16 @@ pub const FontDims = struct { pub fn writeRender( self: *FrameTimeGraph, + io: std16.Io, sink: *x11.RequestSink, drawable: x11.Drawable, gc_id: x11.GraphicsContext, window_width: u16, window_height: u16, ) error{ WriteFailed, TextTooLong }!void { - const now = std.time.Instant.now() catch @panic("time not supported"); + const now = std16.Io.Timestamp.now(io, .awake); const elapsed_ms: f32 = if (self.previous_time) |prev| - @as(f32, @floatFromInt(now.since(prev))) / std.time.ns_per_ms + @as(f32, @floatFromInt(prev.durationTo(now).toMilliseconds())) else 0; self.previous_time = now; @@ -129,5 +130,8 @@ pub fn writeRender( self.cursor = (self.cursor + 1) % history_len; } +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); diff --git a/examples/dbe.zig b/examples/dbe.zig index 25b0575..eb9f5fe 100644 --- a/examples/dbe.zig +++ b/examples/dbe.zig @@ -6,6 +6,9 @@ const std = @import("std"); const x11 = @import("x11"); +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; +const std16 = if (zig_atleast_16) std else @import("std16"); + const initial_window_width = 400; const initial_window_height = 400; @@ -43,22 +46,30 @@ const Root = struct { depth: x11.Depth, }; -pub fn main() !void { +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init.Minimal) !void { + var t: @import("Threaded") = .init_single_threaded; + try mainCompat(init.environ, t.io()); +} +fn mainBefore16() !void { + try mainCompat(.{}, .legacy); +} +pub fn mainCompat(environ: std16.process.Environ, io: std16.Io) !void { try x11.wsaStartup(); - const stream: std.net.Stream, const ids: Ids, const keyrange: x11.KeycodeRange, const root: Root = blk: { + const socket: x11.Socket, const ids: Ids, const keyrange: x11.KeycodeRange, const root: Root = blk: { var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - errdefer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + errdefer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; std.log.info("setup reply {f}", .{setup}); - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); const screen = (x11.draft.readSetupDynamic(&source, &setup, .{}) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }) orelse { std.log.err("no screen?", .{}); @@ -70,7 +81,7 @@ pub fn main() !void { std.process.exit(0xff); } break :blk .{ - socket_reader.getStream(), + socket_reader.socket, .{ .range = id_range }, try .init(setup.min_keycode, setup.max_keycode), .{ @@ -83,26 +94,27 @@ pub fn main() !void { }, }; }; - defer x11.disconnect(stream); + defer x11.disconnect(io, socket); var write_buffer: [1000]u8 = undefined; var read_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(stream, &write_buffer); - var socket_reader = x11.socketReader(stream, &read_buffer); + var socket_writer = x11.socketWriter(io, socket, &write_buffer); + var socket_reader = x11.socketReader(io, socket, &read_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; - var source: x11.Source = .initAfterSetup(socket_reader.interface()); - run(ids, &root, &sink, &socket_reader, &source, keyrange) catch |err| switch (err) { + var source: x11.Source = .initAfterSetup(&socket_reader.interface); + run(io, ids, &root, &sink, &socket_reader, &source, keyrange) catch |err| switch (err) { error.WriteFailed => |e| return x11.onWriteError(e, socket_writer.err.?), - error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.getError()), + error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.err), error.UnexpectedMessage => |e| return e, }; } fn run( + io: std16.Io, ids: Ids, root: *const Root, sink: *x11.RequestSink, - socket_reader: *std.net.Stream.Reader, + socket_reader: *x11.SocketReader, source: *x11.Source, keyrange: x11.KeycodeRange, ) error{ WriteFailed, ReadFailed, EndOfStream, Protocol, UnexpectedMessage }!void { @@ -166,7 +178,7 @@ fn run( var window_width: u16 = initial_window_width; var window_height: u16 = initial_window_height; - var animate: Animate = .{ .previous_time = std.time.Instant.now() catch @panic("monotonic timer unsupported") }; + var animate: Animate = .{ .previous_time = std16.Io.Timestamp.now(io, .awake) }; var animate_frame_ms: i32 = 15; while (true) { @@ -174,7 +186,7 @@ fn run( const action: enum { timeout, socket } = switch (pollSocketReader(socket_reader, 0)) { .ready => .socket, - .timeout => if (getTimeout(animate.previous_time, animate_frame_ms)) |timeout_ms| switch (pollSocketReader(socket_reader, timeout_ms)) { + .timeout => if (getTimeout(io, animate.previous_time, animate_frame_ms)) |timeout_ms| switch (pollSocketReader(socket_reader, timeout_ms)) { .ready => .socket, .timeout => .timeout, } else .timeout, @@ -183,6 +195,7 @@ fn run( switch (action) { .timeout => { try render( + io, sink, ids.window(), ids.gc(), @@ -241,6 +254,7 @@ fn run( } if (do_render) { try render( + io, sink, ids.window(), ids.gc(), @@ -259,6 +273,7 @@ fn run( const expose = try source.read2(.Expose); std.log.info("{}", .{expose}); try render( + io, sink, ids.window(), ids.gc(), @@ -289,11 +304,11 @@ fn run( } } -fn pollSocketReader(socket_reader: *std.net.Stream.Reader, timeout_ms: i32) enum { ready, timeout } { - if (socket_reader.interface().bufferedLen() > 0) return .ready; +fn pollSocketReader(socket_reader: *x11.SocketReader, timeout_ms: i32) enum { ready, timeout } { + if (socket_reader.interface.bufferedLen() > 0) return .ready; var poll_fds = [_]std.posix.pollfd{ .{ - .fd = socket_reader.getStream().handle, + .fd = socket_reader.socket, .events = std.posix.POLL.IN, .revents = 0, }, @@ -306,15 +321,15 @@ fn pollSocketReader(socket_reader: *std.net.Stream.Reader, timeout_ms: i32) enum }; } -pub fn getTimeout(start: std.time.Instant, duration_ms: i32) ?u31 { - const now = std.time.Instant.now() catch @panic("monotonic timer unsupported"); - const since_ms = @divTrunc(now.since(start), std.time.ns_per_ms); +pub fn getTimeout(io: std16.Io, start: std16.Io.Timestamp, duration_ms: i32) ?u31 { + const now = std16.Io.Timestamp.now(io, .awake); + const since_ms = start.durationTo(now).toMilliseconds(); if (since_ms >= duration_ms) return null; return @intCast(duration_ms - @as(i32, @intCast(since_ms))); } const Animate = struct { - previous_time: std.time.Instant, + previous_time: std16.Io.Timestamp, progress: f32 = 0, }; @@ -336,6 +351,7 @@ const Dbe = union(enum) { }; fn render( + io: std16.Io, sink: *x11.RequestSink, window: x11.Window, gc_id: x11.GraphicsContext, @@ -346,14 +362,14 @@ fn render( window_height: u16, ) !void { const elapsed_ms = blk: { - const now = std.time.Instant.now() catch @panic("monotonic timer unsupported"); - const elapsed_ms = now.since(animate.previous_time); + const now = std16.Io.Timestamp.now(io, .awake); + const elapsed_ms = animate.previous_time.durationTo(now).toMilliseconds(); animate.previous_time = now; break :blk elapsed_ms; }; const animation_duration_ms: f32 = 2000.0; // 2 seconds for full cycle - const elapsed_ms_f32: f32 = @floatFromInt(elapsed_ms / std.time.ns_per_ms); + const elapsed_ms_f32: f32 = @floatFromInt(elapsed_ms); const progress_increment: f32 = elapsed_ms_f32 / animation_duration_ms; animate.progress = @mod(animate.progress + progress_increment, 1.0); diff --git a/examples/draw.zig b/examples/draw.zig index 15af91d..d9fedd7 100644 --- a/examples/draw.zig +++ b/examples/draw.zig @@ -24,22 +24,30 @@ const Root = struct { depth: x11.Depth, }; -pub fn main() !void { +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init.Minimal) !void { + var t: @import("Threaded") = .init_single_threaded; + try mainCompat(init.environ, t.io()); +} +fn mainBefore16() !void { + try mainCompat(.{}, .legacy); +} +pub fn mainCompat(environ: std16.process.Environ, io: std16.Io) !void { try x11.wsaStartup(); - const stream: std.net.Stream, const ids: Ids, const root: Root = blk: { + const socket: x11.Socket, const ids: Ids, const root: Root = blk: { var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - errdefer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + errdefer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; std.log.info("setup reply {f}", .{setup}); - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); const screen = (x11.draft.readSetupDynamic(&source, &setup, .{}) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }) orelse { std.log.err("no screen?", .{}); @@ -51,7 +59,8 @@ pub fn main() !void { std.process.exit(0xff); } break :blk .{ - socket_reader.getStream(), .{ .range = id_range }, .{ + socket_reader.socket, .{ .range = id_range }, + .{ .window = screen.root, .visual = screen.root_visual, .depth = x11.Depth.init(screen.root_depth) orelse std.debug.panic( @@ -61,17 +70,17 @@ pub fn main() !void { }, }; }; - defer x11.disconnect(stream); + defer x11.disconnect(io, socket); var write_buffer: [1000]u8 = undefined; var read_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(stream, &write_buffer); - var socket_reader = x11.socketReader(stream, &read_buffer); + var socket_writer = x11.socketWriter(io, socket, &write_buffer); + var socket_reader = x11.socketReader(io, socket, &read_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; - var source: x11.Source = .initAfterSetup(socket_reader.interface()); + var source: x11.Source = .initAfterSetup(&socket_reader.interface); run(ids, &root, &sink, &source) catch |err| switch (err) { error.WriteFailed => |e| return x11.onWriteError(e, socket_writer.err.?), - error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.getError()), + error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.err), error.UnexpectedMessage => |e| return e, }; } @@ -431,6 +440,10 @@ fn oom(e: error{OutOfMemory}) noreturn { @panic(@errorName(e)); } +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); + const XY = x11.XY; diff --git a/examples/fontviewer.zig b/examples/fontviewer.zig index 9388b5e..650720b 100644 --- a/examples/fontviewer.zig +++ b/examples/fontviewer.zig @@ -1,6 +1,9 @@ const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + pub const log_level = std.log.Level.info; var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); @@ -30,22 +33,30 @@ const Root = struct { depth: x11.Depth, }; -pub fn main() !void { +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init.Minimal) !void { + var t: @import("Threaded") = .init_single_threaded; + try mainCompat(init.environ, t.io()); +} +fn mainBefore16() !void { + try mainCompat(.{}, .legacy); +} +pub fn mainCompat(environ: std16.process.Environ, io: std16.Io) !void { try x11.wsaStartup(); - const stream: std.net.Stream, const ids: Ids, const keyrange: x11.KeycodeRange, const root: Root = blk: { + const socket: x11.Socket, const ids: Ids, const keyrange: x11.KeycodeRange, const root: Root = blk: { var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - errdefer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + errdefer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; std.log.info("setup reply {f}", .{setup}); - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); const screen = (x11.draft.readSetupDynamic(&source, &setup, .{}) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }) orelse { std.log.err("no screen?", .{}); @@ -57,7 +68,7 @@ pub fn main() !void { std.process.exit(0xff); } break :blk .{ - socket_reader.getStream(), + socket_reader.socket, .{ .range = id_range }, try .init(setup.min_keycode, setup.max_keycode), .{ @@ -70,17 +81,17 @@ pub fn main() !void { }, }; }; - defer x11.disconnect(stream); + defer x11.disconnect(io, socket); var write_buffer: [1000]u8 = undefined; var read_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(stream, &write_buffer); - var socket_reader = x11.socketReader(stream, &read_buffer); + var socket_writer = x11.socketWriter(io, socket, &write_buffer); + var socket_reader = x11.socketReader(io, socket, &read_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; - var source: x11.Source = .initAfterSetup(socket_reader.interface()); + var source: x11.Source = .initAfterSetup(&socket_reader.interface); run(ids, &root, &sink, &source, keyrange) catch |err| switch (err) { error.WriteFailed => |e| return x11.onWriteError(e, socket_writer.err.?), - error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.getError()), + error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.err), error.UnexpectedMessage => |e| return e, }; } diff --git a/examples/getserverfontnames.zig b/examples/getserverfontnames.zig index 0d3fc74..b9b86dc 100644 --- a/examples/getserverfontnames.zig +++ b/examples/getserverfontnames.zig @@ -1,40 +1,51 @@ const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + pub const log_level = std.log.Level.info; -pub fn main() !void { +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init.Minimal) !void { + var t: @import("Threaded") = .init_single_threaded; + try mainCompat(init.environ, t.io()); +} +fn mainBefore16() !void { + try mainCompat(.{}, .legacy); +} +pub fn mainCompat(environ: std16.process.Environ, io: std16.Io) !void { try x11.wsaStartup(); - const stream = blk: { + const socket = blk: { var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - errdefer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + errdefer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; std.log.info("setup reply {f}", .{setup}); - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); const screen = (x11.draft.readSetupDynamic(&source, &setup, .{}) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }) orelse { std.log.err("no screen?", .{}); std.process.exit(0xff); }; _ = screen; - break :blk socket_reader.getStream(); + break :blk socket_reader.socket; }; - defer x11.disconnect(stream); + defer x11.disconnect(io, socket); var write_buffer: [1000]u8 = undefined; var read_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(stream, &write_buffer); - var socket_reader = x11.socketReader(stream, &read_buffer); + var socket_writer = x11.socketWriter(io, socket, &write_buffer); + var socket_reader = x11.socketReader(io, socket, &read_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; - var source: x11.Source = .initAfterSetup(socket_reader.interface()); + var source: x11.Source = .initAfterSetup(&socket_reader.interface); if (@as(?error{WriteFailed}, blk: { sink.ListFonts(0xffff, .initComptime("*")) catch |e| break :blk e; @@ -43,10 +54,10 @@ pub fn main() !void { })) |e| return x11.onWriteError(e, socket_writer.err.?); var stdout_buffer: [1000]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdout_writer = std16.Io.File.stdout().writer(io, &stdout_buffer); streamFonts(&source, sink.sequence, &stdout_writer.interface) catch |err| switch (err) { error.WriteFailed => return stdout_writer.err.?, - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol, error.UnexpectedMessage => |e| return e, }; } diff --git a/examples/graphics.zig b/examples/graphics.zig index dc1fdb7..e8e7117 100644 --- a/examples/graphics.zig +++ b/examples/graphics.zig @@ -1,6 +1,9 @@ const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); const allocator = arena.allocator(); @@ -27,22 +30,30 @@ const Root = struct { depth: x11.Depth, }; -pub fn main() !void { +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init.Minimal) !void { + var t: @import("Threaded") = .init_single_threaded; + try mainCompat(init.environ, t.io()); +} +fn mainBefore16() !void { + try mainCompat(.{}, .legacy); +} +pub fn mainCompat(environ: std16.process.Environ, io: std16.Io) !void { try x11.wsaStartup(); - const stream: std.net.Stream, const ids: Ids, const root: Root = blk: { + const socket: x11.Socket, const ids: Ids, const root: Root = blk: { var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - errdefer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + errdefer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; std.log.info("setup reply {f}", .{setup}); - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); const screen = (x11.draft.readSetupDynamic(&source, &setup, .{}) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }) orelse { std.log.err("no screen?", .{}); @@ -54,7 +65,9 @@ pub fn main() !void { std.process.exit(0xff); } break :blk .{ - socket_reader.getStream(), .{ .range = id_range }, .{ + socket_reader.socket, + .{ .range = id_range }, + .{ .window = screen.root, .visual = screen.root_visual, .depth = x11.Depth.init(screen.root_depth) orelse std.debug.panic( @@ -64,17 +77,17 @@ pub fn main() !void { }, }; }; - defer x11.disconnect(stream); + defer x11.disconnect(io, socket); var write_buffer: [1000]u8 = undefined; var read_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(stream, &write_buffer); - var socket_reader = x11.socketReader(stream, &read_buffer); + var socket_writer = x11.socketWriter(io, socket, &write_buffer); + var socket_reader = x11.socketReader(io, socket, &read_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; - var source: x11.Source = .initAfterSetup(socket_reader.interface()); + var source: x11.Source = .initAfterSetup(&socket_reader.interface); run(ids, &root, &sink, &source) catch |err| switch (err) { error.WriteFailed => |e| return x11.onWriteError(e, socket_writer.err.?), - error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.getError()), + error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.err), error.UnexpectedMessage => |e| return e, }; } diff --git a/examples/hello.zig b/examples/hello.zig index 8ef31f4..d9e7a4b 100644 --- a/examples/hello.zig +++ b/examples/hello.zig @@ -1,6 +1,9 @@ const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + const window_width = 400; const window_height = 400; @@ -23,22 +26,31 @@ const Root = struct { visual: x11.Visual, depth: x11.Depth, }; -pub fn main() !void { + +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init.Minimal) !void { + var t: @import("Threaded") = .init_single_threaded; + try mainCompat(init.environ, t.io()); +} +fn mainBefore16() !void { + try mainCompat(.{}, .legacy); +} +pub fn mainCompat(environ: std16.process.Environ, io: std16.Io) !void { try x11.wsaStartup(); - const stream: std.net.Stream, const ids: Ids, const root: Root = blk: { + const socket: x11.Socket, const ids: Ids, const root: Root = blk: { var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - errdefer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + errdefer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; std.log.info("setup reply {f}", .{setup}); - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); const screen = (x11.draft.readSetupDynamic(&source, &setup, .{}) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }) orelse { std.log.err("no screen?", .{}); @@ -50,7 +62,8 @@ pub fn main() !void { std.process.exit(0xff); } break :blk .{ - socket_reader.getStream(), .{ .range = id_range }, .{ + socket_reader.socket, .{ .range = id_range }, + .{ .window = screen.root, .visual = screen.root_visual, .depth = x11.Depth.init(screen.root_depth) orelse std.debug.panic( @@ -60,17 +73,17 @@ pub fn main() !void { }, }; }; - defer x11.disconnect(stream); + defer x11.disconnect(io, socket); var write_buffer: [1000]u8 = undefined; var read_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(stream, &write_buffer); - var socket_reader = x11.socketReader(stream, &read_buffer); + var socket_writer = x11.socketWriter(io, socket, &write_buffer); + var socket_reader = x11.socketReader(io, socket, &read_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; - var source: x11.Source = .initAfterSetup(socket_reader.interface()); + var source: x11.Source = .initAfterSetup(&socket_reader.interface); run(ids, &root, &sink, &source) catch |err| switch (err) { error.WriteFailed => |e| return x11.onWriteError(e, socket_writer.err.?), - error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.getError()), + error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.err), error.UnexpectedMessage => |e| return e, }; } diff --git a/examples/input.zig b/examples/input.zig index 3281f59..e031260 100644 --- a/examples/input.zig +++ b/examples/input.zig @@ -1,6 +1,9 @@ const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + const window_width = 400; const window_height = 400; @@ -46,22 +49,30 @@ const Root = struct { depth: x11.Depth, }; -pub fn main() !void { +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init.Minimal) !void { + var t: @import("Threaded") = .init_single_threaded; + try mainCompat(init.environ, t.io()); +} +fn mainBefore16() !void { + try mainCompat(.{}, .legacy); +} +pub fn mainCompat(environ: std16.process.Environ, io: std16.Io) !void { try x11.wsaStartup(); - const stream: std.net.Stream, const ids: Ids, const keyrange: x11.KeycodeRange, const root: Root = blk: { + const socket: x11.Socket, const ids: Ids, const keyrange: x11.KeycodeRange, const root: Root = blk: { var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - errdefer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + errdefer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; std.log.info("setup reply {f}", .{setup}); - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); const screen = (x11.draft.readSetupDynamic(&source, &setup, .{}) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }) orelse { std.log.err("no screen?", .{}); @@ -73,7 +84,7 @@ pub fn main() !void { std.process.exit(0xff); } break :blk .{ - socket_reader.getStream(), + socket_reader.socket, .{ .range = id_range }, try .init(setup.min_keycode, setup.max_keycode), .{ @@ -86,17 +97,17 @@ pub fn main() !void { }, }; }; - defer x11.disconnect(stream); + defer x11.disconnect(io, socket); var write_buffer: [1000]u8 = undefined; var read_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(stream, &write_buffer); - var socket_reader = x11.socketReader(stream, &read_buffer); + var socket_writer = x11.socketWriter(io, socket, &write_buffer); + var socket_reader = x11.socketReader(io, socket, &read_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; - var source: x11.Source = .initAfterSetup(socket_reader.interface()); + var source: x11.Source = .initAfterSetup(&socket_reader.interface); run(ids, &root, &sink, &source, keyrange) catch |err| switch (err) { error.WriteFailed => |e| return x11.onWriteError(e, socket_writer.err.?), - error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.getError()), + error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.err), error.UnexpectedMessage => |e| return e, }; } diff --git a/examples/keys.zig b/examples/keys.zig index 84458ff..41213b2 100644 --- a/examples/keys.zig +++ b/examples/keys.zig @@ -27,22 +27,30 @@ const Root = struct { depth: x11.Depth, }; -pub fn main() !void { +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init.Minimal) !void { + var t: @import("Threaded") = .init_single_threaded; + try mainCompat(init.environ, t.io()); +} +fn mainBefore16() !void { + try mainCompat(.{}, .legacy); +} +pub fn mainCompat(environ: std16.process.Environ, io: std16.Io) !void { try x11.wsaStartup(); - const stream: std.net.Stream, const ids: Ids, const keyrange: x11.KeycodeRange, const root: Root = blk: { + const socket: x11.Socket, const ids: Ids, const keyrange: x11.KeycodeRange, const root: Root = blk: { var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - errdefer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + errdefer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; std.log.info("setup reply {f}", .{setup}); - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); const screen = (x11.draft.readSetupDynamic(&source, &setup, .{}) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }) orelse { std.log.err("no screen?", .{}); @@ -54,7 +62,7 @@ pub fn main() !void { std.process.exit(0xff); } break :blk .{ - socket_reader.getStream(), + socket_reader.socket, .{ .range = id_range }, try .init(setup.min_keycode, setup.max_keycode), .{ @@ -67,17 +75,17 @@ pub fn main() !void { }, }; }; - defer x11.disconnect(stream); + defer x11.disconnect(io, socket); var write_buffer: [1000]u8 = undefined; var read_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(stream, &write_buffer); - var socket_reader = x11.socketReader(stream, &read_buffer); + var socket_writer = x11.socketWriter(io, socket, &write_buffer); + var socket_reader = x11.socketReader(io, socket, &read_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; - var source: x11.Source = .initAfterSetup(socket_reader.interface()); + var source: x11.Source = .initAfterSetup(&socket_reader.interface); run(ids, &root, &sink, &source, keyrange) catch |err| switch (err) { error.WriteFailed => |e| return x11.onWriteError(e, socket_writer.err.?), - error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.getError()), + error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.err), error.UnexpectedMessage => |e| return e, }; } @@ -320,5 +328,8 @@ fn renderString( ); } +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); diff --git a/examples/present.zig b/examples/present.zig index f724ae4..fa1c209 100644 --- a/examples/present.zig +++ b/examples/present.zig @@ -48,22 +48,30 @@ const Root = struct { depth: x11.Depth, }; -pub fn main() !void { +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init.Minimal) !void { + var t: @import("Threaded") = .init_single_threaded; + try mainCompat(init.environ, t.io()); +} +fn mainBefore16() !void { + try mainCompat(.{}, .legacy); +} +pub fn mainCompat(environ: std16.process.Environ, io: std16.Io) !void { try x11.wsaStartup(); - const stream: std.net.Stream, const ids: Ids, const keyrange: x11.KeycodeRange, const root: Root = blk: { + const socket: x11.Socket, const ids: Ids, const keyrange: x11.KeycodeRange, const root: Root = blk: { var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - errdefer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + errdefer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; std.log.info("setup reply {f}", .{setup}); - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); const screen = (x11.draft.readSetupDynamic(&source, &setup, .{}) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }) orelse { std.log.err("no screen?", .{}); @@ -75,7 +83,7 @@ pub fn main() !void { std.process.exit(0xff); } break :blk .{ - socket_reader.getStream(), + socket_reader.socket, .{ .range = id_range }, try .init(setup.min_keycode, setup.max_keycode), .{ @@ -88,22 +96,23 @@ pub fn main() !void { }, }; }; - defer x11.disconnect(stream); + defer x11.disconnect(io, socket); var write_buffer: [1000]u8 = undefined; var read_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(stream, &write_buffer); - var socket_reader = x11.socketReader(stream, &read_buffer); + var socket_writer = x11.socketWriter(io, socket, &write_buffer); + var socket_reader = x11.socketReader(io, socket, &read_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; - var source: x11.Source = .initAfterSetup(socket_reader.interface()); - run(ids, &root, &sink, &source, keyrange) catch |err| switch (err) { + var source: x11.Source = .initAfterSetup(&socket_reader.interface); + run(io, ids, &root, &sink, &source, keyrange) catch |err| switch (err) { error.WriteFailed => |e| return x11.onWriteError(e, socket_writer.err.?), - error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.getError()), + error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.err), error.UnexpectedMessage => |e| return e, }; } fn run( + io: std16.Io, ids: Ids, root: *const Root, sink: *x11.RequestSink, @@ -243,6 +252,7 @@ fn run( .height = window_height, }})); frame_time_graph.writeRender( + io, sink, pixmap.drawable(), ids.gc(), @@ -257,6 +267,10 @@ fn run( } } +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); + const FrameTimeGraph = @import("FrameTimeGraph.zig"); diff --git a/examples/queryfont.zig b/examples/queryfont.zig index 5431941..cf64bc2 100644 --- a/examples/queryfont.zig +++ b/examples/queryfont.zig @@ -1,54 +1,67 @@ const std = @import("std"); const x11 = @import("x11"); +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; +const std16 = if (zig_atleast_16) std else @import("std16"); + pub const log_level = std.log.Level.info; -pub fn main() !void { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - const allocator = arena.allocator(); - const all_args = try std.process.argsAlloc(allocator); - if (all_args.len <= 1) { +const arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); + +const ArgsIterator = if (zig_atleast_16) std.process.Args.Iterator else std.process.ArgIterator; + +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init) !void { + var args_it = try init.minimal.args.iterateAllocator(init.arena.allocator()); + try mainCompat(&args_it, init.minimal.environ, init.io); +} +fn mainBefore16() !void { + var arena = arena_instance; + var args_it: std.process.ArgIterator = try .initWithAllocator(arena.allocator()); + try mainCompat(&args_it, .{}, .legacy); +} +fn mainCompat(args_it: *ArgsIterator, environ: std16.process.Environ, io: std16.Io) !void { + _ = args_it.next(); // skip program name + const font_name = args_it.next() orelse { std.debug.print("Usage: queryfont FONTNAME\n", .{}); std.process.exit(0); - } - const cmd_args = all_args[1..]; - if (cmd_args.len != 1) { - std.log.err("expected 1 cmd arg (FONTNAME) but got {}", .{cmd_args.len}); + }; + if (args_it.next() != null) { + std.log.err("expected 1 cmd arg (FONTNAME) but got more", .{}); std.process.exit(1); } - const font_name = cmd_args[0]; try x11.wsaStartup(); - const stream, const setup = blk: { + const socket, const setup = blk: { var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - errdefer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + errdefer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; std.log.info("setup reply {f}", .{setup}); - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); const screen = (x11.draft.readSetupDynamic(&source, &setup, .{}) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }) orelse { std.log.err("no screen?", .{}); std.process.exit(0xff); }; _ = screen; - break :blk .{ socket_reader.getStream(), setup }; + break :blk .{ socket_reader.socket, setup }; }; - defer x11.disconnect(stream); + defer x11.disconnect(io, socket); var write_buffer: [1000]u8 = undefined; var read_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(stream, &write_buffer); - var socket_reader = x11.socketReader(stream, &read_buffer); + var socket_writer = x11.socketWriter(io, socket, &write_buffer); + var socket_reader = x11.socketReader(io, socket, &read_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; - var source: x11.Source = .initAfterSetup(socket_reader.interface()); + var source: x11.Source = .initAfterSetup(&socket_reader.interface); const id_range = try x11.IdRange.init(setup.resource_id_base, setup.resource_id_mask); const font_id = id_range.addAssumeCapacity(0).font(); @@ -61,10 +74,10 @@ pub fn main() !void { })) |e| return x11.onWriteError(e, socket_writer.err.?); var stdout_buffer: [1000]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdout_writer = std16.Io.File.stdout().writer(io, &stdout_buffer); streamFont(&source, sink.sequence, &stdout_writer.interface) catch |err| switch (err) { error.WriteFailed => return stdout_writer.err.?, - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol, error.UnexpectedMessage => |e| return e, }; } diff --git a/examples/runall.zig b/examples/runall.zig index f6a10e1..15fa573 100644 --- a/examples/runall.zig +++ b/examples/runall.zig @@ -1,28 +1,41 @@ -pub fn main() !u8 { +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init) !u8 { + var args_it = try init.minimal.args.iterateAllocator(init.arena.allocator()); + return mainCompat(&args_it, init.io); +} +fn mainBefore16() !u8 { + var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); + var args_it: std.process.ArgIterator = try .initWithAllocator(arena_instance.allocator()); + return mainCompat(&args_it, .legacy); +} +fn mainCompat(args_it: *std16.process.Args.Iterator, io: std16.Io) !u8 { var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator); - const arena = arena_instance.allocator(); - const cmdline = try Cmdline.alloc(arena); + _ = args_it.next(); // skip program name var count: usize = 0; - for (1..cmdline.len()) |arg_index| { - const exe = cmdline.arg(arg_index); + while (args_it.next()) |exe| { const name = std.fs.path.stem(exe); - const args1 = .{exe}; - const args2 = .{ exe, "fixed" }; + const args1 = [_][]const u8{exe}; + const args2 = [_][]const u8{ exe, "fixed" }; const args: []const []const u8 = if (std.mem.eql(u8, name, "queryfont")) &args2 else &args1; std.log.info("[RUN] {s}", .{exe}); - var child: std.process.Child = .init(args, arena); - try child.spawn(); - const term = try child.wait(); - switch (term) { - .Exited => |code| if (code != 0) { - std.log.err("{s} failed", .{name}); - return 0xff; - }, - else => |sig| { - std.log.err("{s} {s} with {}", .{ name, @tagName(sig), sig }); - return 0xff; - }, + const exit_code: u8 = if (zig_atleast_16) blk: { + var child = try std.process.spawn(io, .{ .argv = args }); + break :blk switch (try child.wait(io)) { + .exited => |code| code, + else => 0xff, + }; + } else blk: { + var child: std.process.Child = .init(args, arena_instance.allocator()); + try child.spawn(); + break :blk switch (try child.wait()) { + .Exited => |code| code, + else => 0xff, + }; + }; + if (exit_code != 0) { + std.log.err("{s} failed", .{name}); + return 0xff; } count += 1; } @@ -31,5 +44,7 @@ pub fn main() !u8 { return 0; } +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + const std = @import("std"); -const Cmdline = @import("Cmdline.zig"); +const std16 = if (zig_atleast_16) std else @import("std16"); diff --git a/examples/testexample.zig b/examples/testexample.zig index a8cb4b8..b61cac6 100644 --- a/examples/testexample.zig +++ b/examples/testexample.zig @@ -1,7 +1,10 @@ // A working example to test various parts of the API const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + const Endian = std.builtin.Endian; var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); @@ -81,18 +84,26 @@ fn getImageFormat( }; } -pub fn main() !void { +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init.Minimal) !void { + var t: @import("Threaded") = .init_single_threaded; + try mainCompat(init.environ, t.io()); +} +fn mainBefore16() !void { + try mainCompat(.{}, .legacy); +} +pub fn mainCompat(environ: std16.process.Environ, io: std16.Io) !void { try x11.wsaStartup(); var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - defer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + defer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); std.log.info("setup reply {f}", .{setup}); try source.requireReplyAtLeast(setup.required()); { @@ -172,7 +183,7 @@ pub fn main() !void { }; var write_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(socket_reader.getStream(), &write_buffer); + var socket_writer = x11.socketWriter(io, socket_reader.socket, &write_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; const id_range = try x11.IdRange.init(setup.resource_id_base, setup.resource_id_mask); @@ -187,7 +198,7 @@ pub fn main() !void { ); run(&sink, &source, ids, depth, image_format, screen) catch |err| switch (err) { error.WriteFailed => |e| return x11.onWriteError(e, socket_writer.err.?), - error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.getError()), + error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.err), error.UnexpectedMessage => |e| return e, }; } diff --git a/examples/text.zig b/examples/text.zig index c3397c3..e4c9a6f 100644 --- a/examples/text.zig +++ b/examples/text.zig @@ -42,45 +42,49 @@ const Options = struct { size: f32 = 24.0, }; -pub fn main() !void { +const ArgsIterator = if (zig_atleast_16) std.process.Args.Iterator else std.process.ArgIterator; + +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init) !void { + var args_it = try init.minimal.args.iterateAllocator(init.arena.allocator()); + try mainCompat(&args_it, init.minimal.environ, init.io); +} +fn mainBefore16() !void { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + var args_it: std.process.ArgIterator = try .initWithAllocator(arena.allocator()); + try mainCompat(&args_it, .{}, .legacy); +} +fn mainCompat(args_it: *ArgsIterator, environ: std16.process.Environ, io: std16.Io) !void { + _ = args_it.next(); // skip program name + var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator); - // no need to deinit - const cmdline = try Cmdline.alloc(arena_instance.allocator()); var opt: Options = .{}; - { - var i: usize = 1; - while (i < cmdline.len()) : (i += 1) { - const arg = cmdline.arg(i); - if (std.mem.eql(u8, arg, "--font")) { - i += 1; - if (i == cmdline.len()) errExit("--font missing arg", .{}); - opt.font_file = cmdline.arg(i); - } else if (std.mem.eql(u8, arg, "--size")) { - i += 1; - if (i == cmdline.len()) errExit("--size missing arg", .{}); - const size_str = cmdline.arg(i); - opt.size = std.fmt.parseFloat(f32, size_str) catch errExit("invalid --size '{s}'", .{size_str}); - } else errExit("unknown cmdline option '{s}'", .{arg}); - } + while (args_it.next()) |arg| { + if (std.mem.eql(u8, arg, "--font")) { + opt.font_file = args_it.next() orelse errExit("--font missing arg", .{}); + } else if (std.mem.eql(u8, arg, "--size")) { + const size_str = args_it.next() orelse errExit("--size missing arg", .{}); + opt.size = std.fmt.parseFloat(f32, size_str) catch errExit("invalid --size '{s}'", .{size_str}); + } else errExit("unknown cmdline option '{s}'", .{arg}); } try x11.wsaStartup(); - const stream: std.net.Stream, const ids: Ids, const root: Root = blk: { + const socket: x11.Socket, const ids: Ids, const root: Root = blk: { var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - errdefer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + errdefer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; std.log.info("setup reply {f}", .{setup}); - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); const screen = (x11.draft.readSetupDynamic(&source, &setup, .{}) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }) orelse { std.log.err("no screen?", .{}); @@ -92,7 +96,8 @@ pub fn main() !void { std.process.exit(0xff); } break :blk .{ - socket_reader.getStream(), .{ .range = id_range }, .{ + socket_reader.socket, .{ .range = id_range }, + .{ .window = screen.root, .visual = screen.root_visual, .depth = x11.Depth.init(screen.root_depth) orelse std.debug.panic( @@ -102,22 +107,23 @@ pub fn main() !void { }, }; }; - defer x11.disconnect(stream); + defer x11.disconnect(io, socket); var write_buffer: [1000]u8 = undefined; var read_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(stream, &write_buffer); - var socket_reader = x11.socketReader(stream, &read_buffer); + var socket_writer = x11.socketWriter(io, socket, &write_buffer); + var socket_reader = x11.socketReader(io, socket, &read_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; - var source: x11.Source = .initAfterSetup(socket_reader.interface()); - run(ids, &root, &sink, &source, &arena_instance, opt) catch |err| switch (err) { + var source: x11.Source = .initAfterSetup(&socket_reader.interface); + run(io, ids, &root, &sink, &source, &arena_instance, opt) catch |err| switch (err) { error.WriteFailed => |e| return x11.onWriteError(e, socket_writer.err.?), - error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.getError()), + error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.err), error.UnexpectedMessage => |e| return e, }; } fn run( + io: std16.Io, ids: Ids, root: *const Root, sink: *x11.RequestSink, @@ -246,10 +252,11 @@ fn run( const ttf_content = blk: { if (opt.font_file) |font_file| { - break :blk std.fs.cwd().readFileAlloc( - arena_instance.allocator(), + break :blk std16.Io.Dir.cwd().readFileAlloc( + io, font_file, - std.math.maxInt(usize), + arena_instance.allocator(), + .unlimited, ) catch |e| errExit( "read '{s}' failed with {s}", .{ font_file, @errorName(e) }, @@ -554,8 +561,12 @@ fn errExit(comptime fmt: []const u8, args: anytype) noreturn { std.process.exit(0xff); } +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); + const xtt = @import("xtt"); const XY = x11.XY; const assert = std.debug.assert; diff --git a/examples/transparent.zig b/examples/transparent.zig index 0b2de5c..6ea376b 100644 --- a/examples/transparent.zig +++ b/examples/transparent.zig @@ -1,6 +1,9 @@ const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + const window_width = 400; const window_height = 400; @@ -21,25 +24,33 @@ const Ids = struct { const needed_capacity = 4; }; -pub fn main() !void { +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init.Minimal) !void { + var t: @import("Threaded") = .init_single_threaded; + try mainCompat(init.environ, t.io()); +} +fn mainBefore16() !void { + try mainCompat(.{}, .legacy); +} +pub fn mainCompat(environ: std16.process.Environ, io: std16.Io) !void { try x11.wsaStartup(); - const stream: std.net.Stream, const ids: Ids, const root_window: x11.Window, const transparent_visual: x11.Visual = blk: { + const socket: x11.Socket, const ids: Ids, const root_window: x11.Window, const transparent_visual: x11.Visual = blk: { var read_buffer: [1000]u8 = undefined; - var socket_reader, const used_auth = try x11.draft.connect(&read_buffer); - errdefer x11.disconnect(socket_reader.getStream()); + var socket_reader, const used_auth = try x11.draft.connect(io, environ, &read_buffer); + errdefer x11.disconnect(io, socket_reader.socket); _ = used_auth; - const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + const setup = x11.readSetupSuccess(&socket_reader.interface) catch |err| switch (err) { + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }; std.log.info("setup reply {f}", .{setup}); - var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup); + var source: x11.Source = .initFinishSetup(&socket_reader.interface, &setup); var on_visual: OnVisual = .{}; const screen = (x11.draft.readSetupDynamic(&source, &setup, .{ .on_visual = &on_visual.base, }) catch |err| switch (err) { - error.ReadFailed => return socket_reader.getError().?, + error.ReadFailed => return socket_reader.err.?, error.EndOfStream, error.Protocol => |e| return e, }) orelse { std.log.err("no screen?", .{}); @@ -55,23 +66,23 @@ pub fn main() !void { std.process.exit(0xff); } break :blk .{ - socket_reader.getStream(), + socket_reader.socket, .{ .range = id_range }, screen.root, on_visual.transparent_visual, }; }; - defer x11.disconnect(stream); + defer x11.disconnect(io, socket); var write_buffer: [1000]u8 = undefined; var read_buffer: [1000]u8 = undefined; - var socket_writer = x11.socketWriter(stream, &write_buffer); - var socket_reader = x11.socketReader(stream, &read_buffer); + var socket_writer = x11.socketWriter(io, socket, &write_buffer); + var socket_reader = x11.socketReader(io, socket, &read_buffer); var sink: x11.RequestSink = .{ .writer = &socket_writer.interface }; - var source: x11.Source = .initAfterSetup(socket_reader.interface()); + var source: x11.Source = .initAfterSetup(&socket_reader.interface); run(ids, root_window, transparent_visual, &sink, &source) catch |err| switch (err) { error.WriteFailed => |e| return x11.onWriteError(e, socket_writer.err.?), - error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.getError()), + error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.err), error.UnexpectedMessage => |e| return e, }; } diff --git a/src/SocketReader.zig b/src/SocketReader.zig new file mode 100644 index 0000000..5c4b9ed --- /dev/null +++ b/src/SocketReader.zig @@ -0,0 +1,107 @@ +const SocketReader = @This(); + +io: Io, +interface: Io.Reader, +socket: Socket, +err: ?Error, + +const max_iovecs_len = 8; + +pub const Error = error{ + SystemResources, + ConnectionResetByPeer, + Timeout, + SocketUnconnected, + /// The file descriptor does not hold the required rights to read + /// from it. + AccessDenied, + NetworkDown, +} || Io.Cancelable || error{Unexpected}; + +pub fn init(socket: Socket, io: std16.Io, buffer: []u8) SocketReader { + var result: SocketReader = .{ + .io = io, + .interface = .{ + .vtable = &.{ + .stream = streamImpl, + .readVec = readVec, + .discard = discard, + }, + .buffer = buffer, + .seek = 0, + .end = 0, + }, + .socket = socket, + .err = null, + }; + if (!zig_atleast_16) { + switch (builtin.os.tag) { + .windows => { + // workaround https://github.com/ziglang/zig/issues/25620 + if (!zig_atleast_15_3) { + result.interface.vtable = &@import("netpatch.zig").vtable; + } + }, + else => {}, + } + } + return result; +} + +fn streamImpl( + io_r: *std16.Io.Reader, + io_w: *std16.Io.Writer, + limit: std16.Io.Limit, +) std16.Io.Reader.StreamError!usize { + const dest = limit.slice(try io_w.writableSliceGreedy(1)); + var data: [1][]u8 = .{dest}; + const n = try readVec(io_r, &data); + io_w.advance(n); + return n; +} + +fn discard(io_r: *std16.Io.Reader, limit: std16.Io.Limit) std16.Io.Reader.Error!usize { + const r: *SocketReader = @alignCast(@fieldParentPtr("interface", io_r)); + const io = r.io; + var scratch: [4096]u8 = undefined; + const dest_len = @min(scratch.len, @intFromEnum(limit)); + var data: [1][]u8 = .{scratch[0..dest_len]}; + const n = io.vtable.netRead(io.userdata, r.socket, &data) catch |err| { + r.err = err; + return error.ReadFailed; + }; + if (n == 0) return error.EndOfStream; + return n; +} + +fn readVec(io_r: *std16.Io.Reader, data: [][]u8) std16.Io.Reader.Error!usize { + const r: *SocketReader = @alignCast(@fieldParentPtr("interface", io_r)); + const io = r.io; + var iovecs_buffer: [max_iovecs_len][]u8 = undefined; + const dest_n, const data_size = try io_r.writableVector(&iovecs_buffer, data); + const dest = iovecs_buffer[0..dest_n]; + std.debug.assert(dest[0].len > 0); + const n = io.vtable.netRead(io.userdata, r.socket, dest) catch |err| { + r.err = err; + return error.ReadFailed; + }; + if (n == 0) { + return error.EndOfStream; + } + if (n > data_size) { + r.interface.end += n - data_size; + return data_size; + } + return n; +} + +pub const Socket = if (zig_atleast_16) std.Io.net.Socket.Handle else std.net.Stream.Handle; + +pub const zig_atleast_15_3 = builtin.zig_version.order(.{ .major = 0, .minor = 15, .patch = 3 }) != .lt; +pub const zig_atleast_16 = builtin.zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + +const builtin = @import("builtin"); +const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); + +const Io = std16.Io; diff --git a/src/SocketWriter.zig b/src/SocketWriter.zig new file mode 100644 index 0000000..9620a47 --- /dev/null +++ b/src/SocketWriter.zig @@ -0,0 +1,81 @@ +const SocketWriter = @This(); + +io: Io, +interface: Io.Writer, +socket: Socket, +err: ?Error = null, +write_file_err: ?WriteFileError = null, + +pub const Error = error{ + /// Another TCP Fast Open is already in progress. + FastOpenAlreadyInProgress, + /// Network session was unexpectedly closed by recipient. + ConnectionResetByPeer, + /// The output queue for a network interface was full. This generally indicates that the + /// interface has stopped sending, but may be caused by transient congestion. (Normally, + /// this does not occur in Linux. Packets are just silently dropped when a device queue + /// overflows.) + /// + /// This is also caused when there is not enough kernel memory available. + SystemResources, + /// No route to network. + NetworkUnreachable, + /// Network reached but no route to host. + HostUnreachable, + /// The local network interface used to reach the destination is down. + NetworkDown, + /// The destination address is not listening. + ConnectionRefused, + /// The passed address didn't have the correct address family in its sa_family field. + AddressFamilyUnsupported, + /// Local end has been shut down on a connection-oriented socket, or + /// the socket was never connected. + SocketUnconnected, + SocketNotBound, +} || Io.UnexpectedError || Io.Cancelable; + +pub const WriteFileError = error{ + NetworkDown, +} || Io.Cancelable || Io.UnexpectedError; + +pub fn init(socket: Socket, io: Io, buffer: []u8) SocketWriter { + return .{ + .io = io, + .socket = socket, + .interface = .{ + .vtable = &.{ + .drain = drain, + .sendFile = sendFile, + }, + .buffer = buffer, + }, + }; +} + +fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { + const w: *SocketWriter = @alignCast(@fieldParentPtr("interface", io_w)); + const io = w.io; + const buffered = io_w.buffered(); + const n = io.vtable.netWrite(io.userdata, w.socket, buffered, data, splat) catch |err| { + w.err = err; + return error.WriteFailed; + }; + return io_w.consume(n); +} + +fn sendFile(io_w: *Io.Writer, file_reader: *SendFileReader, limit: Io.Limit) Io.Writer.FileError!usize { + _ = io_w; + _ = file_reader; + _ = limit; + return error.Unimplemented; // TODO +} + +pub const Socket = if (zig_atleast_16) std.Io.net.Socket.Handle else std.net.Stream.Handle; + +pub const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + +const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); + +const Io = std16.Io; +const SendFileReader = if (zig_atleast_16) std.Io.File.Reader else std.fs.File.Reader; diff --git a/src/x.zig b/src/x.zig index c7a1806..8037c19 100644 --- a/src/x.zig +++ b/src/x.zig @@ -27,6 +27,7 @@ const zigversion = @import("zigversion"); const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const stdext = @import("stdext.zig"); const testing = std.testing; const builtin = @import("builtin"); @@ -66,30 +67,27 @@ pub const Charset = charset.Charset; pub const Slice = zigversion.Slice; pub const SliceWithMaxLen = zigversion.SliceWithMaxLen; pub const keymap = @import("keymap.zig"); +pub const SocketReader = @import("SocketReader.zig"); +pub const SocketWriter = @import("SocketWriter.zig"); pub const log = std.log.scoped(.x11); -pub const zig_atleast_15_3 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 15, .patch = 3 }) != .lt; +pub const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + +const IpAddress = if (zig_atleast_16) std.Io.net.IpAddress else void; + +pub const Stream = std16.Io.net.Stream; +pub const Socket = if (zig_atleast_16) std.Io.net.Socket.Handle else std.net.Stream.Handle; +pub const FileReaderError = if (zig_atleast_16) std.Io.File.Reader.Error else std.fs.File.ReadError; // TODO: drop this function when support for 0.15 is dropped. -pub fn socketWriter(stream: std.net.Stream, buffer: []u8) std.net.Stream.Writer { - return stream.writer(buffer); +pub fn socketWriter(io: std16.Io, socket: Socket, buffer: []u8) SocketWriter { + return .init(socket, io, buffer); } // TODO: drop this function when support for 0.15 is dropped. -pub fn socketReader(stream: std.net.Stream, buffer: []u8) std.net.Stream.Reader { - switch (builtin.os.tag) { - .windows => { - // workaround https://github.com/ziglang/zig/issues/25620 - if (!zig_atleast_15_3) { - var reader = stream.reader(buffer); - reader.interface_state.vtable = &@import("netpatch.zig").vtable; - return reader; - } - }, - else => {}, - } - return stream.reader(buffer); +pub fn socketReader(io: std16.Io, socket: Socket, buffer: []u8) SocketReader { + return .init(socket, io, buffer); } const x_test = @import("test/x_test.zig"); @@ -217,24 +215,29 @@ pub const GetDisplayError = error{ /// Returns the DISPLAY environment variable for any platform. /// On windows it just uses the page allocator and leaks the result. /// Use getDisplayPosix which has no possible error for non-windows systems. -pub fn getDisplay() GetDisplayError!Display { +pub fn getDisplay(environ: std16.process.Environ) GetDisplayError!Display { if (builtin.os.tag == .windows) { // we'll just make an allocator and never free it, no // big deal var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - return .{ .string = std.process.getEnvVarOwned(arena.allocator(), "DISPLAY") catch |err| switch (err) { + return .{ .string = environ.getAlloc(arena.allocator(), "DISPLAY") catch |err| if (zig_atleast_16) switch (err) { + error.EnvironmentVariableMissing => null, + error.OutOfMemory, + error.InvalidWtf8, + => |e| return e, + } else switch (err) { error.EnvironmentVariableNotFound => null, error.OutOfMemory, error.InvalidWtf8, => |e| return e, } }; } - return getDisplayPosix(); + return getDisplayPosix(environ); } /// Posix-specific function to get DISPLAY which cannot fail. -pub fn getDisplayPosix() Display { - return .{ .string = posix.getenv("DISPLAY") }; +pub fn getDisplayPosix(environ: std16.process.Environ) Display { + return .{ .string = environ.getPosix("DISPLAY") }; } pub const Protocol = enum { @@ -341,16 +344,59 @@ pub fn parseDisplay(display: Display) InvalidDisplayError!ParsedDisplay { return parsed; } +pub const Address = if (zig_atleast_16) union(enum) { + in: std.Io.net.Ip4Address, + in6: std.Io.net.Ip6Address, + un: if (std.Io.net.has_unix_sockets) posix.sockaddr.un else void, + + pub fn initIp4(addr: [4]u8, port: u16) Address { + return .{ .in = .{ .bytes = addr, .port = port } }; + } + pub fn initIp6(addr: [16]u8, port: u16, flowinfo: u32, scope_id: u32) Address { + return .{ .in6 = std.Io.net.Ip6Address.init(addr, port, flowinfo, scope_id) }; + } + + pub fn initUnix(path: []const u8) error{NameTooLong}!Address { + var result: Address = .{ .un = .{ .family = posix.AF.UNIX, .path = undefined } }; + if (path.len + 1 > result.un.path.len) return error.NameTooLong; + @memcpy(result.un.path[0..path.len], path); + result.un.path[path.len] = 0; + return result; + } + + pub fn parseIp4(buf: []const u8, port: u16) !Address { + return .{ .in = try std.Io.net.Ip4Address.parse(buf, port) }; + } + pub fn parseIp6(buf: []const u8, port: u16) !Address { + return .{ .in6 = try std.Io.net.Ip6Address.parse(buf, port) }; + } + + pub fn fromIpAddress(ip: std.Io.net.IpAddress) Address { + return switch (ip) { + .ip4 => |a| .{ .in = a }, + .ip6 => |a| .{ .in6 = a }, + }; + } + + pub fn format(self: Address, w: *std.Io.Writer) error{WriteFailed}!void { + switch (self) { + .in => |ip4| try ip4.format(w), + .in6 => |ip6| try ip6.format(w), + .un => |ua| try w.writeAll(std.mem.sliceTo(&ua.path, 0)), + } + } +} else std.net.Address; + pub const Host = union(enum) { - address: std.net.Address, + address: Address, domain_name: struct { string: []const u8, port: u16, }, pub fn initDomainName(host: []const u8, port: u16) Host { std.debug.assert(host.len != 0); - if (std.net.Address.parseIp4(host, port)) |addr| return .{ .address = addr } else |_| {} - if (std.net.Address.parseIp6(host, port)) |addr| return .{ .address = addr } else |_| {} + if (Address.parseIp4(host, port)) |addr| return .{ .address = addr } else |_| {} + if (Address.parseIp6(host, port)) |addr| return .{ .address = addr } else |_| {} // TODO: should/could we check if this is a valid hostname? // maybe not since we might detect this later during DNS resolution? return .{ .domain_name = .{ .string = host, .port = port } }; @@ -363,7 +409,7 @@ pub const Host = union(enum) { } }; -fn localhostIp4(port: u16) std.net.Address { +fn localhostIp4(port: u16) Address { return .initIp4([_]u8{ 127, 0, 0, 1 }, port); } @@ -375,13 +421,13 @@ pub fn getHost(display: Display, parsed: *const ParsedDisplay) error{X11BadDispl log.err("not sure if it's valid for DISPLAY to host with the unix protocol", .{}); return error.X11BadDisplay; } - var addr: std.net.Address = .{ .un = .{ .family = posix.AF.UNIX, .path = undefined } }; + var host: Host = .{ .address = .{ .un = .{ .family = posix.AF.UNIX, .path = undefined } } }; _ = std.fmt.bufPrintZ( - &addr.un.path, + &host.address.un.path, "/tmp/.X11-unix/X{d}", .{@intFromEnum(parsed.display_num)}, ) catch unreachable; - return .{ .address = addr }; + return host; }, .tcp, .inet => if (host_or_empty.len == 0) return .{ .address = localhostIp4(parsed.display_num.asPort()), @@ -401,7 +447,7 @@ pub fn getHost(display: Display, parsed: *const ParsedDisplay) error{X11BadDispl } if (host_or_empty[0] == '/') { // TODO: should we check if this file exists? - return .{ .address = std.net.Address.initUnix(host_or_empty) catch |e| switch (e) { + return .{ .address = Address.initUnix(host_or_empty) catch |e| switch (e) { error.NameTooLong => { log.err("unix socket path '{s}' is too long ({})", .{ host_or_empty, host_or_empty.len }); return error.X11BadDisplay; @@ -418,7 +464,7 @@ pub fn getHost(display: Display, parsed: *const ParsedDisplay) error{X11BadDispl std.process.exit(0xff); } - var addr: std.net.Address = .{ .un = .{ .family = posix.AF.UNIX, .path = undefined } }; + var addr: Address = .{ .un = .{ .family = posix.AF.UNIX, .path = undefined } }; _ = std.fmt.bufPrintZ( &addr.un.path, "/tmp/.X11-unix/X{d}", @@ -446,11 +492,13 @@ pub const AuthFileKind = enum { /// Note that if an authentication fails, it will reconnect and reset /// the given IO. pub const Authenticator = struct { + environ: std16.process.Environ, + io: std16.Io, display: Display, parsed_display: *const ParsedDisplay, host: *const Host, - address: *const std.net.Address, - stream: std.net.Stream, + address: *const Address, + socket: Socket, stream_read_buffer: []u8, filename_buffer: []u8, order: Order, @@ -461,7 +509,7 @@ pub const Authenticator = struct { need_reconnect: bool = false, pub fn deinit(authenticator: *Authenticator) void { - authenticator.state.deinit(); + authenticator.state.deinit(authenticator.io); authenticator.* = undefined; } @@ -500,15 +548,15 @@ pub const Authenticator = struct { connect: AuthIndex, read_file: ReadFile, done: DoneReason, - pub fn deinit(state: *State) void { + pub fn deinit(state: *State, io: std16.Io) void { switch (state.*) { .connect => {}, - .read_file => |*r| r.close(), + .read_file => |*r| r.close(io), .done => {}, } } - pub fn update(state: *State, new_state: State) void { - state.deinit(); + pub fn update(state: *State, io: std16.Io, new_state: State) void { + state.deinit(io); state.* = new_state; } }; @@ -517,7 +565,7 @@ pub const Authenticator = struct { no_more_auth, }; - pub const Success = struct { std.net.Stream.Reader, bool }; + pub const Success = struct { SocketReader, bool }; pub const Event = union(enum) { reply: union(enum) { @@ -536,15 +584,15 @@ pub const Authenticator = struct { open_auth_file_error: struct { kind: AuthFileKind, filename: []const u8, - err: std.fs.File.OpenError, + err: std16.Io.File.OpenError, }, auth_file_opened: struct { kind: AuthFileKind, filename: []const u8, }, io_error: union(enum) { - write_error: std.net.Stream.WriteError, - read_error: (error{EndOfStream} || std.net.Stream.ReadError), + write_error: std16.Io.net.Stream.Writer.Error, + read_error: (error{EndOfStream} || std16.Io.net.Stream.Reader.Error), protocol, }, }; @@ -553,12 +601,12 @@ pub const Authenticator = struct { auth_index: AuthIndex, auth_file_kind: AuthFileKind, filename: []const u8, - file: std.fs.File, + file: std16.Io.File, read_buf: [400]u8 = undefined, - file_reader: std.fs.File.Reader, + file_reader: std16.Io.File.Reader, reader: AuthReader, - pub fn close(self: *ReadFile) void { - self.file.close(); + pub fn close(self: *ReadFile, io: std16.Io) void { + self.file.close(io); } }; @@ -567,41 +615,42 @@ pub const Authenticator = struct { .connect => |index| switch (authFromIndex(authenticator.order, index)) { .no_auth => { authenticator.connect() catch |e| { - authenticator.state.update(.{ .done = .{ .reconnect_error = e } }); + authenticator.state.update(authenticator.io, .{ .done = .{ .reconnect_error = e } }); continue; }; - authenticator.state.update(index.nextAuthState()); + authenticator.state.update(authenticator.io, index.nextAuthState()); authenticator.need_reconnect = true; log.info("sending setup with no auth...", .{}); - writeSetupNoAuth(authenticator.stream) catch |err| return .{ + writeSetupNoAuth(authenticator.io, authenticator.socket) catch |err| return .{ .io_error = .{ .write_error = err }, }; return authenticator.readSetupReply(.{ .used_auth = false }); }, .auth => |auth_file_kind| { const maybe_auth_filename = getAuthFilename( + authenticator.environ, auth_file_kind, authenticator.filename_buffer, ) catch |err| { - authenticator.state.update(index.nextAuthState()); + authenticator.state.update(authenticator.io, index.nextAuthState()); return .{ .get_auth_filename_error = .{ .kind = auth_file_kind, .err = err, } }; }; const auth_filename = maybe_auth_filename orelse { - authenticator.state.update(index.nextAuthState()); + authenticator.state.update(authenticator.io, index.nextAuthState()); continue; }; - const file = std.fs.cwd().openFile(auth_filename, .{}) catch |err| { - authenticator.state.update(index.nextAuthState()); + const file = std16.Io.Dir.cwd().openFile(authenticator.io, auth_filename, .{}) catch |err| { + authenticator.state.update(authenticator.io, index.nextAuthState()); return .{ .open_auth_file_error = .{ .kind = auth_file_kind, .filename = auth_filename, .err = err, } }; }; - authenticator.state.update(.{ .read_file = .{ + authenticator.state.update(authenticator.io, .{ .read_file = .{ .auth_file_kind = auth_file_kind, .auth_index = index, .filename = auth_filename, @@ -610,7 +659,7 @@ pub const Authenticator = struct { .file_reader = undefined, .reader = undefined, } }); - authenticator.state.read_file.file_reader = .init(file, &authenticator.state.read_file.read_buf); + authenticator.state.read_file.file_reader = file.reader(authenticator.io, &authenticator.state.read_file.read_buf); authenticator.state.read_file.reader = .{ .reader = &authenticator.state.read_file.file_reader.interface }; }, }, @@ -624,11 +673,11 @@ pub const Authenticator = struct { authenticator.auth_count, ) catch |err| { reportAuthReadError(r.filename, err, r.file_reader.err); - authenticator.state.update(r.auth_index.nextAuthState()); + authenticator.state.update(authenticator.io, r.auth_index.nextAuthState()); continue; }) { .eof => { - authenticator.state.update(r.auth_index.nextAuthState()); + authenticator.state.update(authenticator.io, r.auth_index.nextAuthState()); continue; }, .skip => { @@ -642,7 +691,7 @@ pub const Authenticator = struct { const auth_index = authenticator.auth_count; authenticator.auth_count += 1; authenticator.connect() catch |e| { - authenticator.state.update(.{ .done = .{ .reconnect_error = e } }); + authenticator.state.update(authenticator.io, .{ .done = .{ .reconnect_error = e } }); continue; }; authenticator.need_reconnect = true; @@ -650,9 +699,10 @@ pub const Authenticator = struct { "sending setup with auth {} from '{s}' ({s}) ({s})", .{ auth_index, r.filename, r.auth_file_kind.context(), match.name.nativeSlice() }, ); - var write_error: ?std.net.Stream.WriteError = null; + var write_error: ?SocketWriter.Error = null; writeSetupWithAuth( - authenticator.stream, + authenticator.io, + authenticator.socket, &r.reader, match.name, match.data_len, @@ -663,7 +713,7 @@ pub const Authenticator = struct { } }, error.ReadFailed, error.EndOfStream => |e| { reportAuthReadError(r.filename, e, r.file_reader.err); - authenticator.state.update(r.auth_index.nextAuthState()); + authenticator.state.update(authenticator.io, r.auth_index.nextAuthState()); continue; }, }; @@ -678,22 +728,22 @@ pub const Authenticator = struct { } fn connect(authenticator: *Authenticator) ConnectAddressError!void { if (authenticator.need_reconnect) { - const new_stream = connectAddress(authenticator.address) catch |e| { + const new_stream = connectAddress(authenticator.io, authenticator.address) catch |e| { log.err("reconnect to {f} failed with {t}", .{ authenticator.address.*, e }); return e; }; log.info("reconnected", .{}); - std.debug.assert(authenticator.stream.handle != new_stream.handle); - disconnect(authenticator.stream); - authenticator.stream = new_stream; + std.debug.assert(authenticator.socket != new_stream); + disconnect(authenticator.io, authenticator.socket); + authenticator.socket = new_stream; authenticator.need_reconnect = false; } } fn readSetupReply(authenticator: *Authenticator, named: struct { used_auth: bool }) Event { - var socket_reader = socketReader(authenticator.stream, authenticator.stream_read_buffer); - return switch (readSetupReply1(socket_reader.interface()) catch |err| return switch (err) { + var socket_reader = socketReader(authenticator.io, authenticator.socket, authenticator.stream_read_buffer); + return switch (readSetupReply1(&socket_reader.interface) catch |err| return switch (err) { error.ReadFailed => .{ .io_error = .{ - .read_error = socket_reader.getError() orelse error.Unexpected, + .read_error = socket_reader.err orelse error.Unexpected, } }, error.EndOfStream => |e| .{ .io_error = .{ .read_error = e, @@ -717,7 +767,11 @@ pub const Authenticator = struct { }, }; } - fn reportAuthReadError(filename: []const u8, err: error{ ReadFailed, EndOfStream }, file_error: ?std.fs.File.ReadError) void { + fn reportAuthReadError( + filename: []const u8, + err: error{ ReadFailed, EndOfStream }, + file_error: ?FileReaderError, + ) void { switch (err) { error.ReadFailed => { const e = file_error orelse error.Unexpected; @@ -733,9 +787,9 @@ pub const Authenticator = struct { // The size of the client's initial setup message if both name and data are empty const write_setup_no_auth_len = 12; -fn writeSetupNoAuth(stream: std.net.Stream) std.net.Stream.WriteError!void { +fn writeSetupNoAuth(io: std16.Io, socket: Socket) SocketWriter.Error!void { var write_buffer: [write_setup_no_auth_len]u8 = undefined; - var socket_writer = stream.writer(&write_buffer); + var socket_writer = socketWriter(io, socket, &write_buffer); const w = &socket_writer.interface; writeSetupHeader(w, .empty, 0) catch unreachable; writeSetupData(w, .empty) catch unreachable; @@ -745,15 +799,16 @@ fn writeSetupNoAuth(stream: std.net.Stream) std.net.Stream.WriteError!void { // name is assumed to be taken from the AuthReader buffer, so, it becomes invalidated // as soon as any more data is read from AuthReader. fn writeSetupWithAuth( - stream: std.net.Stream, + io: std16.Io, + socket: Socket, auth_reader: *AuthReader, name: Slice(u16, [*]const u8), data_len: u16, - out_write_error: *?std.net.Stream.WriteError, + out_write_error: *?SocketWriter.Error, ) error{ ReadFailed, EndOfStream, WriteFailed }!void { std.debug.assert(out_write_error.* == null); var write_buffer: [write_setup_no_auth_len + 400]u8 = undefined; - var socket_writer = stream.writer(&write_buffer); + var socket_writer = socketWriter(io, socket, &write_buffer); defer out_write_error.* = socket_writer.err; const w = &socket_writer.interface; writeSetupHeader(w, name, data_len) catch unreachable; @@ -818,7 +873,7 @@ fn matchAuthEntry( display: Display, parsed_display: *const ParsedDisplay, host: *const Host, - address: *const std.net.Address, + address: *const Address, auth_reader: *AuthReader, entry_index: u32, ) error{ ReadFailed, EndOfStream }!union(enum) { @@ -891,46 +946,88 @@ pub const ConnectError = error{UnknownHostName} || ConnectTcpError || ConnectUnixError; -pub fn connect(addr: *const Host) ConnectError!struct { std.net.Address, std.net.Stream } { - switch (addr.*) { - .address => |*address| return .{ address.*, try connectAddress(address) }, - .domain_name => |*host| { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - const list = try getAddressList(arena.allocator(), host.string, host.port); - defer list.deinit(); - var ip4_only = true; - while (true) { - for (list.addrs) |net_addr| { - const is_ip4 = (net_addr.any.family == posix.AF.INET); - if (is_ip4 != ip4_only) continue; - if (connectTcp(&net_addr)) |stream| { - return .{ net_addr, stream }; - } else |err| switch (err) { - error.ConnectionRefused, error.Unexpected => continue, - error.AccessDenied, - error.SystemResources, - => |e| return e, - } - } - if (!ip4_only) break; - ip4_only = false; +pub fn connect(io: std16.Io, addr: *const Host) ConnectError!struct { Address, Socket } { + return switch (addr.*) { + .address => |*address| .{ address.*, try connectAddress(io, address) }, + .domain_name => |*host| if (zig_atleast_16) + try connectDomainName16(io, host.string, host.port) + else + try connectDomainNameLegacy(host.string, host.port), + }; +} + +fn connectDomainNameLegacy(name: []const u8, port: u16) ConnectError!struct { Address, Socket } { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + const list = try getAddressList(arena.allocator(), name, port); + defer list.deinit(); + var ip4_only = true; + while (true) { + for (list.addrs) |net_addr| { + const is_ip4 = (net_addr.any.family == posix.AF.INET); + if (is_ip4 != ip4_only) continue; + if (connectTcpLegacy(&net_addr)) |fd| { + return .{ net_addr, fd }; + } else |err| switch (err) { + error.ConnectionRefused, error.Unexpected => continue, + error.AccessDenied, + error.SystemResources, + => |e| return e, } - if (list.addrs.len == 0) return error.UnknownHostName; - return error.ConnectionRefused; - }, + } + if (!ip4_only) break; + ip4_only = false; } + if (list.addrs.len == 0) return error.UnknownHostName; + return error.ConnectionRefused; } -const ConnectAddressError = ConnectTcpError || ConnectUnixError; -fn connectAddress(addr: *const std.net.Address) ConnectAddressError!std.net.Stream { - return switch (addr.any.family) { - posix.AF.UNIX => try connectUnix( - &addr.un, - std.mem.sliceTo(&addr.un.path, 0).len, - ), - else => try connectTcp(addr), +fn connectDomainName16(io: std.Io, name: []const u8, port: u16) ConnectError!struct { Address, std.posix.fd_t } { + const host_name = std.Io.net.HostName.init(name) catch |err| { + log.err("invalid hostname '{s}': {t}", .{ name, err }); + return error.UnknownHostName; }; + var lookup_buf: [32]std.Io.net.HostName.LookupResult = undefined; + var lookup_queue: std.Io.Queue(std.Io.net.HostName.LookupResult) = .init(&lookup_buf); + host_name.lookup(io, &lookup_queue, .{ .port = port }) catch |err| { + log.err("DNS lookup for '{s}' failed: {t}", .{ name, err }); + return error.UnknownHostName; + }; + // Drain resolved addresses from the queue. + var addrs: [32]IpAddress = undefined; + var addr_count: usize = 0; + while (true) { + const result = lookup_queue.getOne(io) catch break; // Closed = done + switch (result) { + .address => |ip_addr| { + if (addr_count < addrs.len) { + addrs[addr_count] = ip_addr; + addr_count += 1; + } + }, + .canonical_name => {}, + } + } + if (addr_count == 0) return error.UnknownHostName; + // Try IPv4 first, then IPv6, matching the legacy behavior. + var ip4_only = true; + while (true) { + for (addrs[0..addr_count]) |*ip_addr| { + const is_ip4 = (ip_addr.* == .ip4); + if (is_ip4 != ip4_only) continue; + if (connectTcp(io, ip_addr)) |fd| { + return .{ Address.fromIpAddress(ip_addr.*), fd }; + } else |err| switch (err) { + error.ConnectionRefused, error.Unexpected => continue, + error.AccessDenied, + error.SystemResources, + => |e| return e, + } + } + if (!ip4_only) break; + ip4_only = false; + } + return error.ConnectionRefused; } pub fn getAddressList(allocator: std.mem.Allocator, name: []const u8, port: u16) ConnectError!*std.net.AddressList { @@ -954,17 +1051,64 @@ pub fn getAddressList(allocator: std.mem.Allocator, name: []const u8, port: u16) }; } +const ConnectAddressError = ConnectTcpError || ConnectUnixError; +fn connectAddress(io: std16.Io, addr: *const Address) ConnectAddressError!Socket { + return if (zig_atleast_16) switch (addr.*) { + .in => |*ip4| connectTcp(io, &.{ .ip4 = ip4.* }), + .in6 => |*ip6| connectTcp(io, &.{ .ip6 = ip6.* }), + .un => |*ua| connectUnix16(io, ua, std.mem.sliceTo(&ua.path, 0).len), + } else switch (addr.any.family) { + posix.AF.UNIX => connectUnix(&addr.un, std.mem.sliceTo(&addr.un.path, 0).len), + else => connectTcpLegacy(addr), + }; +} + const ConnectTcpError = error{ AccessDenied, ConnectionRefused, SystemResources, Unexpected, }; -fn connectTcp(addr: *const std.net.Address) ConnectTcpError!std.net.Stream { - // tcp connections can take a while so let's add a log to know - // if we're blocked on it +fn connectTcp(io: std16.Io, addr: *const std16.Io.net.IpAddress) ConnectTcpError!Socket { log.info("connecting to {f}", .{addr.*}); - return std.net.tcpConnectToAddress(addr.*) catch |err| switch (err) { + if (zig_atleast_16) { + const stream = std.Io.net.IpAddress.connect(addr, io, .{ .mode = .stream }) catch |err| switch (err) { + error.ConnectionRefused, + error.ConnectionResetByPeer, + error.HostUnreachable, + error.NetworkUnreachable, + error.ConnectionPending, + error.Timeout, + => return error.ConnectionRefused, + error.SystemResources, + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + => return error.SystemResources, + error.AccessDenied, + => return error.AccessDenied, + error.AddressUnavailable, + error.AddressFamilyUnsupported, + error.WouldBlock, + error.OptionUnsupported, + error.ProtocolUnsupportedBySystem, + error.ProtocolUnsupportedByAddressFamily, + error.SocketModeUnsupported, + error.NetworkDown, + error.Unexpected, + error.Canceled, + => |e| { + log.err("TCP connect failed unexpectedly with {t}", .{e}); + return error.Unexpected; + }, + }; + return stream.socket.handle; + } else { + return connectTcpLegacy(&addr.toStdAddress()); + } +} + +fn connectTcpLegacy(addr: *const std.net.Address) ConnectTcpError!Socket { + const stream = std.net.tcpConnectToAddress(addr.*) catch |err| switch (err) { error.ConnectionTimedOut, error.ConnectionRefused, error.NetworkUnreachable, @@ -988,15 +1132,16 @@ fn connectTcp(addr: *const std.net.Address) ConnectTcpError!std.net.Stream { error.ProtocolNotSupported, error.SocketTypeNotSupported, => |e| { - log.err("TCP connect to {f} failed unexpectedly with {s}", .{ addr, @errorName(e) }); + log.err("TCP connect failed unexpectedly with {t}", .{e}); return error.Unexpected; }, }; + return stream.handle; } -pub fn disconnect(stream: std.net.Stream) void { - posix.shutdown(stream.handle, .both) catch {}; // ignore any error here - stream.close(); +pub fn disconnect(io: std16.Io, socket: Socket) void { + io.vtable.netShutdown(io.userdata, socket, .both) catch {}; + io.vtable.netClose(io.userdata, (&socket)[0..1]); } const ConnectUnixError = error{ @@ -1011,7 +1156,7 @@ const ConnectUnixError = error{ SystemFdQuotaExceeded, Unexpected, }; -pub fn connectUnix(addr: *const posix.sockaddr.un, path_len: usize) ConnectUnixError!std.net.Stream { +pub fn connectUnix(addr: *const posix.sockaddr.un, path_len: usize) ConnectUnixError!Socket { const sock = posix.socket(posix.AF.UNIX, posix.SOCK.STREAM, 0) catch |err| switch (err) { error.SystemResources => |e| return e, error.AccessDenied => return error.AccessDenied, @@ -1053,7 +1198,36 @@ pub fn connectUnix(addr: *const posix.sockaddr.un, path_len: usize) ConnectUnixE return error.Unexpected; }, }; - return .{ .handle = sock }; + return sock; +} + +fn connectUnix16(io: std.Io, addr: *const posix.sockaddr.un, path_len: usize) ConnectUnixError!Socket { + const ua = std.Io.net.UnixAddress.init(std.mem.sliceTo(&addr.path, 0)[0..path_len]) catch + return error.Unexpected; + const stream = std.Io.net.UnixAddress.connect(&ua, io) catch |err| switch (err) { + error.FileNotFound, + error.SystemResources, + error.AccessDenied, + error.PermissionDenied, + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + => |e| return e, + error.AddressFamilyUnsupported, + error.ProtocolUnsupportedBySystem, + error.SocketModeUnsupported, + error.SymLinkLoop, + error.NotDir, + error.ReadOnlyFileSystem, + error.WouldBlock, + error.NetworkDown, + error.Unexpected, + error.Canceled, + => |e| { + log.err("connect unix socket unexpectedly failed with {t}", .{e}); + return error.Unexpected; + }, + }; + return stream.socket.handle; } fn unexpectedError(e: anyerror) error{Unexpected} { @@ -1133,26 +1307,30 @@ pub const GetAuthFilenameError = error{ Unexpected, }; -pub fn getAuthFilename(kind: AuthFileKind, filename_buf: []u8) GetAuthFilenameError!?[]const u8 { +pub fn getAuthFilename(environ: std16.process.Environ, kind: AuthFileKind, filename_buf: []u8) GetAuthFilenameError!?[]const u8 { switch (kind) { .env => { if (builtin.os.tag == .windows) { var fba: std.heap.FixedBufferAllocator = .init(filename_buf); - if (std.process.getEnvVarOwned(fba.allocator(), "XAUTHORITY")) |filename| { + if (environ.getAlloc(fba.allocator(), "XAUTHORITY")) |filename| { return filename; - } else |err| return switch (err) { + } else |err| return if (zig_atleast_16) switch (err) { + error.OutOfMemory => return error.XauthorityEnvTooBig, + error.EnvironmentVariableMissing => null, + error.InvalidWtf8 => error.XauthorityEnvInvalidUnicode, + } else switch (err) { error.OutOfMemory => return error.XauthorityEnvTooBig, error.EnvironmentVariableNotFound => null, error.InvalidWtf8 => error.XauthorityEnvInvalidUnicode, }; } else { - if (posix.getenv("XAUTHORITY")) |xauth| return xauth; + if (environ.getPosix("XAUTHORITY")) |xauth| return xauth; return null; } }, .home => { if (builtin.os.tag == .windows) return null; - const home = posix.getenv("HOME") orelse return null; + const home = environ.getPosix("HOME") orelse return null; const basename = ".Xauthority"; const len = home.len + 1 + basename.len; if (len + 1 > filename_buf.len) return error.HomeTooLong; @@ -1206,7 +1384,7 @@ pub const AuthFamily = enum(u16) { fn matchAddr( connect_host: *const Host, - connect_addr: *const std.net.Address, + connect_addr: *const Address, file_addr: AuthFileAddr, ) bool { // todo: we'll probably need this to implement better address matching @@ -1217,9 +1395,14 @@ fn matchAddr( log.err("expected xauth inet addr to be 4 bytes but got {}", .{file_addr.data.len}); return false; } - if (connect_addr.any.family != posix.AF.INET) return false; - comptime std.debug.assert(@sizeOf(@TypeOf(connect_addr.in.sa.addr)) == 4); - const addr4 = std.mem.asBytes(&connect_addr.in.sa.addr); + const addr4 = if (zig_atleast_16) blk: { + if (connect_addr.* != .in) return false; + break :blk &connect_addr.in.bytes; + } else blk: { + if (connect_addr.any.family != posix.AF.INET) return false; + break :blk std.mem.asBytes(&connect_addr.in.sa.addr); + }; + comptime std.debug.assert(addr4.len == 4); return std.mem.eql(u8, addr4, file_addr.data); }, .inet6 => { @@ -1227,13 +1410,22 @@ fn matchAddr( log.err("expected xauth inet6 addr to be 16 bytes but got {}", .{file_addr.data.len}); return false; } - if (connect_addr.any.family != posix.AF.INET6) return false; - comptime std.debug.assert(@sizeOf(@TypeOf(connect_addr.in6.sa.addr)) == 16); - const addr6 = std.mem.asBytes(&connect_addr.in6.sa.addr); + const addr6 = if (zig_atleast_16) blk: { + if (connect_addr.* != .in6) return false; + break :blk &connect_addr.in6.bytes; + } else blk: { + if (connect_addr.any.family != posix.AF.INET6) return false; + break :blk std.mem.asBytes(&connect_addr.in6.sa.addr); + }; + comptime std.debug.assert(addr6.len == 16); return std.mem.eql(u8, addr6, file_addr.data); }, .unix => { - if (connect_addr.any.family != posix.AF.UNIX) return false; + if (zig_atleast_16) { + if (connect_addr.* != .un) return false; + } else { + if (connect_addr.any.family != posix.AF.UNIX) return false; + } // not sure how to properly compare the paths here so I'm just // going to always return true if we've connected to any unix path return true; @@ -1431,11 +1623,11 @@ pub const AuthReader = struct { /// Call to handle a WriteFailed on the socket writer and/or RequestSink after the intial setup. /// It will either return an error or nothing if the error is a BrokenPipe which means the /// connection to the X server has been closed. -pub fn onWriteError(write_failed: error{WriteFailed}, err: std.net.Stream.WriteError) !void { +pub fn onWriteError(write_failed: error{WriteFailed}, err: SocketWriter.Error) !void { write_failed catch {}; switch (err) { - error.BrokenPipe => { - log.info("BrokenPipe", .{}); + error.SocketUnconnected => { + log.info("SocketUnconnected", .{}); }, else => |e| return e, } @@ -4140,8 +4332,8 @@ pub const Source = struct { pub fn onReadError( source: *const Source, read_err: error{ ReadFailed, EndOfStream, Protocol }, - stream_err: ?std.net.Stream.ReadError, - ) (error{ EndOfStream, Protocol } || std.net.Stream.ReadError)!void { + stream_err: ?SocketReader.Error, + ) (error{ EndOfStream, Protocol } || SocketReader.Error)!void { switch (read_err) { error.ReadFailed => switch (stream_err.?) { error.ConnectionResetByPeer => |e| { @@ -5560,7 +5752,9 @@ pub fn charsetName(set: Charset) ?[]const u8 { // the start of their program to setup WSA on Windows pub fn wsaStartup() !void { if (builtin.os.tag == .windows) { - _ = try windows.WSAStartup(2, 2); + if (!zig_atleast_16) { + _ = try windows.WSAStartup(2, 2); + } } } diff --git a/src/x/draft.zig b/src/x/draft.zig index 006033e..43f9aa6 100644 --- a/src/x/draft.zig +++ b/src/x/draft.zig @@ -19,8 +19,8 @@ pub const ConnectError = error{ // Error from authenticate error AuthRejected, }; -pub fn connect(read_buffer: []u8) ConnectError!x11.Authenticator.Success { - const display = x11.getDisplay() catch |err| { +pub fn connect(io: std16.Io, environ: std16.process.Environ, read_buffer: []u8) ConnectError!x11.Authenticator.Success { + const display = x11.getDisplay(environ) catch |err| { x11.log.err("failed to get x11 display with {s}", .{@errorName(err)}); return error.GetDisplay; }; @@ -35,7 +35,7 @@ pub fn connect(read_buffer: []u8) ConnectError!x11.Authenticator.Success { return error.BadDisplay; }, }; - const address, const initial_stream = x11.connect(&host) catch |err| return switch (err) { + const address, const initial_stream = x11.connect(io, &host) catch |err| return switch (err) { error.SystemResources, error.ConnectionTimedOut, error.AccessDenied, @@ -52,9 +52,11 @@ pub fn connect(read_buffer: []u8) ConnectError!x11.Authenticator.Success { return e; }, }; - errdefer x11.disconnect(initial_stream); + errdefer x11.disconnect(io, initial_stream); x11.log.info("connected to {f}", .{address}); return try x11.draft.authenticate( + environ, + io, display, &parsed_display, &host, @@ -66,11 +68,13 @@ pub fn connect(read_buffer: []u8) ConnectError!x11.Authenticator.Success { } pub fn authenticate( + environ: std16.process.Environ, + io: std16.Io, display: x11.Display, parsed_display: *const x11.ParsedDisplay, host: *const x11.Host, - address: *const std.net.Address, - stream: std.net.Stream, + address: *const x11.Address, + socket: x11.Socket, stream_read_buffer: []u8, opt: struct { order: x11.Authenticator.Order = .auth_first, @@ -78,11 +82,13 @@ pub fn authenticate( ) error{AuthRejected}!x11.Authenticator.Success { var filename_buffer: [std.fs.max_path_bytes]u8 = undefined; var authenticator: x11.Authenticator = .{ + .environ = environ, + .io = io, .display = display, .parsed_display = parsed_display, .host = host, .address = address, - .stream = stream, + .socket = socket, .stream_read_buffer = stream_read_buffer, .filename_buffer = &filename_buffer, .order = opt.order, @@ -229,5 +235,8 @@ pub fn synchronousQueryExtension( return result; } +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("../x.zig"); diff --git a/src/xauth.zig b/src/xauth.zig index fbced14..6bcaa61 100644 --- a/src/xauth.zig +++ b/src/xauth.zig @@ -1,6 +1,9 @@ const std = @import("std"); +const std16 = if (zig_atleast_16) std else @import("std16"); const x11 = @import("x11"); +const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + const global = struct { pub var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); pub const arena = arena_instance.allocator(); @@ -24,76 +27,74 @@ fn usage() void { , .{}); } -pub fn main() !void { - const all_args = try std.process.argsAlloc(global.arena); +const ArgsIterator = if (zig_atleast_16) std.process.Args.Iterator else std.process.ArgIterator; + +pub const main = if (zig_atleast_16) mainAtleast16 else mainBefore16; +fn mainAtleast16(init: std.process.Init) !void { + var args_it = try init.minimal.args.iterateAllocator(init.arena.allocator()); + try mainCompat(&args_it, init.minimal.environ, init.io); +} +fn mainBefore16() !void { + var args_it: std.process.ArgIterator = try .initWithAllocator(global.arena); // no need to free + try mainCompat(&args_it, .{}, .legacy); +} +fn mainCompat(args_it: *ArgsIterator, environ: std16.process.Environ, io: std16.Io) !void { + _ = args_it.next(); var opt = Opt{}; - const args = blk: { - var new_arg_count: usize = 0; - var arg_index: usize = 1; - while (arg_index < all_args.len) : (arg_index += 1) { - const arg = all_args[arg_index]; + const cmd: []const u8 = blk: { + var maybe_cmd: ?[]const u8 = null; + while (args_it.next()) |arg| { if (!std.mem.startsWith(u8, arg, "-")) { - all_args[new_arg_count] = arg; - new_arg_count += 1; + if (maybe_cmd != null) { + std.log.err("too many cmdline args", .{}); + std.process.exit(1); + } + maybe_cmd = arg; } else if (std.mem.eql(u8, arg, "-f")) { - arg_index += 1; - if (arg_index >= all_args.len) { + opt.auth_filename = args_it.next() orelse { std.log.err("missing authfilename after option -f", .{}); std.process.exit(1); - } - opt.auth_filename = all_args[arg_index]; + }; } else { std.log.err("invalid option \"{s}\"", .{arg}); std.process.exit(1); } } - break :blk all_args[0..new_arg_count]; + break :blk maybe_cmd orelse return usage(); }; - if (args.len == 0) { - usage(); - return; - } - const cmd = args[0]; - const cmd_args = args[1..]; - if (std.mem.eql(u8, cmd, "help")) { usage(); } else if (std.mem.eql(u8, cmd, "list")) { - try list(opt, cmd_args); + try list(environ, io, opt); } else { std.log.err("invalid command \"{s}\"", .{cmd}); std.process.exit(1); } } -fn list(opt: Opt, cmd_args: []const [:0]const u8) !void { - if (cmd_args.len != 0) { - std.log.err("list command doesn't accept any arguments", .{}); - std.process.exit(1); - } - +fn list(environ: std16.process.Environ, io: std16.Io, opt: Opt) !void { if (opt.auth_filename) |filename| { - const file = std.fs.cwd().openFile(filename, .{}) catch |err| { + const file = std16.Io.Dir.cwd().openFile(io, filename, .{}) catch |err| { std.log.err("open '{s}' failed with {s}", .{ filename, @errorName(err) }); std.process.exit(1); }; - defer file.close(); - try list2(file); + defer file.close(io); + try list2(io, file); } else { var filename_buf: [std.fs.max_path_bytes]u8 = undefined; for (std.enums.valuesFromFields( x11.AuthFileKind, @typeInfo(x11.AuthFileKind).@"enum".fields, )) |kind| { - if (x11.getAuthFilename(kind, &filename_buf) catch |err| { + if (x11.getAuthFilename(environ, kind, &filename_buf) catch |err| { std.log.err("get auth filename ({s}) failed with {s}", .{ kind.context(), @errorName(err) }); continue; }) |filename| { - if (std.fs.cwd().openFile(filename, .{})) |file| { - defer file.close(); - try list2(file); + if (std16.Io.Dir.cwd().openFile(io, filename, .{})) |file| { + defer file.close(io); + try list2(io, file); } else |err| { std.log.info("open '{s}' failed with {s}", .{ filename, @errorName(err) }); } @@ -102,19 +103,19 @@ fn list(opt: Opt, cmd_args: []const [:0]const u8) !void { } } -fn list2(file: std.fs.File) !void { +fn list2(io: std16.Io, file: std16.Io.File) !void { var file_read_buf: [4096]u8 = undefined; - var file_reader = file.reader(&file_read_buf); + var file_reader = file.reader(io, &file_read_buf); var reader: x11.AuthReader = .{ .reader = &file_reader.interface }; - list3(&reader) catch |err| return switch (err) { + list3(io, &reader) catch |err| return switch (err) { error.ReadFailed => file_reader.err orelse error.ReadFailed, else => |e| e, }; } -fn list3(reader: *x11.AuthReader) !void { +fn list3(io: std16.Io, reader: *x11.AuthReader) !void { var stdout_buffer: [1000]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdout_writer = std16.Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; var entry_index: u32 = 0; while (true) : (entry_index += 1) { diff --git a/std16/build.zig b/std16/build.zig new file mode 100644 index 0000000..53636fc --- /dev/null +++ b/std16/build.zig @@ -0,0 +1,7 @@ +pub fn build(b: *std.Build) void { + _ = b.addModule("std16", .{ + .root_source_file = b.path("src/std.zig"), + }); +} + +const std = @import("std"); diff --git a/std16/build.zig.zon b/std16/build.zig.zon new file mode 100644 index 0000000..9b2e2cc --- /dev/null +++ b/std16/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = .std16, + .version = "0.0.0", + .fingerprint = 0xda34ab1eec55af90, + .minimum_zig_version = "0.15.2", + .dependencies = .{}, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/std16/src/Io.zig b/std16/src/Io.zig new file mode 100644 index 0000000..ee2687e --- /dev/null +++ b/std16/src/Io.zig @@ -0,0 +1,1072 @@ +const Io = @This(); + +const builtin = @import("builtin"); + +const std = @import("std.zig"); +const math = std.math; +const assert = std.debug.assert; +// const Allocator = std.mem.Allocator; +// const Alignment = std.mem.Alignment; + +userdata: ?*anyopaque, +vtable: *const VTable, + +pub const legacy: Io = .{ .userdata = null, .vtable = &legacy_vtable }; + +pub const Reader = @import("std").Io.Reader; +pub const Writer = @import("std").Io.Writer; +pub const net = @import("Io/net.zig"); +pub const Dir = @import("Io/Dir.zig"); +pub const File = @import("Io/File.zig"); + +pub const VTable = struct { + now: *const fn (?*anyopaque, Clock) Timestamp, + netRead: *const fn (?*anyopaque, src: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize, + netWrite: *const fn (?*anyopaque, dest: net.Socket.Handle, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, + netClose: *const fn (?*anyopaque, handle: []const net.Socket.Handle) void, + netShutdown: *const fn (?*anyopaque, handle: net.Socket.Handle, how: net.ShutdownHow) net.ShutdownError!void, +}; + +// pub const Operation = union(enum) { +// file_read_streaming: FileReadStreaming, +// file_write_streaming: FileWriteStreaming, +// /// On Windows this is NtDeviceIoControlFile. On POSIX this is ioctl. On +// /// other systems this tag is unreachable. +// device_io_control: DeviceIoControl, +// net_receive: NetReceive, + +// pub const Tag = @typeInfo(Operation).@"union".tag_type.?; + +// /// May return 0 reads which is different than `error.EndOfStream`. +// pub const FileReadStreaming = struct { +// file: File, +// data: []const []u8, + +// pub const Error = UnendingError || error{EndOfStream}; +// pub const UnendingError = error{ +// InputOutput, +// SystemResources, +// /// Trying to read a directory file descriptor as if it were a file. +// IsDir, +// ConnectionResetByPeer, +// /// File was not opened with read capability. +// NotOpenForReading, +// SocketUnconnected, +// /// Non-blocking has been enabled, and reading from the file descriptor +// /// would block. +// WouldBlock, +// /// In WASI, this error occurs when the file descriptor does +// /// not hold the required rights to read from it. +// AccessDenied, +// /// Unable to read file due to lock. Depending on the `Io` implementation, +// /// reading from a locked file may return this error, or may ignore the +// /// lock. +// LockViolation, +// } || Io.UnexpectedError; + +// pub const Result = Error!usize; +// }; + +// pub const FileWriteStreaming = struct { +// file: File, +// header: []const u8 = &.{}, +// data: []const []const u8, +// splat: usize = 1, + +// pub const Error = error{ +// DiskQuota, +// FileTooBig, +// InputOutput, +// NoSpaceLeft, +// DeviceBusy, +// /// File descriptor does not hold the required rights to write to it. +// AccessDenied, +// PermissionDenied, +// /// File is an unconnected socket, or closed its read end. +// BrokenPipe, +// /// Insufficient kernel memory to read from in_fd. +// SystemResources, +// NotOpenForWriting, +// /// The process cannot access the file because another process has locked +// /// a portion of the file. Windows-only. +// LockViolation, +// /// Non-blocking has been enabled and this operation would block. +// WouldBlock, +// /// This error occurs when a device gets disconnected before or mid-flush +// /// while it's being written to - errno(6): No such device or address. +// NoDevice, +// FileBusy, +// } || Io.UnexpectedError; + +// pub const Result = Error!usize; +// }; + +// pub const DeviceIoControl = switch (builtin.os.tag) { +// .wasi => noreturn, +// .windows => struct { +// file: File, +// code: std.os.windows.CTL_CODE, +// in: []const u8 = &.{}, +// out: []u8 = &.{}, + +// pub const Result = std.os.windows.IO_STATUS_BLOCK; +// }, +// else => struct { +// file: File, +// /// Device-dependent operation code. +// code: u32, +// arg: ?*anyopaque, + +// /// Device and operation dependent result. Negative values are +// /// negative errno. +// pub const Result = i32; +// }, +// }; + +// pub const NetReceive = struct { +// socket_handle: net.Socket.Handle, +// message_buffer: []net.IncomingMessage, +// data_buffer: []u8, +// flags: net.ReceiveFlags, + +// pub const Error = error{ +// /// Insufficient memory or other resource internal to the operating system. +// SystemResources, +// /// Per-process limit on the number of open file descriptors has been reached. +// ProcessFdQuotaExceeded, +// /// System-wide limit on the total number of open files has been reached. +// SystemFdQuotaExceeded, +// /// Local end has been shut down on a connection-oriented socket, or +// /// the socket was never connected. +// SocketUnconnected, +// /// The socket type requires that message be sent atomically, and the +// /// size of the message to be sent made this impossible. The message +// /// was not transmitted, or was partially transmitted. +// MessageOversize, +// /// Network connection was unexpectedly closed by sender. +// ConnectionResetByPeer, +// /// The local network interface used to reach the destination is offline. +// NetworkDown, +// /// A connectionless packet was previously sent successfully, +// /// however, it was not received because no service is operating at +// /// the destination port of the transport on the remote system. +// /// This caused an ICMP port unreachable packet to be returned to +// /// the OS where it was queued up to be reported at the next call +// /// to send or receive on the bound socket. +// PortUnreachable, +// } || Io.UnexpectedError; + +// pub const Result = struct { ?net.Socket.ReceiveError, usize }; +// }; + +// pub const Result = Result: { +// const operation_fields = @typeInfo(Operation).@"union".fields; +// var fields: [operation_fields.len]std.builtin.TypeInfo.UnionField = undefined; +// for (operation_fields, &fields) |field, *union_field| { +// union_field.* = .{ +// .name = field.name, +// .type = if (field.type == noreturn) noreturn else field.type.Result, +// }; +// } +// break :Result @Type(.{ .@"union" = .{ +// .layout = .auto, +// .tag_type = Tag, +// .fields = &fields, +// .decls = &.{}, +// } }); +// }; + +// pub const Storage = union { +// unused: List.DoubleNode, +// submission: Submission, +// pending: Pending, +// completion: Completion, + +// pub const Submission = struct { +// node: List.SingleNode, +// operation: Operation, +// }; + +// pub const Pending = struct { +// node: List.DoubleNode, +// tag: Tag, +// userdata: Userdata align(@max(@alignOf(usize), 4)), + +// pub const Userdata = [7]usize; +// }; + +// pub const Completion = struct { +// node: List.SingleNode, +// result: Result, +// }; +// }; + +// pub const OptionalIndex = enum(u32) { +// none = std.math.maxInt(u32), +// _, + +// pub fn fromIndex(i: usize) OptionalIndex { +// const oi: OptionalIndex = @enumFromInt(i); +// assert(oi != .none); +// return oi; +// } + +// pub fn toIndex(oi: OptionalIndex) u32 { +// assert(oi != .none); +// return @intFromEnum(oi); +// } +// }; +// pub const List = struct { +// head: OptionalIndex, +// tail: OptionalIndex, + +// pub const empty: List = .{ .head = .none, .tail = .none }; + +// pub const SingleNode = struct { next: OptionalIndex }; +// pub const DoubleNode = struct { prev: OptionalIndex, next: OptionalIndex }; +// }; +// }; + +// /// Performs one `Operation`. +// pub fn operate(io: Io, operation: Operation) Cancelable!Operation.Result { +// return io.vtable.operate(io.userdata, operation); +// } + +// pub const OperateTimeoutError = Cancelable || Timeout.Error || ConcurrentError; + +// /// Performs one `Operation` with provided `timeout`. +// pub fn operateTimeout(io: Io, operation: Operation, timeout: Timeout) OperateTimeoutError!Operation.Result { +// var storage: [1]Operation.Storage = undefined; +// var batch: Batch = .init(&storage); +// batch.addAt(0, operation); +// try batch.awaitConcurrent(io, timeout); +// const completion = batch.next().?; +// assert(completion.index == 0); +// return completion.result; +// } + +// /// Submits many operations together without waiting for all of them to +// /// complete. +// /// +// /// This is a low-level abstraction based on `Operation`. For a higher +// /// level API that operates on `Future`, see `Select` and `Group`. +// pub const Batch = struct { +// storage: []Operation.Storage, +// unused: Operation.List, +// submitted: Operation.List, +// pending: Operation.List, +// completed: Operation.List, +// userdata: ?*anyopaque align(@max(@alignOf(?*anyopaque), 4)), + +// /// After calling this, it is safe to unconditionally defer a call to +// /// `cancel`. `storage` is a pre-allocated buffer of undefined memory that +// /// determines the maximum number of active operations that can be +// /// submitted via `add` and `addAt`. +// pub fn init(storage: []Operation.Storage) Batch { +// var prev: Operation.OptionalIndex = .none; +// for (storage, 0..) |*operation, index| { +// operation.* = .{ .unused = .{ .prev = prev, .next = .fromIndex(index + 1) } }; +// prev = .fromIndex(index); +// } +// storage[storage.len - 1].unused.next = .none; +// return .{ +// .storage = storage, +// .unused = .{ +// .head = .fromIndex(0), +// .tail = .fromIndex(storage.len - 1), +// }, +// .submitted = .empty, +// .pending = .empty, +// .completed = .empty, +// .userdata = null, +// }; +// } + +// /// Adds an operation to be performed at the next await call. +// /// Returns the index that will be returned by `next` after the operation completes. +// /// Asserts that no more than `storage.len` operations are active at a time. +// pub fn add(batch: *Batch, operation: Operation) u32 { +// const index = batch.unused.head.toIndex(); +// batch.addAt(index, operation); +// return index; +// } + +// /// Adds an operation to be performed at the next await call. +// /// After the operation completes, `next` will return `index`. +// /// Asserts that the operation at `index` is not active. +// pub fn addAt(batch: *Batch, index: u32, operation: Operation) void { +// const storage = &batch.storage[index]; +// const unused = storage.unused; +// switch (unused.prev) { +// .none => batch.unused.head = unused.next, +// else => |prev_index| batch.storage[prev_index.toIndex()].unused.next = unused.next, +// } +// switch (unused.next) { +// .none => batch.unused.tail = unused.prev, +// else => |next_index| batch.storage[next_index.toIndex()].unused.prev = unused.prev, +// } + +// switch (batch.submitted.tail) { +// .none => batch.submitted.head = .fromIndex(index), +// else => |tail_index| batch.storage[tail_index.toIndex()].submission.node.next = .fromIndex(index), +// } +// storage.* = .{ .submission = .{ .node = .{ .next = .none }, .operation = operation } }; +// batch.submitted.tail = .fromIndex(index); +// } + +// pub const Completion = struct { +// /// The element within the provided operation storage that completed. +// /// `addAt` can be used to re-arm the `Batch` using this `index`. +// index: u32, +// /// The return value of the operation. +// result: Operation.Result, +// }; + +// /// After calling `awaitAsync`, `awaitConcurrent`, or `cancel`, this +// /// function iterates over the completed operations. +// /// +// /// Each completion returned from this function dequeues from the `Batch`. +// /// It is not required to dequeue all completions before awaiting again. +// pub fn next(batch: *Batch) ?Completion { +// const index = batch.completed.head; +// if (index == .none) return null; +// const storage = &batch.storage[index.toIndex()]; +// const completion = storage.completion; +// const next_index = completion.node.next; +// batch.completed.head = next_index; +// if (next_index == .none) batch.completed.tail = .none; + +// const tail_index = batch.unused.tail; +// switch (tail_index) { +// .none => batch.unused.head = index, +// else => batch.storage[tail_index.toIndex()].unused.next = index, +// } +// storage.* = .{ .unused = .{ .prev = tail_index, .next = .none } }; +// batch.unused.tail = index; +// return .{ .index = index.toIndex(), .result = completion.result }; +// } + +// /// Waits for at least one of the submitted operations to complete. After +// /// this function returns the completed operations can be iterated with +// /// `next`. +// /// +// /// This function provides opportunity for the implementation to introduce +// /// concurrency into the batched operations, but unlike `awaitConcurrent`, +// /// does not require it, and therefore cannot fail with +// /// `error.ConcurrencyUnavailable`. +// pub fn awaitAsync(batch: *Batch, io: Io) Cancelable!void { +// return io.vtable.batchAwaitAsync(io.userdata, batch); +// } + +// pub const AwaitConcurrentError = ConcurrentError || Cancelable || Timeout.Error; + +// /// Waits for at least one of the submitted operations to complete. After +// /// this function returns the completed operations can be iterated with +// /// `next`. +// /// +// /// Unlike `awaitAsync`, this function requires the implementation to +// /// perform the operations concurrently and therefore can fail with +// /// `error.ConcurrencyUnavailable`. +// pub fn awaitConcurrent(batch: *Batch, io: Io, timeout: Timeout) AwaitConcurrentError!void { +// return io.vtable.batchAwaitConcurrent(io.userdata, batch, timeout); +// } + +// /// Requests all pending operations to be interrupted, then waits for all +// /// pending operations to complete. After this returns, the `Batch` is in a +// /// well-defined state, ready to be iterated with `next`. Successfully +// /// canceled operations will be absent from the iteration. Some operations +// /// may have successfully completed regardless of the cancel request and +// /// will appear in the iteration. +// pub fn cancel(batch: *Batch, io: Io) void { +// { // abort pending submissions +// var tail_index = batch.unused.tail; +// defer batch.unused.tail = tail_index; +// var index = batch.submitted.head; +// errdefer batch.submissions.head = index; +// while (index != .none) { +// const next_index = batch.storage[index.toIndex()].submission.node.next; +// switch (tail_index) { +// .none => batch.unused.head = index, +// else => batch.storage[tail_index.toIndex()].unused.next = index, +// } +// batch.storage[index.toIndex()] = .{ .unused = .{ .prev = tail_index, .next = .none } }; +// tail_index = index; +// index = next_index; +// } +// batch.submitted = .{ .head = .none, .tail = .none }; +// } +// io.vtable.batchCancel(io.userdata, batch); +// assert(batch.submitted.head == .none and batch.submitted.tail == .none); +// assert(batch.pending.head == .none and batch.pending.tail == .none); +// assert(batch.userdata == null); // that was the last chance to deallocate resources +// } +// }; + +pub const Limit = @import("std").Io.Limit; +// pub const Limit = enum(usize) { +// nothing = 0, +// unlimited = math.maxInt(usize), +// _, + +// /// `math.maxInt(usize)` is interpreted to mean `.unlimited`. +// pub fn limited(n: usize) Limit { +// return @enumFromInt(n); +// } + +// /// Any value grater than `math.maxInt(usize)` is interpreted to mean +// /// `.unlimited`. +// pub fn limited64(n: u64) Limit { +// return @enumFromInt(@min(n, math.maxInt(usize))); +// } + +// pub fn countVec(data: []const []const u8) Limit { +// var total: usize = 0; +// for (data) |d| total += d.len; +// return .limited(total); +// } + +// pub fn min(a: Limit, b: Limit) Limit { +// return @enumFromInt(@min(@intFromEnum(a), @intFromEnum(b))); +// } + +// pub fn max(a: Limit, b: Limit) Limit { +// if (a == .unlimited or b == .unlimited) { +// return .unlimited; +// } + +// return @enumFromInt(@max(@intFromEnum(a), @intFromEnum(b))); +// } + +// pub fn minInt(l: Limit, n: usize) usize { +// return @min(n, @intFromEnum(l)); +// } + +// pub fn minInt64(l: Limit, n: u64) usize { +// return @min(n, @intFromEnum(l)); +// } + +// pub fn slice(l: Limit, s: []u8) []u8 { +// return s[0..l.minInt(s.len)]; +// } + +// pub fn sliceConst(l: Limit, s: []const u8) []const u8 { +// return s[0..l.minInt(s.len)]; +// } + +// pub fn toInt(l: Limit) ?usize { +// return switch (l) { +// else => @intFromEnum(l), +// .unlimited => null, +// }; +// } + +// /// Reduces a slice to account for the limit, leaving room for one extra +// /// byte above the limit, allowing for the use case of differentiating +// /// between end-of-stream and reaching the limit. +// pub fn slice1(l: Limit, non_empty_buffer: []u8) []u8 { +// assert(non_empty_buffer.len >= 1); +// return non_empty_buffer[0..@min(@intFromEnum(l) +| 1, non_empty_buffer.len)]; +// } + +// pub fn nonzero(l: Limit) bool { +// return l != .nothing; +// } + +// /// Return a new limit reduced by `amount` or return `null` indicating +// /// limit would be exceeded. +// pub fn subtract(l: Limit, amount: usize) ?Limit { +// if (l == .unlimited) return .unlimited; +// if (amount > @intFromEnum(l)) return null; +// return @enumFromInt(@intFromEnum(l) - amount); +// } +// }; + +pub const Cancelable = error{ + /// Caller has requested the async operation to stop. + Canceled, +}; + +pub const UnexpectedError = error{ + /// The Operating System returned an undocumented error code. + /// + /// This error is in theory not possible, but it would be better + /// to handle this error than to invoke undefined behavior. + /// + /// When this error code is observed, it usually means the Zig Standard + /// Library needs a small patch to add the error code to the error set for + /// the respective function. + Unexpected, +}; + +pub const Clock = enum { + /// A settable system-wide clock that measures real (i.e. wall-clock) + /// time. This clock is affected by discontinuous jumps in the system + /// time (e.g., if the system administrator manually changes the + /// clock), and by frequency adjustments performed by NTP and similar + /// applications. + /// + /// This clock normally counts the number of seconds since 1970-01-01 + /// 00:00:00 Coordinated Universal Time (UTC) except that it ignores + /// leap seconds; near a leap second it is typically adjusted by NTP to + /// stay roughly in sync with UTC. + /// + /// Timestamps returned by implementations of this clock represent time + /// elapsed since 1970-01-01T00:00:00Z, the POSIX/Unix epoch, ignoring + /// leap seconds. This is colloquially known as "Unix time". If the + /// underlying OS uses a different epoch for native timestamps (e.g., + /// Windows, which uses 1601-01-01) they are translated accordingly. + real, + /// A nonsettable system-wide clock that represents time since some + /// unspecified point in the past. + /// + /// Monotonic: Guarantees that the time returned by consecutive calls + /// will not go backwards, but successive calls may return identical + /// (not-increased) time values. + /// + /// Not affected by discontinuous jumps in the system time (e.g., if + /// the system administrator manually changes the clock), but may be + /// affected by frequency adjustments. + /// + /// This clock expresses intent to **exclude time that the system is + /// suspended**. However, implementations may be unable to satisify + /// this, and may include that time. + /// + /// * On Linux, corresponds `CLOCK_MONOTONIC`. + /// * On macOS, corresponds to `CLOCK_UPTIME_RAW`. + awake, + /// Identical to `awake` except it expresses intent to **include time + /// that the system is suspended**, however, due to limitations it may + /// behave identically to `awake`. + /// + /// * On Linux, corresponds `CLOCK_BOOTTIME`. + /// * On macOS, corresponds to `CLOCK_MONOTONIC_RAW`. + boot, + /// Tracks the amount of CPU in user or kernel mode used by the calling + /// process. + cpu_process, + /// Tracks the amount of CPU in user or kernel mode used by the calling + /// thread. + cpu_thread, + + /// This function is not cancelable because it does not block. + /// + /// Resolution is determined by `resolution` which may be 0 if the + /// clock is unsupported. + /// + /// See also: + /// * `Clock.Timestamp.now` + pub fn now(clock: Clock, io: Io) Io.Timestamp { + return io.vtable.now(io.userdata, clock); + } + + pub const ResolutionError = error{ + ClockUnavailable, + Unexpected, + }; + + /// Reveals the granularity of `clock`. May be zero, indicating + /// unsupported clock. + pub fn resolution(clock: Clock, io: Io) ResolutionError!Io.Duration { + return io.vtable.clockResolution(io.userdata, clock); + } + + pub const Timestamp = struct { + raw: Io.Timestamp, + clock: Clock, + + /// This function is not cancelable because it does not block. + /// + /// Resolution is determined by `resolution` which may be 0 if + /// the clock is unsupported. + /// + /// See also: + /// * `Clock.now` + pub fn now(io: Io, clock: Clock) Clock.Timestamp { + return .{ + .raw = io.vtable.now(io.userdata, clock), + .clock = clock, + }; + } + + /// Sleeps until the timestamp arrives. + /// + /// See also: + /// * `Io.sleep` + /// * `Clock.Duration.sleep` + /// * `Timeout.sleep` + pub fn wait(t: Clock.Timestamp, io: Io) Cancelable!void { + return io.vtable.sleep(io.userdata, .{ .deadline = t }); + } + + pub fn durationTo(from: Clock.Timestamp, to: Clock.Timestamp) Clock.Duration { + assert(from.clock == to.clock); + return .{ + .raw = from.raw.durationTo(to.raw), + .clock = from.clock, + }; + } + + pub fn addDuration(from: Clock.Timestamp, duration: Clock.Duration) Clock.Timestamp { + assert(from.clock == duration.clock); + return .{ + .raw = from.raw.addDuration(duration.raw), + .clock = from.clock, + }; + } + + pub fn subDuration(from: Clock.Timestamp, duration: Clock.Duration) Clock.Timestamp { + assert(from.clock == duration.clock); + return .{ + .raw = from.raw.subDuration(duration.raw), + .clock = from.clock, + }; + } + + /// Resolution is determined by `resolution` which may be 0 if + /// the clock is unsupported. + pub fn fromNow(io: Io, duration: Clock.Duration) Clock.Timestamp { + return .{ + .clock = duration.clock, + .raw = duration.clock.now(io).addDuration(duration.raw), + }; + } + + /// Resolution is determined by `resolution` which may be 0 if + /// the clock is unsupported. + pub fn untilNow(timestamp: Clock.Timestamp, io: Io) Clock.Duration { + const now_ts = Clock.Timestamp.now(io, timestamp.clock); + return timestamp.durationTo(now_ts); + } + + /// Resolution is determined by `resolution` which may be 0 if + /// the clock is unsupported. + pub fn durationFromNow(timestamp: Clock.Timestamp, io: Io) Clock.Duration { + const now_ts = timestamp.clock.now(io); + return .{ + .clock = timestamp.clock, + .raw = now_ts.durationTo(timestamp.raw), + }; + } + + /// Resolution is determined by `resolution` which may be 0 if + /// the clock is unsupported. + pub fn toClock(t: Clock.Timestamp, io: Io, clock: Clock) Clock.Timestamp { + if (t.clock == clock) return t; + const now_old = t.clock.now(io); + const now_new = clock.now(io); + const duration = now_old.durationTo(t); + return .{ + .clock = clock, + .raw = now_new.addDuration(duration), + }; + } + + pub fn compare(lhs: Clock.Timestamp, op: math.CompareOperator, rhs: Clock.Timestamp) bool { + assert(lhs.clock == rhs.clock); + return math.compare(lhs.raw.nanoseconds, op, rhs.raw.nanoseconds); + } + }; + + pub const Duration = struct { + raw: Io.Duration, + clock: Clock, + + /// Waits until a specified amount of time has passed on `clock`. + /// + /// See also: + /// * `Io.sleep` + /// * `Clock.Timestamp.wait` + /// * `Timeout.sleep` + pub fn sleep(duration: Clock.Duration, io: Io) Cancelable!void { + return io.vtable.sleep(io.userdata, .{ .duration = duration }); + } + }; +}; + +pub const Timestamp = struct { + nanoseconds: i96, + + pub fn now(io: Io, clock: Clock) Io.Timestamp { + return io.vtable.now(io.userdata, clock); + } + + pub const zero: Timestamp = .{ .nanoseconds = 0 }; + + pub fn durationTo(from: Timestamp, to: Timestamp) Duration { + return .{ .nanoseconds = to.nanoseconds - from.nanoseconds }; + } + + pub fn addDuration(from: Timestamp, duration: Duration) Timestamp { + return .{ .nanoseconds = from.nanoseconds + duration.nanoseconds }; + } + + pub fn subDuration(from: Timestamp, duration: Duration) Timestamp { + return .{ .nanoseconds = from.nanoseconds - duration.nanoseconds }; + } + + pub fn withClock(t: Timestamp, clock: Clock) Clock.Timestamp { + return .{ .raw = t, .clock = clock }; + } + + pub fn fromNanoseconds(x: i96) Timestamp { + return .{ .nanoseconds = x }; + } + + pub fn toMicroseconds(t: Timestamp) i64 { + return @intCast(@divTrunc(t.nanoseconds, std.time.ns_per_us)); + } + + pub fn toMilliseconds(t: Timestamp) i64 { + return @intCast(@divTrunc(t.nanoseconds, std.time.ns_per_ms)); + } + + pub fn toSeconds(t: Timestamp) i64 { + return @intCast(@divTrunc(t.nanoseconds, std.time.ns_per_s)); + } + + pub fn toNanoseconds(t: Timestamp) i96 { + return t.nanoseconds; + } + + pub fn formatNumber(t: Timestamp, w: *std.Io.Writer, n: std.fmt.Number) std.Io.Writer.Error!void { + return w.printInt(t.nanoseconds, n.mode.base() orelse 10, n.case, .{ + .precision = n.precision, + .width = n.width, + .alignment = n.alignment, + .fill = n.fill, + }); + } + + /// Resolution is determined by `Clock.resolution` which may be 0 if + /// the clock is unsupported. + pub fn untilNow(t: Timestamp, io: Io, clock: Clock) Duration { + const now_ts = clock.now(io); + return t.durationTo(now_ts); + } +}; + +pub const Duration = struct { + nanoseconds: i96, + + pub const zero: Duration = .{ .nanoseconds = 0 }; + pub const max: Duration = .{ .nanoseconds = math.maxInt(i96) }; + + pub fn fromNanoseconds(x: i96) Duration { + return .{ .nanoseconds = x }; + } + + pub fn fromMicroseconds(x: i64) Duration { + return .{ .nanoseconds = @as(i96, x) * std.time.ns_per_us }; + } + + pub fn fromMilliseconds(x: i64) Duration { + return .{ .nanoseconds = @as(i96, x) * std.time.ns_per_ms }; + } + + pub fn fromSeconds(x: i64) Duration { + return .{ .nanoseconds = @as(i96, x) * std.time.ns_per_s }; + } + + pub fn toMicroseconds(d: Duration) i64 { + return @intCast(@divTrunc(d.nanoseconds, std.time.ns_per_us)); + } + + pub fn toMilliseconds(d: Duration) i64 { + return @intCast(@divTrunc(d.nanoseconds, std.time.ns_per_ms)); + } + + pub fn toSeconds(d: Duration) i64 { + return @intCast(@divTrunc(d.nanoseconds, std.time.ns_per_s)); + } + + pub fn toNanoseconds(d: Duration) i96 { + return d.nanoseconds; + } + + /// Write number of nanoseconds according to its signed magnitude: + /// `[#y][#w][#d][#h][#m]#[.###][n|u|m]s` + pub fn format(duration: Duration, w: *Writer) Writer.Error!void { + if (duration.nanoseconds < 0) try w.writeByte('-'); + return formatUnsigned(w, @abs(duration.nanoseconds)); + } + + fn formatUnsigned(w: *Writer, ns: u96) Writer.Error!void { + var ns_remaining = ns; + inline for (.{ + .{ .ns = 365 * std.time.ns_per_day, .sep = 'y' }, + .{ .ns = std.time.ns_per_week, .sep = 'w' }, + .{ .ns = std.time.ns_per_day, .sep = 'd' }, + .{ .ns = std.time.ns_per_hour, .sep = 'h' }, + .{ .ns = std.time.ns_per_min, .sep = 'm' }, + }) |unit| { + if (ns_remaining >= unit.ns) { + const units = ns_remaining / unit.ns; + try w.printInt(units, 10, .lower, .{}); + try w.writeByte(unit.sep); + ns_remaining -= units * unit.ns; + if (ns_remaining == 0) return; + } + } + + inline for (.{ + .{ .ns = std.time.ns_per_s, .sep = "s" }, + .{ .ns = std.time.ns_per_ms, .sep = "ms" }, + .{ .ns = std.time.ns_per_us, .sep = "us" }, + }) |unit| { + const kunits = ns_remaining * 1000 / unit.ns; + if (kunits >= 1000) { + try w.printInt(kunits / 1000, 10, .lower, .{}); + const frac = kunits % 1000; + if (frac > 0) { + // Write up to 3 decimal places + var decimal_buf = [_]u8{ '.', 0, 0, 0 }; + var inner: Writer = .fixed(decimal_buf[1..]); + inner.printInt(frac, 10, .lower, .{ .fill = '0', .width = 3 }) catch unreachable; + var end: usize = 4; + while (end > 1) : (end -= 1) { + if (decimal_buf[end - 1] != '0') break; + } + try w.writeAll(decimal_buf[0..end]); + } + return w.writeAll(unit.sep); + } + } + + try w.printInt(ns_remaining, 10, .lower, .{}); + try w.writeAll("ns"); + } + + test format { + try testFormat("0ns", 0); + try testFormat("1ns", 1); + try testFormat("-1ns", -(1)); + try testFormat("999ns", std.time.ns_per_us - 1); + try testFormat("-999ns", -(std.time.ns_per_us - 1)); + try testFormat("1us", std.time.ns_per_us); + try testFormat("-1us", -(std.time.ns_per_us)); + try testFormat("1.45us", 1450); + try testFormat("-1.45us", -(1450)); + try testFormat("1.5us", 3 * std.time.ns_per_us / 2); + try testFormat("-1.5us", -(3 * std.time.ns_per_us / 2)); + try testFormat("14.5us", 14500); + try testFormat("-14.5us", -(14500)); + try testFormat("145us", 145000); + try testFormat("-145us", -(145000)); + try testFormat("999.999us", std.time.ns_per_ms - 1); + try testFormat("-999.999us", -(std.time.ns_per_ms - 1)); + try testFormat("1ms", std.time.ns_per_ms + 1); + try testFormat("-1ms", -(std.time.ns_per_ms + 1)); + try testFormat("1.5ms", 3 * std.time.ns_per_ms / 2); + try testFormat("-1.5ms", -(3 * std.time.ns_per_ms / 2)); + try testFormat("1.11ms", 1110000); + try testFormat("-1.11ms", -(1110000)); + try testFormat("1.111ms", 1111000); + try testFormat("-1.111ms", -(1111000)); + try testFormat("1.111ms", 1111100); + try testFormat("-1.111ms", -(1111100)); + try testFormat("999.999ms", std.time.ns_per_s - 1); + try testFormat("-999.999ms", -(std.time.ns_per_s - 1)); + try testFormat("1s", std.time.ns_per_s); + try testFormat("-1s", -(std.time.ns_per_s)); + try testFormat("59.999s", std.time.ns_per_min - 1); + try testFormat("-59.999s", -(std.time.ns_per_min - 1)); + try testFormat("1m", std.time.ns_per_min); + try testFormat("-1m", -(std.time.ns_per_min)); + try testFormat("1h", std.time.ns_per_hour); + try testFormat("-1h", -(std.time.ns_per_hour)); + try testFormat("1d", std.time.ns_per_day); + try testFormat("-1d", -(std.time.ns_per_day)); + try testFormat("1w", std.time.ns_per_week); + try testFormat("-1w", -(std.time.ns_per_week)); + try testFormat("1y", 365 * std.time.ns_per_day); + try testFormat("-1y", -(365 * std.time.ns_per_day)); + try testFormat("1y52w23h59m59.999s", 730 * std.time.ns_per_day - 1); // 365d = 52w1d + try testFormat("-1y52w23h59m59.999s", -(730 * std.time.ns_per_day - 1)); // 365d = 52w1d + try testFormat("1y1h1.001s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms); + try testFormat("-1y1h1.001s", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms)); + try testFormat("1y1h1s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us); + try testFormat("-1y1h1s", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us)); + try testFormat("1y1h999.999us", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1); + try testFormat("-1y1h999.999us", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1)); + try testFormat("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms); + try testFormat("-1y1h1ms", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms)); + try testFormat("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1); + try testFormat("-1y1h1ms", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1)); + try testFormat("1y1m999ns", 365 * std.time.ns_per_day + std.time.ns_per_min + 999); + try testFormat("-1y1m999ns", -(365 * std.time.ns_per_day + std.time.ns_per_min + 999)); + try testFormat("292y24w3d23h47m16.854s", std.math.maxInt(i64)); + try testFormat("-292y24w3d23h47m16.854s", std.math.minInt(i64) + 1); + try testFormat("-292y24w3d23h47m16.854s", std.math.minInt(i64)); + } + + fn testFormat(expected: []const u8, input: i96) !void { + // worst case: "-XXXXXXXXXXXXXyXXwXXdXXhXXmXX.XXXs".len = 34 + var buf: [34]u8 = undefined; + var w: Writer = .fixed(&buf); + try w.print("{f}", .{Duration{ .nanoseconds = input }}); + try std.testing.expectEqualStrings(expected, w.buffered()); + } +}; + +pub const AnyFuture = opaque {}; + +pub const ConcurrentError = error{ + /// May occur due to a temporary condition such as resource exhaustion, or + /// to the Io implementation not supporting concurrency. + ConcurrencyUnavailable, +}; + +// Implementation of the Io vtable for pre-0.16 using std.net.Stream. +const std15 = @import("std"); + +pub const legacy_vtable: VTable = .{ + .now = legacyNow, + .netRead = legacyNetRead, + .netWrite = legacyNetWrite, + .netClose = legacyNetClose, + .netShutdown = legacyNetShutdown, +}; + +pub fn legacyIo() Io { + return .{ .userdata = null, .vtable = &legacy_vtable }; +} + +fn toStream(handle: net.Socket.Handle) std15.net.Stream { + return .{ .handle = handle }; +} + +fn legacyNow(_: ?*anyopaque, clock: Clock) Timestamp { + const Clock2 = enum { awake, boot, cpu_process, cpu_thread }; + const clock2: Clock2 = switch (clock) { + .real => return .{ .nanoseconds = @intCast(std15.time.nanoTimestamp()) }, + inline else => |c| @field(Clock2, @tagName(c)), + }; + if (builtin.os.tag == .windows) { + switch (clock2) { + .awake, .boot => { + const qpc = std.os.windows.QueryPerformanceCounter(); + const qpf = std.os.windows.QueryPerformanceFrequency(); + return .{ .nanoseconds = @divFloor(@as(i96, qpc) * std15.time.ns_per_s, @as(i96, qpf)) }; + }, + .cpu_process, .cpu_thread => @panic("cpu clocks not implemented on windows"), + } + } else { + const posix = std15.posix; + const is_darwin = switch (builtin.os.tag) { + .macos, .ios, .tvos, .watchos, .visionos => true, + else => false, + }; + const clock_id: posix.clockid_t = switch (clock2) { + .awake => if (is_darwin) posix.CLOCK.UPTIME_RAW else posix.CLOCK.MONOTONIC, + .boot => if (is_darwin) posix.CLOCK.MONOTONIC_RAW else posix.CLOCK.BOOTTIME, + .cpu_process => posix.CLOCK.PROCESS_CPUTIME_ID, + .cpu_thread => posix.CLOCK.THREAD_CPUTIME_ID, + }; + const ts = posix.clock_gettime(clock_id) catch @panic("clock_gettime failed"); + const ns = @as(i96, ts.sec) * std15.time.ns_per_s + @as(i96, ts.nsec); + return .{ .nanoseconds = ns }; + } +} + +fn legacyNetRead(_: ?*anyopaque, src: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { + if (builtin.os.tag == .windows) { + // ReadFile doesn't work with WSA_FLAG_OVERLAPPED sockets created by posix.socket; use WSARecv. + const windows = std.os.windows; + const ws2_32 = windows.ws2_32; + var bufs: [8]ws2_32.WSABUF = undefined; + var count: u32 = 0; + for (data) |d| { + if (d.len > 0 and count < bufs.len) { + bufs[count] = .{ .len = @intCast(d.len), .buf = d.ptr }; + count += 1; + } + } + if (count == 0) return 0; + var n: u32 = undefined; + var flags: u32 = 0; + var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED); + if (ws2_32.WSARecv(src, &bufs, count, &n, &flags, &overlapped, null) == ws2_32.SOCKET_ERROR) switch (ws2_32.WSAGetLastError()) { + .WSA_IO_PENDING => { + var result_flags: u32 = undefined; + if (ws2_32.WSAGetOverlappedResult(src, &overlapped, &n, windows.TRUE, &result_flags) == windows.FALSE) switch (ws2_32.WSAGetLastError()) { + .WSAECONNRESET, .WSAECONNABORTED, .WSAENETRESET => return error.ConnectionResetByPeer, + else => return error.Unexpected, + }; + }, + .WSAECONNRESET, .WSAECONNABORTED, .WSAENETRESET => return error.ConnectionResetByPeer, + else => return error.Unexpected, + }; + return n; + } + return toStream(src).readv(@ptrCast(data)) catch |err| switch (err) { + error.ConnectionResetByPeer => return error.ConnectionResetByPeer, + error.Unexpected => return error.Unexpected, + else => return error.Unexpected, + }; +} + +fn legacyNetWrite(_: ?*anyopaque, dest: net.Socket.Handle, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize { + _ = splat; + if (builtin.os.tag == .windows) { + // WriteFile doesn't work with WSA_FLAG_OVERLAPPED sockets created by posix.socket; use WSASend. + const windows = std.os.windows; + const ws2_32 = windows.ws2_32; + var bufs: [9]ws2_32.WSABUF = undefined; + var count: u32 = 0; + if (header.len > 0) { + bufs[count] = .{ .len = @intCast(header.len), .buf = @constCast(header.ptr) }; + count += 1; + } + for (data) |d| { + if (d.len > 0 and count < bufs.len) { + bufs[count] = .{ .len = @intCast(d.len), .buf = @constCast(d.ptr) }; + count += 1; + } + } + if (count == 0) return 0; + var n: u32 = undefined; + var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED); + if (ws2_32.WSASend(dest, &bufs, count, &n, 0, &overlapped, null) == ws2_32.SOCKET_ERROR) switch (ws2_32.WSAGetLastError()) { + .WSA_IO_PENDING => { + var result_flags: u32 = undefined; + if (ws2_32.WSAGetOverlappedResult(dest, &overlapped, &n, windows.TRUE, &result_flags) == windows.FALSE) switch (ws2_32.WSAGetLastError()) { + .WSAECONNRESET, .WSAECONNABORTED, .WSAENETRESET => return error.ConnectionResetByPeer, + else => return error.Unexpected, + }; + }, + .WSAECONNRESET, .WSAECONNABORTED, .WSAENETRESET => return error.ConnectionResetByPeer, + else => return error.Unexpected, + }; + return n; + } + var iovecs: [9]std15.posix.iovec_const = undefined; + var count: usize = 0; + if (header.len > 0) { + iovecs[count] = .{ .base = header.ptr, .len = header.len }; + count += 1; + } + for (data) |d| { + if (d.len > 0 and count < iovecs.len) { + iovecs[count] = .{ .base = d.ptr, .len = d.len }; + count += 1; + } + } + return toStream(dest).writev(@ptrCast(iovecs[0..count])) catch |err| switch (err) { + error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, + error.Unexpected => return error.Unexpected, + else => return error.Unexpected, + }; +} + +fn legacyNetClose(_: ?*anyopaque, handles: []const net.Socket.Handle) void { + for (handles) |h| { + toStream(h).close(); + } +} + +fn legacyNetShutdown(_: ?*anyopaque, handle: net.Socket.Handle, how: net.ShutdownHow) net.ShutdownError!void { + std15.posix.shutdown(handle, @enumFromInt(@intFromEnum(how))) catch |err| switch (err) { + error.Unexpected => return error.Unexpected, + else => return error.Unexpected, + }; +} diff --git a/std16/src/Io/Dir.zig b/std16/src/Io/Dir.zig new file mode 100644 index 0000000..39bd853 --- /dev/null +++ b/std16/src/Io/Dir.zig @@ -0,0 +1,37 @@ +const Dir = @This(); + +const std = @import("../std.zig"); +const Io = std.Io; +const File = Io.File; + +handle: Handle, + +pub const Handle = std.posix.fd_t; + +fn legacy(dir: Dir) std.fs.Dir { + return .{ .fd = dir.handle }; +} + +pub fn cwd() Dir { + return .{ .handle = std.fs.cwd().fd }; +} + +pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: std.fs.File.OpenFlags) std.fs.File.OpenError!File { + _ = io; + return .{ .handle = (try dir.legacy().openFile(sub_path, flags)).handle }; +} + +pub fn readFileAlloc( + dir: Dir, + io: Io, + sub_path: []const u8, + gpa: std.mem.Allocator, + limit: Io.Limit, +) ![]u8 { + _ = io; + const max_bytes: usize = switch (limit) { + .unlimited => std.math.maxInt(usize), + else => @intCast(@intFromEnum(limit)), + }; + return dir.legacy().readFileAlloc(gpa, sub_path, max_bytes); +} diff --git a/std16/src/Io/File.zig b/std16/src/Io/File.zig new file mode 100644 index 0000000..a4f9e28 --- /dev/null +++ b/std16/src/Io/File.zig @@ -0,0 +1,56 @@ +const File = @This(); + +const std = @import("../std.zig"); +const Io = std.Io; + +handle: Handle, + +pub const Handle = std.posix.fd_t; + +pub const Reader = std.fs.File.Reader; +pub const Writer = std.fs.File.Writer; + +fn legacy(dir: File) std.fs.File { + return .{ .handle = dir.handle }; +} + +pub fn stdout() File { + return .{ .handle = std.fs.File.stdout().handle }; +} + +pub fn stderr() File { + return .{ .handle = std.fs.File.stderr().handle }; +} + +pub const StatError = error{ + SystemResources, + /// In WASI, this error may occur when the file descriptor does + /// not hold the required rights to get its filestat information. + AccessDenied, + PermissionDenied, + /// Attempted to stat a non-file stream. + Streaming, +} || Io.Cancelable || Io.UnexpectedError; + +pub const OpenError = std.fs.File.OpenError; + +pub fn close(file: File, io: Io) void { + _ = io; + file.legacy().close(); +} + +pub const SeekError = error{ + Unseekable, + /// The file descriptor does not hold the required rights to seek on it. + AccessDenied, +} || Io.Cancelable || Io.UnexpectedError; + +pub fn reader(file: File, io: Io, buffer: []u8) Reader { + _ = io; + return file.legacy().reader(buffer); +} + +pub fn writer(file: File, io: Io, buffer: []u8) Writer { + _ = io; + return file.legacy().writer(buffer); +} diff --git a/std16/src/Io/net.zig b/std16/src/Io/net.zig new file mode 100644 index 0000000..71c43a9 --- /dev/null +++ b/std16/src/Io/net.zig @@ -0,0 +1,1350 @@ +const std = @import("../std.zig"); +const Io = std.Io; +const assert = std.debug.assert; + +/// Source of truth: Internet Assigned Numbers Authority (IANA) +pub const Protocol = enum(u32) { + hopopts = 0, + icmp = 1, + igmp = 2, + ipip = 4, + tcp = 6, + egp = 8, + pup = 12, + udp = 17, + idp = 22, + tp = 29, + dccp = 33, + ipv6 = 41, + routing = 43, + fragment = 44, + rsvp = 46, + gre = 47, + esp = 50, + ah = 51, + icmpv6 = 58, + none = 59, + dstopts = 60, + mtp = 92, + beetph = 94, + encap = 98, + pim = 103, + comp = 108, + sctp = 132, + mh = 135, + udplite = 136, + mpls = 137, + ethernet = 143, + raw = 255, + mptcp = 262, + _, +}; + +// /// Windows 10 added support for unix sockets in build 17063, redstone 4 is the +// /// first release to support them. +// pub const has_unix_sockets = switch (native_os) { +// .windows => builtin.os.version_range.windows.isAtLeast(.win10_rs4) orelse false, +// .wasi => false, +// else => true, +// }; + +pub const default_kernel_backlog = 128; + +pub const IpAddress = union(enum) { + ip4: Ip4Address, + ip6: Ip6Address, + + pub const Family = @typeInfo(IpAddress).@"union".tag_type.?; + + pub const ParseLiteralError = error{ InvalidAddress, InvalidPort }; + + /// Parse an IP address which may include a port. + /// + /// For IPv4, this is written `address:port`. + /// + /// For IPv6, RFC 3986 defines this as an "IP literal", and the port is + /// differentiated from the address by surrounding the address part in + /// brackets "[addr]:port". Even if the port is not given, the brackets are + /// mandatory. + pub fn parseLiteral(text: []const u8) ParseLiteralError!IpAddress { + if (text.len == 0) return error.InvalidAddress; + if (text[0] == '[') { + const addr_end = std.mem.findScalar(u8, text, ']') orelse + return error.InvalidAddress; + const addr_text = text[1..addr_end]; + const port: u16 = p: { + if (addr_end == text.len - 1) break :p 0; + if (text[addr_end + 1] != ':') return error.InvalidAddress; + break :p std.fmt.parseInt(u16, text[addr_end + 2 ..], 10) catch return error.InvalidPort; + }; + return parseIp6(addr_text, port) catch error.InvalidAddress; + } + if (std.mem.findScalar(u8, text, ':')) |i| { + const addr = Ip4Address.parse(text[0..i], 0) catch return error.InvalidAddress; + return .{ .ip4 = .{ + .bytes = addr.bytes, + .port = std.fmt.parseInt(u16, text[i + 1 ..], 10) catch return error.InvalidPort, + } }; + } + return parseIp4(text, 0) catch error.InvalidAddress; + } + + /// Parse the given IP address string into an `IpAddress` value. + /// + /// This is a pure function but it cannot handle IPv6 addresses that have + /// scope ids ("%foo" at the end). To also handle those, `resolve` must be + /// called instead. + pub fn parse(text: []const u8, port: u16) !IpAddress { + if (parseIp4(text, port)) |ip4| return ip4 else |err| switch (err) { + error.Overflow, + error.InvalidEnd, + error.InvalidCharacter, + error.Incomplete, + error.NonCanonical, + => {}, + } + + return parseIp6(text, port); + } + + pub fn parseIp4(text: []const u8, port: u16) Ip4Address.ParseError!IpAddress { + return .{ .ip4 = try Ip4Address.parse(text, port) }; + } + + /// This is a pure function but it cannot handle IPv6 addresses that have + /// scope ids ("%foo" at the end). To also handle those, `resolveIp6` must be + /// called instead. + pub fn parseIp6(text: []const u8, port: u16) Ip6Address.ParseError!IpAddress { + return .{ .ip6 = try Ip6Address.parse(text, port) }; + } + + /// This function requires an `Io` parameter because it must query the operating + /// system to convert interface name to index. For example, in + /// "fe80::e0e:76ff:fed4:cf22%eno1", "eno1" must be resolved to an index by + /// creating a socket and then using an `ioctl` syscall. + /// + /// For a pure function that cannot handle scopes, see `parse`. + pub fn resolve(io: Io, text: []const u8, port: u16) !IpAddress { + if (parseIp4(text, port)) |ip4| return ip4 else |err| switch (err) { + error.Overflow, + error.InvalidEnd, + error.InvalidCharacter, + error.Incomplete, + error.NonCanonical, + => {}, + } + + return resolveIp6(io, text, port); + } + + pub fn resolveIp6(io: Io, text: []const u8, port: u16) Ip6Address.ResolveError!IpAddress { + return .{ .ip6 = try Ip6Address.resolve(io, text, port) }; + } + + /// Returns the port in native endian. + pub fn getPort(a: IpAddress) u16 { + return switch (a) { + inline .ip4, .ip6 => |x| x.port, + }; + } + + /// `port` is native-endian. + pub fn setPort(a: *IpAddress, port: u16) void { + switch (a.*) { + .ip4 => a.ip4.port = port, + .ip6 => a.ip6.port = port, + } + } + + /// Converts from a pre-0.16 std.net.Address. + pub fn fromStdAddress(addr: *const std.net.Address) IpAddress { + return switch (addr.any.family) { + std.posix.AF.INET => .{ .ip4 = .{ + .bytes = @bitCast(addr.in.sa.addr), + .port = std.mem.bigToNative(u16, addr.in.sa.port), + } }, + std.posix.AF.INET6 => .{ .ip6 = .{ + .bytes = addr.in6.sa.addr, + .port = std.mem.bigToNative(u16, addr.in6.sa.port), + .flowinfo = std.mem.bigToNative(u32, addr.in6.sa.flowinfo), + .scope_id = std.mem.bigToNative(u32, addr.in6.sa.scope_id), + } }, + else => unreachable, + }; + } + + /// Converts to a pre-0.16 std.net.Address for use with legacy APIs. + pub fn toStdAddress(self: IpAddress) @import("std").net.Address { + return switch (self) { + .ip4 => |ip4| .{ .in = .{ .sa = .{ + .port = @import("std").mem.nativeToBig(u16, ip4.port), + .addr = @bitCast(ip4.bytes), + } } }, + .ip6 => |ip6| .{ .in6 = .{ .sa = .{ + .port = @import("std").mem.nativeToBig(u16, ip6.port), + .flowinfo = @import("std").mem.nativeToBig(u32, ip6.flowinfo), + .addr = ip6.bytes, + .scope_id = @import("std").mem.nativeToBig(u32, ip6.scope_id), + } } }, + }; + } + + /// Converts from an IPv4-mapped IPv6 address, or returns the IPv6 address directly. + pub fn fromIp6(ip6: Ip6Address) IpAddress { + return if (Ip4Address.fromIp6(ip6)) |ip4| .{ .ip4 = ip4 } else .{ .ip6 = ip6 }; + } + + /// Includes the optional scope ("%foo" at the end) in IPv6 addresses. + /// + /// See `format` for an alternative that omits scopes and does + /// not require an `Io` parameter. + pub fn formatResolved(a: IpAddress, io: Io, w: *Io.Writer) Ip6Address.FormatError!void { + switch (a) { + .ip4 => |x| return x.format(w), + .ip6 => |x| return x.formatResolved(io, w), + } + } + + /// See `formatResolved` for an alternative that additionally prints the optional + /// scope at the end of IPv6 addresses and requires an `Io` parameter. + pub fn format(a: IpAddress, w: *Io.Writer) Io.Writer.Error!void { + switch (a) { + inline .ip4, .ip6 => |x| return x.format(w), + } + } + + pub fn eql(a: *const IpAddress, b: *const IpAddress) bool { + return switch (a.*) { + .ip4 => |a_ip4| switch (b.*) { + .ip4 => |b_ip4| a_ip4.eql(b_ip4), + else => false, + }, + .ip6 => |a_ip6| switch (b.*) { + .ip6 => |b_ip6| a_ip6.eql(b_ip6), + else => false, + }, + }; + } + + pub const ListenError = error{ + /// The address is already taken. Can occur when bound port is 0 but + /// all ephemeral ports are already in use. + AddressInUse, + /// A nonexistent interface was requested or the requested address was not local. + AddressUnavailable, + /// The local network interface used to reach the destination is offline. + NetworkDown, + /// Insufficient memory or other resource internal to the operating system. + SystemResources, + /// Per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// System-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + /// The requested address family (IPv4 or IPv6) is not supported by the operating system. + AddressFamilyUnsupported, + ProtocolUnsupportedBySystem, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, + /// One of the `ListenOptions` is not supported by the Io + /// implementation. + OptionUnsupported, + } || Io.UnexpectedError || Io.Cancelable; + + pub const ListenOptions = struct { + /// How many connections the kernel will accept on the application's behalf. + /// If more than this many connections pool in the kernel, clients will start + /// seeing "Connection refused". + kernel_backlog: u31 = default_kernel_backlog, + /// Sets SO_REUSEADDR and SO_REUSEPORT on POSIX. + /// Sets SO_REUSEADDR on Windows, which is roughly equivalent. + reuse_address: bool = false, + /// Only connection-oriented modes may be used here, which includes: + /// * `Socket.Mode.stream` + /// * `Socket.Mode.seqpacket` + mode: Socket.Mode = .stream, + /// Only connection-oriented protocols may be used here, which includes: + /// * `Protocol.tcp` + /// * `Protocol.tp` + /// * `Protocol.dccp` + /// * `Protocol.sctp` + protocol: Protocol = .tcp, + }; + + /// Waits for a TCP connection. When using this API, `bind` does not need + /// to be called. The returned `Server` has an open `stream`. + // pub fn listen(address: *const IpAddress, io: Io, options: ListenOptions) ListenError!Server { + // return .{ + // .socket = try io.vtable.netListenIp(io.userdata, address, options), + // .options = if (Server.AcceptOptions != void) .{ + // .mode = options.mode, + // .protocol = options.protocol, + // }, + // }; + // } + + pub const BindError = error{ + /// The address is already taken. Can occur when bound port is 0 but + /// all ephemeral ports are already in use. + AddressInUse, + /// A nonexistent interface was requested or the requested address was not local. + AddressUnavailable, + /// The address is not valid for the address family of socket. + AddressFamilyUnsupported, + /// Insufficient memory or other resource internal to the operating system. + SystemResources, + /// The local network interface used to reach the destination is offline. + NetworkDown, + ProtocolUnsupportedBySystem, + ProtocolUnsupportedByAddressFamily, + /// Per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// System-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + SocketModeUnsupported, + /// One of the `BindOptions` is not supported by the Io + /// implementation. + OptionUnsupported, + } || Io.UnexpectedError || Io.Cancelable; + + pub const BindOptions = struct { + /// The socket is restricted to sending and receiving IPv6 packets only. + /// In this case, an IPv4 and an IPv6 application can bind to a single port + /// at the same time. + ip6_only: bool = false, + /// Allow the socket to send datagrams to broadcast addresses. + /// When not enabled any attempt to send datagrams to a broadcast address + /// will fail with `error.AccessDenied` + allow_broadcast: bool = false, + mode: Socket.Mode, + protocol: ?Protocol = null, + }; + + /// Associates an address with a `Socket` which can be used to receive UDP + /// packets and other kinds of non-streaming messages. See `listen` for a + /// streaming alternative. + /// + /// One bound `Socket` can be used to receive messages from multiple + /// different addresses. + pub fn bind(address: *const IpAddress, io: Io, options: BindOptions) BindError!Socket { + return io.vtable.netBindIp(io.userdata, address, options); + } + + pub const ConnectError = error{ + AddressUnavailable, + AddressFamilyUnsupported, + /// Insufficient memory or other resource internal to the operating system. + SystemResources, + ConnectionPending, + ConnectionRefused, + ConnectionResetByPeer, + HostUnreachable, + NetworkUnreachable, + Timeout, + /// One of the `ConnectOptions` is not supported by the Io + /// implementation. + OptionUnsupported, + /// Per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// System-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + ProtocolUnsupportedBySystem, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, + /// The user tried to connect to a broadcast address without having the socket broadcast flag enabled or + /// the connection request failed because of a local firewall rule. + AccessDenied, + /// Non-blocking was requested and the operation cannot return immediately. + WouldBlock, + NetworkDown, + } || Io.Timeout.Error || Io.UnexpectedError || Io.Cancelable; + + pub const ConnectOptions = struct { + mode: Socket.Mode, + protocol: ?Protocol = null, + timeout: Io.Timeout = .none, + }; + + /// Initiates a connection-oriented network stream. + pub fn connect(address: *const IpAddress, io: Io, options: ConnectOptions) ConnectError!Stream { + return .{ .socket = try io.vtable.netConnectIp(io.userdata, address, options) }; + } +}; + +/// An IPv4 address in binary memory layout. +pub const Ip4Address = struct { + bytes: [4]u8, + port: u16, + + pub fn loopback(port: u16) Ip4Address { + return .{ + .bytes = .{ 127, 0, 0, 1 }, + .port = port, + }; + } + + pub fn unspecified(port: u16) Ip4Address { + return .{ + .bytes = .{ 0, 0, 0, 0 }, + .port = port, + }; + } + + /// Converts from an IPv4-mapped IPv6 address, or returns `null`. + pub fn fromIp6(ip6: Ip6Address) ?Ip4Address { + return if (std.mem.eql(u8, ip6.bytes[0..12], &.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) .{ + .bytes = ip6.bytes[12..].*, + .port = ip6.port, + } else null; + } + + /// Given an `IpAddress`, converts it to an `Ip4Address` directly, or from + /// an IPv4-mapped IPv6 address, or returns `null`. + pub fn fromAny(addr: IpAddress) ?Ip6Address { + return switch (addr) { + .ip4 => |ip4| ip4, + .ip6 => |ip6| fromIp6(ip6), + }; + } + + pub const ParseError = error{ + Overflow, + InvalidEnd, + InvalidCharacter, + Incomplete, + NonCanonical, + }; + + pub fn parse(buffer: []const u8, port: u16) ParseError!Ip4Address { + var bytes: [4]u8 = @splat(0); + var index: u8 = 0; + var saw_any_digits = false; + var has_zero_prefix = false; + for (buffer) |c| switch (c) { + '.' => { + if (!saw_any_digits) return error.InvalidCharacter; + if (index == 3) return error.InvalidEnd; + index += 1; + saw_any_digits = false; + has_zero_prefix = false; + }, + '0'...'9' => { + if (c == '0' and !saw_any_digits) { + has_zero_prefix = true; + } else if (has_zero_prefix) { + return error.NonCanonical; + } + saw_any_digits = true; + bytes[index] = try std.math.mul(u8, bytes[index], 10); + bytes[index] = try std.math.add(u8, bytes[index], c - '0'); + }, + else => return error.InvalidCharacter, + }; + if (index == 3 and saw_any_digits) return .{ + .bytes = bytes, + .port = port, + }; + return error.Incomplete; + } + + pub fn format(a: Ip4Address, w: *Io.Writer) Io.Writer.Error!void { + const bytes = &a.bytes; + try w.print("{d}.{d}.{d}.{d}:{d}", .{ bytes[0], bytes[1], bytes[2], bytes[3], a.port }); + } + + pub fn eql(a: Ip4Address, b: Ip4Address) bool { + const a_int: u32 = @bitCast(a.bytes); + const b_int: u32 = @bitCast(b.bytes); + return a.port == b.port and a_int == b_int; + } +}; + +/// An IPv6 address in binary memory layout. +pub const Ip6Address = struct { + /// Native endian + port: u16, + /// Big endian + bytes: [16]u8, + flow: u32 = 0, + interface: Interface = .none, + + pub const Policy = struct { + addr: [16]u8, + len: u8, + mask: u8, + prec: u8, + label: u8, + }; + + pub fn loopback(port: u16) Ip6Address { + return .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, + .port = port, + }; + } + + pub fn unspecified(port: u16) Ip6Address { + return .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + .port = port, + }; + } + + /// Constructs an IPv4-mapped IPv6 address. + pub fn fromIp4(ip4: Ip4Address) Ip6Address { + return .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff } ++ ip4.bytes, + .port = ip4.port, + }; + } + + /// Given an `IpAddress`, converts it to an `Ip6Address` directly, or via + /// constructing an IPv4-mapped IPv6 address. + pub fn fromAny(addr: IpAddress) Ip6Address { + return switch (addr) { + .ip4 => |ip4| fromIp4(ip4), + .ip6 => |ip6| ip6, + }; + } + + /// An IPv6 address but with `Interface` as a name rather than index. + pub const Unresolved = struct { + /// Big endian + bytes: [16]u8, + /// Has not been checked to be a valid native interface name. + /// Externally managed memory. + interface_name: ?[]const u8, + + pub const Parsed = union(enum) { + success: Unresolved, + invalid_byte: usize, + incomplete, + junk_after_end: usize, + interface_name_oversized: usize, + invalid_ip4_mapping: usize, + overflow: usize, + }; + + pub fn parse(text: []const u8) Parsed { + if (text.len < 2) return .incomplete; + const ip4_prefix = "::ffff:"; + if (std.ascii.startsWithIgnoreCase(text, ip4_prefix)) { + const parsed = Ip4Address.parse(text[ip4_prefix.len..], 0) catch + return .{ .invalid_ip4_mapping = ip4_prefix.len }; + const b = parsed.bytes; + return .{ .success = .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] }, + .interface_name = null, + } }; + } + // Has to be u16 elements to handle 3-digit hex numbers from compression. + var parts: [8]u16 = @splat(0); + var parts_i: u8 = 0; + var text_i: u8 = 0; + var digit_i: u8 = 0; + var compress_start: ?u8 = null; + var interface_name_text: ?[]const u8 = null; + const State = union(enum) { digit, end }; + state: switch (State.digit) { + .digit => c: switch (text[text_i]) { + 'a'...'f' => |c| { + const digit = c - 'a' + 10; + parts[parts_i] = (std.math.mul(u16, parts[parts_i], 16) catch return .{ + .overflow = text_i, + }) + digit; + if (digit_i == 4) return .{ .invalid_byte = text_i }; + digit_i += 1; + text_i += 1; + if (text.len - text_i == 0) { + parts_i += 1; + continue :state .end; + } + continue :c text[text_i]; + }, + 'A'...'F' => |c| continue :c c - 'A' + 'a', + '0'...'9' => |c| { + const digit = c - '0'; + parts[parts_i] = (std.math.mul(u16, parts[parts_i], 16) catch return .{ + .overflow = text_i, + }) + digit; + if (digit_i == 4) return .{ .invalid_byte = text_i }; + digit_i += 1; + text_i += 1; + if (text.len - text_i == 0) { + parts_i += 1; + continue :state .end; + } + continue :c text[text_i]; + }, + ':' => { + if (digit_i == 0) { + if (compress_start != null) return .{ .invalid_byte = text_i }; + if (text_i == 0) { + text_i += 1; + if (text[text_i] != ':') return .{ .invalid_byte = text_i }; + assert(parts_i == 0); + } + compress_start = parts_i; + text_i += 1; + if (text.len - text_i == 0) continue :state .end; + continue :c text[text_i]; + } else { + parts_i += 1; + if (parts.len - parts_i == 0) continue :state .end; + digit_i = 0; + text_i += 1; + if (text.len - text_i == 0) return .incomplete; + continue :c text[text_i]; + } + }, + '%' => { + if (digit_i == 0) return .{ .invalid_byte = text_i }; + parts_i += 1; + text_i += 1; + const name = text[text_i..]; + if (name.len == 0) return .incomplete; + interface_name_text = name; + text_i = @intCast(text.len); + continue :state .end; + }, + else => return .{ .invalid_byte = text_i }, + }, + .end => { + if (text.len - text_i != 0) return .{ .junk_after_end = text_i }; + const remaining = parts.len - parts_i; + if (compress_start) |s| { + const src = parts[s..parts_i]; + @memmove(parts[parts.len - src.len ..], src); + @memset(parts[s..][0..remaining], 0); + } else { + if (remaining != 0) return .incomplete; + } + + // Workaround that can be removed when this proposal is + // implemented https://github.com/ziglang/zig/issues/19755 + if ((comptime @import("builtin").cpu.arch.endian()) != .big) { + for (&parts) |*part| part.* = @byteSwap(part.*); + } + + return .{ .success = .{ + .bytes = @bitCast(parts), + .interface_name = interface_name_text, + } }; + }, + } + } + + pub fn format(u: *const Unresolved, w: *Io.Writer) Io.Writer.Error!void { + const bytes = &u.bytes; + if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) { + try w.print("::ffff:{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] }); + } else { + const parts: [8]u16 = .{ + std.mem.readInt(u16, bytes[0..2], .big), + std.mem.readInt(u16, bytes[2..4], .big), + std.mem.readInt(u16, bytes[4..6], .big), + std.mem.readInt(u16, bytes[6..8], .big), + std.mem.readInt(u16, bytes[8..10], .big), + std.mem.readInt(u16, bytes[10..12], .big), + std.mem.readInt(u16, bytes[12..14], .big), + std.mem.readInt(u16, bytes[14..16], .big), + }; + + // Find the longest zero run + var longest_start: usize = 8; + var longest_len: usize = 0; + var current_start: usize = 0; + var current_len: usize = 0; + + for (parts, 0..) |part, i| { + if (part == 0) { + if (current_len == 0) { + current_start = i; + } + current_len += 1; + if (current_len > longest_len) { + longest_start = current_start; + longest_len = current_len; + } + } else { + current_len = 0; + } + } + + // Only compress if the longest zero run is 2 or more + if (longest_len < 2) { + longest_start = 8; + longest_len = 0; + } + + var i: usize = 0; + while (parts.len - i != 0) : (i += 1) { + if (i == longest_start) { + // Emit "::" for the longest zero run + try w.writeAll(if (i == 0) "::" else ":"); + i += longest_len - 1; // Skip the compressed range + continue; + } + try w.print("{x}", .{parts[i]}); + if (i != parts.len - 1) { + try w.writeAll(":"); + } + } + } + if (u.interface_name) |n| try w.print("%{s}", .{n}); + } + }; + + pub const ParseError = error{ + /// If this is returned, more detailed diagnostics can be obtained by + /// calling `Ip6Address.Parsed.init`. + ParseFailed, + /// If this is returned, the IPv6 address had a scope id on it ("%foo" + /// at the end) which requires calling `resolve`. + UnresolvedScope, + }; + + /// This is a pure function but it cannot handle IPv6 addresses that have + /// scope ids ("%foo" at the end). To also handle those, `resolve` must be + /// called instead, or the lower level `Unresolved` API may be used. + pub fn parse(buffer: []const u8, port: u16) ParseError!Ip6Address { + switch (Unresolved.parse(buffer)) { + .success => |p| return .{ + .bytes = p.bytes, + .port = port, + .interface = if (p.interface_name != null) return error.UnresolvedScope else .none, + }, + else => return error.ParseFailed, + } + return .{ .ip6 = try Ip6Address.parse(buffer, port) }; + } + + pub const ResolveError = error{ + /// If this is returned, more detailed diagnostics can be obtained by + /// calling the `Parsed.init` function. + ParseFailed, + /// The interface name is longer than the host operating system supports. + NameTooLong, + } || Interface.Name.ResolveError; + + /// This function requires an `Io` parameter because it must query the operating + /// system to convert interface name to index. For example, in + /// "fe80::e0e:76ff:fed4:cf22%eno1", "eno1" must be resolved to an index by + /// creating a socket and then using an `ioctl` syscall. + pub fn resolve(io: Io, buffer: []const u8, port: u16) ResolveError!Ip6Address { + return switch (Unresolved.parse(buffer)) { + .success => |p| return .{ + .bytes = p.bytes, + .port = port, + .interface = i: { + const text = p.interface_name orelse break :i .none; + const name: Interface.Name = try .fromSlice(text); + break :i try name.resolve(io); + }, + }, + else => return error.ParseFailed, + }; + } + + pub const FormatError = Io.Writer.Error || Interface.NameError; + + /// Includes the optional scope ("%foo" at the end). + /// + /// See `format` for an alternative that omits scopes and does + /// not require an `Io` parameter. + pub fn formatResolved(a: *const Ip6Address, io: Io, w: *Io.Writer) FormatError!void { + const interface_name = if (a.interface.isNone()) null else try a.interface.name(io); + const u: Unresolved = .{ + .bytes = a.bytes, + .interface_name = if (interface_name) |name| name.toSlice() else null, + }; + try w.print("[{f}]:{d}", .{ u, a.port }); + } + + /// See `formatResolved` for an alternative that additionally prints the optional + /// scope at the end of addresses and requires an `Io` parameter. + pub fn format(a: *const Ip6Address, w: *Io.Writer) Io.Writer.Error!void { + const u: Unresolved = .{ .bytes = a.bytes, .interface_name = null }; + try w.print("[{f}]:{d}", .{ u, a.port }); + } + + pub fn eql(a: Ip6Address, b: Ip6Address) bool { + return a.port == b.port and std.mem.eql(u8, &a.bytes, &b.bytes); + } + + pub fn isMultiCast(a: Ip6Address) bool { + return a.bytes[0] == 0xff; + } + + pub fn isLinkLocal(a: Ip6Address) bool { + const b = &a.bytes; + return b[0] == 0xfe and (b[1] & 0xc0) == 0x80; + } + + pub fn isLoopBack(a: Ip6Address) bool { + const b = &a.bytes; + return b[0] == 0 and b[1] == 0 and + b[2] == 0 and + b[12] == 0 and b[13] == 0 and + b[14] == 0 and b[15] == 1; + } + + pub fn isSiteLocal(a: Ip6Address) bool { + const b = &a.bytes; + return b[0] == 0xfe and (b[1] & 0xc0) == 0xc0; + } + + pub fn policy(a: Ip6Address) *const Policy { + const b = &a.bytes; + for (&defined_policies) |*p| { + if (!std.mem.eql(u8, b[0..p.len], p.addr[0..p.len])) continue; + if ((b[p.len] & p.mask) != p.addr[p.len]) continue; + return p; + } + unreachable; + } + + pub fn scope(a: Ip6Address) u8 { + if (isMultiCast(a)) return a.bytes[1] & 15; + if (isLinkLocal(a)) return 2; + if (isLoopBack(a)) return 2; + if (isSiteLocal(a)) return 5; + return 14; + } + + const defined_policies = [_]Policy{ + .{ + .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01".*, + .len = 15, + .mask = 0xff, + .prec = 50, + .label = 0, + }, + .{ + .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00".*, + .len = 11, + .mask = 0xff, + .prec = 35, + .label = 4, + }, + .{ + .addr = "\x20\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, + .len = 1, + .mask = 0xff, + .prec = 30, + .label = 2, + }, + .{ + .addr = "\x20\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, + .len = 3, + .mask = 0xff, + .prec = 5, + .label = 5, + }, + .{ + .addr = "\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, + .len = 0, + .mask = 0xfe, + .prec = 3, + .label = 13, + }, + // These are deprecated and/or returned to the address + // pool, so despite the RFC, treating them as special + // is probably wrong. + // { "", 11, 0xff, 1, 3 }, + // { "\xfe\xc0", 1, 0xc0, 1, 11 }, + // { "\x3f\xfe", 1, 0xff, 1, 12 }, + // Last rule must match all addresses to stop loop. + .{ + .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, + .len = 0, + .mask = 0, + .prec = 40, + .label = 1, + }, + }; +}; + +pub const ReceiveFlags = packed struct(u8) { + oob: bool = false, + peek: bool = false, + trunc: bool = false, + _: u5 = 0, +}; + +pub const IncomingMessage = struct { + /// Populated by receive functions. + from: IpAddress, + /// Populated by receive functions, points into the caller-supplied buffer. + data: []u8, + /// Supplied by caller before calling receive functions; mutated by receive + /// functions. + control: []u8, + /// Populated by receive functions. + flags: Flags, + + /// Useful for initializing before calling `receiveManyTimeout`. + pub const init: IncomingMessage = .{ + .from = undefined, + .data = undefined, + .control = &.{}, + .flags = undefined, + }; + + pub const Flags = packed struct(u8) { + /// indicates end-of-record; the data returned completed a record + /// (generally used with sockets of type SOCK_SEQPACKET). + eor: bool, + /// indicates that the trailing portion of a datagram was discarded + /// because the datagram was larger than the buffer supplied. + trunc: bool, + /// indicates that some control data was discarded due to lack of + /// space in the buffer for ancil‐ lary data. + ctrunc: bool, + /// indicates expedited or out-of-band data was received. + oob: bool, + /// indicates that no data was received but an extended error from the + /// socket error queue. + errqueue: bool, + _: u3 = 0, + }; +}; + +pub const OutgoingMessage = struct { + address: *const IpAddress, + data_ptr: [*]const u8, + /// Initialized with how many bytes of `data_ptr` to send. After sending + /// succeeds, replaced with how many bytes were actually sent. + data_len: usize, + control: []const u8 = &.{}, +}; + +pub const SendFlags = packed struct(u8) { + confirm: bool = false, + dont_route: bool = false, + eor: bool = false, + oob: bool = false, + fastopen: bool = false, + _: u3 = 0, +}; + +pub const ShutdownHow = enum { recv, send, both }; + +pub const ShutdownError = error{ + ConnectionAborted, + ConnectionResetByPeer, + NetworkDown, + SocketUnconnected, + SystemResources, +} || error{Unexpected} || error{Cancelable}; + +pub const Interface = struct { + /// Value 0 indicates `none`. + index: u32, + + pub const none: Interface = .{ .index = 0 }; + + pub const Name = struct { + bytes: [max_len:0]u8, + + pub const max_len = if (@TypeOf(std.posix.IFNAMESIZE) == void) 0 else std.posix.IFNAMESIZE - 1; + + pub fn toSlice(n: *const Name) []const u8 { + return std.mem.sliceTo(&n.bytes, 0); + } + + pub fn fromSlice(bytes: []const u8) error{NameTooLong}!Name { + if (bytes.len > max_len) return error.NameTooLong; + return .fromSliceUnchecked(bytes); + } + + /// Asserts bytes.len fits in `max_len`. + pub fn fromSliceUnchecked(bytes: []const u8) Name { + assert(bytes.len <= max_len); + var result: Name = undefined; + @memcpy(result.bytes[0..bytes.len], bytes); + result.bytes[bytes.len] = 0; + return result; + } + + pub const ResolveError = error{ + InterfaceNotFound, + AccessDenied, + SystemResources, + } || Io.UnexpectedError || Io.Cancelable; + + /// Corresponds to "if_nametoindex" in libc. + pub fn resolve(n: *const Name, io: Io) ResolveError!Interface { + return io.vtable.netInterfaceNameResolve(io.userdata, n); + } + }; + + pub const NameError = error{ + /// Out of range `index`. + InterfaceNotFound, + /// Interface name longer than `Name.max_len`. + NameTooLong, + } || Io.UnexpectedError || Io.Cancelable; + + /// Asserts not `none`. + /// + /// Corresponds to "if_indextoname" in libc. + pub fn name(i: Interface, io: Io) NameError!Name { + assert(i.index != 0); + return io.vtable.netInterfaceName(io.userdata, i); + } + + pub fn isNone(i: Interface) bool { + return i.index == 0; + } +}; + +/// An open port with unspecified protocol. +pub const Socket = struct { + handle: Handle, + /// Contains the resolved ephemeral port number if requested. + address: IpAddress, + + pub const Mode = enum { + /// Provides sequenced, reliable, two-way, connection-based byte + /// streams. An out-of-band data transmission mechanism may be + /// supported. + stream, + /// Supports datagrams (connectionless, unreliable messages of a fixed + /// maximum length). + dgram, + /// Provides a sequenced, reliable, two-way connection-based data + /// transmission path for datagrams of fixed maximum length; a consumer + /// is required to read an entire packet with each input system call. + seqpacket, + /// Provides raw network protocol access. + raw, + /// Provides a reliable datagram layer that does not guarantee ordering. + rdm, + }; + + /// Underlying platform-defined type which may or may not be + /// interchangeable with a file system file descriptor. + pub const Handle = if (@import("builtin").os.tag == .windows) std.os.windows.ws2_32.SOCKET else std.posix.fd_t; + + /// Leaves `address` in a valid state. + pub fn close(s: *const Socket, io: Io) void { + io.vtable.netClose(io.userdata, (&s.handle)[0..1]); + } + + pub fn closeMany(io: Io, sockets: []const Socket) void { + io.vtable.netClose(io.userdata, sockets); + } + + pub const SendError = error{ + /// The socket type requires that message be sent atomically, and the + /// size of the message to be sent made this impossible. The message + /// was not transmitted, or was partially transmitted. + MessageOversize, + /// The output queue for a network interface was full. This generally indicates that the + /// interface has stopped sending, but may be caused by transient congestion. (Normally, + /// this does not occur in Linux. Packets are just silently dropped when a device queue + /// overflows.) + /// + /// This is also caused when there is not enough kernel memory available. + SystemResources, + /// No route to network. + NetworkUnreachable, + /// Network reached but no route to host. + HostUnreachable, + /// The local network interface used to reach the destination is offline. + NetworkDown, + /// The destination address is not listening. Can still occur for + /// connectionless messages. + ConnectionRefused, + /// Operating system or protocol does not support the address family. + AddressFamilyUnsupported, + /// Another TCP Fast Open is already in progress. + FastOpenAlreadyInProgress, + /// Network session was unexpectedly closed by recipient. + ConnectionResetByPeer, + /// Local end has been shut down on a connection-oriented socket, or + /// the socket was never connected. + SocketUnconnected, + /// An attempt was made to send to a network/broadcast address as + /// though it was a unicast address. + AccessDenied, + } || Io.UnexpectedError || Io.Cancelable; + + /// Transfers `data` to `dest`, connectionless, in one packet. + pub fn send(s: *const Socket, io: Io, dest: *const IpAddress, data: []const u8) SendError!void { + var message: OutgoingMessage = .{ .address = dest, .data_ptr = data.ptr, .data_len = data.len }; + const err, const n = io.vtable.netSend(io.userdata, s.handle, (&message)[0..1], .{}); + if (n != 1) return err.?; + if (message.data_len != data.len) return error.MessageOversize; + } + + pub fn sendMany(s: *const Socket, io: Io, messages: []OutgoingMessage, flags: SendFlags) SendError!void { + const err, const n = io.vtable.netSend(io.userdata, s.handle, messages, flags); + if (n != messages.len) return err.?; + } + + pub const ReceiveError = Io.Operation.NetReceive.Error || Io.Cancelable; + + /// Waits for data. Connectionless. + /// + /// See also: + /// * `receiveTimeout` + pub fn receive(s: *const Socket, io: Io, buffer: []u8) ReceiveError!IncomingMessage { + var message: IncomingMessage = .init; + const maybe_err, const count = (try io.operate(.{ .net_receive = .{ + .socket_handle = s.handle, + .message_buffer = (&message)[0..1], + .data_buffer = buffer, + .flags = .{}, + } })).net_receive; + if (maybe_err) |err| return err; + assert(1 == count); + return message; + } + + pub const ReceiveTimeoutError = ReceiveError || Io.Timeout.Error || Io.ConcurrentError; + + /// Waits for data. Connectionless. + /// + /// Returns `error.Timeout` if no message arrives early enough. + /// + /// See also: + /// * `receive` + /// * `receiveManyTimeout` + pub fn receiveTimeout( + s: *const Socket, + io: Io, + buffer: []u8, + timeout: Io.Timeout, + ) ReceiveTimeoutError!IncomingMessage { + var message: IncomingMessage = .init; + const maybe_err, const count = (try io.operateTimeout(.{ .net_receive = .{ + .socket_handle = s.handle, + .message_buffer = (&message)[0..1], + .data_buffer = buffer, + .flags = .{}, + } }, timeout)).net_receive; + if (maybe_err) |err| return err; + assert(1 == count); + return message; + } + + /// Waits until at least one message is delivered, possibly returning more + /// than one message. Connectionless. + /// + /// Returns number of messages received, or `error.Timeout` if no message + /// arrives early enough. + /// + /// See also: + /// * `receive` + /// * `receiveTimeout` + pub fn receiveManyTimeout( + s: *const Socket, + io: Io, + /// Function assumes each element has initialized `control` field. + /// Initializing with `IncomingMessage.init` may be helpful. + message_buffer: []IncomingMessage, + data_buffer: []u8, + flags: ReceiveFlags, + timeout: Io.Timeout, + ) struct { ?ReceiveTimeoutError, usize } { + const result = io.operateTimeout(.{ .net_receive = .{ + .socket_handle = s.handle, + .message_buffer = message_buffer, + .data_buffer = data_buffer, + .flags = flags, + } }, timeout) catch |err| return .{ err, 0 }; + return result.net_receive; + } + + pub const CreatePairError = error{ + OperationUnsupported, + AccessDenied, + AddressFamilyUnsupported, + ProtocolUnsupportedBySystem, + /// The per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// The system-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + /// Insufficient memory is available. The socket cannot be created + /// until sufficient resources are freed. + SystemResources, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, + } || Io.UnexpectedError || Io.Cancelable; + + pub const CreatePairOptions = struct { + family: IpAddress.Family = .ip4, + mode: Mode = .stream, + protocol: ?Protocol = null, + }; + + /// Create a set of two sockets that are connected to each other. + /// + /// Also known as "socketpair". + pub fn createPair(io: Io, options: CreatePairOptions) CreatePairError![2]Socket { + return io.vtable.netSocketCreatePair(io.userdata, options); + } +}; + +/// An open socket connection with a network protocol that guarantees +/// sequencing, delivery, and prevents repetition. Typically TCP or UNIX domain +/// socket. +pub const Stream = struct { + socket: Socket, + + const max_iovecs_len = 8; + + pub fn close(s: *const Stream, io: Io) void { + io.vtable.netClose(io.userdata, (&s.socket.handle)[0..1]); + } + + pub fn shutdown(s: *const Stream, io: Io, how: ShutdownHow) ShutdownError!void { + return io.vtable.netShutdown(io.userdata, s.socket.handle, how); + } + + pub const Reader = struct { + io: Io, + interface: Io.Reader, + stream: Stream, + err: ?Error, + + pub const Error = error{ + SystemResources, + ConnectionResetByPeer, + Timeout, + SocketUnconnected, + /// The file descriptor does not hold the required rights to read + /// from it. + AccessDenied, + NetworkDown, + } || error{Canceled} || error{Unexpected}; + + pub fn init(stream: Stream, io: Io, buffer: []u8) Reader { + return .{ + .io = io, + .interface = .{ + .vtable = &.{ + .stream = streamImpl, + .readVec = readVec, + }, + .buffer = buffer, + .seek = 0, + .end = 0, + }, + .stream = stream, + .err = null, + }; + } + + fn streamImpl(io_r: *Io.Reader, io_w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { + const dest = limit.slice(try io_w.writableSliceGreedy(1)); + var data: [1][]u8 = .{dest}; + const n = try readVec(io_r, &data); + io_w.advance(n); + return n; + } + + fn readVec(io_r: *Io.Reader, data: [][]u8) Io.Reader.Error!usize { + const r: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); + const io = r.io; + var iovecs_buffer: [max_iovecs_len][]u8 = undefined; + const dest_n, const data_size = try io_r.writableVector(&iovecs_buffer, data); + const dest = iovecs_buffer[0..dest_n]; + assert(dest[0].len > 0); + const n = io.vtable.netRead(io.userdata, r.stream.socket.handle, dest) catch |err| { + r.err = err; + return error.ReadFailed; + }; + if (n == 0) { + return error.EndOfStream; + } + if (n > data_size) { + r.interface.end += n - data_size; + return data_size; + } + return n; + } + }; + + pub const Writer = struct { + io: Io, + interface: Io.Writer, + stream: Stream, + err: ?Error = null, + write_file_err: ?WriteFileError = null, + + pub const Error = error{ + /// Another TCP Fast Open is already in progress. + FastOpenAlreadyInProgress, + /// Network session was unexpectedly closed by recipient. + ConnectionResetByPeer, + /// The output queue for a network interface was full. This generally indicates that the + /// interface has stopped sending, but may be caused by transient congestion. (Normally, + /// this does not occur in Linux. Packets are just silently dropped when a device queue + /// overflows.) + /// + /// This is also caused when there is not enough kernel memory available. + SystemResources, + /// No route to network. + NetworkUnreachable, + /// Network reached but no route to host. + HostUnreachable, + /// The local network interface used to reach the destination is down. + NetworkDown, + /// The destination address is not listening. + ConnectionRefused, + /// The passed address didn't have the correct address family in its sa_family field. + AddressFamilyUnsupported, + /// Local end has been shut down on a connection-oriented socket, or + /// the socket was never connected. + SocketUnconnected, + SocketNotBound, + } || Io.UnexpectedError || Io.Cancelable; + + pub const WriteFileError = error{ + NetworkDown, + } || Io.Cancelable || Io.UnexpectedError; + + pub fn init(stream: Stream, io: Io, buffer: []u8) Writer { + return .{ + .io = io, + .stream = stream, + .interface = .{ + .vtable = &.{ + .drain = drain, + .sendFile = sendFile, + }, + .buffer = buffer, + }, + }; + } + + fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { + const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); + const io = w.io; + const buffered = io_w.buffered(); + const handle = w.stream.socket.handle; + const n = io.vtable.netWrite(io.userdata, handle, buffered, data, splat) catch |err| { + w.err = err; + return error.WriteFailed; + }; + return io_w.consume(n); + } + + fn sendFile(io_w: *Io.Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize { + _ = io_w; + _ = file_reader; + _ = limit; + return error.Unimplemented; // TODO + } + }; + + pub fn reader(stream: Stream, io: Io, buffer: []u8) Reader { + return .init(stream, io, buffer); + } + + pub fn writer(stream: Stream, io: Io, buffer: []u8) Writer { + return .init(stream, io, buffer); + } +}; diff --git a/std16/src/process.zig b/std16/src/process.zig new file mode 100644 index 0000000..cb1ccce --- /dev/null +++ b/std16/src/process.zig @@ -0,0 +1,4 @@ +pub const Args = @import("process/Args.zig"); +pub const Environ = @import("process/Environ.zig"); + +pub const getEnvVarOwned = @import("std").process.getEnvVarOwned; diff --git a/std16/src/process/Args.zig b/std16/src/process/Args.zig new file mode 100644 index 0000000..e17e67c --- /dev/null +++ b/std16/src/process/Args.zig @@ -0,0 +1 @@ +pub const Iterator = @import("std").process.ArgIterator; diff --git a/std16/src/process/Environ.zig b/std16/src/process/Environ.zig new file mode 100644 index 0000000..e6ed3bc --- /dev/null +++ b/std16/src/process/Environ.zig @@ -0,0 +1,14 @@ +const Environ = @This(); + +const std = @import("../std.zig"); + +pub fn getPosix(e: Environ, key: []const u8) ?[:0]const u8 { + _ = e; + return std.posix.getenv(key); +} + +pub const GetAllocError = error{ OutOfMemory, InvalidWtf8, EnvironmentVariableNotFound }; +pub fn getAlloc(e: Environ, gpa: std.mem.Allocator, key: []const u8) GetAllocError![]u8 { + _ = e; + return std.process.getEnvVarOwned(gpa, key); +} diff --git a/std16/src/std.zig b/std16/src/std.zig new file mode 100644 index 0000000..d631d23 --- /dev/null +++ b/std16/src/std.zig @@ -0,0 +1,20 @@ +comptime { + const zig_atleast_16 = @import("builtin").zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) != .lt; + if (zig_atleast_16) @compileError("this module should only be used on zig 0.15"); +} + +pub const Io = @import("Io.zig"); + +pub const debug = @import("std").debug; +pub const fs = @import("std").fs; +pub const math = @import("std").math; +pub const mem = @import("std").mem; +pub const os = @import("std").os; +pub const posix = @import("std").posix; +pub const process = @import("process.zig"); +pub const testing = @import("std").testing; +pub const time = @import("std").time; + +test { + testing.refAllDecls(@This()); +}