package xturbo_test import ( "bytes" "encoding/binary" "testing" "bnema.dev/xturbo" "bnema.dev/xturbo/internal/xtest" "bnema.dev/xturbo/render" "bnema.dev/xturbo/shape" "bnema.dev/xturbo/xfixes" "bnema.dev/xturbo/xproto" ) // Xvfb negotiated XFIXES as major 138, first event 87, first error 140 when the // goldens were captured; the BadRegion and SelectionNotify goldens confirm it. const ( xfixesMajor, xfixesEvent, xfixesError = 138, 87, 140 geMajor = 0x83 // hand-written XGE golden ) func loadEvent(t testing.TB, name string) []byte { t.Helper() raw, err := xtest.LoadHex("testdata/golden/" + name + ".event.hex") if err != nil { t.Fatal(err) } return raw } func goldenExt() *xturbo.ExtTable { var ext xturbo.ExtTable ext.Set(xturbo.ExtXFixes, xfixesMajor, xfixesEvent, xfixesError) ext.Set(xturbo.ExtGenericEvent, geMajor, 0, 0) ext.Set(xturbo.ExtShape, 129, 64, 0) return &ext } func decode(t testing.TB, raw []byte, ext *xturbo.ExtTable) xturbo.Event { t.Helper() ev, n, ok := xturbo.DecodeEvent(raw, ext) if !ok || n != len(raw) || !bytes.Equal(ev.Raw[:], raw[:32]) { t.Fatalf("DecodeEvent(%x) = n %d ok %v", raw, n, ok) } return ev } // Expected values follow from the capture script (second client's window at // 10,20 300x200, ConfigureWindow x=-5 y=40 width=640, a format-32 ClientMessage // with words 0x01020304*(i+1), SetSelectionOwner PRIMARY) or from raw golden // offsets defined by the X11 protocol, not from generated code. func TestEventGoldens(t *testing.T) { u32 := func(raw []byte, off int) uint32 { return binary.LittleEndian.Uint32(raw[off:]) } ext := goldenExt() core := func(name string, kind xturbo.EventKind) ([]byte, xturbo.Event) { raw := loadEvent(t, "xproto/"+name) ev := decode(t, raw, ext) if ev.Kind != kind || ev.Ext != xturbo.ExtCore || ev.Seq != xturbo.Seq(binary.LittleEndian.Uint16(raw[2:])) || ev.Tail != nil { t.Fatalf("%s: kind %d ext %d seq %d", name, ev.Kind, ev.Ext, ev.Seq) } return raw, ev } t.Run("MapRequest", func(t *testing.T) { raw, ev := core("MapRequest", xproto.KindMapRequest) e := xproto.MapRequestOf(&ev) if ev.SendEvent || e.Parent() != xproto.Window(u32(raw, 4)) || e.Window() != xproto.Window(u32(raw, 8)) || e.Window()&0xffff != 1 { t.Fatal("fields") } d := xproto.MapRequestEventData{Parent: e.Parent(), Window: e.Window()} var out [32]byte d.Put(&out) want := [32]byte(raw) want[2], want[3] = 0, 0 // Put leaves the sequence to the server if out != want { t.Fatalf("Put %x want %x", out, want) } }) t.Run("ConfigureRequest", func(t *testing.T) { raw, ev := core("ConfigureRequest", xproto.KindConfigureRequest) e := xproto.ConfigureRequestOf(&ev) if e.StackMode() != xproto.StackModeAbove || e.Parent() != xproto.Window(u32(raw, 4)) || e.Window()&0xffff != 1 || e.Sibling() != xproto.WindowNone || e.X() != -5 || e.Y() != 40 || e.Width() != 640 || e.Height() != 200 || e.BorderWidth() != 0 || e.ValueMask() != xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth { t.Fatalf("fields x=%d y=%d w=%d h=%d mask=%d", e.X(), e.Y(), e.Width(), e.Height(), e.ValueMask()) } d := xproto.ConfigureRequestEventData{StackMode: e.StackMode(), Parent: e.Parent(), Window: e.Window(), Sibling: e.Sibling(), X: e.X(), Y: e.Y(), Width: e.Width(), Height: e.Height(), BorderWidth: e.BorderWidth(), ValueMask: e.ValueMask()} var out [32]byte d.Put(&out) if !bytes.Equal(out[4:], raw[4:]) || out[0] != 23 || out[1] != raw[1] { t.Fatalf("Put %x", out) } }) t.Run("PropertyNotify", func(t *testing.T) { raw, ev := core("PropertyNotify", xproto.KindPropertyNotify) e := xproto.PropertyNotifyOf(&ev) if e.Window() != xproto.Window(u32(raw, 4)) || e.Atom() != xproto.Atom(u32(raw, 8)) || e.Atom() < 69 || e.Time() != xproto.Timestamp(u32(raw, 12)) || e.State() != xproto.PropertyNewValue { t.Fatal("fields") } }) t.Run("ClientMessage", func(t *testing.T) { raw, ev := core("ClientMessage", xproto.KindClientMessage) e := xproto.ClientMessageOf(&ev) if !ev.SendEvent || e.Format() != 32 || e.Window()&0xffff != 1 || e.Type() != xproto.Atom(u32(raw, 8)) { t.Fatal("fields") } words := e.Data().Data32() for i, w := range words { if w != uint32(0x01020304*(i+1)) { t.Fatalf("data32[%d] = %x", i, w) } } d := xproto.ClientMessageEventData{Format: 32, Window: e.Window(), Type: e.Type(), Data: e.Data()} var out [32]byte d.Put(&out) want := [32]byte(raw) want[0] &^= 0x80 want[2], want[3] = 0, 0 if out != want { t.Fatalf("Put %x want %x", out, want) } }) t.Run("CreateNotify", func(t *testing.T) { _, ev := core("CreateNotify", xproto.KindCreateNotify) e := xproto.CreateNotifyOf(&ev) if e.X() != 10 || e.Y() != 20 || e.Width() != 300 || e.Height() != 200 || e.OverrideRedirect() { t.Fatal("fields") } }) t.Run("Map/Unmap/DestroyNotify", func(t *testing.T) { _, ev := core("MapNotify", xproto.KindMapNotify) if m := xproto.MapNotifyOf(&ev); m.Window()&0xffff != 1 || m.OverrideRedirect() { t.Fatal("MapNotify") } _, ev = core("UnmapNotify", xproto.KindUnmapNotify) if u := xproto.UnmapNotifyOf(&ev); u.Window()&0xffff != 1 || u.FromConfigure() { t.Fatal("UnmapNotify") } _, ev = core("DestroyNotify", xproto.KindDestroyNotify) if d := xproto.DestroyNotifyOf(&ev); d.Window()&0xffff != 1 { t.Fatal("DestroyNotify") } }) t.Run("SelectionNotify", func(t *testing.T) { _, ev := core("SelectionNotify", xproto.KindSelectionNotify) e := xproto.SelectionNotifyOf(&ev) if e.Requestor()&0xffff != 1 || e.Selection() != xproto.AtomSecondary || e.Target() != xproto.AtomString || e.Property() != xproto.AtomNone { t.Fatal("fields") } }) t.Run("xfixes/SelectionNotify", func(t *testing.T) { raw := loadEvent(t, "xfixes/SelectionNotify") ev := decode(t, raw, ext) if xfixes.KindSelectionNotify != xturbo.EventKind(xturbo.ExtXFixes)<<8 { t.Fatal("KindSelectionNotify value") } if ev.Kind != xfixes.KindSelectionNotify || ev.Ext != xturbo.ExtXFixes { t.Fatalf("kind %x", ev.Kind) } e := xfixes.SelectionNotifyOf(&ev) if e.Subtype() != xfixes.SelectionEventSetSelectionOwner || e.Window() != xproto.Window(u32(raw, 4)) || e.Owner()&0xffff != 1 || e.Selection() != xproto.AtomPrimary || e.Timestamp() != e.SelectionTimestamp() { t.Fatal("fields") } // Without XFIXES negotiated the same bytes are an unknown event. if ev, _, ok := xturbo.DecodeEvent(raw, nil); !ok || ev.Kind != xturbo.KindUnknown { t.Fatal("nil ext must decode as unknown") } }) t.Run("KeymapNotify", func(t *testing.T) { raw := loadEvent(t, "xproto/KeymapNotify") ev := decode(t, raw, ext) if ev.Kind != xproto.KindKeymapNotify || ev.Seq != 0 { t.Fatalf("kind %d seq %d", ev.Kind, ev.Seq) } e := xproto.KeymapNotifyOf(&ev) if !bytes.Equal(e.Keys(), raw[1:]) { t.Fatal("keys") } d := xproto.KeymapNotifyEventData{Keys: [31]uint8(raw[1:])} var out [32]byte d.Put(&out) if !bytes.Equal(out[:], raw) { t.Fatalf("Put %x", out) } }) t.Run("GeGeneric", func(t *testing.T) { raw := loadEvent(t, "xproto/GeGeneric") ev := decode(t, raw, ext) if ev.Kind != xturbo.EventKind(xturbo.ExtGenericEvent)<<8|0x80|1 || ev.Ext != xturbo.ExtGenericEvent || ev.Seq != 5 || !bytes.Equal(ev.Tail, raw[32:]) { t.Fatalf("kind %x tail %x", ev.Kind, ev.Tail) } g, ok := xproto.GeGenericOf(&ev) if !ok || g.Extension() != geMajor || g.Length() != 2 || g.EventType() != 1 || !bytes.Equal(g.Tail(), raw[32:]) { t.Fatal("view") } // Unknown major: still framed so Dispatch skips the tail. ev, n, ok := xturbo.DecodeEvent(raw, nil) if !ok || n != 40 || ev.Kind != xturbo.KindUnknown || len(ev.Tail) != 8 { t.Fatal("unknown XGE framing") } ev.Tail = ev.Tail[:4] if _, ok := xproto.GeGenericOf(&ev); ok { t.Fatal("short Tail accepted") } }) } // TestEventDataPutInPlace re-encodes events into the Event.Raw their fields // were read from. ClientMessage's Data is a union view over Raw; KeymapNotify's // Keys is a slice view over Raw. Put must read every field before writing dst. func TestEventDataPutInPlace(t *testing.T) { raw := loadEvent(t, "xproto/ClientMessage") ev := decode(t, raw, nil) e := xproto.ClientMessageOf(&ev) d := xproto.ClientMessageEventData{Format: e.Format(), Window: e.Window(), Type: e.Type(), Data: e.Data()} d.Put(&ev.Raw) want := [32]byte(raw) want[0] &^= 0x80 want[2], want[3] = 0, 0 if ev.Raw != want { t.Fatalf("ClientMessage in place %x want %x", ev.Raw, want) } if n := testing.AllocsPerRun(1000, func() { d.Data = e.Data(); d.Put(&ev.Raw) }); n != 0 { t.Fatalf("in-place Put allocates %v times", n) } raw = loadEvent(t, "xproto/KeymapNotify") ev = decode(t, raw, nil) keys := xproto.KeymapNotifyOf(&ev).Keys() k := xproto.KeymapNotifyEventData{Keys: [31]uint8(keys)} k.Put(&ev.Raw) if ev.Raw != [32]byte(raw) || !bytes.Equal(keys, raw[1:]) { t.Fatalf("KeymapNotify in place %x", ev.Raw) } } func TestEventCopyAliases(t *testing.T) { if xproto.KindButtonRelease != 5 || xproto.KindKeyRelease != 3 || xproto.KindLeaveNotify != 8 || xproto.KindFocusOut != 10 || xproto.KindCirculateRequest != 27 { t.Fatal("eventcopy kinds") } var raw [32]byte raw[0], raw[1] = 5, 3 // ButtonRelease, button 3 binary.LittleEndian.PutUint16(raw[24:], 0x100) ev := decode(t, raw[:], nil) var r xproto.ButtonReleaseEvent = xproto.ButtonReleaseOf(&ev) var p xproto.ButtonPressEvent = r // alias: same type if p.Detail() != 3 || r.EventX() != 0x100 { t.Fatal("fields") } d := xproto.ButtonReleaseEventData{Detail: 3, EventX: 0x100} var out [32]byte d.Put(&out) raw[2], raw[3] = 0, 0 if out != raw { t.Fatalf("Put %x", out) } } func TestShapeEvent(t *testing.T) { var raw [32]byte raw[0], raw[1] = 64, 2 // first event 64, SK Input binary.LittleEndian.PutUint32(raw[4:], 0x400001) binary.LittleEndian.PutUint16(raw[8:], 0xfffe) raw[20] = 1 // shaped: after window(4) extents(8) server_time(4) ev := decode(t, raw[:], goldenExt()) if ev.Kind != shape.KindNotify || ev.Ext != xturbo.ExtShape { t.Fatalf("kind %x", ev.Kind) } e := shape.NotifyOf(&ev) if e.ShapeKind() != shape.SkInput || e.AffectedWindow() != 0x400001 || e.ExtentsX() != -2 || !e.Shaped() { t.Fatal("fields") } } func TestDecodeEventRejects(t *testing.T) { raw := make([]byte, 64) for _, code := range []byte{0, 1, 0x80, 0x81} { raw[0] = code if _, _, ok := xturbo.DecodeEvent(raw, nil); ok { t.Fatalf("code %#x accepted", code) } } raw[0] = 2 if _, _, ok := xturbo.DecodeEvent(raw[:31], nil); ok { t.Fatal("short event accepted") } raw[0] = 35 binary.LittleEndian.PutUint32(raw[4:], 9) // 36 tail bytes > 32 available if _, _, ok := xturbo.DecodeEvent(raw, nil); ok { t.Fatal("truncated XGE accepted") } binary.LittleEndian.PutUint32(raw[4:], 0xffffffff) if _, _, ok := xturbo.DecodeEvent(raw, nil); ok { t.Fatal("huge XGE length accepted") } raw[0] = 100 // extension range, nothing negotiated if ev, n, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || n != 32 || ev.Kind != xturbo.KindUnknown { t.Fatal("unknown extension event") } raw[0] = 40 // core range gap 36..63 if ev, _, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || ev.Kind != xturbo.KindUnknown { t.Fatal("unassigned core code") } raw[0] = 35 // XGE evtype >= 128 is never a known kind binary.LittleEndian.PutUint32(raw[4:], 0) raw[1] = geMajor binary.LittleEndian.PutUint16(raw[8:], 0x80) if ev, _, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || ev.Kind != xturbo.KindUnknown { t.Fatal("XGE evtype >= 128") } } func TestExtTableReverseMaps(t *testing.T) { ext := goldenExt() if id, rel, ok := ext.ByEvent(xfixesEvent + 1); !ok || id != xturbo.ExtXFixes || rel != 1 { t.Fatal("ByEvent CursorNotify") } if _, _, ok := ext.ByEvent(xfixesEvent + 2); ok { t.Fatal("XFIXES declares two events") } if id, rel, ok := ext.ByError(xfixesError); !ok || id != xturbo.ExtXFixes || rel != xfixes.BadRegion { t.Fatal("ByError") } if id, ok := ext.ByMajor(xfixesMajor); !ok || id != xturbo.ExtXFixes { t.Fatal("ByMajor") } ext.Set(xturbo.ExtXFixes, 140, 90, 150) // re-negotiation drops stale entries if _, ok := ext.ByMajor(xfixesMajor); ok { t.Fatal("stale major") } if _, _, ok := ext.ByEvent(xfixesEvent); ok { t.Fatal("stale event") } if id, _, ok := ext.ByError(150); !ok || id != xturbo.ExtXFixes { t.Fatal("new error base") } var render xturbo.ExtTable render.Set(xturbo.ExtRender, 139, 0, 142) for code := uint8(142); code < 147; code++ { if id, rel, ok := render.ByError(code); !ok || id != xturbo.ExtRender || render.Info(id).FirstError+rel != code { t.Fatalf("render error %d", code) } } if _, _, ok := render.ByError(147); ok { t.Fatal("RENDER declares five errors") } } func TestErrorNames(t *testing.T) { if xproto.BadWindow != 3 || xproto.BadAtom != 5 || xproto.BadImplementation != 17 || xproto.ErrorName(xproto.BadWindow) != "BadWindow" || xproto.ErrorName(0) != "" || xproto.ErrorName(200) != "" { t.Fatal("xproto errors") } if xfixes.BadRegion != 0 || xfixes.ErrorName(0) != "BadRegion" { t.Fatal("xfixes errors") } for code, name := range []string{"BadPictFormat", "BadPicture", "BadPictOp", "BadGlyphSet", "BadGlyph"} { if render.ErrorName(uint8(code)) != name { t.Fatalf("render %d", code) } } } // mixedEvents is 10k events: core, xfixes, XGE with a tail and unknown codes. func mixedEvents(t testing.TB) []byte { var golden [][]byte for _, name := range []string{"xproto/MapRequest", "xproto/ConfigureRequest", "xproto/PropertyNotify", "xproto/ClientMessage", "xproto/KeymapNotify", "xfixes/SelectionNotify", "xproto/GeGeneric"} { golden = append(golden, loadEvent(t, name)) } unknown := make([]byte, 32) unknown[0] = 100 golden = append(golden, unknown) var stream []byte for i := range 10000 { stream = append(stream, golden[i%len(golden)]...) } return stream } // eventSink touches the per-kind view getters a window manager reads. func eventSink(ev *xturbo.Event) uint64 { switch ev.Kind { case xproto.KindMapRequest: e := xproto.MapRequestOf(ev) return uint64(e.Window()) + uint64(e.Parent()) case xproto.KindConfigureRequest: e := xproto.ConfigureRequestOf(ev) return uint64(e.Window()) + uint64(e.Width()) + uint64(e.ValueMask()) + uint64(int64(e.X())) case xproto.KindPropertyNotify: e := xproto.PropertyNotifyOf(ev) return uint64(e.Atom()) + uint64(e.State()) case xproto.KindClientMessage: e := xproto.ClientMessageOf(ev) return uint64(e.Type()) + uint64(e.Data().Data32()[0]) case xproto.KindKeymapNotify: return uint64(len(xproto.KeymapNotifyOf(ev).Keys())) case xfixes.KindSelectionNotify: e := xfixes.SelectionNotifyOf(ev) return uint64(e.Owner()) + uint64(e.Selection()) case xturbo.EventKind(xturbo.ExtGenericEvent)<<8 | 0x80 | 1: if g, ok := xproto.GeGenericOf(ev); ok { return uint64(len(g.Tail())) } } return 0 } func decodeAll(t testing.TB, stream []byte, ext *xturbo.ExtTable) (sum uint64) { for len(stream) > 0 { ev, n, ok := xturbo.DecodeEvent(stream, ext) if !ok { t.Fatal("decode") } sum += eventSink(&ev) stream = stream[n:] } return sum } func TestAllocEvents(t *testing.T) { stream := mixedEvents(t) ext := goldenExt() var sink uint64 if n := testing.AllocsPerRun(10, func() { sink += decodeAll(t, stream, ext) }); n != 0 { t.Fatalf("DecodeEvent x10k + views allocate %v times", n) } cm := xproto.ClientMessageEventData{Format: 32, Window: 1, Type: 2} cfg := xproto.ConfigureNotifyEventData{Window: 1, Width: 640, Height: 480} var out [32]byte if n := testing.AllocsPerRun(1000, func() { cm.Put(&out); cfg.Put(&out) }); n != 0 { t.Fatalf("EventData.Put allocates %v times", n) } b := xturbo.NewBuf(0) if n := testing.AllocsPerRun(1000, func() { b.Reset() cm.Put(&out) xproto.SendEvent(b, false, 1, xproto.EventMaskSubstructureRedirect, &out) }); n != 0 { t.Fatalf("SendEvent allocates %v times", n) } _ = sink } func BenchmarkDecodeEvents10k(b *testing.B) { stream := mixedEvents(b) ext := goldenExt() var sink uint64 b.SetBytes(int64(len(stream))) b.ReportAllocs() for b.Loop() { sink += decodeAll(b, stream, ext) } _ = sink } // FuzzDecodeEvent: no panic; n is 32 or the XGE length; Raw copies the first // 32 bytes; Tail stays within raw; Kind is consistent with Ext. extSeed picks // which extensions are negotiated and where their bases lie. func FuzzDecodeEvent(f *testing.F) { for _, name := range []string{"xproto/MapRequest", "xproto/ClientMessage", "xproto/KeymapNotify", "xfixes/SelectionNotify", "xproto/GeGeneric"} { f.Add(loadEvent(f, name), uint32(0x8a578c83)) } f.Add(make([]byte, 32), uint32(0)) f.Fuzz(func(t *testing.T, raw []byte, extSeed uint32) { var ext xturbo.ExtTable ext.Set(xturbo.ExtXFixes, uint8(extSeed>>24), uint8(extSeed>>16), uint8(extSeed>>8)) ext.Set(xturbo.ExtGenericEvent, uint8(extSeed), 0, 0) ext.Set(xturbo.ExtShape, uint8(extSeed>>4), uint8(extSeed>>12)|64, 0) table := &ext if extSeed&1 != 0 { table = nil } ev, n, ok := xturbo.DecodeEvent(raw, table) if !ok { if n != 0 { t.Fatal("n on failure") } return } if len(raw) < 32 || raw[0]&0x7f < 2 || n > len(raw) || !bytes.Equal(ev.Raw[:], raw[:32]) || ev.SendEvent != (raw[0]&0x80 != 0) { t.Fatalf("n %d", n) } if raw[0]&0x7f == 35 { if n != 32+4*int(binary.LittleEndian.Uint32(raw[4:])) || len(ev.Tail) != n-32 { t.Fatal("XGE framing") } } else if n != 32 || ev.Tail != nil { t.Fatal("fixed framing") } if ev.Kind != xturbo.KindUnknown && ev.Kind>>8 != xturbo.EventKind(ev.Ext) { t.Fatal("Kind/Ext mismatch") } _ = eventSink(&ev) }) }