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/nefergui/internal/edit"
15 "github.com/bnema/wlturbo"
16 "github.com/bnema/wlturbo/protocol/core"
17 "golang.org/x/sys/unix"
18)
19
20func clipboardFixture(t *testing.T) (*Clipboard, *wlturbo.Display, *net.UnixConn) {
21 t.Helper()
22 ca, server := inputPair(t)
23 d, err := wlturbo.ConnectFromConn(ca)
24 if err != nil {
25 t.Fatal(err)
26 }
27 t.Cleanup(func() { _ = d.Close() })
28 inputRequest(t, server)
29 ctx := d.Context()
30 manager := core.NewDataDeviceManager(ctx)
31 if err = d.Registry().Bind(1, core.DataDeviceManagerInterface, 3, manager); err != nil {
32 t.Fatal(err)
33 }
34 inputRequest(t, server)
35 seat := core.NewSeat(ctx)
36 seat.SetID(20)
37 ctx.Register(seat)
38 device, err := manager.GetDataDevice(seat)
39 if err != nil {
40 t.Fatal(err)
41 }
42 inputRequest(t, server)
43 c := &Clipboard{manager: manager, device: device, serial: func() uint32 { return 42 }, pending: make(map[*core.DataOffer]map[string]bool), offers: make(map[uint32]*core.DataOffer)}
44 c.post = func(e ClipboardEvent) { c.ApplyClipboard(e) }
45 return c, d, server
46}
47func TestClipboardReadOverSocketpair(t *testing.T) {
48 c, d, server := clipboardFixture(t)
49 offer := core.NewDataOffer(d.Context())
50 offer.SetID(30)
51 d.Context().Register(offer)
52 c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: offer})
53 for _, m := range []string{mimePlain, mimeX11, mimeUTF8} {
54 c.ApplyClipboard(ClipboardEvent{kind: "mime", offer: offer, mime: m})
55 }
56 c.ApplyClipboard(ClipboardEvent{kind: "selection", offer: offer})
57 if c.bestMIME() != mimeUTF8 {
58 t.Fatal("MIME priority")
59 }
60 results := make(chan ClipboardResult, 2)
61 // The peer reads receive's SCM_RIGHTS and writes to the transferred pipe.
62 go func() {
63 b := make([]byte, 512)
64 oob := make([]byte, 128)
65 _ = server.SetReadDeadline(time.Now().Add(time.Second))
66 _, n, _, _, err := server.ReadMsgUnix(b, oob)
67 if err != nil {
68 results <- ClipboardResult{Err: err}
69 return
70 }
71 messages, err := unix.ParseSocketControlMessage(oob[:n])
72 if err != nil {
73 results <- ClipboardResult{Err: err}
74 return
75 }
76 for _, m := range messages {
77 fd, e := unix.ParseUnixRights(&m)
78 if e != nil || len(fd) == 0 {
79 continue
80 }
81 f := os.NewFile(uintptr(fd[0]), "offer-write")
82 _, e = io.WriteString(f, "hello")
83 _ = f.Close()
84 if e != nil {
85 results <- ClipboardResult{Err: e}
86 }
87 return
88 }
89 results <- ClipboardResult{Err: errors.New("missing pipe FD")}
90 }()
91 if err := c.ReadTextAsync(context.Background(), edit.MaxClipboardBytes, func(b []byte, e error) { results <- ClipboardResult{Data: b, Err: e} }); err != nil {
92 t.Fatal(err)
93 }
94 select {
95 case result := <-results:
96 if result.Err != nil || string(result.Data) != "hello" {
97 t.Fatalf("result: %+v", result)
98 }
99 case <-time.After(2 * time.Second):
100 t.Fatal("read blocked")
101 }
102}
103func TestClipboardBoundAndCancel(t *testing.T) {
104 r, w, err := os.Pipe()
105 if err != nil {
106 t.Fatal(err)
107 }
108 defer w.Close()
109 results := make(chan ClipboardResult, 1)
110 go func() { data, e := io.ReadAll(io.LimitReader(r, 5)); results <- ClipboardResult{data, e} }()
111 _, _ = w.Write([]byte("123456"))
112 select {
113 case result := <-results:
114 if len(result.Data) != 5 {
115 t.Fatal(result)
116 }
117 case <-time.After(time.Second):
118 t.Fatal("limit blocked")
119 }
120 state := edit.State{}
121 if ok, _ := state.PasteAsync([]byte{0xff}, nil); ok {
122 t.Fatal("accepted invalid UTF-8")
123 }
124 if ok, _ := state.PasteAsync(make([]byte, edit.MaxClipboardBytes+1), nil); ok {
125 t.Fatal("accepted oversized paste")
126 }
127}