1package wlturbo
2
3import (
4 "encoding/binary"
5 "errors"
6 "io"
7 "net"
8 "reflect"
9 "testing"
10 "time"
11)
12
13// EventRecord is what a test observes for one dispatched event.
14type EventRecord struct {
15 Object uint32
16 Opcode uint16
17 Body []byte
18}
19
20// recordingProxy records every dispatched event. It is a stand-in for a real
21// protocol proxy so framing tests need no compositor.
22type recordingProxy struct {
23 BaseProxy
24 records *[]EventRecord
25}
26
27func (p *recordingProxy) Dispatch(event *Event) {
28 *p.records = append(*p.records, EventRecord{
29 Object: event.ProxyID,
30 Opcode: event.Opcode,
31 Body: append([]byte(nil), event.Data()...),
32 })
33}
34
35// chunkConn returns one chunk per Read, so a test controls exactly how bytes
36// are split across read boundaries.
37type chunkConn struct {
38 chunks [][]byte
39 idx int
40}
41
42func (c *chunkConn) Read(p []byte) (int, error) {
43 if c.idx >= len(c.chunks) {
44 return 0, io.EOF
45 }
46 chunk := c.chunks[c.idx]
47 n := copy(p, chunk)
48 switch {
49 case n == len(chunk):
50 c.idx++
51 case n < len(chunk):
52 // Caller's buffer was smaller than the chunk: keep the remainder.
53 c.chunks[c.idx] = chunk[n:]
54 }
55 return n, nil
56}
57
58func (c *chunkConn) Write(p []byte) (int, error) { return len(p), nil }
59func (c *chunkConn) Close() error { return nil }
60func (c *chunkConn) LocalAddr() net.Addr { return testAddr{} }
61func (c *chunkConn) RemoteAddr() net.Addr { return testAddr{} }
62func (c *chunkConn) SetDeadline(time.Time) error { return nil }
63func (c *chunkConn) SetReadDeadline(time.Time) error { return nil }
64func (c *chunkConn) SetWriteDeadline(time.Time) error { return nil }
65
66type testAddr struct{}
67
68func (testAddr) Network() string { return "test" }
69func (testAddr) String() string { return "test" }
70
71// header builds a Wayland message header for the given object, opcode and
72// total message size.
73func header(object uint32, opcode uint16, size uint32) []byte {
74 buf := make([]byte, HeaderSize)
75 binary.LittleEndian.PutUint32(buf[0:4], object)
76 binary.LittleEndian.PutUint32(buf[4:8], (size<<16)|uint32(opcode))
77 return buf
78}
79
80// message builds one complete 4-byte-aligned Wayland message.
81func message(object uint32, opcode uint16, body []byte) []byte {
82 msg := header(object, opcode, uint32(HeaderSize+len(body)))
83 return append(msg, body...)
84}
85
86// newChunkDisplay builds a Display over a connection that yields the given
87// chunks, with a recording proxy registered at object ID 7.
88func newChunkDisplay(chunks [][]byte) (*Display, *[]EventRecord) {
89 d := newDisplay(&chunkConn{chunks: chunks})
90 records := []EventRecord{}
91 d.objects.Store(uint32(7), &recordingProxy{
92 BaseProxy: BaseProxy{id: 7, context: d.context},
93 records: &records,
94 })
95 for _, opcode := range []uint16{3, 4, 5} {
96 signature := "uint,"
97 if opcode == 3 {
98 signature = "uint,uint,"
99 }
100 d.RegisterEventSignature(7, opcode, signature)
101 }
102 return d, &records
103}
104
105// dispatchUntilError dispatches at most max frames and returns the first error.
106func dispatchUntilError(d *Display, max int) error {
107 for i := 0; i < max; i++ {
108 if err := d.Dispatch(); err != nil {
109 return err
110 }
111 }
112 return nil
113}
114
115func TestDisplayDispatch_Fragmented(t *testing.T) {
116 body := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
117 msg := message(7, 3, body)
118
119 // Split the header across three reads and the body byte by byte.
120 chunks := [][]byte{
121 msg[0:3],
122 msg[3:5],
123 msg[5:HeaderSize],
124 }
125 for i := HeaderSize; i < len(msg); i++ {
126 chunks = append(chunks, msg[i:i+1])
127 }
128
129 d, records := newChunkDisplay(chunks)
130 err := dispatchUntilError(d, 2)
131 if !errors.Is(err, io.ErrUnexpectedEOF) {
132 t.Fatalf("Dispatch after the message = %v, want %v", err, io.ErrUnexpectedEOF)
133 }
134
135 want := []EventRecord{{Object: 7, Opcode: 3, Body: body}}
136 if !reflect.DeepEqual(*records, want) {
137 t.Fatalf("records = %#v, want %#v", *records, want)
138 }
139}
140
141func TestDisplayDispatch_Coalesced(t *testing.T) {
142 first := message(7, 4, []byte{0x11, 0x12, 0x13, 0x14})
143 second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24})
144
145 d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)})
146
147 // Two messages arrive in a single read; one Dispatch per message.
148 if err := d.Dispatch(); err != nil {
149 t.Fatalf("first Dispatch: %v", err)
150 }
151 if err := d.Dispatch(); err != nil {
152 t.Fatalf("second Dispatch: %v", err)
153 }
154
155 want := []EventRecord{
156 {Object: 7, Opcode: 4, Body: []byte{0x11, 0x12, 0x13, 0x14}},
157 {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}},
158 }
159 if !reflect.DeepEqual(*records, want) {
160 t.Fatalf("records = %#v, want %#v", *records, want)
161 }
162}
163
164func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
165 // A read boundary that ends between the header and the body of the second
166 // message must not be mistaken for a frame boundary.
167 whole := append(message(7, 4, []byte{0x01, 0x02, 0x03, 0x04}),
168 message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...)
169
170 split := HeaderSize + 4 + 4 // mid-body of the second message
171 d, records := newChunkDisplay([][]byte{whole[:split], whole[split:]})
172
173 if err := d.Dispatch(); err != nil {
174 t.Fatalf("first Dispatch: %v", err)
175 }
176 if err := d.Dispatch(); err != nil {
177 t.Fatalf("second Dispatch: %v", err)
178 }
179
180 want := []EventRecord{
181 {Object: 7, Opcode: 4, Body: []byte{0x01, 0x02, 0x03, 0x04}},
182 {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}},
183 }
184 if !reflect.DeepEqual(*records, want) {
185 t.Fatalf("records = %#v, want %#v", *records, want)
186 }
187}
188
189func TestDisplayDispatch_ProtocolError(t *testing.T) {
190 cases := []struct {
191 name string
192 chunks [][]byte
193 maxLen uint32
194 wantErr error
195 wantKind string
196 }{
197 {
198 name: "size smaller than header",
199 chunks: [][]byte{header(7, 3, 0)},
200 wantErr: ErrMalformedFrame,
201 wantKind: "header_size",
202 },
203 {
204 name: "misaligned size",
205 chunks: [][]byte{header(7, 3, 10)},
206 wantErr: ErrMalformedFrame,
207 wantKind: "misaligned_size",
208 },
209 {
210 name: "size over configured maximum",
211 chunks: [][]byte{message(7, 3, make([]byte, 32))},
212 maxLen: 16,
213 wantErr: ErrMalformedFrame,
214 wantKind: "size_over_limit",
215 },
216 {
217 name: "unknown object",
218 chunks: [][]byte{message(9, 1, []byte{0x01, 0x02, 0x03, 0x04})},
219 wantErr: ErrUnknownObject,
220 wantKind: "unknown_object",
221 },
222 {
223 name: "unknown opcode on display",
224 chunks: [][]byte{message(displayObjectID, 9, nil)},
225 wantErr: ErrUnknownOpcode,
226 wantKind: "unknown_opcode",
227 },
228 }
229
230 for _, tc := range cases {
231 t.Run(tc.name, func(t *testing.T) {
232 d, records := newChunkDisplay(tc.chunks)
233 if tc.maxLen != 0 {
234 d.maxMsgLen = tc.maxLen
235 }
236
237 err := d.Dispatch()
238 if !errors.Is(err, tc.wantErr) {
239 t.Fatalf("Dispatch error = %v, want %v", err, tc.wantErr)
240 }
241
242 var perr *ProtocolError
243 if !errors.As(err, &perr) {
244 t.Fatalf("Dispatch error type = %T, want *ProtocolError", err)
245 }
246 if perr.Kind != tc.wantKind {
247 t.Fatalf("error kind = %q, want %q", perr.Kind, tc.wantKind)
248 }
249 if len(*records) != 0 {
250 t.Fatalf("records = %#v, want none", *records)
251 }
252 })
253 }
254}
255
256func TestDisplayDispatch_DisplayError(t *testing.T) {
257 // wl_display.error: object id, code, message (padded to 4 bytes).
258 msg := []byte("boom")
259 body := make([]byte, 0, 8+4+8)
260 body = binary.LittleEndian.AppendUint32(body, 7)
261 body = binary.LittleEndian.AppendUint32(body, 3)
262 body = binary.LittleEndian.AppendUint32(body, uint32(len(msg)+1))
263 body = append(body, msg...)
264 body = append(body, 0, 0, 0, 0)
265
266 d, _ := newChunkDisplay([][]byte{message(displayObjectID, 0, body)})
267
268 err := d.Dispatch()
269 if !errors.Is(err, ErrDisplayError) {
270 t.Fatalf("Dispatch error = %v, want %v", err, ErrDisplayError)
271 }
272
273 var derr *DisplayError
274 if !errors.As(err, &derr) {
275 t.Fatalf("Dispatch error type = %T, want *DisplayError", err)
276 }
277 if derr.ObjectID != 7 || derr.Code != 3 || derr.Message != "boom" {
278 t.Fatalf("DisplayError = %+v, want object=7 code=3 message=boom", derr)
279 }
280}
281
282func TestDisplayDispatch_DeleteID(t *testing.T) {
283 d, records := newChunkDisplay([][]byte{message(displayObjectID, 1, []byte{7, 0, 0, 0})})
284 for d.nextID <= 7 {
285 d.allocateID()
286 }
287 d.objects.Delete(uint32(7))
288
289 if err := d.Dispatch(); err != nil {
290 t.Fatalf("Dispatch: %v", err)
291 }
292 if _, ok := d.objects.Load(uint32(7)); ok {
293 t.Fatal("delete_id did not remove object 7")
294 }
295 if len(*records) != 0 {
296 t.Fatalf("records = %#v, want none", *records)
297 }
298}
299
300func TestDisplayDispatch_PeerClose(t *testing.T) {
301 cases := []struct {
302 name string
303 chunks [][]byte
304 }{
305 {name: "closed before any byte"},
306 {name: "closed mid header", chunks: [][]byte{{0x01, 0x02, 0x03}}},
307 {
308 name: "closed mid body",
309 chunks: [][]byte{append(header(7, 3, 16), 0x01, 0x02, 0x03, 0x04)},
310 },
311 {
312 name: "closed after a complete message",
313 chunks: [][]byte{message(7, 4, []byte{0x01, 0x02, 0x03, 0x04})},
314 },
315 }
316
317 for _, tc := range cases {
318 t.Run(tc.name, func(t *testing.T) {
319 d, _ := newChunkDisplay(tc.chunks)
320
321 var err error
322 for i := 0; i < 4; i++ {
323 if err = d.Dispatch(); err != nil {
324 break
325 }
326 }
327 if !errors.Is(err, io.ErrUnexpectedEOF) {
328 t.Fatalf("Dispatch error = %v, want %v", err, io.ErrUnexpectedEOF)
329 }
330 })
331 }
332}
333
334func TestDisplayDispatch_ClosedReturnsErrClosed(t *testing.T) {
335 d, _ := newChunkDisplay([][]byte{message(7, 3, nil)})
336 if err := d.Close(); err != nil {
337 t.Fatalf("Close: %v", err)
338 }
339 if err := d.Dispatch(); !errors.Is(err, net.ErrClosed) {
340 t.Fatalf("Dispatch after Close = %v, want %v", err, net.ErrClosed)
341 }
342}
343
344func TestReadFrame(t *testing.T) {
345 body := []byte{0x0a, 0x0b, 0x0c, 0x0d}
346 object, opcode, got, err := readFrame(&chunkConn{chunks: [][]byte{message(7, 3, body)}}, DefaultMaxMessageSize)
347 if err != nil {
348 t.Fatalf("readFrame: %v", err)
349 }
350 if object != 7 || opcode != 3 || !reflect.DeepEqual(got, body) {
351 t.Fatalf("readFrame = (%d, %d, %x), want (7, 3, %x)", object, opcode, got, body)
352 }
353
354 // A header-only message has an empty body.
355 if _, _, got, err := readFrame(&chunkConn{chunks: [][]byte{message(7, 3, nil)}}, 0); err != nil || len(got) != 0 {
356 t.Fatalf("readFrame(header only) = (%x, %v), want (empty, nil)", got, err)
357 }
358
359 // Truncated bodies are reported, never returned partially.
360 if _, _, _, err := readFrame(&chunkConn{chunks: [][]byte{append(header(7, 3, 16), 0x01)}}, 0); !errors.Is(err, io.ErrUnexpectedEOF) {
361 t.Fatalf("readFrame(truncated) error = %v, want %v", err, io.ErrUnexpectedEOF)
362 }
363}
364
365func TestDeleteIDReuseAndInvalid(t *testing.T) {
366 d := newDisplay(&chunkConn{})
367 id := d.AllocateID()
368 body := make([]byte, 4)
369 binary.LittleEndian.PutUint32(body, id)
370 if err := d.handleDisplayEvent(1, body); err != nil {
371 t.Fatal(err)
372 }
373 if err := d.handleDisplayEvent(1, body); err == nil {
374 t.Fatal("duplicate delete_id accepted")
375 }
376 d.nextID = 0xff000000 // simulate exhaustion
377 if got := d.AllocateID(); got != id {
378 t.Fatalf("reused %d, want %d", got, id)
379 }
380 if got := d.AllocateID(); got != 0 {
381 t.Fatalf("allocated server-range ID %x", got)
382 }
383 d.objects.Store(id, d)
384 if err := d.handleDisplayEvent(1, body); err == nil {
385 t.Fatal("live ID deleted")
386 }
387 for _, bad := range []uint32{0, 1, 0xff000001} {
388 binary.LittleEndian.PutUint32(body, bad)
389 if err := d.handleDisplayEvent(1, body); err == nil {
390 t.Fatalf("accepted reserved ID %x", bad)
391 }
392 }
393}