1package tray
2
3import (
4 "bufio"
5 "encoding/binary"
6 "io"
7 "net"
8 "path/filepath"
9 "testing"
10
11 "github.com/bnema/zerobus"
12)
13
14// wb builds D-Bus wire data by hand (little endian), so tests can send bodies
15// that zerobus's own encoder refuses to produce: truncated or inconsistent.
16type wb struct{ b []byte }
17
18func (w *wb) pad(n int) {
19 for len(w.b)%n != 0 {
20 w.b = append(w.b, 0)
21 }
22}
23
24func (w *wb) u32(v uint32) { w.pad(4); w.b = binary.LittleEndian.AppendUint32(w.b, v) }
25func (w *wb) i32(v int32) { w.u32(uint32(v)) }
26
27func (w *wb) str(s string) {
28 w.u32(uint32(len(s)))
29 w.b = append(append(w.b, s...), 0)
30}
31
32func (w *wb) sig(s string) { w.b = append(append(w.b, byte(len(s))), append([]byte(s), 0)...) }
33
34// lenPrefixed writes an array whose data starts aligned to align and is filled by fn.
35func (w *wb) lenPrefixed(align int, fn func()) {
36 w.u32(0)
37 at := len(w.b) - 4
38 w.pad(align)
39 start := len(w.b)
40 fn()
41 binary.LittleEndian.PutUint32(w.b[at:], uint32(len(w.b)-start))
42}
43
44// layoutBody is the body of a GetLayout reply for root: the revision, then the tree.
45func layoutBody(root *tnode) []byte {
46 var w wb
47 w.u32(1)
48 w.node(root)
49 return w.b
50}
51
52func (w *wb) node(n *tnode) {
53 w.pad(8)
54 w.i32(n.id)
55 w.lenPrefixed(8, func() {
56 for _, p := range n.props {
57 w.pad(8)
58 w.str(p.k)
59 switch v := p.v.(type) {
60 case string:
61 w.sig("s")
62 w.str(v)
63 case bool:
64 w.sig("b")
65 if v {
66 w.u32(1)
67 } else {
68 w.u32(0)
69 }
70 case int32:
71 w.sig("i")
72 w.i32(v)
73 }
74 }
75 })
76 w.lenPrefixed(1, func() {
77 if n.junk {
78 w.sig("s")
79 w.str("not a node")
80 }
81 for i := range n.kids {
82 w.sig(layoutSig)
83 w.node(&n.kids[i])
84 }
85 })
86}
87
88// frame is a method return of the given serials, with signature sig and body.
89func frame(serial, replySerial uint32, sig string, body []byte) []byte {
90 // Header field 5 (REPLY_SERIAL, u) and 8 (SIGNATURE, g).
91 var fields wb
92 fields.pad(8)
93 fields.b = append(fields.b, 5)
94 fields.sig("u")
95 fields.u32(replySerial)
96 if sig != "" {
97 fields.pad(8)
98 fields.b = append(fields.b, 8)
99 fields.sig("g")
100 fields.sig(sig)
101 }
102 h := []byte{'l', 2, 0, 1}
103 h = binary.LittleEndian.AppendUint32(h, uint32(len(body)))
104 h = binary.LittleEndian.AppendUint32(h, serial)
105 h = binary.LittleEndian.AppendUint32(h, uint32(len(fields.b)))
106 h = append(h, fields.b...)
107 for len(h)%8 != 0 {
108 h = append(h, 0)
109 }
110 return append(h, body...)
111}
112
113// reply is what the raw bus answers to the next method call.
114type reply struct {
115 sig string
116 body []byte
117}
118
119// rawBus is a bus with one client that answers every method call after Hello
120// with the next queued reply. It returns the address to dial.
121func rawBus(t testing.TB) (addr string, next chan<- reply) {
122 t.Helper()
123 sock := filepath.Join(t.TempDir(), "bus")
124 l, err := net.Listen("unix", sock)
125 if err != nil {
126 t.Fatal(err)
127 }
128 t.Cleanup(func() { _ = l.Close() })
129 ch := make(chan reply, 16)
130 go func() {
131 c, err := l.Accept()
132 if err != nil {
133 return
134 }
135 defer c.Close()
136 r := bufio.NewReader(c)
137 if _, err = r.ReadString('\n'); err != nil { // \0AUTH EXTERNAL ...
138 return
139 }
140 if _, err = io.WriteString(c, "OK 0123456789abcdef\r\n"); err != nil {
141 return
142 }
143 if _, err = r.ReadString('\n'); err != nil { // BEGIN
144 return
145 }
146 var hello wb
147 hello.str(":1.7")
148 serial := uint32(1)
149 first := true
150 for {
151 var hdr [16]byte
152 if _, err = io.ReadFull(r, hdr[:]); err != nil {
153 return
154 }
155 fields := binary.LittleEndian.Uint32(hdr[12:])
156 rest := (16+int(fields)+7)/8*8 - 16 + int(binary.LittleEndian.Uint32(hdr[4:]))
157 if _, err = io.CopyN(io.Discard, r, int64(rest)); err != nil {
158 return
159 }
160 callSerial := binary.LittleEndian.Uint32(hdr[8:])
161 var out []byte
162 if first {
163 first = false
164 out = frame(serial, callSerial, "s", hello.b)
165 } else {
166 rp, ok := <-ch
167 if !ok {
168 return
169 }
170 out = frame(serial, callSerial, rp.sig, rp.body)
171 }
172 serial++
173 if _, err = c.Write(out); err != nil {
174 return
175 }
176 }
177 }()
178 return "unix:path=" + sock, ch
179}
180
181// rawDial connects to a rawBus.
182func rawDial(t testing.TB, addr string) *zerobus.Conn {
183 t.Helper()
184 c, err := zerobus.Dial(addr)
185 if err != nil {
186 t.Fatal(err)
187 }
188 t.Cleanup(func() { _ = c.Close() })
189 return c
190}