detached 36fb296

Merge 5f9aee7852c50d4df9eaddf46f0ffa36d8527d47 into 722264ac773fc5df4f73671ee4fcce79537e93a6

commit
36fb296998ba5f8477c5468dbaff298975fbad4f
author
ermite · · 2026-10-22 09:32 UTC
merge of
722264a + 5f9aee7 · diff against the first parent
signature
unsigned
10 files · +1246 -1
A conn.go +100 -0
@@ -0,0 +1,100 @@
1+package xturbo
2+
3+import (
4+ "os"
5+
6+ "golang.org/x/sys/unix"
7+)
8+
9+// Conn is a client connection to an X server over a stream socket. It is not
10+// safe for concurrent use: one goroutine owns it.
11+type Conn struct {
12+ fd int
13+ buf *Buf
14+ ext ExtTable
15+ setup []byte
16+ root uint32
17+
18+ xidBase, xidMask uint32 // resource-id allocation, used by NewXID
19+}
20+
21+// NewConn takes ownership of fd, a connected stream socket to an X server,
22+// and runs the connection setup without authorization (e.g. the -wm fd of an
23+// XWayland the caller spawned). The handshake blocks; afterwards fd is
24+// switched to O_NONBLOCK. On failure fd is closed.
25+func NewConn(fd int) (*Conn, error) {
26+ return newConn(fd, "", nil)
27+}
28+
29+func newConn(fd int, authName string, authData []byte) (*Conn, error) {
30+ c, err := setupConn(fd, authName, authData)
31+ if err != nil {
32+ _ = unix.Close(fd)
33+ return nil, err
34+ }
35+ return c, nil
36+}
37+
38+func setupConn(fd int, authName string, authData []byte) (*Conn, error) {
39+ if err := unix.SetNonblock(fd, false); err != nil {
40+ return nil, os.NewSyscallError("fcntl", err)
41+ }
42+ setup, info, err := handshake(fd, authName, authData)
43+ if err != nil {
44+ return nil, err
45+ }
46+ if err := unix.SetNonblock(fd, true); err != nil {
47+ return nil, os.NewSyscallError("fcntl", err)
48+ }
49+ c := &Conn{
50+ fd: fd,
51+ buf: NewBuf(0),
52+ setup: setup,
53+ root: info.root,
54+ xidBase: info.xidBase,
55+ xidMask: info.xidMask,
56+ }
57+ c.buf.Ext = &c.ext
58+ return c, nil
59+}
60+
61+// Close closes the socket. Later calls, and Flush after Close, return
62+// ErrClosed.
63+func (c *Conn) Close() error {
64+ if c.fd < 0 {
65+ return ErrClosed
66+ }
67+ fd := c.fd
68+ c.fd = -1
69+ c.buf.Fail(ErrClosed)
70+ if err := unix.Close(fd); err != nil {
71+ return os.NewSyscallError("close", err)
72+ }
73+ return nil
74+}
75+
76+// Fd returns the socket, for the caller's poll loop; -1 after Close.
77+func (c *Conn) Fd() int { return c.fd }
78+
79+// Buf returns the request encoder bound to this connection.
80+func (c *Conn) Buf() *Buf { return c.buf }
81+
82+// Ext returns the connection's extension table.
83+func (c *Conn) Ext() *ExtTable { return &c.ext }
84+
85+// SetupBytes returns the full setup reply. It is retained for the life of the
86+// Conn; parse it with the xproto Setup view. Do not modify it.
87+func (c *Conn) SetupBytes() []byte { return c.setup }
88+
89+// Root returns the root window of the first screen.
90+func (c *Conn) Root() uint32 { return c.root }
91+
92+// Flush writes queued requests without blocking. It returns ErrWouldBlock when
93+// the socket is full (arm writable interest and call again) and the sticky
94+// Buf error once the connection is unusable.
95+func (c *Conn) Flush() error {
96+ if c.fd < 0 {
97+ return ErrClosed
98+ }
99+ return c.buf.flush(c.fd)
100+}
@@ -0,0 +1,100 @@
1+package xturbo
2+
3+import (
4+ "os"
5+
6+ "golang.org/x/sys/unix"
7+)
8+
9+// Conn is a client connection to an X server over a stream socket. It is not
10+// safe for concurrent use: one goroutine owns it.
11+type Conn struct {
12+ fd int
13+ buf *Buf
14+ ext ExtTable
15+ setup []byte
16+ root uint32
17+
18+ xidBase, xidMask uint32 // resource-id allocation, used by NewXID
19+}
20+
21+// NewConn takes ownership of fd, a connected stream socket to an X server,
22+// and runs the connection setup without authorization (e.g. the -wm fd of an
23+// XWayland the caller spawned). The handshake blocks; afterwards fd is
24+// switched to O_NONBLOCK. On failure fd is closed.
25+func NewConn(fd int) (*Conn, error) {
26+ return newConn(fd, "", nil)
27+}
28+
29+func newConn(fd int, authName string, authData []byte) (*Conn, error) {
30+ c, err := setupConn(fd, authName, authData)
31+ if err != nil {
32+ _ = unix.Close(fd)
33+ return nil, err
34+ }
35+ return c, nil
36+}
37+
38+func setupConn(fd int, authName string, authData []byte) (*Conn, error) {
39+ if err := unix.SetNonblock(fd, false); err != nil {
40+ return nil, os.NewSyscallError("fcntl", err)
41+ }
42+ setup, info, err := handshake(fd, authName, authData)
43+ if err != nil {
44+ return nil, err
45+ }
46+ if err := unix.SetNonblock(fd, true); err != nil {
47+ return nil, os.NewSyscallError("fcntl", err)
48+ }
49+ c := &Conn{
50+ fd: fd,
51+ buf: NewBuf(0),
52+ setup: setup,
53+ root: info.root,
54+ xidBase: info.xidBase,
55+ xidMask: info.xidMask,
56+ }
57+ c.buf.Ext = &c.ext
58+ return c, nil
59+}
60+
61+// Close closes the socket. Later calls, and Flush after Close, return
62+// ErrClosed.
63+func (c *Conn) Close() error {
64+ if c.fd < 0 {
65+ return ErrClosed
66+ }
67+ fd := c.fd
68+ c.fd = -1
69+ c.buf.Fail(ErrClosed)
70+ if err := unix.Close(fd); err != nil {
71+ return os.NewSyscallError("close", err)
72+ }
73+ return nil
74+}
75+
76+// Fd returns the socket, for the caller's poll loop; -1 after Close.
77+func (c *Conn) Fd() int { return c.fd }
78+
79+// Buf returns the request encoder bound to this connection.
80+func (c *Conn) Buf() *Buf { return c.buf }
81+
82+// Ext returns the connection's extension table.
83+func (c *Conn) Ext() *ExtTable { return &c.ext }
84+
85+// SetupBytes returns the full setup reply. It is retained for the life of the
86+// Conn; parse it with the xproto Setup view. Do not modify it.
87+func (c *Conn) SetupBytes() []byte { return c.setup }
88+
89+// Root returns the root window of the first screen.
90+func (c *Conn) Root() uint32 { return c.root }
91+
92+// Flush writes queued requests without blocking. It returns ErrWouldBlock when
93+// the socket is full (arm writable interest and call again) and the sticky
94+// Buf error once the connection is unusable.
95+func (c *Conn) Flush() error {
96+ if c.fd < 0 {
97+ return ErrClosed
98+ }
99+ return c.buf.flush(c.fd)
100+}
A conn_test.go +272 -0
@@ -0,0 +1,272 @@
1+package xturbo
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "errors"
7+ "io"
8+ "os"
9+ "path/filepath"
10+ "testing"
11+
12+ "bnema.dev/xturbo/internal/xtest"
13+ "golang.org/x/sys/unix"
14+)
15+
16+func loadSetup(t testing.TB, name string) []byte {
17+ t.Helper()
18+ b, err := xtest.LoadHex(filepath.Join("testdata", "setup", name+".setup.hex"))
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ return b
23+}
24+
25+// scriptedServer runs the server side of a socketpair: it reads one setup
26+// request, reports it on the returned channel, writes reply in chunks of
27+// chunk bytes (0 = all at once) and, if hangup, closes its end.
28+func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client int, req <-chan []byte) {
29+ t.Helper()
30+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
31+ if err != nil {
32+ t.Fatal(err)
33+ }
34+ // Exercise NewConn switching a non-blocking fd back to blocking.
35+ if err := unix.SetNonblock(fds[0], true); err != nil {
36+ t.Fatal(err)
37+ }
38+ server := os.NewFile(uintptr(fds[1]), "server")
39+ ch := make(chan []byte, 1)
40+ done := make(chan struct{})
41+ t.Cleanup(func() { <-done; server.Close() })
42+ go func() {
43+ defer close(done)
44+ var hdr [12]byte
45+ if _, err := io.ReadFull(server, hdr[:]); err != nil {
46+ ch <- nil
47+ return
48+ }
49+ n := pad4(int(binary.LittleEndian.Uint16(hdr[6:]))) + pad4(int(binary.LittleEndian.Uint16(hdr[8:])))
50+ rest := make([]byte, n)
51+ if _, err := io.ReadFull(server, rest); err != nil {
52+ ch <- nil
53+ return
54+ }
55+ ch <- append(hdr[:], rest...)
56+ for p := reply; len(p) > 0; {
57+ k := len(p)
58+ if chunk > 0 {
59+ k = min(k, chunk)
60+ }
61+ if _, err := server.Write(p[:k]); err != nil {
62+ return
63+ }
64+ p = p[k:]
65+ }
66+ if hangup {
67+ server.Close()
68+ }
69+ }()
70+ return fds[0], ch
71+}
72+
73+func TestNewConnSuccess(t *testing.T) {
74+ setup := loadSetup(t, "Success")
75+ fd, req := scriptedServer(t, setup, 7, false)
76+ c, err := NewConn(fd)
77+ if err != nil {
78+ t.Fatal(err)
79+ }
80+ defer c.Close()
81+ got := <-req
82+ want := []byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0}
83+ if !bytes.Equal(got, want) {
84+ t.Fatalf("setup request %x, want %x", got, want)
85+ }
86+ if !bytes.Equal(c.SetupBytes(), setup) {
87+ t.Fatal("SetupBytes differs from the reply")
88+ }
89+ // Expected values are read from the fixture header comment
90+ // (Success.setup.hex), i.e. what that recorded Xvfb assigned.
91+ if c.Root() != 0x39f || c.xidBase != 0x00200000 || c.xidMask != 0x001fffff {
92+ t.Fatalf("root %#x base %#x mask %#x", c.Root(), c.xidBase, c.xidMask)
93+ }
94+ if c.Fd() != fd || c.Buf().Ext != c.Ext() || c.Buf().Next() != 1 {
95+ t.Fatal("accessors")
96+ }
97+ if c.Ext().Major(ExtXFixes) != 0 || !c.Ext().Info(ExtCore).Present {
98+ t.Fatal("ExtTable not empty")
99+ }
100+ flags, err := unix.FcntlInt(uintptr(fd), unix.F_GETFL, 0)
101+ if err != nil || flags&unix.O_NONBLOCK == 0 {
102+ t.Fatalf("fd not O_NONBLOCK after setup: %v", err)
103+ }
104+}
105+
106+func TestNewConnAuthRequest(t *testing.T) {
107+ fd, req := scriptedServer(t, loadSetup(t, "Success"), 0, false)
108+ cookie := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
109+ c, err := newConn(fd, authMagicCookie, cookie)
110+ if err != nil {
111+ t.Fatal(err)
112+ }
113+ defer c.Close()
114+ want := append([]byte{'l', 0, 11, 0, 0, 0, 18, 0, 16, 0, 0, 0}, authMagicCookie+"\x00\x00"...)
115+ want = append(want, cookie...)
116+ if got := <-req; !bytes.Equal(got, want) {
117+ t.Fatalf("setup request %x, want %x", got, want)
118+ }
119+}
120+
121+func TestNewConnRefused(t *testing.T) {
122+ authenticate := []byte{2, 0, 0, 0, 0, 0, 2, 0, 'n', 'o', 'p', 'e', '!', 0, 0, 0}
123+ for _, tc := range []struct {
124+ name string
125+ reply []byte
126+ status uint8
127+ reason string
128+ }{
129+ {"failed", loadSetup(t, "Failed"), 0, "Authorization required, but no authorization protocol specified\n"},
130+ {"authenticate", authenticate, 2, "nope!"},
131+ } {
132+ t.Run(tc.name, func(t *testing.T) {
133+ fd, _ := scriptedServer(t, tc.reply, 5, true)
134+ c, err := NewConn(fd)
135+ var se *SetupError
136+ if c != nil || !errors.As(err, &se) || !errors.Is(err, ErrSetupFailed) {
137+ t.Fatalf("conn %t, err %v", c != nil, err)
138+ }
139+ if se.Status != tc.status || se.Reason != tc.reason {
140+ t.Fatalf("status %d reason %q", se.Status, se.Reason)
141+ }
142+ if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err != unix.EBADF {
143+ t.Fatalf("fd not closed on failure: %v", err)
144+ }
145+ })
146+ }
147+}
148+
149+func TestNewConnMalformed(t *testing.T) {
150+ setup := loadSetup(t, "Success")
151+ noScreens := bytes.Clone(setup)
152+ noScreens[28] = 0
153+ badMask := bytes.Clone(setup)
154+ binary.LittleEndian.PutUint32(badMask[16:], 0)
155+ badVersion := bytes.Clone(setup)
156+ badVersion[2] = 10
157+ badStatus := bytes.Clone(setup)
158+ badStatus[0] = 3
159+ // Fixed Setup only (length 8 words): one root declared, no SCREEN bytes.
160+ screenPast := bytes.Clone(setup[:40])
161+ binary.LittleEndian.PutUint16(screenPast[6:], 8)
162+ binary.LittleEndian.PutUint16(screenPast[24:], 0) // vendor_len
163+ screenPast[29] = 0 // pixmap_formats_len
164+ // Walk the recorded reply to find the first screen and its first depth
165+ // that carries visuals; every extent is a multiple of 4 bytes.
166+ screen := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(setup[24:]))) + formatSize*int(setup[29])
167+ visualDepth := screen + screenFixedSize
168+ for binary.LittleEndian.Uint16(setup[visualDepth+2:]) == 0 {
169+ visualDepth += depthFixedSize
170+ }
171+ // Screen header present, its allowed depths cut off.
172+ missingDepths := truncateSetup(setup, screen+screenFixedSize)
173+ // A depth header announcing visuals, the visuals cut off.
174+ missingVisuals := truncateSetup(setup, visualDepth+depthFixedSize)
175+ // One visual of the depth present, the rest cut off.
176+ partialVisuals := truncateSetup(setup, visualDepth+depthFixedSize+visualSize)
177+ // Complete first screen, a second screen declared but absent.
178+ missingScreen := bytes.Clone(setup)
179+ missingScreen[28] = 2
180+ // Header claims 8 bytes of body, failure reason claims 20.
181+ reasonPast := []byte{0, 20, 11, 0, 0, 0, 2, 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
182+ // Success header whose length is shorter than the fixed Setup.
183+ shortSuccess := []byte{1, 0, 11, 0, 0, 0, 1, 0, 0, 0, 0, 0}
184+ for _, tc := range []struct {
185+ name string
186+ reply []byte
187+ }{
188+ {"empty", nil},
189+ {"truncated header", setup[:5]},
190+ {"truncated body", setup[:len(setup)-3]},
191+ {"no screens", noScreens},
192+ {"bad xid mask", badMask},
193+ {"protocol major", badVersion},
194+ {"unknown status", badStatus},
195+ {"screen past end", screenPast},
196+ {"missing depths", missingDepths},
197+ {"missing visuals", missingVisuals},
198+ {"partial visuals", partialVisuals},
199+ {"missing subsequent screen", missingScreen},
200+ {"reason past end", reasonPast},
201+ {"short success", shortSuccess},
202+ } {
203+ t.Run(tc.name, func(t *testing.T) {
204+ fd, _ := scriptedServer(t, tc.reply, 3, true)
205+ c, err := NewConn(fd)
206+ if c != nil || !errors.Is(err, ErrMalformed) || errors.Is(err, ErrSetupFailed) {
207+ t.Fatalf("conn %t, err %v", c != nil, err)
208+ }
209+ })
210+ }
211+}
212+
213+// truncateSetup keeps the first n bytes (n ≡ 0 mod 4) of a setup reply and
214+// rewrites its additional-data length to match.
215+func truncateSetup(setup []byte, n int) []byte {
216+ p := bytes.Clone(setup[:n])
217+ binary.LittleEndian.PutUint16(p[6:], uint16((n-setupHeaderSize)/4))
218+ return p
219+}
220+
221+func TestConnFlushClose(t *testing.T) {
222+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
223+ if err != nil {
224+ t.Fatal(err)
225+ }
226+ server := os.NewFile(uintptr(fds[1]), "server")
227+ defer server.Close()
228+ setup := loadSetup(t, "Success")
229+ go func() {
230+ var req [12]byte
231+ _, _ = io.ReadFull(server, req[:])
232+ _, _ = server.Write(setup)
233+ }()
234+ c, err := NewConn(fds[0])
235+ if err != nil {
236+ t.Fatal(err)
237+ }
238+ p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus
239+ if p == nil || seq != 1 {
240+ t.Fatal("request")
241+ }
242+ if err := c.Flush(); err != nil {
243+ t.Fatal(err)
244+ }
245+ var got [4]byte
246+ if _, err := io.ReadFull(server, got[:]); err != nil || got != [4]byte{43, 0, 1, 0} {
247+ t.Fatalf("%x %v", got, err)
248+ }
249+ if err := c.Close(); err != nil {
250+ t.Fatal(err)
251+ }
252+ if c.Fd() != -1 || !errors.Is(c.Close(), ErrClosed) || !errors.Is(c.Flush(), ErrClosed) {
253+ t.Fatal("after Close")
254+ }
255+ if p, _ := c.Buf().Request(43, 0, 0); p != nil {
256+ t.Fatal("Buf usable after Close")
257+ }
258+}
259+
260+func TestSetupErrorMessage(t *testing.T) {
261+ for _, tc := range []struct {
262+ e SetupError
263+ want string
264+ }{
265+ {SetupError{0, "no"}, "xturbo: connection setup failed: no"},
266+ {SetupError{2, "why"}, "xturbo: connection setup authenticate: why"},
267+ } {
268+ if got := tc.e.Error(); got != tc.want {
269+ t.Errorf("%q, want %q", got, tc.want)
270+ }
271+ }
272+}
@@ -0,0 +1,272 @@
1+package xturbo
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "errors"
7+ "io"
8+ "os"
9+ "path/filepath"
10+ "testing"
11+
12+ "bnema.dev/xturbo/internal/xtest"
13+ "golang.org/x/sys/unix"
14+)
15+
16+func loadSetup(t testing.TB, name string) []byte {
17+ t.Helper()
18+ b, err := xtest.LoadHex(filepath.Join("testdata", "setup", name+".setup.hex"))
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ return b
23+}
24+
25+// scriptedServer runs the server side of a socketpair: it reads one setup
26+// request, reports it on the returned channel, writes reply in chunks of
27+// chunk bytes (0 = all at once) and, if hangup, closes its end.
28+func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client int, req <-chan []byte) {
29+ t.Helper()
30+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
31+ if err != nil {
32+ t.Fatal(err)
33+ }
34+ // Exercise NewConn switching a non-blocking fd back to blocking.
35+ if err := unix.SetNonblock(fds[0], true); err != nil {
36+ t.Fatal(err)
37+ }
38+ server := os.NewFile(uintptr(fds[1]), "server")
39+ ch := make(chan []byte, 1)
40+ done := make(chan struct{})
41+ t.Cleanup(func() { <-done; server.Close() })
42+ go func() {
43+ defer close(done)
44+ var hdr [12]byte
45+ if _, err := io.ReadFull(server, hdr[:]); err != nil {
46+ ch <- nil
47+ return
48+ }
49+ n := pad4(int(binary.LittleEndian.Uint16(hdr[6:]))) + pad4(int(binary.LittleEndian.Uint16(hdr[8:])))
50+ rest := make([]byte, n)
51+ if _, err := io.ReadFull(server, rest); err != nil {
52+ ch <- nil
53+ return
54+ }
55+ ch <- append(hdr[:], rest...)
56+ for p := reply; len(p) > 0; {
57+ k := len(p)
58+ if chunk > 0 {
59+ k = min(k, chunk)
60+ }
61+ if _, err := server.Write(p[:k]); err != nil {
62+ return
63+ }
64+ p = p[k:]
65+ }
66+ if hangup {
67+ server.Close()
68+ }
69+ }()
70+ return fds[0], ch
71+}
72+
73+func TestNewConnSuccess(t *testing.T) {
74+ setup := loadSetup(t, "Success")
75+ fd, req := scriptedServer(t, setup, 7, false)
76+ c, err := NewConn(fd)
77+ if err != nil {
78+ t.Fatal(err)
79+ }
80+ defer c.Close()
81+ got := <-req
82+ want := []byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0}
83+ if !bytes.Equal(got, want) {
84+ t.Fatalf("setup request %x, want %x", got, want)
85+ }
86+ if !bytes.Equal(c.SetupBytes(), setup) {
87+ t.Fatal("SetupBytes differs from the reply")
88+ }
89+ // Expected values are read from the fixture header comment
90+ // (Success.setup.hex), i.e. what that recorded Xvfb assigned.
91+ if c.Root() != 0x39f || c.xidBase != 0x00200000 || c.xidMask != 0x001fffff {
92+ t.Fatalf("root %#x base %#x mask %#x", c.Root(), c.xidBase, c.xidMask)
93+ }
94+ if c.Fd() != fd || c.Buf().Ext != c.Ext() || c.Buf().Next() != 1 {
95+ t.Fatal("accessors")
96+ }
97+ if c.Ext().Major(ExtXFixes) != 0 || !c.Ext().Info(ExtCore).Present {
98+ t.Fatal("ExtTable not empty")
99+ }
100+ flags, err := unix.FcntlInt(uintptr(fd), unix.F_GETFL, 0)
101+ if err != nil || flags&unix.O_NONBLOCK == 0 {
102+ t.Fatalf("fd not O_NONBLOCK after setup: %v", err)
103+ }
104+}
105+
106+func TestNewConnAuthRequest(t *testing.T) {
107+ fd, req := scriptedServer(t, loadSetup(t, "Success"), 0, false)
108+ cookie := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
109+ c, err := newConn(fd, authMagicCookie, cookie)
110+ if err != nil {
111+ t.Fatal(err)
112+ }
113+ defer c.Close()
114+ want := append([]byte{'l', 0, 11, 0, 0, 0, 18, 0, 16, 0, 0, 0}, authMagicCookie+"\x00\x00"...)
115+ want = append(want, cookie...)
116+ if got := <-req; !bytes.Equal(got, want) {
117+ t.Fatalf("setup request %x, want %x", got, want)
118+ }
119+}
120+
121+func TestNewConnRefused(t *testing.T) {
122+ authenticate := []byte{2, 0, 0, 0, 0, 0, 2, 0, 'n', 'o', 'p', 'e', '!', 0, 0, 0}
123+ for _, tc := range []struct {
124+ name string
125+ reply []byte
126+ status uint8
127+ reason string
128+ }{
129+ {"failed", loadSetup(t, "Failed"), 0, "Authorization required, but no authorization protocol specified\n"},
130+ {"authenticate", authenticate, 2, "nope!"},
131+ } {
132+ t.Run(tc.name, func(t *testing.T) {
133+ fd, _ := scriptedServer(t, tc.reply, 5, true)
134+ c, err := NewConn(fd)
135+ var se *SetupError
136+ if c != nil || !errors.As(err, &se) || !errors.Is(err, ErrSetupFailed) {
137+ t.Fatalf("conn %t, err %v", c != nil, err)
138+ }
139+ if se.Status != tc.status || se.Reason != tc.reason {
140+ t.Fatalf("status %d reason %q", se.Status, se.Reason)
141+ }
142+ if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err != unix.EBADF {
143+ t.Fatalf("fd not closed on failure: %v", err)
144+ }
145+ })
146+ }
147+}
148+
149+func TestNewConnMalformed(t *testing.T) {
150+ setup := loadSetup(t, "Success")
151+ noScreens := bytes.Clone(setup)
152+ noScreens[28] = 0
153+ badMask := bytes.Clone(setup)
154+ binary.LittleEndian.PutUint32(badMask[16:], 0)
155+ badVersion := bytes.Clone(setup)
156+ badVersion[2] = 10
157+ badStatus := bytes.Clone(setup)
158+ badStatus[0] = 3
159+ // Fixed Setup only (length 8 words): one root declared, no SCREEN bytes.
160+ screenPast := bytes.Clone(setup[:40])
161+ binary.LittleEndian.PutUint16(screenPast[6:], 8)
162+ binary.LittleEndian.PutUint16(screenPast[24:], 0) // vendor_len
163+ screenPast[29] = 0 // pixmap_formats_len
164+ // Walk the recorded reply to find the first screen and its first depth
165+ // that carries visuals; every extent is a multiple of 4 bytes.
166+ screen := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(setup[24:]))) + formatSize*int(setup[29])
167+ visualDepth := screen + screenFixedSize
168+ for binary.LittleEndian.Uint16(setup[visualDepth+2:]) == 0 {
169+ visualDepth += depthFixedSize
170+ }
171+ // Screen header present, its allowed depths cut off.
172+ missingDepths := truncateSetup(setup, screen+screenFixedSize)
173+ // A depth header announcing visuals, the visuals cut off.
174+ missingVisuals := truncateSetup(setup, visualDepth+depthFixedSize)
175+ // One visual of the depth present, the rest cut off.
176+ partialVisuals := truncateSetup(setup, visualDepth+depthFixedSize+visualSize)
177+ // Complete first screen, a second screen declared but absent.
178+ missingScreen := bytes.Clone(setup)
179+ missingScreen[28] = 2
180+ // Header claims 8 bytes of body, failure reason claims 20.
181+ reasonPast := []byte{0, 20, 11, 0, 0, 0, 2, 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
182+ // Success header whose length is shorter than the fixed Setup.
183+ shortSuccess := []byte{1, 0, 11, 0, 0, 0, 1, 0, 0, 0, 0, 0}
184+ for _, tc := range []struct {
185+ name string
186+ reply []byte
187+ }{
188+ {"empty", nil},
189+ {"truncated header", setup[:5]},
190+ {"truncated body", setup[:len(setup)-3]},
191+ {"no screens", noScreens},
192+ {"bad xid mask", badMask},
193+ {"protocol major", badVersion},
194+ {"unknown status", badStatus},
195+ {"screen past end", screenPast},
196+ {"missing depths", missingDepths},
197+ {"missing visuals", missingVisuals},
198+ {"partial visuals", partialVisuals},
199+ {"missing subsequent screen", missingScreen},
200+ {"reason past end", reasonPast},
201+ {"short success", shortSuccess},
202+ } {
203+ t.Run(tc.name, func(t *testing.T) {
204+ fd, _ := scriptedServer(t, tc.reply, 3, true)
205+ c, err := NewConn(fd)
206+ if c != nil || !errors.Is(err, ErrMalformed) || errors.Is(err, ErrSetupFailed) {
207+ t.Fatalf("conn %t, err %v", c != nil, err)
208+ }
209+ })
210+ }
211+}
212+
213+// truncateSetup keeps the first n bytes (n ≡ 0 mod 4) of a setup reply and
214+// rewrites its additional-data length to match.
215+func truncateSetup(setup []byte, n int) []byte {
216+ p := bytes.Clone(setup[:n])
217+ binary.LittleEndian.PutUint16(p[6:], uint16((n-setupHeaderSize)/4))
218+ return p
219+}
220+
221+func TestConnFlushClose(t *testing.T) {
222+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
223+ if err != nil {
224+ t.Fatal(err)
225+ }
226+ server := os.NewFile(uintptr(fds[1]), "server")
227+ defer server.Close()
228+ setup := loadSetup(t, "Success")
229+ go func() {
230+ var req [12]byte
231+ _, _ = io.ReadFull(server, req[:])
232+ _, _ = server.Write(setup)
233+ }()
234+ c, err := NewConn(fds[0])
235+ if err != nil {
236+ t.Fatal(err)
237+ }
238+ p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus
239+ if p == nil || seq != 1 {
240+ t.Fatal("request")
241+ }
242+ if err := c.Flush(); err != nil {
243+ t.Fatal(err)
244+ }
245+ var got [4]byte
246+ if _, err := io.ReadFull(server, got[:]); err != nil || got != [4]byte{43, 0, 1, 0} {
247+ t.Fatalf("%x %v", got, err)
248+ }
249+ if err := c.Close(); err != nil {
250+ t.Fatal(err)
251+ }
252+ if c.Fd() != -1 || !errors.Is(c.Close(), ErrClosed) || !errors.Is(c.Flush(), ErrClosed) {
253+ t.Fatal("after Close")
254+ }
255+ if p, _ := c.Buf().Request(43, 0, 0); p != nil {
256+ t.Fatal("Buf usable after Close")
257+ }
258+}
259+
260+func TestSetupErrorMessage(t *testing.T) {
261+ for _, tc := range []struct {
262+ e SetupError
263+ want string
264+ }{
265+ {SetupError{0, "no"}, "xturbo: connection setup failed: no"},
266+ {SetupError{2, "why"}, "xturbo: connection setup authenticate: why"},
267+ } {
268+ if got := tc.e.Error(); got != tc.want {
269+ t.Errorf("%q, want %q", got, tc.want)
270+ }
271+ }
272+}
A conn_xserver_test.go +161 -0
@@ -0,0 +1,161 @@
1+//go:build xserver
2+
3+package xturbo_test
4+
5+import (
6+ "bytes"
7+ "crypto/rand"
8+ "encoding/binary"
9+ "errors"
10+ "os"
11+ "os/exec"
12+ "path/filepath"
13+ "strconv"
14+ "strings"
15+ "testing"
16+ "time"
17+
18+ "bnema.dev/xturbo"
19+ "bnema.dev/xturbo/xproto"
20+ "golang.org/x/sys/unix"
21+)
22+
23+// startXvfb runs Xvfb on a free display with -nolisten tcp, requiring the
24+// MIT-MAGIC-COOKIE-1 in authFile. It returns the DISPLAY string.
25+func startXvfb(t *testing.T, authFile string) string {
26+ t.Helper()
27+ if _, err := exec.LookPath("Xvfb"); err != nil {
28+ t.Skip("Xvfb not installed")
29+ }
30+ r, w, err := os.Pipe()
31+ if err != nil {
32+ t.Fatal(err)
33+ }
34+ // Xvfb may write to -displayfd again (e.g. after a server reset); closing
35+ // the read end early makes that write fail and kills the server.
36+ t.Cleanup(func() { _ = r.Close() })
37+ cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-auth", authFile)
38+ cmd.ExtraFiles = []*os.File{w}
39+ var log bytes.Buffer
40+ cmd.Stderr = &log
41+ if err := cmd.Start(); err != nil {
42+ w.Close()
43+ t.Fatal(err)
44+ }
45+ w.Close()
46+ t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() })
47+ _ = r.SetReadDeadline(time.Now().Add(10 * time.Second))
48+ var buf [32]byte
49+ n, err := r.Read(buf[:])
50+ if err != nil {
51+ t.Fatalf("Xvfb: %v: %s", err, log.String())
52+ }
53+ num := strings.TrimSpace(string(buf[:n]))
54+ if _, err := strconv.Atoi(num); err != nil {
55+ t.Fatalf("Xvfb display %q", num)
56+ }
57+ return ":" + num
58+}
59+
60+// writeXauthority writes one FamilyLocal entry for this host, any display.
61+func writeXauthority(t *testing.T, path string, cookie []byte) {
62+ t.Helper()
63+ host, err := os.Hostname()
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+ var b []byte
68+ field := func(s []byte) {
69+ b = binary.BigEndian.AppendUint16(b, uint16(len(s)))
70+ b = append(b, s...)
71+ }
72+ b = binary.BigEndian.AppendUint16(b, 256) // FamilyLocal
73+ field([]byte(host))
74+ field(nil) // any display number
75+ field([]byte("MIT-MAGIC-COOKIE-1"))
76+ field(cookie)
77+ if err := os.WriteFile(path, b, 0o600); err != nil {
78+ t.Fatal(err)
79+ }
80+}
81+
82+func TestXserverDial(t *testing.T) {
83+ dir := t.TempDir()
84+ auth := filepath.Join(dir, "Xauthority")
85+ cookie := make([]byte, 16)
86+ _, _ = rand.Read(cookie)
87+ writeXauthority(t, auth, cookie)
88+ display := startXvfb(t, auth)
89+
90+ // Without a cookie the server refuses the setup: some builds answer Failed,
91+ // others close the connection without a reply.
92+ t.Setenv("XAUTHORITY", filepath.Join(dir, "missing"))
93+ if _, err := xturbo.Dial(display); !errors.Is(err, xturbo.ErrSetupFailed) && !errors.Is(err, xturbo.ErrClosed) {
94+ t.Fatalf("unauthenticated Dial: %v", err)
95+ }
96+
97+ t.Setenv("XAUTHORITY", auth)
98+ c, err := xturbo.Dial(display + ".0")
99+ if err != nil {
100+ t.Fatal(err)
101+ }
102+ defer c.Close()
103+
104+ // Compare root's own parsing against the generated Setup view instead of
105+ // server-assigned constants (they differ between X server builds).
106+ setup := c.SetupBytes()
107+ if c.Root() == 0 || len(setup) < xproto.SetupSize || len(setup) != 8+4*int(binary.LittleEndian.Uint16(setup[6:])) {
108+ t.Fatalf("root %#x, setup %d bytes", c.Root(), len(setup))
109+ }
110+ it := xproto.NewSetupIter(setup, 1)
111+ if !it.Next() {
112+ t.Fatal("setup does not parse")
113+ }
114+ sv := it.Value()
115+ if sv.Size() != len(setup) || sv.Status() != 1 || sv.ProtocolMajorVersion() != 11 {
116+ t.Fatalf("setup size %d status %d", sv.Size(), sv.Status())
117+ }
118+ screens := sv.Roots()
119+ if !screens.Next() || uint32(screens.Value().Root()) != c.Root() {
120+ t.Fatalf("Root %#x, setup view says otherwise", c.Root())
121+ }
122+
123+ // A trivial round trip: GetInputFocus, then read its reply.
124+ seq := xproto.GetInputFocus(c.Buf())
125+ if seq != 1 {
126+ t.Fatalf("seq %d", seq)
127+ }
128+ if err := c.Flush(); err != nil {
129+ t.Fatal(err)
130+ }
131+ reply := readPacket(t, c.Fd())
132+ if reply[0] != 1 || binary.LittleEndian.Uint16(reply[2:]) != uint16(seq) {
133+ t.Fatalf("reply %x", reply)
134+ }
135+}
136+
137+// readPacket polls the non-blocking fd and reads one 32-byte packet.
138+func readPacket(t *testing.T, fd int) [32]byte {
139+ t.Helper()
140+ var p [32]byte
141+ got := 0
142+ deadline := time.Now().Add(5 * time.Second)
143+ for got < len(p) {
144+ if time.Now().After(deadline) {
145+ t.Fatal("timeout reading reply")
146+ }
147+ fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
148+ if _, err := unix.Poll(fds, 1000); err != nil && err != unix.EINTR {
149+ t.Fatal(err)
150+ }
151+ n, err := unix.Read(fd, p[got:])
152+ if err == unix.EAGAIN || err == unix.EINTR {
153+ continue
154+ }
155+ if err != nil || n == 0 {
156+ t.Fatalf("read: %d %v", n, err)
157+ }
158+ got += n
159+ }
160+ return p
161+}
@@ -0,0 +1,161 @@
1+//go:build xserver
2+
3+package xturbo_test
4+
5+import (
6+ "bytes"
7+ "crypto/rand"
8+ "encoding/binary"
9+ "errors"
10+ "os"
11+ "os/exec"
12+ "path/filepath"
13+ "strconv"
14+ "strings"
15+ "testing"
16+ "time"
17+
18+ "bnema.dev/xturbo"
19+ "bnema.dev/xturbo/xproto"
20+ "golang.org/x/sys/unix"
21+)
22+
23+// startXvfb runs Xvfb on a free display with -nolisten tcp, requiring the
24+// MIT-MAGIC-COOKIE-1 in authFile. It returns the DISPLAY string.
25+func startXvfb(t *testing.T, authFile string) string {
26+ t.Helper()
27+ if _, err := exec.LookPath("Xvfb"); err != nil {
28+ t.Skip("Xvfb not installed")
29+ }
30+ r, w, err := os.Pipe()
31+ if err != nil {
32+ t.Fatal(err)
33+ }
34+ // Xvfb may write to -displayfd again (e.g. after a server reset); closing
35+ // the read end early makes that write fail and kills the server.
36+ t.Cleanup(func() { _ = r.Close() })
37+ cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-auth", authFile)
38+ cmd.ExtraFiles = []*os.File{w}
39+ var log bytes.Buffer
40+ cmd.Stderr = &log
41+ if err := cmd.Start(); err != nil {
42+ w.Close()
43+ t.Fatal(err)
44+ }
45+ w.Close()
46+ t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() })
47+ _ = r.SetReadDeadline(time.Now().Add(10 * time.Second))
48+ var buf [32]byte
49+ n, err := r.Read(buf[:])
50+ if err != nil {
51+ t.Fatalf("Xvfb: %v: %s", err, log.String())
52+ }
53+ num := strings.TrimSpace(string(buf[:n]))
54+ if _, err := strconv.Atoi(num); err != nil {
55+ t.Fatalf("Xvfb display %q", num)
56+ }
57+ return ":" + num
58+}
59+
60+// writeXauthority writes one FamilyLocal entry for this host, any display.
61+func writeXauthority(t *testing.T, path string, cookie []byte) {
62+ t.Helper()
63+ host, err := os.Hostname()
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+ var b []byte
68+ field := func(s []byte) {
69+ b = binary.BigEndian.AppendUint16(b, uint16(len(s)))
70+ b = append(b, s...)
71+ }
72+ b = binary.BigEndian.AppendUint16(b, 256) // FamilyLocal
73+ field([]byte(host))
74+ field(nil) // any display number
75+ field([]byte("MIT-MAGIC-COOKIE-1"))
76+ field(cookie)
77+ if err := os.WriteFile(path, b, 0o600); err != nil {
78+ t.Fatal(err)
79+ }
80+}
81+
82+func TestXserverDial(t *testing.T) {
83+ dir := t.TempDir()
84+ auth := filepath.Join(dir, "Xauthority")
85+ cookie := make([]byte, 16)
86+ _, _ = rand.Read(cookie)
87+ writeXauthority(t, auth, cookie)
88+ display := startXvfb(t, auth)
89+
90+ // Without a cookie the server refuses the setup: some builds answer Failed,
91+ // others close the connection without a reply.
92+ t.Setenv("XAUTHORITY", filepath.Join(dir, "missing"))
93+ if _, err := xturbo.Dial(display); !errors.Is(err, xturbo.ErrSetupFailed) && !errors.Is(err, xturbo.ErrClosed) {
94+ t.Fatalf("unauthenticated Dial: %v", err)
95+ }
96+
97+ t.Setenv("XAUTHORITY", auth)
98+ c, err := xturbo.Dial(display + ".0")
99+ if err != nil {
100+ t.Fatal(err)
101+ }
102+ defer c.Close()
103+
104+ // Compare root's own parsing against the generated Setup view instead of
105+ // server-assigned constants (they differ between X server builds).
106+ setup := c.SetupBytes()
107+ if c.Root() == 0 || len(setup) < xproto.SetupSize || len(setup) != 8+4*int(binary.LittleEndian.Uint16(setup[6:])) {
108+ t.Fatalf("root %#x, setup %d bytes", c.Root(), len(setup))
109+ }
110+ it := xproto.NewSetupIter(setup, 1)
111+ if !it.Next() {
112+ t.Fatal("setup does not parse")
113+ }
114+ sv := it.Value()
115+ if sv.Size() != len(setup) || sv.Status() != 1 || sv.ProtocolMajorVersion() != 11 {
116+ t.Fatalf("setup size %d status %d", sv.Size(), sv.Status())
117+ }
118+ screens := sv.Roots()
119+ if !screens.Next() || uint32(screens.Value().Root()) != c.Root() {
120+ t.Fatalf("Root %#x, setup view says otherwise", c.Root())
121+ }
122+
123+ // A trivial round trip: GetInputFocus, then read its reply.
124+ seq := xproto.GetInputFocus(c.Buf())
125+ if seq != 1 {
126+ t.Fatalf("seq %d", seq)
127+ }
128+ if err := c.Flush(); err != nil {
129+ t.Fatal(err)
130+ }
131+ reply := readPacket(t, c.Fd())
132+ if reply[0] != 1 || binary.LittleEndian.Uint16(reply[2:]) != uint16(seq) {
133+ t.Fatalf("reply %x", reply)
134+ }
135+}
136+
137+// readPacket polls the non-blocking fd and reads one 32-byte packet.
138+func readPacket(t *testing.T, fd int) [32]byte {
139+ t.Helper()
140+ var p [32]byte
141+ got := 0
142+ deadline := time.Now().Add(5 * time.Second)
143+ for got < len(p) {
144+ if time.Now().After(deadline) {
145+ t.Fatal("timeout reading reply")
146+ }
147+ fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
148+ if _, err := unix.Poll(fds, 1000); err != nil && err != unix.EINTR {
149+ t.Fatal(err)
150+ }
151+ n, err := unix.Read(fd, p[got:])
152+ if err == unix.EAGAIN || err == unix.EINTR {
153+ continue
154+ }
155+ if err != nil || n == 0 {
156+ t.Fatalf("read: %d %v", n, err)
157+ }
158+ got += n
159+ }
160+ return p
161+}
A dial.go +152 -0
@@ -0,0 +1,152 @@
1+package xturbo
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "fmt"
7+ "io"
8+ "os"
9+ "path/filepath"
10+ "strconv"
11+ "strings"
12+
13+ "golang.org/x/sys/unix"
14+)
15+
16+const (
17+ authMagicCookie = "MIT-MAGIC-COOKIE-1"
18+ familyLocal = 256 // FamilyLocal in Xauthority files
19+ familyWild = 0xffff // FamilyWild: matches any address
20+ socketDir = "/tmp/.X11-unix/X"
21+)
22+
23+// Dial connects to a local X server named by a DISPLAY string (":N", ":N.S",
24+// "unix:N"; empty means $DISPLAY). It tries the abstract socket
25+// "@/tmp/.X11-unix/XN" first, then the filesystem socket, and authenticates
26+// with MIT-MAGIC-COOKIE-1 from $XAUTHORITY or ~/.Xauthority when a matching
27+// entry exists. Meant for tests and tools; the XWM uses NewConn.
28+func Dial(display string) (*Conn, error) {
29+ if display == "" {
30+ display = os.Getenv("DISPLAY")
31+ }
32+ num, err := parseDisplay(display)
33+ if err != nil {
34+ return nil, err
35+ }
36+ fd, err := dialUnix(num)
37+ if err != nil {
38+ return nil, err
39+ }
40+ var cookie []byte
41+ if path := xauthorityPath(); path != "" {
42+ host, _ := os.Hostname()
43+ cookie, err = readCookie(path, host, num)
44+ if err != nil && !errors.Is(err, os.ErrNotExist) {
45+ _ = unix.Close(fd)
46+ return nil, err
47+ }
48+ }
49+ if cookie == nil {
50+ return newConn(fd, "", nil)
51+ }
52+ return newConn(fd, authMagicCookie, cookie)
53+}
54+
55+// parseDisplay returns the display number of a local DISPLAY string. The
56+// screen suffix is accepted and ignored (Root is always the first screen).
57+func parseDisplay(display string) (int, error) {
58+ host, rest, ok := strings.Cut(display, ":")
59+ if !ok || (host != "" && host != "unix") || strings.HasPrefix(rest, ":") {
60+ return 0, fmt.Errorf("xturbo: unsupported DISPLAY %q (local :N or unix:N only)", display)
61+ }
62+ numStr, screen, hasScreen := strings.Cut(rest, ".")
63+ num, err := strconv.ParseUint(numStr, 10, 31)
64+ if err != nil {
65+ return 0, fmt.Errorf("xturbo: bad DISPLAY %q: display number", display)
66+ }
67+ if hasScreen {
68+ if _, err := strconv.ParseUint(screen, 10, 31); err != nil {
69+ return 0, fmt.Errorf("xturbo: bad DISPLAY %q: screen number", display)
70+ }
71+ }
72+ return int(num), nil
73+}
74+
75+// dialUnix connects a blocking stream socket to display num, abstract
76+// namespace first (as libxcb on Linux), then the filesystem path.
77+func dialUnix(num int) (int, error) {
78+ path := socketDir + strconv.Itoa(num)
79+ var firstErr error
80+ for _, name := range []string{"@" + path, path} {
81+ fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
82+ if err != nil {
83+ return -1, os.NewSyscallError("socket", err)
84+ }
85+ for {
86+ err = unix.Connect(fd, &unix.SockaddrUnix{Name: name})
87+ if err != unix.EINTR {
88+ break
89+ }
90+ }
91+ if err == nil {
92+ return fd, nil
93+ }
94+ _ = unix.Close(fd)
95+ if firstErr == nil {
96+ firstErr = err
97+ }
98+ }
99+ return -1, fmt.Errorf("xturbo: connect %s: %w", path, firstErr)
100+}
101+
102+func xauthorityPath() string {
103+ if p := os.Getenv("XAUTHORITY"); p != "" {
104+ return p
105+ }
106+ if home, err := os.UserHomeDir(); err == nil {
107+ return filepath.Join(home, ".Xauthority")
108+ }
109+ return ""
110+}
111+
112+// readCookie returns the MIT-MAGIC-COOKIE-1 for a local display from an
113+// Xauthority file: the first entry of family local with address host (or
114+// FamilyWild), display number num (or empty, meaning any). It returns nil,
115+// nil when no entry matches.
116+func readCookie(path, host string, num int) ([]byte, error) {
117+ data, err := os.ReadFile(path)
118+ if err != nil {
119+ return nil, err
120+ }
121+ return findCookie(data, host, num)
122+}
123+
124+func findCookie(data []byte, host string, num int) ([]byte, error) {
125+ want := strconv.Itoa(num)
126+ for len(data) > 0 {
127+ if len(data) < 2 {
128+ return nil, malformed("Xauthority entry truncated")
129+ }
130+ family := binary.BigEndian.Uint16(data)
131+ data = data[2:]
132+ var fields [4][]byte // address, number, name, data
133+ for i := range fields {
134+ if len(data) < 2 {
135+ return nil, fmt.Errorf("%w: Xauthority entry truncated: %w", ErrMalformed, io.ErrUnexpectedEOF)
136+ }
137+ n := int(binary.BigEndian.Uint16(data))
138+ if len(data) < 2+n {
139+ return nil, fmt.Errorf("%w: Xauthority field truncated: %w", ErrMalformed, io.ErrUnexpectedEOF)
140+ }
141+ fields[i] = data[2 : 2+n]
142+ data = data[2+n:]
143+ }
144+ addr, number, name, cookie := fields[0], fields[1], fields[2], fields[3]
145+ hostOK := family == familyWild || (family == familyLocal && string(addr) == host)
146+ numOK := len(number) == 0 || string(number) == want
147+ if hostOK && numOK && string(name) == authMagicCookie {
148+ return append([]byte(nil), cookie...), nil
149+ }
150+ }
151+ return nil, nil
152+}
@@ -0,0 +1,152 @@
1+package xturbo
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "fmt"
7+ "io"
8+ "os"
9+ "path/filepath"
10+ "strconv"
11+ "strings"
12+
13+ "golang.org/x/sys/unix"
14+)
15+
16+const (
17+ authMagicCookie = "MIT-MAGIC-COOKIE-1"
18+ familyLocal = 256 // FamilyLocal in Xauthority files
19+ familyWild = 0xffff // FamilyWild: matches any address
20+ socketDir = "/tmp/.X11-unix/X"
21+)
22+
23+// Dial connects to a local X server named by a DISPLAY string (":N", ":N.S",
24+// "unix:N"; empty means $DISPLAY). It tries the abstract socket
25+// "@/tmp/.X11-unix/XN" first, then the filesystem socket, and authenticates
26+// with MIT-MAGIC-COOKIE-1 from $XAUTHORITY or ~/.Xauthority when a matching
27+// entry exists. Meant for tests and tools; the XWM uses NewConn.
28+func Dial(display string) (*Conn, error) {
29+ if display == "" {
30+ display = os.Getenv("DISPLAY")
31+ }
32+ num, err := parseDisplay(display)
33+ if err != nil {
34+ return nil, err
35+ }
36+ fd, err := dialUnix(num)
37+ if err != nil {
38+ return nil, err
39+ }
40+ var cookie []byte
41+ if path := xauthorityPath(); path != "" {
42+ host, _ := os.Hostname()
43+ cookie, err = readCookie(path, host, num)
44+ if err != nil && !errors.Is(err, os.ErrNotExist) {
45+ _ = unix.Close(fd)
46+ return nil, err
47+ }
48+ }
49+ if cookie == nil {
50+ return newConn(fd, "", nil)
51+ }
52+ return newConn(fd, authMagicCookie, cookie)
53+}
54+
55+// parseDisplay returns the display number of a local DISPLAY string. The
56+// screen suffix is accepted and ignored (Root is always the first screen).
57+func parseDisplay(display string) (int, error) {
58+ host, rest, ok := strings.Cut(display, ":")
59+ if !ok || (host != "" && host != "unix") || strings.HasPrefix(rest, ":") {
60+ return 0, fmt.Errorf("xturbo: unsupported DISPLAY %q (local :N or unix:N only)", display)
61+ }
62+ numStr, screen, hasScreen := strings.Cut(rest, ".")
63+ num, err := strconv.ParseUint(numStr, 10, 31)
64+ if err != nil {
65+ return 0, fmt.Errorf("xturbo: bad DISPLAY %q: display number", display)
66+ }
67+ if hasScreen {
68+ if _, err := strconv.ParseUint(screen, 10, 31); err != nil {
69+ return 0, fmt.Errorf("xturbo: bad DISPLAY %q: screen number", display)
70+ }
71+ }
72+ return int(num), nil
73+}
74+
75+// dialUnix connects a blocking stream socket to display num, abstract
76+// namespace first (as libxcb on Linux), then the filesystem path.
77+func dialUnix(num int) (int, error) {
78+ path := socketDir + strconv.Itoa(num)
79+ var firstErr error
80+ for _, name := range []string{"@" + path, path} {
81+ fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
82+ if err != nil {
83+ return -1, os.NewSyscallError("socket", err)
84+ }
85+ for {
86+ err = unix.Connect(fd, &unix.SockaddrUnix{Name: name})
87+ if err != unix.EINTR {
88+ break
89+ }
90+ }
91+ if err == nil {
92+ return fd, nil
93+ }
94+ _ = unix.Close(fd)
95+ if firstErr == nil {
96+ firstErr = err
97+ }
98+ }
99+ return -1, fmt.Errorf("xturbo: connect %s: %w", path, firstErr)
100+}
101+
102+func xauthorityPath() string {
103+ if p := os.Getenv("XAUTHORITY"); p != "" {
104+ return p
105+ }
106+ if home, err := os.UserHomeDir(); err == nil {
107+ return filepath.Join(home, ".Xauthority")
108+ }
109+ return ""
110+}
111+
112+// readCookie returns the MIT-MAGIC-COOKIE-1 for a local display from an
113+// Xauthority file: the first entry of family local with address host (or
114+// FamilyWild), display number num (or empty, meaning any). It returns nil,
115+// nil when no entry matches.
116+func readCookie(path, host string, num int) ([]byte, error) {
117+ data, err := os.ReadFile(path)
118+ if err != nil {
119+ return nil, err
120+ }
121+ return findCookie(data, host, num)
122+}
123+
124+func findCookie(data []byte, host string, num int) ([]byte, error) {
125+ want := strconv.Itoa(num)
126+ for len(data) > 0 {
127+ if len(data) < 2 {
128+ return nil, malformed("Xauthority entry truncated")
129+ }
130+ family := binary.BigEndian.Uint16(data)
131+ data = data[2:]
132+ var fields [4][]byte // address, number, name, data
133+ for i := range fields {
134+ if len(data) < 2 {
135+ return nil, fmt.Errorf("%w: Xauthority entry truncated: %w", ErrMalformed, io.ErrUnexpectedEOF)
136+ }
137+ n := int(binary.BigEndian.Uint16(data))
138+ if len(data) < 2+n {
139+ return nil, fmt.Errorf("%w: Xauthority field truncated: %w", ErrMalformed, io.ErrUnexpectedEOF)
140+ }
141+ fields[i] = data[2 : 2+n]
142+ data = data[2+n:]
143+ }
144+ addr, number, name, cookie := fields[0], fields[1], fields[2], fields[3]
145+ hostOK := family == familyWild || (family == familyLocal && string(addr) == host)
146+ numOK := len(number) == 0 || string(number) == want
147+ if hostOK && numOK && string(name) == authMagicCookie {
148+ return append([]byte(nil), cookie...), nil
149+ }
150+ }
151+ return nil, nil
152+}
A dial_test.go +124 -0
@@ -0,0 +1,124 @@
1+package xturbo
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "errors"
7+ "os"
8+ "path/filepath"
9+ "testing"
10+)
11+
12+type authEntry struct {
13+ family uint16
14+ addr, number, name string
15+ data []byte
16+}
17+
18+func encodeAuth(entries ...authEntry) []byte {
19+ var b []byte
20+ str := func(s []byte) {
21+ b = binary.BigEndian.AppendUint16(b, uint16(len(s)))
22+ b = append(b, s...)
23+ }
24+ for _, e := range entries {
25+ b = binary.BigEndian.AppendUint16(b, e.family)
26+ str([]byte(e.addr))
27+ str([]byte(e.number))
28+ str([]byte(e.name))
29+ str(e.data)
30+ }
31+ return b
32+}
33+
34+func TestReadCookie(t *testing.T) {
35+ cookie := []byte("0123456789abcdef")
36+ other := []byte("fedcba9876543210")
37+ local := func(host, num string, data []byte) authEntry {
38+ return authEntry{familyLocal, host, num, authMagicCookie, data}
39+ }
40+ for _, tc := range []struct {
41+ name string
42+ file []byte
43+ want []byte
44+ wantErr error
45+ }{
46+ {"match", encodeAuth(local("arch", "7", cookie)), cookie, nil},
47+ {"first match wins", encodeAuth(local("other", "7", other), local("arch", "3", other), local("arch", "7", cookie), local("arch", "7", other)), cookie, nil},
48+ {"wild family", encodeAuth(authEntry{familyWild, "", "7", authMagicCookie, cookie}), cookie, nil},
49+ {"empty number matches any", encodeAuth(local("arch", "", cookie)), cookie, nil},
50+ {"other host", encodeAuth(local("box", "7", cookie)), nil, nil},
51+ {"other display", encodeAuth(local("arch", "70", cookie)), nil, nil},
52+ {"other family", encodeAuth(authEntry{0, "arch", "7", authMagicCookie, cookie}), nil, nil},
53+ {"other protocol", encodeAuth(authEntry{familyLocal, "arch", "7", "XDM-AUTHORIZATION-1", cookie}), nil, nil},
54+ {"empty file", nil, nil, nil},
55+ {"truncated family", []byte{1}, nil, ErrMalformed},
56+ {"truncated field", encodeAuth(local("arch", "7", cookie))[:30], nil, ErrMalformed},
57+ {"missing fields", encodeAuth(local("arch", "7", cookie))[:10], nil, ErrMalformed},
58+ } {
59+ t.Run(tc.name, func(t *testing.T) {
60+ path := filepath.Join(t.TempDir(), "Xauthority")
61+ if err := os.WriteFile(path, tc.file, 0o600); err != nil {
62+ t.Fatal(err)
63+ }
64+ got, err := readCookie(path, "arch", 7)
65+ if !errors.Is(err, tc.wantErr) || (tc.wantErr == nil && err != nil) || !bytes.Equal(got, tc.want) {
66+ t.Fatalf("got %q, %v; want %q, %v", got, err, tc.want, tc.wantErr)
67+ }
68+ })
69+ }
70+ if _, err := readCookie(filepath.Join(t.TempDir(), "missing"), "arch", 7); !errors.Is(err, os.ErrNotExist) {
71+ t.Fatal(err)
72+ }
73+}
74+
75+func TestXauthorityPath(t *testing.T) {
76+ t.Setenv("XAUTHORITY", "/x/auth")
77+ if got := xauthorityPath(); got != "/x/auth" {
78+ t.Fatal(got)
79+ }
80+ t.Setenv("XAUTHORITY", "")
81+ t.Setenv("HOME", "/home/u")
82+ if got := xauthorityPath(); got != "/home/u/.Xauthority" {
83+ t.Fatal(got)
84+ }
85+}
86+
87+func TestParseDisplay(t *testing.T) {
88+ for _, tc := range []struct {
89+ in string
90+ want int
91+ ok bool
92+ }{
93+ {":0", 0, true},
94+ {":12", 12, true},
95+ {":3.1", 3, true},
96+ {"unix:5", 5, true},
97+ {"unix:5.0", 5, true},
98+ {"", 0, false},
99+ {"0", 0, false},
100+ {":", 0, false},
101+ {":x", 0, false},
102+ {":1.", 0, false},
103+ {":1.x", 0, false},
104+ {":-1", 0, false},
105+ {"localhost:0", 0, false},
106+ {"host::0", 0, false},
107+ {"::1:0", 0, false},
108+ } {
109+ got, err := parseDisplay(tc.in)
110+ if (err == nil) != tc.ok || got != tc.want {
111+ t.Errorf("%q: %d, %v", tc.in, got, err)
112+ }
113+ }
114+}
115+
116+func TestDialNoServer(t *testing.T) {
117+ // Display numbers above any real server: both abstract and path fail.
118+ if _, err := Dial(":2147483000"); err == nil {
119+ t.Fatal("Dial succeeded without a server")
120+ }
121+ if _, err := Dial("tcp/host:0"); err == nil {
122+ t.Fatal("remote DISPLAY accepted")
123+ }
124+}
@@ -0,0 +1,124 @@
1+package xturbo
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "errors"
7+ "os"
8+ "path/filepath"
9+ "testing"
10+)
11+
12+type authEntry struct {
13+ family uint16
14+ addr, number, name string
15+ data []byte
16+}
17+
18+func encodeAuth(entries ...authEntry) []byte {
19+ var b []byte
20+ str := func(s []byte) {
21+ b = binary.BigEndian.AppendUint16(b, uint16(len(s)))
22+ b = append(b, s...)
23+ }
24+ for _, e := range entries {
25+ b = binary.BigEndian.AppendUint16(b, e.family)
26+ str([]byte(e.addr))
27+ str([]byte(e.number))
28+ str([]byte(e.name))
29+ str(e.data)
30+ }
31+ return b
32+}
33+
34+func TestReadCookie(t *testing.T) {
35+ cookie := []byte("0123456789abcdef")
36+ other := []byte("fedcba9876543210")
37+ local := func(host, num string, data []byte) authEntry {
38+ return authEntry{familyLocal, host, num, authMagicCookie, data}
39+ }
40+ for _, tc := range []struct {
41+ name string
42+ file []byte
43+ want []byte
44+ wantErr error
45+ }{
46+ {"match", encodeAuth(local("arch", "7", cookie)), cookie, nil},
47+ {"first match wins", encodeAuth(local("other", "7", other), local("arch", "3", other), local("arch", "7", cookie), local("arch", "7", other)), cookie, nil},
48+ {"wild family", encodeAuth(authEntry{familyWild, "", "7", authMagicCookie, cookie}), cookie, nil},
49+ {"empty number matches any", encodeAuth(local("arch", "", cookie)), cookie, nil},
50+ {"other host", encodeAuth(local("box", "7", cookie)), nil, nil},
51+ {"other display", encodeAuth(local("arch", "70", cookie)), nil, nil},
52+ {"other family", encodeAuth(authEntry{0, "arch", "7", authMagicCookie, cookie}), nil, nil},
53+ {"other protocol", encodeAuth(authEntry{familyLocal, "arch", "7", "XDM-AUTHORIZATION-1", cookie}), nil, nil},
54+ {"empty file", nil, nil, nil},
55+ {"truncated family", []byte{1}, nil, ErrMalformed},
56+ {"truncated field", encodeAuth(local("arch", "7", cookie))[:30], nil, ErrMalformed},
57+ {"missing fields", encodeAuth(local("arch", "7", cookie))[:10], nil, ErrMalformed},
58+ } {
59+ t.Run(tc.name, func(t *testing.T) {
60+ path := filepath.Join(t.TempDir(), "Xauthority")
61+ if err := os.WriteFile(path, tc.file, 0o600); err != nil {
62+ t.Fatal(err)
63+ }
64+ got, err := readCookie(path, "arch", 7)
65+ if !errors.Is(err, tc.wantErr) || (tc.wantErr == nil && err != nil) || !bytes.Equal(got, tc.want) {
66+ t.Fatalf("got %q, %v; want %q, %v", got, err, tc.want, tc.wantErr)
67+ }
68+ })
69+ }
70+ if _, err := readCookie(filepath.Join(t.TempDir(), "missing"), "arch", 7); !errors.Is(err, os.ErrNotExist) {
71+ t.Fatal(err)
72+ }
73+}
74+
75+func TestXauthorityPath(t *testing.T) {
76+ t.Setenv("XAUTHORITY", "/x/auth")
77+ if got := xauthorityPath(); got != "/x/auth" {
78+ t.Fatal(got)
79+ }
80+ t.Setenv("XAUTHORITY", "")
81+ t.Setenv("HOME", "/home/u")
82+ if got := xauthorityPath(); got != "/home/u/.Xauthority" {
83+ t.Fatal(got)
84+ }
85+}
86+
87+func TestParseDisplay(t *testing.T) {
88+ for _, tc := range []struct {
89+ in string
90+ want int
91+ ok bool
92+ }{
93+ {":0", 0, true},
94+ {":12", 12, true},
95+ {":3.1", 3, true},
96+ {"unix:5", 5, true},
97+ {"unix:5.0", 5, true},
98+ {"", 0, false},
99+ {"0", 0, false},
100+ {":", 0, false},
101+ {":x", 0, false},
102+ {":1.", 0, false},
103+ {":1.x", 0, false},
104+ {":-1", 0, false},
105+ {"localhost:0", 0, false},
106+ {"host::0", 0, false},
107+ {"::1:0", 0, false},
108+ } {
109+ got, err := parseDisplay(tc.in)
110+ if (err == nil) != tc.ok || got != tc.want {
111+ t.Errorf("%q: %d, %v", tc.in, got, err)
112+ }
113+ }
114+}
115+
116+func TestDialNoServer(t *testing.T) {
117+ // Display numbers above any real server: both abstract and path fail.
118+ if _, err := Dial(":2147483000"); err == nil {
119+ t.Fatal("Dial succeeded without a server")
120+ }
121+ if _, err := Dial("tcp/host:0"); err == nil {
122+ t.Fatal("remote DISPLAY accepted")
123+ }
124+}
A errors.go +37 -0
@@ -0,0 +1,37 @@
1+package xturbo
2+
3+import "errors"
4+
5+var (
6+ // ErrMalformed reports bytes from the server or a local file that do not
7+ // follow the wire format (truncated, impossible lengths).
8+ ErrMalformed = errors.New("xturbo: malformed data")
9+ // ErrSetupFailed is wrapped by SetupError when the server refuses the
10+ // connection.
11+ ErrSetupFailed = errors.New("xturbo: connection setup failed")
12+)
13+
14+// Setup reply status bytes (xproto Setup*.status).
15+const (
16+ setupStatusFailed = 0
17+ setupStatusSuccess = 1
18+ setupStatusAuthenticate = 2
19+)
20+
21+// SetupError is the server's refusal of the connection setup: Status 0
22+// (Failed) or 2 (Authenticate), with the server's reason. It wraps
23+// ErrSetupFailed.
24+type SetupError struct {
25+ Status uint8
26+ Reason string
27+}
28+
29+func (e *SetupError) Error() string {
30+ kind := "failed"
31+ if e.Status == setupStatusAuthenticate {
32+ kind = "authenticate"
33+ }
34+ return "xturbo: connection setup " + kind + ": " + e.Reason
35+}
36+
37+func (e *SetupError) Unwrap() error { return ErrSetupFailed }
@@ -0,0 +1,37 @@
1+package xturbo
2+
3+import "errors"
4+
5+var (
6+ // ErrMalformed reports bytes from the server or a local file that do not
7+ // follow the wire format (truncated, impossible lengths).
8+ ErrMalformed = errors.New("xturbo: malformed data")
9+ // ErrSetupFailed is wrapped by SetupError when the server refuses the
10+ // connection.
11+ ErrSetupFailed = errors.New("xturbo: connection setup failed")
12+)
13+
14+// Setup reply status bytes (xproto Setup*.status).
15+const (
16+ setupStatusFailed = 0
17+ setupStatusSuccess = 1
18+ setupStatusAuthenticate = 2
19+)
20+
21+// SetupError is the server's refusal of the connection setup: Status 0
22+// (Failed) or 2 (Authenticate), with the server's reason. It wraps
23+// ErrSetupFailed.
24+type SetupError struct {
25+ Status uint8
26+ Reason string
27+}
28+
29+func (e *SetupError) Error() string {
30+ kind := "failed"
31+ if e.Status == setupStatusAuthenticate {
32+ kind = "authenticate"
33+ }
34+ return "xturbo: connection setup " + kind + ": " + e.Reason
35+}
36+
37+func (e *SetupError) Unwrap() error { return ErrSetupFailed }
M internal/xtest/capture_test.go +3 -1
@@ -101,7 +101,9 @@ func TestCaptureRequests(t *testing.T) {
101101 if err != nil {
102102 t.Fatal(err)
103103 }
104- defer r.Close()
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() })
105107 cmd := exec.Command("Xvfb", xvfbArgs()...)
106108 cmd.ExtraFiles = []*os.File{w}
107109 var log bytes.Buffer
@@ -101,7 +101,9 @@ func TestCaptureRequests(t *testing.T) {
101 if err != nil {101 if err != nil {
102 t.Fatal(err)102 t.Fatal(err)
103 }103 }
104- defer r.Close()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() })
105 cmd := exec.Command("Xvfb", xvfbArgs()...)107 cmd := exec.Command("Xvfb", xvfbArgs()...)
106 cmd.ExtraFiles = []*os.File{w}108 cmd.ExtraFiles = []*os.File{w}
107 var log bytes.Buffer109 var log bytes.Buffer
A setup.go +178 -0
@@ -0,0 +1,178 @@
1+package xturbo
2+
3+import (
4+ "encoding/binary"
5+ "fmt"
6+ "io"
7+ "os"
8+
9+ "golang.org/x/sys/unix"
10+)
11+
12+// Fixed layout of the xproto Setup reply (offsets from its status byte).
13+const (
14+ setupHeaderSize = 8 // status, pad/reason_len, protocol major, minor, length
15+ setupFixedSize = 40 // fixed part of a successful Setup
16+ screenFixedSize = 40 // fixed part of a SCREEN
17+ formatSize = 8 // FORMAT
18+ depthFixedSize = 8 // fixed part of a DEPTH
19+ visualSize = 24 // VISUALTYPE
20+ protocolMajor = 11
21+ protocolMinor = 0
22+)
23+
24+// setupInfo is what root keeps from a successful setup reply. Everything else
25+// is parsed by callers from Conn.SetupBytes.
26+type setupInfo struct {
27+ xidBase, xidMask uint32
28+ root uint32
29+}
30+
31+func malformed(what string) error { return fmt.Errorf("%w: %s", ErrMalformed, what) }
32+
33+// encodeSetupRequest builds the little-endian connection setup request.
34+func encodeSetupRequest(authName string, authData []byte) []byte {
35+ n, d := pad4(len(authName)), pad4(len(authData))
36+ p := make([]byte, 12+n+d)
37+ p[0] = 'l'
38+ binary.LittleEndian.PutUint16(p[2:], protocolMajor)
39+ binary.LittleEndian.PutUint16(p[4:], protocolMinor)
40+ binary.LittleEndian.PutUint16(p[6:], uint16(len(authName)))
41+ binary.LittleEndian.PutUint16(p[8:], uint16(len(authData)))
42+ copy(p[12:], authName)
43+ copy(p[12+n:], authData)
44+ return p
45+}
46+
47+func pad4(n int) int { return (n + 3) &^ 3 }
48+
49+// handshake runs the blocking setup exchange on fd and returns the full setup
50+// reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed.
51+func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) {
52+ if len(authName) > 0xffff || len(authData) > 0xffff {
53+ return nil, setupInfo{}, ErrBadLength
54+ }
55+ if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil {
56+ return nil, setupInfo{}, err
57+ }
58+ var hdr [setupHeaderSize]byte
59+ if err := readFull(fd, hdr[:]); err != nil {
60+ return nil, setupInfo{}, err
61+ }
62+ reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:])))
63+ copy(reply, hdr[:])
64+ if err := readFull(fd, reply[setupHeaderSize:]); err != nil {
65+ return nil, setupInfo{}, err
66+ }
67+ info, err := parseSetup(reply)
68+ if err != nil {
69+ return nil, setupInfo{}, err
70+ }
71+ return reply, info, nil
72+}
73+
74+// parseSetup validates a complete setup reply (header length already
75+// honoured) and extracts what root needs.
76+func parseSetup(p []byte) (setupInfo, error) {
77+ if len(p) < setupHeaderSize {
78+ return setupInfo{}, malformed("short setup reply")
79+ }
80+ switch p[0] {
81+ case setupStatusFailed:
82+ // SetupFailed: reason_len at byte 1, reason after the header.
83+ n := int(p[1])
84+ if setupHeaderSize+n > len(p) {
85+ return setupInfo{}, malformed("setup failure reason overflows reply")
86+ }
87+ return setupInfo{}, &SetupError{Status: setupStatusFailed, Reason: string(p[setupHeaderSize : setupHeaderSize+n])}
88+ case setupStatusAuthenticate:
89+ // SetupAuthenticate: reason fills length*4 bytes, NUL padded.
90+ reason := p[setupHeaderSize:]
91+ for len(reason) > 0 && reason[len(reason)-1] == 0 {
92+ reason = reason[:len(reason)-1]
93+ }
94+ return setupInfo{}, &SetupError{Status: setupStatusAuthenticate, Reason: string(reason)}
95+ case setupStatusSuccess:
96+ default:
97+ return setupInfo{}, malformed(fmt.Sprintf("setup status %d", p[0]))
98+ }
99+ if len(p) < setupFixedSize {
100+ return setupInfo{}, malformed("short setup reply")
101+ }
102+ if binary.LittleEndian.Uint16(p[2:]) != protocolMajor {
103+ return setupInfo{}, malformed(fmt.Sprintf("protocol major %d", binary.LittleEndian.Uint16(p[2:])))
104+ }
105+ info := setupInfo{
106+ xidBase: binary.LittleEndian.Uint32(p[12:]),
107+ xidMask: binary.LittleEndian.Uint32(p[16:]),
108+ }
109+ if info.xidMask == 0 || info.xidBase&info.xidMask != 0 {
110+ return setupInfo{}, malformed("resource id base/mask")
111+ }
112+ if p[28] == 0 {
113+ return setupInfo{}, malformed("setup has no screens")
114+ }
115+ off := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(p[24:]))) + formatSize*int(p[29])
116+ if off > len(p) {
117+ return setupInfo{}, malformed("vendor or pixmap formats overflow setup reply")
118+ }
119+ roots := int(p[28])
120+ for i := range roots {
121+ if len(p)-off < screenFixedSize {
122+ return setupInfo{}, malformed(fmt.Sprintf("screen %d overflows setup reply", i))
123+ }
124+ if i == 0 {
125+ info.root = binary.LittleEndian.Uint32(p[off:])
126+ }
127+ depths := int(p[off+39])
128+ off += screenFixedSize
129+ for range depths {
130+ if len(p)-off < depthFixedSize {
131+ return setupInfo{}, malformed(fmt.Sprintf("screen %d depth overflows setup reply", i))
132+ }
133+ visuals := visualSize * int(binary.LittleEndian.Uint16(p[off+2:]))
134+ off += depthFixedSize
135+ if len(p)-off < visuals {
136+ return setupInfo{}, malformed(fmt.Sprintf("screen %d visuals overflow setup reply", i))
137+ }
138+ off += visuals
139+ }
140+ }
141+ return info, nil
142+}
143+
144+func writeFull(fd int, p []byte) error {
145+ for len(p) > 0 {
146+ n, err := unix.Write(fd, p)
147+ switch {
148+ case err == unix.EINTR:
149+ continue
150+ case err == unix.EPIPE || err == unix.ECONNRESET:
151+ return ErrClosed
152+ case err != nil:
153+ return os.NewSyscallError("write", err)
154+ case n <= 0:
155+ return io.ErrShortWrite
156+ }
157+ p = p[n:]
158+ }
159+ return nil
160+}
161+
162+func readFull(fd int, p []byte) error {
163+ for len(p) > 0 {
164+ n, err := unix.Read(fd, p)
165+ switch {
166+ case err == unix.EINTR:
167+ continue
168+ case err == unix.ECONNRESET:
169+ return ErrClosed
170+ case err != nil:
171+ return os.NewSyscallError("read", err)
172+ case n == 0:
173+ return fmt.Errorf("%w: truncated setup reply: %w", ErrMalformed, io.ErrUnexpectedEOF)
174+ }
175+ p = p[n:]
176+ }
177+ return nil
178+}
@@ -0,0 +1,178 @@
1+package xturbo
2+
3+import (
4+ "encoding/binary"
5+ "fmt"
6+ "io"
7+ "os"
8+
9+ "golang.org/x/sys/unix"
10+)
11+
12+// Fixed layout of the xproto Setup reply (offsets from its status byte).
13+const (
14+ setupHeaderSize = 8 // status, pad/reason_len, protocol major, minor, length
15+ setupFixedSize = 40 // fixed part of a successful Setup
16+ screenFixedSize = 40 // fixed part of a SCREEN
17+ formatSize = 8 // FORMAT
18+ depthFixedSize = 8 // fixed part of a DEPTH
19+ visualSize = 24 // VISUALTYPE
20+ protocolMajor = 11
21+ protocolMinor = 0
22+)
23+
24+// setupInfo is what root keeps from a successful setup reply. Everything else
25+// is parsed by callers from Conn.SetupBytes.
26+type setupInfo struct {
27+ xidBase, xidMask uint32
28+ root uint32
29+}
30+
31+func malformed(what string) error { return fmt.Errorf("%w: %s", ErrMalformed, what) }
32+
33+// encodeSetupRequest builds the little-endian connection setup request.
34+func encodeSetupRequest(authName string, authData []byte) []byte {
35+ n, d := pad4(len(authName)), pad4(len(authData))
36+ p := make([]byte, 12+n+d)
37+ p[0] = 'l'
38+ binary.LittleEndian.PutUint16(p[2:], protocolMajor)
39+ binary.LittleEndian.PutUint16(p[4:], protocolMinor)
40+ binary.LittleEndian.PutUint16(p[6:], uint16(len(authName)))
41+ binary.LittleEndian.PutUint16(p[8:], uint16(len(authData)))
42+ copy(p[12:], authName)
43+ copy(p[12+n:], authData)
44+ return p
45+}
46+
47+func pad4(n int) int { return (n + 3) &^ 3 }
48+
49+// handshake runs the blocking setup exchange on fd and returns the full setup
50+// reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed.
51+func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) {
52+ if len(authName) > 0xffff || len(authData) > 0xffff {
53+ return nil, setupInfo{}, ErrBadLength
54+ }
55+ if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil {
56+ return nil, setupInfo{}, err
57+ }
58+ var hdr [setupHeaderSize]byte
59+ if err := readFull(fd, hdr[:]); err != nil {
60+ return nil, setupInfo{}, err
61+ }
62+ reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:])))
63+ copy(reply, hdr[:])
64+ if err := readFull(fd, reply[setupHeaderSize:]); err != nil {
65+ return nil, setupInfo{}, err
66+ }
67+ info, err := parseSetup(reply)
68+ if err != nil {
69+ return nil, setupInfo{}, err
70+ }
71+ return reply, info, nil
72+}
73+
74+// parseSetup validates a complete setup reply (header length already
75+// honoured) and extracts what root needs.
76+func parseSetup(p []byte) (setupInfo, error) {
77+ if len(p) < setupHeaderSize {
78+ return setupInfo{}, malformed("short setup reply")
79+ }
80+ switch p[0] {
81+ case setupStatusFailed:
82+ // SetupFailed: reason_len at byte 1, reason after the header.
83+ n := int(p[1])
84+ if setupHeaderSize+n > len(p) {
85+ return setupInfo{}, malformed("setup failure reason overflows reply")
86+ }
87+ return setupInfo{}, &SetupError{Status: setupStatusFailed, Reason: string(p[setupHeaderSize : setupHeaderSize+n])}
88+ case setupStatusAuthenticate:
89+ // SetupAuthenticate: reason fills length*4 bytes, NUL padded.
90+ reason := p[setupHeaderSize:]
91+ for len(reason) > 0 && reason[len(reason)-1] == 0 {
92+ reason = reason[:len(reason)-1]
93+ }
94+ return setupInfo{}, &SetupError{Status: setupStatusAuthenticate, Reason: string(reason)}
95+ case setupStatusSuccess:
96+ default:
97+ return setupInfo{}, malformed(fmt.Sprintf("setup status %d", p[0]))
98+ }
99+ if len(p) < setupFixedSize {
100+ return setupInfo{}, malformed("short setup reply")
101+ }
102+ if binary.LittleEndian.Uint16(p[2:]) != protocolMajor {
103+ return setupInfo{}, malformed(fmt.Sprintf("protocol major %d", binary.LittleEndian.Uint16(p[2:])))
104+ }
105+ info := setupInfo{
106+ xidBase: binary.LittleEndian.Uint32(p[12:]),
107+ xidMask: binary.LittleEndian.Uint32(p[16:]),
108+ }
109+ if info.xidMask == 0 || info.xidBase&info.xidMask != 0 {
110+ return setupInfo{}, malformed("resource id base/mask")
111+ }
112+ if p[28] == 0 {
113+ return setupInfo{}, malformed("setup has no screens")
114+ }
115+ off := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(p[24:]))) + formatSize*int(p[29])
116+ if off > len(p) {
117+ return setupInfo{}, malformed("vendor or pixmap formats overflow setup reply")
118+ }
119+ roots := int(p[28])
120+ for i := range roots {
121+ if len(p)-off < screenFixedSize {
122+ return setupInfo{}, malformed(fmt.Sprintf("screen %d overflows setup reply", i))
123+ }
124+ if i == 0 {
125+ info.root = binary.LittleEndian.Uint32(p[off:])
126+ }
127+ depths := int(p[off+39])
128+ off += screenFixedSize
129+ for range depths {
130+ if len(p)-off < depthFixedSize {
131+ return setupInfo{}, malformed(fmt.Sprintf("screen %d depth overflows setup reply", i))
132+ }
133+ visuals := visualSize * int(binary.LittleEndian.Uint16(p[off+2:]))
134+ off += depthFixedSize
135+ if len(p)-off < visuals {
136+ return setupInfo{}, malformed(fmt.Sprintf("screen %d visuals overflow setup reply", i))
137+ }
138+ off += visuals
139+ }
140+ }
141+ return info, nil
142+}
143+
144+func writeFull(fd int, p []byte) error {
145+ for len(p) > 0 {
146+ n, err := unix.Write(fd, p)
147+ switch {
148+ case err == unix.EINTR:
149+ continue
150+ case err == unix.EPIPE || err == unix.ECONNRESET:
151+ return ErrClosed
152+ case err != nil:
153+ return os.NewSyscallError("write", err)
154+ case n <= 0:
155+ return io.ErrShortWrite
156+ }
157+ p = p[n:]
158+ }
159+ return nil
160+}
161+
162+func readFull(fd int, p []byte) error {
163+ for len(p) > 0 {
164+ n, err := unix.Read(fd, p)
165+ switch {
166+ case err == unix.EINTR:
167+ continue
168+ case err == unix.ECONNRESET:
169+ return ErrClosed
170+ case err != nil:
171+ return os.NewSyscallError("read", err)
172+ case n == 0:
173+ return fmt.Errorf("%w: truncated setup reply: %w", ErrMalformed, io.ErrUnexpectedEOF)
174+ }
175+ p = p[n:]
176+ }
177+ return nil
178+}
A testdata/setup/Failed.setup.hex +5 -0
@@ -0,0 +1,5 @@
1+# SetupFailed reply (status 0) captured from Xvfb -auth <file> for a client
2+# sending no authorization.
3+00400b0000001000417574686f72697a6174696f6e2072657175697265642c20
4+627574206e6f20617574686f72697a6174696f6e2070726f746f636f6c207370
5+656369666965640a
@@ -0,0 +1,5 @@
1+# SetupFailed reply (status 0) captured from Xvfb -auth <file> for a client
2+# sending no authorization.
3+00400b0000001000417574686f72697a6174696f6e2072657175697265642c20
4+627574206e6f20617574686f72697a6174696f6e2070726f746f636f6c207370
5+656369666965640a
A testdata/setup/Success.setup.hex +214 -0
@@ -0,0 +1,214 @@
1+# Setup reply (status 1) captured from Xvfb -screen 0 800x600x24 -nolisten tcp -ac,
2+# little-endian client, protocol 11.0, no authorization. Raw socket capture.
3+# resource_id_base 0x00200000, mask 0x001fffff, first screen root 0x0000039f.
4+01000b0000008f06a1a5b80000002000ffff1f00000100001400ffff01060000
5+202008ff0000000054686520582e4f726720466f756e646174696f6e01012000
6+0000000004082000000000000808200000000000101020000000000018202000
7+0000000020202000000000009f03000020000000ffffff000000000000000000
8+20035802cb009800010001002100000001001806180000010000000021000000
9+040800010000ff0000ff0000ff0000000000000022000000050800010000ff00
10+00ff0000ff0000000000000091020000040800010000ff0000ff0000ff000000
11+0000000092020000040800010000ff0000ff0000ff0000000000000093020000
12+040800010000ff0000ff0000ff0000000000000094020000040800010000ff00
13+00ff0000ff0000000000000095020000040800010000ff0000ff0000ff000000
14+0000000096020000040800010000ff0000ff0000ff0000000000000097020000
15+040800010000ff0000ff0000ff0000000000000098020000040800010000ff00
16+00ff0000ff0000000000000099020000040800010000ff0000ff0000ff000000
17+000000009a020000040800010000ff0000ff0000ff000000000000009b020000
18+040800010000ff0000ff0000ff000000000000009c020000040800010000ff00
19+00ff0000ff000000000000009d020000040800010000ff0000ff0000ff000000
20+000000009e020000040800010000ff0000ff0000ff000000000000009f020000
21+040800010000ff0000ff0000ff00000000000000a0020000040800010000ff00
22+00ff0000ff00000000000000a1020000040800010000ff0000ff0000ff000000
23+00000000a2020000040800010000ff0000ff0000ff00000000000000a3020000
24+040800010000ff0000ff0000ff00000000000000a4020000040800010000ff00
25+00ff0000ff00000000000000a5020000040800010000ff0000ff0000ff000000
26+00000000a6020000040800010000ff0000ff0000ff00000000000000a7020000
27+040800010000ff0000ff0000ff00000000000000a8020000040800010000ff00
28+00ff0000ff00000000000000a9020000040800010000ff0000ff0000ff000000
29+00000000aa020000040800010000ff0000ff0000ff00000000000000ab020000
30+040800010000ff0000ff0000ff00000000000000ac020000040800010000ff00
31+00ff0000ff00000000000000ad020000040800010000ff0000ff0000ff000000
32+00000000ae020000040800010000ff0000ff0000ff00000000000000af020000
33+040800010000ff0000ff0000ff00000000000000b0020000040800010000ff00
34+00ff0000ff00000000000000b1020000040800010000ff0000ff0000ff000000
35+00000000b2020000040800010000ff0000ff0000ff00000000000000b3020000
36+040800010000ff0000ff0000ff00000000000000b4020000040800010000ff00
37+00ff0000ff00000000000000b5020000040800010000ff0000ff0000ff000000
38+00000000b6020000040800010000ff0000ff0000ff00000000000000b7020000
39+040800010000ff0000ff0000ff00000000000000b8020000040800010000ff00
40+00ff0000ff00000000000000b9020000040800010000ff0000ff0000ff000000
41+00000000ba020000040800010000ff0000ff0000ff00000000000000bb020000
42+040800010000ff0000ff0000ff00000000000000bc020000040800010000ff00
43+00ff0000ff00000000000000bd020000040800010000ff0000ff0000ff000000
44+00000000be020000040800010000ff0000ff0000ff00000000000000bf020000
45+040800010000ff0000ff0000ff00000000000000c0020000040800010000ff00
46+00ff0000ff00000000000000c1020000040800010000ff0000ff0000ff000000
47+00000000c2020000040800010000ff0000ff0000ff00000000000000c3020000
48+040800010000ff0000ff0000ff00000000000000c4020000040800010000ff00
49+00ff0000ff00000000000000c5020000040800010000ff0000ff0000ff000000
50+00000000c6020000040800010000ff0000ff0000ff00000000000000c7020000
51+040800010000ff0000ff0000ff00000000000000c8020000040800010000ff00
52+00ff0000ff00000000000000c9020000040800010000ff0000ff0000ff000000
53+00000000ca020000040800010000ff0000ff0000ff00000000000000cb020000
54+040800010000ff0000ff0000ff00000000000000cc020000040800010000ff00
55+00ff0000ff00000000000000cd020000040800010000ff0000ff0000ff000000
56+00000000ce020000040800010000ff0000ff0000ff00000000000000cf020000
57+040800010000ff0000ff0000ff00000000000000d0020000040800010000ff00
58+00ff0000ff00000000000000d1020000040800010000ff0000ff0000ff000000
59+00000000d2020000040800010000ff0000ff0000ff00000000000000d3020000
60+040800010000ff0000ff0000ff00000000000000d4020000040800010000ff00
61+00ff0000ff00000000000000d5020000040800010000ff0000ff0000ff000000
62+00000000d6020000040800010000ff0000ff0000ff00000000000000d7020000
63+040800010000ff0000ff0000ff00000000000000d8020000040800010000ff00
64+00ff0000ff00000000000000d9020000040800010000ff0000ff0000ff000000
65+00000000da020000040800010000ff0000ff0000ff00000000000000db020000
66+040800010000ff0000ff0000ff00000000000000dc020000040800010000ff00
67+00ff0000ff00000000000000dd020000040800010000ff0000ff0000ff000000
68+00000000de020000040800010000ff0000ff0000ff00000000000000df020000
69+040800010000ff0000ff0000ff00000000000000e0020000040800010000ff00
70+00ff0000ff00000000000000e1020000040800010000ff0000ff0000ff000000
71+00000000e2020000040800010000ff0000ff0000ff00000000000000e3020000
72+040800010000ff0000ff0000ff00000000000000e4020000040800010000ff00
73+00ff0000ff00000000000000e5020000040800010000ff0000ff0000ff000000
74+00000000e6020000040800010000ff0000ff0000ff00000000000000e7020000
75+040800010000ff0000ff0000ff00000000000000e8020000040800010000ff00
76+00ff0000ff00000000000000e9020000040800010000ff0000ff0000ff000000
77+00000000ea020000040800010000ff0000ff0000ff00000000000000eb020000
78+040800010000ff0000ff0000ff00000000000000ec020000040800010000ff00
79+00ff0000ff00000000000000ed020000040800010000ff0000ff0000ff000000
80+00000000ee020000040800010000ff0000ff0000ff00000000000000ef020000
81+040800010000ff0000ff0000ff00000000000000f0020000040800010000ff00
82+00ff0000ff00000000000000f1020000040800010000ff0000ff0000ff000000
83+00000000f2020000040800010000ff0000ff0000ff00000000000000f3020000
84+040800010000ff0000ff0000ff00000000000000f4020000040800010000ff00
85+00ff0000ff00000000000000f5020000040800010000ff0000ff0000ff000000
86+00000000f6020000040800010000ff0000ff0000ff00000000000000f7020000
87+040800010000ff0000ff0000ff00000000000000f8020000040800010000ff00
88+00ff0000ff00000000000000f9020000040800010000ff0000ff0000ff000000
89+00000000fa020000040800010000ff0000ff0000ff00000000000000fb020000
90+040800010000ff0000ff0000ff00000000000000fc020000040800010000ff00
91+00ff0000ff00000000000000fd020000040800010000ff0000ff0000ff000000
92+00000000fe020000040800010000ff0000ff0000ff00000000000000ff020000
93+040800010000ff0000ff0000ff0000000000000000030000040800010000ff00
94+00ff0000ff0000000000000001030000040800010000ff0000ff0000ff000000
95+0000000002030000040800010000ff0000ff0000ff0000000000000003030000
96+040800010000ff0000ff0000ff0000000000000004030000040800010000ff00
97+00ff0000ff0000000000000005030000040800010000ff0000ff0000ff000000
98+0000000006030000040800010000ff0000ff0000ff0000000000000007030000
99+040800010000ff0000ff0000ff0000000000000008030000040800010000ff00
100+00ff0000ff0000000000000009030000040800010000ff0000ff0000ff000000
101+000000000a030000040800010000ff0000ff0000ff000000000000000b030000
102+040800010000ff0000ff0000ff000000000000000c030000040800010000ff00
103+00ff0000ff000000000000000d030000040800010000ff0000ff0000ff000000
104+000000000e030000040800010000ff0000ff0000ff000000000000000f030000
105+040800010000ff0000ff0000ff0000000000000010030000050800010000ff00
106+00ff0000ff0000000000000011030000050800010000ff0000ff0000ff000000
107+0000000012030000050800010000ff0000ff0000ff0000000000000013030000
108+050800010000ff0000ff0000ff0000000000000014030000050800010000ff00
109+00ff0000ff0000000000000015030000050800010000ff0000ff0000ff000000
110+0000000016030000050800010000ff0000ff0000ff0000000000000017030000
111+050800010000ff0000ff0000ff0000000000000018030000050800010000ff00
112+00ff0000ff0000000000000019030000050800010000ff0000ff0000ff000000
113+000000001a030000050800010000ff0000ff0000ff000000000000001b030000
114+050800010000ff0000ff0000ff000000000000001c030000050800010000ff00
115+00ff0000ff000000000000001d030000050800010000ff0000ff0000ff000000
116+000000001e030000050800010000ff0000ff0000ff000000000000001f030000
117+050800010000ff0000ff0000ff0000000000000020030000050800010000ff00
118+00ff0000ff0000000000000021030000050800010000ff0000ff0000ff000000
119+0000000022030000050800010000ff0000ff0000ff0000000000000023030000
120+050800010000ff0000ff0000ff0000000000000024030000050800010000ff00
121+00ff0000ff0000000000000025030000050800010000ff0000ff0000ff000000
122+0000000026030000050800010000ff0000ff0000ff0000000000000027030000
123+050800010000ff0000ff0000ff0000000000000028030000050800010000ff00
124+00ff0000ff0000000000000029030000050800010000ff0000ff0000ff000000
125+000000002a030000050800010000ff0000ff0000ff000000000000002b030000
126+050800010000ff0000ff0000ff000000000000002c030000050800010000ff00
127+00ff0000ff000000000000002d030000050800010000ff0000ff0000ff000000
128+000000002e030000050800010000ff0000ff0000ff000000000000002f030000
129+050800010000ff0000ff0000ff0000000000000030030000050800010000ff00
130+00ff0000ff0000000000000031030000050800010000ff0000ff0000ff000000
131+0000000032030000050800010000ff0000ff0000ff0000000000000033030000
132+050800010000ff0000ff0000ff0000000000000034030000050800010000ff00
133+00ff0000ff0000000000000035030000050800010000ff0000ff0000ff000000
134+0000000036030000050800010000ff0000ff0000ff0000000000000037030000
135+050800010000ff0000ff0000ff0000000000000038030000050800010000ff00
136+00ff0000ff0000000000000039030000050800010000ff0000ff0000ff000000
137+000000003a030000050800010000ff0000ff0000ff000000000000003b030000
138+050800010000ff0000ff0000ff000000000000003c030000050800010000ff00
139+00ff0000ff000000000000003d030000050800010000ff0000ff0000ff000000
140+000000003e030000050800010000ff0000ff0000ff000000000000003f030000
141+050800010000ff0000ff0000ff0000000000000040030000050800010000ff00
142+00ff0000ff0000000000000041030000050800010000ff0000ff0000ff000000
143+0000000042030000050800010000ff0000ff0000ff0000000000000043030000
144+050800010000ff0000ff0000ff0000000000000044030000050800010000ff00
145+00ff0000ff0000000000000045030000050800010000ff0000ff0000ff000000
146+0000000046030000050800010000ff0000ff0000ff0000000000000047030000
147+050800010000ff0000ff0000ff0000000000000048030000050800010000ff00
148+00ff0000ff0000000000000049030000050800010000ff0000ff0000ff000000
149+000000004a030000050800010000ff0000ff0000ff000000000000004b030000
150+050800010000ff0000ff0000ff000000000000004c030000050800010000ff00
151+00ff0000ff000000000000004d030000050800010000ff0000ff0000ff000000
152+000000004e030000050800010000ff0000ff0000ff000000000000004f030000
153+050800010000ff0000ff0000ff0000000000000050030000050800010000ff00
154+00ff0000ff0000000000000051030000050800010000ff0000ff0000ff000000
155+0000000052030000050800010000ff0000ff0000ff0000000000000053030000
156+050800010000ff0000ff0000ff0000000000000054030000050800010000ff00
157+00ff0000ff0000000000000055030000050800010000ff0000ff0000ff000000
158+0000000056030000050800010000ff0000ff0000ff0000000000000057030000
159+050800010000ff0000ff0000ff0000000000000058030000050800010000ff00
160+00ff0000ff0000000000000059030000050800010000ff0000ff0000ff000000
161+000000005a030000050800010000ff0000ff0000ff000000000000005b030000
162+050800010000ff0000ff0000ff000000000000005c030000050800010000ff00
163+00ff0000ff000000000000005d030000050800010000ff0000ff0000ff000000
164+000000005e030000050800010000ff0000ff0000ff000000000000005f030000
165+050800010000ff0000ff0000ff0000000000000060030000050800010000ff00
166+00ff0000ff0000000000000061030000050800010000ff0000ff0000ff000000
167+0000000062030000050800010000ff0000ff0000ff0000000000000063030000
168+050800010000ff0000ff0000ff0000000000000064030000050800010000ff00
169+00ff0000ff0000000000000065030000050800010000ff0000ff0000ff000000
170+0000000066030000050800010000ff0000ff0000ff0000000000000067030000
171+050800010000ff0000ff0000ff0000000000000068030000050800010000ff00
172+00ff0000ff0000000000000069030000050800010000ff0000ff0000ff000000
173+000000006a030000050800010000ff0000ff0000ff000000000000006b030000
174+050800010000ff0000ff0000ff000000000000006c030000050800010000ff00
175+00ff0000ff000000000000006d030000050800010000ff0000ff0000ff000000
176+000000006e030000050800010000ff0000ff0000ff000000000000006f030000
177+050800010000ff0000ff0000ff0000000000000070030000050800010000ff00
178+00ff0000ff0000000000000071030000050800010000ff0000ff0000ff000000
179+0000000072030000050800010000ff0000ff0000ff0000000000000073030000
180+050800010000ff0000ff0000ff0000000000000074030000050800010000ff00
181+00ff0000ff0000000000000075030000050800010000ff0000ff0000ff000000
182+0000000076030000050800010000ff0000ff0000ff0000000000000077030000
183+050800010000ff0000ff0000ff0000000000000078030000050800010000ff00
184+00ff0000ff0000000000000079030000050800010000ff0000ff0000ff000000
185+000000007a030000050800010000ff0000ff0000ff000000000000007b030000
186+050800010000ff0000ff0000ff000000000000007c030000050800010000ff00
187+00ff0000ff000000000000007d030000050800010000ff0000ff0000ff000000
188+000000007e030000050800010000ff0000ff0000ff000000000000007f030000
189+050800010000ff0000ff0000ff0000000000000080030000050800010000ff00
190+00ff0000ff0000000000000081030000050800010000ff0000ff0000ff000000
191+0000000082030000050800010000ff0000ff0000ff0000000000000083030000
192+050800010000ff0000ff0000ff0000000000000084030000050800010000ff00
193+00ff0000ff0000000000000085030000050800010000ff0000ff0000ff000000
194+0000000086030000050800010000ff0000ff0000ff0000000000000087030000
195+050800010000ff0000ff0000ff0000000000000088030000050800010000ff00
196+00ff0000ff0000000000000089030000050800010000ff0000ff0000ff000000
197+000000008a030000050800010000ff0000ff0000ff000000000000008b030000
198+050800010000ff0000ff0000ff000000000000008c030000050800010000ff00
199+00ff0000ff000000000000008d030000050800010000ff0000ff0000ff000000
200+000000008e030000050800010000ff0000ff0000ff0000000000000001000000
201+0000000004000000000000000800000000000000100000000000000020001000
202+0000000040000000040800010000ff0000ff0000ff000000000000008f030000
203+040800010000ff0000ff0000ff0000000000000090030000040800010000ff00
204+00ff0000ff0000000000000091030000040800010000ff0000ff0000ff000000
205+0000000092030000040800010000ff0000ff0000ff0000000000000093030000
206+040800010000ff0000ff0000ff0000000000000094030000040800010000ff00
207+00ff0000ff0000000000000095030000040800010000ff0000ff0000ff000000
208+0000000096030000040800010000ff0000ff0000ff0000000000000097030000
209+040800010000ff0000ff0000ff0000000000000098030000040800010000ff00
210+00ff0000ff0000000000000099030000040800010000ff0000ff0000ff000000
211+000000009a030000040800010000ff0000ff0000ff000000000000009b030000
212+040800010000ff0000ff0000ff000000000000009c030000040800010000ff00
213+00ff0000ff000000000000009d030000040800010000ff0000ff0000ff000000
214+00000000
@@ -0,0 +1,214 @@
1+# Setup reply (status 1) captured from Xvfb -screen 0 800x600x24 -nolisten tcp -ac,
2+# little-endian client, protocol 11.0, no authorization. Raw socket capture.
3+# resource_id_base 0x00200000, mask 0x001fffff, first screen root 0x0000039f.
4+01000b0000008f06a1a5b80000002000ffff1f00000100001400ffff01060000
5+202008ff0000000054686520582e4f726720466f756e646174696f6e01012000
6+0000000004082000000000000808200000000000101020000000000018202000
7+0000000020202000000000009f03000020000000ffffff000000000000000000
8+20035802cb009800010001002100000001001806180000010000000021000000
9+040800010000ff0000ff0000ff0000000000000022000000050800010000ff00
10+00ff0000ff0000000000000091020000040800010000ff0000ff0000ff000000
11+0000000092020000040800010000ff0000ff0000ff0000000000000093020000
12+040800010000ff0000ff0000ff0000000000000094020000040800010000ff00
13+00ff0000ff0000000000000095020000040800010000ff0000ff0000ff000000
14+0000000096020000040800010000ff0000ff0000ff0000000000000097020000
15+040800010000ff0000ff0000ff0000000000000098020000040800010000ff00
16+00ff0000ff0000000000000099020000040800010000ff0000ff0000ff000000
17+000000009a020000040800010000ff0000ff0000ff000000000000009b020000
18+040800010000ff0000ff0000ff000000000000009c020000040800010000ff00
19+00ff0000ff000000000000009d020000040800010000ff0000ff0000ff000000
20+000000009e020000040800010000ff0000ff0000ff000000000000009f020000
21+040800010000ff0000ff0000ff00000000000000a0020000040800010000ff00
22+00ff0000ff00000000000000a1020000040800010000ff0000ff0000ff000000
23+00000000a2020000040800010000ff0000ff0000ff00000000000000a3020000
24+040800010000ff0000ff0000ff00000000000000a4020000040800010000ff00
25+00ff0000ff00000000000000a5020000040800010000ff0000ff0000ff000000
26+00000000a6020000040800010000ff0000ff0000ff00000000000000a7020000
27+040800010000ff0000ff0000ff00000000000000a8020000040800010000ff00
28+00ff0000ff00000000000000a9020000040800010000ff0000ff0000ff000000
29+00000000aa020000040800010000ff0000ff0000ff00000000000000ab020000
30+040800010000ff0000ff0000ff00000000000000ac020000040800010000ff00
31+00ff0000ff00000000000000ad020000040800010000ff0000ff0000ff000000
32+00000000ae020000040800010000ff0000ff0000ff00000000000000af020000
33+040800010000ff0000ff0000ff00000000000000b0020000040800010000ff00
34+00ff0000ff00000000000000b1020000040800010000ff0000ff0000ff000000
35+00000000b2020000040800010000ff0000ff0000ff00000000000000b3020000
36+040800010000ff0000ff0000ff00000000000000b4020000040800010000ff00
37+00ff0000ff00000000000000b5020000040800010000ff0000ff0000ff000000
38+00000000b6020000040800010000ff0000ff0000ff00000000000000b7020000
39+040800010000ff0000ff0000ff00000000000000b8020000040800010000ff00
40+00ff0000ff00000000000000b9020000040800010000ff0000ff0000ff000000
41+00000000ba020000040800010000ff0000ff0000ff00000000000000bb020000
42+040800010000ff0000ff0000ff00000000000000bc020000040800010000ff00
43+00ff0000ff00000000000000bd020000040800010000ff0000ff0000ff000000
44+00000000be020000040800010000ff0000ff0000ff00000000000000bf020000
45+040800010000ff0000ff0000ff00000000000000c0020000040800010000ff00
46+00ff0000ff00000000000000c1020000040800010000ff0000ff0000ff000000
47+00000000c2020000040800010000ff0000ff0000ff00000000000000c3020000
48+040800010000ff0000ff0000ff00000000000000c4020000040800010000ff00
49+00ff0000ff00000000000000c5020000040800010000ff0000ff0000ff000000
50+00000000c6020000040800010000ff0000ff0000ff00000000000000c7020000
51+040800010000ff0000ff0000ff00000000000000c8020000040800010000ff00
52+00ff0000ff00000000000000c9020000040800010000ff0000ff0000ff000000
53+00000000ca020000040800010000ff0000ff0000ff00000000000000cb020000
54+040800010000ff0000ff0000ff00000000000000cc020000040800010000ff00
55+00ff0000ff00000000000000cd020000040800010000ff0000ff0000ff000000
56+00000000ce020000040800010000ff0000ff0000ff00000000000000cf020000
57+040800010000ff0000ff0000ff00000000000000d0020000040800010000ff00
58+00ff0000ff00000000000000d1020000040800010000ff0000ff0000ff000000
59+00000000d2020000040800010000ff0000ff0000ff00000000000000d3020000
60+040800010000ff0000ff0000ff00000000000000d4020000040800010000ff00
61+00ff0000ff00000000000000d5020000040800010000ff0000ff0000ff000000
62+00000000d6020000040800010000ff0000ff0000ff00000000000000d7020000
63+040800010000ff0000ff0000ff00000000000000d8020000040800010000ff00
64+00ff0000ff00000000000000d9020000040800010000ff0000ff0000ff000000
65+00000000da020000040800010000ff0000ff0000ff00000000000000db020000
66+040800010000ff0000ff0000ff00000000000000dc020000040800010000ff00
67+00ff0000ff00000000000000dd020000040800010000ff0000ff0000ff000000
68+00000000de020000040800010000ff0000ff0000ff00000000000000df020000
69+040800010000ff0000ff0000ff00000000000000e0020000040800010000ff00
70+00ff0000ff00000000000000e1020000040800010000ff0000ff0000ff000000
71+00000000e2020000040800010000ff0000ff0000ff00000000000000e3020000
72+040800010000ff0000ff0000ff00000000000000e4020000040800010000ff00
73+00ff0000ff00000000000000e5020000040800010000ff0000ff0000ff000000
74+00000000e6020000040800010000ff0000ff0000ff00000000000000e7020000
75+040800010000ff0000ff0000ff00000000000000e8020000040800010000ff00
76+00ff0000ff00000000000000e9020000040800010000ff0000ff0000ff000000
77+00000000ea020000040800010000ff0000ff0000ff00000000000000eb020000
78+040800010000ff0000ff0000ff00000000000000ec020000040800010000ff00
79+00ff0000ff00000000000000ed020000040800010000ff0000ff0000ff000000
80+00000000ee020000040800010000ff0000ff0000ff00000000000000ef020000
81+040800010000ff0000ff0000ff00000000000000f0020000040800010000ff00
82+00ff0000ff00000000000000f1020000040800010000ff0000ff0000ff000000
83+00000000f2020000040800010000ff0000ff0000ff00000000000000f3020000
84+040800010000ff0000ff0000ff00000000000000f4020000040800010000ff00
85+00ff0000ff00000000000000f5020000040800010000ff0000ff0000ff000000
86+00000000f6020000040800010000ff0000ff0000ff00000000000000f7020000
87+040800010000ff0000ff0000ff00000000000000f8020000040800010000ff00
88+00ff0000ff00000000000000f9020000040800010000ff0000ff0000ff000000
89+00000000fa020000040800010000ff0000ff0000ff00000000000000fb020000
90+040800010000ff0000ff0000ff00000000000000fc020000040800010000ff00
91+00ff0000ff00000000000000fd020000040800010000ff0000ff0000ff000000
92+00000000fe020000040800010000ff0000ff0000ff00000000000000ff020000
93+040800010000ff0000ff0000ff0000000000000000030000040800010000ff00
94+00ff0000ff0000000000000001030000040800010000ff0000ff0000ff000000
95+0000000002030000040800010000ff0000ff0000ff0000000000000003030000
96+040800010000ff0000ff0000ff0000000000000004030000040800010000ff00
97+00ff0000ff0000000000000005030000040800010000ff0000ff0000ff000000
98+0000000006030000040800010000ff0000ff0000ff0000000000000007030000
99+040800010000ff0000ff0000ff0000000000000008030000040800010000ff00
100+00ff0000ff0000000000000009030000040800010000ff0000ff0000ff000000
101+000000000a030000040800010000ff0000ff0000ff000000000000000b030000
102+040800010000ff0000ff0000ff000000000000000c030000040800010000ff00
103+00ff0000ff000000000000000d030000040800010000ff0000ff0000ff000000
104+000000000e030000040800010000ff0000ff0000ff000000000000000f030000
105+040800010000ff0000ff0000ff0000000000000010030000050800010000ff00
106+00ff0000ff0000000000000011030000050800010000ff0000ff0000ff000000
107+0000000012030000050800010000ff0000ff0000ff0000000000000013030000
108+050800010000ff0000ff0000ff0000000000000014030000050800010000ff00
109+00ff0000ff0000000000000015030000050800010000ff0000ff0000ff000000
110+0000000016030000050800010000ff0000ff0000ff0000000000000017030000
111+050800010000ff0000ff0000ff0000000000000018030000050800010000ff00
112+00ff0000ff0000000000000019030000050800010000ff0000ff0000ff000000
113+000000001a030000050800010000ff0000ff0000ff000000000000001b030000
114+050800010000ff0000ff0000ff000000000000001c030000050800010000ff00
115+00ff0000ff000000000000001d030000050800010000ff0000ff0000ff000000
116+000000001e030000050800010000ff0000ff0000ff000000000000001f030000
117+050800010000ff0000ff0000ff0000000000000020030000050800010000ff00
118+00ff0000ff0000000000000021030000050800010000ff0000ff0000ff000000
119+0000000022030000050800010000ff0000ff0000ff0000000000000023030000
120+050800010000ff0000ff0000ff0000000000000024030000050800010000ff00
121+00ff0000ff0000000000000025030000050800010000ff0000ff0000ff000000
122+0000000026030000050800010000ff0000ff0000ff0000000000000027030000
123+050800010000ff0000ff0000ff0000000000000028030000050800010000ff00
124+00ff0000ff0000000000000029030000050800010000ff0000ff0000ff000000
125+000000002a030000050800010000ff0000ff0000ff000000000000002b030000
126+050800010000ff0000ff0000ff000000000000002c030000050800010000ff00
127+00ff0000ff000000000000002d030000050800010000ff0000ff0000ff000000
128+000000002e030000050800010000ff0000ff0000ff000000000000002f030000
129+050800010000ff0000ff0000ff0000000000000030030000050800010000ff00
130+00ff0000ff0000000000000031030000050800010000ff0000ff0000ff000000
131+0000000032030000050800010000ff0000ff0000ff0000000000000033030000
132+050800010000ff0000ff0000ff0000000000000034030000050800010000ff00
133+00ff0000ff0000000000000035030000050800010000ff0000ff0000ff000000
134+0000000036030000050800010000ff0000ff0000ff0000000000000037030000
135+050800010000ff0000ff0000ff0000000000000038030000050800010000ff00
136+00ff0000ff0000000000000039030000050800010000ff0000ff0000ff000000
137+000000003a030000050800010000ff0000ff0000ff000000000000003b030000
138+050800010000ff0000ff0000ff000000000000003c030000050800010000ff00
139+00ff0000ff000000000000003d030000050800010000ff0000ff0000ff000000
140+000000003e030000050800010000ff0000ff0000ff000000000000003f030000
141+050800010000ff0000ff0000ff0000000000000040030000050800010000ff00
142+00ff0000ff0000000000000041030000050800010000ff0000ff0000ff000000
143+0000000042030000050800010000ff0000ff0000ff0000000000000043030000
144+050800010000ff0000ff0000ff0000000000000044030000050800010000ff00
145+00ff0000ff0000000000000045030000050800010000ff0000ff0000ff000000
146+0000000046030000050800010000ff0000ff0000ff0000000000000047030000
147+050800010000ff0000ff0000ff0000000000000048030000050800010000ff00
148+00ff0000ff0000000000000049030000050800010000ff0000ff0000ff000000
149+000000004a030000050800010000ff0000ff0000ff000000000000004b030000
150+050800010000ff0000ff0000ff000000000000004c030000050800010000ff00
151+00ff0000ff000000000000004d030000050800010000ff0000ff0000ff000000
152+000000004e030000050800010000ff0000ff0000ff000000000000004f030000
153+050800010000ff0000ff0000ff0000000000000050030000050800010000ff00
154+00ff0000ff0000000000000051030000050800010000ff0000ff0000ff000000
155+0000000052030000050800010000ff0000ff0000ff0000000000000053030000
156+050800010000ff0000ff0000ff0000000000000054030000050800010000ff00
157+00ff0000ff0000000000000055030000050800010000ff0000ff0000ff000000
158+0000000056030000050800010000ff0000ff0000ff0000000000000057030000
159+050800010000ff0000ff0000ff0000000000000058030000050800010000ff00
160+00ff0000ff0000000000000059030000050800010000ff0000ff0000ff000000
161+000000005a030000050800010000ff0000ff0000ff000000000000005b030000
162+050800010000ff0000ff0000ff000000000000005c030000050800010000ff00
163+00ff0000ff000000000000005d030000050800010000ff0000ff0000ff000000
164+000000005e030000050800010000ff0000ff0000ff000000000000005f030000
165+050800010000ff0000ff0000ff0000000000000060030000050800010000ff00
166+00ff0000ff0000000000000061030000050800010000ff0000ff0000ff000000
167+0000000062030000050800010000ff0000ff0000ff0000000000000063030000
168+050800010000ff0000ff0000ff0000000000000064030000050800010000ff00
169+00ff0000ff0000000000000065030000050800010000ff0000ff0000ff000000
170+0000000066030000050800010000ff0000ff0000ff0000000000000067030000
171+050800010000ff0000ff0000ff0000000000000068030000050800010000ff00
172+00ff0000ff0000000000000069030000050800010000ff0000ff0000ff000000
173+000000006a030000050800010000ff0000ff0000ff000000000000006b030000
174+050800010000ff0000ff0000ff000000000000006c030000050800010000ff00
175+00ff0000ff000000000000006d030000050800010000ff0000ff0000ff000000
176+000000006e030000050800010000ff0000ff0000ff000000000000006f030000
177+050800010000ff0000ff0000ff0000000000000070030000050800010000ff00
178+00ff0000ff0000000000000071030000050800010000ff0000ff0000ff000000
179+0000000072030000050800010000ff0000ff0000ff0000000000000073030000
180+050800010000ff0000ff0000ff0000000000000074030000050800010000ff00
181+00ff0000ff0000000000000075030000050800010000ff0000ff0000ff000000
182+0000000076030000050800010000ff0000ff0000ff0000000000000077030000
183+050800010000ff0000ff0000ff0000000000000078030000050800010000ff00
184+00ff0000ff0000000000000079030000050800010000ff0000ff0000ff000000
185+000000007a030000050800010000ff0000ff0000ff000000000000007b030000
186+050800010000ff0000ff0000ff000000000000007c030000050800010000ff00
187+00ff0000ff000000000000007d030000050800010000ff0000ff0000ff000000
188+000000007e030000050800010000ff0000ff0000ff000000000000007f030000
189+050800010000ff0000ff0000ff0000000000000080030000050800010000ff00
190+00ff0000ff0000000000000081030000050800010000ff0000ff0000ff000000
191+0000000082030000050800010000ff0000ff0000ff0000000000000083030000
192+050800010000ff0000ff0000ff0000000000000084030000050800010000ff00
193+00ff0000ff0000000000000085030000050800010000ff0000ff0000ff000000
194+0000000086030000050800010000ff0000ff0000ff0000000000000087030000
195+050800010000ff0000ff0000ff0000000000000088030000050800010000ff00
196+00ff0000ff0000000000000089030000050800010000ff0000ff0000ff000000
197+000000008a030000050800010000ff0000ff0000ff000000000000008b030000
198+050800010000ff0000ff0000ff000000000000008c030000050800010000ff00
199+00ff0000ff000000000000008d030000050800010000ff0000ff0000ff000000
200+000000008e030000050800010000ff0000ff0000ff0000000000000001000000
201+0000000004000000000000000800000000000000100000000000000020001000
202+0000000040000000040800010000ff0000ff0000ff000000000000008f030000
203+040800010000ff0000ff0000ff0000000000000090030000040800010000ff00
204+00ff0000ff0000000000000091030000040800010000ff0000ff0000ff000000
205+0000000092030000040800010000ff0000ff0000ff0000000000000093030000
206+040800010000ff0000ff0000ff0000000000000094030000040800010000ff00
207+00ff0000ff0000000000000095030000040800010000ff0000ff0000ff000000
208+0000000096030000040800010000ff0000ff0000ff0000000000000097030000
209+040800010000ff0000ff0000ff0000000000000098030000040800010000ff00
210+00ff0000ff0000000000000099030000040800010000ff0000ff0000ff000000
211+000000009a030000040800010000ff0000ff0000ff000000000000009b030000
212+040800010000ff0000ff0000ff000000000000009c030000040800010000ff00
213+00ff0000ff000000000000009d030000040800010000ff0000ff0000ff000000
214+00000000

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
esc
close a dialog, leave a field

Branches 1

Tags 1

↑↓ moveenter openesc close