1//go:build xserver
2
3package xtest
4
5import (
6 "bytes"
7 "encoding/binary"
8 "encoding/hex"
9 "encoding/json"
10 "flag"
11 "fmt"
12 "io"
13 "maps"
14 "net"
15 "os"
16 "os/exec"
17 "path/filepath"
18 "slices"
19 "strconv"
20 "strings"
21 "testing"
22 "time"
23
24 "golang.org/x/sys/unix"
25)
26
27var update = flag.Bool("update", false, "rewrite wire goldens from Xvfb instead of comparing against them")
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.
31type 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.
43var 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.
78var structural = map[string]func(p []byte, roles map[string]uint32) error{
79 "xproto/ListExtensions.reply": checkListExtensions,
80 "render/QueryPictFormats.reply": checkQueryPictFormats,
81}
82
83const 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.
87func 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.
95func TestCaptureRequests(t *testing.T) {
96 if _, err := exec.LookPath("Xvfb"); err != nil {
97 t.Skip("Xvfb not installed")
98 }
99 // displayfd avoids display-number races and tells us when Xvfb is ready.
100 r, w, err := os.Pipe()
101 if err != nil {
102 t.Fatal(err)
103 }
104 // Xvfb writes to displayfd again after a server reset; keep the read end
105 // open until the test ends so that write cannot kill the server.
106 t.Cleanup(func() { _ = r.Close() })
107 cmd := exec.Command("Xvfb", xvfbArgs()...)
108 cmd.ExtraFiles = []*os.File{w}
109 var log bytes.Buffer
110 cmd.Stderr = &log
111 if err = cmd.Start(); err != nil {
112 w.Close()
113 t.Fatal(err)
114 }
115 w.Close()
116 defer func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }()
117 _ = r.SetReadDeadline(time.Now().Add(5 * time.Second))
118 var display [32]byte
119 n, err := r.Read(display[:])
120 if err != nil {
121 t.Fatalf("Xvfb: %v: %s", err, log.String())
122 }
123 num, err := strconv.Atoi(strings.TrimSpace(string(display[:n])))
124 if err != nil {
125 t.Fatal(err)
126 }
127 server, err := net.DialTimeout("unix", fmt.Sprintf("/tmp/.X11-unix/X%d", num), 5*time.Second)
128 if err != nil {
129 t.Fatal(err)
130 }
131 defer server.Close()
132 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
133 if err != nil {
134 t.Fatal(err)
135 }
136 a := os.NewFile(uintptr(fds[0]), "capture-client")
137 b := os.NewFile(uintptr(fds[1]), "capture-tee")
138 client, err := net.FileConn(a)
139 a.Close()
140 if err != nil {
141 b.Close()
142 t.Fatal(err)
143 }
144 defer client.Close()
145 tee, err := net.FileConn(b)
146 b.Close()
147 if err != nil {
148 t.Fatal(err)
149 }
150 defer tee.Close()
151 _ = client.SetDeadline(time.Now().Add(5 * time.Second))
152 _ = server.SetDeadline(time.Now().Add(5 * time.Second))
153 var recorded bytes.Buffer
154 copied := make(chan error, 1)
155 go func() { _, err := io.Copy(server, io.TeeReader(tee, &recorded)); copied <- err }()
156 go func() { _, _ = io.Copy(tee, server) }()
157 write := func(p []byte) {
158 t.Helper()
159 if _, err := client.Write(p); err != nil {
160 t.Fatal(err)
161 }
162 }
163 read := func(p []byte) {
164 t.Helper()
165 if _, err := io.ReadFull(client, p); err != nil {
166 t.Fatal(err)
167 }
168 }
169 // Independent reference encoder: no generated bindings participate in capture.
170 write([]byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0})
171 var setupHead [8]byte
172 read(setupHead[:])
173 if setupHead[0] != 1 {
174 t.Fatalf("setup failed: %x", setupHead)
175 }
176 setup := make([]byte, int(binary.LittleEndian.Uint16(setupHead[6:]))*4)
177 read(setup)
178 vendorLen := int(binary.LittleEndian.Uint16(setup[16:]))
179 formats := int(setup[21])
180 screenOff := 32 + (vendorLen+3)&^3 + 8*formats
181 root := binary.LittleEndian.Uint32(setup[screenOff:])
182 // Query XFIXES dynamically rather than assuming a server-specific major opcode.
183 query := []byte{98, 0, 4, 0, 6, 0, 0, 0, 'X', 'F', 'I', 'X', 'E', 'S', 0, 0}
184 write(query)
185 // Server bytes reach the client through the same tee; keep named replies as
186 // reply goldens (reply byte 0 through 32+4*length).
187 replies := map[string][]byte{}
188 keep := func(name string, p []byte) []byte {
189 t.Helper()
190 if p[0] != 1 {
191 t.Fatalf("%s: expected a reply, got %x", name, p)
192 }
193 replies[name] = p
194 return p
195 }
196 packet := func() []byte {
197 t.Helper()
198 head := make([]byte, 32)
199 read(head)
200 if head[0] == 1 {
201 tail := make([]byte, int(binary.LittleEndian.Uint32(head[4:]))*4)
202 read(tail)
203 head = append(head, tail...)
204 }
205 return head
206 }
207 ext := keep("xproto/QueryExtension", packet())
208 if ext[0] != 1 || ext[8] != 1 {
209 t.Fatalf("XFIXES unavailable: %x", ext)
210 }
211 major := ext[9]
212 version := make([]byte, 12)
213 version[0] = major
214 version[2] = 3
215 binary.LittleEndian.PutUint32(version[4:], 6)
216 write(version)
217 reply := keep("xfixes/QueryVersion", packet())
218 if reply[0] != 1 {
219 t.Fatalf("QueryVersion failed: %x", reply)
220 }
221 mapReq := make([]byte, 8)
222 mapReq[0] = 8
223 mapReq[2] = 2
224 binary.LittleEndian.PutUint32(mapReq[4:], root)
225 write(mapReq)
226 geometry := append([]byte(nil), mapReq...)
227 geometry[0] = 14
228 write(geometry)
229 geom := keep("xproto/GetGeometry", packet())
230 if geom[0] != 1 || binary.LittleEndian.Uint16(geom[2:]) != 4 {
231 t.Fatalf("MapWindow/GetGeometry rejected: %x", geom)
232 }
233
234 // InternAtom, ChangeProperty and XRes QueryClientIds exercise derived lengths,
235 // byte expressions and fixed-struct lists with independently assembled bytes.
236 intern := []byte{16, 0, 4, 0, 7, 0, 0, 0, 'X', 'T', 'U', 'R', 'B', 'O', '_', 0}
237 write(intern)
238 atomReply := keep("xproto/InternAtom", packet())
239 if atomReply[0] != 1 {
240 t.Fatalf("InternAtom: %x", atomReply)
241 }
242 atom := binary.LittleEndian.Uint32(atomReply[8:])
243 property := make([]byte, 28)
244 property[0] = 18
245 property[2] = 7
246 binary.LittleEndian.PutUint32(property[4:], root)
247 binary.LittleEndian.PutUint32(property[8:], atom)
248 binary.LittleEndian.PutUint32(property[12:], 31)
249 property[16] = 8
250 binary.LittleEndian.PutUint32(property[20:], 3)
251 copy(property[24:], "abc")
252 write(property)
253 // A successful GetProperty reply proves the preceding write was accepted.
254 get := make([]byte, 24)
255 get[0] = 20
256 get[2] = 6
257 binary.LittleEndian.PutUint32(get[4:], root)
258 binary.LittleEndian.PutUint32(get[8:], atom)
259 binary.LittleEndian.PutUint32(get[20:], 1)
260 write(get)
261 got := keep("xproto/GetProperty", packet())
262 if got[0] != 1 || string(got[32:35]) != "abc" {
263 t.Fatalf("ChangeProperty: %x", got)
264 }
265 resName := "X-Resource"
266 queryRes := make([]byte, 20)
267 queryRes[0] = 98
268 queryRes[2] = 5
269 queryRes[4] = byte(len(resName))
270 copy(queryRes[8:], resName)
271 write(queryRes)
272 resExt := packet()
273 if resExt[0] != 1 || resExt[8] != 1 {
274 t.Fatalf("XRes unavailable: %x", resExt)
275 }
276 resMajor := resExt[9]
277 resVersion := []byte{resMajor, 0, 2, 0, 1, 2, 0, 0}
278 write(resVersion)
279 if p := packet(); p[0] != 1 {
280 t.Fatalf("XRes version: %x", p)
281 }
282 ids := make([]byte, 16)
283 ids[0] = resMajor
284 ids[1] = 4
285 ids[2] = 4
286 binary.LittleEndian.PutUint32(ids[4:], 1)
287 binary.LittleEndian.PutUint32(ids[12:], 1)
288 write(ids)
289 keep("res/QueryClientIds", packet())
290 // Value-list reference packets are assembled independently, including mask-bit
291 // ordering. Two sibling windows allow all seven ConfigureWindow bits to be
292 // exercised legally. A final reply fences all void requests and exposes errors.
293 base := binary.LittleEndian.Uint32(setup[4:])
294 child, sibling, picture := base|1, base|2, base|3
295 create := func(id uint32, mask uint32, values ...uint32) {
296 p := make([]byte, 32+4*len(values))
297 p[0] = 1
298 binary.LittleEndian.PutUint16(p[2:], uint16(len(p)/4))
299 binary.LittleEndian.PutUint32(p[4:], id)
300 binary.LittleEndian.PutUint32(p[8:], root)
301 binary.LittleEndian.PutUint16(p[16:], 64)
302 binary.LittleEndian.PutUint16(p[18:], 48)
303 binary.LittleEndian.PutUint16(p[22:], 1) // InputOutput, inherited depth/visual
304 binary.LittleEndian.PutUint32(p[28:], mask)
305 for i, v := range values {
306 binary.LittleEndian.PutUint32(p[32+4*i:], v)
307 }
308 write(p)
309 }
310 create(sibling, 0)
311 create(child, 2|512|1024, 0x123456, 1, 0)
312 change := make([]byte, 20)
313 change[0], change[2] = 2, 5
314 binary.LittleEndian.PutUint32(change[4:], child)
315 binary.LittleEndian.PutUint32(change[8:], 512|1024)
316 binary.LittleEndian.PutUint32(change[16:], 1) // false, true BOOL32 words
317 write(change)
318 configure := func(mask uint16, values ...uint32) {
319 p := make([]byte, 12+4*len(values))
320 p[0] = 12
321 binary.LittleEndian.PutUint16(p[2:], uint16(len(p)/4))
322 binary.LittleEndian.PutUint32(p[4:], child)
323 binary.LittleEndian.PutUint16(p[8:], mask)
324 for i, v := range values {
325 binary.LittleEndian.PutUint32(p[12+4*i:], v)
326 }
327 write(p)
328 }
329 configure(0)
330 configure(1|4|64, 0xfffffff9, 80, 0)
331 configure(127, 0xfffffffd, 9, 96, 72, 2, sibling, 0)
332 queryRender := []byte{98, 0, 4, 0, 6, 0, 0, 0, 'R', 'E', 'N', 'D', 'E', 'R', 0, 0}
333 write(queryRender)
334 renderExt := packet()
335 if renderExt[0] != 1 || renderExt[8] != 1 {
336 t.Fatalf("value requests/RENDER query: %x", renderExt)
337 }
338 renderMajor := renderExt[9]
339 write([]byte{renderMajor, 1, 1, 0}) // QueryPictFormats
340 formatsReply := keep("render/QueryPictFormats", packet())
341 if formatsReply[0] != 1 {
342 t.Fatalf("QueryPictFormats: %x", formatsReply)
343 }
344 // Match the root visual rather than guessing a server-assigned format ID.
345 rootVisual := binary.LittleEndian.Uint32(setup[screenOff+32:])
346 off := 32 + 28*int(binary.LittleEndian.Uint32(formatsReply[8:]))
347 var format uint32
348 for range binary.LittleEndian.Uint32(formatsReply[12:]) {
349 depths := binary.LittleEndian.Uint32(formatsReply[off:])
350 off += 8
351 for range depths {
352 visuals := binary.LittleEndian.Uint16(formatsReply[off+2:])
353 off += 8
354 for range visuals {
355 if binary.LittleEndian.Uint32(formatsReply[off:]) == rootVisual {
356 format = binary.LittleEndian.Uint32(formatsReply[off+4:])
357 }
358 off += 8
359 }
360 }
361 }
362 if format == 0 {
363 t.Fatal("root visual has no Render format")
364 }
365 pic := make([]byte, 32)
366 pic[0], pic[1], pic[2] = renderMajor, 4, 8
367 binary.LittleEndian.PutUint32(pic[4:], picture)
368 binary.LittleEndian.PutUint32(pic[8:], child)
369 binary.LittleEndian.PutUint32(pic[12:], format)
370 binary.LittleEndian.PutUint32(pic[16:], 1|16|32)
371 binary.LittleEndian.PutUint32(pic[20:], 1) // RepeatNormal
372 binary.LittleEndian.PutUint32(pic[24:], 0xfffffffb)
373 binary.LittleEndian.PutUint32(pic[28:], 7)
374 write(pic)
375 write([]byte{43, 0, 1, 0}) // GetInputFocus fences CreatePicture
376 if p := packet(); p[0] != 1 {
377 t.Fatalf("CreatePicture rejected: %x", p)
378 }
379 // Reply-only captures. Opcodes 3, 15, 20 and 99 are not request goldens, so
380 // the request-stream classifier below is unaffected.
381 attrs := make([]byte, 8)
382 attrs[0], attrs[2] = 3, 2
383 binary.LittleEndian.PutUint32(attrs[4:], child)
384 write(attrs)
385 keep("xproto/GetWindowAttributes", packet())
386 tree := make([]byte, 8)
387 tree[0], tree[2] = 15, 2
388 binary.LittleEndian.PutUint32(tree[4:], root)
389 write(tree)
390 keep("xproto/QueryTree", packet())
391 // WM_NAME is unset on a fresh Xvfb root: a zero-length GetProperty reply.
392 empty := make([]byte, 24)
393 empty[0], empty[2] = 20, 6
394 binary.LittleEndian.PutUint32(empty[4:], root)
395 binary.LittleEndian.PutUint32(empty[8:], 39)
396 binary.LittleEndian.PutUint32(empty[20:], 1)
397 write(empty)
398 keep("xproto/GetPropertyEmpty", packet())
399 write([]byte{99, 0, 1, 0}) // ListExtensions: variable STR iterator
400 keep("xproto/ListExtensions", packet())
401 // Event and error captures. The tee client acts as a window manager on the
402 // root; a second raw client (direct socket, not teed) triggers the events.
403 // Requests sent here on the tee use window/minor values the request-golden
404 // classifier below ignores.
405 xfixesEvent, xfixesError := ext[10], ext[11]
406 events := map[string][]byte{}
407 xerrors := map[string][]byte{}
408 // collect reads events and errors until the reply fencing them.
409 collect := func(names map[byte]string) {
410 t.Helper()
411 write([]byte{43, 0, 1, 0}) // GetInputFocus
412 for {
413 p := packet()
414 switch {
415 case p[0] == 1:
416 return
417 case p[0] == 0:
418 name := names[p[1]]
419 if name == "" {
420 t.Fatalf("unexpected error %x", p)
421 }
422 xerrors[name] = p
423 default:
424 name := names[p[0]]
425 if name == "" {
426 t.Fatalf("unexpected event %x", p)
427 }
428 events[name] = p
429 }
430 }
431 }
432 const (
433 maskSubstructureNotify = 1 << 19
434 maskSubstructureRedirect = 1 << 20
435 maskPropertyChange = 1 << 22
436 atomPrimary, atomSecondary, atomString = 1, 2, 31
437 )
438 selectRoot := make([]byte, 16)
439 selectRoot[0], selectRoot[2] = 2, 4
440 binary.LittleEndian.PutUint32(selectRoot[4:], root)
441 binary.LittleEndian.PutUint32(selectRoot[8:], 1<<11) // CWEventMask
442 binary.LittleEndian.PutUint32(selectRoot[12:], maskSubstructureNotify|maskSubstructureRedirect|maskPropertyChange)
443 write(selectRoot)
444 selectSel := make([]byte, 16)
445 selectSel[0], selectSel[1], selectSel[2] = major, 2, 4
446 binary.LittleEndian.PutUint32(selectSel[4:], root)
447 binary.LittleEndian.PutUint32(selectSel[8:], atomPrimary)
448 binary.LittleEndian.PutUint32(selectSel[12:], 1) // SetSelectionOwner
449 write(selectSel)
450 collect(nil)
451
452 other, err := net.DialTimeout("unix", fmt.Sprintf("/tmp/.X11-unix/X%d", num), 5*time.Second)
453 if err != nil {
454 t.Fatal(err)
455 }
456 defer other.Close()
457 _ = other.SetDeadline(time.Now().Add(5 * time.Second))
458 send := func(p []byte) {
459 t.Helper()
460 if _, err := other.Write(p); err != nil {
461 t.Fatal(err)
462 }
463 }
464 recv := func(p []byte) {
465 t.Helper()
466 if _, err := io.ReadFull(other, p); err != nil {
467 t.Fatal(err)
468 }
469 }
470 // sync round-trips the second client; any error fails the capture.
471 sync := func() {
472 t.Helper()
473 send([]byte{43, 0, 1, 0})
474 var p [32]byte
475 recv(p[:])
476 if p[0] != 1 {
477 t.Fatalf("second client: %x", p)
478 }
479 }
480 send([]byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0})
481 var otherHead [8]byte
482 recv(otherHead[:])
483 if otherHead[0] != 1 {
484 t.Fatalf("second setup failed: %x", otherHead)
485 }
486 otherSetup := make([]byte, int(binary.LittleEndian.Uint16(otherHead[6:]))*4)
487 recv(otherSetup)
488 client2 := binary.LittleEndian.Uint32(otherSetup[4:]) | 1
489 win := make([]byte, 32)
490 win[0], win[2] = 1, 8
491 binary.LittleEndian.PutUint32(win[4:], client2)
492 binary.LittleEndian.PutUint32(win[8:], root)
493 binary.LittleEndian.PutUint16(win[12:], 10)
494 binary.LittleEndian.PutUint16(win[14:], 20)
495 binary.LittleEndian.PutUint16(win[16:], 300)
496 binary.LittleEndian.PutUint16(win[18:], 200)
497 binary.LittleEndian.PutUint16(win[22:], 1)
498 send(win) // CreateNotify
499 mapOther := make([]byte, 8)
500 mapOther[0], mapOther[2] = 8, 2
501 binary.LittleEndian.PutUint32(mapOther[4:], client2)
502 send(mapOther) // MapRequest (redirected)
503 cfg := make([]byte, 24)
504 cfg[0], cfg[2] = 12, 6
505 binary.LittleEndian.PutUint32(cfg[4:], client2)
506 binary.LittleEndian.PutUint16(cfg[8:], 1|2|4) // x, y, width
507 binary.LittleEndian.PutUint32(cfg[12:], 0xfffffffb)
508 binary.LittleEndian.PutUint32(cfg[16:], 40)
509 binary.LittleEndian.PutUint32(cfg[20:], 640)
510 send(cfg) // ConfigureRequest (redirected)
511 prop := make([]byte, 28)
512 prop[0], prop[2] = 18, 7
513 binary.LittleEndian.PutUint32(prop[4:], root)
514 binary.LittleEndian.PutUint32(prop[8:], atom)
515 binary.LittleEndian.PutUint32(prop[12:], atomString)
516 prop[16] = 8
517 binary.LittleEndian.PutUint32(prop[20:], 2)
518 copy(prop[24:], "hi")
519 send(prop) // PropertyNotify (NewValue)
520 msg := make([]byte, 44)
521 msg[0], msg[2] = 25, 11
522 binary.LittleEndian.PutUint32(msg[4:], root)
523 binary.LittleEndian.PutUint32(msg[8:], maskSubstructureNotify|maskSubstructureRedirect)
524 msg[12], msg[13] = 33, 32 // ClientMessage, format 32
525 binary.LittleEndian.PutUint32(msg[16:], client2)
526 binary.LittleEndian.PutUint32(msg[20:], atom)
527 for i := range 5 {
528 binary.LittleEndian.PutUint32(msg[24+4*i:], uint32(0x01020304*(i+1)))
529 }
530 send(msg) // ClientMessage (SendEvent bit set)
531 owner := make([]byte, 16)
532 owner[0], owner[2] = 22, 4
533 binary.LittleEndian.PutUint32(owner[4:], client2)
534 binary.LittleEndian.PutUint32(owner[8:], atomPrimary)
535 send(owner) // xfixes SelectionNotify (SetSelectionOwner)
536 sync()
537 collect(map[byte]string{16: "xproto/CreateNotify", 20: "xproto/MapRequest", 23: "xproto/ConfigureRequest",
538 28: "xproto/PropertyNotify", 0x80 | 33: "xproto/ClientMessage", xfixesEvent: "xfixes/SelectionNotify"})
539 // The window manager maps the window itself (MapNotify); the second client
540 // then unmaps and destroys it.
541 mapByWM := make([]byte, 8)
542 mapByWM[0], mapByWM[2] = 8, 2
543 binary.LittleEndian.PutUint32(mapByWM[4:], client2)
544 write(mapByWM)
545 collect(map[byte]string{19: "xproto/MapNotify"})
546 unmap := []byte{10, 0, 2, 0, 0, 0, 0, 0}
547 binary.LittleEndian.PutUint32(unmap[4:], client2)
548 send(unmap)
549 destroy := []byte{4, 0, 2, 0, 0, 0, 0, 0}
550 binary.LittleEndian.PutUint32(destroy[4:], client2)
551 send(destroy)
552 sync()
553 collect(map[byte]string{18: "xproto/UnmapNotify", 17: "xproto/DestroyNotify"})
554 // Core SelectionNotify: SECONDARY has no owner, so the server replies itself.
555 convert := make([]byte, 24)
556 convert[0], convert[2] = 24, 6
557 binary.LittleEndian.PutUint32(convert[4:], child)
558 binary.LittleEndian.PutUint32(convert[8:], atomSecondary)
559 binary.LittleEndian.PutUint32(convert[12:], atomString)
560 binary.LittleEndian.PutUint32(convert[16:], atom)
561 write(convert)
562 collect(map[byte]string{31: "xproto/SelectionNotify"})
563 // Errors: BadAtom, BadWindow and an extension error (XFIXES BadRegion).
564 badAtom := []byte{17, 0, 2, 0, 0xef, 0xbe, 0xad, 0x0d}
565 write(badAtom)
566 badWindow := []byte{3, 0, 2, 0, 0, 0, 0, 0}
567 binary.LittleEndian.PutUint32(badWindow[4:], base|0x7777)
568 write(badWindow)
569 badRegion := []byte{major, 10, 2, 0, 0, 0, 0, 0}
570 binary.LittleEndian.PutUint32(badRegion[4:], base|0x8888)
571 write(badRegion)
572 collect(map[byte]string{5: "xproto/BadAtom", 3: "xproto/BadWindow", xfixesError: "xfixes/BadRegion"})
573 if len(events) != 10 || len(xerrors) != 3 {
574 t.Fatalf("captured %d events, %d errors", len(events), len(xerrors))
575 }
576 // Closing the client drains the request direction; wait before inspecting tee.
577 client.Close()
578 select {
579 case err := <-copied:
580 if err != nil {
581 t.Fatal(err)
582 }
583 case <-time.After(5 * time.Second):
584 t.Fatal("tee did not drain")
585 }
586 stream := recorded.Bytes()
587 requests := stream[12:]
588 captured := map[string][]byte{}
589 configIndex := 0
590 for len(requests) > 0 {
591 size := int(binary.LittleEndian.Uint16(requests[2:])) * 4
592 if size < 4 || size > len(requests) {
593 t.Fatal("malformed capture")
594 }
595 p := requests[:size]
596 switch p[0] {
597 case 1:
598 if binary.LittleEndian.Uint32(p[4:]) == child {
599 captured["xproto/CreateWindow"] = p
600 }
601 case 2:
602 if binary.LittleEndian.Uint32(p[4:]) == child {
603 captured["xproto/ChangeWindowAttributes"] = p
604 }
605 case 12:
606 names := []string{"ConfigureWindow", "ConfigureWindowSparse", "ConfigureWindowAll"}
607 captured["xproto/"+names[configIndex]] = p
608 configIndex++
609 case 8:
610 if binary.LittleEndian.Uint32(p[4:]) == root {
611 captured["xproto/MapWindow"] = p
612 }
613 case 14:
614 captured["xproto/GetGeometry"] = p
615 case 16:
616 captured["xproto/InternAtom"] = p
617 case 18:
618 captured["xproto/ChangeProperty"] = p
619 default:
620 if p[0] == renderMajor && p[1] == 4 {
621 captured["render/CreatePicture"] = p
622 }
623 if p[0] == resMajor && p[1] == 4 {
624 captured["res/QueryClientIds"] = p
625 }
626 if p[0] == major && p[1] == 0 {
627 captured["xfixes/QueryVersion"] = p
628 }
629 }
630 requests = requests[size:]
631 }
632 if len(captured) != 12 || len(replies) != 11 {
633 t.Fatal("missing captured requests or replies")
634 }
635 out := map[string][]byte{}
636 for name, p := range replies {
637 out[name+".reply"] = p
638 }
639 for name, p := range captured {
640 out[name+".req"] = p
641 }
642 for name, p := range events {
643 out[name+".event"] = p
644 }
645 for name, p := range xerrors {
646 out[name+".error"] = p
647 }
648 // Server-chosen values, keyed by the roles used in serverFields.
649 roles := map[string]uint32{
650 "root": root, "atom": atom, "base": base, "child": child, "sibling": sibling, "picture": picture,
651 "client2": client2, "badWindow": base | 0x7777, "badRegion": base | 0x8888,
652 "rootVisual": rootVisual, "rootColormap": binary.LittleEndian.Uint32(setup[screenOff+4:]), "rootFormat": format,
653 "xfixesMajor": uint32(major), "xfixesEvent": uint32(xfixesEvent), "xfixesError": uint32(xfixesError),
654 "resMajor": uint32(resMajor), "renderMajor": uint32(renderMajor),
655 }
656 if *update {
657 for name, p := range out {
658 writeGolden(t, name, p)
659 }
660 rolesJSON, err := json.MarshalIndent(roles, "", " ")
661 if err != nil {
662 t.Fatal(err)
663 }
664 if err := os.WriteFile(rolesPath, append(rolesJSON, '\n'), 0644); err != nil {
665 t.Fatal(err)
666 }
667 return
668 }
669 rolesJSON, err := os.ReadFile(rolesPath)
670 if err != nil {
671 t.Fatal(err)
672 }
673 var capturedRoles map[string]uint32
674 if err := json.Unmarshal(rolesJSON, &capturedRoles); err != nil {
675 t.Fatal(err)
676 }
677 for _, role := range slices.Sorted(maps.Keys(roles)) {
678 if roles[role] != capturedRoles[role] {
679 t.Logf("server-chosen %s: live %#x, golden %#x", role, roles[role], capturedRoles[role])
680 }
681 }
682 for name, p := range out {
683 compareGolden(t, name, p, roles, capturedRoles)
684 }
685}
686
687var sources = map[string]string{
688 "req": "# source: Xvfb via raw socketpair tee; independent reference encoder; server accepted\n",
689 "reply": "# source: Xvfb reply via raw socketpair tee to an independent reference request\n",
690 "event": "# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request\n",
691 "error": "# source: Xvfb error via raw socketpair tee to an independent reference request\n",
692}
693
694func writeGolden(t *testing.T, name string, p []byte) {
695 t.Helper()
696 path := filepath.Join("../../testdata/golden", name+".hex")
697 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
698 t.Fatal(err)
699 }
700 source := sources[name[strings.LastIndexByte(name, '.')+1:]]
701 text := source + "# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n"
702 if err := os.WriteFile(path, []byte(text), 0644); err != nil {
703 t.Fatal(err)
704 }
705}
706
707// compareGolden checks a live packet against the committed golden without
708// depending on the server build: server-chosen fields must hold each side's
709// own value (live roles, recorded roles) and are masked before an exact
710// comparison; structural replies are checked for invariants only.
711func compareGolden(t *testing.T, name string, live []byte, roles, recorded map[string]uint32) {
712 t.Helper()
713 want, err := LoadHex(filepath.Join("../../testdata/golden", name+".hex"))
714 if err != nil {
715 t.Errorf("%s: %v (run with -update to create it)", name, err)
716 return
717 }
718 if check := structural[name]; check != nil {
719 if err := check(live, roles); err != nil {
720 t.Errorf("%s (live): %v", name, err)
721 }
722 if err := check(want, recorded); err != nil {
723 t.Errorf("%s (golden): %v", name, err)
724 }
725 return
726 }
727 got, exp, err := normalize(name, live, roles), normalize(name, want, recorded), error(nil)
728 for _, e := range []struct {
729 side string
730 err error
731 }{{"live", got.err}, {"golden", exp.err}} {
732 if e.err != nil {
733 t.Errorf("%s (%s): %v", name, e.side, e.err)
734 err = e.err
735 }
736 }
737 if err == nil && !bytes.Equal(got.b, exp.b) {
738 t.Errorf("%s differs from live Xvfb after masking server-chosen fields:\n got %x\nwant %x", name, live, want)
739 }
740}
741
742type normalized struct {
743 b []byte
744 err error
745}
746
747// normalize verifies each server-chosen field of p holds roles[Role] and
748// replaces it with zero bytes.
749func normalize(name string, p []byte, roles map[string]uint32) normalized {
750 b := bytes.Clone(p)
751 for _, f := range serverFields[name] {
752 if f.Off+f.Size > len(b) {
753 return normalized{err: fmt.Errorf("field %s at %d beyond %d bytes", f.Role, f.Off, len(b))}
754 }
755 if f.Role != "time" {
756 v, ok := roles[f.Role]
757 if !ok {
758 return normalized{err: fmt.Errorf("no recorded value for role %s", f.Role)}
759 }
760 var got uint32
761 if f.Size == 1 {
762 got = uint32(b[f.Off])
763 } else {
764 got = binary.LittleEndian.Uint32(b[f.Off:])
765 }
766 if got != v {
767 return normalized{err: fmt.Errorf("field %s at %d = %#x, want %#x", f.Role, f.Off, got, v)}
768 }
769 }
770 clear(b[f.Off : f.Off+f.Size])
771 }
772 return normalized{b: b}
773}
774
775// checkListExtensions: a framed reply whose STR list holds exactly names_len
776// entries filling the reply, including the extensions xturbo binds.
777func checkListExtensions(p []byte, _ map[string]uint32) error {
778 if len(p) < 32 || p[0] != 1 || len(p) != 32+4*int(binary.LittleEndian.Uint32(p[4:])) {
779 return fmt.Errorf("bad framing")
780 }
781 names := map[string]bool{}
782 off := 32
783 for range int(p[1]) {
784 if off >= len(p) || off+1+int(p[off]) > len(p) {
785 return fmt.Errorf("STR list overruns the reply")
786 }
787 names[string(p[off+1:off+1+int(p[off])])] = true
788 off += 1 + int(p[off])
789 }
790 if (off+3)&^3 != len(p) {
791 return fmt.Errorf("STR list ends at %d, reply has %d bytes", off, len(p))
792 }
793 for _, want := range []string{"BIG-REQUESTS", "XC-MISC", "XFIXES", "RENDER", "SHAPE", "Composite", "X-Resource", "Generic Event Extension"} {
794 if !names[want] {
795 return fmt.Errorf("missing extension %q", want)
796 }
797 }
798 return nil
799}
800
801// checkQueryPictFormats: declared counts match the list sizes exactly and the
802// root visual maps to the picture format CreatePicture used.
803func checkQueryPictFormats(p []byte, roles map[string]uint32) error {
804 u32 := binary.LittleEndian.Uint32
805 if len(p) < 32 || p[0] != 1 || len(p) != 32+4*int(u32(p[4:])) {
806 return fmt.Errorf("bad framing")
807 }
808 formats, screens, subpixels := int(u32(p[8:])), int(u32(p[12:])), int(u32(p[24:]))
809 off := 32 + 28*formats
810 found := false
811 for range screens {
812 if off+8 > len(p) {
813 return fmt.Errorf("screens overrun")
814 }
815 depths := int(u32(p[off:]))
816 off += 8
817 for range depths {
818 if off+8 > len(p) {
819 return fmt.Errorf("depths overrun")
820 }
821 visuals := int(binary.LittleEndian.Uint16(p[off+2:]))
822 off += 8
823 for range visuals {
824 if off+8 > len(p) {
825 return fmt.Errorf("visuals overrun")
826 }
827 if u32(p[off:]) == roles["rootVisual"] && u32(p[off+4:]) == roles["rootFormat"] {
828 found = true
829 }
830 off += 8
831 }
832 }
833 }
834 off += 4 * subpixels
835 if off != len(p) {
836 return fmt.Errorf("lists end at %d, reply has %d bytes", off, len(p))
837 }
838 if !found {
839 return fmt.Errorf("root visual %#x does not map to format %#x", roles["rootVisual"], roles["rootFormat"])
840 }
841 return nil
842}