detached 722264a

#2 feat(xtgen): decode events and errors

state
merged · 722264a · fast-forward
branch
feat/xtgen-events → main
opened
bnema ·
updated
CommitSubjectAuthorDateSignatureStatus
722264atest(xtest): compare live Xvfb captures independently of the server buildbriceGPG Verified
d5bc75bfix(xtgen): encode EventData into a local array before storing to dstbriceGPG Verified
7f3ee2efeat(xtgen): decode events and errors as tagged values with viewsbriceGPG Verified

Checks 4

43 files · +4569 -34
4/4
M cmd/xtgen/emit.go +5 -0
@@ -241,6 +241,11 @@ func emitTypes(modules []*Module) error {
241241 var extensions []*Module
242242 for _, m := range modules {
243243 if m.ExtName != "" {
244+ count, err := eventCount(m)
245+ if err != nil {
246+ return err
247+ }
248+ m.NumEvents, m.NumErrors = count, errorCount(m)
244249 extensions = append(extensions, m)
245250 }
246251 }
@@ -241,6 +241,11 @@ func emitTypes(modules []*Module) error {
241 var extensions []*Module241 var extensions []*Module
242 for _, m := range modules {242 for _, m := range modules {
243 if m.ExtName != "" {243 if m.ExtName != "" {
244+ count, err := eventCount(m)
245+ if err != nil {
246+ return err
247+ }
248+ m.NumEvents, m.NumErrors = count, errorCount(m)
244 extensions = append(extensions, m)249 extensions = append(extensions, m)
245 }250 }
246 }251 }
A cmd/xtgen/events.go +434 -0
@@ -0,0 +1,434 @@
1+package main
2+
3+import (
4+ "fmt"
5+ "path/filepath"
6+ "sort"
7+ "strconv"
8+ "strings"
9+)
10+
11+// eventIR is one <event> or <eventcopy>. Copies point at their source IR and
12+// share its container.
13+type eventIR struct {
14+ Name string // Go base name, e.g. MapRequest
15+ Code int // core: wire code; extension: relative code; XGE: evtype
16+ XGE bool
17+ Copy *eventIR
18+ C *container
19+ Node *Node
20+ KindExpr string // "" when the event has no Kind (core XGE envelope)
21+}
22+
23+const eventLifetime = "Getters read the copied Event.Raw, so a view stays valid as long as the Event value it was taken from."
24+
25+func eventCode(n *Node) (int, error) {
26+ code, err := strconv.Atoi(n.Attr["number"])
27+ if err != nil || code < 0 || code > 255 {
28+ return 0, n.fail("invalid event number %q", n.Attr["number"])
29+ }
30+ return code, nil
31+}
32+
33+func kindExpr(m *Module, e *eventIR) (string, error) {
34+ switch {
35+ case m.ExtName == "" && e.XGE:
36+ // The core XGE envelope is not an event type: DecodeEvent resolves
37+ // every XGE packet to its extension's kind or KindUnknown.
38+ return "", nil
39+ case m.ExtName == "":
40+ if e.Code < 2 || e.Code > 34 {
41+ return "", e.Node.fail("core event code %d outside 2..34", e.Code)
42+ }
43+ return fmt.Sprintf("xturbo.EventKind(%d)", e.Code), nil
44+ case e.XGE:
45+ return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s)<<8 | 0x80 | %d", m.ExtName, e.Code), nil
46+ }
47+ if e.Code > 127 {
48+ return "", e.Node.fail("extension event number %d above 127", e.Code)
49+ }
50+ if e.Code == 0 {
51+ // "| 0" is a no-op that staticcheck rejects (SA4016).
52+ return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s) << 8", m.ExtName), nil
53+ }
54+ return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s)<<8 | %d", m.ExtName, e.Code), nil
55+}
56+
57+// xgeReserved are the implicit XGE header getters.
58+const xgeReserved = "Raw Extension Length EventType Tail"
59+
60+func resolveEvents(m *Module) ([]*eventIR, error) {
61+ var events []*eventIR
62+ byXName := map[string]*eventIR{}
63+ for _, n := range m.Root.Children {
64+ if n.Kind != "event" && n.Kind != "eventcopy" {
65+ continue
66+ }
67+ code, err := eventCode(n)
68+ if err != nil {
69+ return nil, err
70+ }
71+ e := &eventIR{Name: goName(n.Attr["name"]), Code: code, Node: n}
72+ if n.Kind == "eventcopy" {
73+ src := byXName[n.Attr["ref"]]
74+ if src == nil {
75+ return nil, n.fail("eventcopy of unknown event %s", n.Attr["ref"])
76+ }
77+ if src.Copy != nil {
78+ src = src.Copy
79+ }
80+ e.Copy, e.C, e.XGE = src, src.C, src.XGE
81+ } else {
82+ e.XGE = n.Attr["xge"] == "true"
83+ lay, reserved := layoutEvent, "Raw"
84+ switch {
85+ case e.XGE:
86+ lay, reserved = layoutXGE, xgeReserved
87+ if code > 127 {
88+ return nil, n.fail("xge evtype %d >= 128", code)
89+ }
90+ case n.Attr["no-sequence-number"] == "true":
91+ lay = layoutEventNoSeq
92+ }
93+ c, err := buildContainer(m, e.Name+"Event", "e", n.Children, lay, reserved)
94+ if err != nil {
95+ return nil, err
96+ }
97+ if c.Dynamic >= 0 || c.Fixed > 32 {
98+ return nil, n.fail("event members must lie within the first 32 bytes")
99+ }
100+ for _, mem := range c.Members {
101+ if mem.Kind == "list" && mem.ListKind == "iter" {
102+ return nil, n.fail("variable-size struct list in event")
103+ }
104+ }
105+ e.C = c
106+ }
107+ if e.KindExpr, err = kindExpr(m, e); err != nil {
108+ return nil, err
109+ }
110+ names := []string{e.Name + "Event", e.Name + "Of"}
111+ if e.KindExpr != "" {
112+ names = append(names, "Kind"+e.Name)
113+ }
114+ if m.ExtName == "" && !e.XGE {
115+ names = append(names, e.Name+"EventData")
116+ }
117+ for _, s := range names {
118+ if err := m.claim(s, n); err != nil {
119+ return nil, err
120+ }
121+ }
122+ byXName[n.Attr["name"]] = e
123+ events = append(events, e)
124+ }
125+ return events, nil
126+}
127+
128+// dataField renders one EventData struct field and its Put statement.
129+func dataField(c *container, mem member) (field, put string, err error) {
130+ name := mem.Method
131+ if name == "Put" {
132+ name += "_"
133+ }
134+ src := "d." + name
135+ at := fmt.Sprintf("tmp[%d:]", mem.Static)
136+ switch {
137+ case mem.Kind == "field" && isScalar(mem.Type):
138+ return name + " " + mem.GoType, putScalar(mem.Type, src, at), nil
139+ case mem.Kind == "field":
140+ return name + " " + mem.GoType, fmt.Sprintf("%s.Put(%s);", src, at), nil
141+ case mem.Kind == "list" && mem.Count.Kind == "value":
142+ count := mem.Size / mem.Type.Size
143+ elem := mem.Elem
144+ var b strings.Builder
145+ fmt.Fprintf(&b, "for i := range %s {", src)
146+ target := fmt.Sprintf("tmp[%d+i*%d:]", mem.Static, mem.Type.Size)
147+ if isScalar(mem.Type) {
148+ b.WriteString(putScalar(mem.Type, src+"[i]", target))
149+ } else {
150+ fmt.Fprintf(&b, "%s[i].Put(%s);", src, target)
151+ }
152+ b.WriteString("};")
153+ return fmt.Sprintf("%s [%d]%s", name, count, elem), b.String(), nil
154+ }
155+ return "", "", c.Module.Root.fail("unsupported event member %s for EventData", mem.XName)
156+}
157+
158+func eventDecl(m *Module, e *eventIR, lists map[string]string) (string, error) {
159+ var b strings.Builder
160+ if e.KindExpr != "" {
161+ fmt.Fprintf(&b, "// Kind%s identifies %s events in xturbo.Event.Kind.\nconst Kind%s = %s;\n", e.Name, e.Name, e.Name, e.KindExpr)
162+ }
163+ view := e.Name + "Event"
164+ if e.Copy != nil {
165+ src := e.Copy.Name
166+ fmt.Fprintf(&b, "// %s shares %s's layout (eventcopy).\ntype %s = %sEvent;\n", view, src, view, src)
167+ fmt.Fprintf(&b, "// %sOf views ev as %s without checking ev.Kind.\nfunc %sOf(ev *xturbo.Event) %s { return %sOf(ev) };\n", e.Name, e.Name, e.Name, view, src)
168+ if m.ExtName == "" && !e.XGE {
169+ fmt.Fprintf(&b, "// %sEventData encodes a %s event for SendEvent with %s's fields.\ntype %sEventData %sEventData;\n", e.Name, e.Name, src, e.Name, src)
170+ fmt.Fprintf(&b, "// Put writes the event (code %d, pads and sequence zeroed) into dst.\nfunc (d *%sEventData) Put(dst *[32]byte) { (*%sEventData)(d).Put(dst); dst[0] = %d };\n", e.Code, e.Name, src, e.Code)
171+ }
172+ return b.String(), nil
173+ }
174+ c := e.C
175+ if e.XGE {
176+ fmt.Fprintf(&b, "// %s is a view over a %s generic event: the copied Event.Raw plus the borrowed Event.Tail.\n// Tail is valid only until the Handler callback returns; copy what you keep.\n", view, e.Name)
177+ fmt.Fprintf(&b, "type %s struct { raw *[32]byte; tail []byte };\n", view)
178+ fmt.Fprintf(&b, "// %sOf views ev as %s; false when ev is not a generic event or Tail does not match its length.\n", e.Name, e.Name)
179+ fmt.Fprintf(&b, "func %sOf(ev *xturbo.Event) (%s, bool) { if ev.Raw[0]&0x7f != 35 || len(ev.Tail) != 4*int(binary.LittleEndian.Uint32(ev.Raw[4:])) { return %s{}, false }; return %s{raw: &ev.Raw, tail: ev.Tail}, true };\n", e.Name, view, view, view)
180+ fmt.Fprintf(&b, "func (e %s) Extension() uint8 { return e.raw[1] };\n", view)
181+ fmt.Fprintf(&b, "func (e %s) Length() uint32 { return binary.LittleEndian.Uint32(e.raw[4:]) };\n", view)
182+ fmt.Fprintf(&b, "func (e %s) EventType() uint16 { return binary.LittleEndian.Uint16(e.raw[8:]) };\n", view)
183+ fmt.Fprintf(&b, "func (e %s) Tail() []byte { return e.tail };\n", view)
184+ } else {
185+ fmt.Fprintf(&b, "// %s is a view over a %s event's 32 bytes. %s\n", view, e.Name, eventLifetime)
186+ fmt.Fprintf(&b, "type %s struct { raw *[32]byte };\n", view)
187+ fmt.Fprintf(&b, "// %sOf views ev as %s without checking ev.Kind: a wrong kind yields garbage, never a panic.\n", e.Name, e.Name)
188+ fmt.Fprintf(&b, "func %sOf(ev *xturbo.Event) %s { return %s{raw: &ev.Raw} };\n", e.Name, view, view)
189+ }
190+ fmt.Fprintf(&b, "func (e %s) Raw() *[32]byte { return e.raw };\n", view)
191+ if err := c.getters(&b, view, lists); err != nil {
192+ return "", err
193+ }
194+ if m.ExtName != "" || e.XGE {
195+ return b.String(), nil
196+ }
197+ var fields, puts []string
198+ for _, mem := range c.Members {
199+ if mem.Kind == "pad" {
200+ continue
201+ }
202+ f, p, err := dataField(c, mem)
203+ if err != nil {
204+ return "", err
205+ }
206+ fields = append(fields, f)
207+ puts = append(puts, p)
208+ }
209+ fmt.Fprintf(&b, "// %sEventData encodes a %s event for SendEvent.\ntype %sEventData struct {\n%s\n};\n", e.Name, e.Name, e.Name, strings.Join(fields, "\n"))
210+ // Encode into a local array and store it last: borrowed fields (union
211+ // views such as ClientMessageData) may alias dst, e.g. d.Put(&ev.Raw).
212+ fmt.Fprintf(&b, "// Put writes the event (code %d, pads and sequence zeroed) into dst. dst may alias\n// memory that d's borrowed fields view, such as the Event.Raw they were read from.\nfunc (d *%sEventData) Put(dst *[32]byte) { var tmp [32]byte; tmp[0] = %d; %s; *dst = tmp };\n", e.Code, e.Name, e.Code, strings.Join(puts, ""))
213+ return b.String(), nil
214+}
215+
216+// errorIR is one <error> or <errorcopy>.
217+type errorIR struct {
218+ Const, Name string
219+ Code int
220+}
221+
222+func errorConst(xname string) string {
223+ return "Bad" + goName(strings.TrimPrefix(xname, "Bad"))
224+}
225+
226+func resolveErrors(m *Module) ([]errorIR, error) {
227+ var errs []errorIR
228+ seen := map[int]bool{}
229+ for _, n := range m.Root.Children {
230+ if n.Kind != "error" && n.Kind != "errorcopy" {
231+ continue
232+ }
233+ code, err := strconv.Atoi(n.Attr["number"])
234+ if err != nil || code < 0 || code > 255 || seen[code] {
235+ return nil, n.fail("invalid or duplicate error number %q", n.Attr["number"])
236+ }
237+ seen[code] = true
238+ e := errorIR{Const: errorConst(n.Attr["name"]), Code: code}
239+ e.Name = e.Const
240+ if err := m.claim(e.Const, n); err != nil {
241+ return nil, err
242+ }
243+ errs = append(errs, e)
244+ }
245+ return errs, nil
246+}
247+
248+// errorCount is one past the highest error number.
249+func errorCount(m *Module) int {
250+ count := 0
251+ for _, n := range m.Root.Children {
252+ if n.Kind == "error" || n.Kind == "errorcopy" {
253+ code, _ := strconv.Atoi(n.Attr["number"])
254+ count = max(count, code+1)
255+ }
256+ }
257+ return count
258+}
259+
260+// eventCount is one past the highest non-XGE event number.
261+func eventCount(m *Module) (int, error) {
262+ count := 0
263+ for _, n := range m.Root.Children {
264+ if (n.Kind == "event" || n.Kind == "eventcopy") && n.Attr["xge"] != "true" {
265+ code, err := eventCode(n)
266+ if err != nil {
267+ return 0, err
268+ }
269+ count = max(count, code+1)
270+ }
271+ }
272+ return count, nil
273+}
274+
275+func errorsDecl(m *Module, errs []errorIR) string {
276+ var b strings.Builder
277+ what := "wire error codes"
278+ if m.ExtName != "" {
279+ what = "error codes relative to the extension's FirstError (XError.Rel)"
280+ }
281+ fmt.Fprintf(&b, "// Error codes: %s.\nconst (\n", what)
282+ for _, e := range errs {
283+ fmt.Fprintf(&b, "%s uint8 = %d\n", e.Const, e.Code)
284+ }
285+ b.WriteString(");\n")
286+ names := make([]string, errorCount(m))
287+ for _, e := range errs {
288+ names[e.Code] = e.Name
289+ }
290+ b.WriteString("var errorNames = [...]string{")
291+ for _, n := range names {
292+ fmt.Fprintf(&b, "%q,", n)
293+ }
294+ b.WriteString("};\n")
295+ fmt.Fprintf(&b, "// ErrorName returns the name of an error code (%s), or \"\" when unknown.\n", what)
296+ b.WriteString("func ErrorName(code uint8) string { if int(code) < len(errorNames) { return errorNames[code] }; return \"\" };\n")
297+ return b.String()
298+}
299+
300+func emitEvents(modules []*Module) error {
301+ for _, m := range modules {
302+ events, err := resolveEvents(m)
303+ if err != nil {
304+ return err
305+ }
306+ lists := map[string]string{}
307+ var decls []string
308+ for _, e := range events {
309+ d, err := eventDecl(m, e, lists)
310+ if err != nil {
311+ return err
312+ }
313+ decls = append(decls, d)
314+ }
315+ var names []string
316+ for name := range lists {
317+ // Reuse a list view already declared by replies_generated.go.
318+ if old := m.Names[name]; old != nil && old == m.Root {
319+ continue
320+ }
321+ if err := m.claim(name, m.Root); err != nil {
322+ return err
323+ }
324+ names = append(names, name)
325+ }
326+ sort.Strings(names)
327+ for _, name := range names {
328+ decls = append(decls, lists[name])
329+ }
330+ errs, err := resolveErrors(m)
331+ if err != nil {
332+ return err
333+ }
334+ if len(errs) > 0 {
335+ data := struct {
336+ Pkg string
337+ Decls []string
338+ }{Pkg: m.Pkg, Decls: []string{errorsDecl(m, errs)}}
339+ if err := writeTemplate(filepath.Join(m.Pkg, "errors_generated.go"), "errors", data); err != nil {
340+ return err
341+ }
342+ }
343+ if len(decls) == 0 {
344+ continue
345+ }
346+ imports := importsFor(m, strings.Join(decls, "\n")) + "\nimport \"bnema.dev/xturbo\""
347+ decls = append([]string{imports}, decls...)
348+ data := struct {
349+ Pkg string
350+ Decls []string
351+ }{Pkg: m.Pkg, Decls: decls}
352+ if err := writeTemplate(filepath.Join(m.Pkg, "events_generated.go"), "events", data); err != nil {
353+ return err
354+ }
355+ if err := emitEventFuzz(m, events); err != nil {
356+ return err
357+ }
358+ }
359+ return nil
360+}
361+
362+// emitEventFuzz writes FuzzEventViews: every view getter runs on arbitrary
363+// bytes, and for core events EventData built from the getters survives a
364+// Put/view round trip. Seeds are the package's event goldens.
365+func emitEventFuzz(m *Module, events []*eventIR) error {
366+ var b strings.Builder
367+ b.WriteString("func FuzzEventViews(f *testing.F) {\nf.Add(make([]byte, 32));\n")
368+ goldens, _ := filepath.Glob(filepath.Join("testdata/golden", m.Pkg, "*.event.hex"))
369+ sort.Strings(goldens)
370+ for _, g := range goldens {
371+ fmt.Fprintf(&b, "if raw, err := xtest.LoadHex(%q); err != nil { f.Fatal(err) } else { f.Add(raw) };\n", "../"+filepath.ToSlash(g))
372+ }
373+ b.WriteString("f.Fuzz(func(t *testing.T, raw []byte) {\nif len(raw) < 32 { return };\nvar ev xturbo.Event;\ncopy(ev.Raw[:], raw);\nev.Tail = raw[32:];\n")
374+ for _, e := range events {
375+ if e.Copy != nil {
376+ fmt.Fprintf(&b, "_ = %sOf(&ev);\n", e.Name)
377+ continue
378+ }
379+ v := "v" + e.Name
380+ if e.XGE {
381+ fmt.Fprintf(&b, "if %s, ok := %sOf(&ev); ok { _ = %s.Extension(); _ = %s.Length(); _ = %s.EventType(); _ = len(%s.Tail());", v, e.Name, v, v, v, v)
382+ } else {
383+ fmt.Fprintf(&b, "{ %s := %sOf(&ev);", v, e.Name)
384+ }
385+ touch(&b, e.C, v, 0, nil, `t.Fatal("iterator error")`)
386+ if m.ExtName == "" && !e.XGE {
387+ var fields, checks []string
388+ for _, mem := range e.C.Members {
389+ if mem.Kind == "pad" {
390+ continue
391+ }
392+ name := mem.Method
393+ if name == "Put" {
394+ name += "_"
395+ }
396+ switch {
397+ case mem.Kind == "list" && mem.ListKind == "bytes":
398+ fields = append(fields, fmt.Sprintf("%s: [%d]%s(%s.%s())", name, mem.Size/mem.Type.Size, mem.Elem, v, mem.Method))
399+ checks = append(checks, fmt.Sprintf("if !bytes.Equal(w.%s(), %s.%s()) { t.Fatal(%q) };", mem.Method, v, mem.Method, e.Name+"."+mem.Method))
400+ case mem.Kind == "field" && isScalar(mem.Type):
401+ fields = append(fields, fmt.Sprintf("%s: %s.%s()", name, v, mem.Method))
402+ checks = append(checks, fmt.Sprintf("if w.%s() != %s.%s() { t.Fatal(%q) };", mem.Method, v, mem.Method, e.Name+"."+mem.Method))
403+ case mem.Kind == "field":
404+ fields = append(fields, fmt.Sprintf("%s: %s.%s()", name, v, mem.Method))
405+ checks = append(checks, fmt.Sprintf("if !bytes.Equal(out[%d:%d], ev.Raw[%d:%d]) { t.Fatal(%q) };", mem.Static, mem.Static+mem.Size, mem.Static, mem.Static+mem.Size, e.Name+"."+mem.Method))
406+ default:
407+ return e.Node.fail("event member %s has no EventData round trip", mem.XName)
408+ }
409+ }
410+ fmt.Fprintf(&b, "d := %sEventData{%s}; var out [32]byte; d.Put(&out);", e.Name, strings.Join(fields, ", "))
411+ fmt.Fprintf(&b, "if out[0] != %d { t.Fatal(\"%s code\") };", e.Code, e.Name)
412+ b.WriteString("back := xturbo.Event{Raw: out};")
413+ fmt.Fprintf(&b, "w := %sOf(&back); %s", e.Name, strings.Join(checks, ""))
414+ // In place: dst aliases the Raw that d's borrowed fields view.
415+ fmt.Fprintf(&b, "in := xturbo.Event{Raw: ev.Raw}; vi := %sOf(&in); di := %sEventData{%s}; di.Put(&in.Raw); if in.Raw != out { t.Fatal(%q) };",
416+ e.Name, e.Name, strings.ReplaceAll(strings.Join(fields, ", "), v+".", "vi."), e.Name+" in-place Put")
417+ }
418+ b.WriteString("};\n")
419+ }
420+ b.WriteString("})\n}\n")
421+ body := b.String()
422+ imports := "import \"testing\"\nimport \"bnema.dev/xturbo\""
423+ if len(goldens) > 0 {
424+ imports += "\nimport \"bnema.dev/xturbo/internal/xtest\""
425+ }
426+ if strings.Contains(body, "bytes.") {
427+ imports += "\nimport \"bytes\""
428+ }
429+ data := struct {
430+ Pkg string
431+ Decls []string
432+ }{Pkg: m.Pkg, Decls: []string{imports, body}}
433+ return writeTemplate(filepath.Join(m.Pkg, "events_fuzz_test.go"), "events", data)
434+}
@@ -0,0 +1,434 @@
1+package main
2+
3+import (
4+ "fmt"
5+ "path/filepath"
6+ "sort"
7+ "strconv"
8+ "strings"
9+)
10+
11+// eventIR is one <event> or <eventcopy>. Copies point at their source IR and
12+// share its container.
13+type eventIR struct {
14+ Name string // Go base name, e.g. MapRequest
15+ Code int // core: wire code; extension: relative code; XGE: evtype
16+ XGE bool
17+ Copy *eventIR
18+ C *container
19+ Node *Node
20+ KindExpr string // "" when the event has no Kind (core XGE envelope)
21+}
22+
23+const eventLifetime = "Getters read the copied Event.Raw, so a view stays valid as long as the Event value it was taken from."
24+
25+func eventCode(n *Node) (int, error) {
26+ code, err := strconv.Atoi(n.Attr["number"])
27+ if err != nil || code < 0 || code > 255 {
28+ return 0, n.fail("invalid event number %q", n.Attr["number"])
29+ }
30+ return code, nil
31+}
32+
33+func kindExpr(m *Module, e *eventIR) (string, error) {
34+ switch {
35+ case m.ExtName == "" && e.XGE:
36+ // The core XGE envelope is not an event type: DecodeEvent resolves
37+ // every XGE packet to its extension's kind or KindUnknown.
38+ return "", nil
39+ case m.ExtName == "":
40+ if e.Code < 2 || e.Code > 34 {
41+ return "", e.Node.fail("core event code %d outside 2..34", e.Code)
42+ }
43+ return fmt.Sprintf("xturbo.EventKind(%d)", e.Code), nil
44+ case e.XGE:
45+ return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s)<<8 | 0x80 | %d", m.ExtName, e.Code), nil
46+ }
47+ if e.Code > 127 {
48+ return "", e.Node.fail("extension event number %d above 127", e.Code)
49+ }
50+ if e.Code == 0 {
51+ // "| 0" is a no-op that staticcheck rejects (SA4016).
52+ return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s) << 8", m.ExtName), nil
53+ }
54+ return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s)<<8 | %d", m.ExtName, e.Code), nil
55+}
56+
57+// xgeReserved are the implicit XGE header getters.
58+const xgeReserved = "Raw Extension Length EventType Tail"
59+
60+func resolveEvents(m *Module) ([]*eventIR, error) {
61+ var events []*eventIR
62+ byXName := map[string]*eventIR{}
63+ for _, n := range m.Root.Children {
64+ if n.Kind != "event" && n.Kind != "eventcopy" {
65+ continue
66+ }
67+ code, err := eventCode(n)
68+ if err != nil {
69+ return nil, err
70+ }
71+ e := &eventIR{Name: goName(n.Attr["name"]), Code: code, Node: n}
72+ if n.Kind == "eventcopy" {
73+ src := byXName[n.Attr["ref"]]
74+ if src == nil {
75+ return nil, n.fail("eventcopy of unknown event %s", n.Attr["ref"])
76+ }
77+ if src.Copy != nil {
78+ src = src.Copy
79+ }
80+ e.Copy, e.C, e.XGE = src, src.C, src.XGE
81+ } else {
82+ e.XGE = n.Attr["xge"] == "true"
83+ lay, reserved := layoutEvent, "Raw"
84+ switch {
85+ case e.XGE:
86+ lay, reserved = layoutXGE, xgeReserved
87+ if code > 127 {
88+ return nil, n.fail("xge evtype %d >= 128", code)
89+ }
90+ case n.Attr["no-sequence-number"] == "true":
91+ lay = layoutEventNoSeq
92+ }
93+ c, err := buildContainer(m, e.Name+"Event", "e", n.Children, lay, reserved)
94+ if err != nil {
95+ return nil, err
96+ }
97+ if c.Dynamic >= 0 || c.Fixed > 32 {
98+ return nil, n.fail("event members must lie within the first 32 bytes")
99+ }
100+ for _, mem := range c.Members {
101+ if mem.Kind == "list" && mem.ListKind == "iter" {
102+ return nil, n.fail("variable-size struct list in event")
103+ }
104+ }
105+ e.C = c
106+ }
107+ if e.KindExpr, err = kindExpr(m, e); err != nil {
108+ return nil, err
109+ }
110+ names := []string{e.Name + "Event", e.Name + "Of"}
111+ if e.KindExpr != "" {
112+ names = append(names, "Kind"+e.Name)
113+ }
114+ if m.ExtName == "" && !e.XGE {
115+ names = append(names, e.Name+"EventData")
116+ }
117+ for _, s := range names {
118+ if err := m.claim(s, n); err != nil {
119+ return nil, err
120+ }
121+ }
122+ byXName[n.Attr["name"]] = e
123+ events = append(events, e)
124+ }
125+ return events, nil
126+}
127+
128+// dataField renders one EventData struct field and its Put statement.
129+func dataField(c *container, mem member) (field, put string, err error) {
130+ name := mem.Method
131+ if name == "Put" {
132+ name += "_"
133+ }
134+ src := "d." + name
135+ at := fmt.Sprintf("tmp[%d:]", mem.Static)
136+ switch {
137+ case mem.Kind == "field" && isScalar(mem.Type):
138+ return name + " " + mem.GoType, putScalar(mem.Type, src, at), nil
139+ case mem.Kind == "field":
140+ return name + " " + mem.GoType, fmt.Sprintf("%s.Put(%s);", src, at), nil
141+ case mem.Kind == "list" && mem.Count.Kind == "value":
142+ count := mem.Size / mem.Type.Size
143+ elem := mem.Elem
144+ var b strings.Builder
145+ fmt.Fprintf(&b, "for i := range %s {", src)
146+ target := fmt.Sprintf("tmp[%d+i*%d:]", mem.Static, mem.Type.Size)
147+ if isScalar(mem.Type) {
148+ b.WriteString(putScalar(mem.Type, src+"[i]", target))
149+ } else {
150+ fmt.Fprintf(&b, "%s[i].Put(%s);", src, target)
151+ }
152+ b.WriteString("};")
153+ return fmt.Sprintf("%s [%d]%s", name, count, elem), b.String(), nil
154+ }
155+ return "", "", c.Module.Root.fail("unsupported event member %s for EventData", mem.XName)
156+}
157+
158+func eventDecl(m *Module, e *eventIR, lists map[string]string) (string, error) {
159+ var b strings.Builder
160+ if e.KindExpr != "" {
161+ fmt.Fprintf(&b, "// Kind%s identifies %s events in xturbo.Event.Kind.\nconst Kind%s = %s;\n", e.Name, e.Name, e.Name, e.KindExpr)
162+ }
163+ view := e.Name + "Event"
164+ if e.Copy != nil {
165+ src := e.Copy.Name
166+ fmt.Fprintf(&b, "// %s shares %s's layout (eventcopy).\ntype %s = %sEvent;\n", view, src, view, src)
167+ fmt.Fprintf(&b, "// %sOf views ev as %s without checking ev.Kind.\nfunc %sOf(ev *xturbo.Event) %s { return %sOf(ev) };\n", e.Name, e.Name, e.Name, view, src)
168+ if m.ExtName == "" && !e.XGE {
169+ fmt.Fprintf(&b, "// %sEventData encodes a %s event for SendEvent with %s's fields.\ntype %sEventData %sEventData;\n", e.Name, e.Name, src, e.Name, src)
170+ fmt.Fprintf(&b, "// Put writes the event (code %d, pads and sequence zeroed) into dst.\nfunc (d *%sEventData) Put(dst *[32]byte) { (*%sEventData)(d).Put(dst); dst[0] = %d };\n", e.Code, e.Name, src, e.Code)
171+ }
172+ return b.String(), nil
173+ }
174+ c := e.C
175+ if e.XGE {
176+ fmt.Fprintf(&b, "// %s is a view over a %s generic event: the copied Event.Raw plus the borrowed Event.Tail.\n// Tail is valid only until the Handler callback returns; copy what you keep.\n", view, e.Name)
177+ fmt.Fprintf(&b, "type %s struct { raw *[32]byte; tail []byte };\n", view)
178+ fmt.Fprintf(&b, "// %sOf views ev as %s; false when ev is not a generic event or Tail does not match its length.\n", e.Name, e.Name)
179+ fmt.Fprintf(&b, "func %sOf(ev *xturbo.Event) (%s, bool) { if ev.Raw[0]&0x7f != 35 || len(ev.Tail) != 4*int(binary.LittleEndian.Uint32(ev.Raw[4:])) { return %s{}, false }; return %s{raw: &ev.Raw, tail: ev.Tail}, true };\n", e.Name, view, view, view)
180+ fmt.Fprintf(&b, "func (e %s) Extension() uint8 { return e.raw[1] };\n", view)
181+ fmt.Fprintf(&b, "func (e %s) Length() uint32 { return binary.LittleEndian.Uint32(e.raw[4:]) };\n", view)
182+ fmt.Fprintf(&b, "func (e %s) EventType() uint16 { return binary.LittleEndian.Uint16(e.raw[8:]) };\n", view)
183+ fmt.Fprintf(&b, "func (e %s) Tail() []byte { return e.tail };\n", view)
184+ } else {
185+ fmt.Fprintf(&b, "// %s is a view over a %s event's 32 bytes. %s\n", view, e.Name, eventLifetime)
186+ fmt.Fprintf(&b, "type %s struct { raw *[32]byte };\n", view)
187+ fmt.Fprintf(&b, "// %sOf views ev as %s without checking ev.Kind: a wrong kind yields garbage, never a panic.\n", e.Name, e.Name)
188+ fmt.Fprintf(&b, "func %sOf(ev *xturbo.Event) %s { return %s{raw: &ev.Raw} };\n", e.Name, view, view)
189+ }
190+ fmt.Fprintf(&b, "func (e %s) Raw() *[32]byte { return e.raw };\n", view)
191+ if err := c.getters(&b, view, lists); err != nil {
192+ return "", err
193+ }
194+ if m.ExtName != "" || e.XGE {
195+ return b.String(), nil
196+ }
197+ var fields, puts []string
198+ for _, mem := range c.Members {
199+ if mem.Kind == "pad" {
200+ continue
201+ }
202+ f, p, err := dataField(c, mem)
203+ if err != nil {
204+ return "", err
205+ }
206+ fields = append(fields, f)
207+ puts = append(puts, p)
208+ }
209+ fmt.Fprintf(&b, "// %sEventData encodes a %s event for SendEvent.\ntype %sEventData struct {\n%s\n};\n", e.Name, e.Name, e.Name, strings.Join(fields, "\n"))
210+ // Encode into a local array and store it last: borrowed fields (union
211+ // views such as ClientMessageData) may alias dst, e.g. d.Put(&ev.Raw).
212+ fmt.Fprintf(&b, "// Put writes the event (code %d, pads and sequence zeroed) into dst. dst may alias\n// memory that d's borrowed fields view, such as the Event.Raw they were read from.\nfunc (d *%sEventData) Put(dst *[32]byte) { var tmp [32]byte; tmp[0] = %d; %s; *dst = tmp };\n", e.Code, e.Name, e.Code, strings.Join(puts, ""))
213+ return b.String(), nil
214+}
215+
216+// errorIR is one <error> or <errorcopy>.
217+type errorIR struct {
218+ Const, Name string
219+ Code int
220+}
221+
222+func errorConst(xname string) string {
223+ return "Bad" + goName(strings.TrimPrefix(xname, "Bad"))
224+}
225+
226+func resolveErrors(m *Module) ([]errorIR, error) {
227+ var errs []errorIR
228+ seen := map[int]bool{}
229+ for _, n := range m.Root.Children {
230+ if n.Kind != "error" && n.Kind != "errorcopy" {
231+ continue
232+ }
233+ code, err := strconv.Atoi(n.Attr["number"])
234+ if err != nil || code < 0 || code > 255 || seen[code] {
235+ return nil, n.fail("invalid or duplicate error number %q", n.Attr["number"])
236+ }
237+ seen[code] = true
238+ e := errorIR{Const: errorConst(n.Attr["name"]), Code: code}
239+ e.Name = e.Const
240+ if err := m.claim(e.Const, n); err != nil {
241+ return nil, err
242+ }
243+ errs = append(errs, e)
244+ }
245+ return errs, nil
246+}
247+
248+// errorCount is one past the highest error number.
249+func errorCount(m *Module) int {
250+ count := 0
251+ for _, n := range m.Root.Children {
252+ if n.Kind == "error" || n.Kind == "errorcopy" {
253+ code, _ := strconv.Atoi(n.Attr["number"])
254+ count = max(count, code+1)
255+ }
256+ }
257+ return count
258+}
259+
260+// eventCount is one past the highest non-XGE event number.
261+func eventCount(m *Module) (int, error) {
262+ count := 0
263+ for _, n := range m.Root.Children {
264+ if (n.Kind == "event" || n.Kind == "eventcopy") && n.Attr["xge"] != "true" {
265+ code, err := eventCode(n)
266+ if err != nil {
267+ return 0, err
268+ }
269+ count = max(count, code+1)
270+ }
271+ }
272+ return count, nil
273+}
274+
275+func errorsDecl(m *Module, errs []errorIR) string {
276+ var b strings.Builder
277+ what := "wire error codes"
278+ if m.ExtName != "" {
279+ what = "error codes relative to the extension's FirstError (XError.Rel)"
280+ }
281+ fmt.Fprintf(&b, "// Error codes: %s.\nconst (\n", what)
282+ for _, e := range errs {
283+ fmt.Fprintf(&b, "%s uint8 = %d\n", e.Const, e.Code)
284+ }
285+ b.WriteString(");\n")
286+ names := make([]string, errorCount(m))
287+ for _, e := range errs {
288+ names[e.Code] = e.Name
289+ }
290+ b.WriteString("var errorNames = [...]string{")
291+ for _, n := range names {
292+ fmt.Fprintf(&b, "%q,", n)
293+ }
294+ b.WriteString("};\n")
295+ fmt.Fprintf(&b, "// ErrorName returns the name of an error code (%s), or \"\" when unknown.\n", what)
296+ b.WriteString("func ErrorName(code uint8) string { if int(code) < len(errorNames) { return errorNames[code] }; return \"\" };\n")
297+ return b.String()
298+}
299+
300+func emitEvents(modules []*Module) error {
301+ for _, m := range modules {
302+ events, err := resolveEvents(m)
303+ if err != nil {
304+ return err
305+ }
306+ lists := map[string]string{}
307+ var decls []string
308+ for _, e := range events {
309+ d, err := eventDecl(m, e, lists)
310+ if err != nil {
311+ return err
312+ }
313+ decls = append(decls, d)
314+ }
315+ var names []string
316+ for name := range lists {
317+ // Reuse a list view already declared by replies_generated.go.
318+ if old := m.Names[name]; old != nil && old == m.Root {
319+ continue
320+ }
321+ if err := m.claim(name, m.Root); err != nil {
322+ return err
323+ }
324+ names = append(names, name)
325+ }
326+ sort.Strings(names)
327+ for _, name := range names {
328+ decls = append(decls, lists[name])
329+ }
330+ errs, err := resolveErrors(m)
331+ if err != nil {
332+ return err
333+ }
334+ if len(errs) > 0 {
335+ data := struct {
336+ Pkg string
337+ Decls []string
338+ }{Pkg: m.Pkg, Decls: []string{errorsDecl(m, errs)}}
339+ if err := writeTemplate(filepath.Join(m.Pkg, "errors_generated.go"), "errors", data); err != nil {
340+ return err
341+ }
342+ }
343+ if len(decls) == 0 {
344+ continue
345+ }
346+ imports := importsFor(m, strings.Join(decls, "\n")) + "\nimport \"bnema.dev/xturbo\""
347+ decls = append([]string{imports}, decls...)
348+ data := struct {
349+ Pkg string
350+ Decls []string
351+ }{Pkg: m.Pkg, Decls: decls}
352+ if err := writeTemplate(filepath.Join(m.Pkg, "events_generated.go"), "events", data); err != nil {
353+ return err
354+ }
355+ if err := emitEventFuzz(m, events); err != nil {
356+ return err
357+ }
358+ }
359+ return nil
360+}
361+
362+// emitEventFuzz writes FuzzEventViews: every view getter runs on arbitrary
363+// bytes, and for core events EventData built from the getters survives a
364+// Put/view round trip. Seeds are the package's event goldens.
365+func emitEventFuzz(m *Module, events []*eventIR) error {
366+ var b strings.Builder
367+ b.WriteString("func FuzzEventViews(f *testing.F) {\nf.Add(make([]byte, 32));\n")
368+ goldens, _ := filepath.Glob(filepath.Join("testdata/golden", m.Pkg, "*.event.hex"))
369+ sort.Strings(goldens)
370+ for _, g := range goldens {
371+ fmt.Fprintf(&b, "if raw, err := xtest.LoadHex(%q); err != nil { f.Fatal(err) } else { f.Add(raw) };\n", "../"+filepath.ToSlash(g))
372+ }
373+ b.WriteString("f.Fuzz(func(t *testing.T, raw []byte) {\nif len(raw) < 32 { return };\nvar ev xturbo.Event;\ncopy(ev.Raw[:], raw);\nev.Tail = raw[32:];\n")
374+ for _, e := range events {
375+ if e.Copy != nil {
376+ fmt.Fprintf(&b, "_ = %sOf(&ev);\n", e.Name)
377+ continue
378+ }
379+ v := "v" + e.Name
380+ if e.XGE {
381+ fmt.Fprintf(&b, "if %s, ok := %sOf(&ev); ok { _ = %s.Extension(); _ = %s.Length(); _ = %s.EventType(); _ = len(%s.Tail());", v, e.Name, v, v, v, v)
382+ } else {
383+ fmt.Fprintf(&b, "{ %s := %sOf(&ev);", v, e.Name)
384+ }
385+ touch(&b, e.C, v, 0, nil, `t.Fatal("iterator error")`)
386+ if m.ExtName == "" && !e.XGE {
387+ var fields, checks []string
388+ for _, mem := range e.C.Members {
389+ if mem.Kind == "pad" {
390+ continue
391+ }
392+ name := mem.Method
393+ if name == "Put" {
394+ name += "_"
395+ }
396+ switch {
397+ case mem.Kind == "list" && mem.ListKind == "bytes":
398+ fields = append(fields, fmt.Sprintf("%s: [%d]%s(%s.%s())", name, mem.Size/mem.Type.Size, mem.Elem, v, mem.Method))
399+ checks = append(checks, fmt.Sprintf("if !bytes.Equal(w.%s(), %s.%s()) { t.Fatal(%q) };", mem.Method, v, mem.Method, e.Name+"."+mem.Method))
400+ case mem.Kind == "field" && isScalar(mem.Type):
401+ fields = append(fields, fmt.Sprintf("%s: %s.%s()", name, v, mem.Method))
402+ checks = append(checks, fmt.Sprintf("if w.%s() != %s.%s() { t.Fatal(%q) };", mem.Method, v, mem.Method, e.Name+"."+mem.Method))
403+ case mem.Kind == "field":
404+ fields = append(fields, fmt.Sprintf("%s: %s.%s()", name, v, mem.Method))
405+ checks = append(checks, fmt.Sprintf("if !bytes.Equal(out[%d:%d], ev.Raw[%d:%d]) { t.Fatal(%q) };", mem.Static, mem.Static+mem.Size, mem.Static, mem.Static+mem.Size, e.Name+"."+mem.Method))
406+ default:
407+ return e.Node.fail("event member %s has no EventData round trip", mem.XName)
408+ }
409+ }
410+ fmt.Fprintf(&b, "d := %sEventData{%s}; var out [32]byte; d.Put(&out);", e.Name, strings.Join(fields, ", "))
411+ fmt.Fprintf(&b, "if out[0] != %d { t.Fatal(\"%s code\") };", e.Code, e.Name)
412+ b.WriteString("back := xturbo.Event{Raw: out};")
413+ fmt.Fprintf(&b, "w := %sOf(&back); %s", e.Name, strings.Join(checks, ""))
414+ // In place: dst aliases the Raw that d's borrowed fields view.
415+ fmt.Fprintf(&b, "in := xturbo.Event{Raw: ev.Raw}; vi := %sOf(&in); di := %sEventData{%s}; di.Put(&in.Raw); if in.Raw != out { t.Fatal(%q) };",
416+ e.Name, e.Name, strings.ReplaceAll(strings.Join(fields, ", "), v+".", "vi."), e.Name+" in-place Put")
417+ }
418+ b.WriteString("};\n")
419+ }
420+ b.WriteString("})\n}\n")
421+ body := b.String()
422+ imports := "import \"testing\"\nimport \"bnema.dev/xturbo\""
423+ if len(goldens) > 0 {
424+ imports += "\nimport \"bnema.dev/xturbo/internal/xtest\""
425+ }
426+ if strings.Contains(body, "bytes.") {
427+ imports += "\nimport \"bytes\""
428+ }
429+ data := struct {
430+ Pkg string
431+ Decls []string
432+ }{Pkg: m.Pkg, Decls: []string{imports, body}}
433+ return writeTemplate(filepath.Join(m.Pkg, "events_fuzz_test.go"), "events", data)
434+}
A cmd/xtgen/events_test.go +82 -0
@@ -0,0 +1,82 @@
1+package main
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+// TestEventLayouts pins wire offsets that the X11 protocol defines, checked
9+// against the event containers (independent of generated code).
10+func TestEventLayouts(t *testing.T) {
11+ want := map[string]map[string]int{
12+ "xproto.MapRequest": {"Parent": 4, "Window": 8},
13+ "xproto.ConfigureRequest": {"StackMode": 1, "Parent": 4, "Window": 8, "Sibling": 12, "X": 16, "Y": 18, "Width": 20, "Height": 22, "BorderWidth": 24, "ValueMask": 26},
14+ "xproto.KeymapNotify": {"Keys": 1},
15+ "xproto.ClientMessage": {"Format": 1, "Window": 4, "Type": 8, "Data": 12},
16+ "xproto.PropertyNotify": {"Window": 4, "Atom": 8, "Time": 12, "State": 16},
17+ "xfixes.SelectionNotify": {"Subtype": 1, "Window": 4, "Owner": 8, "Selection": 12, "Timestamp": 16, "SelectionTimestamp": 20},
18+ "shape.Notify": {"ShapeKind": 1, "AffectedWindow": 4, "ServerTime": 16, "Shaped": 20},
19+ }
20+ copies := 0
21+ for _, m := range testModules(t) {
22+ events, err := resolveEvents(m)
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ for _, e := range events {
27+ if e.Copy != nil {
28+ copies++
29+ if e.C != e.Copy.C {
30+ t.Errorf("%s: copy does not share its source layout", e.Name)
31+ }
32+ continue
33+ }
34+ exp := want[m.Pkg+"."+e.Name]
35+ for _, mem := range e.C.Members {
36+ if off, ok := exp[mem.Method]; ok {
37+ if mem.Static != off {
38+ t.Errorf("%s.%s.%s at %d want %d", m.Pkg, e.Name, mem.Method, mem.Static, off)
39+ }
40+ delete(exp, mem.Method)
41+ }
42+ }
43+ if len(exp) != 0 {
44+ t.Errorf("%s.%s: missing %v", m.Pkg, e.Name, exp)
45+ }
46+ }
47+ }
48+ if copies != 5 {
49+ t.Errorf("xproto has 5 eventcopy, got %d", copies)
50+ }
51+}
52+
53+func TestEventGeneratorRejects(t *testing.T) {
54+ for _, tc := range []struct{ body, want string }{
55+ {`<event name="E" number="200" xge="true"><pad bytes="22"/></event>`, "xge evtype"},
56+ {`<event name="E" number="1"><field type="CARD32" name="a"/></event>`, "header data byte"},
57+ {`<eventcopy name="C" number="3" ref="Missing"/>`, "unknown event"},
58+ {`<event name="E" number="2"><pad bytes="1"/><list type="CARD8" name="l"><fieldref>n</fieldref></list></event>`, "first 32 bytes"},
59+ } {
60+ // Some constructs are already rejected by the parser; either stage must
61+ // fail with a source location.
62+ n, err := parseXML("ev.xml", strings.NewReader(`<xcb header="test" extension-name="Test" extension-xname="TEST">`+tc.body+`</xcb>`))
63+ if err == nil {
64+ var ms []*Module
65+ if ms, err = resolveModules([]*Node{n}); err != nil {
66+ t.Fatal(err)
67+ }
68+ _, err = resolveEvents(ms[0])
69+ }
70+ if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "ev.xml:1:") {
71+ t.Errorf("%s: got %v", tc.want, err)
72+ }
73+ }
74+}
75+
76+func TestErrorConstNames(t *testing.T) {
77+ for in, want := range map[string]string{"Window": "BadWindow", "BadRegion": "BadRegion", "PictFormat": "BadPictFormat", "IDChoice": "BadIDChoice"} {
78+ if got := errorConst(in); got != want {
79+ t.Errorf("%s => %s want %s", in, got, want)
80+ }
81+ }
82+}
@@ -0,0 +1,82 @@
1+package main
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+// TestEventLayouts pins wire offsets that the X11 protocol defines, checked
9+// against the event containers (independent of generated code).
10+func TestEventLayouts(t *testing.T) {
11+ want := map[string]map[string]int{
12+ "xproto.MapRequest": {"Parent": 4, "Window": 8},
13+ "xproto.ConfigureRequest": {"StackMode": 1, "Parent": 4, "Window": 8, "Sibling": 12, "X": 16, "Y": 18, "Width": 20, "Height": 22, "BorderWidth": 24, "ValueMask": 26},
14+ "xproto.KeymapNotify": {"Keys": 1},
15+ "xproto.ClientMessage": {"Format": 1, "Window": 4, "Type": 8, "Data": 12},
16+ "xproto.PropertyNotify": {"Window": 4, "Atom": 8, "Time": 12, "State": 16},
17+ "xfixes.SelectionNotify": {"Subtype": 1, "Window": 4, "Owner": 8, "Selection": 12, "Timestamp": 16, "SelectionTimestamp": 20},
18+ "shape.Notify": {"ShapeKind": 1, "AffectedWindow": 4, "ServerTime": 16, "Shaped": 20},
19+ }
20+ copies := 0
21+ for _, m := range testModules(t) {
22+ events, err := resolveEvents(m)
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ for _, e := range events {
27+ if e.Copy != nil {
28+ copies++
29+ if e.C != e.Copy.C {
30+ t.Errorf("%s: copy does not share its source layout", e.Name)
31+ }
32+ continue
33+ }
34+ exp := want[m.Pkg+"."+e.Name]
35+ for _, mem := range e.C.Members {
36+ if off, ok := exp[mem.Method]; ok {
37+ if mem.Static != off {
38+ t.Errorf("%s.%s.%s at %d want %d", m.Pkg, e.Name, mem.Method, mem.Static, off)
39+ }
40+ delete(exp, mem.Method)
41+ }
42+ }
43+ if len(exp) != 0 {
44+ t.Errorf("%s.%s: missing %v", m.Pkg, e.Name, exp)
45+ }
46+ }
47+ }
48+ if copies != 5 {
49+ t.Errorf("xproto has 5 eventcopy, got %d", copies)
50+ }
51+}
52+
53+func TestEventGeneratorRejects(t *testing.T) {
54+ for _, tc := range []struct{ body, want string }{
55+ {`<event name="E" number="200" xge="true"><pad bytes="22"/></event>`, "xge evtype"},
56+ {`<event name="E" number="1"><field type="CARD32" name="a"/></event>`, "header data byte"},
57+ {`<eventcopy name="C" number="3" ref="Missing"/>`, "unknown event"},
58+ {`<event name="E" number="2"><pad bytes="1"/><list type="CARD8" name="l"><fieldref>n</fieldref></list></event>`, "first 32 bytes"},
59+ } {
60+ // Some constructs are already rejected by the parser; either stage must
61+ // fail with a source location.
62+ n, err := parseXML("ev.xml", strings.NewReader(`<xcb header="test" extension-name="Test" extension-xname="TEST">`+tc.body+`</xcb>`))
63+ if err == nil {
64+ var ms []*Module
65+ if ms, err = resolveModules([]*Node{n}); err != nil {
66+ t.Fatal(err)
67+ }
68+ _, err = resolveEvents(ms[0])
69+ }
70+ if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "ev.xml:1:") {
71+ t.Errorf("%s: got %v", tc.want, err)
72+ }
73+ }
74+}
75+
76+func TestErrorConstNames(t *testing.T) {
77+ for in, want := range map[string]string{"Window": "BadWindow", "BadRegion": "BadRegion", "PictFormat": "BadPictFormat", "IDChoice": "BadIDChoice"} {
78+ if got := errorConst(in); got != want {
79+ t.Errorf("%s => %s want %s", in, got, want)
80+ }
81+ }
82+}
M cmd/xtgen/ir.go +1 -0
@@ -36,4 +36,5 @@ type Module struct {
3636 Symbols map[string]*Type
3737 Names map[string]*Node
3838 Decls, SizeChecks []string
39+ NumEvents, NumErrors int // extension event/error code ranges
3940 }
@@ -36,4 +36,5 @@ type Module struct {
36 Symbols map[string]*Type36 Symbols map[string]*Type
37 Names map[string]*Node37 Names map[string]*Node
38 Decls, SizeChecks []string38 Decls, SizeChecks []string
39+ NumEvents, NumErrors int // extension event/error code ranges
39 }40 }
M cmd/xtgen/main.go +3 -0
@@ -35,6 +35,9 @@ func main() {
3535 if err == nil {
3636 err = emitReplies(modules)
3737 }
38+ if err == nil {
39+ err = emitEvents(modules)
40+ }
3841 if err != nil {
3942 fmt.Fprintln(os.Stderr, err)
4043 os.Exit(1)
@@ -35,6 +35,9 @@ func main() {
35 if err == nil {35 if err == nil {
36 err = emitReplies(modules)36 err = emitReplies(modules)
37 }37 }
38+ if err == nil {
39+ err = emitEvents(modules)
40+ }
38 if err != nil {41 if err != nil {
39 fmt.Fprintln(os.Stderr, err)42 fmt.Fprintln(os.Stderr, err)
40 os.Exit(1)43 os.Exit(1)
M cmd/xtgen/replies.go +39 -10
@@ -58,14 +58,43 @@ func elemName(goType string) string {
5858 return strings.ToUpper(goType[:1]) + goType[1:]
5959 }
6060
61-// buildContainer lowers XML members. Replies start at byte 1 (the first XML
62-// member occupies the header data byte) and continue at byte 8.
63-func buildContainer(m *Module, name, recv string, nodes []*Node, reply bool, reserved string) (*container, error) {
61+// layout selects where a container's XML members land on the wire.
62+type layout int
63+
64+const (
65+ layoutStruct layout = iota // members from byte 0
66+ layoutReply // byte 1, then byte 8 (after seq and length)
67+ layoutEvent // byte 1, then byte 4 (after seq)
68+ layoutEventNoSeq // contiguous from byte 1
69+ layoutXGE // from byte 10 (after extension, seq, length, evtype)
70+)
71+
72+// headerJump returns the offset following the header data byte, or 0 when the
73+// layout has no data byte.
74+func (l layout) headerJump() int {
75+ switch l {
76+ case layoutReply:
77+ return 8
78+ case layoutEvent:
79+ return 4
80+ }
81+ return 0
82+}
83+
84+// buildContainer lowers XML members. Replies and events start at byte 1 (the
85+// first XML member occupies the header data byte) and continue after the
86+// sequence (and, for replies, length) header.
87+func buildContainer(m *Module, name, recv string, nodes []*Node, lay layout, reserved string) (*container, error) {
88+ reply := lay == layoutReply
6489 c := &container{Name: name, Recv: recv, Reply: reply, Dynamic: -1, Module: m}
6590 off := 0
66- if reply {
91+ switch lay {
92+ case layoutReply, layoutEvent, layoutEventNoSeq:
6793 off = 1
94+ case layoutXGE:
95+ off = 10
6896 }
97+ what := map[layout]string{layoutStruct: "struct", layoutReply: "reply", layoutEvent: "event", layoutEventNoSeq: "event", layoutXGE: "event"}[lay]
6998 first := true
7099 methods := map[string]bool{}
71100 for _, n := range nodes {
@@ -138,13 +167,13 @@ func buildContainer(m *Module, name, recv string, nodes []*Node, reply bool, res
138167 c.Dynamic = len(c.Members)
139168 }
140169 default:
141- return nil, n.fail("unsupported %s member %s", map[bool]string{true: "reply", false: "struct"}[reply], n.Kind)
170+ return nil, n.fail("unsupported %s member %s", what, n.Kind)
142171 }
143- if reply && first {
172+ if jump := lay.headerJump(); jump > 0 && first {
144173 if mem.Size != 1 {
145- return nil, n.fail("first reply member must occupy the header data byte")
174+ return nil, n.fail("first %s member must occupy the header data byte", what)
146175 }
147- off = 8
176+ off = jump
148177 first = false
149178 c.Members = append(c.Members, mem)
150179 continue
@@ -340,7 +369,7 @@ func resolveReplies(m *Module) ([]replyIR, []*container, error) {
340369 return nil, nil, err
341370 }
342371 }
343- c, err := buildContainer(m, name+"Reply", "r", r.Children, true, "Raw")
372+ c, err := buildContainer(m, name+"Reply", "r", r.Children, layoutReply, "Raw")
344373 if err != nil {
345374 return nil, nil, err
346375 }
@@ -357,7 +386,7 @@ func resolveReplies(m *Module) ([]replyIR, []*container, error) {
357386 return nil, nil, err
358387 }
359388 }
360- c, err := buildContainer(m, t.GoType+"View", "v", t.Node.Children, false, "Raw Size")
389+ c, err := buildContainer(m, t.GoType+"View", "v", t.Node.Children, layoutStruct, "Raw Size")
361390 if err != nil {
362391 return nil, nil, err
363392 }
@@ -58,14 +58,43 @@ func elemName(goType string) string {
58 return strings.ToUpper(goType[:1]) + goType[1:]58 return strings.ToUpper(goType[:1]) + goType[1:]
59 }59 }
60 60
61-// buildContainer lowers XML members. Replies start at byte 1 (the first XML61+// layout selects where a container's XML members land on the wire.
62-// member occupies the header data byte) and continue at byte 8.62+type layout int
63-func buildContainer(m *Module, name, recv string, nodes []*Node, reply bool, reserved string) (*container, error) {63+
64+const (
65+ layoutStruct layout = iota // members from byte 0
66+ layoutReply // byte 1, then byte 8 (after seq and length)
67+ layoutEvent // byte 1, then byte 4 (after seq)
68+ layoutEventNoSeq // contiguous from byte 1
69+ layoutXGE // from byte 10 (after extension, seq, length, evtype)
70+)
71+
72+// headerJump returns the offset following the header data byte, or 0 when the
73+// layout has no data byte.
74+func (l layout) headerJump() int {
75+ switch l {
76+ case layoutReply:
77+ return 8
78+ case layoutEvent:
79+ return 4
80+ }
81+ return 0
82+}
83+
84+// buildContainer lowers XML members. Replies and events start at byte 1 (the
85+// first XML member occupies the header data byte) and continue after the
86+// sequence (and, for replies, length) header.
87+func buildContainer(m *Module, name, recv string, nodes []*Node, lay layout, reserved string) (*container, error) {
88+ reply := lay == layoutReply
64 c := &container{Name: name, Recv: recv, Reply: reply, Dynamic: -1, Module: m}89 c := &container{Name: name, Recv: recv, Reply: reply, Dynamic: -1, Module: m}
65 off := 090 off := 0
66- if reply {91+ switch lay {
92+ case layoutReply, layoutEvent, layoutEventNoSeq:
67 off = 193 off = 1
94+ case layoutXGE:
95+ off = 10
68 }96 }
97+ what := map[layout]string{layoutStruct: "struct", layoutReply: "reply", layoutEvent: "event", layoutEventNoSeq: "event", layoutXGE: "event"}[lay]
69 first := true98 first := true
70 methods := map[string]bool{}99 methods := map[string]bool{}
71 for _, n := range nodes {100 for _, n := range nodes {
@@ -138,13 +167,13 @@ func buildContainer(m *Module, name, recv string, nodes []*Node, reply bool, res
138 c.Dynamic = len(c.Members)167 c.Dynamic = len(c.Members)
139 }168 }
140 default:169 default:
141- return nil, n.fail("unsupported %s member %s", map[bool]string{true: "reply", false: "struct"}[reply], n.Kind)170+ return nil, n.fail("unsupported %s member %s", what, n.Kind)
142 }171 }
143- if reply && first {172+ if jump := lay.headerJump(); jump > 0 && first {
144 if mem.Size != 1 {173 if mem.Size != 1 {
145- return nil, n.fail("first reply member must occupy the header data byte")174+ return nil, n.fail("first %s member must occupy the header data byte", what)
146 }175 }
147- off = 8176+ off = jump
148 first = false177 first = false
149 c.Members = append(c.Members, mem)178 c.Members = append(c.Members, mem)
150 continue179 continue
@@ -340,7 +369,7 @@ func resolveReplies(m *Module) ([]replyIR, []*container, error) {
340 return nil, nil, err369 return nil, nil, err
341 }370 }
342 }371 }
343- c, err := buildContainer(m, name+"Reply", "r", r.Children, true, "Raw")372+ c, err := buildContainer(m, name+"Reply", "r", r.Children, layoutReply, "Raw")
344 if err != nil {373 if err != nil {
345 return nil, nil, err374 return nil, nil, err
346 }375 }
@@ -357,7 +386,7 @@ func resolveReplies(m *Module) ([]replyIR, []*container, error) {
357 return nil, nil, err386 return nil, nil, err
358 }387 }
359 }388 }
360- c, err := buildContainer(m, t.GoType+"View", "v", t.Node.Children, false, "Raw Size")389+ c, err := buildContainer(m, t.GoType+"View", "v", t.Node.Children, layoutStruct, "Raw Size")
361 if err != nil {390 if err != nil {
362 return nil, nil, err391 return nil, nil, err
363 }392 }
M cmd/xtgen/replies_test.go +1 -1
@@ -60,7 +60,7 @@ func TestReplyReservedGetter(t *testing.T) {
6060 {Kind: "pad", Attr: map[string]string{"bytes": "1"}},
6161 {Kind: "field", Attr: map[string]string{"name": "raw", "type": "CARD32"}},
6262 }
63- c, err := buildContainer(m, "XReply", "r", nodes, true, "Raw")
63+ c, err := buildContainer(m, "XReply", "r", nodes, layoutReply, "Raw")
6464 if err != nil {
6565 t.Fatal(err)
6666 }
@@ -60,7 +60,7 @@ func TestReplyReservedGetter(t *testing.T) {
60 {Kind: "pad", Attr: map[string]string{"bytes": "1"}},60 {Kind: "pad", Attr: map[string]string{"bytes": "1"}},
61 {Kind: "field", Attr: map[string]string{"name": "raw", "type": "CARD32"}},61 {Kind: "field", Attr: map[string]string{"name": "raw", "type": "CARD32"}},
62 }62 }
63- c, err := buildContainer(m, "XReply", "r", nodes, true, "Raw")63+ c, err := buildContainer(m, "XReply", "r", nodes, layoutReply, "Raw")
64 if err != nil {64 if err != nil {
65 t.Fatal(err)65 t.Fatal(err)
66 }66 }
A cmd/xtgen/templates/errors.tmpl +5 -0
@@ -0,0 +1,5 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package {{.Pkg}}
3+{{range .Decls}}
4+{{.}}
5+{{end}}
@@ -0,0 +1,5 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package {{.Pkg}}
3+{{range .Decls}}
4+{{.}}
5+{{end}}
A cmd/xtgen/templates/events.tmpl +5 -0
@@ -0,0 +1,5 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package {{.Pkg}}
3+{{range .Decls}}
4+{{.}}
5+{{end}}
@@ -0,0 +1,5 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package {{.Pkg}}
3+{{range .Decls}}
4+{{.}}
5+{{end}}
M cmd/xtgen/templates/ext.tmpl +4 -0
@@ -8,3 +8,7 @@ const (
88 {{end}} extCount
99 )
1010 var ExtXName = [extCount]string{"", {{range .}}{{printf "%q" .XName}},{{end}}}
11+// extEvents and extErrors are the event and error code ranges each extension
12+// declares in the pinned XML; ExtTable.Set uses them to fill reverse maps.
13+var extEvents = [extCount]uint8{0, {{range .}}{{.NumEvents}},{{end}}}
14+var extErrors = [extCount]uint8{0, {{range .}}{{.NumErrors}},{{end}}}
@@ -8,3 +8,7 @@ const (
8 {{end}} extCount8 {{end}} extCount
9 )9 )
10 var ExtXName = [extCount]string{"", {{range .}}{{printf "%q" .XName}},{{end}}}10 var ExtXName = [extCount]string{"", {{range .}}{{printf "%q" .XName}},{{end}}}
11+// extEvents and extErrors are the event and error code ranges each extension
12+// declares in the pinned XML; ExtTable.Set uses them to fill reverse maps.
13+var extEvents = [extCount]uint8{0, {{range .}}{{.NumEvents}},{{end}}}
14+var extErrors = [extCount]uint8{0, {{range .}}{{.NumErrors}},{{end}}}
A event.go +77 -0
@@ -0,0 +1,77 @@
1+package xturbo
2+
3+import "encoding/binary"
4+
5+// EventKind identifies a decoded event: uint16(ExtID)<<8 | code. Core events
6+// use the wire code (2..34); extension events the code relative to the
7+// extension's first event; XGE events 0x80|evtype.
8+type EventKind uint16
9+
10+// KindUnknown marks an event whose code or XGE extension is not known.
11+const KindUnknown EventKind = 0xFFFF
12+
13+const (
14+ codeGenericEvent = 35
15+ codeKeymapNotify = 11
16+)
17+
18+// Event is a decoded event value. Raw is a copy and survives Dispatch; Tail
19+// borrows the read buffer (XGE bytes beyond 32) and is valid only until the
20+// Handler callback returns. Copy what you keep.
21+type Event struct {
22+ Kind EventKind
23+ SendEvent bool
24+ Ext ExtID
25+ Seq Seq // wire sequence; Conn widens it. 0 for KeymapNotify.
26+ Raw [32]byte
27+ Tail []byte
28+}
29+
30+// DecodeEvent decodes one event packet at the start of raw. n is the number
31+// of bytes consumed (32, or 32+4*length for XGE). ok is false for error and
32+// reply packets and for short input. Unknown codes decode with
33+// Kind == KindUnknown and ok == true. A nil ext decodes core events only.
34+// The returned Seq is the 16-bit wire value; Conn widens it.
35+func DecodeEvent(raw []byte, ext *ExtTable) (ev Event, n int, ok bool) {
36+ if len(raw) < 32 {
37+ return Event{}, 0, false
38+ }
39+ code := raw[0] & 0x7f
40+ if code < 2 {
41+ return Event{}, 0, false
42+ }
43+ n = 32
44+ if code == codeGenericEvent {
45+ length := uint64(binary.LittleEndian.Uint32(raw[4:]))
46+ if uint64(len(raw)-32)/4 < length {
47+ return Event{}, 0, false
48+ }
49+ n = 32 + int(length)*4
50+ ev.Tail = raw[32:n:n]
51+ }
52+ copy(ev.Raw[:], raw[:32])
53+ ev.SendEvent = raw[0]&0x80 != 0
54+ if code != codeKeymapNotify {
55+ ev.Seq = Seq(binary.LittleEndian.Uint16(raw[2:]))
56+ }
57+ ev.Kind = KindUnknown
58+ switch {
59+ case code < codeGenericEvent:
60+ ev.Kind = EventKind(code)
61+ case code == codeGenericEvent:
62+ if ext == nil {
63+ break
64+ }
65+ evtype := binary.LittleEndian.Uint16(raw[8:])
66+ if id, found := ext.ByMajor(raw[1]); found && evtype < 0x80 {
67+ ev.Ext = id
68+ ev.Kind = EventKind(id)<<8 | 0x80 | EventKind(evtype)
69+ }
70+ case code >= 64 && ext != nil:
71+ if id, rel, found := ext.ByEvent(code); found {
72+ ev.Ext = id
73+ ev.Kind = EventKind(id)<<8 | EventKind(rel)
74+ }
75+ }
76+ return ev, n, true
77+}
@@ -0,0 +1,77 @@
1+package xturbo
2+
3+import "encoding/binary"
4+
5+// EventKind identifies a decoded event: uint16(ExtID)<<8 | code. Core events
6+// use the wire code (2..34); extension events the code relative to the
7+// extension's first event; XGE events 0x80|evtype.
8+type EventKind uint16
9+
10+// KindUnknown marks an event whose code or XGE extension is not known.
11+const KindUnknown EventKind = 0xFFFF
12+
13+const (
14+ codeGenericEvent = 35
15+ codeKeymapNotify = 11
16+)
17+
18+// Event is a decoded event value. Raw is a copy and survives Dispatch; Tail
19+// borrows the read buffer (XGE bytes beyond 32) and is valid only until the
20+// Handler callback returns. Copy what you keep.
21+type Event struct {
22+ Kind EventKind
23+ SendEvent bool
24+ Ext ExtID
25+ Seq Seq // wire sequence; Conn widens it. 0 for KeymapNotify.
26+ Raw [32]byte
27+ Tail []byte
28+}
29+
30+// DecodeEvent decodes one event packet at the start of raw. n is the number
31+// of bytes consumed (32, or 32+4*length for XGE). ok is false for error and
32+// reply packets and for short input. Unknown codes decode with
33+// Kind == KindUnknown and ok == true. A nil ext decodes core events only.
34+// The returned Seq is the 16-bit wire value; Conn widens it.
35+func DecodeEvent(raw []byte, ext *ExtTable) (ev Event, n int, ok bool) {
36+ if len(raw) < 32 {
37+ return Event{}, 0, false
38+ }
39+ code := raw[0] & 0x7f
40+ if code < 2 {
41+ return Event{}, 0, false
42+ }
43+ n = 32
44+ if code == codeGenericEvent {
45+ length := uint64(binary.LittleEndian.Uint32(raw[4:]))
46+ if uint64(len(raw)-32)/4 < length {
47+ return Event{}, 0, false
48+ }
49+ n = 32 + int(length)*4
50+ ev.Tail = raw[32:n:n]
51+ }
52+ copy(ev.Raw[:], raw[:32])
53+ ev.SendEvent = raw[0]&0x80 != 0
54+ if code != codeKeymapNotify {
55+ ev.Seq = Seq(binary.LittleEndian.Uint16(raw[2:]))
56+ }
57+ ev.Kind = KindUnknown
58+ switch {
59+ case code < codeGenericEvent:
60+ ev.Kind = EventKind(code)
61+ case code == codeGenericEvent:
62+ if ext == nil {
63+ break
64+ }
65+ evtype := binary.LittleEndian.Uint16(raw[8:])
66+ if id, found := ext.ByMajor(raw[1]); found && evtype < 0x80 {
67+ ev.Ext = id
68+ ev.Kind = EventKind(id)<<8 | 0x80 | EventKind(evtype)
69+ }
70+ case code >= 64 && ext != nil:
71+ if id, rel, found := ext.ByEvent(code); found {
72+ ev.Ext = id
73+ ev.Kind = EventKind(id)<<8 | EventKind(rel)
74+ }
75+ }
76+ return ev, n, true
77+}
A events_test.go +499 -0
@@ -0,0 +1,499 @@
1+package xturbo_test
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "testing"
7+
8+ "bnema.dev/xturbo"
9+ "bnema.dev/xturbo/internal/xtest"
10+ "bnema.dev/xturbo/render"
11+ "bnema.dev/xturbo/shape"
12+ "bnema.dev/xturbo/xfixes"
13+ "bnema.dev/xturbo/xproto"
14+)
15+
16+// Xvfb negotiated XFIXES as major 138, first event 87, first error 140 when the
17+// goldens were captured; the BadRegion and SelectionNotify goldens confirm it.
18+const (
19+ xfixesMajor, xfixesEvent, xfixesError = 138, 87, 140
20+ geMajor = 0x83 // hand-written XGE golden
21+)
22+
23+func loadEvent(t testing.TB, name string) []byte {
24+ t.Helper()
25+ raw, err := xtest.LoadHex("testdata/golden/" + name + ".event.hex")
26+ if err != nil {
27+ t.Fatal(err)
28+ }
29+ return raw
30+}
31+
32+func goldenExt() *xturbo.ExtTable {
33+ var ext xturbo.ExtTable
34+ ext.Set(xturbo.ExtXFixes, xfixesMajor, xfixesEvent, xfixesError)
35+ ext.Set(xturbo.ExtGenericEvent, geMajor, 0, 0)
36+ ext.Set(xturbo.ExtShape, 129, 64, 0)
37+ return &ext
38+}
39+
40+func decode(t testing.TB, raw []byte, ext *xturbo.ExtTable) xturbo.Event {
41+ t.Helper()
42+ ev, n, ok := xturbo.DecodeEvent(raw, ext)
43+ if !ok || n != len(raw) || !bytes.Equal(ev.Raw[:], raw[:32]) {
44+ t.Fatalf("DecodeEvent(%x) = n %d ok %v", raw, n, ok)
45+ }
46+ return ev
47+}
48+
49+// Expected values follow from the capture script (second client's window at
50+// 10,20 300x200, ConfigureWindow x=-5 y=40 width=640, a format-32 ClientMessage
51+// with words 0x01020304*(i+1), SetSelectionOwner PRIMARY) or from raw golden
52+// offsets defined by the X11 protocol, not from generated code.
53+func TestEventGoldens(t *testing.T) {
54+ u32 := func(raw []byte, off int) uint32 { return binary.LittleEndian.Uint32(raw[off:]) }
55+ ext := goldenExt()
56+ core := func(name string, kind xturbo.EventKind) ([]byte, xturbo.Event) {
57+ raw := loadEvent(t, "xproto/"+name)
58+ ev := decode(t, raw, ext)
59+ if ev.Kind != kind || ev.Ext != xturbo.ExtCore || ev.Seq != xturbo.Seq(binary.LittleEndian.Uint16(raw[2:])) || ev.Tail != nil {
60+ t.Fatalf("%s: kind %d ext %d seq %d", name, ev.Kind, ev.Ext, ev.Seq)
61+ }
62+ return raw, ev
63+ }
64+ t.Run("MapRequest", func(t *testing.T) {
65+ raw, ev := core("MapRequest", xproto.KindMapRequest)
66+ e := xproto.MapRequestOf(&ev)
67+ if ev.SendEvent || e.Parent() != xproto.Window(u32(raw, 4)) || e.Window() != xproto.Window(u32(raw, 8)) || e.Window()&0xffff != 1 {
68+ t.Fatal("fields")
69+ }
70+ d := xproto.MapRequestEventData{Parent: e.Parent(), Window: e.Window()}
71+ var out [32]byte
72+ d.Put(&out)
73+ want := [32]byte(raw)
74+ want[2], want[3] = 0, 0 // Put leaves the sequence to the server
75+ if out != want {
76+ t.Fatalf("Put %x want %x", out, want)
77+ }
78+ })
79+ t.Run("ConfigureRequest", func(t *testing.T) {
80+ raw, ev := core("ConfigureRequest", xproto.KindConfigureRequest)
81+ e := xproto.ConfigureRequestOf(&ev)
82+ if e.StackMode() != xproto.StackModeAbove || e.Parent() != xproto.Window(u32(raw, 4)) || e.Window()&0xffff != 1 ||
83+ e.Sibling() != xproto.WindowNone || e.X() != -5 || e.Y() != 40 || e.Width() != 640 || e.Height() != 200 ||
84+ e.BorderWidth() != 0 || e.ValueMask() != xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth {
85+ t.Fatalf("fields x=%d y=%d w=%d h=%d mask=%d", e.X(), e.Y(), e.Width(), e.Height(), e.ValueMask())
86+ }
87+ d := xproto.ConfigureRequestEventData{StackMode: e.StackMode(), Parent: e.Parent(), Window: e.Window(), Sibling: e.Sibling(),
88+ X: e.X(), Y: e.Y(), Width: e.Width(), Height: e.Height(), BorderWidth: e.BorderWidth(), ValueMask: e.ValueMask()}
89+ var out [32]byte
90+ d.Put(&out)
91+ if !bytes.Equal(out[4:], raw[4:]) || out[0] != 23 || out[1] != raw[1] {
92+ t.Fatalf("Put %x", out)
93+ }
94+ })
95+ t.Run("PropertyNotify", func(t *testing.T) {
96+ raw, ev := core("PropertyNotify", xproto.KindPropertyNotify)
97+ e := xproto.PropertyNotifyOf(&ev)
98+ if e.Window() != xproto.Window(u32(raw, 4)) || e.Atom() != xproto.Atom(u32(raw, 8)) || e.Atom() < 69 ||
99+ e.Time() != xproto.Timestamp(u32(raw, 12)) || e.State() != xproto.PropertyNewValue {
100+ t.Fatal("fields")
101+ }
102+ })
103+ t.Run("ClientMessage", func(t *testing.T) {
104+ raw, ev := core("ClientMessage", xproto.KindClientMessage)
105+ e := xproto.ClientMessageOf(&ev)
106+ if !ev.SendEvent || e.Format() != 32 || e.Window()&0xffff != 1 || e.Type() != xproto.Atom(u32(raw, 8)) {
107+ t.Fatal("fields")
108+ }
109+ words := e.Data().Data32()
110+ for i, w := range words {
111+ if w != uint32(0x01020304*(i+1)) {
112+ t.Fatalf("data32[%d] = %x", i, w)
113+ }
114+ }
115+ d := xproto.ClientMessageEventData{Format: 32, Window: e.Window(), Type: e.Type(), Data: e.Data()}
116+ var out [32]byte
117+ d.Put(&out)
118+ want := [32]byte(raw)
119+ want[0] &^= 0x80
120+ want[2], want[3] = 0, 0
121+ if out != want {
122+ t.Fatalf("Put %x want %x", out, want)
123+ }
124+ })
125+ t.Run("CreateNotify", func(t *testing.T) {
126+ _, ev := core("CreateNotify", xproto.KindCreateNotify)
127+ e := xproto.CreateNotifyOf(&ev)
128+ if e.X() != 10 || e.Y() != 20 || e.Width() != 300 || e.Height() != 200 || e.OverrideRedirect() {
129+ t.Fatal("fields")
130+ }
131+ })
132+ t.Run("Map/Unmap/DestroyNotify", func(t *testing.T) {
133+ _, ev := core("MapNotify", xproto.KindMapNotify)
134+ if m := xproto.MapNotifyOf(&ev); m.Window()&0xffff != 1 || m.OverrideRedirect() {
135+ t.Fatal("MapNotify")
136+ }
137+ _, ev = core("UnmapNotify", xproto.KindUnmapNotify)
138+ if u := xproto.UnmapNotifyOf(&ev); u.Window()&0xffff != 1 || u.FromConfigure() {
139+ t.Fatal("UnmapNotify")
140+ }
141+ _, ev = core("DestroyNotify", xproto.KindDestroyNotify)
142+ if d := xproto.DestroyNotifyOf(&ev); d.Window()&0xffff != 1 {
143+ t.Fatal("DestroyNotify")
144+ }
145+ })
146+ t.Run("SelectionNotify", func(t *testing.T) {
147+ _, ev := core("SelectionNotify", xproto.KindSelectionNotify)
148+ e := xproto.SelectionNotifyOf(&ev)
149+ if e.Requestor()&0xffff != 1 || e.Selection() != xproto.AtomSecondary || e.Target() != xproto.AtomString || e.Property() != xproto.AtomNone {
150+ t.Fatal("fields")
151+ }
152+ })
153+ t.Run("xfixes/SelectionNotify", func(t *testing.T) {
154+ raw := loadEvent(t, "xfixes/SelectionNotify")
155+ ev := decode(t, raw, ext)
156+ if xfixes.KindSelectionNotify != xturbo.EventKind(xturbo.ExtXFixes)<<8 {
157+ t.Fatal("KindSelectionNotify value")
158+ }
159+ if ev.Kind != xfixes.KindSelectionNotify || ev.Ext != xturbo.ExtXFixes {
160+ t.Fatalf("kind %x", ev.Kind)
161+ }
162+ e := xfixes.SelectionNotifyOf(&ev)
163+ if e.Subtype() != xfixes.SelectionEventSetSelectionOwner || e.Window() != xproto.Window(u32(raw, 4)) ||
164+ e.Owner()&0xffff != 1 || e.Selection() != xproto.AtomPrimary || e.Timestamp() != e.SelectionTimestamp() {
165+ t.Fatal("fields")
166+ }
167+ // Without XFIXES negotiated the same bytes are an unknown event.
168+ if ev, _, ok := xturbo.DecodeEvent(raw, nil); !ok || ev.Kind != xturbo.KindUnknown {
169+ t.Fatal("nil ext must decode as unknown")
170+ }
171+ })
172+ t.Run("KeymapNotify", func(t *testing.T) {
173+ raw := loadEvent(t, "xproto/KeymapNotify")
174+ ev := decode(t, raw, ext)
175+ if ev.Kind != xproto.KindKeymapNotify || ev.Seq != 0 {
176+ t.Fatalf("kind %d seq %d", ev.Kind, ev.Seq)
177+ }
178+ e := xproto.KeymapNotifyOf(&ev)
179+ if !bytes.Equal(e.Keys(), raw[1:]) {
180+ t.Fatal("keys")
181+ }
182+ d := xproto.KeymapNotifyEventData{Keys: [31]uint8(raw[1:])}
183+ var out [32]byte
184+ d.Put(&out)
185+ if !bytes.Equal(out[:], raw) {
186+ t.Fatalf("Put %x", out)
187+ }
188+ })
189+ t.Run("GeGeneric", func(t *testing.T) {
190+ raw := loadEvent(t, "xproto/GeGeneric")
191+ ev := decode(t, raw, ext)
192+ if ev.Kind != xturbo.EventKind(xturbo.ExtGenericEvent)<<8|0x80|1 || ev.Ext != xturbo.ExtGenericEvent || ev.Seq != 5 ||
193+ !bytes.Equal(ev.Tail, raw[32:]) {
194+ t.Fatalf("kind %x tail %x", ev.Kind, ev.Tail)
195+ }
196+ g, ok := xproto.GeGenericOf(&ev)
197+ if !ok || g.Extension() != geMajor || g.Length() != 2 || g.EventType() != 1 || !bytes.Equal(g.Tail(), raw[32:]) {
198+ t.Fatal("view")
199+ }
200+ // Unknown major: still framed so Dispatch skips the tail.
201+ ev, n, ok := xturbo.DecodeEvent(raw, nil)
202+ if !ok || n != 40 || ev.Kind != xturbo.KindUnknown || len(ev.Tail) != 8 {
203+ t.Fatal("unknown XGE framing")
204+ }
205+ ev.Tail = ev.Tail[:4]
206+ if _, ok := xproto.GeGenericOf(&ev); ok {
207+ t.Fatal("short Tail accepted")
208+ }
209+ })
210+}
211+
212+// TestEventDataPutInPlace re-encodes events into the Event.Raw their fields
213+// were read from. ClientMessage's Data is a union view over Raw; KeymapNotify's
214+// Keys is a slice view over Raw. Put must read every field before writing dst.
215+func TestEventDataPutInPlace(t *testing.T) {
216+ raw := loadEvent(t, "xproto/ClientMessage")
217+ ev := decode(t, raw, nil)
218+ e := xproto.ClientMessageOf(&ev)
219+ d := xproto.ClientMessageEventData{Format: e.Format(), Window: e.Window(), Type: e.Type(), Data: e.Data()}
220+ d.Put(&ev.Raw)
221+ want := [32]byte(raw)
222+ want[0] &^= 0x80
223+ want[2], want[3] = 0, 0
224+ if ev.Raw != want {
225+ t.Fatalf("ClientMessage in place %x want %x", ev.Raw, want)
226+ }
227+ if n := testing.AllocsPerRun(1000, func() { d.Data = e.Data(); d.Put(&ev.Raw) }); n != 0 {
228+ t.Fatalf("in-place Put allocates %v times", n)
229+ }
230+ raw = loadEvent(t, "xproto/KeymapNotify")
231+ ev = decode(t, raw, nil)
232+ keys := xproto.KeymapNotifyOf(&ev).Keys()
233+ k := xproto.KeymapNotifyEventData{Keys: [31]uint8(keys)}
234+ k.Put(&ev.Raw)
235+ if ev.Raw != [32]byte(raw) || !bytes.Equal(keys, raw[1:]) {
236+ t.Fatalf("KeymapNotify in place %x", ev.Raw)
237+ }
238+}
239+
240+func TestEventCopyAliases(t *testing.T) {
241+ if xproto.KindButtonRelease != 5 || xproto.KindKeyRelease != 3 || xproto.KindLeaveNotify != 8 || xproto.KindFocusOut != 10 || xproto.KindCirculateRequest != 27 {
242+ t.Fatal("eventcopy kinds")
243+ }
244+ var raw [32]byte
245+ raw[0], raw[1] = 5, 3 // ButtonRelease, button 3
246+ binary.LittleEndian.PutUint16(raw[24:], 0x100)
247+ ev := decode(t, raw[:], nil)
248+ var r xproto.ButtonReleaseEvent = xproto.ButtonReleaseOf(&ev)
249+ var p xproto.ButtonPressEvent = r // alias: same type
250+ if p.Detail() != 3 || r.EventX() != 0x100 {
251+ t.Fatal("fields")
252+ }
253+ d := xproto.ButtonReleaseEventData{Detail: 3, EventX: 0x100}
254+ var out [32]byte
255+ d.Put(&out)
256+ raw[2], raw[3] = 0, 0
257+ if out != raw {
258+ t.Fatalf("Put %x", out)
259+ }
260+}
261+
262+func TestShapeEvent(t *testing.T) {
263+ var raw [32]byte
264+ raw[0], raw[1] = 64, 2 // first event 64, SK Input
265+ binary.LittleEndian.PutUint32(raw[4:], 0x400001)
266+ binary.LittleEndian.PutUint16(raw[8:], 0xfffe)
267+ raw[20] = 1 // shaped: after window(4) extents(8) server_time(4)
268+ ev := decode(t, raw[:], goldenExt())
269+ if ev.Kind != shape.KindNotify || ev.Ext != xturbo.ExtShape {
270+ t.Fatalf("kind %x", ev.Kind)
271+ }
272+ e := shape.NotifyOf(&ev)
273+ if e.ShapeKind() != shape.SkInput || e.AffectedWindow() != 0x400001 || e.ExtentsX() != -2 || !e.Shaped() {
274+ t.Fatal("fields")
275+ }
276+}
277+
278+func TestDecodeEventRejects(t *testing.T) {
279+ raw := make([]byte, 64)
280+ for _, code := range []byte{0, 1, 0x80, 0x81} {
281+ raw[0] = code
282+ if _, _, ok := xturbo.DecodeEvent(raw, nil); ok {
283+ t.Fatalf("code %#x accepted", code)
284+ }
285+ }
286+ raw[0] = 2
287+ if _, _, ok := xturbo.DecodeEvent(raw[:31], nil); ok {
288+ t.Fatal("short event accepted")
289+ }
290+ raw[0] = 35
291+ binary.LittleEndian.PutUint32(raw[4:], 9) // 36 tail bytes > 32 available
292+ if _, _, ok := xturbo.DecodeEvent(raw, nil); ok {
293+ t.Fatal("truncated XGE accepted")
294+ }
295+ binary.LittleEndian.PutUint32(raw[4:], 0xffffffff)
296+ if _, _, ok := xturbo.DecodeEvent(raw, nil); ok {
297+ t.Fatal("huge XGE length accepted")
298+ }
299+ raw[0] = 100 // extension range, nothing negotiated
300+ if ev, n, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || n != 32 || ev.Kind != xturbo.KindUnknown {
301+ t.Fatal("unknown extension event")
302+ }
303+ raw[0] = 40 // core range gap 36..63
304+ if ev, _, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || ev.Kind != xturbo.KindUnknown {
305+ t.Fatal("unassigned core code")
306+ }
307+ raw[0] = 35 // XGE evtype >= 128 is never a known kind
308+ binary.LittleEndian.PutUint32(raw[4:], 0)
309+ raw[1] = geMajor
310+ binary.LittleEndian.PutUint16(raw[8:], 0x80)
311+ if ev, _, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || ev.Kind != xturbo.KindUnknown {
312+ t.Fatal("XGE evtype >= 128")
313+ }
314+}
315+
316+func TestExtTableReverseMaps(t *testing.T) {
317+ ext := goldenExt()
318+ if id, rel, ok := ext.ByEvent(xfixesEvent + 1); !ok || id != xturbo.ExtXFixes || rel != 1 {
319+ t.Fatal("ByEvent CursorNotify")
320+ }
321+ if _, _, ok := ext.ByEvent(xfixesEvent + 2); ok {
322+ t.Fatal("XFIXES declares two events")
323+ }
324+ if id, rel, ok := ext.ByError(xfixesError); !ok || id != xturbo.ExtXFixes || rel != xfixes.BadRegion {
325+ t.Fatal("ByError")
326+ }
327+ if id, ok := ext.ByMajor(xfixesMajor); !ok || id != xturbo.ExtXFixes {
328+ t.Fatal("ByMajor")
329+ }
330+ ext.Set(xturbo.ExtXFixes, 140, 90, 150) // re-negotiation drops stale entries
331+ if _, ok := ext.ByMajor(xfixesMajor); ok {
332+ t.Fatal("stale major")
333+ }
334+ if _, _, ok := ext.ByEvent(xfixesEvent); ok {
335+ t.Fatal("stale event")
336+ }
337+ if id, _, ok := ext.ByError(150); !ok || id != xturbo.ExtXFixes {
338+ t.Fatal("new error base")
339+ }
340+ var render xturbo.ExtTable
341+ render.Set(xturbo.ExtRender, 139, 0, 142)
342+ for code := uint8(142); code < 147; code++ {
343+ if id, rel, ok := render.ByError(code); !ok || id != xturbo.ExtRender || render.Info(id).FirstError+rel != code {
344+ t.Fatalf("render error %d", code)
345+ }
346+ }
347+ if _, _, ok := render.ByError(147); ok {
348+ t.Fatal("RENDER declares five errors")
349+ }
350+}
351+
352+func TestErrorNames(t *testing.T) {
353+ if xproto.BadWindow != 3 || xproto.BadAtom != 5 || xproto.BadImplementation != 17 ||
354+ xproto.ErrorName(xproto.BadWindow) != "BadWindow" || xproto.ErrorName(0) != "" || xproto.ErrorName(200) != "" {
355+ t.Fatal("xproto errors")
356+ }
357+ if xfixes.BadRegion != 0 || xfixes.ErrorName(0) != "BadRegion" {
358+ t.Fatal("xfixes errors")
359+ }
360+ for code, name := range []string{"BadPictFormat", "BadPicture", "BadPictOp", "BadGlyphSet", "BadGlyph"} {
361+ if render.ErrorName(uint8(code)) != name {
362+ t.Fatalf("render %d", code)
363+ }
364+ }
365+}
366+
367+// mixedEvents is 10k events: core, xfixes, XGE with a tail and unknown codes.
368+func mixedEvents(t testing.TB) []byte {
369+ var golden [][]byte
370+ for _, name := range []string{"xproto/MapRequest", "xproto/ConfigureRequest", "xproto/PropertyNotify", "xproto/ClientMessage",
371+ "xproto/KeymapNotify", "xfixes/SelectionNotify", "xproto/GeGeneric"} {
372+ golden = append(golden, loadEvent(t, name))
373+ }
374+ unknown := make([]byte, 32)
375+ unknown[0] = 100
376+ golden = append(golden, unknown)
377+ var stream []byte
378+ for i := range 10000 {
379+ stream = append(stream, golden[i%len(golden)]...)
380+ }
381+ return stream
382+}
383+
384+// eventSink touches the per-kind view getters a window manager reads.
385+func eventSink(ev *xturbo.Event) uint64 {
386+ switch ev.Kind {
387+ case xproto.KindMapRequest:
388+ e := xproto.MapRequestOf(ev)
389+ return uint64(e.Window()) + uint64(e.Parent())
390+ case xproto.KindConfigureRequest:
391+ e := xproto.ConfigureRequestOf(ev)
392+ return uint64(e.Window()) + uint64(e.Width()) + uint64(e.ValueMask()) + uint64(int64(e.X()))
393+ case xproto.KindPropertyNotify:
394+ e := xproto.PropertyNotifyOf(ev)
395+ return uint64(e.Atom()) + uint64(e.State())
396+ case xproto.KindClientMessage:
397+ e := xproto.ClientMessageOf(ev)
398+ return uint64(e.Type()) + uint64(e.Data().Data32()[0])
399+ case xproto.KindKeymapNotify:
400+ return uint64(len(xproto.KeymapNotifyOf(ev).Keys()))
401+ case xfixes.KindSelectionNotify:
402+ e := xfixes.SelectionNotifyOf(ev)
403+ return uint64(e.Owner()) + uint64(e.Selection())
404+ case xturbo.EventKind(xturbo.ExtGenericEvent)<<8 | 0x80 | 1:
405+ if g, ok := xproto.GeGenericOf(ev); ok {
406+ return uint64(len(g.Tail()))
407+ }
408+ }
409+ return 0
410+}
411+
412+func decodeAll(t testing.TB, stream []byte, ext *xturbo.ExtTable) (sum uint64) {
413+ for len(stream) > 0 {
414+ ev, n, ok := xturbo.DecodeEvent(stream, ext)
415+ if !ok {
416+ t.Fatal("decode")
417+ }
418+ sum += eventSink(&ev)
419+ stream = stream[n:]
420+ }
421+ return sum
422+}
423+
424+func TestAllocEvents(t *testing.T) {
425+ stream := mixedEvents(t)
426+ ext := goldenExt()
427+ var sink uint64
428+ if n := testing.AllocsPerRun(10, func() { sink += decodeAll(t, stream, ext) }); n != 0 {
429+ t.Fatalf("DecodeEvent x10k + views allocate %v times", n)
430+ }
431+ cm := xproto.ClientMessageEventData{Format: 32, Window: 1, Type: 2}
432+ cfg := xproto.ConfigureNotifyEventData{Window: 1, Width: 640, Height: 480}
433+ var out [32]byte
434+ if n := testing.AllocsPerRun(1000, func() { cm.Put(&out); cfg.Put(&out) }); n != 0 {
435+ t.Fatalf("EventData.Put allocates %v times", n)
436+ }
437+ b := xturbo.NewBuf(0)
438+ if n := testing.AllocsPerRun(1000, func() {
439+ b.Reset()
440+ cm.Put(&out)
441+ xproto.SendEvent(b, false, 1, xproto.EventMaskSubstructureRedirect, &out)
442+ }); n != 0 {
443+ t.Fatalf("SendEvent allocates %v times", n)
444+ }
445+ _ = sink
446+}
447+
448+func BenchmarkDecodeEvents10k(b *testing.B) {
449+ stream := mixedEvents(b)
450+ ext := goldenExt()
451+ var sink uint64
452+ b.SetBytes(int64(len(stream)))
453+ b.ReportAllocs()
454+ for b.Loop() {
455+ sink += decodeAll(b, stream, ext)
456+ }
457+ _ = sink
458+}
459+
460+// FuzzDecodeEvent: no panic; n is 32 or the XGE length; Raw copies the first
461+// 32 bytes; Tail stays within raw; Kind is consistent with Ext. extSeed picks
462+// which extensions are negotiated and where their bases lie.
463+func FuzzDecodeEvent(f *testing.F) {
464+ for _, name := range []string{"xproto/MapRequest", "xproto/ClientMessage", "xproto/KeymapNotify", "xfixes/SelectionNotify", "xproto/GeGeneric"} {
465+ f.Add(loadEvent(f, name), uint32(0x8a578c83))
466+ }
467+ f.Add(make([]byte, 32), uint32(0))
468+ f.Fuzz(func(t *testing.T, raw []byte, extSeed uint32) {
469+ var ext xturbo.ExtTable
470+ ext.Set(xturbo.ExtXFixes, uint8(extSeed>>24), uint8(extSeed>>16), uint8(extSeed>>8))
471+ ext.Set(xturbo.ExtGenericEvent, uint8(extSeed), 0, 0)
472+ ext.Set(xturbo.ExtShape, uint8(extSeed>>4), uint8(extSeed>>12)|64, 0)
473+ table := &ext
474+ if extSeed&1 != 0 {
475+ table = nil
476+ }
477+ ev, n, ok := xturbo.DecodeEvent(raw, table)
478+ if !ok {
479+ if n != 0 {
480+ t.Fatal("n on failure")
481+ }
482+ return
483+ }
484+ if len(raw) < 32 || raw[0]&0x7f < 2 || n > len(raw) || !bytes.Equal(ev.Raw[:], raw[:32]) || ev.SendEvent != (raw[0]&0x80 != 0) {
485+ t.Fatalf("n %d", n)
486+ }
487+ if raw[0]&0x7f == 35 {
488+ if n != 32+4*int(binary.LittleEndian.Uint32(raw[4:])) || len(ev.Tail) != n-32 {
489+ t.Fatal("XGE framing")
490+ }
491+ } else if n != 32 || ev.Tail != nil {
492+ t.Fatal("fixed framing")
493+ }
494+ if ev.Kind != xturbo.KindUnknown && ev.Kind>>8 != xturbo.EventKind(ev.Ext) {
495+ t.Fatal("Kind/Ext mismatch")
496+ }
497+ _ = eventSink(&ev)
498+ })
499+}
@@ -0,0 +1,499 @@
1+package xturbo_test
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "testing"
7+
8+ "bnema.dev/xturbo"
9+ "bnema.dev/xturbo/internal/xtest"
10+ "bnema.dev/xturbo/render"
11+ "bnema.dev/xturbo/shape"
12+ "bnema.dev/xturbo/xfixes"
13+ "bnema.dev/xturbo/xproto"
14+)
15+
16+// Xvfb negotiated XFIXES as major 138, first event 87, first error 140 when the
17+// goldens were captured; the BadRegion and SelectionNotify goldens confirm it.
18+const (
19+ xfixesMajor, xfixesEvent, xfixesError = 138, 87, 140
20+ geMajor = 0x83 // hand-written XGE golden
21+)
22+
23+func loadEvent(t testing.TB, name string) []byte {
24+ t.Helper()
25+ raw, err := xtest.LoadHex("testdata/golden/" + name + ".event.hex")
26+ if err != nil {
27+ t.Fatal(err)
28+ }
29+ return raw
30+}
31+
32+func goldenExt() *xturbo.ExtTable {
33+ var ext xturbo.ExtTable
34+ ext.Set(xturbo.ExtXFixes, xfixesMajor, xfixesEvent, xfixesError)
35+ ext.Set(xturbo.ExtGenericEvent, geMajor, 0, 0)
36+ ext.Set(xturbo.ExtShape, 129, 64, 0)
37+ return &ext
38+}
39+
40+func decode(t testing.TB, raw []byte, ext *xturbo.ExtTable) xturbo.Event {
41+ t.Helper()
42+ ev, n, ok := xturbo.DecodeEvent(raw, ext)
43+ if !ok || n != len(raw) || !bytes.Equal(ev.Raw[:], raw[:32]) {
44+ t.Fatalf("DecodeEvent(%x) = n %d ok %v", raw, n, ok)
45+ }
46+ return ev
47+}
48+
49+// Expected values follow from the capture script (second client's window at
50+// 10,20 300x200, ConfigureWindow x=-5 y=40 width=640, a format-32 ClientMessage
51+// with words 0x01020304*(i+1), SetSelectionOwner PRIMARY) or from raw golden
52+// offsets defined by the X11 protocol, not from generated code.
53+func TestEventGoldens(t *testing.T) {
54+ u32 := func(raw []byte, off int) uint32 { return binary.LittleEndian.Uint32(raw[off:]) }
55+ ext := goldenExt()
56+ core := func(name string, kind xturbo.EventKind) ([]byte, xturbo.Event) {
57+ raw := loadEvent(t, "xproto/"+name)
58+ ev := decode(t, raw, ext)
59+ if ev.Kind != kind || ev.Ext != xturbo.ExtCore || ev.Seq != xturbo.Seq(binary.LittleEndian.Uint16(raw[2:])) || ev.Tail != nil {
60+ t.Fatalf("%s: kind %d ext %d seq %d", name, ev.Kind, ev.Ext, ev.Seq)
61+ }
62+ return raw, ev
63+ }
64+ t.Run("MapRequest", func(t *testing.T) {
65+ raw, ev := core("MapRequest", xproto.KindMapRequest)
66+ e := xproto.MapRequestOf(&ev)
67+ if ev.SendEvent || e.Parent() != xproto.Window(u32(raw, 4)) || e.Window() != xproto.Window(u32(raw, 8)) || e.Window()&0xffff != 1 {
68+ t.Fatal("fields")
69+ }
70+ d := xproto.MapRequestEventData{Parent: e.Parent(), Window: e.Window()}
71+ var out [32]byte
72+ d.Put(&out)
73+ want := [32]byte(raw)
74+ want[2], want[3] = 0, 0 // Put leaves the sequence to the server
75+ if out != want {
76+ t.Fatalf("Put %x want %x", out, want)
77+ }
78+ })
79+ t.Run("ConfigureRequest", func(t *testing.T) {
80+ raw, ev := core("ConfigureRequest", xproto.KindConfigureRequest)
81+ e := xproto.ConfigureRequestOf(&ev)
82+ if e.StackMode() != xproto.StackModeAbove || e.Parent() != xproto.Window(u32(raw, 4)) || e.Window()&0xffff != 1 ||
83+ e.Sibling() != xproto.WindowNone || e.X() != -5 || e.Y() != 40 || e.Width() != 640 || e.Height() != 200 ||
84+ e.BorderWidth() != 0 || e.ValueMask() != xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth {
85+ t.Fatalf("fields x=%d y=%d w=%d h=%d mask=%d", e.X(), e.Y(), e.Width(), e.Height(), e.ValueMask())
86+ }
87+ d := xproto.ConfigureRequestEventData{StackMode: e.StackMode(), Parent: e.Parent(), Window: e.Window(), Sibling: e.Sibling(),
88+ X: e.X(), Y: e.Y(), Width: e.Width(), Height: e.Height(), BorderWidth: e.BorderWidth(), ValueMask: e.ValueMask()}
89+ var out [32]byte
90+ d.Put(&out)
91+ if !bytes.Equal(out[4:], raw[4:]) || out[0] != 23 || out[1] != raw[1] {
92+ t.Fatalf("Put %x", out)
93+ }
94+ })
95+ t.Run("PropertyNotify", func(t *testing.T) {
96+ raw, ev := core("PropertyNotify", xproto.KindPropertyNotify)
97+ e := xproto.PropertyNotifyOf(&ev)
98+ if e.Window() != xproto.Window(u32(raw, 4)) || e.Atom() != xproto.Atom(u32(raw, 8)) || e.Atom() < 69 ||
99+ e.Time() != xproto.Timestamp(u32(raw, 12)) || e.State() != xproto.PropertyNewValue {
100+ t.Fatal("fields")
101+ }
102+ })
103+ t.Run("ClientMessage", func(t *testing.T) {
104+ raw, ev := core("ClientMessage", xproto.KindClientMessage)
105+ e := xproto.ClientMessageOf(&ev)
106+ if !ev.SendEvent || e.Format() != 32 || e.Window()&0xffff != 1 || e.Type() != xproto.Atom(u32(raw, 8)) {
107+ t.Fatal("fields")
108+ }
109+ words := e.Data().Data32()
110+ for i, w := range words {
111+ if w != uint32(0x01020304*(i+1)) {
112+ t.Fatalf("data32[%d] = %x", i, w)
113+ }
114+ }
115+ d := xproto.ClientMessageEventData{Format: 32, Window: e.Window(), Type: e.Type(), Data: e.Data()}
116+ var out [32]byte
117+ d.Put(&out)
118+ want := [32]byte(raw)
119+ want[0] &^= 0x80
120+ want[2], want[3] = 0, 0
121+ if out != want {
122+ t.Fatalf("Put %x want %x", out, want)
123+ }
124+ })
125+ t.Run("CreateNotify", func(t *testing.T) {
126+ _, ev := core("CreateNotify", xproto.KindCreateNotify)
127+ e := xproto.CreateNotifyOf(&ev)
128+ if e.X() != 10 || e.Y() != 20 || e.Width() != 300 || e.Height() != 200 || e.OverrideRedirect() {
129+ t.Fatal("fields")
130+ }
131+ })
132+ t.Run("Map/Unmap/DestroyNotify", func(t *testing.T) {
133+ _, ev := core("MapNotify", xproto.KindMapNotify)
134+ if m := xproto.MapNotifyOf(&ev); m.Window()&0xffff != 1 || m.OverrideRedirect() {
135+ t.Fatal("MapNotify")
136+ }
137+ _, ev = core("UnmapNotify", xproto.KindUnmapNotify)
138+ if u := xproto.UnmapNotifyOf(&ev); u.Window()&0xffff != 1 || u.FromConfigure() {
139+ t.Fatal("UnmapNotify")
140+ }
141+ _, ev = core("DestroyNotify", xproto.KindDestroyNotify)
142+ if d := xproto.DestroyNotifyOf(&ev); d.Window()&0xffff != 1 {
143+ t.Fatal("DestroyNotify")
144+ }
145+ })
146+ t.Run("SelectionNotify", func(t *testing.T) {
147+ _, ev := core("SelectionNotify", xproto.KindSelectionNotify)
148+ e := xproto.SelectionNotifyOf(&ev)
149+ if e.Requestor()&0xffff != 1 || e.Selection() != xproto.AtomSecondary || e.Target() != xproto.AtomString || e.Property() != xproto.AtomNone {
150+ t.Fatal("fields")
151+ }
152+ })
153+ t.Run("xfixes/SelectionNotify", func(t *testing.T) {
154+ raw := loadEvent(t, "xfixes/SelectionNotify")
155+ ev := decode(t, raw, ext)
156+ if xfixes.KindSelectionNotify != xturbo.EventKind(xturbo.ExtXFixes)<<8 {
157+ t.Fatal("KindSelectionNotify value")
158+ }
159+ if ev.Kind != xfixes.KindSelectionNotify || ev.Ext != xturbo.ExtXFixes {
160+ t.Fatalf("kind %x", ev.Kind)
161+ }
162+ e := xfixes.SelectionNotifyOf(&ev)
163+ if e.Subtype() != xfixes.SelectionEventSetSelectionOwner || e.Window() != xproto.Window(u32(raw, 4)) ||
164+ e.Owner()&0xffff != 1 || e.Selection() != xproto.AtomPrimary || e.Timestamp() != e.SelectionTimestamp() {
165+ t.Fatal("fields")
166+ }
167+ // Without XFIXES negotiated the same bytes are an unknown event.
168+ if ev, _, ok := xturbo.DecodeEvent(raw, nil); !ok || ev.Kind != xturbo.KindUnknown {
169+ t.Fatal("nil ext must decode as unknown")
170+ }
171+ })
172+ t.Run("KeymapNotify", func(t *testing.T) {
173+ raw := loadEvent(t, "xproto/KeymapNotify")
174+ ev := decode(t, raw, ext)
175+ if ev.Kind != xproto.KindKeymapNotify || ev.Seq != 0 {
176+ t.Fatalf("kind %d seq %d", ev.Kind, ev.Seq)
177+ }
178+ e := xproto.KeymapNotifyOf(&ev)
179+ if !bytes.Equal(e.Keys(), raw[1:]) {
180+ t.Fatal("keys")
181+ }
182+ d := xproto.KeymapNotifyEventData{Keys: [31]uint8(raw[1:])}
183+ var out [32]byte
184+ d.Put(&out)
185+ if !bytes.Equal(out[:], raw) {
186+ t.Fatalf("Put %x", out)
187+ }
188+ })
189+ t.Run("GeGeneric", func(t *testing.T) {
190+ raw := loadEvent(t, "xproto/GeGeneric")
191+ ev := decode(t, raw, ext)
192+ if ev.Kind != xturbo.EventKind(xturbo.ExtGenericEvent)<<8|0x80|1 || ev.Ext != xturbo.ExtGenericEvent || ev.Seq != 5 ||
193+ !bytes.Equal(ev.Tail, raw[32:]) {
194+ t.Fatalf("kind %x tail %x", ev.Kind, ev.Tail)
195+ }
196+ g, ok := xproto.GeGenericOf(&ev)
197+ if !ok || g.Extension() != geMajor || g.Length() != 2 || g.EventType() != 1 || !bytes.Equal(g.Tail(), raw[32:]) {
198+ t.Fatal("view")
199+ }
200+ // Unknown major: still framed so Dispatch skips the tail.
201+ ev, n, ok := xturbo.DecodeEvent(raw, nil)
202+ if !ok || n != 40 || ev.Kind != xturbo.KindUnknown || len(ev.Tail) != 8 {
203+ t.Fatal("unknown XGE framing")
204+ }
205+ ev.Tail = ev.Tail[:4]
206+ if _, ok := xproto.GeGenericOf(&ev); ok {
207+ t.Fatal("short Tail accepted")
208+ }
209+ })
210+}
211+
212+// TestEventDataPutInPlace re-encodes events into the Event.Raw their fields
213+// were read from. ClientMessage's Data is a union view over Raw; KeymapNotify's
214+// Keys is a slice view over Raw. Put must read every field before writing dst.
215+func TestEventDataPutInPlace(t *testing.T) {
216+ raw := loadEvent(t, "xproto/ClientMessage")
217+ ev := decode(t, raw, nil)
218+ e := xproto.ClientMessageOf(&ev)
219+ d := xproto.ClientMessageEventData{Format: e.Format(), Window: e.Window(), Type: e.Type(), Data: e.Data()}
220+ d.Put(&ev.Raw)
221+ want := [32]byte(raw)
222+ want[0] &^= 0x80
223+ want[2], want[3] = 0, 0
224+ if ev.Raw != want {
225+ t.Fatalf("ClientMessage in place %x want %x", ev.Raw, want)
226+ }
227+ if n := testing.AllocsPerRun(1000, func() { d.Data = e.Data(); d.Put(&ev.Raw) }); n != 0 {
228+ t.Fatalf("in-place Put allocates %v times", n)
229+ }
230+ raw = loadEvent(t, "xproto/KeymapNotify")
231+ ev = decode(t, raw, nil)
232+ keys := xproto.KeymapNotifyOf(&ev).Keys()
233+ k := xproto.KeymapNotifyEventData{Keys: [31]uint8(keys)}
234+ k.Put(&ev.Raw)
235+ if ev.Raw != [32]byte(raw) || !bytes.Equal(keys, raw[1:]) {
236+ t.Fatalf("KeymapNotify in place %x", ev.Raw)
237+ }
238+}
239+
240+func TestEventCopyAliases(t *testing.T) {
241+ if xproto.KindButtonRelease != 5 || xproto.KindKeyRelease != 3 || xproto.KindLeaveNotify != 8 || xproto.KindFocusOut != 10 || xproto.KindCirculateRequest != 27 {
242+ t.Fatal("eventcopy kinds")
243+ }
244+ var raw [32]byte
245+ raw[0], raw[1] = 5, 3 // ButtonRelease, button 3
246+ binary.LittleEndian.PutUint16(raw[24:], 0x100)
247+ ev := decode(t, raw[:], nil)
248+ var r xproto.ButtonReleaseEvent = xproto.ButtonReleaseOf(&ev)
249+ var p xproto.ButtonPressEvent = r // alias: same type
250+ if p.Detail() != 3 || r.EventX() != 0x100 {
251+ t.Fatal("fields")
252+ }
253+ d := xproto.ButtonReleaseEventData{Detail: 3, EventX: 0x100}
254+ var out [32]byte
255+ d.Put(&out)
256+ raw[2], raw[3] = 0, 0
257+ if out != raw {
258+ t.Fatalf("Put %x", out)
259+ }
260+}
261+
262+func TestShapeEvent(t *testing.T) {
263+ var raw [32]byte
264+ raw[0], raw[1] = 64, 2 // first event 64, SK Input
265+ binary.LittleEndian.PutUint32(raw[4:], 0x400001)
266+ binary.LittleEndian.PutUint16(raw[8:], 0xfffe)
267+ raw[20] = 1 // shaped: after window(4) extents(8) server_time(4)
268+ ev := decode(t, raw[:], goldenExt())
269+ if ev.Kind != shape.KindNotify || ev.Ext != xturbo.ExtShape {
270+ t.Fatalf("kind %x", ev.Kind)
271+ }
272+ e := shape.NotifyOf(&ev)
273+ if e.ShapeKind() != shape.SkInput || e.AffectedWindow() != 0x400001 || e.ExtentsX() != -2 || !e.Shaped() {
274+ t.Fatal("fields")
275+ }
276+}
277+
278+func TestDecodeEventRejects(t *testing.T) {
279+ raw := make([]byte, 64)
280+ for _, code := range []byte{0, 1, 0x80, 0x81} {
281+ raw[0] = code
282+ if _, _, ok := xturbo.DecodeEvent(raw, nil); ok {
283+ t.Fatalf("code %#x accepted", code)
284+ }
285+ }
286+ raw[0] = 2
287+ if _, _, ok := xturbo.DecodeEvent(raw[:31], nil); ok {
288+ t.Fatal("short event accepted")
289+ }
290+ raw[0] = 35
291+ binary.LittleEndian.PutUint32(raw[4:], 9) // 36 tail bytes > 32 available
292+ if _, _, ok := xturbo.DecodeEvent(raw, nil); ok {
293+ t.Fatal("truncated XGE accepted")
294+ }
295+ binary.LittleEndian.PutUint32(raw[4:], 0xffffffff)
296+ if _, _, ok := xturbo.DecodeEvent(raw, nil); ok {
297+ t.Fatal("huge XGE length accepted")
298+ }
299+ raw[0] = 100 // extension range, nothing negotiated
300+ if ev, n, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || n != 32 || ev.Kind != xturbo.KindUnknown {
301+ t.Fatal("unknown extension event")
302+ }
303+ raw[0] = 40 // core range gap 36..63
304+ if ev, _, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || ev.Kind != xturbo.KindUnknown {
305+ t.Fatal("unassigned core code")
306+ }
307+ raw[0] = 35 // XGE evtype >= 128 is never a known kind
308+ binary.LittleEndian.PutUint32(raw[4:], 0)
309+ raw[1] = geMajor
310+ binary.LittleEndian.PutUint16(raw[8:], 0x80)
311+ if ev, _, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || ev.Kind != xturbo.KindUnknown {
312+ t.Fatal("XGE evtype >= 128")
313+ }
314+}
315+
316+func TestExtTableReverseMaps(t *testing.T) {
317+ ext := goldenExt()
318+ if id, rel, ok := ext.ByEvent(xfixesEvent + 1); !ok || id != xturbo.ExtXFixes || rel != 1 {
319+ t.Fatal("ByEvent CursorNotify")
320+ }
321+ if _, _, ok := ext.ByEvent(xfixesEvent + 2); ok {
322+ t.Fatal("XFIXES declares two events")
323+ }
324+ if id, rel, ok := ext.ByError(xfixesError); !ok || id != xturbo.ExtXFixes || rel != xfixes.BadRegion {
325+ t.Fatal("ByError")
326+ }
327+ if id, ok := ext.ByMajor(xfixesMajor); !ok || id != xturbo.ExtXFixes {
328+ t.Fatal("ByMajor")
329+ }
330+ ext.Set(xturbo.ExtXFixes, 140, 90, 150) // re-negotiation drops stale entries
331+ if _, ok := ext.ByMajor(xfixesMajor); ok {
332+ t.Fatal("stale major")
333+ }
334+ if _, _, ok := ext.ByEvent(xfixesEvent); ok {
335+ t.Fatal("stale event")
336+ }
337+ if id, _, ok := ext.ByError(150); !ok || id != xturbo.ExtXFixes {
338+ t.Fatal("new error base")
339+ }
340+ var render xturbo.ExtTable
341+ render.Set(xturbo.ExtRender, 139, 0, 142)
342+ for code := uint8(142); code < 147; code++ {
343+ if id, rel, ok := render.ByError(code); !ok || id != xturbo.ExtRender || render.Info(id).FirstError+rel != code {
344+ t.Fatalf("render error %d", code)
345+ }
346+ }
347+ if _, _, ok := render.ByError(147); ok {
348+ t.Fatal("RENDER declares five errors")
349+ }
350+}
351+
352+func TestErrorNames(t *testing.T) {
353+ if xproto.BadWindow != 3 || xproto.BadAtom != 5 || xproto.BadImplementation != 17 ||
354+ xproto.ErrorName(xproto.BadWindow) != "BadWindow" || xproto.ErrorName(0) != "" || xproto.ErrorName(200) != "" {
355+ t.Fatal("xproto errors")
356+ }
357+ if xfixes.BadRegion != 0 || xfixes.ErrorName(0) != "BadRegion" {
358+ t.Fatal("xfixes errors")
359+ }
360+ for code, name := range []string{"BadPictFormat", "BadPicture", "BadPictOp", "BadGlyphSet", "BadGlyph"} {
361+ if render.ErrorName(uint8(code)) != name {
362+ t.Fatalf("render %d", code)
363+ }
364+ }
365+}
366+
367+// mixedEvents is 10k events: core, xfixes, XGE with a tail and unknown codes.
368+func mixedEvents(t testing.TB) []byte {
369+ var golden [][]byte
370+ for _, name := range []string{"xproto/MapRequest", "xproto/ConfigureRequest", "xproto/PropertyNotify", "xproto/ClientMessage",
371+ "xproto/KeymapNotify", "xfixes/SelectionNotify", "xproto/GeGeneric"} {
372+ golden = append(golden, loadEvent(t, name))
373+ }
374+ unknown := make([]byte, 32)
375+ unknown[0] = 100
376+ golden = append(golden, unknown)
377+ var stream []byte
378+ for i := range 10000 {
379+ stream = append(stream, golden[i%len(golden)]...)
380+ }
381+ return stream
382+}
383+
384+// eventSink touches the per-kind view getters a window manager reads.
385+func eventSink(ev *xturbo.Event) uint64 {
386+ switch ev.Kind {
387+ case xproto.KindMapRequest:
388+ e := xproto.MapRequestOf(ev)
389+ return uint64(e.Window()) + uint64(e.Parent())
390+ case xproto.KindConfigureRequest:
391+ e := xproto.ConfigureRequestOf(ev)
392+ return uint64(e.Window()) + uint64(e.Width()) + uint64(e.ValueMask()) + uint64(int64(e.X()))
393+ case xproto.KindPropertyNotify:
394+ e := xproto.PropertyNotifyOf(ev)
395+ return uint64(e.Atom()) + uint64(e.State())
396+ case xproto.KindClientMessage:
397+ e := xproto.ClientMessageOf(ev)
398+ return uint64(e.Type()) + uint64(e.Data().Data32()[0])
399+ case xproto.KindKeymapNotify:
400+ return uint64(len(xproto.KeymapNotifyOf(ev).Keys()))
401+ case xfixes.KindSelectionNotify:
402+ e := xfixes.SelectionNotifyOf(ev)
403+ return uint64(e.Owner()) + uint64(e.Selection())
404+ case xturbo.EventKind(xturbo.ExtGenericEvent)<<8 | 0x80 | 1:
405+ if g, ok := xproto.GeGenericOf(ev); ok {
406+ return uint64(len(g.Tail()))
407+ }
408+ }
409+ return 0
410+}
411+
412+func decodeAll(t testing.TB, stream []byte, ext *xturbo.ExtTable) (sum uint64) {
413+ for len(stream) > 0 {
414+ ev, n, ok := xturbo.DecodeEvent(stream, ext)
415+ if !ok {
416+ t.Fatal("decode")
417+ }
418+ sum += eventSink(&ev)
419+ stream = stream[n:]
420+ }
421+ return sum
422+}
423+
424+func TestAllocEvents(t *testing.T) {
425+ stream := mixedEvents(t)
426+ ext := goldenExt()
427+ var sink uint64
428+ if n := testing.AllocsPerRun(10, func() { sink += decodeAll(t, stream, ext) }); n != 0 {
429+ t.Fatalf("DecodeEvent x10k + views allocate %v times", n)
430+ }
431+ cm := xproto.ClientMessageEventData{Format: 32, Window: 1, Type: 2}
432+ cfg := xproto.ConfigureNotifyEventData{Window: 1, Width: 640, Height: 480}
433+ var out [32]byte
434+ if n := testing.AllocsPerRun(1000, func() { cm.Put(&out); cfg.Put(&out) }); n != 0 {
435+ t.Fatalf("EventData.Put allocates %v times", n)
436+ }
437+ b := xturbo.NewBuf(0)
438+ if n := testing.AllocsPerRun(1000, func() {
439+ b.Reset()
440+ cm.Put(&out)
441+ xproto.SendEvent(b, false, 1, xproto.EventMaskSubstructureRedirect, &out)
442+ }); n != 0 {
443+ t.Fatalf("SendEvent allocates %v times", n)
444+ }
445+ _ = sink
446+}
447+
448+func BenchmarkDecodeEvents10k(b *testing.B) {
449+ stream := mixedEvents(b)
450+ ext := goldenExt()
451+ var sink uint64
452+ b.SetBytes(int64(len(stream)))
453+ b.ReportAllocs()
454+ for b.Loop() {
455+ sink += decodeAll(b, stream, ext)
456+ }
457+ _ = sink
458+}
459+
460+// FuzzDecodeEvent: no panic; n is 32 or the XGE length; Raw copies the first
461+// 32 bytes; Tail stays within raw; Kind is consistent with Ext. extSeed picks
462+// which extensions are negotiated and where their bases lie.
463+func FuzzDecodeEvent(f *testing.F) {
464+ for _, name := range []string{"xproto/MapRequest", "xproto/ClientMessage", "xproto/KeymapNotify", "xfixes/SelectionNotify", "xproto/GeGeneric"} {
465+ f.Add(loadEvent(f, name), uint32(0x8a578c83))
466+ }
467+ f.Add(make([]byte, 32), uint32(0))
468+ f.Fuzz(func(t *testing.T, raw []byte, extSeed uint32) {
469+ var ext xturbo.ExtTable
470+ ext.Set(xturbo.ExtXFixes, uint8(extSeed>>24), uint8(extSeed>>16), uint8(extSeed>>8))
471+ ext.Set(xturbo.ExtGenericEvent, uint8(extSeed), 0, 0)
472+ ext.Set(xturbo.ExtShape, uint8(extSeed>>4), uint8(extSeed>>12)|64, 0)
473+ table := &ext
474+ if extSeed&1 != 0 {
475+ table = nil
476+ }
477+ ev, n, ok := xturbo.DecodeEvent(raw, table)
478+ if !ok {
479+ if n != 0 {
480+ t.Fatal("n on failure")
481+ }
482+ return
483+ }
484+ if len(raw) < 32 || raw[0]&0x7f < 2 || n > len(raw) || !bytes.Equal(ev.Raw[:], raw[:32]) || ev.SendEvent != (raw[0]&0x80 != 0) {
485+ t.Fatalf("n %d", n)
486+ }
487+ if raw[0]&0x7f == 35 {
488+ if n != 32+4*int(binary.LittleEndian.Uint32(raw[4:])) || len(ev.Tail) != n-32 {
489+ t.Fatal("XGE framing")
490+ }
491+ } else if n != 32 || ev.Tail != nil {
492+ t.Fatal("fixed framing")
493+ }
494+ if ev.Kind != xturbo.KindUnknown && ev.Kind>>8 != xturbo.EventKind(ev.Ext) {
495+ t.Fatal("Kind/Ext mismatch")
496+ }
497+ _ = eventSink(&ev)
498+ })
499+}
M ext.go +66 -4
@@ -7,12 +7,47 @@ type ExtInfo struct {
77 }
88
99 // ExtTable is populated during setup. Fixed arrays avoid lookup allocations.
10-// Reverse mapping is completed with the read-side runtime in Phase 3.
11-type ExtTable struct{ info [extCount]ExtInfo }
10+// Reverse maps store ExtID+1 so the zero value means "none".
11+type ExtTable struct {
12+ info [extCount]ExtInfo
13+ byEvent [128]uint8
14+ byError [256]uint8
15+ byMajor [256]uint8
16+}
1217
18+// Set records an extension's negotiated opcodes and fills the reverse maps
19+// for the event and error codes the extension declares in the pinned XML.
1320 func (t *ExtTable) Set(id ExtID, major, firstEvent, firstError uint8) {
14- if id < extCount {
15- t.info[id] = ExtInfo{true, major, firstEvent, firstError}
21+ if id == ExtCore || id >= extCount {
22+ return
23+ }
24+ tag := uint8(id) + 1
25+ for i := range t.byEvent {
26+ if t.byEvent[i] == tag {
27+ t.byEvent[i] = 0
28+ }
29+ }
30+ for i := range t.byError {
31+ if t.byError[i] == tag {
32+ t.byError[i] = 0
33+ }
34+ if t.byMajor[i] == tag {
35+ t.byMajor[i] = 0
36+ }
37+ }
38+ t.info[id] = ExtInfo{true, major, firstEvent, firstError}
39+ if major != 0 {
40+ t.byMajor[major] = tag
41+ }
42+ if firstEvent != 0 {
43+ for i := 0; i < int(extEvents[id]) && int(firstEvent)+i < len(t.byEvent); i++ {
44+ t.byEvent[int(firstEvent)+i] = tag
45+ }
46+ }
47+ if firstError != 0 {
48+ for i := 0; i < int(extErrors[id]) && int(firstError)+i < len(t.byError); i++ {
49+ t.byError[int(firstError)+i] = tag
50+ }
1651 }
1752 }
1853 func (t *ExtTable) Info(id ExtID) ExtInfo {
@@ -25,3 +60,30 @@ func (t *ExtTable) Info(id ExtID) ExtInfo {
2560 return t.info[id]
2661 }
2762 func (t *ExtTable) Major(id ExtID) uint8 { return t.Info(id).Major }
63+
64+// ByEvent maps an event code (raw[0]&0x7f) to its extension and the code
65+// relative to the extension's first event.
66+func (t *ExtTable) ByEvent(code uint8) (id ExtID, rel uint8, ok bool) {
67+ if code >= 128 || t.byEvent[code] == 0 {
68+ return ExtCore, 0, false
69+ }
70+ id = ExtID(t.byEvent[code] - 1)
71+ return id, code - t.info[id].FirstEvent, true
72+}
73+
74+// ByError maps a wire error code to its extension and relative error code.
75+func (t *ExtTable) ByError(code uint8) (id ExtID, rel uint8, ok bool) {
76+ if t.byError[code] == 0 {
77+ return ExtCore, 0, false
78+ }
79+ id = ExtID(t.byError[code] - 1)
80+ return id, code - t.info[id].FirstError, true
81+}
82+
83+// ByMajor maps a major opcode to its extension.
84+func (t *ExtTable) ByMajor(major uint8) (id ExtID, ok bool) {
85+ if t.byMajor[major] == 0 {
86+ return ExtCore, false
87+ }
88+ return ExtID(t.byMajor[major] - 1), true
89+}
@@ -7,12 +7,47 @@ type ExtInfo struct {
7 }7 }
8 8
9 // ExtTable is populated during setup. Fixed arrays avoid lookup allocations.9 // ExtTable is populated during setup. Fixed arrays avoid lookup allocations.
10-// Reverse mapping is completed with the read-side runtime in Phase 3.10+// Reverse maps store ExtID+1 so the zero value means "none".
11-type ExtTable struct{ info [extCount]ExtInfo }11+type ExtTable struct {
12+ info [extCount]ExtInfo
13+ byEvent [128]uint8
14+ byError [256]uint8
15+ byMajor [256]uint8
16+}
12 17
18+// Set records an extension's negotiated opcodes and fills the reverse maps
19+// for the event and error codes the extension declares in the pinned XML.
13 func (t *ExtTable) Set(id ExtID, major, firstEvent, firstError uint8) {20 func (t *ExtTable) Set(id ExtID, major, firstEvent, firstError uint8) {
14- if id < extCount {21+ if id == ExtCore || id >= extCount {
15- t.info[id] = ExtInfo{true, major, firstEvent, firstError}22+ return
23+ }
24+ tag := uint8(id) + 1
25+ for i := range t.byEvent {
26+ if t.byEvent[i] == tag {
27+ t.byEvent[i] = 0
28+ }
29+ }
30+ for i := range t.byError {
31+ if t.byError[i] == tag {
32+ t.byError[i] = 0
33+ }
34+ if t.byMajor[i] == tag {
35+ t.byMajor[i] = 0
36+ }
37+ }
38+ t.info[id] = ExtInfo{true, major, firstEvent, firstError}
39+ if major != 0 {
40+ t.byMajor[major] = tag
41+ }
42+ if firstEvent != 0 {
43+ for i := 0; i < int(extEvents[id]) && int(firstEvent)+i < len(t.byEvent); i++ {
44+ t.byEvent[int(firstEvent)+i] = tag
45+ }
46+ }
47+ if firstError != 0 {
48+ for i := 0; i < int(extErrors[id]) && int(firstError)+i < len(t.byError); i++ {
49+ t.byError[int(firstError)+i] = tag
50+ }
16 }51 }
17 }52 }
18 func (t *ExtTable) Info(id ExtID) ExtInfo {53 func (t *ExtTable) Info(id ExtID) ExtInfo {
@@ -25,3 +60,30 @@ func (t *ExtTable) Info(id ExtID) ExtInfo {
25 return t.info[id]60 return t.info[id]
26 }61 }
27 func (t *ExtTable) Major(id ExtID) uint8 { return t.Info(id).Major }62 func (t *ExtTable) Major(id ExtID) uint8 { return t.Info(id).Major }
63+
64+// ByEvent maps an event code (raw[0]&0x7f) to its extension and the code
65+// relative to the extension's first event.
66+func (t *ExtTable) ByEvent(code uint8) (id ExtID, rel uint8, ok bool) {
67+ if code >= 128 || t.byEvent[code] == 0 {
68+ return ExtCore, 0, false
69+ }
70+ id = ExtID(t.byEvent[code] - 1)
71+ return id, code - t.info[id].FirstEvent, true
72+}
73+
74+// ByError maps a wire error code to its extension and relative error code.
75+func (t *ExtTable) ByError(code uint8) (id ExtID, rel uint8, ok bool) {
76+ if t.byError[code] == 0 {
77+ return ExtCore, 0, false
78+ }
79+ id = ExtID(t.byError[code] - 1)
80+ return id, code - t.info[id].FirstError, true
81+}
82+
83+// ByMajor maps a major opcode to its extension.
84+func (t *ExtTable) ByMajor(major uint8) (id ExtID, ok bool) {
85+ if t.byMajor[major] == 0 {
86+ return ExtCore, false
87+ }
88+ return ExtID(t.byMajor[major] - 1), true
89+}
M ext_generated.go +5 -0
@@ -17,3 +17,8 @@ const (
1717 )
1818
1919 var ExtXName = [extCount]string{"", "BIG-REQUESTS", "Generic Event Extension", "XC-MISC", "Composite", "XFIXES", "X-Resource", "RENDER", "SHAPE"}
20+
21+// extEvents and extErrors are the event and error code ranges each extension
22+// declares in the pinned XML; ExtTable.Set uses them to fill reverse maps.
23+var extEvents = [extCount]uint8{0, 0, 0, 0, 0, 2, 0, 0, 1}
24+var extErrors = [extCount]uint8{0, 0, 0, 0, 0, 1, 0, 5, 0}
@@ -17,3 +17,8 @@ const (
17 )17 )
18 18
19 var ExtXName = [extCount]string{"", "BIG-REQUESTS", "Generic Event Extension", "XC-MISC", "Composite", "XFIXES", "X-Resource", "RENDER", "SHAPE"}19 var ExtXName = [extCount]string{"", "BIG-REQUESTS", "Generic Event Extension", "XC-MISC", "Composite", "XFIXES", "X-Resource", "RENDER", "SHAPE"}
20+
21+// extEvents and extErrors are the event and error code ranges each extension
22+// declares in the pinned XML; ExtTable.Set uses them to fill reverse maps.
23+var extEvents = [extCount]uint8{0, 0, 0, 0, 0, 2, 0, 0, 1}
24+var extErrors = [extCount]uint8{0, 0, 0, 0, 0, 1, 0, 5, 0}
M internal/xtest/README.md +33 -1
@@ -1,6 +1,27 @@
11 # Wire goldens
22
3-Refresh the request fixtures from the repository root:
3+`make xserver` captures from a fresh Xvfb and compares every packet with the
4+committed golden. Comparison is independent of the server build:
5+
6+- Server-chosen values (root window, visual, colormap, interned atom,
7+ resource IDs from the client's resource base, extension opcodes and
8+ event/error bases) are listed per golden by field offset in
9+ `serverFields`. `-update` records their capture-time values in
10+ `testdata/golden/xvfb-roles.json`. Compare mode checks that the golden holds
11+ the recorded value and the live packet holds the live server's value, masks
12+ both, then compares the remaining bytes exactly. Server timestamps are
13+ masked without a value check.
14+- `ListExtensions` and Render `QueryPictFormats` replies depend on how the
15+ server was built (extension list, visuals, formats), so live compare
16+ checks invariants: framing, counts matching list lengths, the bound
17+ extensions present, the root visual mapped to the format CreatePicture uses.
18+
19+Unit tests still decode the committed goldens byte-exactly. The test is
20+skipped when Xvfb is not installed. `XTURBO_XVFB_ARGS` adds Xvfb options to
21+simulate another server layout, e.g. `XTURBO_XVFB_ARGS="-extension GLX"`.
22+Screen size and depth (800x600x24) and the bound extensions are capture
23+inputs and must stay as captured. Rewrite the goldens from the repository
24+root with:
425
526 ```
627 PATH="$PATH:/usr/sbin" go test -tags xserver ./internal/xtest -update
@@ -38,3 +59,14 @@ adds GetWindowAttributes (child window), QueryTree (root), an unset WM_NAME
3859 GetProperty (zero-length value) and ListExtensions (variable STR list). These
3960 reply-only requests use opcodes outside the request-golden classifier.
4061 Generated `FuzzParse*Reply` targets seed from every matching reply golden.
62+
63+T2.7 adds event and error goldens (`<Name>.event.hex`, `<Name>.error.hex`).
64+The tee client selects SubstructureRedirect/Notify and PropertyChange on the
65+root plus XFIXES selection input; a second raw client (not teed) creates, maps,
66+configures, unmaps and destroys a window, writes a root property, sends a
67+format-32 ClientMessage and takes PRIMARY. ConvertSelection of an unowned
68+SECONDARY yields a core SelectionNotify. GetAtomName, GetWindowAttributes and
69+XFIXES DestroyRegion on bad IDs yield BadAtom, BadWindow and BadRegion. Each
70+batch is fenced by a GetInputFocus reply; unexpected packets fail capture.
71+`KeymapNotify` and `GeGeneric` (XGE framing with a tail) are hand-written
72+(`# hand:`): Xvfb produces neither for these clients.
@@ -1,6 +1,27 @@
1 # Wire goldens1 # Wire goldens
2 2
3-Refresh the request fixtures from the repository root:3+`make xserver` captures from a fresh Xvfb and compares every packet with the
4+committed golden. Comparison is independent of the server build:
5+
6+- Server-chosen values (root window, visual, colormap, interned atom,
7+ resource IDs from the client's resource base, extension opcodes and
8+ event/error bases) are listed per golden by field offset in
9+ `serverFields`. `-update` records their capture-time values in
10+ `testdata/golden/xvfb-roles.json`. Compare mode checks that the golden holds
11+ the recorded value and the live packet holds the live server's value, masks
12+ both, then compares the remaining bytes exactly. Server timestamps are
13+ masked without a value check.
14+- `ListExtensions` and Render `QueryPictFormats` replies depend on how the
15+ server was built (extension list, visuals, formats), so live compare
16+ checks invariants: framing, counts matching list lengths, the bound
17+ extensions present, the root visual mapped to the format CreatePicture uses.
18+
19+Unit tests still decode the committed goldens byte-exactly. The test is
20+skipped when Xvfb is not installed. `XTURBO_XVFB_ARGS` adds Xvfb options to
21+simulate another server layout, e.g. `XTURBO_XVFB_ARGS="-extension GLX"`.
22+Screen size and depth (800x600x24) and the bound extensions are capture
23+inputs and must stay as captured. Rewrite the goldens from the repository
24+root with:
4 25
5 ```26 ```
6 PATH="$PATH:/usr/sbin" go test -tags xserver ./internal/xtest -update27 PATH="$PATH:/usr/sbin" go test -tags xserver ./internal/xtest -update
@@ -38,3 +59,14 @@ adds GetWindowAttributes (child window), QueryTree (root), an unset WM_NAME
38 GetProperty (zero-length value) and ListExtensions (variable STR list). These59 GetProperty (zero-length value) and ListExtensions (variable STR list). These
39 reply-only requests use opcodes outside the request-golden classifier.60 reply-only requests use opcodes outside the request-golden classifier.
40 Generated `FuzzParse*Reply` targets seed from every matching reply golden.61 Generated `FuzzParse*Reply` targets seed from every matching reply golden.
62+
63+T2.7 adds event and error goldens (`<Name>.event.hex`, `<Name>.error.hex`).
64+The tee client selects SubstructureRedirect/Notify and PropertyChange on the
65+root plus XFIXES selection input; a second raw client (not teed) creates, maps,
66+configures, unmaps and destroys a window, writes a root property, sends a
67+format-32 ClientMessage and takes PRIMARY. ConvertSelection of an unowned
68+SECONDARY yields a core SelectionNotify. GetAtomName, GetWindowAttributes and
69+XFIXES DestroyRegion on bad IDs yield BadAtom, BadWindow and BadRegion. Each
70+batch is fenced by a GetInputFocus reply; unexpected packets fail capture.
71+`KeymapNotify` and `GeGeneric` (XGE framing with a tail) are hand-written
72+(`# hand:`): Xvfb produces neither for these clients.
M internal/xtest/capture_test.go +453 -18
@@ -6,13 +6,16 @@ import (
66 "bytes"
77 "encoding/binary"
88 "encoding/hex"
9+ "encoding/json"
910 "flag"
1011 "fmt"
1112 "io"
13+ "maps"
1214 "net"
1315 "os"
1416 "os/exec"
1517 "path/filepath"
18+ "slices"
1619 "strconv"
1720 "strings"
1821 "testing"
@@ -21,11 +24,77 @@ import (
2124 "golang.org/x/sys/unix"
2225 )
2326
24-var update = flag.Bool("update", false, "refresh request goldens using Xvfb and a raw socketpair tee")
27+var update = flag.Bool("update", false, "rewrite wire goldens from Xvfb instead of comparing against them")
2528
29+// field is a server-dependent value inside a golden: the size-byte
30+// little-endian integer at Off must equal the server's value for Role.
31+type field struct {
32+ Off, Size int
33+ Role string
34+}
35+
36+// serverFields lists, per golden, every value the X server chooses: resource
37+// IDs derived from a server-assigned base, the root window and its visual and
38+// colormap, interned atoms, extension opcodes and bases, and timestamps.
39+// Compare mode checks that the golden holds the value recorded at capture
40+// (xvfb-roles.json) and the live packet holds the live server's value, then
41+// masks both and compares the remaining bytes exactly. Role "time" is a
42+// per-run server timestamp and is masked without a value check.
43+var serverFields = map[string][]field{
44+ "xproto/MapWindow.req": {{4, 4, "root"}},
45+ "xproto/GetGeometry.req": {{4, 4, "root"}},
46+ "xproto/ChangeProperty.req": {{4, 4, "root"}, {8, 4, "atom"}},
47+ "xproto/CreateWindow.req": {{4, 4, "child"}, {8, 4, "root"}},
48+ "xproto/ChangeWindowAttributes.req": {{4, 4, "child"}},
49+ "xproto/ConfigureWindow.req": {{4, 4, "child"}},
50+ "xproto/ConfigureWindowSparse.req": {{4, 4, "child"}},
51+ "xproto/ConfigureWindowAll.req": {{4, 4, "child"}, {32, 4, "sibling"}},
52+ "render/CreatePicture.req": {{0, 1, "renderMajor"}, {4, 4, "picture"}, {8, 4, "child"}, {12, 4, "rootFormat"}},
53+ "res/QueryClientIds.req": {{0, 1, "resMajor"}},
54+ "xfixes/QueryVersion.req": {{0, 1, "xfixesMajor"}},
55+ "xproto/QueryExtension.reply": {{9, 1, "xfixesMajor"}, {10, 1, "xfixesEvent"}, {11, 1, "xfixesError"}},
56+ "xproto/GetGeometry.reply": {{8, 4, "root"}},
57+ "xproto/InternAtom.reply": {{8, 4, "atom"}},
58+ "xproto/GetWindowAttributes.reply": {{8, 4, "rootVisual"}, {28, 4, "rootColormap"}},
59+ "xproto/QueryTree.reply": {{8, 4, "root"}, {32, 4, "sibling"}, {36, 4, "child"}},
60+ "res/QueryClientIds.reply": {{44, 4, "base"}},
61+ "xproto/CreateNotify.event": {{4, 4, "root"}, {8, 4, "client2"}},
62+ "xproto/MapRequest.event": {{4, 4, "root"}, {8, 4, "client2"}},
63+ "xproto/ConfigureRequest.event": {{4, 4, "root"}, {8, 4, "client2"}},
64+ "xproto/MapNotify.event": {{4, 4, "root"}, {8, 4, "client2"}},
65+ "xproto/UnmapNotify.event": {{4, 4, "root"}, {8, 4, "client2"}},
66+ "xproto/DestroyNotify.event": {{4, 4, "root"}, {8, 4, "client2"}},
67+ "xproto/PropertyNotify.event": {{4, 4, "root"}, {8, 4, "atom"}, {12, 4, "time"}},
68+ "xproto/ClientMessage.event": {{4, 4, "client2"}, {8, 4, "atom"}},
69+ "xproto/SelectionNotify.event": {{8, 4, "child"}},
70+ "xfixes/SelectionNotify.event": {{0, 1, "xfixesEvent"}, {4, 4, "root"}, {8, 4, "client2"}, {16, 4, "time"}, {20, 4, "time"}},
71+ "xproto/BadWindow.error": {{4, 4, "badWindow"}},
72+ "xfixes/BadRegion.error": {{1, 1, "xfixesError"}, {4, 4, "badRegion"}, {10, 1, "xfixesMajor"}},
73+}
74+
75+// structural replies depend on the server build itself (extension list,
76+// visuals and picture formats), so live compare checks invariants instead of
77+// bytes; unit tests still decode the committed golden byte-exactly.
78+var structural = map[string]func(p []byte, roles map[string]uint32) error{
79+ "xproto/ListExtensions.reply": checkListExtensions,
80+ "render/QueryPictFormats.reply": checkQueryPictFormats,
81+}
82+
83+const rolesPath = "../../testdata/golden/xvfb-roles.json"
84+
85+// xvfbArgs lets a run simulate a different server layout, e.g.
86+// XTURBO_XVFB_ARGS="-extension GLX" shifts resource IDs, atoms and visuals.
87+func xvfbArgs() []string {
88+ args := []string{"-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-ac"}
89+ return append(args, strings.Fields(os.Getenv("XTURBO_XVFB_ARGS"))...)
90+}
91+
92+// TestCaptureRequests drives a fresh Xvfb through a raw socketpair tee. By
93+// default it compares every captured packet with the committed golden and
94+// fails on a difference; -update rewrites the goldens instead.
2695 func TestCaptureRequests(t *testing.T) {
27- if !*update {
28- t.Skip("pass -update to capture request goldens")
96+ if _, err := exec.LookPath("Xvfb"); err != nil {
97+ t.Skip("Xvfb not installed")
2998 }
3099 // displayfd avoids display-number races and tells us when Xvfb is ready.
31100 r, w, err := os.Pipe()
@@ -33,7 +102,7 @@ func TestCaptureRequests(t *testing.T) {
33102 t.Fatal(err)
34103 }
35104 defer r.Close()
36- cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-ac")
105+ cmd := exec.Command("Xvfb", xvfbArgs()...)
37106 cmd.ExtraFiles = []*os.File{w}
38107 var log bytes.Buffer
39108 cmd.Stderr = &log
@@ -327,6 +396,181 @@ func TestCaptureRequests(t *testing.T) {
327396 keep("xproto/GetPropertyEmpty", packet())
328397 write([]byte{99, 0, 1, 0}) // ListExtensions: variable STR iterator
329398 keep("xproto/ListExtensions", packet())
399+ // Event and error captures. The tee client acts as a window manager on the
400+ // root; a second raw client (direct socket, not teed) triggers the events.
401+ // Requests sent here on the tee use window/minor values the request-golden
402+ // classifier below ignores.
403+ xfixesEvent, xfixesError := ext[10], ext[11]
404+ events := map[string][]byte{}
405+ xerrors := map[string][]byte{}
406+ // collect reads events and errors until the reply fencing them.
407+ collect := func(names map[byte]string) {
408+ t.Helper()
409+ write([]byte{43, 0, 1, 0}) // GetInputFocus
410+ for {
411+ p := packet()
412+ switch {
413+ case p[0] == 1:
414+ return
415+ case p[0] == 0:
416+ name := names[p[1]]
417+ if name == "" {
418+ t.Fatalf("unexpected error %x", p)
419+ }
420+ xerrors[name] = p
421+ default:
422+ name := names[p[0]]
423+ if name == "" {
424+ t.Fatalf("unexpected event %x", p)
425+ }
426+ events[name] = p
427+ }
428+ }
429+ }
430+ const (
431+ maskSubstructureNotify = 1 << 19
432+ maskSubstructureRedirect = 1 << 20
433+ maskPropertyChange = 1 << 22
434+ atomPrimary, atomSecondary, atomString = 1, 2, 31
435+ )
436+ selectRoot := make([]byte, 16)
437+ selectRoot[0], selectRoot[2] = 2, 4
438+ binary.LittleEndian.PutUint32(selectRoot[4:], root)
439+ binary.LittleEndian.PutUint32(selectRoot[8:], 1<<11) // CWEventMask
440+ binary.LittleEndian.PutUint32(selectRoot[12:], maskSubstructureNotify|maskSubstructureRedirect|maskPropertyChange)
441+ write(selectRoot)
442+ selectSel := make([]byte, 16)
443+ selectSel[0], selectSel[1], selectSel[2] = major, 2, 4
444+ binary.LittleEndian.PutUint32(selectSel[4:], root)
445+ binary.LittleEndian.PutUint32(selectSel[8:], atomPrimary)
446+ binary.LittleEndian.PutUint32(selectSel[12:], 1) // SetSelectionOwner
447+ write(selectSel)
448+ collect(nil)
449+
450+ other, err := net.DialTimeout("unix", fmt.Sprintf("/tmp/.X11-unix/X%d", num), 5*time.Second)
451+ if err != nil {
452+ t.Fatal(err)
453+ }
454+ defer other.Close()
455+ _ = other.SetDeadline(time.Now().Add(5 * time.Second))
456+ send := func(p []byte) {
457+ t.Helper()
458+ if _, err := other.Write(p); err != nil {
459+ t.Fatal(err)
460+ }
461+ }
462+ recv := func(p []byte) {
463+ t.Helper()
464+ if _, err := io.ReadFull(other, p); err != nil {
465+ t.Fatal(err)
466+ }
467+ }
468+ // sync round-trips the second client; any error fails the capture.
469+ sync := func() {
470+ t.Helper()
471+ send([]byte{43, 0, 1, 0})
472+ var p [32]byte
473+ recv(p[:])
474+ if p[0] != 1 {
475+ t.Fatalf("second client: %x", p)
476+ }
477+ }
478+ send([]byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0})
479+ var otherHead [8]byte
480+ recv(otherHead[:])
481+ if otherHead[0] != 1 {
482+ t.Fatalf("second setup failed: %x", otherHead)
483+ }
484+ otherSetup := make([]byte, int(binary.LittleEndian.Uint16(otherHead[6:]))*4)
485+ recv(otherSetup)
486+ client2 := binary.LittleEndian.Uint32(otherSetup[4:]) | 1
487+ win := make([]byte, 32)
488+ win[0], win[2] = 1, 8
489+ binary.LittleEndian.PutUint32(win[4:], client2)
490+ binary.LittleEndian.PutUint32(win[8:], root)
491+ binary.LittleEndian.PutUint16(win[12:], 10)
492+ binary.LittleEndian.PutUint16(win[14:], 20)
493+ binary.LittleEndian.PutUint16(win[16:], 300)
494+ binary.LittleEndian.PutUint16(win[18:], 200)
495+ binary.LittleEndian.PutUint16(win[22:], 1)
496+ send(win) // CreateNotify
497+ mapOther := make([]byte, 8)
498+ mapOther[0], mapOther[2] = 8, 2
499+ binary.LittleEndian.PutUint32(mapOther[4:], client2)
500+ send(mapOther) // MapRequest (redirected)
501+ cfg := make([]byte, 24)
502+ cfg[0], cfg[2] = 12, 6
503+ binary.LittleEndian.PutUint32(cfg[4:], client2)
504+ binary.LittleEndian.PutUint16(cfg[8:], 1|2|4) // x, y, width
505+ binary.LittleEndian.PutUint32(cfg[12:], 0xfffffffb)
506+ binary.LittleEndian.PutUint32(cfg[16:], 40)
507+ binary.LittleEndian.PutUint32(cfg[20:], 640)
508+ send(cfg) // ConfigureRequest (redirected)
509+ prop := make([]byte, 28)
510+ prop[0], prop[2] = 18, 7
511+ binary.LittleEndian.PutUint32(prop[4:], root)
512+ binary.LittleEndian.PutUint32(prop[8:], atom)
513+ binary.LittleEndian.PutUint32(prop[12:], atomString)
514+ prop[16] = 8
515+ binary.LittleEndian.PutUint32(prop[20:], 2)
516+ copy(prop[24:], "hi")
517+ send(prop) // PropertyNotify (NewValue)
518+ msg := make([]byte, 44)
519+ msg[0], msg[2] = 25, 11
520+ binary.LittleEndian.PutUint32(msg[4:], root)
521+ binary.LittleEndian.PutUint32(msg[8:], maskSubstructureNotify|maskSubstructureRedirect)
522+ msg[12], msg[13] = 33, 32 // ClientMessage, format 32
523+ binary.LittleEndian.PutUint32(msg[16:], client2)
524+ binary.LittleEndian.PutUint32(msg[20:], atom)
525+ for i := range 5 {
526+ binary.LittleEndian.PutUint32(msg[24+4*i:], uint32(0x01020304*(i+1)))
527+ }
528+ send(msg) // ClientMessage (SendEvent bit set)
529+ owner := make([]byte, 16)
530+ owner[0], owner[2] = 22, 4
531+ binary.LittleEndian.PutUint32(owner[4:], client2)
532+ binary.LittleEndian.PutUint32(owner[8:], atomPrimary)
533+ send(owner) // xfixes SelectionNotify (SetSelectionOwner)
534+ sync()
535+ collect(map[byte]string{16: "xproto/CreateNotify", 20: "xproto/MapRequest", 23: "xproto/ConfigureRequest",
536+ 28: "xproto/PropertyNotify", 0x80 | 33: "xproto/ClientMessage", xfixesEvent: "xfixes/SelectionNotify"})
537+ // The window manager maps the window itself (MapNotify); the second client
538+ // then unmaps and destroys it.
539+ mapByWM := make([]byte, 8)
540+ mapByWM[0], mapByWM[2] = 8, 2
541+ binary.LittleEndian.PutUint32(mapByWM[4:], client2)
542+ write(mapByWM)
543+ collect(map[byte]string{19: "xproto/MapNotify"})
544+ unmap := []byte{10, 0, 2, 0, 0, 0, 0, 0}
545+ binary.LittleEndian.PutUint32(unmap[4:], client2)
546+ send(unmap)
547+ destroy := []byte{4, 0, 2, 0, 0, 0, 0, 0}
548+ binary.LittleEndian.PutUint32(destroy[4:], client2)
549+ send(destroy)
550+ sync()
551+ collect(map[byte]string{18: "xproto/UnmapNotify", 17: "xproto/DestroyNotify"})
552+ // Core SelectionNotify: SECONDARY has no owner, so the server replies itself.
553+ convert := make([]byte, 24)
554+ convert[0], convert[2] = 24, 6
555+ binary.LittleEndian.PutUint32(convert[4:], child)
556+ binary.LittleEndian.PutUint32(convert[8:], atomSecondary)
557+ binary.LittleEndian.PutUint32(convert[12:], atomString)
558+ binary.LittleEndian.PutUint32(convert[16:], atom)
559+ write(convert)
560+ collect(map[byte]string{31: "xproto/SelectionNotify"})
561+ // Errors: BadAtom, BadWindow and an extension error (XFIXES BadRegion).
562+ badAtom := []byte{17, 0, 2, 0, 0xef, 0xbe, 0xad, 0x0d}
563+ write(badAtom)
564+ badWindow := []byte{3, 0, 2, 0, 0, 0, 0, 0}
565+ binary.LittleEndian.PutUint32(badWindow[4:], base|0x7777)
566+ write(badWindow)
567+ badRegion := []byte{major, 10, 2, 0, 0, 0, 0, 0}
568+ binary.LittleEndian.PutUint32(badRegion[4:], base|0x8888)
569+ write(badRegion)
570+ collect(map[byte]string{5: "xproto/BadAtom", 3: "xproto/BadWindow", xfixesError: "xfixes/BadRegion"})
571+ if len(events) != 10 || len(xerrors) != 3 {
572+ t.Fatalf("captured %d events, %d errors", len(events), len(xerrors))
573+ }
330574 // Closing the client drains the request direction; wait before inspecting tee.
331575 client.Close()
332576 select {
@@ -353,13 +597,17 @@ func TestCaptureRequests(t *testing.T) {
353597 captured["xproto/CreateWindow"] = p
354598 }
355599 case 2:
356- captured["xproto/ChangeWindowAttributes"] = p
600+ if binary.LittleEndian.Uint32(p[4:]) == child {
601+ captured["xproto/ChangeWindowAttributes"] = p
602+ }
357603 case 12:
358604 names := []string{"ConfigureWindow", "ConfigureWindowSparse", "ConfigureWindowAll"}
359605 captured["xproto/"+names[configIndex]] = p
360606 configIndex++
361607 case 8:
362- captured["xproto/MapWindow"] = p
608+ if binary.LittleEndian.Uint32(p[4:]) == root {
609+ captured["xproto/MapWindow"] = p
610+ }
363611 case 14:
364612 captured["xproto/GetGeometry"] = p
365613 case 16:
@@ -373,7 +621,7 @@ func TestCaptureRequests(t *testing.T) {
373621 if p[0] == resMajor && p[1] == 4 {
374622 captured["res/QueryClientIds"] = p
375623 }
376- if p[0] == major {
624+ if p[0] == major && p[1] == 0 {
377625 captured["xfixes/QueryVersion"] = p
378626 }
379627 }
@@ -382,24 +630,211 @@ func TestCaptureRequests(t *testing.T) {
382630 if len(captured) != 12 || len(replies) != 11 {
383631 t.Fatal("missing captured requests or replies")
384632 }
633+ out := map[string][]byte{}
385634 for name, p := range replies {
386- path := filepath.Join("../../testdata/golden", name+".reply.hex")
387- if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
635+ out[name+".reply"] = p
636+ }
637+ for name, p := range captured {
638+ out[name+".req"] = p
639+ }
640+ for name, p := range events {
641+ out[name+".event"] = p
642+ }
643+ for name, p := range xerrors {
644+ out[name+".error"] = p
645+ }
646+ // Server-chosen values, keyed by the roles used in serverFields.
647+ roles := map[string]uint32{
648+ "root": root, "atom": atom, "base": base, "child": child, "sibling": sibling, "picture": picture,
649+ "client2": client2, "badWindow": base | 0x7777, "badRegion": base | 0x8888,
650+ "rootVisual": rootVisual, "rootColormap": binary.LittleEndian.Uint32(setup[screenOff+4:]), "rootFormat": format,
651+ "xfixesMajor": uint32(major), "xfixesEvent": uint32(xfixesEvent), "xfixesError": uint32(xfixesError),
652+ "resMajor": uint32(resMajor), "renderMajor": uint32(renderMajor),
653+ }
654+ if *update {
655+ for name, p := range out {
656+ writeGolden(t, name, p)
657+ }
658+ rolesJSON, err := json.MarshalIndent(roles, "", " ")
659+ if err != nil {
388660 t.Fatal(err)
389661 }
390- text := "# source: Xvfb reply via raw socketpair tee to an independent reference request\n# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n"
391- if err := os.WriteFile(path, []byte(text), 0644); err != nil {
662+ if err := os.WriteFile(rolesPath, append(rolesJSON, '\n'), 0644); err != nil {
392663 t.Fatal(err)
393664 }
665+ return
394666 }
395- for name, p := range captured {
396- path := filepath.Join("../../testdata/golden", name+".req.hex")
397- if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
398- t.Fatal(err)
667+ rolesJSON, err := os.ReadFile(rolesPath)
668+ if err != nil {
669+ t.Fatal(err)
670+ }
671+ var capturedRoles map[string]uint32
672+ if err := json.Unmarshal(rolesJSON, &capturedRoles); err != nil {
673+ t.Fatal(err)
674+ }
675+ for _, role := range slices.Sorted(maps.Keys(roles)) {
676+ if roles[role] != capturedRoles[role] {
677+ t.Logf("server-chosen %s: live %#x, golden %#x", role, roles[role], capturedRoles[role])
399678 }
400- text := "# source: Xvfb via raw socketpair tee; independent reference encoder; server accepted\n# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n"
401- if err := os.WriteFile(path, []byte(text), 0644); err != nil {
402- t.Fatal(err)
679+ }
680+ for name, p := range out {
681+ compareGolden(t, name, p, roles, capturedRoles)
682+ }
683+}
684+
685+var sources = map[string]string{
686+ "req": "# source: Xvfb via raw socketpair tee; independent reference encoder; server accepted\n",
687+ "reply": "# source: Xvfb reply via raw socketpair tee to an independent reference request\n",
688+ "event": "# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request\n",
689+ "error": "# source: Xvfb error via raw socketpair tee to an independent reference request\n",
690+}
691+
692+func writeGolden(t *testing.T, name string, p []byte) {
693+ t.Helper()
694+ path := filepath.Join("../../testdata/golden", name+".hex")
695+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
696+ t.Fatal(err)
697+ }
698+ source := sources[name[strings.LastIndexByte(name, '.')+1:]]
699+ text := source + "# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n"
700+ if err := os.WriteFile(path, []byte(text), 0644); err != nil {
701+ t.Fatal(err)
702+ }
703+}
704+
705+// compareGolden checks a live packet against the committed golden without
706+// depending on the server build: server-chosen fields must hold each side's
707+// own value (live roles, recorded roles) and are masked before an exact
708+// comparison; structural replies are checked for invariants only.
709+func compareGolden(t *testing.T, name string, live []byte, roles, recorded map[string]uint32) {
710+ t.Helper()
711+ want, err := LoadHex(filepath.Join("../../testdata/golden", name+".hex"))
712+ if err != nil {
713+ t.Errorf("%s: %v (run with -update to create it)", name, err)
714+ return
715+ }
716+ if check := structural[name]; check != nil {
717+ if err := check(live, roles); err != nil {
718+ t.Errorf("%s (live): %v", name, err)
719+ }
720+ if err := check(want, recorded); err != nil {
721+ t.Errorf("%s (golden): %v", name, err)
722+ }
723+ return
724+ }
725+ got, exp, err := normalize(name, live, roles), normalize(name, want, recorded), error(nil)
726+ for _, e := range []struct {
727+ side string
728+ err error
729+ }{{"live", got.err}, {"golden", exp.err}} {
730+ if e.err != nil {
731+ t.Errorf("%s (%s): %v", name, e.side, e.err)
732+ err = e.err
733+ }
734+ }
735+ if err == nil && !bytes.Equal(got.b, exp.b) {
736+ t.Errorf("%s differs from live Xvfb after masking server-chosen fields:\n got %x\nwant %x", name, live, want)
737+ }
738+}
739+
740+type normalized struct {
741+ b []byte
742+ err error
743+}
744+
745+// normalize verifies each server-chosen field of p holds roles[Role] and
746+// replaces it with zero bytes.
747+func normalize(name string, p []byte, roles map[string]uint32) normalized {
748+ b := bytes.Clone(p)
749+ for _, f := range serverFields[name] {
750+ if f.Off+f.Size > len(b) {
751+ return normalized{err: fmt.Errorf("field %s at %d beyond %d bytes", f.Role, f.Off, len(b))}
752+ }
753+ if f.Role != "time" {
754+ v, ok := roles[f.Role]
755+ if !ok {
756+ return normalized{err: fmt.Errorf("no recorded value for role %s", f.Role)}
757+ }
758+ var got uint32
759+ if f.Size == 1 {
760+ got = uint32(b[f.Off])
761+ } else {
762+ got = binary.LittleEndian.Uint32(b[f.Off:])
763+ }
764+ if got != v {
765+ return normalized{err: fmt.Errorf("field %s at %d = %#x, want %#x", f.Role, f.Off, got, v)}
766+ }
767+ }
768+ clear(b[f.Off : f.Off+f.Size])
769+ }
770+ return normalized{b: b}
771+}
772+
773+// checkListExtensions: a framed reply whose STR list holds exactly names_len
774+// entries filling the reply, including the extensions xturbo binds.
775+func checkListExtensions(p []byte, _ map[string]uint32) error {
776+ if len(p) < 32 || p[0] != 1 || len(p) != 32+4*int(binary.LittleEndian.Uint32(p[4:])) {
777+ return fmt.Errorf("bad framing")
778+ }
779+ names := map[string]bool{}
780+ off := 32
781+ for range int(p[1]) {
782+ if off >= len(p) || off+1+int(p[off]) > len(p) {
783+ return fmt.Errorf("STR list overruns the reply")
784+ }
785+ names[string(p[off+1:off+1+int(p[off])])] = true
786+ off += 1 + int(p[off])
787+ }
788+ if (off+3)&^3 != len(p) {
789+ return fmt.Errorf("STR list ends at %d, reply has %d bytes", off, len(p))
790+ }
791+ for _, want := range []string{"BIG-REQUESTS", "XC-MISC", "XFIXES", "RENDER", "SHAPE", "Composite", "X-Resource", "Generic Event Extension"} {
792+ if !names[want] {
793+ return fmt.Errorf("missing extension %q", want)
794+ }
795+ }
796+ return nil
797+}
798+
799+// checkQueryPictFormats: declared counts match the list sizes exactly and the
800+// root visual maps to the picture format CreatePicture used.
801+func checkQueryPictFormats(p []byte, roles map[string]uint32) error {
802+ u32 := binary.LittleEndian.Uint32
803+ if len(p) < 32 || p[0] != 1 || len(p) != 32+4*int(u32(p[4:])) {
804+ return fmt.Errorf("bad framing")
805+ }
806+ formats, screens, subpixels := int(u32(p[8:])), int(u32(p[12:])), int(u32(p[24:]))
807+ off := 32 + 28*formats
808+ found := false
809+ for range screens {
810+ if off+8 > len(p) {
811+ return fmt.Errorf("screens overrun")
403812 }
813+ depths := int(u32(p[off:]))
814+ off += 8
815+ for range depths {
816+ if off+8 > len(p) {
817+ return fmt.Errorf("depths overrun")
818+ }
819+ visuals := int(binary.LittleEndian.Uint16(p[off+2:]))
820+ off += 8
821+ for range visuals {
822+ if off+8 > len(p) {
823+ return fmt.Errorf("visuals overrun")
824+ }
825+ if u32(p[off:]) == roles["rootVisual"] && u32(p[off+4:]) == roles["rootFormat"] {
826+ found = true
827+ }
828+ off += 8
829+ }
830+ }
831+ }
832+ off += 4 * subpixels
833+ if off != len(p) {
834+ return fmt.Errorf("lists end at %d, reply has %d bytes", off, len(p))
835+ }
836+ if !found {
837+ return fmt.Errorf("root visual %#x does not map to format %#x", roles["rootVisual"], roles["rootFormat"])
404838 }
839+ return nil
405840 }
@@ -6,13 +6,16 @@ import (
6 "bytes"6 "bytes"
7 "encoding/binary"7 "encoding/binary"
8 "encoding/hex"8 "encoding/hex"
9+ "encoding/json"
9 "flag"10 "flag"
10 "fmt"11 "fmt"
11 "io"12 "io"
13+ "maps"
12 "net"14 "net"
13 "os"15 "os"
14 "os/exec"16 "os/exec"
15 "path/filepath"17 "path/filepath"
18+ "slices"
16 "strconv"19 "strconv"
17 "strings"20 "strings"
18 "testing"21 "testing"
@@ -21,11 +24,77 @@ import (
21 "golang.org/x/sys/unix"24 "golang.org/x/sys/unix"
22 )25 )
23 26
24-var update = flag.Bool("update", false, "refresh request goldens using Xvfb and a raw socketpair tee")27+var update = flag.Bool("update", false, "rewrite wire goldens from Xvfb instead of comparing against them")
25 28
29+// field is a server-dependent value inside a golden: the size-byte
30+// little-endian integer at Off must equal the server's value for Role.
31+type field struct {
32+ Off, Size int
33+ Role string
34+}
35+
36+// serverFields lists, per golden, every value the X server chooses: resource
37+// IDs derived from a server-assigned base, the root window and its visual and
38+// colormap, interned atoms, extension opcodes and bases, and timestamps.
39+// Compare mode checks that the golden holds the value recorded at capture
40+// (xvfb-roles.json) and the live packet holds the live server's value, then
41+// masks both and compares the remaining bytes exactly. Role "time" is a
42+// per-run server timestamp and is masked without a value check.
43+var serverFields = map[string][]field{
44+ "xproto/MapWindow.req": {{4, 4, "root"}},
45+ "xproto/GetGeometry.req": {{4, 4, "root"}},
46+ "xproto/ChangeProperty.req": {{4, 4, "root"}, {8, 4, "atom"}},
47+ "xproto/CreateWindow.req": {{4, 4, "child"}, {8, 4, "root"}},
48+ "xproto/ChangeWindowAttributes.req": {{4, 4, "child"}},
49+ "xproto/ConfigureWindow.req": {{4, 4, "child"}},
50+ "xproto/ConfigureWindowSparse.req": {{4, 4, "child"}},
51+ "xproto/ConfigureWindowAll.req": {{4, 4, "child"}, {32, 4, "sibling"}},
52+ "render/CreatePicture.req": {{0, 1, "renderMajor"}, {4, 4, "picture"}, {8, 4, "child"}, {12, 4, "rootFormat"}},
53+ "res/QueryClientIds.req": {{0, 1, "resMajor"}},
54+ "xfixes/QueryVersion.req": {{0, 1, "xfixesMajor"}},
55+ "xproto/QueryExtension.reply": {{9, 1, "xfixesMajor"}, {10, 1, "xfixesEvent"}, {11, 1, "xfixesError"}},
56+ "xproto/GetGeometry.reply": {{8, 4, "root"}},
57+ "xproto/InternAtom.reply": {{8, 4, "atom"}},
58+ "xproto/GetWindowAttributes.reply": {{8, 4, "rootVisual"}, {28, 4, "rootColormap"}},
59+ "xproto/QueryTree.reply": {{8, 4, "root"}, {32, 4, "sibling"}, {36, 4, "child"}},
60+ "res/QueryClientIds.reply": {{44, 4, "base"}},
61+ "xproto/CreateNotify.event": {{4, 4, "root"}, {8, 4, "client2"}},
62+ "xproto/MapRequest.event": {{4, 4, "root"}, {8, 4, "client2"}},
63+ "xproto/ConfigureRequest.event": {{4, 4, "root"}, {8, 4, "client2"}},
64+ "xproto/MapNotify.event": {{4, 4, "root"}, {8, 4, "client2"}},
65+ "xproto/UnmapNotify.event": {{4, 4, "root"}, {8, 4, "client2"}},
66+ "xproto/DestroyNotify.event": {{4, 4, "root"}, {8, 4, "client2"}},
67+ "xproto/PropertyNotify.event": {{4, 4, "root"}, {8, 4, "atom"}, {12, 4, "time"}},
68+ "xproto/ClientMessage.event": {{4, 4, "client2"}, {8, 4, "atom"}},
69+ "xproto/SelectionNotify.event": {{8, 4, "child"}},
70+ "xfixes/SelectionNotify.event": {{0, 1, "xfixesEvent"}, {4, 4, "root"}, {8, 4, "client2"}, {16, 4, "time"}, {20, 4, "time"}},
71+ "xproto/BadWindow.error": {{4, 4, "badWindow"}},
72+ "xfixes/BadRegion.error": {{1, 1, "xfixesError"}, {4, 4, "badRegion"}, {10, 1, "xfixesMajor"}},
73+}
74+
75+// structural replies depend on the server build itself (extension list,
76+// visuals and picture formats), so live compare checks invariants instead of
77+// bytes; unit tests still decode the committed golden byte-exactly.
78+var structural = map[string]func(p []byte, roles map[string]uint32) error{
79+ "xproto/ListExtensions.reply": checkListExtensions,
80+ "render/QueryPictFormats.reply": checkQueryPictFormats,
81+}
82+
83+const rolesPath = "../../testdata/golden/xvfb-roles.json"
84+
85+// xvfbArgs lets a run simulate a different server layout, e.g.
86+// XTURBO_XVFB_ARGS="-extension GLX" shifts resource IDs, atoms and visuals.
87+func xvfbArgs() []string {
88+ args := []string{"-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-ac"}
89+ return append(args, strings.Fields(os.Getenv("XTURBO_XVFB_ARGS"))...)
90+}
91+
92+// TestCaptureRequests drives a fresh Xvfb through a raw socketpair tee. By
93+// default it compares every captured packet with the committed golden and
94+// fails on a difference; -update rewrites the goldens instead.
26 func TestCaptureRequests(t *testing.T) {95 func TestCaptureRequests(t *testing.T) {
27- if !*update {96+ if _, err := exec.LookPath("Xvfb"); err != nil {
28- t.Skip("pass -update to capture request goldens")97+ t.Skip("Xvfb not installed")
29 }98 }
30 // displayfd avoids display-number races and tells us when Xvfb is ready.99 // displayfd avoids display-number races and tells us when Xvfb is ready.
31 r, w, err := os.Pipe()100 r, w, err := os.Pipe()
@@ -33,7 +102,7 @@ func TestCaptureRequests(t *testing.T) {
33 t.Fatal(err)102 t.Fatal(err)
34 }103 }
35 defer r.Close()104 defer r.Close()
36- cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-ac")105+ cmd := exec.Command("Xvfb", xvfbArgs()...)
37 cmd.ExtraFiles = []*os.File{w}106 cmd.ExtraFiles = []*os.File{w}
38 var log bytes.Buffer107 var log bytes.Buffer
39 cmd.Stderr = &log108 cmd.Stderr = &log
@@ -327,6 +396,181 @@ func TestCaptureRequests(t *testing.T) {
327 keep("xproto/GetPropertyEmpty", packet())396 keep("xproto/GetPropertyEmpty", packet())
328 write([]byte{99, 0, 1, 0}) // ListExtensions: variable STR iterator397 write([]byte{99, 0, 1, 0}) // ListExtensions: variable STR iterator
329 keep("xproto/ListExtensions", packet())398 keep("xproto/ListExtensions", packet())
399+ // Event and error captures. The tee client acts as a window manager on the
400+ // root; a second raw client (direct socket, not teed) triggers the events.
401+ // Requests sent here on the tee use window/minor values the request-golden
402+ // classifier below ignores.
403+ xfixesEvent, xfixesError := ext[10], ext[11]
404+ events := map[string][]byte{}
405+ xerrors := map[string][]byte{}
406+ // collect reads events and errors until the reply fencing them.
407+ collect := func(names map[byte]string) {
408+ t.Helper()
409+ write([]byte{43, 0, 1, 0}) // GetInputFocus
410+ for {
411+ p := packet()
412+ switch {
413+ case p[0] == 1:
414+ return
415+ case p[0] == 0:
416+ name := names[p[1]]
417+ if name == "" {
418+ t.Fatalf("unexpected error %x", p)
419+ }
420+ xerrors[name] = p
421+ default:
422+ name := names[p[0]]
423+ if name == "" {
424+ t.Fatalf("unexpected event %x", p)
425+ }
426+ events[name] = p
427+ }
428+ }
429+ }
430+ const (
431+ maskSubstructureNotify = 1 << 19
432+ maskSubstructureRedirect = 1 << 20
433+ maskPropertyChange = 1 << 22
434+ atomPrimary, atomSecondary, atomString = 1, 2, 31
435+ )
436+ selectRoot := make([]byte, 16)
437+ selectRoot[0], selectRoot[2] = 2, 4
438+ binary.LittleEndian.PutUint32(selectRoot[4:], root)
439+ binary.LittleEndian.PutUint32(selectRoot[8:], 1<<11) // CWEventMask
440+ binary.LittleEndian.PutUint32(selectRoot[12:], maskSubstructureNotify|maskSubstructureRedirect|maskPropertyChange)
441+ write(selectRoot)
442+ selectSel := make([]byte, 16)
443+ selectSel[0], selectSel[1], selectSel[2] = major, 2, 4
444+ binary.LittleEndian.PutUint32(selectSel[4:], root)
445+ binary.LittleEndian.PutUint32(selectSel[8:], atomPrimary)
446+ binary.LittleEndian.PutUint32(selectSel[12:], 1) // SetSelectionOwner
447+ write(selectSel)
448+ collect(nil)
449+
450+ other, err := net.DialTimeout("unix", fmt.Sprintf("/tmp/.X11-unix/X%d", num), 5*time.Second)
451+ if err != nil {
452+ t.Fatal(err)
453+ }
454+ defer other.Close()
455+ _ = other.SetDeadline(time.Now().Add(5 * time.Second))
456+ send := func(p []byte) {
457+ t.Helper()
458+ if _, err := other.Write(p); err != nil {
459+ t.Fatal(err)
460+ }
461+ }
462+ recv := func(p []byte) {
463+ t.Helper()
464+ if _, err := io.ReadFull(other, p); err != nil {
465+ t.Fatal(err)
466+ }
467+ }
468+ // sync round-trips the second client; any error fails the capture.
469+ sync := func() {
470+ t.Helper()
471+ send([]byte{43, 0, 1, 0})
472+ var p [32]byte
473+ recv(p[:])
474+ if p[0] != 1 {
475+ t.Fatalf("second client: %x", p)
476+ }
477+ }
478+ send([]byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0})
479+ var otherHead [8]byte
480+ recv(otherHead[:])
481+ if otherHead[0] != 1 {
482+ t.Fatalf("second setup failed: %x", otherHead)
483+ }
484+ otherSetup := make([]byte, int(binary.LittleEndian.Uint16(otherHead[6:]))*4)
485+ recv(otherSetup)
486+ client2 := binary.LittleEndian.Uint32(otherSetup[4:]) | 1
487+ win := make([]byte, 32)
488+ win[0], win[2] = 1, 8
489+ binary.LittleEndian.PutUint32(win[4:], client2)
490+ binary.LittleEndian.PutUint32(win[8:], root)
491+ binary.LittleEndian.PutUint16(win[12:], 10)
492+ binary.LittleEndian.PutUint16(win[14:], 20)
493+ binary.LittleEndian.PutUint16(win[16:], 300)
494+ binary.LittleEndian.PutUint16(win[18:], 200)
495+ binary.LittleEndian.PutUint16(win[22:], 1)
496+ send(win) // CreateNotify
497+ mapOther := make([]byte, 8)
498+ mapOther[0], mapOther[2] = 8, 2
499+ binary.LittleEndian.PutUint32(mapOther[4:], client2)
500+ send(mapOther) // MapRequest (redirected)
501+ cfg := make([]byte, 24)
502+ cfg[0], cfg[2] = 12, 6
503+ binary.LittleEndian.PutUint32(cfg[4:], client2)
504+ binary.LittleEndian.PutUint16(cfg[8:], 1|2|4) // x, y, width
505+ binary.LittleEndian.PutUint32(cfg[12:], 0xfffffffb)
506+ binary.LittleEndian.PutUint32(cfg[16:], 40)
507+ binary.LittleEndian.PutUint32(cfg[20:], 640)
508+ send(cfg) // ConfigureRequest (redirected)
509+ prop := make([]byte, 28)
510+ prop[0], prop[2] = 18, 7
511+ binary.LittleEndian.PutUint32(prop[4:], root)
512+ binary.LittleEndian.PutUint32(prop[8:], atom)
513+ binary.LittleEndian.PutUint32(prop[12:], atomString)
514+ prop[16] = 8
515+ binary.LittleEndian.PutUint32(prop[20:], 2)
516+ copy(prop[24:], "hi")
517+ send(prop) // PropertyNotify (NewValue)
518+ msg := make([]byte, 44)
519+ msg[0], msg[2] = 25, 11
520+ binary.LittleEndian.PutUint32(msg[4:], root)
521+ binary.LittleEndian.PutUint32(msg[8:], maskSubstructureNotify|maskSubstructureRedirect)
522+ msg[12], msg[13] = 33, 32 // ClientMessage, format 32
523+ binary.LittleEndian.PutUint32(msg[16:], client2)
524+ binary.LittleEndian.PutUint32(msg[20:], atom)
525+ for i := range 5 {
526+ binary.LittleEndian.PutUint32(msg[24+4*i:], uint32(0x01020304*(i+1)))
527+ }
528+ send(msg) // ClientMessage (SendEvent bit set)
529+ owner := make([]byte, 16)
530+ owner[0], owner[2] = 22, 4
531+ binary.LittleEndian.PutUint32(owner[4:], client2)
532+ binary.LittleEndian.PutUint32(owner[8:], atomPrimary)
533+ send(owner) // xfixes SelectionNotify (SetSelectionOwner)
534+ sync()
535+ collect(map[byte]string{16: "xproto/CreateNotify", 20: "xproto/MapRequest", 23: "xproto/ConfigureRequest",
536+ 28: "xproto/PropertyNotify", 0x80 | 33: "xproto/ClientMessage", xfixesEvent: "xfixes/SelectionNotify"})
537+ // The window manager maps the window itself (MapNotify); the second client
538+ // then unmaps and destroys it.
539+ mapByWM := make([]byte, 8)
540+ mapByWM[0], mapByWM[2] = 8, 2
541+ binary.LittleEndian.PutUint32(mapByWM[4:], client2)
542+ write(mapByWM)
543+ collect(map[byte]string{19: "xproto/MapNotify"})
544+ unmap := []byte{10, 0, 2, 0, 0, 0, 0, 0}
545+ binary.LittleEndian.PutUint32(unmap[4:], client2)
546+ send(unmap)
547+ destroy := []byte{4, 0, 2, 0, 0, 0, 0, 0}
548+ binary.LittleEndian.PutUint32(destroy[4:], client2)
549+ send(destroy)
550+ sync()
551+ collect(map[byte]string{18: "xproto/UnmapNotify", 17: "xproto/DestroyNotify"})
552+ // Core SelectionNotify: SECONDARY has no owner, so the server replies itself.
553+ convert := make([]byte, 24)
554+ convert[0], convert[2] = 24, 6
555+ binary.LittleEndian.PutUint32(convert[4:], child)
556+ binary.LittleEndian.PutUint32(convert[8:], atomSecondary)
557+ binary.LittleEndian.PutUint32(convert[12:], atomString)
558+ binary.LittleEndian.PutUint32(convert[16:], atom)
559+ write(convert)
560+ collect(map[byte]string{31: "xproto/SelectionNotify"})
561+ // Errors: BadAtom, BadWindow and an extension error (XFIXES BadRegion).
562+ badAtom := []byte{17, 0, 2, 0, 0xef, 0xbe, 0xad, 0x0d}
563+ write(badAtom)
564+ badWindow := []byte{3, 0, 2, 0, 0, 0, 0, 0}
565+ binary.LittleEndian.PutUint32(badWindow[4:], base|0x7777)
566+ write(badWindow)
567+ badRegion := []byte{major, 10, 2, 0, 0, 0, 0, 0}
568+ binary.LittleEndian.PutUint32(badRegion[4:], base|0x8888)
569+ write(badRegion)
570+ collect(map[byte]string{5: "xproto/BadAtom", 3: "xproto/BadWindow", xfixesError: "xfixes/BadRegion"})
571+ if len(events) != 10 || len(xerrors) != 3 {
572+ t.Fatalf("captured %d events, %d errors", len(events), len(xerrors))
573+ }
330 // Closing the client drains the request direction; wait before inspecting tee.574 // Closing the client drains the request direction; wait before inspecting tee.
331 client.Close()575 client.Close()
332 select {576 select {
@@ -353,13 +597,17 @@ func TestCaptureRequests(t *testing.T) {
353 captured["xproto/CreateWindow"] = p597 captured["xproto/CreateWindow"] = p
354 }598 }
355 case 2:599 case 2:
356- captured["xproto/ChangeWindowAttributes"] = p600+ if binary.LittleEndian.Uint32(p[4:]) == child {
601+ captured["xproto/ChangeWindowAttributes"] = p
602+ }
357 case 12:603 case 12:
358 names := []string{"ConfigureWindow", "ConfigureWindowSparse", "ConfigureWindowAll"}604 names := []string{"ConfigureWindow", "ConfigureWindowSparse", "ConfigureWindowAll"}
359 captured["xproto/"+names[configIndex]] = p605 captured["xproto/"+names[configIndex]] = p
360 configIndex++606 configIndex++
361 case 8:607 case 8:
362- captured["xproto/MapWindow"] = p608+ if binary.LittleEndian.Uint32(p[4:]) == root {
609+ captured["xproto/MapWindow"] = p
610+ }
363 case 14:611 case 14:
364 captured["xproto/GetGeometry"] = p612 captured["xproto/GetGeometry"] = p
365 case 16:613 case 16:
@@ -373,7 +621,7 @@ func TestCaptureRequests(t *testing.T) {
373 if p[0] == resMajor && p[1] == 4 {621 if p[0] == resMajor && p[1] == 4 {
374 captured["res/QueryClientIds"] = p622 captured["res/QueryClientIds"] = p
375 }623 }
376- if p[0] == major {624+ if p[0] == major && p[1] == 0 {
377 captured["xfixes/QueryVersion"] = p625 captured["xfixes/QueryVersion"] = p
378 }626 }
379 }627 }
@@ -382,24 +630,211 @@ func TestCaptureRequests(t *testing.T) {
382 if len(captured) != 12 || len(replies) != 11 {630 if len(captured) != 12 || len(replies) != 11 {
383 t.Fatal("missing captured requests or replies")631 t.Fatal("missing captured requests or replies")
384 }632 }
633+ out := map[string][]byte{}
385 for name, p := range replies {634 for name, p := range replies {
386- path := filepath.Join("../../testdata/golden", name+".reply.hex")635+ out[name+".reply"] = p
387- if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {636+ }
637+ for name, p := range captured {
638+ out[name+".req"] = p
639+ }
640+ for name, p := range events {
641+ out[name+".event"] = p
642+ }
643+ for name, p := range xerrors {
644+ out[name+".error"] = p
645+ }
646+ // Server-chosen values, keyed by the roles used in serverFields.
647+ roles := map[string]uint32{
648+ "root": root, "atom": atom, "base": base, "child": child, "sibling": sibling, "picture": picture,
649+ "client2": client2, "badWindow": base | 0x7777, "badRegion": base | 0x8888,
650+ "rootVisual": rootVisual, "rootColormap": binary.LittleEndian.Uint32(setup[screenOff+4:]), "rootFormat": format,
651+ "xfixesMajor": uint32(major), "xfixesEvent": uint32(xfixesEvent), "xfixesError": uint32(xfixesError),
652+ "resMajor": uint32(resMajor), "renderMajor": uint32(renderMajor),
653+ }
654+ if *update {
655+ for name, p := range out {
656+ writeGolden(t, name, p)
657+ }
658+ rolesJSON, err := json.MarshalIndent(roles, "", " ")
659+ if err != nil {
388 t.Fatal(err)660 t.Fatal(err)
389 }661 }
390- text := "# source: Xvfb reply via raw socketpair tee to an independent reference request\n# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n"662+ if err := os.WriteFile(rolesPath, append(rolesJSON, '\n'), 0644); err != nil {
391- if err := os.WriteFile(path, []byte(text), 0644); err != nil {
392 t.Fatal(err)663 t.Fatal(err)
393 }664 }
665+ return
394 }666 }
395- for name, p := range captured {667+ rolesJSON, err := os.ReadFile(rolesPath)
396- path := filepath.Join("../../testdata/golden", name+".req.hex")668+ if err != nil {
397- if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {669+ t.Fatal(err)
398- t.Fatal(err)670+ }
671+ var capturedRoles map[string]uint32
672+ if err := json.Unmarshal(rolesJSON, &capturedRoles); err != nil {
673+ t.Fatal(err)
674+ }
675+ for _, role := range slices.Sorted(maps.Keys(roles)) {
676+ if roles[role] != capturedRoles[role] {
677+ t.Logf("server-chosen %s: live %#x, golden %#x", role, roles[role], capturedRoles[role])
399 }678 }
400- text := "# source: Xvfb via raw socketpair tee; independent reference encoder; server accepted\n# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n"679+ }
401- if err := os.WriteFile(path, []byte(text), 0644); err != nil {680+ for name, p := range out {
402- t.Fatal(err)681+ compareGolden(t, name, p, roles, capturedRoles)
682+ }
683+}
684+
685+var sources = map[string]string{
686+ "req": "# source: Xvfb via raw socketpair tee; independent reference encoder; server accepted\n",
687+ "reply": "# source: Xvfb reply via raw socketpair tee to an independent reference request\n",
688+ "event": "# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request\n",
689+ "error": "# source: Xvfb error via raw socketpair tee to an independent reference request\n",
690+}
691+
692+func writeGolden(t *testing.T, name string, p []byte) {
693+ t.Helper()
694+ path := filepath.Join("../../testdata/golden", name+".hex")
695+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
696+ t.Fatal(err)
697+ }
698+ source := sources[name[strings.LastIndexByte(name, '.')+1:]]
699+ text := source + "# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n"
700+ if err := os.WriteFile(path, []byte(text), 0644); err != nil {
701+ t.Fatal(err)
702+ }
703+}
704+
705+// compareGolden checks a live packet against the committed golden without
706+// depending on the server build: server-chosen fields must hold each side's
707+// own value (live roles, recorded roles) and are masked before an exact
708+// comparison; structural replies are checked for invariants only.
709+func compareGolden(t *testing.T, name string, live []byte, roles, recorded map[string]uint32) {
710+ t.Helper()
711+ want, err := LoadHex(filepath.Join("../../testdata/golden", name+".hex"))
712+ if err != nil {
713+ t.Errorf("%s: %v (run with -update to create it)", name, err)
714+ return
715+ }
716+ if check := structural[name]; check != nil {
717+ if err := check(live, roles); err != nil {
718+ t.Errorf("%s (live): %v", name, err)
719+ }
720+ if err := check(want, recorded); err != nil {
721+ t.Errorf("%s (golden): %v", name, err)
722+ }
723+ return
724+ }
725+ got, exp, err := normalize(name, live, roles), normalize(name, want, recorded), error(nil)
726+ for _, e := range []struct {
727+ side string
728+ err error
729+ }{{"live", got.err}, {"golden", exp.err}} {
730+ if e.err != nil {
731+ t.Errorf("%s (%s): %v", name, e.side, e.err)
732+ err = e.err
733+ }
734+ }
735+ if err == nil && !bytes.Equal(got.b, exp.b) {
736+ t.Errorf("%s differs from live Xvfb after masking server-chosen fields:\n got %x\nwant %x", name, live, want)
737+ }
738+}
739+
740+type normalized struct {
741+ b []byte
742+ err error
743+}
744+
745+// normalize verifies each server-chosen field of p holds roles[Role] and
746+// replaces it with zero bytes.
747+func normalize(name string, p []byte, roles map[string]uint32) normalized {
748+ b := bytes.Clone(p)
749+ for _, f := range serverFields[name] {
750+ if f.Off+f.Size > len(b) {
751+ return normalized{err: fmt.Errorf("field %s at %d beyond %d bytes", f.Role, f.Off, len(b))}
752+ }
753+ if f.Role != "time" {
754+ v, ok := roles[f.Role]
755+ if !ok {
756+ return normalized{err: fmt.Errorf("no recorded value for role %s", f.Role)}
757+ }
758+ var got uint32
759+ if f.Size == 1 {
760+ got = uint32(b[f.Off])
761+ } else {
762+ got = binary.LittleEndian.Uint32(b[f.Off:])
763+ }
764+ if got != v {
765+ return normalized{err: fmt.Errorf("field %s at %d = %#x, want %#x", f.Role, f.Off, got, v)}
766+ }
767+ }
768+ clear(b[f.Off : f.Off+f.Size])
769+ }
770+ return normalized{b: b}
771+}
772+
773+// checkListExtensions: a framed reply whose STR list holds exactly names_len
774+// entries filling the reply, including the extensions xturbo binds.
775+func checkListExtensions(p []byte, _ map[string]uint32) error {
776+ if len(p) < 32 || p[0] != 1 || len(p) != 32+4*int(binary.LittleEndian.Uint32(p[4:])) {
777+ return fmt.Errorf("bad framing")
778+ }
779+ names := map[string]bool{}
780+ off := 32
781+ for range int(p[1]) {
782+ if off >= len(p) || off+1+int(p[off]) > len(p) {
783+ return fmt.Errorf("STR list overruns the reply")
784+ }
785+ names[string(p[off+1:off+1+int(p[off])])] = true
786+ off += 1 + int(p[off])
787+ }
788+ if (off+3)&^3 != len(p) {
789+ return fmt.Errorf("STR list ends at %d, reply has %d bytes", off, len(p))
790+ }
791+ for _, want := range []string{"BIG-REQUESTS", "XC-MISC", "XFIXES", "RENDER", "SHAPE", "Composite", "X-Resource", "Generic Event Extension"} {
792+ if !names[want] {
793+ return fmt.Errorf("missing extension %q", want)
794+ }
795+ }
796+ return nil
797+}
798+
799+// checkQueryPictFormats: declared counts match the list sizes exactly and the
800+// root visual maps to the picture format CreatePicture used.
801+func checkQueryPictFormats(p []byte, roles map[string]uint32) error {
802+ u32 := binary.LittleEndian.Uint32
803+ if len(p) < 32 || p[0] != 1 || len(p) != 32+4*int(u32(p[4:])) {
804+ return fmt.Errorf("bad framing")
805+ }
806+ formats, screens, subpixels := int(u32(p[8:])), int(u32(p[12:])), int(u32(p[24:]))
807+ off := 32 + 28*formats
808+ found := false
809+ for range screens {
810+ if off+8 > len(p) {
811+ return fmt.Errorf("screens overrun")
403 }812 }
813+ depths := int(u32(p[off:]))
814+ off += 8
815+ for range depths {
816+ if off+8 > len(p) {
817+ return fmt.Errorf("depths overrun")
818+ }
819+ visuals := int(binary.LittleEndian.Uint16(p[off+2:]))
820+ off += 8
821+ for range visuals {
822+ if off+8 > len(p) {
823+ return fmt.Errorf("visuals overrun")
824+ }
825+ if u32(p[off:]) == roles["rootVisual"] && u32(p[off+4:]) == roles["rootFormat"] {
826+ found = true
827+ }
828+ off += 8
829+ }
830+ }
831+ }
832+ off += 4 * subpixels
833+ if off != len(p) {
834+ return fmt.Errorf("lists end at %d, reply has %d bytes", off, len(p))
835+ }
836+ if !found {
837+ return fmt.Errorf("root visual %#x does not map to format %#x", roles["rootVisual"], roles["rootFormat"])
404 }838 }
839+ return nil
405 }840 }
A render/errors_generated.go +21 -0
@@ -0,0 +1,21 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package render
3+
4+// Error codes: error codes relative to the extension's FirstError (XError.Rel).
5+const (
6+ BadPictFormat uint8 = 0
7+ BadPicture uint8 = 1
8+ BadPictOp uint8 = 2
9+ BadGlyphSet uint8 = 3
10+ BadGlyph uint8 = 4
11+)
12+
13+var errorNames = [...]string{"BadPictFormat", "BadPicture", "BadPictOp", "BadGlyphSet", "BadGlyph"}
14+
15+// ErrorName returns the name of an error code (error codes relative to the extension's FirstError (XError.Rel)), or "" when unknown.
16+func ErrorName(code uint8) string {
17+ if int(code) < len(errorNames) {
18+ return errorNames[code]
19+ }
20+ return ""
21+}
@@ -0,0 +1,21 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package render
3+
4+// Error codes: error codes relative to the extension's FirstError (XError.Rel).
5+const (
6+ BadPictFormat uint8 = 0
7+ BadPicture uint8 = 1
8+ BadPictOp uint8 = 2
9+ BadGlyphSet uint8 = 3
10+ BadGlyph uint8 = 4
11+)
12+
13+var errorNames = [...]string{"BadPictFormat", "BadPicture", "BadPictOp", "BadGlyphSet", "BadGlyph"}
14+
15+// ErrorName returns the name of an error code (error codes relative to the extension's FirstError (XError.Rel)), or "" when unknown.
16+func ErrorName(code uint8) string {
17+ if int(code) < len(errorNames) {
18+ return errorNames[code]
19+ }
20+ return ""
21+}
A shape/events_fuzz_test.go +28 -0
@@ -0,0 +1,28 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package shape
3+
4+import "testing"
5+import "bnema.dev/xturbo"
6+
7+func FuzzEventViews(f *testing.F) {
8+ f.Add(make([]byte, 32))
9+ f.Fuzz(func(t *testing.T, raw []byte) {
10+ if len(raw) < 32 {
11+ return
12+ }
13+ var ev xturbo.Event
14+ copy(ev.Raw[:], raw)
15+ ev.Tail = raw[32:]
16+ {
17+ vNotify := NotifyOf(&ev)
18+ _ = vNotify.ShapeKind()
19+ _ = vNotify.AffectedWindow()
20+ _ = vNotify.ExtentsX()
21+ _ = vNotify.ExtentsY()
22+ _ = vNotify.ExtentsWidth()
23+ _ = vNotify.ExtentsHeight()
24+ _ = vNotify.ServerTime()
25+ _ = vNotify.Shaped()
26+ }
27+ })
28+}
@@ -0,0 +1,28 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package shape
3+
4+import "testing"
5+import "bnema.dev/xturbo"
6+
7+func FuzzEventViews(f *testing.F) {
8+ f.Add(make([]byte, 32))
9+ f.Fuzz(func(t *testing.T, raw []byte) {
10+ if len(raw) < 32 {
11+ return
12+ }
13+ var ev xturbo.Event
14+ copy(ev.Raw[:], raw)
15+ ev.Tail = raw[32:]
16+ {
17+ vNotify := NotifyOf(&ev)
18+ _ = vNotify.ShapeKind()
19+ _ = vNotify.AffectedWindow()
20+ _ = vNotify.ExtentsX()
21+ _ = vNotify.ExtentsY()
22+ _ = vNotify.ExtentsWidth()
23+ _ = vNotify.ExtentsHeight()
24+ _ = vNotify.ServerTime()
25+ _ = vNotify.Shaped()
26+ }
27+ })
28+}
A shape/events_generated.go +28 -0
@@ -0,0 +1,28 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package shape
3+
4+import "bnema.dev/xturbo/xproto"
5+import "encoding/binary"
6+import "bnema.dev/xturbo"
7+
8+// KindNotify identifies Notify events in xturbo.Event.Kind.
9+const KindNotify = xturbo.EventKind(xturbo.ExtShape) << 8
10+
11+// NotifyEvent is a view over a Notify event's 32 bytes. Getters read the copied Event.Raw, so a view stays valid as long as the Event value it was taken from.
12+type NotifyEvent struct{ raw *[32]byte }
13+
14+// NotifyOf views ev as Notify without checking ev.Kind: a wrong kind yields garbage, never a panic.
15+func NotifyOf(ev *xturbo.Event) NotifyEvent { return NotifyEvent{raw: &ev.Raw} }
16+func (e NotifyEvent) Raw() *[32]byte { return e.raw }
17+func (e NotifyEvent) ShapeKind() Sk { return Sk(e.raw[1:][0]) }
18+func (e NotifyEvent) AffectedWindow() xproto.Window {
19+ return xproto.Window(binary.LittleEndian.Uint32(e.raw[4:]))
20+}
21+func (e NotifyEvent) ExtentsX() int16 { return int16(binary.LittleEndian.Uint16(e.raw[8:])) }
22+func (e NotifyEvent) ExtentsY() int16 { return int16(binary.LittleEndian.Uint16(e.raw[10:])) }
23+func (e NotifyEvent) ExtentsWidth() uint16 { return uint16(binary.LittleEndian.Uint16(e.raw[12:])) }
24+func (e NotifyEvent) ExtentsHeight() uint16 { return uint16(binary.LittleEndian.Uint16(e.raw[14:])) }
25+func (e NotifyEvent) ServerTime() xproto.Timestamp {
26+ return xproto.Timestamp(binary.LittleEndian.Uint32(e.raw[16:]))
27+}
28+func (e NotifyEvent) Shaped() bool { return (e.raw[20:][0] != 0) }
@@ -0,0 +1,28 @@
1+// Code generated by xtgen; DO NOT EDIT.
2+package shape
3+
4+import "bnema.dev/xturbo/xproto"
5+import "encoding/binary"
6+import "bnema.dev/xturbo"
7+
8+// KindNotify identifies Notify events in xturbo.Event.Kind.
9+const KindNotify = xturbo.EventKind(xturbo.ExtShape) << 8
10+
11+// NotifyEvent is a view over a Notify event's 32 bytes. Getters read the copied Event.Raw, so a view stays valid as long as the Event value it was taken from.
12+type NotifyEvent struct{ raw *[32]byte }
13+
14+// NotifyOf views ev as Notify without checking ev.Kind: a wrong kind yields garbage, never a panic.
15+func NotifyOf(ev *xturbo.Event) NotifyEvent { return NotifyEvent{raw: &ev.Raw} }
16+func (e NotifyEvent) Raw() *[32]byte { return e.raw }
17+func (e NotifyEvent) ShapeKind() Sk { return Sk(e.raw[1:][0]) }
18+func (e NotifyEvent) AffectedWindow() xproto.Window {
19+ return xproto.Window(binary.LittleEndian.Uint32(e.raw[4:]))
20+}
21+func (e NotifyEvent) ExtentsX() int16 { return int16(binary.LittleEndian.Uint16(e.raw[8:])) }
22+func (e NotifyEvent) ExtentsY() int16 { return int16(binary.LittleEndian.Uint16(e.raw[10:])) }
23+func (e NotifyEvent) ExtentsWidth() uint16 { return uint16(binary.LittleEndian.Uint16(e.raw[12:])) }
24+func (e NotifyEvent) ExtentsHeight() uint16 { return uint16(binary.LittleEndian.Uint16(e.raw[14:])) }
25+func (e NotifyEvent) ServerTime() xproto.Timestamp {
26+ return xproto.Timestamp(binary.LittleEndian.Uint32(e.raw[16:]))
27+}
28+func (e NotifyEvent) Shaped() bool { return (e.raw[20:][0] != 0) }
A testdata/golden/xfixes/BadRegion.error.hex +3 -0
@@ -0,0 +1,3 @@
1+# source: Xvfb error via raw socketpair tee to an independent reference request
2+# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace
3+008c2400888820000a008a000000000000000000000000000000000000000000
@@ -0,0 +1,3 @@
1+# source: Xvfb error via raw socketpair tee to an independent reference request
2+# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace
3+008c2400888820000a008a000000000000000000000000000000000000000000
A testdata/golden/xfixes/SelectionNotify.event.hex +3 -0
@@ -0,0 +1,3 @@
1+# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request
2+# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace
3+57001b009f03000001004000010000003e8987003e8987000000000000000000
@@ -0,0 +1,3 @@
1+# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request
2+# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace
3+57001b009f03000001004000010000003e8987003e8987000000000000000000
A testdata/golden/xproto/BadAtom.error.hex +3 -0
@@ -0,0 +1,3 @@
1+# source: Xvfb error via raw socketpair tee to an independent reference request
2+# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace
3+00052200efbead0d000011000000000000000000000000000000000000000000
@@ -0,0 +1,3 @@
1+# source: Xvfb error via raw socketpair tee to an independent reference request
2+# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace
3+00052200efbead0d000011000000000000000000000000000000000000000000
A testdata/golden/xproto/BadWindow.error.hex +3 -0
@@ -0,0 +1,3 @@
1+# source: Xvfb error via raw socketpair tee to an independent reference request
2+# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace
3+0003230077772000000003000000000000000000000000000000000000000000
@@ -0,0 +1,3 @@
1+# source: Xvfb error via raw socketpair tee to an independent reference request
2+# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace
3+0003230077772000000003000000000000000000000000000000000000000000
A testdata/golden/xproto/ClientMessage.event.hex +3 -0
@@ -0,0 +1,3 @@
1+# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request
2+# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace
3+a1201b0001004000e700000004030201080604020c090603100c0804140f0a05
@@ -0,0 +1,3 @@
1+# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request
2+# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace
3+a1201b0001004000e700000004030201080604020c090603100c0804140f0a05
A testdata/golden/xproto/ConfigureRequest.event.hex +3 -0
@@ -0,0 +1,3 @@
1+# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request
2+# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace
3+17001b009f0300000100400000000000fbff28008002c8000000070000000000
@@ -0,0 +1,3 @@
1+# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request
2+# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace
3+17001b009f0300000100400000000000fbff28008002c8000000070000000000
A testdata/golden/xproto/CreateNotify.event.hex +3 -0
A testdata/golden/xproto/DestroyNotify.event.hex +3 -0
A testdata/golden/xproto/GeGeneric.event.hex +5 -0
A testdata/golden/xproto/KeymapNotify.event.hex +3 -0
A testdata/golden/xproto/MapNotify.event.hex +3 -0
A testdata/golden/xproto/MapRequest.event.hex +3 -0
A testdata/golden/xproto/PropertyNotify.event.hex +3 -0
A testdata/golden/xproto/SelectionNotify.event.hex +3 -0
A testdata/golden/xproto/UnmapNotify.event.hex +3 -0
A testdata/golden/xvfb-roles.json +19 -0
A xerror.go +54 -0
A xerror_test.go +67 -0
A xfixes/errors_generated.go +17 -0
A xfixes/events_fuzz_test.go +40 -0
A xfixes/events_generated.go +57 -0
A xproto/errors_generated.go +33 -0
A xproto/events_fuzz_test.go +1189 -0
A xproto/events_generated.go +1257 -0

Comments0 open / 0

no comments yet.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 1

Tags 1

↑↓ moveenter openesc close