detached 5856658

test(protocol): exercise extension lifecycle and feedback over socketpair

commit
5856658fbad5922b599855ddeb9c4937d166dc72
author
brice · · 2026-09-28 07:55 UTC
parent
975d460
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +576 -1
M consumer_test.go +13 -1
@@ -35,7 +35,19 @@ func TestExternalConsumer(t *testing.T) {
3535 if err := os.WriteFile(filepath.Join(temp, "consumer_test.go"), source, 0600); err != nil {
3636 t.Fatal(err)
3737 }
38- cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild", "./...")
38+ // Keep the extension fake server in a separate package: both fixtures
39+ // independently own their socketpair helpers.
40+ extension, err := os.ReadFile(filepath.Join(root, "protocol/extensions_test.go"))
41+ if err != nil {
42+ t.Fatal(err)
43+ }
44+ if err := os.Mkdir(filepath.Join(temp, "extensions"), 0700); err != nil {
45+ t.Fatal(err)
46+ }
47+ if err := os.WriteFile(filepath.Join(temp, "extensions", "extensions_test.go"), extension, 0600); err != nil {
48+ t.Fatal(err)
49+ }
50+ cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild|TestExtensionsOverSocketpair", "./...")
3951 cmd.Dir = temp
4052 cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod")
4153 if output, err := cmd.CombinedOutput(); err != nil {
@@ -35,7 +35,19 @@ func TestExternalConsumer(t *testing.T) {
35 if err := os.WriteFile(filepath.Join(temp, "consumer_test.go"), source, 0600); err != nil {35 if err := os.WriteFile(filepath.Join(temp, "consumer_test.go"), source, 0600); err != nil {
36 t.Fatal(err)36 t.Fatal(err)
37 }37 }
38- cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild", "./...")38+ // Keep the extension fake server in a separate package: both fixtures
39+ // independently own their socketpair helpers.
40+ extension, err := os.ReadFile(filepath.Join(root, "protocol/extensions_test.go"))
41+ if err != nil {
42+ t.Fatal(err)
43+ }
44+ if err := os.Mkdir(filepath.Join(temp, "extensions"), 0700); err != nil {
45+ t.Fatal(err)
46+ }
47+ if err := os.WriteFile(filepath.Join(temp, "extensions", "extensions_test.go"), extension, 0600); err != nil {
48+ t.Fatal(err)
49+ }
50+ cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild|TestExtensionsOverSocketpair", "./...")
39 cmd.Dir = temp51 cmd.Dir = temp
40 cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod")52 cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod")
41 if output, err := cmd.CombinedOutput(); err != nil {53 if output, err := cmd.CombinedOutput(); err != nil {
A protocol/extensions_test.go +467 -0
@@ -0,0 +1,467 @@
1+//go:build linux
2+
3+package protocol_test
4+
5+import (
6+ "encoding/binary"
7+ "net"
8+ "os"
9+ "strings"
10+ "testing"
11+ "time"
12+
13+ "github.com/bnema/wlturbo"
14+ "github.com/bnema/wlturbo/protocol/core"
15+ "github.com/bnema/wlturbo/protocol/drmsyncobj"
16+ "github.com/bnema/wlturbo/protocol/fractionalscale"
17+ "github.com/bnema/wlturbo/protocol/linuxdmabuf"
18+ "github.com/bnema/wlturbo/protocol/textinput"
19+ "github.com/bnema/wlturbo/protocol/viewporter"
20+ "github.com/bnema/wlturbo/protocol/xdgshell"
21+ "golang.org/x/sys/unix"
22+)
23+
24+func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
25+ t.Helper()
26+ fds, e := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
27+ if e != nil {
28+ t.Fatal(e)
29+ }
30+ a := os.NewFile(uintptr(fds[0]), "a")
31+ b := os.NewFile(uintptr(fds[1]), "b")
32+ ca, e := net.FileConn(a)
33+ a.Close()
34+ if e != nil {
35+ t.Fatal(e)
36+ }
37+ cb, e := net.FileConn(b)
38+ b.Close()
39+ if e != nil {
40+ t.Fatal(e)
41+ }
42+ t.Cleanup(func() { ca.Close(); cb.Close() })
43+ return ca.(*net.UnixConn), cb.(*net.UnixConn)
44+}
45+func msg(id uint32, op uint16, body ...uint32) []byte {
46+ b := make([]byte, 8+4*len(body))
47+ binary.NativeEndian.PutUint32(b, id)
48+ binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
49+ for i, v := range body {
50+ binary.NativeEndian.PutUint32(b[8+i*4:], v)
51+ }
52+ return b
53+}
54+func payload(id uint32, op uint16, b ...byte) []byte {
55+ p := make([]byte, 8+len(b))
56+ copy(p, msg(id, op))
57+ copy(p[8:], b)
58+ binary.NativeEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op))
59+ return p
60+}
61+func arr(b []byte) []byte {
62+ out := make([]byte, 4+(len(b)+3)&^3)
63+ binary.NativeEndian.PutUint32(out, uint32(len(b)))
64+ copy(out[4:], b)
65+ return out
66+}
67+func str(s string) []byte { return arr(append([]byte(s), 0)) }
68+func send(t *testing.T, p *net.UnixConn, b []byte, fd ...int) {
69+ t.Helper()
70+ var rights []byte
71+ if len(fd) > 0 {
72+ rights = unix.UnixRights(fd...)
73+ }
74+ if _, _, e := p.WriteMsgUnix(b, rights, nil); e != nil {
75+ t.Fatal(e)
76+ }
77+}
78+func request(t *testing.T, p *net.UnixConn) (uint32, uint16, []byte) {
79+ t.Helper()
80+ p.SetReadDeadline(time.Now().Add(2 * time.Second))
81+ b := make([]byte, 4096)
82+ n, e := p.Read(b)
83+ if e != nil {
84+ t.Fatal(e)
85+ }
86+ if n < 8 {
87+ t.Fatal("short request")
88+ }
89+ return binary.NativeEndian.Uint32(b), uint16(binary.NativeEndian.Uint32(b[4:])), b[8:n]
90+}
91+func noerr(t *testing.T, e error) {
92+ t.Helper()
93+ if e != nil {
94+ t.Fatal(e)
95+ }
96+}
97+func bind(t *testing.T, d *wlturbo.Display, p *net.UnixConn, name uint32, iface string, supported uint32, proxy wlturbo.Proxy) {
98+ t.Helper()
99+ v := supported
100+ if v > 4 {
101+ v = 4
102+ }
103+ noerr(t, d.Registry().Bind(name, iface, v, proxy))
104+ _, op, b := request(t, p)
105+ if op != 0 || binary.NativeEndian.Uint32(b) != name || binary.NativeEndian.Uint32(b[len(b)-8:]) != v || binary.NativeEndian.Uint32(b[len(b)-4:]) != proxy.ID() {
106+ t.Fatalf("bad bind %s version=%d %x", iface, v, b)
107+ }
108+}
109+func TestExtensionsOverSocketpair(t *testing.T) {
110+ c, p := pair(t)
111+ d, e := wlturbo.ConnectFromConn(c)
112+ noerr(t, e)
113+ defer d.Close()
114+ request(t, p)
115+ // A registry announcement is the only input to optional-global discovery.
116+ send(t, p, payload(d.Registry().ID(), 0, append(msg(0, 0, 11)[8:12], append(str(fractionalscale.WpFractionalScaleManagerInterface), msg(0, 0, 1)[8:12]...)...)...))
117+ noerr(t, d.Dispatch())
118+ if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); !ok || g.Version != 1 {
119+ t.Fatal("announced fractional scale missing", g, ok)
120+ }
121+ if _, ok := d.Registry().FindGlobal(textinput.TextInputManagerV3Interface); ok {
122+ t.Fatal("absent text input")
123+ }
124+ surface := core.NewSurface(d.Context())
125+ surface.SetID(d.AllocateID())
126+ d.Context().Register(surface)
127+ seat := core.NewSeat(d.Context())
128+ seat.SetID(d.AllocateID())
129+ d.Context().Register(seat)
130+ wm := xdgshell.NewXdgWmBase(d.Context())
131+ bind(t, d, p, 1, xdgshell.XdgWmBaseInterface, 7, wm)
132+ xs, e := wm.GetXdgSurface(surface)
133+ noerr(t, e)
134+ request(t, p)
135+ top, e := xs.GetToplevel()
136+ noerr(t, e)
137+ request(t, p)
138+ var sequence []string
139+ top.OnConfigure(func(w, h int32, states []byte) {
140+ if w != 800 || h != 600 {
141+ t.Fatalf("size %d,%d", w, h)
142+ }
143+ sequence = append(sequence, "top")
144+ })
145+ xs.OnConfigure(func(serial uint32) { sequence = append(sequence, "surface"); noerr(t, xs.AckConfigure(serial)) })
146+ send(t, p, payload(top.ID(), 0, append(msg(0, 0, 800, 600)[8:], arr(nil)...)...))
147+ noerr(t, d.Dispatch())
148+ send(t, p, msg(xs.ID(), 0, 9))
149+ noerr(t, d.Dispatch())
150+ _, op, b := request(t, p)
151+ if op != 4 || binary.NativeEndian.Uint32(b) != 9 || strings.Join(sequence, ",") != "top,surface" {
152+ t.Fatalf("xdg %d %x %v", op, b, sequence)
153+ }
154+ noerr(t, top.Destroy())
155+ request(t, p)
156+ noerr(t, xs.Destroy())
157+ request(t, p)
158+ noerr(t, wm.Destroy())
159+ request(t, p)
160+ dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
161+ bind(t, d, p, 2, linuxdmabuf.LinuxDmabufInterface, 6, dm)
162+ fb, e := dm.GetDefaultFeedback()
163+ noerr(t, e)
164+ request(t, p)
165+ table, e := os.CreateTemp(t.TempDir(), "table")
166+ noerr(t, e)
167+ var bytes [32]byte
168+ binary.NativeEndian.PutUint32(bytes[:], 0x34325258)
169+ binary.NativeEndian.PutUint64(bytes[8:], 19)
170+ binary.NativeEndian.PutUint32(bytes[16:], 0x34325241)
171+ binary.NativeEndian.PutUint64(bytes[24:], 42)
172+ _, e = table.Write(bytes[:])
173+ noerr(t, e)
174+ defer table.Close()
175+ var entries []linuxdmabuf.FormatEntry
176+ var indices []uint16
177+ var feedback []string
178+ fb.OnMainDevice(func(dev []byte) {
179+ if len(dev) != 8 {
180+ t.Fatalf("device %x", dev)
181+ }
182+ feedback = append(feedback, "main")
183+ })
184+ fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
185+ entries, e = linuxdmabuf.ReadFormatTable(fd, size)
186+ noerr(t, e)
187+ feedback = append(feedback, "table")
188+ })
189+ fb.OnTrancheTargetDevice(func(dev []byte) { feedback = append(feedback, "target") })
190+ fb.OnTrancheFormats(func(data []byte) {
191+ indices, e = linuxdmabuf.TrancheIndices(data, len(entries))
192+ noerr(t, e)
193+ feedback = append(feedback, "formats")
194+ })
195+ fb.OnTrancheFlags(func(flags uint32) {
196+ if flags != 1 {
197+ t.Fatal(flags)
198+ }
199+ feedback = append(feedback, "flags")
200+ })
201+ fb.OnTrancheDone(func() { feedback = append(feedback, "tranche") })
202+ fb.OnDone(func() { feedback = append(feedback, "done") })
203+ send(t, p, payload(fb.ID(), 2, arr(make([]byte, 8))...))
204+ noerr(t, d.Dispatch())
205+ send(t, p, msg(fb.ID(), 1, 32), int(table.Fd()))
206+ noerr(t, d.Dispatch())
207+ send(t, p, payload(fb.ID(), 4, arr(make([]byte, 8))...))
208+ noerr(t, d.Dispatch())
209+ send(t, p, payload(fb.ID(), 5, arr([]byte{0, 0, 1, 0})...))
210+ noerr(t, d.Dispatch())
211+ send(t, p, msg(fb.ID(), 6, 1))
212+ noerr(t, d.Dispatch())
213+ send(t, p, msg(fb.ID(), 3))
214+ noerr(t, d.Dispatch())
215+ send(t, p, msg(fb.ID(), 0))
216+ noerr(t, d.Dispatch())
217+ if len(entries) != 2 || entries[1].Modifier != 42 || len(indices) != 2 || indices[1] != 1 || strings.Join(feedback, ",") != "main,table,target,formats,flags,tranche,done" {
218+ t.Fatalf("feedback %v %v %v", entries, indices, feedback)
219+ }
220+ noerr(t, fb.Destroy())
221+ request(t, p)
222+ params, e := dm.CreateParams()
223+ noerr(t, e)
224+ request(t, p)
225+ f, e := os.Open("/dev/null")
226+ noerr(t, e)
227+ noerr(t, params.Add(int(f.Fd()), 0, 0, 4, 0, 0))
228+ request(t, p)
229+ if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil {
230+ t.Fatal("sent fd not closed")
231+ }
232+ f.Close()
233+ var buffer *core.Buffer
234+ params.OnCreated(func(b *core.Buffer) { buffer = b })
235+ params.OnFailed(func() { sequence = append(sequence, "failed") })
236+ noerr(t, params.Create(1, 1, 0x34325258, 0))
237+ request(t, p)
238+ send(t, p, msg(params.ID(), 0, 999))
239+ noerr(t, d.Dispatch())
240+ if buffer == nil || buffer.ID() != 999 {
241+ t.Fatal("typed buffer missing")
242+ }
243+ noerr(t, buffer.Destroy())
244+ request(t, p)
245+ noerr(t, params.Destroy())
246+ request(t, p)
247+ failedParams, e := dm.CreateParams()
248+ noerr(t, e)
249+ request(t, p)
250+ failedParams.OnFailed(func() { sequence = append(sequence, "failed") })
251+ noerr(t, failedParams.Create(1, 1, 0x34325258, 0))
252+ request(t, p)
253+ send(t, p, msg(failedParams.ID(), 1))
254+ noerr(t, d.Dispatch())
255+ if sequence[len(sequence)-1] != "failed" {
256+ t.Fatal(sequence)
257+ }
258+ noerr(t, failedParams.Destroy())
259+ request(t, p)
260+ immedParams, e := dm.CreateParams()
261+ noerr(t, e)
262+ request(t, p)
263+ immediate, e := immedParams.CreateImmed(1, 1, 0x34325258, 0)
264+ noerr(t, e)
265+ request(t, p)
266+ noerr(t, immediate.Destroy())
267+ request(t, p)
268+ noerr(t, immedParams.Destroy())
269+ request(t, p)
270+ noerr(t, dm.Destroy())
271+ request(t, p)
272+ sync := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context())
273+ bind(t, d, p, 3, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, sync)
274+ ss, e := sync.GetSurface(surface)
275+ noerr(t, e)
276+ request(t, p)
277+ f, e = os.Open("/dev/null")
278+ noerr(t, e)
279+ tl, e := sync.ImportTimeline(int(f.Fd()))
280+ noerr(t, e)
281+ request(t, p)
282+ if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil {
283+ t.Fatal("timeline fd not closed")
284+ }
285+ f.Close()
286+ noerr(t, ss.SetAcquirePoint(tl, 0x123, 0x456))
287+ _, op, b = request(t, p)
288+ if op != 1 || binary.NativeEndian.Uint32(b[4:]) != 0x123 || binary.NativeEndian.Uint32(b[8:]) != 0x456 {
289+ t.Fatalf("acquire %d %x", op, b)
290+ }
291+ noerr(t, ss.SetReleasePoint(tl, 0x789, 0xabc))
292+ _, op, b = request(t, p)
293+ if op != 2 || binary.NativeEndian.Uint32(b[4:]) != 0x789 {
294+ t.Fatalf("release %d %x", op, b)
295+ }
296+ noerr(t, ss.Destroy())
297+ request(t, p)
298+ noerr(t, tl.Destroy())
299+ request(t, p)
300+ noerr(t, sync.Destroy())
301+ request(t, p)
302+ vp := viewporter.NewWpViewporter(d.Context())
303+ bind(t, d, p, 4, viewporter.WpViewporterInterface, 1, vp)
304+ view, e := vp.GetViewport(surface)
305+ noerr(t, e)
306+ request(t, p)
307+ noerr(t, view.Destroy())
308+ request(t, p)
309+ noerr(t, vp.Destroy())
310+ request(t, p)
311+ fs := fractionalscale.NewWpFractionalScaleManager(d.Context())
312+ bind(t, d, p, 5, fractionalscale.WpFractionalScaleManagerInterface, 1, fs)
313+ scale, e := fs.GetFractionalScale(surface)
314+ noerr(t, e)
315+ request(t, p)
316+ var preferred uint32
317+ scale.OnPreferredScale(func(v uint32) { preferred = v })
318+ send(t, p, msg(scale.ID(), 0, 150))
319+ noerr(t, d.Dispatch())
320+ if preferred != 150 {
321+ t.Fatal(preferred)
322+ }
323+ noerr(t, scale.Destroy())
324+ request(t, p)
325+ noerr(t, fs.Destroy())
326+ request(t, p)
327+ ti := textinput.NewTextInputManagerV3(d.Context())
328+ bind(t, d, p, 6, textinput.TextInputManagerV3Interface, 1, ti)
329+ input, e := ti.GetTextInput(seat)
330+ noerr(t, e)
331+ request(t, p)
332+ noerr(t, input.Enable())
333+ request(t, p)
334+ noerr(t, input.SetSurroundingText("hello", 5, 5))
335+ request(t, p)
336+ noerr(t, input.Commit())
337+ request(t, p)
338+ var events []string
339+ input.OnPreeditString(func(s string, _, _ int32) { events = append(events, "preedit:"+s) })
340+ input.OnCommitString(func(s string) { events = append(events, "commit:"+s) })
341+ input.OnDeleteSurroundingText(func(_, _ uint32) { events = append(events, "delete") })
342+ input.OnDone(func(uint32) { events = append(events, "done") })
343+ send(t, p, payload(input.ID(), 2, append(str("é"), msg(0, 0, 0, 0)[8:]...)...))
344+ noerr(t, d.Dispatch())
345+ send(t, p, payload(input.ID(), 3, str("hi")...))
346+ noerr(t, d.Dispatch())
347+ send(t, p, msg(input.ID(), 4, 1, 2))
348+ noerr(t, d.Dispatch())
349+ send(t, p, msg(input.ID(), 5, 1))
350+ noerr(t, d.Dispatch())
351+ if strings.Join(events, ",") != "preedit:é,commit:hi,delete,done" {
352+ t.Fatal(events)
353+ }
354+ noerr(t, input.Disable())
355+ request(t, p)
356+ noerr(t, input.Commit())
357+ request(t, p)
358+ noerr(t, input.Destroy())
359+ request(t, p)
360+ noerr(t, ti.Destroy())
361+ request(t, p)
362+ // Core data-device remains in protocol/core; verify its typed child and
363+ // both directions of clipboard descriptor ownership.
364+ mgr := core.NewDataDeviceManager(d.Context())
365+ bind(t, d, p, 7, core.DataDeviceManagerInterface, 3, mgr)
366+ device, e := mgr.GetDataDevice(seat)
367+ noerr(t, e)
368+ request(t, p)
369+ source, e := mgr.CreateDataSource()
370+ noerr(t, e)
371+ request(t, p)
372+ var offer *core.DataOffer
373+ device.OnDataOffer(func(o *core.DataOffer) { offer = o })
374+ send(t, p, msg(device.ID(), 0, 998))
375+ noerr(t, d.Dispatch())
376+ if offer == nil || offer.ID() != 998 {
377+ t.Fatal("offer not typed")
378+ }
379+ read, write, e := os.Pipe()
380+ noerr(t, e)
381+ defer read.Close()
382+ noerr(t, offer.Receive("text/plain", int(write.Fd())))
383+ request(t, p)
384+ if _, e := unix.FcntlInt(write.Fd(), unix.F_GETFD, 0); e == nil {
385+ t.Fatal("clipboard sent fd retained")
386+ }
387+ write.Close()
388+ source.OnSend(func(mime string, fd *wlturbo.OwnedFD) {
389+ if mime != "text/plain" {
390+ t.Fatal(mime)
391+ }
392+ n, e := fd.Take()
393+ noerr(t, e)
394+ unix.Close(n)
395+ })
396+ received, e := os.Open("/dev/null")
397+ noerr(t, e)
398+ send(t, p, payload(source.ID(), 1, str("text/plain")...), int(received.Fd()))
399+ noerr(t, d.Dispatch())
400+ received.Close()
401+ noerr(t, offer.Destroy())
402+ request(t, p)
403+ noerr(t, source.Destroy())
404+ request(t, p)
405+ noerr(t, device.Release())
406+ request(t, p)
407+ noerr(t, mgr.Release())
408+ request(t, p)
409+
410+}
411+
412+func TestLiveFeedback(t *testing.T) {
413+ if os.Getenv("WLTURBO_LIVE") != "1" {
414+ t.Skip("set WLTURBO_LIVE=1")
415+ }
416+ d, e := wlturbo.Connect("")
417+ noerr(t, e)
418+ defer d.Close()
419+ noerr(t, d.Roundtrip())
420+ for _, iface := range []string{xdgshell.XdgWmBaseInterface, linuxdmabuf.LinuxDmabufInterface, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, viewporter.WpViewporterInterface, fractionalscale.WpFractionalScaleManagerInterface, textinput.TextInputManagerV3Interface} {
421+ g, ok := d.Registry().FindGlobal(iface)
422+ if !ok {
423+ t.Logf("%s absent", iface)
424+ continue
425+ }
426+ v := g.Version
427+ cap := uint32(1)
428+ if iface == xdgshell.XdgWmBaseInterface {
429+ cap = 7
430+ }
431+ if iface == linuxdmabuf.LinuxDmabufInterface {
432+ cap = 4
433+ }
434+ if v > cap {
435+ v = cap
436+ }
437+ t.Logf("%s negotiated=%d (server=%d)", iface, v, g.Version)
438+ if iface != linuxdmabuf.LinuxDmabufInterface {
439+ continue
440+ }
441+ if v < 4 {
442+ t.Fatal("dmabuf feedback requires v4")
443+ }
444+ dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
445+ noerr(t, d.Registry().Bind(g.Name, iface, v, dm))
446+ fb, e := dm.GetDefaultFeedback()
447+ noerr(t, e)
448+ done := false
449+ fb.OnMainDevice(func(b []byte) { t.Logf("main_device=%x", b) })
450+ fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
451+ entries, err := linuxdmabuf.ReadFormatTable(fd, size)
452+ if err != nil {
453+ t.Errorf("table: %v", err)
454+ }
455+ t.Logf("format-table entries=%d", len(entries))
456+ })
457+ fb.OnDone(func() { done = true })
458+ for i := 0; i < 100 && !done; i++ {
459+ noerr(t, d.Dispatch())
460+ }
461+ if !done {
462+ t.Fatal("no feedback done")
463+ }
464+ noerr(t, fb.Destroy())
465+ noerr(t, dm.Destroy())
466+ }
467+}
@@ -0,0 +1,467 @@
1+//go:build linux
2+
3+package protocol_test
4+
5+import (
6+ "encoding/binary"
7+ "net"
8+ "os"
9+ "strings"
10+ "testing"
11+ "time"
12+
13+ "github.com/bnema/wlturbo"
14+ "github.com/bnema/wlturbo/protocol/core"
15+ "github.com/bnema/wlturbo/protocol/drmsyncobj"
16+ "github.com/bnema/wlturbo/protocol/fractionalscale"
17+ "github.com/bnema/wlturbo/protocol/linuxdmabuf"
18+ "github.com/bnema/wlturbo/protocol/textinput"
19+ "github.com/bnema/wlturbo/protocol/viewporter"
20+ "github.com/bnema/wlturbo/protocol/xdgshell"
21+ "golang.org/x/sys/unix"
22+)
23+
24+func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
25+ t.Helper()
26+ fds, e := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
27+ if e != nil {
28+ t.Fatal(e)
29+ }
30+ a := os.NewFile(uintptr(fds[0]), "a")
31+ b := os.NewFile(uintptr(fds[1]), "b")
32+ ca, e := net.FileConn(a)
33+ a.Close()
34+ if e != nil {
35+ t.Fatal(e)
36+ }
37+ cb, e := net.FileConn(b)
38+ b.Close()
39+ if e != nil {
40+ t.Fatal(e)
41+ }
42+ t.Cleanup(func() { ca.Close(); cb.Close() })
43+ return ca.(*net.UnixConn), cb.(*net.UnixConn)
44+}
45+func msg(id uint32, op uint16, body ...uint32) []byte {
46+ b := make([]byte, 8+4*len(body))
47+ binary.NativeEndian.PutUint32(b, id)
48+ binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
49+ for i, v := range body {
50+ binary.NativeEndian.PutUint32(b[8+i*4:], v)
51+ }
52+ return b
53+}
54+func payload(id uint32, op uint16, b ...byte) []byte {
55+ p := make([]byte, 8+len(b))
56+ copy(p, msg(id, op))
57+ copy(p[8:], b)
58+ binary.NativeEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op))
59+ return p
60+}
61+func arr(b []byte) []byte {
62+ out := make([]byte, 4+(len(b)+3)&^3)
63+ binary.NativeEndian.PutUint32(out, uint32(len(b)))
64+ copy(out[4:], b)
65+ return out
66+}
67+func str(s string) []byte { return arr(append([]byte(s), 0)) }
68+func send(t *testing.T, p *net.UnixConn, b []byte, fd ...int) {
69+ t.Helper()
70+ var rights []byte
71+ if len(fd) > 0 {
72+ rights = unix.UnixRights(fd...)
73+ }
74+ if _, _, e := p.WriteMsgUnix(b, rights, nil); e != nil {
75+ t.Fatal(e)
76+ }
77+}
78+func request(t *testing.T, p *net.UnixConn) (uint32, uint16, []byte) {
79+ t.Helper()
80+ p.SetReadDeadline(time.Now().Add(2 * time.Second))
81+ b := make([]byte, 4096)
82+ n, e := p.Read(b)
83+ if e != nil {
84+ t.Fatal(e)
85+ }
86+ if n < 8 {
87+ t.Fatal("short request")
88+ }
89+ return binary.NativeEndian.Uint32(b), uint16(binary.NativeEndian.Uint32(b[4:])), b[8:n]
90+}
91+func noerr(t *testing.T, e error) {
92+ t.Helper()
93+ if e != nil {
94+ t.Fatal(e)
95+ }
96+}
97+func bind(t *testing.T, d *wlturbo.Display, p *net.UnixConn, name uint32, iface string, supported uint32, proxy wlturbo.Proxy) {
98+ t.Helper()
99+ v := supported
100+ if v > 4 {
101+ v = 4
102+ }
103+ noerr(t, d.Registry().Bind(name, iface, v, proxy))
104+ _, op, b := request(t, p)
105+ if op != 0 || binary.NativeEndian.Uint32(b) != name || binary.NativeEndian.Uint32(b[len(b)-8:]) != v || binary.NativeEndian.Uint32(b[len(b)-4:]) != proxy.ID() {
106+ t.Fatalf("bad bind %s version=%d %x", iface, v, b)
107+ }
108+}
109+func TestExtensionsOverSocketpair(t *testing.T) {
110+ c, p := pair(t)
111+ d, e := wlturbo.ConnectFromConn(c)
112+ noerr(t, e)
113+ defer d.Close()
114+ request(t, p)
115+ // A registry announcement is the only input to optional-global discovery.
116+ send(t, p, payload(d.Registry().ID(), 0, append(msg(0, 0, 11)[8:12], append(str(fractionalscale.WpFractionalScaleManagerInterface), msg(0, 0, 1)[8:12]...)...)...))
117+ noerr(t, d.Dispatch())
118+ if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); !ok || g.Version != 1 {
119+ t.Fatal("announced fractional scale missing", g, ok)
120+ }
121+ if _, ok := d.Registry().FindGlobal(textinput.TextInputManagerV3Interface); ok {
122+ t.Fatal("absent text input")
123+ }
124+ surface := core.NewSurface(d.Context())
125+ surface.SetID(d.AllocateID())
126+ d.Context().Register(surface)
127+ seat := core.NewSeat(d.Context())
128+ seat.SetID(d.AllocateID())
129+ d.Context().Register(seat)
130+ wm := xdgshell.NewXdgWmBase(d.Context())
131+ bind(t, d, p, 1, xdgshell.XdgWmBaseInterface, 7, wm)
132+ xs, e := wm.GetXdgSurface(surface)
133+ noerr(t, e)
134+ request(t, p)
135+ top, e := xs.GetToplevel()
136+ noerr(t, e)
137+ request(t, p)
138+ var sequence []string
139+ top.OnConfigure(func(w, h int32, states []byte) {
140+ if w != 800 || h != 600 {
141+ t.Fatalf("size %d,%d", w, h)
142+ }
143+ sequence = append(sequence, "top")
144+ })
145+ xs.OnConfigure(func(serial uint32) { sequence = append(sequence, "surface"); noerr(t, xs.AckConfigure(serial)) })
146+ send(t, p, payload(top.ID(), 0, append(msg(0, 0, 800, 600)[8:], arr(nil)...)...))
147+ noerr(t, d.Dispatch())
148+ send(t, p, msg(xs.ID(), 0, 9))
149+ noerr(t, d.Dispatch())
150+ _, op, b := request(t, p)
151+ if op != 4 || binary.NativeEndian.Uint32(b) != 9 || strings.Join(sequence, ",") != "top,surface" {
152+ t.Fatalf("xdg %d %x %v", op, b, sequence)
153+ }
154+ noerr(t, top.Destroy())
155+ request(t, p)
156+ noerr(t, xs.Destroy())
157+ request(t, p)
158+ noerr(t, wm.Destroy())
159+ request(t, p)
160+ dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
161+ bind(t, d, p, 2, linuxdmabuf.LinuxDmabufInterface, 6, dm)
162+ fb, e := dm.GetDefaultFeedback()
163+ noerr(t, e)
164+ request(t, p)
165+ table, e := os.CreateTemp(t.TempDir(), "table")
166+ noerr(t, e)
167+ var bytes [32]byte
168+ binary.NativeEndian.PutUint32(bytes[:], 0x34325258)
169+ binary.NativeEndian.PutUint64(bytes[8:], 19)
170+ binary.NativeEndian.PutUint32(bytes[16:], 0x34325241)
171+ binary.NativeEndian.PutUint64(bytes[24:], 42)
172+ _, e = table.Write(bytes[:])
173+ noerr(t, e)
174+ defer table.Close()
175+ var entries []linuxdmabuf.FormatEntry
176+ var indices []uint16
177+ var feedback []string
178+ fb.OnMainDevice(func(dev []byte) {
179+ if len(dev) != 8 {
180+ t.Fatalf("device %x", dev)
181+ }
182+ feedback = append(feedback, "main")
183+ })
184+ fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
185+ entries, e = linuxdmabuf.ReadFormatTable(fd, size)
186+ noerr(t, e)
187+ feedback = append(feedback, "table")
188+ })
189+ fb.OnTrancheTargetDevice(func(dev []byte) { feedback = append(feedback, "target") })
190+ fb.OnTrancheFormats(func(data []byte) {
191+ indices, e = linuxdmabuf.TrancheIndices(data, len(entries))
192+ noerr(t, e)
193+ feedback = append(feedback, "formats")
194+ })
195+ fb.OnTrancheFlags(func(flags uint32) {
196+ if flags != 1 {
197+ t.Fatal(flags)
198+ }
199+ feedback = append(feedback, "flags")
200+ })
201+ fb.OnTrancheDone(func() { feedback = append(feedback, "tranche") })
202+ fb.OnDone(func() { feedback = append(feedback, "done") })
203+ send(t, p, payload(fb.ID(), 2, arr(make([]byte, 8))...))
204+ noerr(t, d.Dispatch())
205+ send(t, p, msg(fb.ID(), 1, 32), int(table.Fd()))
206+ noerr(t, d.Dispatch())
207+ send(t, p, payload(fb.ID(), 4, arr(make([]byte, 8))...))
208+ noerr(t, d.Dispatch())
209+ send(t, p, payload(fb.ID(), 5, arr([]byte{0, 0, 1, 0})...))
210+ noerr(t, d.Dispatch())
211+ send(t, p, msg(fb.ID(), 6, 1))
212+ noerr(t, d.Dispatch())
213+ send(t, p, msg(fb.ID(), 3))
214+ noerr(t, d.Dispatch())
215+ send(t, p, msg(fb.ID(), 0))
216+ noerr(t, d.Dispatch())
217+ if len(entries) != 2 || entries[1].Modifier != 42 || len(indices) != 2 || indices[1] != 1 || strings.Join(feedback, ",") != "main,table,target,formats,flags,tranche,done" {
218+ t.Fatalf("feedback %v %v %v", entries, indices, feedback)
219+ }
220+ noerr(t, fb.Destroy())
221+ request(t, p)
222+ params, e := dm.CreateParams()
223+ noerr(t, e)
224+ request(t, p)
225+ f, e := os.Open("/dev/null")
226+ noerr(t, e)
227+ noerr(t, params.Add(int(f.Fd()), 0, 0, 4, 0, 0))
228+ request(t, p)
229+ if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil {
230+ t.Fatal("sent fd not closed")
231+ }
232+ f.Close()
233+ var buffer *core.Buffer
234+ params.OnCreated(func(b *core.Buffer) { buffer = b })
235+ params.OnFailed(func() { sequence = append(sequence, "failed") })
236+ noerr(t, params.Create(1, 1, 0x34325258, 0))
237+ request(t, p)
238+ send(t, p, msg(params.ID(), 0, 999))
239+ noerr(t, d.Dispatch())
240+ if buffer == nil || buffer.ID() != 999 {
241+ t.Fatal("typed buffer missing")
242+ }
243+ noerr(t, buffer.Destroy())
244+ request(t, p)
245+ noerr(t, params.Destroy())
246+ request(t, p)
247+ failedParams, e := dm.CreateParams()
248+ noerr(t, e)
249+ request(t, p)
250+ failedParams.OnFailed(func() { sequence = append(sequence, "failed") })
251+ noerr(t, failedParams.Create(1, 1, 0x34325258, 0))
252+ request(t, p)
253+ send(t, p, msg(failedParams.ID(), 1))
254+ noerr(t, d.Dispatch())
255+ if sequence[len(sequence)-1] != "failed" {
256+ t.Fatal(sequence)
257+ }
258+ noerr(t, failedParams.Destroy())
259+ request(t, p)
260+ immedParams, e := dm.CreateParams()
261+ noerr(t, e)
262+ request(t, p)
263+ immediate, e := immedParams.CreateImmed(1, 1, 0x34325258, 0)
264+ noerr(t, e)
265+ request(t, p)
266+ noerr(t, immediate.Destroy())
267+ request(t, p)
268+ noerr(t, immedParams.Destroy())
269+ request(t, p)
270+ noerr(t, dm.Destroy())
271+ request(t, p)
272+ sync := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context())
273+ bind(t, d, p, 3, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, sync)
274+ ss, e := sync.GetSurface(surface)
275+ noerr(t, e)
276+ request(t, p)
277+ f, e = os.Open("/dev/null")
278+ noerr(t, e)
279+ tl, e := sync.ImportTimeline(int(f.Fd()))
280+ noerr(t, e)
281+ request(t, p)
282+ if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil {
283+ t.Fatal("timeline fd not closed")
284+ }
285+ f.Close()
286+ noerr(t, ss.SetAcquirePoint(tl, 0x123, 0x456))
287+ _, op, b = request(t, p)
288+ if op != 1 || binary.NativeEndian.Uint32(b[4:]) != 0x123 || binary.NativeEndian.Uint32(b[8:]) != 0x456 {
289+ t.Fatalf("acquire %d %x", op, b)
290+ }
291+ noerr(t, ss.SetReleasePoint(tl, 0x789, 0xabc))
292+ _, op, b = request(t, p)
293+ if op != 2 || binary.NativeEndian.Uint32(b[4:]) != 0x789 {
294+ t.Fatalf("release %d %x", op, b)
295+ }
296+ noerr(t, ss.Destroy())
297+ request(t, p)
298+ noerr(t, tl.Destroy())
299+ request(t, p)
300+ noerr(t, sync.Destroy())
301+ request(t, p)
302+ vp := viewporter.NewWpViewporter(d.Context())
303+ bind(t, d, p, 4, viewporter.WpViewporterInterface, 1, vp)
304+ view, e := vp.GetViewport(surface)
305+ noerr(t, e)
306+ request(t, p)
307+ noerr(t, view.Destroy())
308+ request(t, p)
309+ noerr(t, vp.Destroy())
310+ request(t, p)
311+ fs := fractionalscale.NewWpFractionalScaleManager(d.Context())
312+ bind(t, d, p, 5, fractionalscale.WpFractionalScaleManagerInterface, 1, fs)
313+ scale, e := fs.GetFractionalScale(surface)
314+ noerr(t, e)
315+ request(t, p)
316+ var preferred uint32
317+ scale.OnPreferredScale(func(v uint32) { preferred = v })
318+ send(t, p, msg(scale.ID(), 0, 150))
319+ noerr(t, d.Dispatch())
320+ if preferred != 150 {
321+ t.Fatal(preferred)
322+ }
323+ noerr(t, scale.Destroy())
324+ request(t, p)
325+ noerr(t, fs.Destroy())
326+ request(t, p)
327+ ti := textinput.NewTextInputManagerV3(d.Context())
328+ bind(t, d, p, 6, textinput.TextInputManagerV3Interface, 1, ti)
329+ input, e := ti.GetTextInput(seat)
330+ noerr(t, e)
331+ request(t, p)
332+ noerr(t, input.Enable())
333+ request(t, p)
334+ noerr(t, input.SetSurroundingText("hello", 5, 5))
335+ request(t, p)
336+ noerr(t, input.Commit())
337+ request(t, p)
338+ var events []string
339+ input.OnPreeditString(func(s string, _, _ int32) { events = append(events, "preedit:"+s) })
340+ input.OnCommitString(func(s string) { events = append(events, "commit:"+s) })
341+ input.OnDeleteSurroundingText(func(_, _ uint32) { events = append(events, "delete") })
342+ input.OnDone(func(uint32) { events = append(events, "done") })
343+ send(t, p, payload(input.ID(), 2, append(str("é"), msg(0, 0, 0, 0)[8:]...)...))
344+ noerr(t, d.Dispatch())
345+ send(t, p, payload(input.ID(), 3, str("hi")...))
346+ noerr(t, d.Dispatch())
347+ send(t, p, msg(input.ID(), 4, 1, 2))
348+ noerr(t, d.Dispatch())
349+ send(t, p, msg(input.ID(), 5, 1))
350+ noerr(t, d.Dispatch())
351+ if strings.Join(events, ",") != "preedit:é,commit:hi,delete,done" {
352+ t.Fatal(events)
353+ }
354+ noerr(t, input.Disable())
355+ request(t, p)
356+ noerr(t, input.Commit())
357+ request(t, p)
358+ noerr(t, input.Destroy())
359+ request(t, p)
360+ noerr(t, ti.Destroy())
361+ request(t, p)
362+ // Core data-device remains in protocol/core; verify its typed child and
363+ // both directions of clipboard descriptor ownership.
364+ mgr := core.NewDataDeviceManager(d.Context())
365+ bind(t, d, p, 7, core.DataDeviceManagerInterface, 3, mgr)
366+ device, e := mgr.GetDataDevice(seat)
367+ noerr(t, e)
368+ request(t, p)
369+ source, e := mgr.CreateDataSource()
370+ noerr(t, e)
371+ request(t, p)
372+ var offer *core.DataOffer
373+ device.OnDataOffer(func(o *core.DataOffer) { offer = o })
374+ send(t, p, msg(device.ID(), 0, 998))
375+ noerr(t, d.Dispatch())
376+ if offer == nil || offer.ID() != 998 {
377+ t.Fatal("offer not typed")
378+ }
379+ read, write, e := os.Pipe()
380+ noerr(t, e)
381+ defer read.Close()
382+ noerr(t, offer.Receive("text/plain", int(write.Fd())))
383+ request(t, p)
384+ if _, e := unix.FcntlInt(write.Fd(), unix.F_GETFD, 0); e == nil {
385+ t.Fatal("clipboard sent fd retained")
386+ }
387+ write.Close()
388+ source.OnSend(func(mime string, fd *wlturbo.OwnedFD) {
389+ if mime != "text/plain" {
390+ t.Fatal(mime)
391+ }
392+ n, e := fd.Take()
393+ noerr(t, e)
394+ unix.Close(n)
395+ })
396+ received, e := os.Open("/dev/null")
397+ noerr(t, e)
398+ send(t, p, payload(source.ID(), 1, str("text/plain")...), int(received.Fd()))
399+ noerr(t, d.Dispatch())
400+ received.Close()
401+ noerr(t, offer.Destroy())
402+ request(t, p)
403+ noerr(t, source.Destroy())
404+ request(t, p)
405+ noerr(t, device.Release())
406+ request(t, p)
407+ noerr(t, mgr.Release())
408+ request(t, p)
409+
410+}
411+
412+func TestLiveFeedback(t *testing.T) {
413+ if os.Getenv("WLTURBO_LIVE") != "1" {
414+ t.Skip("set WLTURBO_LIVE=1")
415+ }
416+ d, e := wlturbo.Connect("")
417+ noerr(t, e)
418+ defer d.Close()
419+ noerr(t, d.Roundtrip())
420+ for _, iface := range []string{xdgshell.XdgWmBaseInterface, linuxdmabuf.LinuxDmabufInterface, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, viewporter.WpViewporterInterface, fractionalscale.WpFractionalScaleManagerInterface, textinput.TextInputManagerV3Interface} {
421+ g, ok := d.Registry().FindGlobal(iface)
422+ if !ok {
423+ t.Logf("%s absent", iface)
424+ continue
425+ }
426+ v := g.Version
427+ cap := uint32(1)
428+ if iface == xdgshell.XdgWmBaseInterface {
429+ cap = 7
430+ }
431+ if iface == linuxdmabuf.LinuxDmabufInterface {
432+ cap = 4
433+ }
434+ if v > cap {
435+ v = cap
436+ }
437+ t.Logf("%s negotiated=%d (server=%d)", iface, v, g.Version)
438+ if iface != linuxdmabuf.LinuxDmabufInterface {
439+ continue
440+ }
441+ if v < 4 {
442+ t.Fatal("dmabuf feedback requires v4")
443+ }
444+ dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
445+ noerr(t, d.Registry().Bind(g.Name, iface, v, dm))
446+ fb, e := dm.GetDefaultFeedback()
447+ noerr(t, e)
448+ done := false
449+ fb.OnMainDevice(func(b []byte) { t.Logf("main_device=%x", b) })
450+ fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
451+ entries, err := linuxdmabuf.ReadFormatTable(fd, size)
452+ if err != nil {
453+ t.Errorf("table: %v", err)
454+ }
455+ t.Logf("format-table entries=%d", len(entries))
456+ })
457+ fb.OnDone(func() { done = true })
458+ for i := 0; i < 100 && !done; i++ {
459+ noerr(t, d.Dispatch())
460+ }
461+ if !done {
462+ t.Fatal("no feedback done")
463+ }
464+ noerr(t, fb.Destroy())
465+ noerr(t, dm.Destroy())
466+ }
467+}
A protocol/linuxdmabuf/table.go +68 -0
@@ -0,0 +1,68 @@
1+package linuxdmabuf
2+
3+import (
4+ "encoding/binary"
5+ "fmt"
6+ "io"
7+
8+ "github.com/bnema/wlturbo/wl"
9+ "golang.org/x/sys/unix"
10+)
11+
12+// FormatEntry is one 16-byte format-table record. Padding is ignored.
13+type FormatEntry struct {
14+ Format uint32
15+ Modifier uint64
16+}
17+
18+// ReadFormatTable consumes an owned format_table descriptor, even on error.
19+// It reads a bounded snapshot with pread rather than mapping compositor-owned
20+// memory, avoiding SIGBUS if the backing file is unexpectedly truncated.
21+func ReadFormatTable(fd *wl.OwnedFD, size uint32) ([]FormatEntry, error) {
22+ if fd == nil {
23+ return nil, fmt.Errorf("missing format table descriptor")
24+ }
25+ n, err := fd.Take()
26+ if err != nil {
27+ return nil, err
28+ }
29+ defer unix.Close(n)
30+ if size%16 != 0 || size > 16<<20 {
31+ return nil, fmt.Errorf("invalid format table size %d", size)
32+ }
33+ b := make([]byte, size)
34+ for off := 0; off < len(b); {
35+ read, e := unix.Pread(n, b[off:], int64(off))
36+ off += read
37+ if e == unix.EINTR {
38+ continue
39+ }
40+ if e != nil {
41+ return nil, e
42+ }
43+ if read == 0 {
44+ return nil, io.ErrUnexpectedEOF
45+ }
46+ }
47+ entries := make([]FormatEntry, size/16)
48+ for i := range entries {
49+ off := i * 16
50+ entries[i] = FormatEntry{binary.NativeEndian.Uint32(b[off:]), binary.NativeEndian.Uint64(b[off+8:])}
51+ }
52+ return entries, nil
53+}
54+
55+// TrancheIndices decodes the uint16 indices into a format table.
56+func TrancheIndices(data []byte, count int) ([]uint16, error) {
57+ if len(data)%2 != 0 {
58+ return nil, fmt.Errorf("odd tranche indices length")
59+ }
60+ indices := make([]uint16, len(data)/2)
61+ for i := range indices {
62+ indices[i] = binary.NativeEndian.Uint16(data[i*2:])
63+ if int(indices[i]) >= count {
64+ return nil, fmt.Errorf("format table index %d out of range", indices[i])
65+ }
66+ }
67+ return indices, nil
68+}
@@ -0,0 +1,68 @@
1+package linuxdmabuf
2+
3+import (
4+ "encoding/binary"
5+ "fmt"
6+ "io"
7+
8+ "github.com/bnema/wlturbo/wl"
9+ "golang.org/x/sys/unix"
10+)
11+
12+// FormatEntry is one 16-byte format-table record. Padding is ignored.
13+type FormatEntry struct {
14+ Format uint32
15+ Modifier uint64
16+}
17+
18+// ReadFormatTable consumes an owned format_table descriptor, even on error.
19+// It reads a bounded snapshot with pread rather than mapping compositor-owned
20+// memory, avoiding SIGBUS if the backing file is unexpectedly truncated.
21+func ReadFormatTable(fd *wl.OwnedFD, size uint32) ([]FormatEntry, error) {
22+ if fd == nil {
23+ return nil, fmt.Errorf("missing format table descriptor")
24+ }
25+ n, err := fd.Take()
26+ if err != nil {
27+ return nil, err
28+ }
29+ defer unix.Close(n)
30+ if size%16 != 0 || size > 16<<20 {
31+ return nil, fmt.Errorf("invalid format table size %d", size)
32+ }
33+ b := make([]byte, size)
34+ for off := 0; off < len(b); {
35+ read, e := unix.Pread(n, b[off:], int64(off))
36+ off += read
37+ if e == unix.EINTR {
38+ continue
39+ }
40+ if e != nil {
41+ return nil, e
42+ }
43+ if read == 0 {
44+ return nil, io.ErrUnexpectedEOF
45+ }
46+ }
47+ entries := make([]FormatEntry, size/16)
48+ for i := range entries {
49+ off := i * 16
50+ entries[i] = FormatEntry{binary.NativeEndian.Uint32(b[off:]), binary.NativeEndian.Uint64(b[off+8:])}
51+ }
52+ return entries, nil
53+}
54+
55+// TrancheIndices decodes the uint16 indices into a format table.
56+func TrancheIndices(data []byte, count int) ([]uint16, error) {
57+ if len(data)%2 != 0 {
58+ return nil, fmt.Errorf("odd tranche indices length")
59+ }
60+ indices := make([]uint16, len(data)/2)
61+ for i := range indices {
62+ indices[i] = binary.NativeEndian.Uint16(data[i*2:])
63+ if int(indices[i]) >= count {
64+ return nil, fmt.Errorf("format table index %d out of range", indices[i])
65+ }
66+ }
67+ return indices, nil
68+}
A protocol/linuxdmabuf/table_test.go +28 -0
@@ -0,0 +1,28 @@
1+//go:build linux
2+
3+package linuxdmabuf_test
4+
5+import (
6+ "encoding/binary"
7+ "github.com/bnema/wlturbo/protocol/linuxdmabuf"
8+ "testing"
9+)
10+
11+func TestTrancheIndices(t *testing.T) {
12+ b := make([]byte, 4)
13+ binary.NativeEndian.PutUint16(b, 1)
14+ binary.NativeEndian.PutUint16(b[2:], 0)
15+ got, e := linuxdmabuf.TrancheIndices(b, 2)
16+ if e != nil || len(got) != 2 || got[0] != 1 || got[1] != 0 {
17+ t.Fatalf("indices %v %v", got, e)
18+ }
19+ if _, e = linuxdmabuf.TrancheIndices(b, 1); e == nil {
20+ t.Fatal("out of range accepted")
21+ }
22+ if _, e = linuxdmabuf.TrancheIndices(b[:3], 2); e == nil {
23+ t.Fatal("odd length accepted")
24+ }
25+ if _, e = linuxdmabuf.ReadFormatTable(nil, 0); e == nil {
26+ t.Fatal("missing fd accepted")
27+ }
28+}
@@ -0,0 +1,28 @@
1+//go:build linux
2+
3+package linuxdmabuf_test
4+
5+import (
6+ "encoding/binary"
7+ "github.com/bnema/wlturbo/protocol/linuxdmabuf"
8+ "testing"
9+)
10+
11+func TestTrancheIndices(t *testing.T) {
12+ b := make([]byte, 4)
13+ binary.NativeEndian.PutUint16(b, 1)
14+ binary.NativeEndian.PutUint16(b[2:], 0)
15+ got, e := linuxdmabuf.TrancheIndices(b, 2)
16+ if e != nil || len(got) != 2 || got[0] != 1 || got[1] != 0 {
17+ t.Fatalf("indices %v %v", got, e)
18+ }
19+ if _, e = linuxdmabuf.TrancheIndices(b, 1); e == nil {
20+ t.Fatal("out of range accepted")
21+ }
22+ if _, e = linuxdmabuf.TrancheIndices(b[:3], 2); e == nil {
23+ t.Fatal("odd length accepted")
24+ }
25+ if _, e = linuxdmabuf.ReadFormatTable(nil, 0); e == nil {
26+ t.Fatal("missing fd accepted")
27+ }
28+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close