From ddba1c0ac4a368adab78678f74a232f1223c7e64 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Sun, 28 Jul 2024 18:16:38 -0500 Subject: [PATCH 1/4] Add support for XTEST extension (Zig 0.11.0) Conflicts: common.zig --- common.zig | 75 ++++++++++++++++++++++++ testexample.zig | 151 ++++++++++++++++++++++++++++++++++++++--------- x.zig | 1 + xtest.zig | 153 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 352 insertions(+), 28 deletions(-) create mode 100644 xtest.zig diff --git a/common.zig b/common.zig index 94a46e0..441d857 100644 --- a/common.zig +++ b/common.zig @@ -4,6 +4,17 @@ const common = @This(); pub const SocketReader = std.io.Reader(std.posix.socket_t, std.posix.RecvFromError, readSocket); +/// Sanity check that we're not running into data integrity (corruption) issues caused +/// by overflowing and wrapping around to the front ofq the buffer. +fn checkMessageLengthFitsInBuffer(message_length: usize, buffer_limit: usize) !void { + if(message_length > buffer_limit) { + std.debug.panic("Reply is bigger than our buffer (data corruption will ensue) {} > {}. In order to fix, increase the buffer size.", .{ + message_length, + buffer_limit, + }); + } +} + pub fn send(sock: std.posix.socket_t, data: []const u8) !void { const sent = try x.writeSock(sock, data, 0); if (sent != data.len) { @@ -186,3 +197,67 @@ pub fn asReply(comptime T: type, msg_bytes: []align(4) u8) !*T { fn readSocket(sock: std.posix.socket_t, buffer: []u8) !usize { return x.readSock(sock, buffer, 0); } + +/// X server extension info. +pub const ExtensionInfo = struct { + extension_name: []const u8, + /// The extension opcode is used to identify which X extension a given request is + /// intended for (used as the major opcode). This essentially namespaces any extension + /// requests. The extension differentiates its own requests by using a minor opcode. + opcode: u8, + /// Extension error codes are added on top of this base error code. + base_error_code: u8, +}; + +pub const ExtensionVersion = struct { + major_version: u16, + minor_version: u16, +}; + +/// Determines whether the extension is available on the server. +pub fn getExtensionInfo( + sock: std.os.socket_t, + buffer: *x.ContiguousReadBuffer, + comptime extension_name: []const u8, +) !?ExtensionInfo { + const reader = common.SocketReader{ .context = sock }; + const buffer_limit = buffer.half_len; + + { + const ext_name = comptime x.Slice(u16, [*]const u8).initComptime(extension_name); + var message_buffer: [x.query_extension.getLen(ext_name.len)]u8 = undefined; + x.query_extension.serialize(&message_buffer, ext_name); + try common.send(sock, &message_buffer); + } + const message_length = try x.readOneMsg(reader, @alignCast(buffer.nextReadBuffer())); + try checkMessageLengthFitsInBuffer(message_length, buffer_limit); + const optional_extension = blk: { + switch (x.serverMsgTaggedUnion(@alignCast(buffer.double_buffer_ptr))) { + .reply => |msg_reply| { + const msg: *x.ServerMsg.QueryExtension = @ptrCast(msg_reply); + if (msg.present == 0) { + std.log.info("{s} extension: not present", .{extension_name}); + break :blk null; + } + std.debug.assert(msg.present == 1); + std.log.info("{s} extension: opcode={} base_error_code={}", .{ + extension_name, + msg.major_opcode, + msg.first_error, + }); + std.log.info("{s} extension: {}", .{ extension_name, msg }); + break :blk ExtensionInfo{ + .extension_name = extension_name, + .opcode = msg.major_opcode, + .base_error_code = msg.first_error, + }; + }, + else => |msg| { + std.log.err("expected a reply for `x.query_extension` but got {}", .{msg}); + return error.ExpectedReplyButGotSomethingElse; + }, + } + }; + + return optional_extension; +} diff --git a/testexample.zig b/testexample.zig index 35738a8..a3126c9 100644 --- a/testexample.zig +++ b/testexample.zig @@ -211,52 +211,85 @@ pub fn main() !u8 { }; - { - const ext_name = comptime x.Slice(u16, [*]const u8).initComptime("RENDER"); - var msg: [x.query_extension.getLen(ext_name.len)]u8 = undefined; - x.query_extension.serialize(&msg, ext_name); - try conn.send(&msg); - } - _ = try x.readOneMsg(conn.reader(), @alignCast(buf.nextReadBuffer())); - const opt_render_ext: ?struct { opcode: u8 } = blk: { + const opt_render_ext = try common.getExtensionInfo( + conn.sock, + &buf, + "RENDER" + ); + if (opt_render_ext) |render_ext| { + const expected_version: common.ExtensionVersion = .{ .major_version = 0, .minor_version = 11 }; + { + var msg: [x.render.query_version.len]u8 = undefined; + x.render.query_version.serialize(&msg, render_ext.opcode, .{ + .major_version = expected_version.major_version, + .minor_version = expected_version.minor_version, + }); + try conn.send(&msg); + } + _ = try x.readOneMsg(conn.reader(), @alignCast(buf.nextReadBuffer())); switch (x.serverMsgTaggedUnion(@alignCast(buf.double_buffer_ptr))) { .reply => |msg_reply| { - const msg: *x.ServerMsg.QueryExtension = @ptrCast(msg_reply); - if (msg.present == 0) { - std.log.info("RENDER extension: not present", .{}); - break :blk null; + const msg: *x.render.query_version.Reply = @ptrCast(msg_reply); + std.log.info("X RENDER extension: version {}.{}", .{msg.major_version, msg.minor_version}); + if (msg.major_version != expected_version.major_version) { + std.log.err("X RENDER extension major version is {} but we expect {}", .{ + msg.major_version, + expected_version.major_version, + }); + return 1; + } + if (msg.minor_version < expected_version.minor_version) { + std.log.err("X RENDER extension minor version is {}.{} but I've only tested >= {}.{})", .{ + msg.major_version, + msg.minor_version, + expected_version.major_version, + expected_version.minor_version, + }); + return 1; } - std.debug.assert(msg.present == 1); - std.log.info("RENDER extension: opcode={}", .{msg.major_opcode}); - break :blk .{ .opcode = msg.major_opcode }; }, else => |msg| { std.log.err("expected a reply but got {}", .{msg}); return 1; }, - } - }; - if (opt_render_ext) |render_ext| { + } + + const opt_test_ext = try common.getExtensionInfo( + conn.sock, + &buf, + "XTEST" + ); + if (opt_test_ext) |test_ext| { + const expected_version: common.ExtensionVersion = .{ .major_version = 2, .minor_version = 2 }; { - var msg: [x.render.query_version.len]u8 = undefined; - x.render.query_version.serialize(&msg, render_ext.opcode, .{ - .major_version = 0, - .minor_version = 11, + var msg: [x.testext.get_version.len]u8 = undefined; + x.testext.get_version.serialize(&msg, .{ + .ext_opcode = test_ext.opcode, + .wanted_major_version = expected_version.major_version, + .wanted_minor_version = expected_version.minor_version, }); try conn.send(&msg); } _ = try x.readOneMsg(conn.reader(), @alignCast(buf.nextReadBuffer())); switch (x.serverMsgTaggedUnion(@alignCast(buf.double_buffer_ptr))) { .reply => |msg_reply| { - const msg: *x.render.query_version.Reply = @ptrCast(msg_reply); - std.log.info("RENDER extension: version {}.{}", .{msg.major_version, msg.minor_version}); - if (msg.major_version != 0) { - std.log.err("xrender extension major version {} too new", .{msg.major_version}); + const msg: *x.testext.get_version.Reply = @ptrCast(msg_reply); + std.log.info("XTEST extension: version {}.{}", .{msg.major_version, msg.minor_version}); + if (msg.major_version != expected_version.major_version) { + std.log.err("XTEST extension major version is {} but we expect {}", .{ + msg.major_version, + expected_version.major_version, + }); return 1; } - if (msg.minor_version < 11) { - std.log.err("xrender extension minor version {} too old", .{msg.minor_version}); + if (msg.minor_version < expected_version.minor_version) { + std.log.err("XTEST extension minor version is {}.{} but I've only tested >= {}.{})", .{ + msg.major_version, + msg.minor_version, + expected_version.major_version, + expected_version.minor_version, + }); return 1; } }, @@ -273,6 +306,35 @@ pub fn main() !u8 { try conn.send(&msg); } + // Send a fake mouse left-click event + if (opt_test_ext) |test_ext| { + { + var msg: [x.testext.fake_input.len]u8 = undefined; + x.testext.fake_input.serialize(&msg, test_ext.opcode, .{ + .button_press = .{ + .event_type = x.testext.FakeEventType.button_press, + .detail = 1, + .delay_ms = 0, + .device_id = null, + }, + }); + try conn.send(&msg); + } + + { + var msg: [x.testext.fake_input.len]u8 = undefined; + x.testext.fake_input.serialize(&msg, test_ext.opcode, .{ + .button_press = .{ + .event_type = x.testext.FakeEventType.button_release, + .detail = 1, + .delay_ms = 0, + .device_id = null, + }, + }); + try conn.send(&msg); + } + } + while (true) { { const recv_buf = buf.nextReadBuffer(); @@ -529,6 +591,39 @@ fn render( } } + // Draw a representation of the click-through area + { + try changeGcColor(sock, ids.fg_gc(), x.rgb24To(0x963b3b, depth)); + { + const rectangles = [_]x.Rectangle{ + .{ .x = 0, .y = window_height - 50, .width = window_width, .height = 50 }, + }; + var msg: [x.poly_fill_rectangle.getLen(rectangles.len)]u8 = undefined; + x.poly_fill_rectangle.serialize(&msg, .{ + .drawable_id = ids.window(), + .gc_id = ids.fg_gc(), + }, &rectangles); + try common.send(sock, &msg); + } + + try changeGcColor(sock, ids.fg_gc(), x.rgb24To(0xffaadd, depth)); + { + const text_literal: []const u8 = "Click-through area!"; + const text = x.Slice(u8, [*]const u8) { .ptr = text_literal.ptr, .len = text_literal.len }; + var msg: [x.image_text8.getLen(text.len)]u8 = undefined; + + const text_width = font_dims.width * text_literal.len; + + x.image_text8.serialize(&msg, text, .{ + .drawable_id = ids.window(), + .gc_id = ids.fg_gc(), + .x = @divTrunc((window_width - @as(i16, @intCast(text_width))), 2) + font_dims.font_left, + .y = window_height - 50 + @divTrunc(50 - @as(i16, @intCast(font_dims.height)), 2) + font_dims.font_ascent, + }); + try common.send(sock, &msg); + } + } + } fn changeGcColor(sock: std.posix.socket_t, gc_id: u32, color: u32) !void { diff --git a/x.zig b/x.zig index 9c62213..6591549 100644 --- a/x.zig +++ b/x.zig @@ -35,6 +35,7 @@ const windows = std.os.windows; pub const inputext = @import("xinputext.zig"); pub const render = @import("xrender.zig"); pub const dbe = @import("xdbe.zig"); +pub const testext = @import("xtest.zig"); // Expose some helpful stuff pub const MappedFile = @import("MappedFile.zig"); diff --git a/xtest.zig b/xtest.zig new file mode 100644 index 0000000..4d98c6c --- /dev/null +++ b/xtest.zig @@ -0,0 +1,153 @@ +const std = @import("std"); + +const x = @import("x.zig"); + + +pub const ExtOpcode = enum(u8) { + get_version = 0, + // compare_cursor = 1, + fake_input = 2, + // grab_control = 3, +}; + +pub const FakeEventType = enum(u8) { + key_press = 2, + key_release = 3, + button_press = 4, + button_release = 5, + motion_notify = 6, +}; + +/// Whether the `x_position`/`y_position` fields are relative or absolute +pub const PositionType = enum(u8) { + absolute = 0, + relative = 1, +}; + +pub const get_version = struct { + pub const len = + 2 // extension and command opcodes + + 2 // request length + + 1 // wanted major version + + 1 // unused + + 2 // wanted minor version + ; + pub const Args = struct { + ext_opcode: u8, + wanted_major_version: u8, + wanted_minor_version: u16, + }; + pub fn serialize(buf: [*]u8, args: Args) void { + buf[0] = args.ext_opcode; + buf[1] = @intFromEnum(ExtOpcode.get_version); + comptime { std.debug.assert(len & 0x3 == 0); } + x.writeIntNative(u16, buf + 2, len >> 2); + buf[4] = args.wanted_major_version; + buf[5] = 0; // unused + x.writeIntNative(u16, buf + 6, args.wanted_minor_version); + } + + comptime { std.debug.assert(@sizeOf(Reply) == 32); } + pub const Reply = extern struct { + response_type: x.ReplyKind, + major_version: u8, + sequence: u16, + word_len: u32, // length in 4-byte words + minor_version: u16, + unused_pad: [22]u8, + }; +}; + +/// Simulate input events (key presses, button presses, mouse motion). +/// Can also simulate raw device events if `device_id` is specified. +pub const fake_input = struct { + pub const len = + 2 // extension and command opcodes + + 2 // request length + + 1 // fake event type + + 1 // detail + + 2 // unused + + 4 // delay + + 4 // root window ID for MotionNotify + + 8 // unused + + 2 // x position for MotionNotify + + 2 // y position for MotionNotify + + 1 // device id + + 7 // unused + ; + /// Simulate a key or button press/release + pub const KeyOrButtonPressArgs = struct { + event_type: FakeEventType, + detail: u8, + delay_ms: u32, + device_id: ?u8, + }; + /// Simulate mouse motion + /// + /// Raw device motion events (using `device_id`) are not supported yet since we + /// would also need to send the variable-length state of the axes + pub const MouseNotifyArgs = struct { + event_type: FakeEventType, + detail: PositionType, + delay_ms: u32, + /// This field is the ID of the root window on which the new motion is to take + /// place. If None (0) is specified, the root window of the screen the pointer is + /// currently on is used instead. If this field is not a valid window, then a + /// Window error occurs. + root_window_id: u32, + /// These fields indicate relative distance or absolute pointer coordinates, + /// according to the setting of detail. If the specified coordinates are + /// off-screen, the closest on-screen coordinates will be substituted. + x_position: i16, + y_position: i16, + // device_id: ?u8, + }; + pub const Args = union(enum) { + key_press: KeyOrButtonPressArgs, + button_press: KeyOrButtonPressArgs, + mouse_notify: MouseNotifyArgs, + }; + pub fn serialize(buf: [*]u8, ext_opcode: u8, args: Args) void { + buf[0] = ext_opcode; + buf[1] = @intFromEnum(ExtOpcode.fake_input); + x.writeIntNative(u16, buf + 2, len >> 2); + + switch (args) { + .key_press, .button_press => |args_value| { + buf[4] = @intFromEnum(args_value.event_type); + buf[5] = args_value.detail; + // unused + x.writeIntNative(u16, buf + 6, 0); + x.writeIntNative(u32, buf + 8, args_value.delay_ms); + // unused root window ID + x.writeIntNative(u32, buf + 12, 0); + // unused + x.writeIntNative(u64, buf + 16, 0); + // unused x position + x.writeIntNative(i16, buf + 24, 0); + // unused y position + x.writeIntNative(i16, buf + 26, 0); + buf[28] = args_value.device_id orelse 0; + // unused + x.writeIntNative(u56, buf + 29, 0); + }, + .mouse_notify => |args_value| { + buf[4] = @intFromEnum(args_value.event_type); + buf[5] = @intFromEnum(args_value.detail); + // unused + x.writeIntNative(u16, buf + 6, 0); + x.writeIntNative(u32, buf + 8, args_value.delay_ms); + x.writeIntNative(u32, buf + 12, args_value.root_window_id); + // unused + x.writeIntNative(u64, buf + 16, 0); + x.writeIntNative(i16, buf + 24, args_value.x_position); + x.writeIntNative(i16, buf + 26, args_value.y_position); + // device ID is not supported yet + buf[28] = 0; + // unused + x.writeIntNative(u56, buf + 29, 0); + } + } + + } +}; From 923a576f2720902ba3b604601146544c2cea554d Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 23 Sep 2024 18:39:59 -0500 Subject: [PATCH 2/4] Zig 0.13.0 upgrade --- common.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.zig b/common.zig index 441d857..f8dee5a 100644 --- a/common.zig +++ b/common.zig @@ -216,7 +216,7 @@ pub const ExtensionVersion = struct { /// Determines whether the extension is available on the server. pub fn getExtensionInfo( - sock: std.os.socket_t, + sock: std.posix.socket_t, buffer: *x.ContiguousReadBuffer, comptime extension_name: []const u8, ) !?ExtensionInfo { From 485601139f505a297273c7c773c3094150ad661d Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 23 Sep 2024 18:40:13 -0500 Subject: [PATCH 3/4] Get rid of shape extension changes --- testexample.zig | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/testexample.zig b/testexample.zig index a3126c9..dc9bea3 100644 --- a/testexample.zig +++ b/testexample.zig @@ -590,40 +590,6 @@ fn render( try common.send(sock, &msg); } } - - // Draw a representation of the click-through area - { - try changeGcColor(sock, ids.fg_gc(), x.rgb24To(0x963b3b, depth)); - { - const rectangles = [_]x.Rectangle{ - .{ .x = 0, .y = window_height - 50, .width = window_width, .height = 50 }, - }; - var msg: [x.poly_fill_rectangle.getLen(rectangles.len)]u8 = undefined; - x.poly_fill_rectangle.serialize(&msg, .{ - .drawable_id = ids.window(), - .gc_id = ids.fg_gc(), - }, &rectangles); - try common.send(sock, &msg); - } - - try changeGcColor(sock, ids.fg_gc(), x.rgb24To(0xffaadd, depth)); - { - const text_literal: []const u8 = "Click-through area!"; - const text = x.Slice(u8, [*]const u8) { .ptr = text_literal.ptr, .len = text_literal.len }; - var msg: [x.image_text8.getLen(text.len)]u8 = undefined; - - const text_width = font_dims.width * text_literal.len; - - x.image_text8.serialize(&msg, text, .{ - .drawable_id = ids.window(), - .gc_id = ids.fg_gc(), - .x = @divTrunc((window_width - @as(i16, @intCast(text_width))), 2) + font_dims.font_left, - .y = window_height - 50 + @divTrunc(50 - @as(i16, @intCast(font_dims.height)), 2) + font_dims.font_ascent, - }); - try common.send(sock, &msg); - } - } - } fn changeGcColor(sock: std.posix.socket_t, gc_id: u32, color: u32) !void { From 20030d8dc4527432ea13b9982b00ff55b5e4ff60 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 23 Sep 2024 18:41:26 -0500 Subject: [PATCH 4/4] Don't run fake events in example by default --- testexample.zig | 54 ++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/testexample.zig b/testexample.zig index dc9bea3..9acde31 100644 --- a/testexample.zig +++ b/testexample.zig @@ -307,33 +307,33 @@ pub fn main() !u8 { } // Send a fake mouse left-click event - if (opt_test_ext) |test_ext| { - { - var msg: [x.testext.fake_input.len]u8 = undefined; - x.testext.fake_input.serialize(&msg, test_ext.opcode, .{ - .button_press = .{ - .event_type = x.testext.FakeEventType.button_press, - .detail = 1, - .delay_ms = 0, - .device_id = null, - }, - }); - try conn.send(&msg); - } - - { - var msg: [x.testext.fake_input.len]u8 = undefined; - x.testext.fake_input.serialize(&msg, test_ext.opcode, .{ - .button_press = .{ - .event_type = x.testext.FakeEventType.button_release, - .detail = 1, - .delay_ms = 0, - .device_id = null, - }, - }); - try conn.send(&msg); - } - } + // if (opt_test_ext) |test_ext| { + // { + // var msg: [x.testext.fake_input.len]u8 = undefined; + // x.testext.fake_input.serialize(&msg, test_ext.opcode, .{ + // .button_press = .{ + // .event_type = x.testext.FakeEventType.button_press, + // .detail = 1, + // .delay_ms = 0, + // .device_id = null, + // }, + // }); + // try conn.send(&msg); + // } + + // { + // var msg: [x.testext.fake_input.len]u8 = undefined; + // x.testext.fake_input.serialize(&msg, test_ext.opcode, .{ + // .button_press = .{ + // .event_type = x.testext.FakeEventType.button_release, + // .detail = 1, + // .delay_ms = 0, + // .device_id = null, + // }, + // }); + // try conn.send(&msg); + // } + // } while (true) { {