xserver/test/pyxtest/proto/xi.py
Peter Hutterer b9ed4bd4c0 pyxtest: add tests for XI property data byte-swap fix
Add tests for commit b243ef9bc2 ("Xi: Swap property data in
SProcXChangeDeviceProperty/SProcXIChangeProperty").

Both tests set a format=32 property from a byte-swapped client and
read it back, verifying the values round-trip correctly. Without the
property data swap, the stored values have the wrong byte order.

Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2187>
2026-05-10 23:42:44 +00:00

373 lines
9.5 KiB
Python

# SPDX-License-Identifier: MIT
#
# XI/XI2 (XInput) extension protocol request builders
import struct
from dataclasses import dataclass
# XI (v1) minor opcodes
XChangeDeviceControl = 35
XChangeDeviceProperty = 37
XGetDeviceProperty = 39
# XI2 minor opcodes
XIQueryVersion = 47
XIPassiveGrabDevice = 54
XIPassiveUngrabDevice = 55
XIChangeProperty = 57
XIGetProperty = 59
XI2_MAJOR = 2
XI2_MINOR = 4
# Grab types
XIGrabtypeButton = 0
XIGrabtypeKeycode = 1
XIGrabtypeEnter = 2
XIGrabtypeFocusIn = 3
# Grab modes
XIGrabModeSync = 0
XIGrabModeAsync = 1
# Special values
XIAllDevices = 0
XIAllMasterDevices = 1
XIAnyModifier = 1 << 31
# Grab status codes (returned as X11 error codes when used as
# ProcXIPassiveGrabDevice return values)
XIAlreadyGrabbed = 1
# Property modes
PropModeReplace = 0
PropModePrepend = 1
PropModeAppend = 2
# Virtual core device IDs (always present)
VirtualCorePointer = 2
VirtualCoreKeyboard = 3
# Device control types (for XChangeDeviceControl)
DEVICE_RESOLUTION = 1
DEVICE_ABS_CALIB = 2
DEVICE_ABS_AREA = 3
DEVICE_CORE = 4
DEVICE_ENABLE = 5
@dataclass
class XIQueryVersionRequest:
"""XIQueryVersion request."""
opcode: int
major: int = XI2_MAJOR
minor: int = XI2_MINOR
def to_bytes(self, byte_order: str = "<") -> bytes:
return struct.pack(
f"{byte_order}BBHHH",
self.opcode,
XIQueryVersion,
2, # 8 bytes = 2 words
self.major,
self.minor,
)
@dataclass
class XIPassiveGrabDeviceRequest:
"""XIPassiveGrabDevice request."""
opcode: int
grab_window: int
detail: int
deviceid: int = XIAllMasterDevices
grab_type: int = XIGrabtypeButton
grab_mode: int = XIGrabModeAsync
paired_device_mode: int = XIGrabModeAsync
owner_events: bool = False
mask: bytes = b"\x00" * 4
modifiers: list[int] | None = None
def to_bytes(self, byte_order: str = "<") -> bytes:
mods = self.modifiers if self.modifiers is not None else [XIAnyModifier]
num_modifiers = len(mods)
mask_padded = self.mask + b"\x00" * ((4 - len(self.mask) % 4) % 4)
mask_len = len(mask_padded) // 4
# Header: 32 bytes, mask: mask_len*4, modifiers: num_modifiers*4
total = 32 + len(mask_padded) + num_modifiers * 4
length = total // 4
header = struct.pack(
f"{byte_order}BBH IIII HHH BBBB H",
self.opcode,
XIPassiveGrabDevice,
length,
0, # time = CurrentTime
self.grab_window,
0, # cursor = None
self.detail,
self.deviceid,
num_modifiers,
mask_len,
self.grab_type,
self.grab_mode,
self.paired_device_mode,
1 if self.owner_events else 0,
0, # pad
)
mod_data = b""
for mod in mods:
mod_data += struct.pack(f"{byte_order}I", mod)
return header + mask_padded + mod_data
@dataclass
class XIPassiveUngrabDeviceRequest:
"""XIPassiveUngrabDevice request."""
opcode: int
grab_window: int
detail: int
deviceid: int = XIAllMasterDevices
grab_type: int = XIGrabtypeButton
modifiers: list[int] | None = None
def to_bytes(self, byte_order: str = "<") -> bytes:
mods = self.modifiers if self.modifiers is not None else [XIAnyModifier]
num_modifiers = len(mods)
# Header is 20 bytes, followed by num_modifiers * 4 bytes
length = (20 + num_modifiers * 4) // 4
header = struct.pack(
f"{byte_order}BBH II HH Bx H",
self.opcode,
XIPassiveUngrabDevice,
length,
self.grab_window,
self.detail,
self.deviceid,
num_modifiers,
self.grab_type,
# pad0, pad1
0,
)
mod_data = b""
for mod in mods:
mod_data += struct.pack(f"{byte_order}I", mod)
return header + mod_data
@dataclass
class XIChangePropertyRequest:
"""XIChangeProperty request."""
opcode: int
deviceid: int
property_atom: int
type_atom: int
format: int = 32
mode: int = PropModeReplace
data: bytes = b""
num_items: int | None = None
def to_bytes(self, byte_order: str = "<") -> bytes:
num_items = (
self.num_items
if self.num_items is not None
else (len(self.data) // (self.format // 8) if self.data else 0)
)
total_bytes = 20 + len(self.data)
pad_len = (4 - total_bytes % 4) % 4
total_bytes += pad_len
length = total_bytes // 4
header = struct.pack(
f"{byte_order}BBH HBB II I",
self.opcode,
XIChangeProperty,
length,
self.deviceid,
self.mode,
self.format,
self.property_atom,
self.type_atom,
num_items,
)
return header + self.data + b"\x00" * pad_len
@dataclass
class XIGetPropertyRequest:
"""XIGetProperty request."""
opcode: int
deviceid: int
property_atom: int
type_atom: int = 0 # AnyPropertyType
offset: int = 0
length: int = 0xFFFF
delete: bool = False
def to_bytes(self, byte_order: str = "<") -> bytes:
return struct.pack(
f"{byte_order}BBH HBx II II",
self.opcode,
XIGetProperty,
6, # 24 bytes = 6 words
self.deviceid,
1 if self.delete else 0,
self.property_atom,
self.type_atom,
self.offset,
self.length,
)
@dataclass
class XChangeDevicePropertyRequest:
"""XChangeDeviceProperty request (XI v1, minor opcode 37).
Wire format (xChangeDevicePropertyReq):
opcode(1) + ReqType(1) + length(2) + property(4) + type(4) +
deviceid(1) + format(1) + mode(1) + pad(1) + nUnits(4)
Total header: 20 bytes, followed by property data.
"""
opcode: int
deviceid: int
property_atom: int
type_atom: int
format: int = 32
mode: int = PropModeReplace
data: bytes = b""
num_items: int | None = None
def to_bytes(self, byte_order: str = "<") -> bytes:
num_items = (
self.num_items
if self.num_items is not None
else (len(self.data) // (self.format // 8) if self.data else 0)
)
total_bytes = 20 + len(self.data)
pad_len = (4 - total_bytes % 4) % 4
total_bytes += pad_len
length = total_bytes // 4
header = struct.pack(
f"{byte_order}BBH II BBBx I",
self.opcode,
XChangeDeviceProperty,
length,
self.property_atom,
self.type_atom,
self.deviceid,
self.format,
self.mode,
num_items,
)
return header + self.data + b"\x00" * pad_len
@dataclass
class XGetDevicePropertyRequest:
"""XGetDeviceProperty request (XI v1, minor opcode 39).
Wire format (xGetDevicePropertyReq):
opcode(1) + ReqType(1) + length(2) + property(4) + type(4) +
longOffset(4) + longLength(4) + deviceid(1) + delete(1) + pad(2)
Total: 24 bytes = 6 words.
"""
opcode: int
deviceid: int
property_atom: int
type_atom: int = 0 # AnyPropertyType
offset: int = 0
req_length: int = 0xFFFF
delete: bool = False
def to_bytes(self, byte_order: str = "<") -> bytes:
return struct.pack(
f"{byte_order}BBH II II BBxx",
self.opcode,
XGetDeviceProperty,
6, # 24 bytes = 6 words
self.property_atom,
self.type_atom,
self.offset,
self.req_length,
self.deviceid,
1 if self.delete else 0,
)
@dataclass
class XChangeDeviceControlRequest:
"""XChangeDeviceControl request (XI v1, minor opcode 35).
The request header is 8 bytes (xChangeDeviceControlReq), followed by
a device control structure that depends on the control type.
For DEVICE_RESOLUTION, the control is xDeviceResolutionCtl (12 bytes)
followed by num_valuators CARD32 values.
"""
opcode: int
control: int
deviceid: int
control_data: bytes = b""
def to_bytes(self, byte_order: str = "<") -> bytes:
total = 8 + len(self.control_data)
pad_len = (4 - total % 4) % 4
length = (total + pad_len) // 4
header = struct.pack(
f"{byte_order}BBH HBx",
self.opcode,
XChangeDeviceControl,
length,
self.control,
self.deviceid,
)
return header + self.control_data + b"\x00" * pad_len
@dataclass
class DeviceResolutionCtl:
"""xDeviceResolutionCtl structure (8 bytes + valuator values).
control(2) + length(2) + first_valuator(1) + num_valuators(1) + pad(2)
followed by num_valuators CARD32 resolution values.
"""
first_valuator: int = 0
num_valuators: int = 0
resolutions: list[int] | None = None
def to_bytes(self, byte_order: str = "<") -> bytes:
vals = self.resolutions if self.resolutions is not None else []
val_data = b""
for v in vals:
val_data += struct.pack(f"{byte_order}I", v)
ctl_length = (8 + len(val_data)) // 4
header = struct.pack(
f"{byte_order}HH BB xx",
DEVICE_RESOLUTION,
ctl_length,
self.first_valuator,
self.num_valuators,
)
return header + val_data