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 return d, &records
96}
97
98// dispatchUntilError dispatches at most max frames and returns the first error.
99func dispatchUntilError(d *Display, max int) error {
100 for i := 0; i < max; i++ {
101 if err := d.Dispatch(); err != nil {
102 return err
103 }
104 }
105 return nil
106}
107
108func TestDisplayDispatch_Fragmented(t *testing.T) {
109 body := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
110 msg := message(7, 3, body)
111
112 // Split the header across three reads and the body byte by byte.
113 chunks := [][]byte{
114 msg[0:3],
115 msg[3:5],
116 msg[5:HeaderSize],
117 }
118 for i := HeaderSize; i < len(msg); i++ {
119 chunks = append(chunks, msg[i:i+1])
120 }
121
122 d, records := newChunkDisplay(chunks)
123 err := dispatchUntilError(d, 2)
124 if !errors.Is(err, io.ErrUnexpectedEOF) {
125 t.Fatalf("Dispatch after the message = %v, want %v", err, io.ErrUnexpectedEOF)
126 }
127
128 want := []EventRecord{{Object: 7, Opcode: 3, Body: body}}
129 if !reflect.DeepEqual(*records, want) {
130 t.Fatalf("records = %#v, want %#v", *records, want)
131 }
132}
133
134func TestDisplayDispatch_Coalesced(t *testing.T) {
135 first := message(7, 3, []byte{0x11, 0x12, 0x13, 0x14})
136 second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24})
137
138 d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)})
139
140 // Two messages arrive in a single read; one Dispatch per message.
141 if err := d.Dispatch(); err != nil {
142 t.Fatalf("first Dispatch: %v", err)
143 }
144 if err := d.Dispatch(); err != nil {
145 t.Fatalf("second Dispatch: %v", err)
146 }
147
148 want := []EventRecord{
149 {Object: 7, Opcode: 3, Body: []byte{0x11, 0x12, 0x13, 0x14}},
150 {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}},
151 }
152 if !reflect.DeepEqual(*records, want) {
153 t.Fatalf("records = %#v, want %#v", *records, want)
154 }
155}
156
157func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
158 // A read boundary that ends between the header and the body of the second
159 // message must not be mistaken for a frame boundary.
160 whole := append(message(7, 3, []byte{0x01, 0x02, 0x03, 0x04}),
161 message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...)
162
163 split := HeaderSize + 4 + 4 // mid-body of the second message
164 d, records := newChunkDisplay([][]byte{whole[:split], whole[split:]})
165
166 if err := d.Dispatch(); err != nil {
167 t.Fatalf("first Dispatch: %v", err)
168 }
169 if err := d.Dispatch(); err != nil {
170 t.Fatalf("second Dispatch: %v", err)
171 }
172
173 want := []EventRecord{
174 {Object: 7, Opcode: 3, Body: []byte{0x01, 0x02, 0x03, 0x04}},
175 {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}},
176 }
177 if !reflect.DeepEqual(*records, want) {
178 t.Fatalf("records = %#v, want %#v", *records, want)
179 }
180}
181
182func TestDisplayDispatch_ProtocolError(t *testing.T) {
183 cases := []struct {
184 name string
185 chunks [][]byte
186 maxLen uint32
187 wantErr error
188 wantKind string
189 }{
190 {
191 name: "size smaller than header",
192 chunks: [][]byte{header(7, 3, 0)},
193 wantErr: ErrMalformedFrame,
194 wantKind: "header_size",
195 },
196 {
197 name: "misaligned size",
198 chunks: [][]byte{header(7, 3, 10)},
199 wantErr: ErrMalformedFrame,
200 wantKind: "misaligned_size",
201 },
202 {
203 name: "size over configured maximum",
204 chunks: [][]byte{message(7, 3, make([]byte, 32))},
205 maxLen: 16,
206 wantErr: ErrMalformedFrame,
207 wantKind: "size_over_limit",
208 },
209 {
210 name: "unknown object",
211 chunks: [][]byte{message(9, 1, []byte{0x01, 0x02, 0x03, 0x04})},
212 wantErr: ErrUnknownObject,
213 wantKind: "unknown_object",
214 },
215 {
216 name: "unknown opcode on display",
217 chunks: [][]byte{message(displayObjectID, 9, nil)},
218 wantErr: ErrUnknownOpcode,
219 wantKind: "unknown_opcode",
220 },
221 }
222
223 for _, tc := range cases {
224 t.Run(tc.name, func(t *testing.T) {
225 d, records := newChunkDisplay(tc.chunks)
226 if tc.maxLen != 0 {
227 d.maxMsgLen = tc.maxLen
228 }
229
230 err := d.Dispatch()
231 if !errors.Is(err, tc.wantErr) {
232 t.Fatalf("Dispatch error = %v, want %v", err, tc.wantErr)
233 }
234
235 var perr *ProtocolError
236 if !errors.As(err, &perr) {
237 t.Fatalf("Dispatch error type = %T, want *ProtocolError", err)
238 }
239 if perr.Kind != tc.wantKind {
240 t.Fatalf("error kind = %q, want %q", perr.Kind, tc.wantKind)
241 }
242 if len(*records) != 0 {
243 t.Fatalf("records = %#v, want none", *records)
244 }
245 })
246 }
247}
248
249func TestDisplayDispatch_DisplayError(t *testing.T) {
250 // wl_display.error: object id, code, message (padded to 4 bytes).
251 msg := []byte("boom")
252 body := make([]byte, 0, 8+4+8)
253 body = binary.LittleEndian.AppendUint32(body, 7)
254 body = binary.LittleEndian.AppendUint32(body, 3)
255 body = binary.LittleEndian.AppendUint32(body, uint32(len(msg)+1))
256 body = append(body, msg...)
257 body = append(body, 0, 0, 0, 0)
258
259 d, _ := newChunkDisplay([][]byte{message(displayObjectID, 0, body)})
260
261 err := d.Dispatch()
262 if !errors.Is(err, ErrDisplayError) {
263 t.Fatalf("Dispatch error = %v, want %v", err, ErrDisplayError)
264 }
265
266 var derr *DisplayError
267 if !errors.As(err, &derr) {
268 t.Fatalf("Dispatch error type = %T, want *DisplayError", err)
269 }
270 if derr.ObjectID != 7 || derr.Code != 3 || derr.Message != "boom" {
271 t.Fatalf("DisplayError = %+v, want object=7 code=3 message=boom", derr)
272 }
273}
274
275func TestDisplayDispatch_DeleteID(t *testing.T) {
276 d, records := newChunkDisplay([][]byte{message(displayObjectID, 1, []byte{7, 0, 0, 0})})
277
278 if err := d.Dispatch(); err != nil {
279 t.Fatalf("Dispatch: %v", err)
280 }
281 if _, ok := d.objects.Load(uint32(7)); ok {
282 t.Fatal("delete_id did not remove object 7")
283 }
284 if len(*records) != 0 {
285 t.Fatalf("records = %#v, want none", *records)
286 }
287}
288
289func TestDisplayDispatch_PeerClose(t *testing.T) {
290 cases := []struct {
291 name string
292 chunks [][]byte
293 }{
294 {name: "closed before any byte"},
295 {name: "closed mid header", chunks: [][]byte{{0x01, 0x02, 0x03}}},
296 {
297 name: "closed mid body",
298 chunks: [][]byte{append(header(7, 3, 16), 0x01, 0x02, 0x03, 0x04)},
299 },
300 {
301 name: "closed after a complete message",
302 chunks: [][]byte{message(7, 3, []byte{0x01, 0x02, 0x03, 0x04})},
303 },
304 }
305
306 for _, tc := range cases {
307 t.Run(tc.name, func(t *testing.T) {
308 d, _ := newChunkDisplay(tc.chunks)
309
310 var err error
311 for i := 0; i < 4; i++ {
312 if err = d.Dispatch(); err != nil {
313 break
314 }
315 }
316 if !errors.Is(err, io.ErrUnexpectedEOF) {
317 t.Fatalf("Dispatch error = %v, want %v", err, io.ErrUnexpectedEOF)
318 }
319 })
320 }
321}
322
323func TestDisplayDispatch_ClosedReturnsErrClosed(t *testing.T) {
324 d, _ := newChunkDisplay([][]byte{message(7, 3, nil)})
325 if err := d.Close(); err != nil {
326 t.Fatalf("Close: %v", err)
327 }
328 if err := d.Dispatch(); !errors.Is(err, net.ErrClosed) {
329 t.Fatalf("Dispatch after Close = %v, want %v", err, net.ErrClosed)
330 }
331}
332
333func TestReadFrame(t *testing.T) {
334 body := []byte{0x0a, 0x0b, 0x0c, 0x0d}
335 object, opcode, got, err := readFrame(&chunkConn{chunks: [][]byte{message(7, 3, body)}}, DefaultMaxMessageSize)
336 if err != nil {
337 t.Fatalf("readFrame: %v", err)
338 }
339 if object != 7 || opcode != 3 || !reflect.DeepEqual(got, body) {
340 t.Fatalf("readFrame = (%d, %d, %x), want (7, 3, %x)", object, opcode, got, body)
341 }
342
343 // A header-only message has an empty body.
344 if _, _, got, err := readFrame(&chunkConn{chunks: [][]byte{message(7, 3, nil)}}, 0); err != nil || len(got) != 0 {
345 t.Fatalf("readFrame(header only) = (%x, %v), want (empty, nil)", got, err)
346 }
347
348 // Truncated bodies are reported, never returned partially.
349 if _, _, _, err := readFrame(&chunkConn{chunks: [][]byte{append(header(7, 3, 16), 0x01)}}, 0); !errors.Is(err, io.ErrUnexpectedEOF) {
350 t.Fatalf("readFrame(truncated) error = %v, want %v", err, io.ErrUnexpectedEOF)
351 }
352}