1//go:build linux
2
3package wayland
4
5import (
6 "context"
7 "errors"
8 "io"
9 "net"
10 "os"
11 "testing"
12 "time"
13
14 "github.com/bnema/wlturbo/protocol/core"
15 "golang.org/x/sys/unix"
16)
17
18func TestClipboardOfferReplacement(t *testing.T) {
19 c, d, server := clipboardFixture(t)
20 first := core.NewDataOffer(d.Context())
21 first.SetID(30)
22 d.Context().Register(first)
23 second := core.NewDataOffer(d.Context())
24 second.SetID(31)
25 d.Context().Register(second)
26 for _, o := range []*core.DataOffer{first, second} {
27 c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: o})
28 }
29 c.ApplyClipboard(ClipboardEvent{kind: "mime", offer: first, mime: mimePlain})
30 c.ApplyClipboard(ClipboardEvent{kind: "mime", offer: second, mime: mimeUTF8})
31 c.ApplyClipboard(ClipboardEvent{kind: "selection", offer: second})
32 if c.offer != second || c.bestMIME() != mimeUTF8 {
33 t.Fatal("latest selection not used")
34 }
35 inputRequest(t, server) // old offer destroyed
36}
37func TestClipboardWriteSendOverSocketpair(t *testing.T) {
38 c, d, server := clipboardFixture(t)
39 // WriteText sends create_data_source, three offers and set_selection.
40 if err := c.WriteText([]byte("hello")); err != nil {
41 t.Fatal(err)
42 }
43 // Client writes can coalesce into a single socket read.
44 _ = server.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
45 _, _ = server.Read(make([]byte, 4096))
46 source := c.source
47 r, w, err := os.Pipe()
48 if err != nil {
49 t.Fatal(err)
50 }
51 defer r.Close()
52 defer w.Close()
53 // Wayland event string uses byte length with trailing NUL, padded to 4.
54 mime := append([]byte(mimeUTF8), 0)
55 padded := make([]byte, (len(mime)+3)&^3)
56 copy(padded, mime)
57 body := make([]byte, 4+len(padded))
58 body[0] = byte(len(mime))
59 copy(body[4:], padded)
60 msg := inputFrame(source.ID(), 1)
61 msg = append(msg, body...)
62 msg[6] = byte(len(msg))
63 msg[7] = byte(len(msg) >> 8)
64 inputSend(t, server, msg, int(w.Fd()))
65 if err = d.Dispatch(); err != nil {
66 t.Fatal(err)
67 }
68 data := make([]byte, 5)
69 _ = r.SetReadDeadline(time.Now().Add(time.Second))
70 if _, err = r.Read(data); err != nil || string(data) != "hello" {
71 t.Fatalf("send %q: %v", data, err)
72 }
73}
74func TestClipboardReadLimitAndTimeout(t *testing.T) {
75 fd := make([]int, 2)
76 if err := unix.Pipe2(fd, unix.O_CLOEXEC|unix.O_NONBLOCK); err != nil {
77 t.Fatal(err)
78 }
79 r := os.NewFile(uintptr(fd[0]), "read")
80 defer r.Close()
81 defer unix.Close(fd[1])
82 if _, err := unix.Write(fd[1], []byte("123456")); err != nil {
83 t.Fatal(err)
84 }
85 if _, err := readClipboard(context.Background(), r, 5); err == nil {
86 t.Fatal("oversize accepted")
87 }
88 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
89 defer cancel()
90 if _, err := readClipboard(ctx, r, 5); !errors.Is(err, context.DeadlineExceeded) {
91 t.Fatalf("timeout: %v", err)
92 }
93}
94
95func TestClipboardSendOwnershipAndCancellation(t *testing.T) {
96 c, d, server := clipboardFixture(t)
97 if err := c.WriteText([]byte("hello")); err != nil {
98 t.Fatal(err)
99 }
100 inputRequest(t, server)
101 source := c.source
102 for _, mime := range []string{mimeUTF8, "application/octet-stream"} {
103 r, w, err := os.Pipe()
104 if err != nil {
105 t.Fatal(err)
106 }
107 data := append([]byte(mime), 0)
108 padded := make([]byte, (len(data)+3)&^3)
109 copy(padded, data)
110 msg := inputFrame(source.ID(), 1, uint32(len(data)))
111 msg = append(msg, padded...)
112 msg[6], msg[7] = byte(len(msg)), byte(len(msg)>>8)
113 inputSend(t, server, msg, int(w.Fd()))
114 if err = d.Dispatch(); err != nil {
115 t.Fatal(err)
116 }
117 _ = w.Close()
118 _ = r.SetReadDeadline(time.Now().Add(time.Second))
119 buf := make([]byte, 8)
120 n, err := r.Read(buf)
121 if err != nil && err != io.EOF {
122 t.Fatal(err)
123 }
124 if mime == mimeUTF8 && string(buf[:n]) != "hello" || mime != mimeUTF8 && n != 0 {
125 t.Fatalf("send %s: %q %v", mime, buf[:n], err)
126 }
127 _ = r.Close()
128 }
129 inputSend(t, server, inputFrame(source.ID(), 2))
130 if err := d.Dispatch(); err != nil {
131 t.Fatal(err)
132 }
133 if c.source != nil {
134 t.Fatal("cancelled source retained")
135 }
136 inputRequest(t, server) // source.destroy
137}
138
139func TestClipboardSelectionKeepsDnD(t *testing.T) {
140 c, d, server := clipboardFixture(t)
141 drag := core.NewDataOffer(d.Context())
142 drag.SetID(70)
143 d.Context().Register(drag)
144 selected := core.NewDataOffer(d.Context())
145 selected.SetID(71)
146 d.Context().Register(selected)
147 c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: drag})
148 c.ApplyClipboard(ClipboardEvent{kind: "enter", offer: drag})
149 c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: selected})
150 c.ApplyClipboard(ClipboardEvent{kind: "selection", offer: selected})
151 if c.dnd != drag || c.offer != selected {
152 t.Fatal("lost active drag or selection")
153 }
154 _ = server.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
155 if _, err := server.Read(make([]byte, 128)); err == nil {
156 t.Fatal("active drag destroyed")
157 }
158 c.ApplyClipboard(ClipboardEvent{kind: "drop"})
159 if c.dnd != nil {
160 t.Fatal("drop retained drag")
161 }
162 inputRequest(t, server)
163}
164
165func TestClipboardCloseCancelsReadWithoutDelivery(t *testing.T) {
166 c, d, server := clipboardFixture(t)
167 offer := core.NewDataOffer(d.Context())
168 offer.SetID(80)
169 d.Context().Register(offer)
170 c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: offer})
171 c.ApplyClipboard(ClipboardEvent{kind: "mime", offer: offer, mime: mimePlain})
172 c.ApplyClipboard(ClipboardEvent{kind: "selection", offer: offer})
173 delivered := make(chan struct{}, 1)
174 if err := c.ReadTextAsync(context.Background(), 32, func([]byte, error) { delivered <- struct{}{} }); err != nil {
175 t.Fatal(err)
176 }
177 // Keep the compositor's write end open so the read cannot finish before Close.
178 buf, oob := make([]byte, 256), make([]byte, 128)
179 _ = server.SetReadDeadline(time.Now().Add(time.Second))
180 _, n, _, _, err := server.ReadMsgUnix(buf, oob)
181 if err != nil {
182 t.Fatal(err)
183 }
184 msgs, err := unix.ParseSocketControlMessage(oob[:n])
185 if err != nil {
186 t.Fatal(err)
187 }
188 for _, msg := range msgs {
189 fds, err := unix.ParseUnixRights(&msg)
190 if err != nil {
191 t.Fatal(err)
192 }
193 for _, fd := range fds {
194 defer unix.Close(fd)
195 }
196 }
197 done := make(chan struct{})
198 go func() { c.Close(); close(done) }()
199 select {
200 case <-done:
201 case <-time.After(time.Second):
202 t.Fatal("Close did not wait for cancelled read")
203 }
204 select {
205 case <-delivered:
206 t.Fatal("delivered after Close")
207 default:
208 }
209}
210
211func TestClipboardCloseInsideDelivery(t *testing.T) {
212 c, d, server := clipboardFixture(t)
213 offer := core.NewDataOffer(d.Context())
214 offer.SetID(81)
215 d.Context().Register(offer)
216 c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: offer})
217 c.ApplyClipboard(ClipboardEvent{kind: "mime", offer: offer, mime: mimePlain})
218 c.ApplyClipboard(ClipboardEvent{kind: "selection", offer: offer})
219 done := make(chan struct{})
220 if err := c.ReadTextAsync(context.Background(), 32, func([]byte, error) { c.Close(); close(done) }); err != nil {
221 t.Fatal(err)
222 }
223 clipboardFinishRead(t, server)
224 select {
225 case <-done:
226 case <-time.After(time.Second):
227 t.Fatal("Close deadlocked in delivery")
228 }
229}
230
231func TestClipboardCloseWhileDeliveryBlocks(t *testing.T) {
232 c, d, server := clipboardFixture(t)
233 offer := core.NewDataOffer(d.Context())
234 offer.SetID(82)
235 d.Context().Register(offer)
236 c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: offer})
237 c.ApplyClipboard(ClipboardEvent{kind: "mime", offer: offer, mime: mimePlain})
238 c.ApplyClipboard(ClipboardEvent{kind: "selection", offer: offer})
239 entered := make(chan struct{})
240 unblock := make(chan struct{})
241 callbackDone := make(chan struct{})
242 if err := c.ReadTextAsync(context.Background(), 32, func([]byte, error) {
243 close(entered)
244 <-unblock
245 close(callbackDone)
246 }); err != nil {
247 t.Fatal(err)
248 }
249 clipboardFinishRead(t, server)
250 select {
251 case <-entered:
252 case <-time.After(time.Second):
253 t.Fatal("delivery not called")
254 }
255 done := make(chan struct{})
256 go func() { c.Close(); close(done) }()
257 select {
258 case <-done:
259 case <-time.After(time.Second):
260 t.Fatal("Close blocked on delivery")
261 }
262 close(unblock)
263 select {
264 case <-callbackDone:
265 case <-time.After(time.Second):
266 t.Fatal("callback did not finish")
267 }
268}
269
270// Consume receive and close the transferred write FD to complete the read.
271func clipboardFinishRead(t *testing.T, server *net.UnixConn) {
272 t.Helper()
273 buf, oob := make([]byte, 256), make([]byte, 128)
274 _ = server.SetReadDeadline(time.Now().Add(time.Second))
275 _, n, _, _, err := server.ReadMsgUnix(buf, oob)
276 if err != nil {
277 t.Fatal(err)
278 }
279 msgs, err := unix.ParseSocketControlMessage(oob[:n])
280 if err != nil {
281 t.Fatal(err)
282 }
283 for _, m := range msgs {
284 fds, err := unix.ParseUnixRights(&m)
285 if err != nil {
286 t.Fatal(err)
287 }
288 for _, fd := range fds {
289 _, _ = unix.Write(fd, []byte("ok"))
290 _ = unix.Close(fd)
291 }
292 }
293}