1package xturbo
2
3import (
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
16func 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. Otherwise it
28// then answers requests as a server without extensions (script{}).
29func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client int, req <-chan []byte) {
30 t.Helper()
31 fd, _, ch := serveSetup(t, reply, chunk, hangup, &script{})
32 return fd, ch
33}
34
35// serveSetup is scriptedServer serving sc after the setup.
36func serveSetup(t *testing.T, reply []byte, chunk int, hangup bool, sc *script) (int, *scriptConn, <-chan []byte) {
37 t.Helper()
38 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
39 if err != nil {
40 t.Fatal(err)
41 }
42 // A blocking fd: NewConn switches it to O_NONBLOCK itself.
43 if err := unix.SetNonblock(fds[0], false); err != nil {
44 t.Fatal(err)
45 }
46 server := os.NewFile(uintptr(fds[1]), "server")
47 srv := &scriptConn{f: server, sc: sc}
48 ch := make(chan []byte, 1)
49 done := make(chan struct{})
50 t.Cleanup(func() { <-done; server.Close() })
51 go func() {
52 defer close(done)
53 var hdr [12]byte
54 if _, err := io.ReadFull(server, hdr[:]); err != nil {
55 ch <- nil
56 return
57 }
58 n := pad4(int(binary.LittleEndian.Uint16(hdr[6:]))) + pad4(int(binary.LittleEndian.Uint16(hdr[8:])))
59 rest := make([]byte, n)
60 if _, err := io.ReadFull(server, rest); err != nil {
61 ch <- nil
62 return
63 }
64 ch <- append(hdr[:], rest...)
65 for p := reply; len(p) > 0; {
66 k := len(p)
67 if chunk > 0 {
68 k = min(k, chunk)
69 }
70 if _, err := server.Write(p[:k]); err != nil {
71 return
72 }
73 p = p[k:]
74 }
75 if hangup {
76 server.Close()
77 return
78 }
79 srv.serve()
80 }()
81 return fds[0], srv, ch
82}
83
84func TestNewConnSuccess(t *testing.T) {
85 setup := loadSetup(t, "Success")
86 fd, req := scriptedServer(t, setup, 7, false)
87 c, err := NewConn(fd)
88 if err != nil {
89 t.Fatal(err)
90 }
91 defer c.Close()
92 got := <-req
93 want := []byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0}
94 if !bytes.Equal(got, want) {
95 t.Fatalf("setup request %x, want %x", got, want)
96 }
97 if !bytes.Equal(c.SetupBytes(), setup) {
98 t.Fatal("SetupBytes differs from the reply")
99 }
100 // Expected values are read from the fixture header comment
101 // (Success.setup.hex), i.e. what that recorded Xvfb assigned.
102 if c.Root() != 0x39f || c.xidBase != 0x00200000 || c.xidMask != 0x001fffff {
103 t.Fatalf("root %#x base %#x mask %#x", c.Root(), c.xidBase, c.xidMask)
104 }
105 // The bootstrap issued one QueryExtension per extension and a fence.
106 if c.Fd() != fd || c.Buf().Ext != c.Ext() || c.Buf().Next() != Seq(extCount)+1 {
107 t.Fatal("accessors", c.Buf().Next())
108 }
109 if c.Ext().Major(ExtXFixes) != 0 || !c.Ext().Info(ExtCore).Present {
110 t.Fatal("ExtTable not empty")
111 }
112 flags, err := unix.FcntlInt(uintptr(fd), unix.F_GETFL, 0)
113 if err != nil || flags&unix.O_NONBLOCK == 0 {
114 t.Fatalf("fd not O_NONBLOCK after setup: %v", err)
115 }
116}
117
118func TestNewConnAuthRequest(t *testing.T) {
119 fd, req := scriptedServer(t, loadSetup(t, "Success"), 0, false)
120 cookie := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
121 c, err := newConn(fd, authMagicCookie, cookie)
122 if err != nil {
123 t.Fatal(err)
124 }
125 defer c.Close()
126 want := append([]byte{'l', 0, 11, 0, 0, 0, 18, 0, 16, 0, 0, 0}, authMagicCookie+"\x00\x00"...)
127 want = append(want, cookie...)
128 if got := <-req; !bytes.Equal(got, want) {
129 t.Fatalf("setup request %x, want %x", got, want)
130 }
131}
132
133func TestNewConnRefused(t *testing.T) {
134 authenticate := []byte{2, 0, 0, 0, 0, 0, 2, 0, 'n', 'o', 'p', 'e', '!', 0, 0, 0}
135 for _, tc := range []struct {
136 name string
137 reply []byte
138 status uint8
139 reason string
140 }{
141 {"failed", loadSetup(t, "Failed"), 0, "Authorization required, but no authorization protocol specified\n"},
142 {"authenticate", authenticate, 2, "nope!"},
143 } {
144 t.Run(tc.name, func(t *testing.T) {
145 fd, _ := scriptedServer(t, tc.reply, 5, true)
146 c, err := NewConn(fd)
147 var se *SetupError
148 if c != nil || !errors.As(err, &se) || !errors.Is(err, ErrSetupFailed) {
149 t.Fatalf("conn %t, err %v", c != nil, err)
150 }
151 if se.Status != tc.status || se.Reason != tc.reason {
152 t.Fatalf("status %d reason %q", se.Status, se.Reason)
153 }
154 if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err != unix.EBADF {
155 t.Fatalf("fd not closed on failure: %v", err)
156 }
157 })
158 }
159}
160
161func TestNewConnMalformed(t *testing.T) {
162 setup := loadSetup(t, "Success")
163 noScreens := bytes.Clone(setup)
164 noScreens[28] = 0
165 badMask := bytes.Clone(setup)
166 binary.LittleEndian.PutUint32(badMask[16:], 0)
167 badVersion := bytes.Clone(setup)
168 badVersion[2] = 10
169 badStatus := bytes.Clone(setup)
170 badStatus[0] = 3
171 // Fixed Setup only (length 8 words): one root declared, no SCREEN bytes.
172 screenPast := bytes.Clone(setup[:40])
173 binary.LittleEndian.PutUint16(screenPast[6:], 8)
174 binary.LittleEndian.PutUint16(screenPast[24:], 0) // vendor_len
175 screenPast[29] = 0 // pixmap_formats_len
176 // Walk the recorded reply to find the first screen and its first depth
177 // that carries visuals; every extent is a multiple of 4 bytes.
178 screen := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(setup[24:]))) + formatSize*int(setup[29])
179 visualDepth := screen + screenFixedSize
180 for binary.LittleEndian.Uint16(setup[visualDepth+2:]) == 0 {
181 visualDepth += depthFixedSize
182 }
183 // Screen header present, its allowed depths cut off.
184 missingDepths := truncateSetup(setup, screen+screenFixedSize)
185 // A depth header announcing visuals, the visuals cut off.
186 missingVisuals := truncateSetup(setup, visualDepth+depthFixedSize)
187 // One visual of the depth present, the rest cut off.
188 partialVisuals := truncateSetup(setup, visualDepth+depthFixedSize+visualSize)
189 // Complete first screen, a second screen declared but absent.
190 missingScreen := bytes.Clone(setup)
191 missingScreen[28] = 2
192 // Header claims 8 bytes of body, failure reason claims 20.
193 reasonPast := []byte{0, 20, 11, 0, 0, 0, 2, 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
194 // Success header whose length is shorter than the fixed Setup.
195 shortSuccess := []byte{1, 0, 11, 0, 0, 0, 1, 0, 0, 0, 0, 0}
196 for _, tc := range []struct {
197 name string
198 reply []byte
199 }{
200 {"empty", nil},
201 {"truncated header", setup[:5]},
202 {"truncated body", setup[:len(setup)-3]},
203 {"no screens", noScreens},
204 {"bad xid mask", badMask},
205 {"protocol major", badVersion},
206 {"unknown status", badStatus},
207 {"screen past end", screenPast},
208 {"missing depths", missingDepths},
209 {"missing visuals", missingVisuals},
210 {"partial visuals", partialVisuals},
211 {"missing subsequent screen", missingScreen},
212 {"reason past end", reasonPast},
213 {"short success", shortSuccess},
214 } {
215 t.Run(tc.name, func(t *testing.T) {
216 fd, _ := scriptedServer(t, tc.reply, 3, true)
217 c, err := NewConn(fd)
218 if c != nil || !errors.Is(err, ErrMalformed) || errors.Is(err, ErrSetupFailed) {
219 t.Fatalf("conn %t, err %v", c != nil, err)
220 }
221 })
222 }
223}
224
225// truncateSetup keeps the first n bytes (n ≡ 0 mod 4) of a setup reply and
226// rewrites its additional-data length to match.
227func truncateSetup(setup []byte, n int) []byte {
228 p := bytes.Clone(setup[:n])
229 binary.LittleEndian.PutUint16(p[6:], uint16((n-setupHeaderSize)/4))
230 return p
231}
232
233func TestConnFlushClose(t *testing.T) {
234 c, srv := dialScript(t, &script{})
235 first := c.Buf().Next()
236 p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus
237 if p == nil || seq != first {
238 t.Fatal("request")
239 }
240 if err := c.Flush(); err != nil {
241 t.Fatal(err)
242 }
243 waitRequests(t, srv, int(seq))
244 if got := srv.requests()[seq-1].raw; !bytes.Equal(got, []byte{43, 0, 1, 0}) {
245 t.Fatalf("%x", got)
246 }
247 if err := c.Close(); err != nil {
248 t.Fatal(err)
249 }
250 if c.Fd() != -1 || !errors.Is(c.Close(), ErrClosed) || !errors.Is(c.Flush(), ErrClosed) {
251 t.Fatal("after Close")
252 }
253 if p, _ := c.Buf().Request(43, 0, 0); p != nil {
254 t.Fatal("Buf usable after Close")
255 }
256}
257
258func TestSetupErrorMessage(t *testing.T) {
259 for _, tc := range []struct {
260 e SetupError
261 want string
262 }{
263 {SetupError{0, "no"}, "xturbo: connection setup failed: no"},
264 {SetupError{2, "why"}, "xturbo: connection setup authenticate: why"},
265 } {
266 if got := tc.e.Error(); got != tc.want {
267 t.Errorf("%q, want %q", got, tc.want)
268 }
269 }
270}