detached 106b42b

feat(runtime): add Conn setup handshake, Dial and Xauthority cookies

NewConn takes ownership of a connected stream socket, sends a
little-endian protocol 11.0 setup request, reads the full setup reply
while blocking and then switches the fd to O_NONBLOCK. Root keeps the
reply (SetupBytes), the first screen root and the resource-id base/mask,
parsing the fixed fields itself without importing xproto.

Failed (0) and Authenticate (2) replies become *SetupError wrapping
ErrSetupFailed; truncated or inconsistent replies wrap ErrMalformed. The
fd is closed when setup fails.

Dial accepts local DISPLAY strings (:N, :N.S, unix:N), connects to the
abstract socket and then /tmp/.X11-unix/XN, and sends MIT-MAGIC-COOKIE-1
from $XAUTHORITY or ~/.Xauthority (FamilyLocal + hostname or FamilyWild,
matching or empty display number).

Conn also exposes Close, Fd, Buf, Ext (empty ExtTable bound to Buf.Ext)
and Flush. Tests drive socketpair servers with recorded Xvfb setup
replies, table-test Xauthority parsing, and under the xserver tag dial a
cookie-protected Xvfb and round-trip GetInputFocus.
commit
106b42bd82362ea4f87c88c8fbea10d865c35d86
author
brice · · 2026-10-22 08:17 UTC
committer
brice · · 2026-10-22 08:47 UTC
parent
722264a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +1189 -0
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 +244 -0
@@ -0,0 +1,244 @@
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+ // Header claims 8 bytes of body, failure reason claims 20.
165+ reasonPast := []byte{0, 20, 11, 0, 0, 0, 2, 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
166+ // Success header whose length is shorter than the fixed Setup.
167+ shortSuccess := []byte{1, 0, 11, 0, 0, 0, 1, 0, 0, 0, 0, 0}
168+ for _, tc := range []struct {
169+ name string
170+ reply []byte
171+ }{
172+ {"empty", nil},
173+ {"truncated header", setup[:5]},
174+ {"truncated body", setup[:len(setup)-3]},
175+ {"no screens", noScreens},
176+ {"bad xid mask", badMask},
177+ {"protocol major", badVersion},
178+ {"unknown status", badStatus},
179+ {"screen past end", screenPast},
180+ {"reason past end", reasonPast},
181+ {"short success", shortSuccess},
182+ } {
183+ t.Run(tc.name, func(t *testing.T) {
184+ fd, _ := scriptedServer(t, tc.reply, 3, true)
185+ c, err := NewConn(fd)
186+ if c != nil || !errors.Is(err, ErrMalformed) || errors.Is(err, ErrSetupFailed) {
187+ t.Fatalf("conn %t, err %v", c != nil, err)
188+ }
189+ })
190+ }
191+}
192+
193+func TestConnFlushClose(t *testing.T) {
194+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
195+ if err != nil {
196+ t.Fatal(err)
197+ }
198+ server := os.NewFile(uintptr(fds[1]), "server")
199+ defer server.Close()
200+ setup := loadSetup(t, "Success")
201+ go func() {
202+ var req [12]byte
203+ _, _ = io.ReadFull(server, req[:])
204+ _, _ = server.Write(setup)
205+ }()
206+ c, err := NewConn(fds[0])
207+ if err != nil {
208+ t.Fatal(err)
209+ }
210+ p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus
211+ if p == nil || seq != 1 {
212+ t.Fatal("request")
213+ }
214+ if err := c.Flush(); err != nil {
215+ t.Fatal(err)
216+ }
217+ var got [4]byte
218+ if _, err := io.ReadFull(server, got[:]); err != nil || got != [4]byte{43, 0, 1, 0} {
219+ t.Fatalf("%x %v", got, err)
220+ }
221+ if err := c.Close(); err != nil {
222+ t.Fatal(err)
223+ }
224+ if c.Fd() != -1 || !errors.Is(c.Close(), ErrClosed) || !errors.Is(c.Flush(), ErrClosed) {
225+ t.Fatal("after Close")
226+ }
227+ if p, _ := c.Buf().Request(43, 0, 0); p != nil {
228+ t.Fatal("Buf usable after Close")
229+ }
230+}
231+
232+func TestSetupErrorMessage(t *testing.T) {
233+ for _, tc := range []struct {
234+ e SetupError
235+ want string
236+ }{
237+ {SetupError{0, "no"}, "xturbo: connection setup failed: no"},
238+ {SetupError{2, "why"}, "xturbo: connection setup authenticate: why"},
239+ } {
240+ if got := tc.e.Error(); got != tc.want {
241+ t.Errorf("%q, want %q", got, tc.want)
242+ }
243+ }
244+}
@@ -0,0 +1,244 @@
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+ // Header claims 8 bytes of body, failure reason claims 20.
165+ reasonPast := []byte{0, 20, 11, 0, 0, 0, 2, 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
166+ // Success header whose length is shorter than the fixed Setup.
167+ shortSuccess := []byte{1, 0, 11, 0, 0, 0, 1, 0, 0, 0, 0, 0}
168+ for _, tc := range []struct {
169+ name string
170+ reply []byte
171+ }{
172+ {"empty", nil},
173+ {"truncated header", setup[:5]},
174+ {"truncated body", setup[:len(setup)-3]},
175+ {"no screens", noScreens},
176+ {"bad xid mask", badMask},
177+ {"protocol major", badVersion},
178+ {"unknown status", badStatus},
179+ {"screen past end", screenPast},
180+ {"reason past end", reasonPast},
181+ {"short success", shortSuccess},
182+ } {
183+ t.Run(tc.name, func(t *testing.T) {
184+ fd, _ := scriptedServer(t, tc.reply, 3, true)
185+ c, err := NewConn(fd)
186+ if c != nil || !errors.Is(err, ErrMalformed) || errors.Is(err, ErrSetupFailed) {
187+ t.Fatalf("conn %t, err %v", c != nil, err)
188+ }
189+ })
190+ }
191+}
192+
193+func TestConnFlushClose(t *testing.T) {
194+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
195+ if err != nil {
196+ t.Fatal(err)
197+ }
198+ server := os.NewFile(uintptr(fds[1]), "server")
199+ defer server.Close()
200+ setup := loadSetup(t, "Success")
201+ go func() {
202+ var req [12]byte
203+ _, _ = io.ReadFull(server, req[:])
204+ _, _ = server.Write(setup)
205+ }()
206+ c, err := NewConn(fds[0])
207+ if err != nil {
208+ t.Fatal(err)
209+ }
210+ p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus
211+ if p == nil || seq != 1 {
212+ t.Fatal("request")
213+ }
214+ if err := c.Flush(); err != nil {
215+ t.Fatal(err)
216+ }
217+ var got [4]byte
218+ if _, err := io.ReadFull(server, got[:]); err != nil || got != [4]byte{43, 0, 1, 0} {
219+ t.Fatalf("%x %v", got, err)
220+ }
221+ if err := c.Close(); err != nil {
222+ t.Fatal(err)
223+ }
224+ if c.Fd() != -1 || !errors.Is(c.Close(), ErrClosed) || !errors.Is(c.Flush(), ErrClosed) {
225+ t.Fatal("after Close")
226+ }
227+ if p, _ := c.Buf().Request(43, 0, 0); p != nil {
228+ t.Fatal("Buf usable after Close")
229+ }
230+}
231+
232+func TestSetupErrorMessage(t *testing.T) {
233+ for _, tc := range []struct {
234+ e SetupError
235+ want string
236+ }{
237+ {SetupError{0, "no"}, "xturbo: connection setup failed: no"},
238+ {SetupError{2, "why"}, "xturbo: connection setup authenticate: why"},
239+ } {
240+ if got := tc.e.Error(); got != tc.want {
241+ t.Errorf("%q, want %q", got, tc.want)
242+ }
243+ }
244+}
A conn_xserver_test.go +158 -0
@@ -0,0 +1,158 @@
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+ defer r.Close()
35+ cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-auth", authFile)
36+ cmd.ExtraFiles = []*os.File{w}
37+ var log bytes.Buffer
38+ cmd.Stderr = &log
39+ if err := cmd.Start(); err != nil {
40+ w.Close()
41+ t.Fatal(err)
42+ }
43+ w.Close()
44+ t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() })
45+ _ = r.SetReadDeadline(time.Now().Add(10 * time.Second))
46+ var buf [32]byte
47+ n, err := r.Read(buf[:])
48+ if err != nil {
49+ t.Fatalf("Xvfb: %v: %s", err, log.String())
50+ }
51+ num := strings.TrimSpace(string(buf[:n]))
52+ if _, err := strconv.Atoi(num); err != nil {
53+ t.Fatalf("Xvfb display %q", num)
54+ }
55+ return ":" + num
56+}
57+
58+// writeXauthority writes one FamilyLocal entry for this host, any display.
59+func writeXauthority(t *testing.T, path string, cookie []byte) {
60+ t.Helper()
61+ host, err := os.Hostname()
62+ if err != nil {
63+ t.Fatal(err)
64+ }
65+ var b []byte
66+ field := func(s []byte) {
67+ b = binary.BigEndian.AppendUint16(b, uint16(len(s)))
68+ b = append(b, s...)
69+ }
70+ b = binary.BigEndian.AppendUint16(b, 256) // FamilyLocal
71+ field([]byte(host))
72+ field(nil) // any display number
73+ field([]byte("MIT-MAGIC-COOKIE-1"))
74+ field(cookie)
75+ if err := os.WriteFile(path, b, 0o600); err != nil {
76+ t.Fatal(err)
77+ }
78+}
79+
80+func TestXserverDial(t *testing.T) {
81+ dir := t.TempDir()
82+ auth := filepath.Join(dir, "Xauthority")
83+ cookie := make([]byte, 16)
84+ _, _ = rand.Read(cookie)
85+ writeXauthority(t, auth, cookie)
86+ display := startXvfb(t, auth)
87+
88+ // Without a cookie the server refuses the setup.
89+ t.Setenv("XAUTHORITY", filepath.Join(dir, "missing"))
90+ if _, err := xturbo.Dial(display); !errors.Is(err, xturbo.ErrSetupFailed) {
91+ t.Fatalf("unauthenticated Dial: %v", err)
92+ }
93+
94+ t.Setenv("XAUTHORITY", auth)
95+ c, err := xturbo.Dial(display + ".0")
96+ if err != nil {
97+ t.Fatal(err)
98+ }
99+ defer c.Close()
100+
101+ // Compare root's own parsing against the generated Setup view instead of
102+ // server-assigned constants (they differ between X server builds).
103+ setup := c.SetupBytes()
104+ if c.Root() == 0 || len(setup) < xproto.SetupSize || len(setup) != 8+4*int(binary.LittleEndian.Uint16(setup[6:])) {
105+ t.Fatalf("root %#x, setup %d bytes", c.Root(), len(setup))
106+ }
107+ it := xproto.NewSetupIter(setup, 1)
108+ if !it.Next() {
109+ t.Fatal("setup does not parse")
110+ }
111+ sv := it.Value()
112+ if sv.Size() != len(setup) || sv.Status() != 1 || sv.ProtocolMajorVersion() != 11 {
113+ t.Fatalf("setup size %d status %d", sv.Size(), sv.Status())
114+ }
115+ screens := sv.Roots()
116+ if !screens.Next() || uint32(screens.Value().Root()) != c.Root() {
117+ t.Fatalf("Root %#x, setup view says otherwise", c.Root())
118+ }
119+
120+ // A trivial round trip: GetInputFocus, then read its reply.
121+ seq := xproto.GetInputFocus(c.Buf())
122+ if seq != 1 {
123+ t.Fatalf("seq %d", seq)
124+ }
125+ if err := c.Flush(); err != nil {
126+ t.Fatal(err)
127+ }
128+ reply := readPacket(t, c.Fd())
129+ if reply[0] != 1 || binary.LittleEndian.Uint16(reply[2:]) != uint16(seq) {
130+ t.Fatalf("reply %x", reply)
131+ }
132+}
133+
134+// readPacket polls the non-blocking fd and reads one 32-byte packet.
135+func readPacket(t *testing.T, fd int) [32]byte {
136+ t.Helper()
137+ var p [32]byte
138+ got := 0
139+ deadline := time.Now().Add(5 * time.Second)
140+ for got < len(p) {
141+ if time.Now().After(deadline) {
142+ t.Fatal("timeout reading reply")
143+ }
144+ fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
145+ if _, err := unix.Poll(fds, 1000); err != nil && err != unix.EINTR {
146+ t.Fatal(err)
147+ }
148+ n, err := unix.Read(fd, p[got:])
149+ if err == unix.EAGAIN || err == unix.EINTR {
150+ continue
151+ }
152+ if err != nil || n == 0 {
153+ t.Fatalf("read: %d %v", n, err)
154+ }
155+ got += n
156+ }
157+ return p
158+}
@@ -0,0 +1,158 @@
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+ defer r.Close()
35+ cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-auth", authFile)
36+ cmd.ExtraFiles = []*os.File{w}
37+ var log bytes.Buffer
38+ cmd.Stderr = &log
39+ if err := cmd.Start(); err != nil {
40+ w.Close()
41+ t.Fatal(err)
42+ }
43+ w.Close()
44+ t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() })
45+ _ = r.SetReadDeadline(time.Now().Add(10 * time.Second))
46+ var buf [32]byte
47+ n, err := r.Read(buf[:])
48+ if err != nil {
49+ t.Fatalf("Xvfb: %v: %s", err, log.String())
50+ }
51+ num := strings.TrimSpace(string(buf[:n]))
52+ if _, err := strconv.Atoi(num); err != nil {
53+ t.Fatalf("Xvfb display %q", num)
54+ }
55+ return ":" + num
56+}
57+
58+// writeXauthority writes one FamilyLocal entry for this host, any display.
59+func writeXauthority(t *testing.T, path string, cookie []byte) {
60+ t.Helper()
61+ host, err := os.Hostname()
62+ if err != nil {
63+ t.Fatal(err)
64+ }
65+ var b []byte
66+ field := func(s []byte) {
67+ b = binary.BigEndian.AppendUint16(b, uint16(len(s)))
68+ b = append(b, s...)
69+ }
70+ b = binary.BigEndian.AppendUint16(b, 256) // FamilyLocal
71+ field([]byte(host))
72+ field(nil) // any display number
73+ field([]byte("MIT-MAGIC-COOKIE-1"))
74+ field(cookie)
75+ if err := os.WriteFile(path, b, 0o600); err != nil {
76+ t.Fatal(err)
77+ }
78+}
79+
80+func TestXserverDial(t *testing.T) {
81+ dir := t.TempDir()
82+ auth := filepath.Join(dir, "Xauthority")
83+ cookie := make([]byte, 16)
84+ _, _ = rand.Read(cookie)
85+ writeXauthority(t, auth, cookie)
86+ display := startXvfb(t, auth)
87+
88+ // Without a cookie the server refuses the setup.
89+ t.Setenv("XAUTHORITY", filepath.Join(dir, "missing"))
90+ if _, err := xturbo.Dial(display); !errors.Is(err, xturbo.ErrSetupFailed) {
91+ t.Fatalf("unauthenticated Dial: %v", err)
92+ }
93+
94+ t.Setenv("XAUTHORITY", auth)
95+ c, err := xturbo.Dial(display + ".0")
96+ if err != nil {
97+ t.Fatal(err)
98+ }
99+ defer c.Close()
100+
101+ // Compare root's own parsing against the generated Setup view instead of
102+ // server-assigned constants (they differ between X server builds).
103+ setup := c.SetupBytes()
104+ if c.Root() == 0 || len(setup) < xproto.SetupSize || len(setup) != 8+4*int(binary.LittleEndian.Uint16(setup[6:])) {
105+ t.Fatalf("root %#x, setup %d bytes", c.Root(), len(setup))
106+ }
107+ it := xproto.NewSetupIter(setup, 1)
108+ if !it.Next() {
109+ t.Fatal("setup does not parse")
110+ }
111+ sv := it.Value()
112+ if sv.Size() != len(setup) || sv.Status() != 1 || sv.ProtocolMajorVersion() != 11 {
113+ t.Fatalf("setup size %d status %d", sv.Size(), sv.Status())
114+ }
115+ screens := sv.Roots()
116+ if !screens.Next() || uint32(screens.Value().Root()) != c.Root() {
117+ t.Fatalf("Root %#x, setup view says otherwise", c.Root())
118+ }
119+
120+ // A trivial round trip: GetInputFocus, then read its reply.
121+ seq := xproto.GetInputFocus(c.Buf())
122+ if seq != 1 {
123+ t.Fatalf("seq %d", seq)
124+ }
125+ if err := c.Flush(); err != nil {
126+ t.Fatal(err)
127+ }
128+ reply := readPacket(t, c.Fd())
129+ if reply[0] != 1 || binary.LittleEndian.Uint16(reply[2:]) != uint16(seq) {
130+ t.Fatalf("reply %x", reply)
131+ }
132+}
133+
134+// readPacket polls the non-blocking fd and reads one 32-byte packet.
135+func readPacket(t *testing.T, fd int) [32]byte {
136+ t.Helper()
137+ var p [32]byte
138+ got := 0
139+ deadline := time.Now().Add(5 * time.Second)
140+ for got < len(p) {
141+ if time.Now().After(deadline) {
142+ t.Fatal("timeout reading reply")
143+ }
144+ fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
145+ if _, err := unix.Poll(fds, 1000); err != nil && err != unix.EINTR {
146+ t.Fatal(err)
147+ }
148+ n, err := unix.Read(fd, p[got:])
149+ if err == unix.EAGAIN || err == unix.EINTR {
150+ continue
151+ }
152+ if err != nil || n == 0 {
153+ t.Fatalf("read: %d %v", n, err)
154+ }
155+ got += n
156+ }
157+ return p
158+}
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 }
A setup.go +155 -0
@@ -0,0 +1,155 @@
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+ protocolMajor = 11
19+ protocolMinor = 0
20+)
21+
22+// setupInfo is what root keeps from a successful setup reply. Everything else
23+// is parsed by callers from Conn.SetupBytes.
24+type setupInfo struct {
25+ xidBase, xidMask uint32
26+ root uint32
27+}
28+
29+func malformed(what string) error { return fmt.Errorf("%w: %s", ErrMalformed, what) }
30+
31+// encodeSetupRequest builds the little-endian connection setup request.
32+func encodeSetupRequest(authName string, authData []byte) []byte {
33+ n, d := pad4(len(authName)), pad4(len(authData))
34+ p := make([]byte, 12+n+d)
35+ p[0] = 'l'
36+ binary.LittleEndian.PutUint16(p[2:], protocolMajor)
37+ binary.LittleEndian.PutUint16(p[4:], protocolMinor)
38+ binary.LittleEndian.PutUint16(p[6:], uint16(len(authName)))
39+ binary.LittleEndian.PutUint16(p[8:], uint16(len(authData)))
40+ copy(p[12:], authName)
41+ copy(p[12+n:], authData)
42+ return p
43+}
44+
45+func pad4(n int) int { return (n + 3) &^ 3 }
46+
47+// handshake runs the blocking setup exchange on fd and returns the full setup
48+// reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed.
49+func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) {
50+ if len(authName) > 0xffff || len(authData) > 0xffff {
51+ return nil, setupInfo{}, ErrBadLength
52+ }
53+ if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil {
54+ return nil, setupInfo{}, err
55+ }
56+ var hdr [setupHeaderSize]byte
57+ if err := readFull(fd, hdr[:]); err != nil {
58+ return nil, setupInfo{}, err
59+ }
60+ reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:])))
61+ copy(reply, hdr[:])
62+ if err := readFull(fd, reply[setupHeaderSize:]); err != nil {
63+ return nil, setupInfo{}, err
64+ }
65+ info, err := parseSetup(reply)
66+ if err != nil {
67+ return nil, setupInfo{}, err
68+ }
69+ return reply, info, nil
70+}
71+
72+// parseSetup validates a complete setup reply (header length already
73+// honoured) and extracts what root needs.
74+func parseSetup(p []byte) (setupInfo, error) {
75+ if len(p) < setupHeaderSize {
76+ return setupInfo{}, malformed("short setup reply")
77+ }
78+ switch p[0] {
79+ case setupStatusFailed:
80+ // SetupFailed: reason_len at byte 1, reason after the header.
81+ n := int(p[1])
82+ if setupHeaderSize+n > len(p) {
83+ return setupInfo{}, malformed("setup failure reason overflows reply")
84+ }
85+ return setupInfo{}, &SetupError{Status: setupStatusFailed, Reason: string(p[setupHeaderSize : setupHeaderSize+n])}
86+ case setupStatusAuthenticate:
87+ // SetupAuthenticate: reason fills length*4 bytes, NUL padded.
88+ reason := p[setupHeaderSize:]
89+ for len(reason) > 0 && reason[len(reason)-1] == 0 {
90+ reason = reason[:len(reason)-1]
91+ }
92+ return setupInfo{}, &SetupError{Status: setupStatusAuthenticate, Reason: string(reason)}
93+ case setupStatusSuccess:
94+ default:
95+ return setupInfo{}, malformed(fmt.Sprintf("setup status %d", p[0]))
96+ }
97+ if len(p) < setupFixedSize {
98+ return setupInfo{}, malformed("short setup reply")
99+ }
100+ if binary.LittleEndian.Uint16(p[2:]) != protocolMajor {
101+ return setupInfo{}, malformed(fmt.Sprintf("protocol major %d", binary.LittleEndian.Uint16(p[2:])))
102+ }
103+ info := setupInfo{
104+ xidBase: binary.LittleEndian.Uint32(p[12:]),
105+ xidMask: binary.LittleEndian.Uint32(p[16:]),
106+ }
107+ if info.xidMask == 0 || info.xidBase&info.xidMask != 0 {
108+ return setupInfo{}, malformed("resource id base/mask")
109+ }
110+ if p[28] == 0 {
111+ return setupInfo{}, malformed("setup has no screens")
112+ }
113+ off := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(p[24:]))) + formatSize*int(p[29])
114+ if off+screenFixedSize > len(p) {
115+ return setupInfo{}, malformed("first screen overflows setup reply")
116+ }
117+ info.root = binary.LittleEndian.Uint32(p[off:])
118+ return info, nil
119+}
120+
121+func writeFull(fd int, p []byte) error {
122+ for len(p) > 0 {
123+ n, err := unix.Write(fd, p)
124+ switch {
125+ case err == unix.EINTR:
126+ continue
127+ case err == unix.EPIPE || err == unix.ECONNRESET:
128+ return ErrClosed
129+ case err != nil:
130+ return os.NewSyscallError("write", err)
131+ case n <= 0:
132+ return io.ErrShortWrite
133+ }
134+ p = p[n:]
135+ }
136+ return nil
137+}
138+
139+func readFull(fd int, p []byte) error {
140+ for len(p) > 0 {
141+ n, err := unix.Read(fd, p)
142+ switch {
143+ case err == unix.EINTR:
144+ continue
145+ case err == unix.ECONNRESET:
146+ return ErrClosed
147+ case err != nil:
148+ return os.NewSyscallError("read", err)
149+ case n == 0:
150+ return fmt.Errorf("%w: truncated setup reply: %w", ErrMalformed, io.ErrUnexpectedEOF)
151+ }
152+ p = p[n:]
153+ }
154+ return nil
155+}
@@ -0,0 +1,155 @@
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+ protocolMajor = 11
19+ protocolMinor = 0
20+)
21+
22+// setupInfo is what root keeps from a successful setup reply. Everything else
23+// is parsed by callers from Conn.SetupBytes.
24+type setupInfo struct {
25+ xidBase, xidMask uint32
26+ root uint32
27+}
28+
29+func malformed(what string) error { return fmt.Errorf("%w: %s", ErrMalformed, what) }
30+
31+// encodeSetupRequest builds the little-endian connection setup request.
32+func encodeSetupRequest(authName string, authData []byte) []byte {
33+ n, d := pad4(len(authName)), pad4(len(authData))
34+ p := make([]byte, 12+n+d)
35+ p[0] = 'l'
36+ binary.LittleEndian.PutUint16(p[2:], protocolMajor)
37+ binary.LittleEndian.PutUint16(p[4:], protocolMinor)
38+ binary.LittleEndian.PutUint16(p[6:], uint16(len(authName)))
39+ binary.LittleEndian.PutUint16(p[8:], uint16(len(authData)))
40+ copy(p[12:], authName)
41+ copy(p[12+n:], authData)
42+ return p
43+}
44+
45+func pad4(n int) int { return (n + 3) &^ 3 }
46+
47+// handshake runs the blocking setup exchange on fd and returns the full setup
48+// reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed.
49+func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) {
50+ if len(authName) > 0xffff || len(authData) > 0xffff {
51+ return nil, setupInfo{}, ErrBadLength
52+ }
53+ if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil {
54+ return nil, setupInfo{}, err
55+ }
56+ var hdr [setupHeaderSize]byte
57+ if err := readFull(fd, hdr[:]); err != nil {
58+ return nil, setupInfo{}, err
59+ }
60+ reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:])))
61+ copy(reply, hdr[:])
62+ if err := readFull(fd, reply[setupHeaderSize:]); err != nil {
63+ return nil, setupInfo{}, err
64+ }
65+ info, err := parseSetup(reply)
66+ if err != nil {
67+ return nil, setupInfo{}, err
68+ }
69+ return reply, info, nil
70+}
71+
72+// parseSetup validates a complete setup reply (header length already
73+// honoured) and extracts what root needs.
74+func parseSetup(p []byte) (setupInfo, error) {
75+ if len(p) < setupHeaderSize {
76+ return setupInfo{}, malformed("short setup reply")
77+ }
78+ switch p[0] {
79+ case setupStatusFailed:
80+ // SetupFailed: reason_len at byte 1, reason after the header.
81+ n := int(p[1])
82+ if setupHeaderSize+n > len(p) {
83+ return setupInfo{}, malformed("setup failure reason overflows reply")
84+ }
85+ return setupInfo{}, &SetupError{Status: setupStatusFailed, Reason: string(p[setupHeaderSize : setupHeaderSize+n])}
86+ case setupStatusAuthenticate:
87+ // SetupAuthenticate: reason fills length*4 bytes, NUL padded.
88+ reason := p[setupHeaderSize:]
89+ for len(reason) > 0 && reason[len(reason)-1] == 0 {
90+ reason = reason[:len(reason)-1]
91+ }
92+ return setupInfo{}, &SetupError{Status: setupStatusAuthenticate, Reason: string(reason)}
93+ case setupStatusSuccess:
94+ default:
95+ return setupInfo{}, malformed(fmt.Sprintf("setup status %d", p[0]))
96+ }
97+ if len(p) < setupFixedSize {
98+ return setupInfo{}, malformed("short setup reply")
99+ }
100+ if binary.LittleEndian.Uint16(p[2:]) != protocolMajor {
101+ return setupInfo{}, malformed(fmt.Sprintf("protocol major %d", binary.LittleEndian.Uint16(p[2:])))
102+ }
103+ info := setupInfo{
104+ xidBase: binary.LittleEndian.Uint32(p[12:]),
105+ xidMask: binary.LittleEndian.Uint32(p[16:]),
106+ }
107+ if info.xidMask == 0 || info.xidBase&info.xidMask != 0 {
108+ return setupInfo{}, malformed("resource id base/mask")
109+ }
110+ if p[28] == 0 {
111+ return setupInfo{}, malformed("setup has no screens")
112+ }
113+ off := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(p[24:]))) + formatSize*int(p[29])
114+ if off+screenFixedSize > len(p) {
115+ return setupInfo{}, malformed("first screen overflows setup reply")
116+ }
117+ info.root = binary.LittleEndian.Uint32(p[off:])
118+ return info, nil
119+}
120+
121+func writeFull(fd int, p []byte) error {
122+ for len(p) > 0 {
123+ n, err := unix.Write(fd, p)
124+ switch {
125+ case err == unix.EINTR:
126+ continue
127+ case err == unix.EPIPE || err == unix.ECONNRESET:
128+ return ErrClosed
129+ case err != nil:
130+ return os.NewSyscallError("write", err)
131+ case n <= 0:
132+ return io.ErrShortWrite
133+ }
134+ p = p[n:]
135+ }
136+ return nil
137+}
138+
139+func readFull(fd int, p []byte) error {
140+ for len(p) > 0 {
141+ n, err := unix.Read(fd, p)
142+ switch {
143+ case err == unix.EINTR:
144+ continue
145+ case err == unix.ECONNRESET:
146+ return ErrClosed
147+ case err != nil:
148+ return os.NewSyscallError("read", err)
149+ case n == 0:
150+ return fmt.Errorf("%w: truncated setup reply: %w", ErrMalformed, io.ErrUnexpectedEOF)
151+ }
152+ p = p[n:]
153+ }
154+ return nil
155+}
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