Skip to content

Instantly share code, notes, and snippets.

@LizzyFleckenstein03
Created March 4, 2025 19:16
Show Gist options
  • Save LizzyFleckenstein03/ffd66968718cefab086cf0814d7a51d0 to your computer and use it in GitHub Desktop.
Save LizzyFleckenstein03/ffd66968718cefab086cf0814d7a51d0 to your computer and use it in GitHub Desktop.
Post-process xtrace output to deserialize XIM messages. Very hacky and incomplete.
-- 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