Created
March 4, 2025 19:16
-
-
Save LizzyFleckenstein03/ffd66968718cefab086cf0814d7a51d0 to your computer and use it in GitHub Desktop.
Post-process xtrace output to deserialize XIM messages. Very hacky and incomplete.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- GPLv3 licensed | |
-- I/O | |
local peek_cursor = 0 | |
local line_buffer = {} | |
local function getline() | |
return io.read() | |
end | |
local function nextline() | |
peek_cursor = 0 | |
return table.remove(line_buffer, 1) or getline() | |
end | |
local function peekline() | |
peek_cursor = peek_cursor + 1 | |
if not line_buffer[peek_cursor] then | |
line_buffer[peek_cursor] = getline() | |
end | |
return line_buffer[peek_cursor] | |
end | |
-- properties | |
local properties = {} | |
local function format_property_key(k) return ("%s-%x-%x"):format(k.conn, k.win, k.prop) end | |
local function get_property(k) | |
return properties[format_property_key(k)] | |
end | |
local function put_property(k, v) | |
local fmt = format_property_key(k) | |
properties[fmt] = v | |
return properties[fmt] | |
end | |
-- parsing | |
local function parse_bytes(str) | |
local bytes = {} | |
local i = 1 | |
while i <= #str do | |
local function byte() | |
if str:sub(i,i) ~= "\\" then | |
i = i + 1 | |
return str:byte(i-1) | |
end | |
i = i + 1 | |
local digit = str:match("^([0-7][0-7][0-7])", i) | |
if digit then | |
i = i + #digit | |
return assert(tonumber(digit, 8)) | |
end | |
local next = str:sub(i,i) | |
i = i + 1 | |
-- HACK - needs to be fixed in xtrace | |
-- if next == "\\" then return ("\\"):byte(1) end | |
if next == "n" then return ("\n"):byte(1) end | |
if next == "t" then return ("\t"):byte(1) end | |
-- HACK - needs to be fixed in xtrace | |
-- error("unknown escape: " .. next) | |
i = i - 1 | |
return ("\\"):byte() | |
end | |
table.insert(bytes, byte()) | |
end | |
return bytes | |
end | |
-- deserialization | |
local function pad(x) | |
return (4-(x%4))%4 | |
end | |
local function slice(t, a, b) | |
return table.move(t, a+1, b or #t, 1, {}) | |
end | |
local function u8(t, x) | |
x = x or 0 | |
return t[x+1] | |
end | |
local function u16(t, x) | |
x = x or 0 | |
return u8(t, x) | (u8(t, x+1) << 8) | |
end | |
local function u32(t, x) | |
x = x or 0 | |
return u8(t, x) | (u8(t, x+1) << 8) | (u8(t, x+2) << 16) | (u8(t, x+3) << 24) | |
end | |
local function s8(...) | |
local v = u8(...) | |
if v > 0x7f then return v - 0x100 | |
else return v end | |
end | |
local function s16(...) | |
local v = u16(...) | |
if v > 0x7fff then return v - 0x10000 | |
else return v end | |
end | |
local function s32(...) | |
local v = u32(...) | |
if v > 0x7fffffff then return v - 0x100000000 | |
else return v end | |
end | |
local function str(t, start, len) | |
if #t == 0 then return end | |
start = start or 0 | |
local offset = 0 | |
if not len then | |
len = u8(t, start) | |
offset = 1 | |
end | |
local bytes = slice(t, start+offset, start+offset+len) | |
assert(#bytes == len) | |
return len+offset, string.char(table.unpack(bytes)) | |
end | |
local function attr(data) | |
if #data == 0 then return end | |
local attr_id = u16(data, 0) | |
local type = u16(data, 2) | |
local name_len = u16(data, 4) | |
local _, name = str(data, 6, name_len) | |
return 6+name_len+pad(name_len+2), ("{attr_id=%d type=%d name_len=0x%x name=%s}") | |
:format(attr_id, type, name_len, name) | |
end | |
local function ext(data) | |
if #data == 0 then return end | |
local major = u8(data, 0) | |
local minor = u8(data, 1) | |
local name_len = u16(data, 2) | |
local _, name = str(data, 4, name_len) | |
return 4+name_len+pad(name_len), ("{major=%d minor=%d name_len=0x%x name=%s}") | |
:format(major, minor, name_len, name) | |
end | |
local function encodinginfo(data) | |
if #data == 0 then return end | |
local len = u16(data, 0) | |
return 2+len+pad(2+len), str(data, 2, len) | |
end | |
local function list(data, f) | |
local items = {} | |
while true do | |
local size, item = f(data) | |
if not size then break end | |
table.insert(items, item) | |
data = slice(data, size) | |
end | |
return items | |
end | |
local function bitmask(val, max, none, flags) | |
local has_flags = {} | |
for i = 0, max-1 do | |
if (val & (1 << i)) ~= 0 then | |
table.insert(has_flags, flags[i+1] or ("(1<<"..i..")")) | |
end | |
end | |
if #has_flags == 0 then | |
return none | |
else | |
return table.concat(has_flags, "|") | |
end | |
end | |
local function eventmask(val) | |
return bitmask(val, 32, "NoEventMask", { | |
"KeyPressMask", | |
"KeyReleaseMask", | |
"ButtonPressMask", | |
"ButtonReleaseMask", | |
"EnterWindowMask", | |
"LeaveWindowMask", | |
"PointerMotionMask", | |
"PointerMotionHintMask", | |
"Button1MotionMask", | |
"Button2MotionMask", | |
"Button3MotionMask", | |
"Button4MotionMask", | |
"Button5MotionMask", | |
"ButtonMotionMask", | |
"KeymapStateMask", | |
"ExposureMask", | |
"VisibilityChangeMask", | |
"StructureNotifyMask", | |
"ResizeRedirectMask", | |
"SubstructureNotifyMask", | |
"SubstructureRedirectMask", | |
"FocusChangeMask", | |
"PropertyChangeMask", | |
"ColormapChangeMask", | |
"OwnerGrabButtonMask", | |
}) | |
end | |
-- translation | |
local translate_data | |
local function format_translate_data(bytes, ...) | |
local relevant, used, str = translate_data(bytes, ...) | |
assert(used <= relevant) | |
if relevant > used then | |
str = str .. " trailing=" | |
for i = used+1, relevant do | |
str = ("%s0x%02x%s"):format(str, bytes[i], i == relevant and ";" or ",") | |
end | |
end | |
return str | |
end | |
local function find_toclient_property(conn, win, prop, length) | |
local function sub_body(x) return x:sub(x:find(":", 12)+2) end | |
local req | |
repeat req = peekline(conn) | |
until not req | |
or (req:sub(1,3) == conn and req:find("GetProperty") and req:find(("%x"):format(prop))) | |
-- this is an application bug. | |
if not req then | |
return nil, "FIXME(missing property request)" | |
end | |
req = sub_body(req) | |
local res | |
repeat res = peekline(conn) until res:sub(1,3) == conn and res:find("GetProperty") | |
res = sub_body(res) | |
local template_req = ([[Request(20): GetProperty delete=true(0x01) window=0x%08x property=0x%x(unrecognized atom) type=any(0x0) long-offset=0x00000000 long-length=0x%08x]]) | |
assert(req == template_req:format(win, prop, length // 4)) | |
local template_res = [[Reply to GetProperty: type=0x1f("STRING") bytes-after=0x00000000 data=']] | |
assert(res:sub(1, #template_res) == template_res) | |
assert(res:sub(-1) == "'") | |
bytes = parse_bytes(res:sub(#template_res+1, -2)) | |
-- this is weird. no idea why it happens. but this is really what the request returns, not a parsing issue. | |
if #bytes == 0 then | |
return nil, "FIXME(empty property)" | |
end | |
assert(#bytes == length, #bytes .. " != " .. length) | |
return bytes | |
end | |
local function translate_long(conn, win, toserver, data) | |
local length = u32(data, 0) | |
local atom = u32(data, 4) | |
local bytes, body | |
if toserver then | |
bytes = assert(get_property { conn = conn, win = win, prop = atom }, ("missing 0x%x"):format(atom)) | |
else | |
bytes, body = find_toclient_property(conn, win, atom, length) | |
end | |
if not body then | |
body = "{"..format_translate_data(bytes, conn, win, toserver, 8, "_XIM_PROTOCOL").."};" | |
end | |
return #data, 8, ("atom=0x%x length=0x%x body=%s"):format(atom, length, body) | |
end | |
local function translate_body(proto, minor, data) | |
if proto == "XIM_CONNECT" then | |
local bo = u8(data, 0) | |
local byteorder | |
if bo == 0x42 then byteorder = "MSB" | |
elseif bo == 0x6c then byteorder = "LSB" | |
else error(("invalid byte order 0x%02x"):format(bo)) end | |
assert(byteorder == "LSB", "unimplemented") | |
return 8, ("byteoder=%s client_proto_major=%d client_proto_minor=%d auth_protos=TODO(num=%d)") | |
:format(byteorder, u16(data, 2), u16(data, 4), u16(data, 6)) | |
elseif proto == "XIM_CONNECT_REPLY" then | |
return 4, ("server_proto_major=%d server_proto_minor=%d") | |
:format(u16(data, 0), u16(data, 2)) | |
elseif proto == "XIM_OPEN" then | |
local loc_size, loc = str(data) | |
return loc_size+pad(loc_size), ("locale=%s"):format(loc) | |
elseif proto == "XIM_OPEN_REPLY" then | |
local input_method_id = u16(data, 0) | |
local im_attrs_size = u16(data, 2) | |
data = slice(data, 2+2) | |
local im_attrs = list(slice(data, 0, im_attrs_size), attr) | |
data = slice(data, im_attrs_size) | |
local ic_attrs_size = u16(data, 0) | |
-- unused u16 | |
data = slice(data, 2+2) | |
local ic_attrs = list(slice(data, 0, ic_attrs_size), attr) | |
return 2+2+im_attrs_size+2+2+ic_attrs_size, ("input_method_id=%d im_attrs=%s; ic_attrs=%s;") | |
:format(input_method_id, table.concat(im_attrs, ","), table.concat(ic_attrs, ",")) | |
elseif proto == "XIM_QUERY_EXTENSION" then | |
local input_method_id = u16(data, 0) | |
local extensions_size = u16(data, 2) | |
local extensions = list(slice(data, 2+2, 2+2+extensions_size), str) | |
return 2+2+extensions_size+pad(extensions_size), ("input_method_id=%d extensions=%s;") | |
:format(input_method_id, table.concat(extensions, ",")) | |
elseif proto == "XIM_QUERY_EXTENSION_REPLY" then | |
local input_method_id = u16(data, 0) | |
local extensions_size = u16(data, 2) | |
local extensions = list(slice(data, 2+2, 2+2+extensions_size), ext) | |
return 2+2+extensions_size, ("input_method_id=%d extensions=%s;") | |
:format(input_method_id, table.concat(extensions, ",")) | |
elseif proto == "XIM_SET_EVENT_MASK" then | |
local forward = u32(data, 4) | |
return 12, ("input_method_id=%d input_context_id=%d forward_mask=%s sync_mask=%s") | |
:format(u16(data, 0), u16(data, 2), eventmask(forward), eventmask(forward & u32(data, 8))) | |
elseif proto == "XIM_CLOSE" or proto == "XIM_CLOSE_REPLY" then | |
return 2+2, ("input_method_id=%d"):format(u16(data, 0)) | |
elseif proto == "XIM_DISCONNECT" or proto == "XIM_DISCONNECT_REPLY" then | |
return 0, "" | |
elseif proto == "XIM_ENCODING_NEGOTIATION" then | |
local im_id = u16(data, 0) | |
local enc_size = u16(data, 2) | |
local enc = list(slice(data, 4, 4+enc_size), str) | |
local after_pad = 4+enc_size+pad(enc_size) | |
local encinfo_size = u16(data, after_pad) | |
local encinfo = list(slice(data, after_pad+4, after_pad+4+encinfo_size), encodinginfo) | |
return after_pad+4+encinfo_size, ("input_method_id=%d encodings=%s; enconding_infos=%s;") | |
:format(im_id, table.concat(enc, ","), table.concat(encinfo, ",")) | |
elseif proto == "XIM_ENCODING_NEGOTIATION_REPLY" then | |
local cat = u16(data, 2) | |
local category | |
if cat == 0 then category = "name" | |
elseif cat == 1 then category = "detailed_data" | |
else error("invalid category: " .. cat) end | |
return 2+2+2+2, ("input_method_id=%d category=%s index=%d") | |
:format(u16(data, 0), category, s16(data, 4)) | |
elseif proto == "XIM_CREATE_IC" then | |
return 2+2, ("input_method_id=%d attributes=TODO(size=%d)") | |
:format(u16(data, 0), u16(data, 2)) | |
elseif | |
proto == "XIM_CREATE_IC_REPLY" or | |
proto == "XIM_DESTROY_IC" or | |
proto == "XIM_DESTROY_IC_REPLY" or | |
proto == "XIM_SET_IC_FOCUS" or | |
proto == "XIM_UNSET_IC_FOCUS" or | |
proto == "XIM_SYNC" or | |
proto == "XIM_SYNC_REPLY" then | |
return 2+2, ("input_method_id=%d input_context_id=%d") | |
:format(u16(data, 0), u16(data, 2)) | |
elseif proto == "XIM_FORWARD_EVENT" then | |
-- local size, evt = xevent(data, 8) | |
local flag = bitmask(u16(data, 4), 16, "0", {"Synchronous", "RequestFiltering", "RequestLookupstring"}) | |
return 2+2+2+2, ("input_method_id=%d input_context_id=%d flag=%s serial=%d event=TODO") | |
:format(u16(data, 0), u16(data, 2), flag, u16(data, 6)) | |
end | |
return 0, "contents=TODO" | |
end | |
translate_data = function(data, conn, win, toserver, format, proto) | |
if proto == "_XIM_MOREDATA" then | |
error("_XIM_MOREDATA not implemented") | |
elseif proto == "_XIM_XCONNECT" then | |
assert(format == 32) | |
local major, minor = u32(data, 4), u32(data, 8) | |
assert(major == 0, "unimplemented") | |
assert(minor == 0, "unimplemented") | |
if toserver then | |
return #data, 12, ("client_window_id=0x%x client_transport_major=%d client_transport_minor=%d") | |
:format(u32(data, 0), major, minor) | |
else | |
return #data, 16, ("im_window_id=0x%x server_transport_major=%d server_transport_minor=%d property_offset=0x%x") | |
:format(u32(data, 0), major, minor, u32(data, 12)) | |
end | |
elseif proto == "_XIM_PROTOCOL" then | |
if format == 32 then | |
return translate_long(conn, win, toserver, data) | |
end | |
assert(format == 8) | |
local major = u8(data, 0) | |
local minor = u8(data, 1) | |
local length = u16(data, 2) | |
local header = 4 | |
local relevant = length*4 + header | |
local protos = { | |
[ 1] = "XIM_CONNECT", | |
[ 2] = "XIM_CONNECT_REPLY", | |
[ 3] = "XIM_DISCONNECT", | |
[ 4] = "XIM_DISCONNECT_REPLY", | |
[30] = "XIM_OPEN", | |
[31] = "XIM_OPEN_REPLY", | |
[32] = "XIM_CLOSE", | |
[33] = "XIM_CLOSE_REPLY", | |
[37] = "XIM_SET_EVENT_MASK", | |
[38] = "XIM_ENCODING_NEGOTIATION", | |
[39] = "XIM_ENCODING_NEGOTIATION_REPLY", | |
[40] = "XIM_QUERY_EXTENSION", | |
[41] = "XIM_QUERY_EXTENSION_REPLY", | |
[44] = "XIM_GET_IM_VALUES", -- TODO | |
[45] = "XIM_GET_IM_VALUES_REPLY", -- TODO | |
[50] = "XIM_CREATE_IC", -- semi-TODO | |
[51] = "XIM_CREATE_IC_REPLY", | |
[52] = "XIM_DESTROY_IC", | |
[53] = "XIM_DESTROY_IC_REPLY", | |
[56] = "XIM_GET_IC_VALUES", -- TODO | |
[57] = "XIM_GET_IC_VALUES_REPLY", -- TODO | |
[58] = "XIM_SET_IC_FOCUS", | |
[59] = "XIM_UNSET_IC_FOCUS", | |
[60] = "XIM_FORWARD_EVENT", -- semi-TODO | |
[61] = "XIM_SYNC", | |
[62] = "XIM_SYNC_REPLY", | |
} | |
local subproto = protos[major] | |
if not subproto then | |
error("unimplemented major opcode: " .. major) | |
end | |
local used, transl = translate_body(subproto, minor, table.move(data, header+1, relevant, 1, {})) | |
return relevant, header+used, ("major=%s(%d) minor=%d length=0x%x %s"):format(subproto, major, minor, length, transl) | |
end | |
end | |
local function translate_line(l) | |
local conn = l:sub(1,3) | |
for _, proto in pairs { "_XIM_XCONNECT", "_XIM_MOREDATA", "_XIM_PROTOCOL" } do | |
local f = "%(\""..proto.."\"%) " | |
local d = "data=" | |
local i = l:find(f..d) | |
if i then | |
local bytes = {} | |
for b in l:sub(i+#f-2+#d):gmatch("0x(..)[,;]") do | |
table.insert(bytes, assert(tonumber(b, 16))) | |
end | |
local toserver = l:sub(5,5) == "<" | |
return l:sub(1, i+#f-2-1) .. format_translate_data( | |
bytes, | |
conn, | |
assert(tonumber(l:match((toserver and "destination" or "window") .. "=0x([0-9a-f]+)"), 16)), | |
toserver, | |
assert(tonumber(l:match("format=0x(..)"), 16)), | |
proto | |
) | |
end | |
end | |
local mode, win, prop, data = l:match("Request%(18%): ChangeProperty mode=[^(]*%(0x([0-9a-f]+)%) window=0x([0-9a-f]+) property=0x([0-9a-f]+)%([^(]*%) type=0x1f%(\"STRING\"%) data='(.+)'$") | |
if mode then | |
mode = assert(tonumber(mode, 16)) | |
local key = { | |
conn = conn, | |
win = assert(tonumber(win, 16)), | |
prop = assert(tonumber(prop, 16)) | |
} | |
local bytes = parse_bytes(data) | |
-- HACK: i suspect this is necessary | |
mode = 0 | |
-- replace = 0 | |
if mode == 0 then | |
put_property(key, bytes) | |
-- append = 2 | |
elseif mode == 2 then | |
local old = get_property(key) or put_property(key, {}) | |
table.move(bytes, 1, #bytes, #old+1, old) | |
else | |
error("unimplemented mode " .. mode) | |
end | |
end | |
return l | |
end | |
while true do | |
local l = nextline() | |
if not l then break end | |
print(translate_line(l)) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment