detached 731ad81

feat: add zero-allocation D-Bus wire format, connection and tray example

commit
731ad813f09f53aa5b854c41bf653ddec8076cc4
author
brice · · 2026-10-08 17:25 UTC
parent
2b1569e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +2148 -7
A .github/workflows/ci.yml +29 -0
@@ -0,0 +1,29 @@
1+name: CI
2+on:
3+ push:
4+ branches: [main]
5+ pull_request:
6+permissions:
7+ contents: read
8+concurrency:
9+ group: ci-${{ github.ref }}
10+ cancel-in-progress: true
11+jobs:
12+ check:
13+ runs-on: ubuntu-latest
14+ steps:
15+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
16+ with:
17+ persist-credentials: false
18+ - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
19+ with:
20+ go-version-file: go.mod
21+ # dbus-daemon runs the integration tests against a private bus.
22+ - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends dbus
23+ - run: test -z "$(gofmt -l .)"
24+ - run: go vet ./...
25+ - run: go test -race -count=1 ./...
26+ - name: Fuzz smoke
27+ run: |
28+ go test -run '^$' -fuzz '^FuzzParse$' -fuzztime 20s .
29+ go test -run '^$' -fuzz '^FuzzSkip$' -fuzztime 20s .
@@ -0,0 +1,29 @@
1+name: CI
2+on:
3+ push:
4+ branches: [main]
5+ pull_request:
6+permissions:
7+ contents: read
8+concurrency:
9+ group: ci-${{ github.ref }}
10+ cancel-in-progress: true
11+jobs:
12+ check:
13+ runs-on: ubuntu-latest
14+ steps:
15+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
16+ with:
17+ persist-credentials: false
18+ - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
19+ with:
20+ go-version-file: go.mod
21+ # dbus-daemon runs the integration tests against a private bus.
22+ - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends dbus
23+ - run: test -z "$(gofmt -l .)"
24+ - run: go vet ./...
25+ - run: go test -race -count=1 ./...
26+ - name: Fuzz smoke
27+ run: |
28+ go test -run '^$' -fuzz '^FuzzParse$' -fuzztime 20s .
29+ go test -run '^$' -fuzz '^FuzzSkip$' -fuzztime 20s .
M README.md +105 -7
@@ -2,17 +2,115 @@
22
33 A small D-Bus client for Go that does not allocate once it is running.
44
5-**Status: early development.** The API is not usable yet and will change.
5+```sh
6+go get github.com/bnema/zerobus
7+```
68
7-## Goals
9+- **Zero allocations per message.** After the first few messages warm up its buffers, a call, a reply or a signal costs no heap allocation.
10+- **Zero-copy reads.** Strings and byte arrays point straight into the receive buffer.
11+- **Standard library only.**
12+- **Small.** It connects over a Unix socket, authenticates with `EXTERNAL`, calls methods, emits signals, answers calls, and adds match rules. Nothing else.
813
9-- **Zero allocations in steady state.** Reading signals and making calls reuses buffers; decoded strings and arrays point into the receive buffer.
10-- **Only the standard library** (plus `golang.org/x/sys` if needed).
11-- **A small client.** It connects to the session or system bus over a Unix socket, authenticates with `EXTERNAL`, calls methods, reads properties, matches signals and exports simple objects.
14+**Status: v0.x.** The API may change between minor versions.
1215
13-## Not a goal
16+## Call a method
1417
15-- A full D-Bus implementation: no TCP transports, no reflection-based marshalling, no code generation.
18+```go
19+c, err := zerobus.SessionBus()
20+if err != nil {
21+ log.Fatal(err)
22+}
23+defer c.Close()
24+
25+e := c.NewCall("org.freedesktop.DBus", "/org/freedesktop/DBus",
26+ "org.freedesktop.DBus", "GetNameOwner", "s")
27+e.Str("org.freedesktop.Notifications")
28+
29+m, err := c.Call()
30+if err != nil {
31+ log.Fatal(err) // a D-Bus error reply is a *zerobus.Error
32+}
33+owner := m.Body().Str()
34+```
35+
36+Build a message with `NewCall`, `NewSignal`, `NewReply` or `NewError`, write its body in the order of its signature, then send it with `Call` (wait for the reply) or `Send` (do not wait). `Send` checks that the body matches the signature and refuses invalid names and strings before they reach the bus.
37+
38+## Read values
39+
40+Read values in the order of the signature. Errors are sticky: check `Err()` once at the end.
41+
42+```go
43+r := m.Body() // signature "a{sv}"
44+end := r.Array('{')
45+for r.More(end) {
46+ r.Struct()
47+ key := r.Str()
48+ switch sig := r.Variant(); sig {
49+ case "s":
50+ fmt.Println(key, r.Str())
51+ default:
52+ r.Skip(sig) // skip values you do not need
53+ }
54+}
55+if err := r.Err(); err != nil {
56+ return err
57+}
58+```
59+
60+**A received message is valid only until the next `ReadMessage` or `Call`.** The strings and byte slices it returns point into the receive buffer and are overwritten by the next message. Copy what you keep:
61+
62+```go
63+name := strings.Clone(r.Str())
64+icon := bytes.Clone(r.ByteArray())
65+```
66+
67+## Receive signals
68+
69+```go
70+c.AddMatch("type='signal',interface='org.kde.StatusNotifierItem'")
71+for {
72+ m, err := c.ReadMessage()
73+ if err != nil {
74+ return err
75+ }
76+ if m.Type == zerobus.TypeSignal {
77+ fmt.Println(m.Sender, m.Member)
78+ }
79+}
80+```
81+
82+`Call` reads messages until its reply arrives. Set `Conn.Handle` to receive the signals that arrive meanwhile; they are dropped otherwise.
83+
84+## Answer calls
85+
86+```go
87+c.RequestName("org.example.Echo", zerobus.NameFlagDoNotQueue)
88+for {
89+ m, err := c.ReadMessage()
90+ if err != nil {
91+ return err
92+ }
93+ if m.Type == zerobus.TypeMethodCall && m.Member == "Echo" {
94+ c.NewReply(m, "s").Str(m.Body().Str())
95+ c.Send()
96+ }
97+}
98+```
99+
100+## Concurrency
101+
102+A `Conn` is not safe for concurrent use. Use it from one goroutine, usually an event loop around `ReadMessage`. `Close` may be called from any goroutine: a blocked `ReadMessage` then returns `ErrClosed`.
103+
104+## Not supported
105+
106+- Transports other than `unix:path` and `unix:abstract` (no TCP, no `launchd`, no `autolaunch`).
107+- Passing file descriptors (`UNIX_FD` values can be read, but no descriptors are received).
108+- Authentication other than `EXTERNAL`.
109+- Reflection-based marshalling, introspection and code generation.
110+
111+## Example
112+
113+[`examples/trayitems`](examples/trayitems) lists the system tray items on the session bus and prints their changes.
16114
17115 ## License
18116
@@ -2,17 +2,115 @@
2 2
3 A small D-Bus client for Go that does not allocate once it is running.3 A small D-Bus client for Go that does not allocate once it is running.
4 4
5-**Status: early development.** The API is not usable yet and will change.5+```sh
6+go get github.com/bnema/zerobus
7+```
6 8
7-## Goals9+- **Zero allocations per message.** After the first few messages warm up its buffers, a call, a reply or a signal costs no heap allocation.
10+- **Zero-copy reads.** Strings and byte arrays point straight into the receive buffer.
11+- **Standard library only.**
12+- **Small.** It connects over a Unix socket, authenticates with `EXTERNAL`, calls methods, emits signals, answers calls, and adds match rules. Nothing else.
8 13
9-- **Zero allocations in steady state.** Reading signals and making calls reuses buffers; decoded strings and arrays point into the receive buffer.14+**Status: v0.x.** The API may change between minor versions.
10-- **Only the standard library** (plus `golang.org/x/sys` if needed).
11-- **A small client.** It connects to the session or system bus over a Unix socket, authenticates with `EXTERNAL`, calls methods, reads properties, matches signals and exports simple objects.
12 15
13-## Not a goal16+## Call a method
14 17
15-- A full D-Bus implementation: no TCP transports, no reflection-based marshalling, no code generation.18+```go
19+c, err := zerobus.SessionBus()
20+if err != nil {
21+ log.Fatal(err)
22+}
23+defer c.Close()
24+
25+e := c.NewCall("org.freedesktop.DBus", "/org/freedesktop/DBus",
26+ "org.freedesktop.DBus", "GetNameOwner", "s")
27+e.Str("org.freedesktop.Notifications")
28+
29+m, err := c.Call()
30+if err != nil {
31+ log.Fatal(err) // a D-Bus error reply is a *zerobus.Error
32+}
33+owner := m.Body().Str()
34+```
35+
36+Build a message with `NewCall`, `NewSignal`, `NewReply` or `NewError`, write its body in the order of its signature, then send it with `Call` (wait for the reply) or `Send` (do not wait). `Send` checks that the body matches the signature and refuses invalid names and strings before they reach the bus.
37+
38+## Read values
39+
40+Read values in the order of the signature. Errors are sticky: check `Err()` once at the end.
41+
42+```go
43+r := m.Body() // signature "a{sv}"
44+end := r.Array('{')
45+for r.More(end) {
46+ r.Struct()
47+ key := r.Str()
48+ switch sig := r.Variant(); sig {
49+ case "s":
50+ fmt.Println(key, r.Str())
51+ default:
52+ r.Skip(sig) // skip values you do not need
53+ }
54+}
55+if err := r.Err(); err != nil {
56+ return err
57+}
58+```
59+
60+**A received message is valid only until the next `ReadMessage` or `Call`.** The strings and byte slices it returns point into the receive buffer and are overwritten by the next message. Copy what you keep:
61+
62+```go
63+name := strings.Clone(r.Str())
64+icon := bytes.Clone(r.ByteArray())
65+```
66+
67+## Receive signals
68+
69+```go
70+c.AddMatch("type='signal',interface='org.kde.StatusNotifierItem'")
71+for {
72+ m, err := c.ReadMessage()
73+ if err != nil {
74+ return err
75+ }
76+ if m.Type == zerobus.TypeSignal {
77+ fmt.Println(m.Sender, m.Member)
78+ }
79+}
80+```
81+
82+`Call` reads messages until its reply arrives. Set `Conn.Handle` to receive the signals that arrive meanwhile; they are dropped otherwise.
83+
84+## Answer calls
85+
86+```go
87+c.RequestName("org.example.Echo", zerobus.NameFlagDoNotQueue)
88+for {
89+ m, err := c.ReadMessage()
90+ if err != nil {
91+ return err
92+ }
93+ if m.Type == zerobus.TypeMethodCall && m.Member == "Echo" {
94+ c.NewReply(m, "s").Str(m.Body().Str())
95+ c.Send()
96+ }
97+}
98+```
99+
100+## Concurrency
101+
102+A `Conn` is not safe for concurrent use. Use it from one goroutine, usually an event loop around `ReadMessage`. `Close` may be called from any goroutine: a blocked `ReadMessage` then returns `ErrClosed`.
103+
104+## Not supported
105+
106+- Transports other than `unix:path` and `unix:abstract` (no TCP, no `launchd`, no `autolaunch`).
107+- Passing file descriptors (`UNIX_FD` values can be read, but no descriptors are received).
108+- Authentication other than `EXTERNAL`.
109+- Reflection-based marshalling, introspection and code generation.
110+
111+## Example
112+
113+[`examples/trayitems`](examples/trayitems) lists the system tray items on the session bus and prints their changes.
16 114
17 ## License115 ## License
18 116
A conn.go +429 -0
@@ -0,0 +1,429 @@
1+package zerobus
2+
3+import (
4+ "bytes"
5+ "errors"
6+ "fmt"
7+ "io"
8+ "net"
9+ "os"
10+ "strconv"
11+ "strings"
12+)
13+
14+const (
15+ busName = "org.freedesktop.DBus"
16+ busPath = "/org/freedesktop/DBus"
17+ maxAuthLine = 512
18+ defaultBufSz = 16 << 10
19+)
20+
21+// ErrClosed reports a read or send on a closed or broken connection.
22+var ErrClosed = errors.New("zerobus: connection closed")
23+
24+// Error is a D-Bus error reply.
25+type Error struct {
26+ Name string // such as org.freedesktop.DBus.Error.ServiceUnknown
27+ Message string // the first string of the body, if any
28+}
29+
30+func (e *Error) Error() string {
31+ if e.Message == "" {
32+ return e.Name
33+ }
34+ return e.Name + ": " + e.Message
35+}
36+
37+// Conn is a connection to a message bus.
38+//
39+// A Conn is not safe for concurrent use: read and send from one goroutine,
40+// usually an event loop around ReadMessage. Close may be called from any
41+// goroutine to stop a blocked read.
42+type Conn struct {
43+ c *net.UnixConn
44+ serial uint32
45+ name string
46+
47+ rbuf []byte // received bytes; rbuf[start:end] are not read yet
48+ start, end int
49+ msg Message
50+ enc Encoder
51+ err error // sticky: the connection is unusable after it
52+
53+ // Handle receives the messages that arrive while Call waits for its
54+ // reply. It may be nil; those messages are then dropped. It must not
55+ // call Call, Send or ReadMessage.
56+ Handle func(*Message)
57+}
58+
59+// SessionBus connects to the session bus named by
60+// DBUS_SESSION_BUS_ADDRESS, or $XDG_RUNTIME_DIR/bus when it is not set.
61+func SessionBus() (*Conn, error) {
62+ addr := os.Getenv("DBUS_SESSION_BUS_ADDRESS")
63+ if addr == "" {
64+ dir := os.Getenv("XDG_RUNTIME_DIR")
65+ if dir == "" {
66+ return nil, errors.New("zerobus: no session bus: DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR are not set")
67+ }
68+ addr = "unix:path=" + dir + "/bus"
69+ }
70+ return Dial(addr)
71+}
72+
73+// SystemBus connects to the system bus named by DBUS_SYSTEM_BUS_ADDRESS, or
74+// the standard socket when it is not set.
75+func SystemBus() (*Conn, error) {
76+ addr := os.Getenv("DBUS_SYSTEM_BUS_ADDRESS")
77+ if addr == "" {
78+ addr = "unix:path=/run/dbus/system_bus_socket"
79+ }
80+ return Dial(addr)
81+}
82+
83+// Dial connects to a bus address such as "unix:path=/run/user/1000/bus",
84+// authenticates, and registers with the bus. Only unix:path and
85+// unix:abstract addresses are supported; with several addresses separated by
86+// ';', the first that connects is used.
87+func Dial(address string) (*Conn, error) {
88+ var errs []error
89+ for a := range strings.SplitSeq(address, ";") {
90+ if a == "" {
91+ continue
92+ }
93+ c, err := dialOne(a)
94+ if err == nil {
95+ return c, nil
96+ }
97+ errs = append(errs, err)
98+ }
99+ if len(errs) == 0 {
100+ return nil, fmt.Errorf("zerobus: empty bus address")
101+ }
102+ return nil, errors.Join(errs...)
103+}
104+
105+func dialOne(addr string) (*Conn, error) {
106+ transport, params, ok := strings.Cut(addr, ":")
107+ if !ok || transport != "unix" {
108+ return nil, fmt.Errorf("zerobus: unsupported address %q", addr)
109+ }
110+ var sock string
111+ for kv := range strings.SplitSeq(params, ",") {
112+ k, v, _ := strings.Cut(kv, "=")
113+ v, err := unescape(v)
114+ if err != nil {
115+ return nil, fmt.Errorf("zerobus: address %q: %w", addr, err)
116+ }
117+ switch k {
118+ case "path":
119+ sock = v
120+ case "abstract":
121+ sock = "@" + v
122+ }
123+ }
124+ if sock == "" {
125+ return nil, fmt.Errorf("zerobus: address %q has no path or abstract socket", addr)
126+ }
127+ uc, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: sock, Net: "unix"})
128+ if err != nil {
129+ return nil, fmt.Errorf("zerobus: %w", err)
130+ }
131+ c := &Conn{c: uc, rbuf: make([]byte, defaultBufSz)}
132+ c.enc.buf = make([]byte, 0, defaultBufSz)
133+ if err := c.auth(); err != nil {
134+ return nil, errors.Join(err, uc.Close())
135+ }
136+ if err := c.hello(); err != nil {
137+ return nil, errors.Join(err, uc.Close())
138+ }
139+ return c, nil
140+}
141+
142+// unescape decodes the %xx escapes of a D-Bus address value.
143+func unescape(s string) (string, error) {
144+ if !strings.Contains(s, "%") {
145+ return s, nil
146+ }
147+ var b strings.Builder
148+ for i := 0; i < len(s); i++ {
149+ if s[i] != '%' {
150+ b.WriteByte(s[i])
151+ continue
152+ }
153+ if i+2 >= len(s) {
154+ return "", errors.New("bad escape")
155+ }
156+ v, err := strconv.ParseUint(s[i+1:i+3], 16, 8)
157+ if err != nil {
158+ return "", errors.New("bad escape")
159+ }
160+ b.WriteByte(byte(v))
161+ i += 2
162+ }
163+ return b.String(), nil
164+}
165+
166+// auth runs the SASL EXTERNAL handshake. The bus checks the peer
167+// credentials of the socket against the user id sent here.
168+func (c *Conn) auth() error {
169+ uid := strconv.Itoa(os.Getuid())
170+ line := fmt.Appendf(nil, "\x00AUTH EXTERNAL %x\r\n", uid)
171+ if _, err := c.c.Write(line); err != nil {
172+ return fmt.Errorf("zerobus: auth: %w", err)
173+ }
174+ reply, err := c.authLine()
175+ if err != nil {
176+ return err
177+ }
178+ if !bytes.HasPrefix(reply, []byte("OK ")) {
179+ return fmt.Errorf("zerobus: auth rejected: %q", reply)
180+ }
181+ if _, err := c.c.Write([]byte("BEGIN\r\n")); err != nil {
182+ return fmt.Errorf("zerobus: auth: %w", err)
183+ }
184+ return nil
185+}
186+
187+// authLine reads one line of the handshake. Bytes after it stay in rbuf for
188+// the message reader.
189+func (c *Conn) authLine() ([]byte, error) {
190+ for {
191+ if i := bytes.Index(c.rbuf[c.start:c.end], []byte("\r\n")); i >= 0 {
192+ line := c.rbuf[c.start : c.start+i]
193+ c.start += i + 2
194+ return line, nil
195+ }
196+ if c.end-c.start >= maxAuthLine {
197+ return nil, errors.New("zerobus: auth: line too long")
198+ }
199+ n, err := c.c.Read(c.rbuf[c.end:])
200+ if err != nil {
201+ return nil, fmt.Errorf("zerobus: auth: %w", err)
202+ }
203+ c.end += n
204+ }
205+}
206+
207+func (c *Conn) hello() error {
208+ c.NewCall(busName, busPath, busName, "Hello", "")
209+ m, err := c.Call()
210+ if err != nil {
211+ return fmt.Errorf("zerobus: hello: %w", err)
212+ }
213+ body := m.Body()
214+ name := body.Str()
215+ if body.Err() != nil {
216+ return fmt.Errorf("zerobus: hello: %w", body.Err())
217+ }
218+ c.name = strings.Clone(name)
219+ return nil
220+}
221+
222+// UniqueName returns the name the bus gave this connection, such as ":1.42".
223+func (c *Conn) UniqueName() string { return c.name }
224+
225+// Close closes the connection. A blocked ReadMessage returns ErrClosed.
226+func (c *Conn) Close() error { return c.c.Close() }
227+
228+func (c *Conn) nextSerial() uint32 {
229+ c.serial++
230+ if c.serial == 0 {
231+ c.serial = 1
232+ }
233+ return c.serial
234+}
235+
236+// NewCall starts a method call and returns the encoder for its body, which
237+// must match sig. Send it with Send or Call. Empty dest and iface are left
238+// out of the header.
239+//
240+// The connection has one encoder: send a message before starting another.
241+func (c *Conn) NewCall(dest, path, iface, member, sig string) *Encoder {
242+ c.enc.begin(TypeMethodCall, c.nextSerial(), 0, dest, path, iface, member, "", sig)
243+ return &c.enc
244+}
245+
246+// NewSignal starts a signal and returns the encoder for its body.
247+func (c *Conn) NewSignal(path, iface, member, sig string) *Encoder {
248+ c.enc.begin(TypeSignal, c.nextSerial(), 0, "", path, iface, member, "", sig)
249+ return &c.enc
250+}
251+
252+// NewReply starts the reply to the method call m and returns the encoder for
253+// its body.
254+func (c *Conn) NewReply(m *Message, sig string) *Encoder {
255+ c.enc.begin(TypeMethodReturn, c.nextSerial(), m.Serial, m.Sender, "", "", "", "", sig)
256+ return &c.enc
257+}
258+
259+// NewError starts an error reply to the method call m. Write a message
260+// string as the body when sig is "s".
261+func (c *Conn) NewError(m *Message, name, sig string) *Encoder {
262+ c.enc.begin(TypeError, c.nextSerial(), m.Serial, m.Sender, "", "", "", name, sig)
263+ return &c.enc
264+}
265+
266+// Send sends the message built since the last NewCall, NewSignal, NewReply
267+// or NewError and returns its serial.
268+func (c *Conn) Send() (uint32, error) {
269+ if c.err != nil {
270+ return 0, c.err
271+ }
272+ b, err := c.enc.finish()
273+ c.enc.body = 0
274+ if err != nil {
275+ return 0, err
276+ }
277+ if _, err := c.c.Write(b); err != nil {
278+ c.err = closedErr(err)
279+ return 0, c.err
280+ }
281+ return readSerial(b), nil
282+}
283+
284+func readSerial(b []byte) uint32 {
285+ return uint32(b[8]) | uint32(b[9])<<8 | uint32(b[10])<<16 | uint32(b[11])<<24
286+}
287+
288+// Call sends the method call being built and waits for its reply. Messages
289+// that arrive meanwhile go to Handle. An error reply returns an *Error.
290+//
291+// The reply is valid until the next read, like ReadMessage's.
292+func (c *Conn) Call() (*Message, error) {
293+ serial, err := c.Send()
294+ if err != nil {
295+ return nil, err
296+ }
297+ for {
298+ m, err := c.ReadMessage()
299+ if err != nil {
300+ return nil, err
301+ }
302+ if m.ReplySerial == serial && (m.Type == TypeMethodReturn || m.Type == TypeError) {
303+ if m.Type == TypeError {
304+ return m, replyError(m)
305+ }
306+ return m, nil
307+ }
308+ if c.Handle != nil {
309+ c.Handle(m)
310+ }
311+ }
312+}
313+
314+// replyError builds an *Error from an error reply. It copies the strings so
315+// the error stays valid; this is the only allocation of an error path.
316+func replyError(m *Message) *Error {
317+ e := &Error{Name: strings.Clone(m.ErrorName)}
318+ if len(m.Signature) > 0 && m.Signature[0] == 's' {
319+ if s := m.Body().Str(); s != "" {
320+ e.Message = strings.Clone(s)
321+ }
322+ }
323+ return e
324+}
325+
326+// ReadMessage blocks until the next message arrives. The message and its
327+// strings are valid until the next call to ReadMessage or Call.
328+func (c *Conn) ReadMessage() (*Message, error) {
329+ if c.err != nil {
330+ return nil, c.err
331+ }
332+ if err := c.fill(headerLen); err != nil {
333+ return nil, err
334+ }
335+ n, err := frameLen(c.rbuf[c.start:])
336+ if err != nil {
337+ c.err = err
338+ return nil, err
339+ }
340+ if err := c.fill(n); err != nil {
341+ return nil, err
342+ }
343+ b := c.rbuf[c.start : c.start+n : c.start+n]
344+ c.start += n
345+ if err := parse(b, &c.msg); err != nil {
346+ c.err = err
347+ return nil, err
348+ }
349+ return &c.msg, nil
350+}
351+
352+// fill reads until rbuf[start:] holds at least n bytes. It moves unread
353+// bytes to the front, which is safe because the previous message is no
354+// longer valid once a new read starts.
355+func (c *Conn) fill(n int) error {
356+ if c.end-c.start >= n {
357+ return nil
358+ }
359+ if c.start > 0 {
360+ c.end = copy(c.rbuf, c.rbuf[c.start:c.end])
361+ c.start = 0
362+ }
363+ if n > len(c.rbuf) {
364+ grown := make([]byte, max(n, 2*len(c.rbuf)))
365+ copy(grown, c.rbuf[:c.end])
366+ c.rbuf = grown
367+ }
368+ for c.end < n {
369+ k, err := c.c.Read(c.rbuf[c.end:])
370+ c.end += k
371+ if err != nil && c.end < n {
372+ c.err = closedErr(err)
373+ return c.err
374+ }
375+ }
376+ return nil
377+}
378+
379+func closedErr(err error) error {
380+ if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
381+ return ErrClosed
382+ }
383+ return fmt.Errorf("zerobus: %w", err)
384+}
385+
386+// AddMatch asks the bus to send this connection the messages that match
387+// rule, such as "type='signal',interface='org.kde.StatusNotifierItem'".
388+func (c *Conn) AddMatch(rule string) error {
389+ c.NewCall(busName, busPath, busName, "AddMatch", "s").Str(rule)
390+ _, err := c.Call()
391+ return err
392+}
393+
394+// RemoveMatch removes a rule added by AddMatch.
395+func (c *Conn) RemoveMatch(rule string) error {
396+ c.NewCall(busName, busPath, busName, "RemoveMatch", "s").Str(rule)
397+ _, err := c.Call()
398+ return err
399+}
400+
401+// Reply codes of RequestName.
402+const (
403+ NamePrimaryOwner = 1
404+ NameInQueue = 2
405+ NameExists = 3
406+ NameAlreadyOwner = 4
407+)
408+
409+// Flags of RequestName.
410+const (
411+ NameFlagAllowReplacement = 0x1
412+ NameFlagReplaceExisting = 0x2
413+ NameFlagDoNotQueue = 0x4
414+)
415+
416+// RequestName asks the bus for a well-known name and returns its reply code,
417+// such as NamePrimaryOwner.
418+func (c *Conn) RequestName(name string, flags uint32) (uint32, error) {
419+ e := c.NewCall(busName, busPath, busName, "RequestName", "su")
420+ e.Str(name)
421+ e.Uint32(flags)
422+ m, err := c.Call()
423+ if err != nil {
424+ return 0, err
425+ }
426+ body := m.Body()
427+ code := body.Uint32()
428+ return code, body.Err()
429+}
@@ -0,0 +1,429 @@
1+package zerobus
2+
3+import (
4+ "bytes"
5+ "errors"
6+ "fmt"
7+ "io"
8+ "net"
9+ "os"
10+ "strconv"
11+ "strings"
12+)
13+
14+const (
15+ busName = "org.freedesktop.DBus"
16+ busPath = "/org/freedesktop/DBus"
17+ maxAuthLine = 512
18+ defaultBufSz = 16 << 10
19+)
20+
21+// ErrClosed reports a read or send on a closed or broken connection.
22+var ErrClosed = errors.New("zerobus: connection closed")
23+
24+// Error is a D-Bus error reply.
25+type Error struct {
26+ Name string // such as org.freedesktop.DBus.Error.ServiceUnknown
27+ Message string // the first string of the body, if any
28+}
29+
30+func (e *Error) Error() string {
31+ if e.Message == "" {
32+ return e.Name
33+ }
34+ return e.Name + ": " + e.Message
35+}
36+
37+// Conn is a connection to a message bus.
38+//
39+// A Conn is not safe for concurrent use: read and send from one goroutine,
40+// usually an event loop around ReadMessage. Close may be called from any
41+// goroutine to stop a blocked read.
42+type Conn struct {
43+ c *net.UnixConn
44+ serial uint32
45+ name string
46+
47+ rbuf []byte // received bytes; rbuf[start:end] are not read yet
48+ start, end int
49+ msg Message
50+ enc Encoder
51+ err error // sticky: the connection is unusable after it
52+
53+ // Handle receives the messages that arrive while Call waits for its
54+ // reply. It may be nil; those messages are then dropped. It must not
55+ // call Call, Send or ReadMessage.
56+ Handle func(*Message)
57+}
58+
59+// SessionBus connects to the session bus named by
60+// DBUS_SESSION_BUS_ADDRESS, or $XDG_RUNTIME_DIR/bus when it is not set.
61+func SessionBus() (*Conn, error) {
62+ addr := os.Getenv("DBUS_SESSION_BUS_ADDRESS")
63+ if addr == "" {
64+ dir := os.Getenv("XDG_RUNTIME_DIR")
65+ if dir == "" {
66+ return nil, errors.New("zerobus: no session bus: DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR are not set")
67+ }
68+ addr = "unix:path=" + dir + "/bus"
69+ }
70+ return Dial(addr)
71+}
72+
73+// SystemBus connects to the system bus named by DBUS_SYSTEM_BUS_ADDRESS, or
74+// the standard socket when it is not set.
75+func SystemBus() (*Conn, error) {
76+ addr := os.Getenv("DBUS_SYSTEM_BUS_ADDRESS")
77+ if addr == "" {
78+ addr = "unix:path=/run/dbus/system_bus_socket"
79+ }
80+ return Dial(addr)
81+}
82+
83+// Dial connects to a bus address such as "unix:path=/run/user/1000/bus",
84+// authenticates, and registers with the bus. Only unix:path and
85+// unix:abstract addresses are supported; with several addresses separated by
86+// ';', the first that connects is used.
87+func Dial(address string) (*Conn, error) {
88+ var errs []error
89+ for a := range strings.SplitSeq(address, ";") {
90+ if a == "" {
91+ continue
92+ }
93+ c, err := dialOne(a)
94+ if err == nil {
95+ return c, nil
96+ }
97+ errs = append(errs, err)
98+ }
99+ if len(errs) == 0 {
100+ return nil, fmt.Errorf("zerobus: empty bus address")
101+ }
102+ return nil, errors.Join(errs...)
103+}
104+
105+func dialOne(addr string) (*Conn, error) {
106+ transport, params, ok := strings.Cut(addr, ":")
107+ if !ok || transport != "unix" {
108+ return nil, fmt.Errorf("zerobus: unsupported address %q", addr)
109+ }
110+ var sock string
111+ for kv := range strings.SplitSeq(params, ",") {
112+ k, v, _ := strings.Cut(kv, "=")
113+ v, err := unescape(v)
114+ if err != nil {
115+ return nil, fmt.Errorf("zerobus: address %q: %w", addr, err)
116+ }
117+ switch k {
118+ case "path":
119+ sock = v
120+ case "abstract":
121+ sock = "@" + v
122+ }
123+ }
124+ if sock == "" {
125+ return nil, fmt.Errorf("zerobus: address %q has no path or abstract socket", addr)
126+ }
127+ uc, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: sock, Net: "unix"})
128+ if err != nil {
129+ return nil, fmt.Errorf("zerobus: %w", err)
130+ }
131+ c := &Conn{c: uc, rbuf: make([]byte, defaultBufSz)}
132+ c.enc.buf = make([]byte, 0, defaultBufSz)
133+ if err := c.auth(); err != nil {
134+ return nil, errors.Join(err, uc.Close())
135+ }
136+ if err := c.hello(); err != nil {
137+ return nil, errors.Join(err, uc.Close())
138+ }
139+ return c, nil
140+}
141+
142+// unescape decodes the %xx escapes of a D-Bus address value.
143+func unescape(s string) (string, error) {
144+ if !strings.Contains(s, "%") {
145+ return s, nil
146+ }
147+ var b strings.Builder
148+ for i := 0; i < len(s); i++ {
149+ if s[i] != '%' {
150+ b.WriteByte(s[i])
151+ continue
152+ }
153+ if i+2 >= len(s) {
154+ return "", errors.New("bad escape")
155+ }
156+ v, err := strconv.ParseUint(s[i+1:i+3], 16, 8)
157+ if err != nil {
158+ return "", errors.New("bad escape")
159+ }
160+ b.WriteByte(byte(v))
161+ i += 2
162+ }
163+ return b.String(), nil
164+}
165+
166+// auth runs the SASL EXTERNAL handshake. The bus checks the peer
167+// credentials of the socket against the user id sent here.
168+func (c *Conn) auth() error {
169+ uid := strconv.Itoa(os.Getuid())
170+ line := fmt.Appendf(nil, "\x00AUTH EXTERNAL %x\r\n", uid)
171+ if _, err := c.c.Write(line); err != nil {
172+ return fmt.Errorf("zerobus: auth: %w", err)
173+ }
174+ reply, err := c.authLine()
175+ if err != nil {
176+ return err
177+ }
178+ if !bytes.HasPrefix(reply, []byte("OK ")) {
179+ return fmt.Errorf("zerobus: auth rejected: %q", reply)
180+ }
181+ if _, err := c.c.Write([]byte("BEGIN\r\n")); err != nil {
182+ return fmt.Errorf("zerobus: auth: %w", err)
183+ }
184+ return nil
185+}
186+
187+// authLine reads one line of the handshake. Bytes after it stay in rbuf for
188+// the message reader.
189+func (c *Conn) authLine() ([]byte, error) {
190+ for {
191+ if i := bytes.Index(c.rbuf[c.start:c.end], []byte("\r\n")); i >= 0 {
192+ line := c.rbuf[c.start : c.start+i]
193+ c.start += i + 2
194+ return line, nil
195+ }
196+ if c.end-c.start >= maxAuthLine {
197+ return nil, errors.New("zerobus: auth: line too long")
198+ }
199+ n, err := c.c.Read(c.rbuf[c.end:])
200+ if err != nil {
201+ return nil, fmt.Errorf("zerobus: auth: %w", err)
202+ }
203+ c.end += n
204+ }
205+}
206+
207+func (c *Conn) hello() error {
208+ c.NewCall(busName, busPath, busName, "Hello", "")
209+ m, err := c.Call()
210+ if err != nil {
211+ return fmt.Errorf("zerobus: hello: %w", err)
212+ }
213+ body := m.Body()
214+ name := body.Str()
215+ if body.Err() != nil {
216+ return fmt.Errorf("zerobus: hello: %w", body.Err())
217+ }
218+ c.name = strings.Clone(name)
219+ return nil
220+}
221+
222+// UniqueName returns the name the bus gave this connection, such as ":1.42".
223+func (c *Conn) UniqueName() string { return c.name }
224+
225+// Close closes the connection. A blocked ReadMessage returns ErrClosed.
226+func (c *Conn) Close() error { return c.c.Close() }
227+
228+func (c *Conn) nextSerial() uint32 {
229+ c.serial++
230+ if c.serial == 0 {
231+ c.serial = 1
232+ }
233+ return c.serial
234+}
235+
236+// NewCall starts a method call and returns the encoder for its body, which
237+// must match sig. Send it with Send or Call. Empty dest and iface are left
238+// out of the header.
239+//
240+// The connection has one encoder: send a message before starting another.
241+func (c *Conn) NewCall(dest, path, iface, member, sig string) *Encoder {
242+ c.enc.begin(TypeMethodCall, c.nextSerial(), 0, dest, path, iface, member, "", sig)
243+ return &c.enc
244+}
245+
246+// NewSignal starts a signal and returns the encoder for its body.
247+func (c *Conn) NewSignal(path, iface, member, sig string) *Encoder {
248+ c.enc.begin(TypeSignal, c.nextSerial(), 0, "", path, iface, member, "", sig)
249+ return &c.enc
250+}
251+
252+// NewReply starts the reply to the method call m and returns the encoder for
253+// its body.
254+func (c *Conn) NewReply(m *Message, sig string) *Encoder {
255+ c.enc.begin(TypeMethodReturn, c.nextSerial(), m.Serial, m.Sender, "", "", "", "", sig)
256+ return &c.enc
257+}
258+
259+// NewError starts an error reply to the method call m. Write a message
260+// string as the body when sig is "s".
261+func (c *Conn) NewError(m *Message, name, sig string) *Encoder {
262+ c.enc.begin(TypeError, c.nextSerial(), m.Serial, m.Sender, "", "", "", name, sig)
263+ return &c.enc
264+}
265+
266+// Send sends the message built since the last NewCall, NewSignal, NewReply
267+// or NewError and returns its serial.
268+func (c *Conn) Send() (uint32, error) {
269+ if c.err != nil {
270+ return 0, c.err
271+ }
272+ b, err := c.enc.finish()
273+ c.enc.body = 0
274+ if err != nil {
275+ return 0, err
276+ }
277+ if _, err := c.c.Write(b); err != nil {
278+ c.err = closedErr(err)
279+ return 0, c.err
280+ }
281+ return readSerial(b), nil
282+}
283+
284+func readSerial(b []byte) uint32 {
285+ return uint32(b[8]) | uint32(b[9])<<8 | uint32(b[10])<<16 | uint32(b[11])<<24
286+}
287+
288+// Call sends the method call being built and waits for its reply. Messages
289+// that arrive meanwhile go to Handle. An error reply returns an *Error.
290+//
291+// The reply is valid until the next read, like ReadMessage's.
292+func (c *Conn) Call() (*Message, error) {
293+ serial, err := c.Send()
294+ if err != nil {
295+ return nil, err
296+ }
297+ for {
298+ m, err := c.ReadMessage()
299+ if err != nil {
300+ return nil, err
301+ }
302+ if m.ReplySerial == serial && (m.Type == TypeMethodReturn || m.Type == TypeError) {
303+ if m.Type == TypeError {
304+ return m, replyError(m)
305+ }
306+ return m, nil
307+ }
308+ if c.Handle != nil {
309+ c.Handle(m)
310+ }
311+ }
312+}
313+
314+// replyError builds an *Error from an error reply. It copies the strings so
315+// the error stays valid; this is the only allocation of an error path.
316+func replyError(m *Message) *Error {
317+ e := &Error{Name: strings.Clone(m.ErrorName)}
318+ if len(m.Signature) > 0 && m.Signature[0] == 's' {
319+ if s := m.Body().Str(); s != "" {
320+ e.Message = strings.Clone(s)
321+ }
322+ }
323+ return e
324+}
325+
326+// ReadMessage blocks until the next message arrives. The message and its
327+// strings are valid until the next call to ReadMessage or Call.
328+func (c *Conn) ReadMessage() (*Message, error) {
329+ if c.err != nil {
330+ return nil, c.err
331+ }
332+ if err := c.fill(headerLen); err != nil {
333+ return nil, err
334+ }
335+ n, err := frameLen(c.rbuf[c.start:])
336+ if err != nil {
337+ c.err = err
338+ return nil, err
339+ }
340+ if err := c.fill(n); err != nil {
341+ return nil, err
342+ }
343+ b := c.rbuf[c.start : c.start+n : c.start+n]
344+ c.start += n
345+ if err := parse(b, &c.msg); err != nil {
346+ c.err = err
347+ return nil, err
348+ }
349+ return &c.msg, nil
350+}
351+
352+// fill reads until rbuf[start:] holds at least n bytes. It moves unread
353+// bytes to the front, which is safe because the previous message is no
354+// longer valid once a new read starts.
355+func (c *Conn) fill(n int) error {
356+ if c.end-c.start >= n {
357+ return nil
358+ }
359+ if c.start > 0 {
360+ c.end = copy(c.rbuf, c.rbuf[c.start:c.end])
361+ c.start = 0
362+ }
363+ if n > len(c.rbuf) {
364+ grown := make([]byte, max(n, 2*len(c.rbuf)))
365+ copy(grown, c.rbuf[:c.end])
366+ c.rbuf = grown
367+ }
368+ for c.end < n {
369+ k, err := c.c.Read(c.rbuf[c.end:])
370+ c.end += k
371+ if err != nil && c.end < n {
372+ c.err = closedErr(err)
373+ return c.err
374+ }
375+ }
376+ return nil
377+}
378+
379+func closedErr(err error) error {
380+ if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
381+ return ErrClosed
382+ }
383+ return fmt.Errorf("zerobus: %w", err)
384+}
385+
386+// AddMatch asks the bus to send this connection the messages that match
387+// rule, such as "type='signal',interface='org.kde.StatusNotifierItem'".
388+func (c *Conn) AddMatch(rule string) error {
389+ c.NewCall(busName, busPath, busName, "AddMatch", "s").Str(rule)
390+ _, err := c.Call()
391+ return err
392+}
393+
394+// RemoveMatch removes a rule added by AddMatch.
395+func (c *Conn) RemoveMatch(rule string) error {
396+ c.NewCall(busName, busPath, busName, "RemoveMatch", "s").Str(rule)
397+ _, err := c.Call()
398+ return err
399+}
400+
401+// Reply codes of RequestName.
402+const (
403+ NamePrimaryOwner = 1
404+ NameInQueue = 2
405+ NameExists = 3
406+ NameAlreadyOwner = 4
407+)
408+
409+// Flags of RequestName.
410+const (
411+ NameFlagAllowReplacement = 0x1
412+ NameFlagReplaceExisting = 0x2
413+ NameFlagDoNotQueue = 0x4
414+)
415+
416+// RequestName asks the bus for a well-known name and returns its reply code,
417+// such as NamePrimaryOwner.
418+func (c *Conn) RequestName(name string, flags uint32) (uint32, error) {
419+ e := c.NewCall(busName, busPath, busName, "RequestName", "su")
420+ e.Str(name)
421+ e.Uint32(flags)
422+ m, err := c.Call()
423+ if err != nil {
424+ return 0, err
425+ }
426+ body := m.Body()
427+ code := body.Uint32()
428+ return code, body.Err()
429+}
A conn_test.go +249 -0
@@ -0,0 +1,249 @@
1+package zerobus
2+
3+import (
4+ "bufio"
5+ "errors"
6+ "os/exec"
7+ "strings"
8+ "testing"
9+ "time"
10+)
11+
12+// privateBus starts a dbus-daemon for the test and returns its address. The
13+// test is skipped when dbus-daemon is not installed.
14+func privateBus(t *testing.T) string {
15+ t.Helper()
16+ path, err := exec.LookPath("dbus-daemon")
17+ if err != nil {
18+ t.Skip("dbus-daemon not installed")
19+ }
20+ cmd := exec.Command(path, "--session", "--nofork", "--print-address")
21+ out, err := cmd.StdoutPipe()
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ if err := cmd.Start(); err != nil {
26+ t.Fatal(err)
27+ }
28+ t.Cleanup(func() {
29+ _ = cmd.Process.Kill()
30+ _ = cmd.Wait()
31+ })
32+ line := make(chan string, 1)
33+ go func() {
34+ s, _ := bufio.NewReader(out).ReadString('\n')
35+ line <- strings.TrimSpace(s)
36+ }()
37+ select {
38+ case addr := <-line:
39+ if addr == "" {
40+ t.Fatal("dbus-daemon printed no address")
41+ }
42+ return addr
43+ case <-time.After(5 * time.Second):
44+ t.Fatal("dbus-daemon did not start")
45+ }
46+ return ""
47+}
48+
49+func dial(t *testing.T, addr string) *Conn {
50+ t.Helper()
51+ c, err := Dial(addr)
52+ if err != nil {
53+ t.Fatal(err)
54+ }
55+ t.Cleanup(func() { _ = c.Close() })
56+ return c
57+}
58+
59+func TestHelloAndListNames(t *testing.T) {
60+ c := dial(t, privateBus(t))
61+ if !strings.HasPrefix(c.UniqueName(), ":") {
62+ t.Fatalf("unique name %q", c.UniqueName())
63+ }
64+ c.NewCall(busName, busPath, busName, "ListNames", "")
65+ m, err := c.Call()
66+ if err != nil {
67+ t.Fatal(err)
68+ }
69+ r := m.Body()
70+ found := false
71+ end := r.Array('s')
72+ for r.More(end) {
73+ if r.Str() == c.UniqueName() {
74+ found = true
75+ }
76+ }
77+ if !r.Done() || !found {
78+ t.Fatalf("own name not listed: %v", r.Err())
79+ }
80+}
81+
82+func TestErrorReply(t *testing.T) {
83+ c := dial(t, privateBus(t))
84+ c.NewCall("org.example.Missing", "/", "org.example.I", "M", "").SetFlags(FlagNoAutoStart)
85+ _, err := c.Call()
86+ var de *Error
87+ // ServiceUnknown or NameHasNoOwner, depending on the bus.
88+ if !errors.As(err, &de) || !strings.HasPrefix(de.Name, "org.freedesktop.DBus.Error.") || de.Message == "" {
89+ t.Fatalf("err = %v", err)
90+ }
91+ // The connection stays usable after an error reply.
92+ if _, err := c.RequestName("org.example.Zerobus", NameFlagDoNotQueue); err != nil {
93+ t.Fatal(err)
94+ }
95+}
96+
97+func TestSignalAndMethodBetweenPeers(t *testing.T) {
98+ addr := privateBus(t)
99+ server := dial(t, addr)
100+ client := dial(t, addr)
101+
102+ if code, err := server.RequestName("org.example.Echo", NameFlagDoNotQueue); err != nil || code != NamePrimaryOwner {
103+ t.Fatalf("RequestName = %d, %v", code, err)
104+ }
105+ if err := client.AddMatch("type='signal',interface='org.example.Echo'"); err != nil {
106+ t.Fatal(err)
107+ }
108+
109+ // The server answers one Echo call, then emits a signal.
110+ done := make(chan error, 1)
111+ go func() {
112+ for {
113+ m, err := server.ReadMessage()
114+ if err != nil {
115+ done <- err
116+ return
117+ }
118+ if m.Type != TypeMethodCall || m.Member != "Echo" {
119+ continue // NameAcquired and similar
120+ }
121+ s := m.Body().Str()
122+ server.NewReply(m, "s").Str(strings.ToUpper(s))
123+ if _, err := server.Send(); err != nil {
124+ done <- err
125+ return
126+ }
127+ server.NewSignal("/org/example/Echo", "org.example.Echo", "Echoed", "s").Str(s)
128+ _, err = server.Send()
129+ done <- err
130+ return
131+ }
132+ }()
133+
134+ client.NewCall("org.example.Echo", "/org/example/Echo", "org.example.Echo", "Echo", "s").Str("hi")
135+ m, err := client.Call()
136+ if err != nil {
137+ t.Fatal(err)
138+ }
139+ if got := m.Body().Str(); got != "HI" {
140+ t.Fatalf("reply %q", got)
141+ }
142+ if err := <-done; err != nil {
143+ t.Fatal(err)
144+ }
145+ for {
146+ m, err := client.ReadMessage()
147+ if err != nil {
148+ t.Fatal(err)
149+ }
150+ if m.Type == TypeSignal && m.Member == "Echoed" {
151+ if s := m.Body().Str(); s != "hi" || m.Sender != server.UniqueName() {
152+ t.Fatalf("signal %q from %q", s, m.Sender)
153+ }
154+ return
155+ }
156+ }
157+}
158+
159+func TestCloseUnblocksRead(t *testing.T) {
160+ c := dial(t, privateBus(t))
161+ errc := make(chan error, 1)
162+ go func() {
163+ for {
164+ if _, err := c.ReadMessage(); err != nil {
165+ errc <- err
166+ return
167+ }
168+ }
169+ }()
170+ time.Sleep(50 * time.Millisecond)
171+ if err := c.Close(); err != nil {
172+ t.Fatal(err)
173+ }
174+ select {
175+ case err := <-errc:
176+ if !errors.Is(err, ErrClosed) {
177+ t.Fatalf("err = %v, want ErrClosed", err)
178+ }
179+ case <-time.After(5 * time.Second):
180+ t.Fatal("ReadMessage did not return after Close")
181+ }
182+}
183+
184+func TestCallNoAllocs(t *testing.T) {
185+ c := dial(t, privateBus(t))
186+ call := func() {
187+ c.NewCall(busName, busPath, busName, "GetNameOwner", "s").Str(busName)
188+ m, err := c.Call()
189+ if err != nil {
190+ t.Fatal(err)
191+ }
192+ if m.Body().Str() == "" {
193+ t.Fatal("empty owner")
194+ }
195+ }
196+ call() // grow the buffers once
197+ if allocs := testing.AllocsPerRun(200, call); allocs != 0 {
198+ t.Fatalf("%v allocations per call, want 0", allocs)
199+ }
200+}
201+
202+func TestLargeMessageGrowsBuffer(t *testing.T) {
203+ addr := privateBus(t)
204+ a := dial(t, addr)
205+ b := dial(t, addr)
206+ if err := b.AddMatch("type='signal',interface='org.example.Big'"); err != nil {
207+ t.Fatal(err)
208+ }
209+ payload := make([]byte, 3*defaultBufSz)
210+ for i := range payload {
211+ payload[i] = byte(i)
212+ }
213+ a.NewSignal("/a", "org.example.Big", "Blob", "ay").ByteArray(payload)
214+ if _, err := a.Send(); err != nil {
215+ t.Fatal(err)
216+ }
217+ for {
218+ m, err := b.ReadMessage()
219+ if err != nil {
220+ t.Fatal(err)
221+ }
222+ if m.Member == "Blob" {
223+ got := m.Body().ByteArray()
224+ if len(got) != len(payload) || got[len(got)-1] != payload[len(payload)-1] {
225+ t.Fatalf("payload of %d bytes", len(got))
226+ }
227+ return
228+ }
229+ }
230+}
231+
232+func TestDialErrors(t *testing.T) {
233+ for _, addr := range []string{"", "tcp:host=localhost,port=1", "unix:guid=abc", "unix:path=/nonexistent/zerobus"} {
234+ if c, err := Dial(addr); err == nil {
235+ _ = c.Close()
236+ t.Errorf("Dial(%q) succeeded", addr)
237+ }
238+ }
239+}
240+
241+func TestUnescape(t *testing.T) {
242+ got, err := unescape("/tmp/a%20b%2c")
243+ if err != nil || got != "/tmp/a b," {
244+ t.Fatalf("got %q, %v", got, err)
245+ }
246+ if _, err := unescape("%zz"); err == nil {
247+ t.Fatal("bad escape accepted")
248+ }
249+}
@@ -0,0 +1,249 @@
1+package zerobus
2+
3+import (
4+ "bufio"
5+ "errors"
6+ "os/exec"
7+ "strings"
8+ "testing"
9+ "time"
10+)
11+
12+// privateBus starts a dbus-daemon for the test and returns its address. The
13+// test is skipped when dbus-daemon is not installed.
14+func privateBus(t *testing.T) string {
15+ t.Helper()
16+ path, err := exec.LookPath("dbus-daemon")
17+ if err != nil {
18+ t.Skip("dbus-daemon not installed")
19+ }
20+ cmd := exec.Command(path, "--session", "--nofork", "--print-address")
21+ out, err := cmd.StdoutPipe()
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ if err := cmd.Start(); err != nil {
26+ t.Fatal(err)
27+ }
28+ t.Cleanup(func() {
29+ _ = cmd.Process.Kill()
30+ _ = cmd.Wait()
31+ })
32+ line := make(chan string, 1)
33+ go func() {
34+ s, _ := bufio.NewReader(out).ReadString('\n')
35+ line <- strings.TrimSpace(s)
36+ }()
37+ select {
38+ case addr := <-line:
39+ if addr == "" {
40+ t.Fatal("dbus-daemon printed no address")
41+ }
42+ return addr
43+ case <-time.After(5 * time.Second):
44+ t.Fatal("dbus-daemon did not start")
45+ }
46+ return ""
47+}
48+
49+func dial(t *testing.T, addr string) *Conn {
50+ t.Helper()
51+ c, err := Dial(addr)
52+ if err != nil {
53+ t.Fatal(err)
54+ }
55+ t.Cleanup(func() { _ = c.Close() })
56+ return c
57+}
58+
59+func TestHelloAndListNames(t *testing.T) {
60+ c := dial(t, privateBus(t))
61+ if !strings.HasPrefix(c.UniqueName(), ":") {
62+ t.Fatalf("unique name %q", c.UniqueName())
63+ }
64+ c.NewCall(busName, busPath, busName, "ListNames", "")
65+ m, err := c.Call()
66+ if err != nil {
67+ t.Fatal(err)
68+ }
69+ r := m.Body()
70+ found := false
71+ end := r.Array('s')
72+ for r.More(end) {
73+ if r.Str() == c.UniqueName() {
74+ found = true
75+ }
76+ }
77+ if !r.Done() || !found {
78+ t.Fatalf("own name not listed: %v", r.Err())
79+ }
80+}
81+
82+func TestErrorReply(t *testing.T) {
83+ c := dial(t, privateBus(t))
84+ c.NewCall("org.example.Missing", "/", "org.example.I", "M", "").SetFlags(FlagNoAutoStart)
85+ _, err := c.Call()
86+ var de *Error
87+ // ServiceUnknown or NameHasNoOwner, depending on the bus.
88+ if !errors.As(err, &de) || !strings.HasPrefix(de.Name, "org.freedesktop.DBus.Error.") || de.Message == "" {
89+ t.Fatalf("err = %v", err)
90+ }
91+ // The connection stays usable after an error reply.
92+ if _, err := c.RequestName("org.example.Zerobus", NameFlagDoNotQueue); err != nil {
93+ t.Fatal(err)
94+ }
95+}
96+
97+func TestSignalAndMethodBetweenPeers(t *testing.T) {
98+ addr := privateBus(t)
99+ server := dial(t, addr)
100+ client := dial(t, addr)
101+
102+ if code, err := server.RequestName("org.example.Echo", NameFlagDoNotQueue); err != nil || code != NamePrimaryOwner {
103+ t.Fatalf("RequestName = %d, %v", code, err)
104+ }
105+ if err := client.AddMatch("type='signal',interface='org.example.Echo'"); err != nil {
106+ t.Fatal(err)
107+ }
108+
109+ // The server answers one Echo call, then emits a signal.
110+ done := make(chan error, 1)
111+ go func() {
112+ for {
113+ m, err := server.ReadMessage()
114+ if err != nil {
115+ done <- err
116+ return
117+ }
118+ if m.Type != TypeMethodCall || m.Member != "Echo" {
119+ continue // NameAcquired and similar
120+ }
121+ s := m.Body().Str()
122+ server.NewReply(m, "s").Str(strings.ToUpper(s))
123+ if _, err := server.Send(); err != nil {
124+ done <- err
125+ return
126+ }
127+ server.NewSignal("/org/example/Echo", "org.example.Echo", "Echoed", "s").Str(s)
128+ _, err = server.Send()
129+ done <- err
130+ return
131+ }
132+ }()
133+
134+ client.NewCall("org.example.Echo", "/org/example/Echo", "org.example.Echo", "Echo", "s").Str("hi")
135+ m, err := client.Call()
136+ if err != nil {
137+ t.Fatal(err)
138+ }
139+ if got := m.Body().Str(); got != "HI" {
140+ t.Fatalf("reply %q", got)
141+ }
142+ if err := <-done; err != nil {
143+ t.Fatal(err)
144+ }
145+ for {
146+ m, err := client.ReadMessage()
147+ if err != nil {
148+ t.Fatal(err)
149+ }
150+ if m.Type == TypeSignal && m.Member == "Echoed" {
151+ if s := m.Body().Str(); s != "hi" || m.Sender != server.UniqueName() {
152+ t.Fatalf("signal %q from %q", s, m.Sender)
153+ }
154+ return
155+ }
156+ }
157+}
158+
159+func TestCloseUnblocksRead(t *testing.T) {
160+ c := dial(t, privateBus(t))
161+ errc := make(chan error, 1)
162+ go func() {
163+ for {
164+ if _, err := c.ReadMessage(); err != nil {
165+ errc <- err
166+ return
167+ }
168+ }
169+ }()
170+ time.Sleep(50 * time.Millisecond)
171+ if err := c.Close(); err != nil {
172+ t.Fatal(err)
173+ }
174+ select {
175+ case err := <-errc:
176+ if !errors.Is(err, ErrClosed) {
177+ t.Fatalf("err = %v, want ErrClosed", err)
178+ }
179+ case <-time.After(5 * time.Second):
180+ t.Fatal("ReadMessage did not return after Close")
181+ }
182+}
183+
184+func TestCallNoAllocs(t *testing.T) {
185+ c := dial(t, privateBus(t))
186+ call := func() {
187+ c.NewCall(busName, busPath, busName, "GetNameOwner", "s").Str(busName)
188+ m, err := c.Call()
189+ if err != nil {
190+ t.Fatal(err)
191+ }
192+ if m.Body().Str() == "" {
193+ t.Fatal("empty owner")
194+ }
195+ }
196+ call() // grow the buffers once
197+ if allocs := testing.AllocsPerRun(200, call); allocs != 0 {
198+ t.Fatalf("%v allocations per call, want 0", allocs)
199+ }
200+}
201+
202+func TestLargeMessageGrowsBuffer(t *testing.T) {
203+ addr := privateBus(t)
204+ a := dial(t, addr)
205+ b := dial(t, addr)
206+ if err := b.AddMatch("type='signal',interface='org.example.Big'"); err != nil {
207+ t.Fatal(err)
208+ }
209+ payload := make([]byte, 3*defaultBufSz)
210+ for i := range payload {
211+ payload[i] = byte(i)
212+ }
213+ a.NewSignal("/a", "org.example.Big", "Blob", "ay").ByteArray(payload)
214+ if _, err := a.Send(); err != nil {
215+ t.Fatal(err)
216+ }
217+ for {
218+ m, err := b.ReadMessage()
219+ if err != nil {
220+ t.Fatal(err)
221+ }
222+ if m.Member == "Blob" {
223+ got := m.Body().ByteArray()
224+ if len(got) != len(payload) || got[len(got)-1] != payload[len(payload)-1] {
225+ t.Fatalf("payload of %d bytes", len(got))
226+ }
227+ return
228+ }
229+ }
230+}
231+
232+func TestDialErrors(t *testing.T) {
233+ for _, addr := range []string{"", "tcp:host=localhost,port=1", "unix:guid=abc", "unix:path=/nonexistent/zerobus"} {
234+ if c, err := Dial(addr); err == nil {
235+ _ = c.Close()
236+ t.Errorf("Dial(%q) succeeded", addr)
237+ }
238+ }
239+}
240+
241+func TestUnescape(t *testing.T) {
242+ got, err := unescape("/tmp/a%20b%2c")
243+ if err != nil || got != "/tmp/a b," {
244+ t.Fatalf("got %q, %v", got, err)
245+ }
246+ if _, err := unescape("%zz"); err == nil {
247+ t.Fatal("bad escape accepted")
248+ }
249+}
A examples/trayitems/main.go +88 -0
@@ -0,0 +1,88 @@
1+// Command trayitems lists the StatusNotifierItems registered on the session
2+// bus, then prints their changes as they happen.
3+package main
4+
5+import (
6+ "fmt"
7+ "log"
8+ "strings"
9+
10+ "github.com/bnema/zerobus"
11+)
12+
13+const (
14+ watcher = "org.kde.StatusNotifierWatcher"
15+ watcherPath = "/StatusNotifierWatcher"
16+ props = "org.freedesktop.DBus.Properties"
17+ item = "org.kde.StatusNotifierItem"
18+)
19+
20+func main() {
21+ c, err := zerobus.SessionBus()
22+ if err != nil {
23+ log.Fatal(err)
24+ }
25+ defer func() { _ = c.Close() }()
26+
27+ e := c.NewCall(watcher, watcherPath, props, "Get", "ss")
28+ e.Str(watcher)
29+ e.Str("RegisteredStatusNotifierItems")
30+ m, err := c.Call()
31+ if err != nil {
32+ log.Fatal(err)
33+ }
34+ // The reply is a variant holding an array of strings. Copy the names:
35+ // the next call reuses the buffer they point into.
36+ r := m.Body()
37+ var items []string
38+ if r.Variant() == "as" {
39+ end := r.Array('s')
40+ for r.More(end) {
41+ items = append(items, strings.Clone(r.Str()))
42+ }
43+ }
44+ if r.Err() != nil {
45+ log.Fatal(r.Err())
46+ }
47+
48+ for _, it := range items {
49+ // An item is "bus-name/object/path", or only a bus name.
50+ dest, path, ok := strings.Cut(it, "/")
51+ if ok {
52+ path = "/" + path
53+ } else {
54+ path = "/StatusNotifierItem"
55+ }
56+ fmt.Printf("%s\n id=%q icon=%q status=%q\n", it, get(c, dest, path, "Id"), get(c, dest, path, "IconName"), get(c, dest, path, "Status"))
57+ }
58+
59+ if err := c.AddMatch("type='signal',interface='" + item + "'"); err != nil {
60+ log.Fatal(err)
61+ }
62+ fmt.Println("waiting for changes (Ctrl+C to stop)")
63+ for {
64+ m, err := c.ReadMessage()
65+ if err != nil {
66+ log.Fatal(err)
67+ }
68+ if m.Type == zerobus.TypeSignal {
69+ fmt.Printf("%s %s %s\n", m.Sender, m.Path, m.Member)
70+ }
71+ }
72+}
73+
74+// get reads a string property, or returns "" when it has none.
75+func get(c *zerobus.Conn, dest, path, name string) string {
76+ e := c.NewCall(dest, path, props, "Get", "ss")
77+ e.Str(item)
78+ e.Str(name)
79+ m, err := c.Call()
80+ if err != nil {
81+ return ""
82+ }
83+ r := m.Body()
84+ if r.Variant() != "s" {
85+ return ""
86+ }
87+ return strings.Clone(r.Str())
88+}
@@ -0,0 +1,88 @@
1+// Command trayitems lists the StatusNotifierItems registered on the session
2+// bus, then prints their changes as they happen.
3+package main
4+
5+import (
6+ "fmt"
7+ "log"
8+ "strings"
9+
10+ "github.com/bnema/zerobus"
11+)
12+
13+const (
14+ watcher = "org.kde.StatusNotifierWatcher"
15+ watcherPath = "/StatusNotifierWatcher"
16+ props = "org.freedesktop.DBus.Properties"
17+ item = "org.kde.StatusNotifierItem"
18+)
19+
20+func main() {
21+ c, err := zerobus.SessionBus()
22+ if err != nil {
23+ log.Fatal(err)
24+ }
25+ defer func() { _ = c.Close() }()
26+
27+ e := c.NewCall(watcher, watcherPath, props, "Get", "ss")
28+ e.Str(watcher)
29+ e.Str("RegisteredStatusNotifierItems")
30+ m, err := c.Call()
31+ if err != nil {
32+ log.Fatal(err)
33+ }
34+ // The reply is a variant holding an array of strings. Copy the names:
35+ // the next call reuses the buffer they point into.
36+ r := m.Body()
37+ var items []string
38+ if r.Variant() == "as" {
39+ end := r.Array('s')
40+ for r.More(end) {
41+ items = append(items, strings.Clone(r.Str()))
42+ }
43+ }
44+ if r.Err() != nil {
45+ log.Fatal(r.Err())
46+ }
47+
48+ for _, it := range items {
49+ // An item is "bus-name/object/path", or only a bus name.
50+ dest, path, ok := strings.Cut(it, "/")
51+ if ok {
52+ path = "/" + path
53+ } else {
54+ path = "/StatusNotifierItem"
55+ }
56+ fmt.Printf("%s\n id=%q icon=%q status=%q\n", it, get(c, dest, path, "Id"), get(c, dest, path, "IconName"), get(c, dest, path, "Status"))
57+ }
58+
59+ if err := c.AddMatch("type='signal',interface='" + item + "'"); err != nil {
60+ log.Fatal(err)
61+ }
62+ fmt.Println("waiting for changes (Ctrl+C to stop)")
63+ for {
64+ m, err := c.ReadMessage()
65+ if err != nil {
66+ log.Fatal(err)
67+ }
68+ if m.Type == zerobus.TypeSignal {
69+ fmt.Printf("%s %s %s\n", m.Sender, m.Path, m.Member)
70+ }
71+ }
72+}
73+
74+// get reads a string property, or returns "" when it has none.
75+func get(c *zerobus.Conn, dest, path, name string) string {
76+ e := c.NewCall(dest, path, props, "Get", "ss")
77+ e.Str(item)
78+ e.Str(name)
79+ m, err := c.Call()
80+ if err != nil {
81+ return ""
82+ }
83+ r := m.Body()
84+ if r.Variant() != "s" {
85+ return ""
86+ }
87+ return strings.Clone(r.Str())
88+}
A message.go +269 -0
@@ -0,0 +1,269 @@
1+package zerobus
2+
3+import (
4+ "encoding/binary"
5+ "math"
6+)
7+
8+// Type is the kind of a message.
9+type Type byte
10+
11+// Message types.
12+const (
13+ TypeMethodCall Type = 1
14+ TypeMethodReturn Type = 2
15+ TypeError Type = 3
16+ TypeSignal Type = 4
17+)
18+
19+// Flags are the header flags of a message.
20+type Flags byte
21+
22+// Message flags.
23+const (
24+ // FlagNoReplyExpected tells the receiver not to answer a method call.
25+ FlagNoReplyExpected Flags = 0x1
26+ // FlagNoAutoStart tells the bus not to start the destination service.
27+ FlagNoAutoStart Flags = 0x2
28+ // FlagAllowInteractiveAuth allows the receiver to prompt the user.
29+ FlagAllowInteractiveAuth Flags = 0x4
30+)
31+
32+// Header field codes.
33+const (
34+ fieldPath = 1
35+ fieldInterface = 2
36+ fieldMember = 3
37+ fieldErrorName = 4
38+ fieldReplySerial = 5
39+ fieldDestination = 6
40+ fieldSender = 7
41+ fieldSignature = 8
42+ fieldUnixFDs = 9
43+)
44+
45+const headerLen = 16 // the fixed part, up to the length of the field array
46+
47+// Message is a received message.
48+//
49+// Its strings point into the connection's receive buffer: they and the
50+// message itself are valid only until the next read on the connection.
51+type Message struct {
52+ Type Type
53+ Flags Flags
54+ Serial uint32
55+ ReplySerial uint32
56+ Path string
57+ Interface string
58+ Member string
59+ ErrorName string
60+ Destination string
61+ Sender string
62+ Signature string // the signature of the body
63+
64+ body []byte
65+ big bool
66+ r Reader
67+}
68+
69+// Body returns a reader at the start of the message body. Each call starts
70+// over from the beginning.
71+func (m *Message) Body() *Reader {
72+ m.r = Reader{buf: m.body, big: m.big}
73+ return &m.r
74+}
75+
76+// frameLen returns the full length of the message whose fixed header is b,
77+// which must hold at least headerLen bytes.
78+func frameLen(b []byte) (int, error) {
79+ var order binary.ByteOrder
80+ switch b[0] {
81+ case 'l':
82+ order = binary.LittleEndian
83+ case 'B':
84+ order = binary.BigEndian
85+ default:
86+ return 0, ErrMalformed
87+ }
88+ if b[3] != 1 {
89+ return 0, ErrMalformed // protocol version
90+ }
91+ body := uint64(order.Uint32(b[4:]))
92+ fields := uint64(order.Uint32(b[12:]))
93+ if body > maxMessage || fields > maxArray {
94+ return 0, ErrTooLarge
95+ }
96+ n := uint64(pad(headerLen+int(fields), 8)) + body
97+ if n > maxMessage {
98+ return 0, ErrTooLarge
99+ }
100+ return int(n), nil
101+}
102+
103+// parse decodes the message b, whose length frameLen returned, into m. The
104+// strings of m point into b.
105+func parse(b []byte, m *Message) error {
106+ *m = Message{big: b[0] == 'B', Type: Type(b[1]), Flags: Flags(b[2])}
107+ r := Reader{buf: b, big: m.big, off: 4}
108+ bodyLen := int(r.Uint32())
109+ m.Serial = r.Uint32()
110+ if m.Serial == 0 {
111+ return ErrMalformed
112+ }
113+ var fds uint32
114+ end := r.Array('(')
115+ for r.More(end) {
116+ r.Struct()
117+ code := r.Byte()
118+ sig := r.Variant()
119+ if r.err != nil {
120+ break
121+ }
122+ want := byte('s')
123+ var dst *string
124+ switch code {
125+ case fieldPath:
126+ want, dst = 'o', &m.Path
127+ case fieldInterface:
128+ dst = &m.Interface
129+ case fieldMember:
130+ dst = &m.Member
131+ case fieldErrorName:
132+ dst = &m.ErrorName
133+ case fieldDestination:
134+ dst = &m.Destination
135+ case fieldSender:
136+ dst = &m.Sender
137+ case fieldSignature:
138+ want, dst = 'g', &m.Signature
139+ case fieldReplySerial, fieldUnixFDs:
140+ if sig != "u" {
141+ return ErrMalformed
142+ }
143+ if code == fieldReplySerial {
144+ m.ReplySerial = r.Uint32()
145+ } else {
146+ fds = r.Uint32()
147+ }
148+ continue
149+ default:
150+ r.Skip(sig) // unknown fields must be ignored
151+ continue
152+ }
153+ if len(sig) != 1 || sig[0] != want {
154+ return ErrMalformed
155+ }
156+ if want == 'g' {
157+ *dst = r.Signature()
158+ } else {
159+ *dst = r.Str()
160+ }
161+ }
162+ if r.err != nil {
163+ return r.err
164+ }
165+ if fds != 0 {
166+ return ErrMalformed // fd passing is not negotiated, so none may come
167+ }
168+ start := pad(r.off, 8)
169+ if start+bodyLen != len(b) || start > len(b) {
170+ return ErrMalformed
171+ }
172+ for _, c := range b[r.off:start] {
173+ if c != 0 {
174+ return ErrMalformed
175+ }
176+ }
177+ m.body = b[start:]
178+ if !validSignature(m.Signature) || m.Path != "" && !validPath(m.Path) {
179+ return ErrMalformed
180+ }
181+ switch m.Type {
182+ case TypeMethodCall:
183+ if m.Path == "" || m.Member == "" {
184+ return ErrMalformed
185+ }
186+ case TypeSignal:
187+ if m.Path == "" || m.Interface == "" || m.Member == "" {
188+ return ErrMalformed
189+ }
190+ case TypeError:
191+ if m.ErrorName == "" || m.ReplySerial == 0 {
192+ return ErrMalformed
193+ }
194+ case TypeMethodReturn:
195+ if m.ReplySerial == 0 {
196+ return ErrMalformed
197+ }
198+ }
199+ return nil
200+}
201+
202+// begin starts a new message in e: the fixed header and the field array, up
203+// to the body. Empty strings are left out of the header.
204+func (e *Encoder) begin(t Type, serial, replySerial uint32, dest, path, iface, member, errName, sig string) {
205+ e.buf = append(e.buf[:0], 'l', byte(t), 0, 1, 0, 0, 0, 0)
206+ e.buf = binary.LittleEndian.AppendUint32(e.buf, serial)
207+ e.body, e.sig, e.err = 0, sig, nil
208+ // A bus disconnects a client that sends an invalid name.
209+ if dest != "" && !validBusName(dest) || iface != "" && !validInterface(iface) ||
210+ member != "" && !validMember(member) || errName != "" && !validInterface(errName) {
211+ e.invalid()
212+ }
213+ fields := e.BeginArray('(')
214+ str := func(code byte, typ, v string) {
215+ if v == "" {
216+ return
217+ }
218+ e.Struct()
219+ e.Byte(code)
220+ e.Signature(typ)
221+ switch typ {
222+ case "o":
223+ e.ObjectPath(v)
224+ case "g":
225+ e.Signature(v)
226+ default:
227+ e.Str(v)
228+ }
229+ }
230+ str(fieldPath, "o", path)
231+ str(fieldInterface, "s", iface)
232+ str(fieldMember, "s", member)
233+ str(fieldErrorName, "s", errName)
234+ if replySerial != 0 {
235+ e.Struct()
236+ e.Byte(fieldReplySerial)
237+ e.Signature("u")
238+ e.Uint32(replySerial)
239+ }
240+ str(fieldDestination, "s", dest)
241+ str(fieldSignature, "g", sig)
242+ e.EndArray(fields)
243+ e.pad(8)
244+ e.body = len(e.buf)
245+}
246+
247+// finish writes the body length and checks the body against the signature.
248+// It returns the bytes to send.
249+func (e *Encoder) finish() ([]byte, error) {
250+ if e.body == 0 {
251+ return nil, ErrInvalid
252+ }
253+ if e.err != nil {
254+ return nil, e.err
255+ }
256+ n := len(e.buf) - e.body
257+ if len(e.buf) > maxMessage || n > math.MaxUint32 {
258+ return nil, ErrTooLarge
259+ }
260+ binary.LittleEndian.PutUint32(e.buf[4:], uint32(n))
261+ // Reading the body back with the signature proves it matches: a bus
262+ // disconnects a client that sends a body that does not.
263+ r := Reader{buf: e.buf[e.body:]}
264+ r.Skip(e.sig)
265+ if !r.Done() {
266+ return nil, ErrInvalid
267+ }
268+ return e.buf, nil
269+}
@@ -0,0 +1,269 @@
1+package zerobus
2+
3+import (
4+ "encoding/binary"
5+ "math"
6+)
7+
8+// Type is the kind of a message.
9+type Type byte
10+
11+// Message types.
12+const (
13+ TypeMethodCall Type = 1
14+ TypeMethodReturn Type = 2
15+ TypeError Type = 3
16+ TypeSignal Type = 4
17+)
18+
19+// Flags are the header flags of a message.
20+type Flags byte
21+
22+// Message flags.
23+const (
24+ // FlagNoReplyExpected tells the receiver not to answer a method call.
25+ FlagNoReplyExpected Flags = 0x1
26+ // FlagNoAutoStart tells the bus not to start the destination service.
27+ FlagNoAutoStart Flags = 0x2
28+ // FlagAllowInteractiveAuth allows the receiver to prompt the user.
29+ FlagAllowInteractiveAuth Flags = 0x4
30+)
31+
32+// Header field codes.
33+const (
34+ fieldPath = 1
35+ fieldInterface = 2
36+ fieldMember = 3
37+ fieldErrorName = 4
38+ fieldReplySerial = 5
39+ fieldDestination = 6
40+ fieldSender = 7
41+ fieldSignature = 8
42+ fieldUnixFDs = 9
43+)
44+
45+const headerLen = 16 // the fixed part, up to the length of the field array
46+
47+// Message is a received message.
48+//
49+// Its strings point into the connection's receive buffer: they and the
50+// message itself are valid only until the next read on the connection.
51+type Message struct {
52+ Type Type
53+ Flags Flags
54+ Serial uint32
55+ ReplySerial uint32
56+ Path string
57+ Interface string
58+ Member string
59+ ErrorName string
60+ Destination string
61+ Sender string
62+ Signature string // the signature of the body
63+
64+ body []byte
65+ big bool
66+ r Reader
67+}
68+
69+// Body returns a reader at the start of the message body. Each call starts
70+// over from the beginning.
71+func (m *Message) Body() *Reader {
72+ m.r = Reader{buf: m.body, big: m.big}
73+ return &m.r
74+}
75+
76+// frameLen returns the full length of the message whose fixed header is b,
77+// which must hold at least headerLen bytes.
78+func frameLen(b []byte) (int, error) {
79+ var order binary.ByteOrder
80+ switch b[0] {
81+ case 'l':
82+ order = binary.LittleEndian
83+ case 'B':
84+ order = binary.BigEndian
85+ default:
86+ return 0, ErrMalformed
87+ }
88+ if b[3] != 1 {
89+ return 0, ErrMalformed // protocol version
90+ }
91+ body := uint64(order.Uint32(b[4:]))
92+ fields := uint64(order.Uint32(b[12:]))
93+ if body > maxMessage || fields > maxArray {
94+ return 0, ErrTooLarge
95+ }
96+ n := uint64(pad(headerLen+int(fields), 8)) + body
97+ if n > maxMessage {
98+ return 0, ErrTooLarge
99+ }
100+ return int(n), nil
101+}
102+
103+// parse decodes the message b, whose length frameLen returned, into m. The
104+// strings of m point into b.
105+func parse(b []byte, m *Message) error {
106+ *m = Message{big: b[0] == 'B', Type: Type(b[1]), Flags: Flags(b[2])}
107+ r := Reader{buf: b, big: m.big, off: 4}
108+ bodyLen := int(r.Uint32())
109+ m.Serial = r.Uint32()
110+ if m.Serial == 0 {
111+ return ErrMalformed
112+ }
113+ var fds uint32
114+ end := r.Array('(')
115+ for r.More(end) {
116+ r.Struct()
117+ code := r.Byte()
118+ sig := r.Variant()
119+ if r.err != nil {
120+ break
121+ }
122+ want := byte('s')
123+ var dst *string
124+ switch code {
125+ case fieldPath:
126+ want, dst = 'o', &m.Path
127+ case fieldInterface:
128+ dst = &m.Interface
129+ case fieldMember:
130+ dst = &m.Member
131+ case fieldErrorName:
132+ dst = &m.ErrorName
133+ case fieldDestination:
134+ dst = &m.Destination
135+ case fieldSender:
136+ dst = &m.Sender
137+ case fieldSignature:
138+ want, dst = 'g', &m.Signature
139+ case fieldReplySerial, fieldUnixFDs:
140+ if sig != "u" {
141+ return ErrMalformed
142+ }
143+ if code == fieldReplySerial {
144+ m.ReplySerial = r.Uint32()
145+ } else {
146+ fds = r.Uint32()
147+ }
148+ continue
149+ default:
150+ r.Skip(sig) // unknown fields must be ignored
151+ continue
152+ }
153+ if len(sig) != 1 || sig[0] != want {
154+ return ErrMalformed
155+ }
156+ if want == 'g' {
157+ *dst = r.Signature()
158+ } else {
159+ *dst = r.Str()
160+ }
161+ }
162+ if r.err != nil {
163+ return r.err
164+ }
165+ if fds != 0 {
166+ return ErrMalformed // fd passing is not negotiated, so none may come
167+ }
168+ start := pad(r.off, 8)
169+ if start+bodyLen != len(b) || start > len(b) {
170+ return ErrMalformed
171+ }
172+ for _, c := range b[r.off:start] {
173+ if c != 0 {
174+ return ErrMalformed
175+ }
176+ }
177+ m.body = b[start:]
178+ if !validSignature(m.Signature) || m.Path != "" && !validPath(m.Path) {
179+ return ErrMalformed
180+ }
181+ switch m.Type {
182+ case TypeMethodCall:
183+ if m.Path == "" || m.Member == "" {
184+ return ErrMalformed
185+ }
186+ case TypeSignal:
187+ if m.Path == "" || m.Interface == "" || m.Member == "" {
188+ return ErrMalformed
189+ }
190+ case TypeError:
191+ if m.ErrorName == "" || m.ReplySerial == 0 {
192+ return ErrMalformed
193+ }
194+ case TypeMethodReturn:
195+ if m.ReplySerial == 0 {
196+ return ErrMalformed
197+ }
198+ }
199+ return nil
200+}
201+
202+// begin starts a new message in e: the fixed header and the field array, up
203+// to the body. Empty strings are left out of the header.
204+func (e *Encoder) begin(t Type, serial, replySerial uint32, dest, path, iface, member, errName, sig string) {
205+ e.buf = append(e.buf[:0], 'l', byte(t), 0, 1, 0, 0, 0, 0)
206+ e.buf = binary.LittleEndian.AppendUint32(e.buf, serial)
207+ e.body, e.sig, e.err = 0, sig, nil
208+ // A bus disconnects a client that sends an invalid name.
209+ if dest != "" && !validBusName(dest) || iface != "" && !validInterface(iface) ||
210+ member != "" && !validMember(member) || errName != "" && !validInterface(errName) {
211+ e.invalid()
212+ }
213+ fields := e.BeginArray('(')
214+ str := func(code byte, typ, v string) {
215+ if v == "" {
216+ return
217+ }
218+ e.Struct()
219+ e.Byte(code)
220+ e.Signature(typ)
221+ switch typ {
222+ case "o":
223+ e.ObjectPath(v)
224+ case "g":
225+ e.Signature(v)
226+ default:
227+ e.Str(v)
228+ }
229+ }
230+ str(fieldPath, "o", path)
231+ str(fieldInterface, "s", iface)
232+ str(fieldMember, "s", member)
233+ str(fieldErrorName, "s", errName)
234+ if replySerial != 0 {
235+ e.Struct()
236+ e.Byte(fieldReplySerial)
237+ e.Signature("u")
238+ e.Uint32(replySerial)
239+ }
240+ str(fieldDestination, "s", dest)
241+ str(fieldSignature, "g", sig)
242+ e.EndArray(fields)
243+ e.pad(8)
244+ e.body = len(e.buf)
245+}
246+
247+// finish writes the body length and checks the body against the signature.
248+// It returns the bytes to send.
249+func (e *Encoder) finish() ([]byte, error) {
250+ if e.body == 0 {
251+ return nil, ErrInvalid
252+ }
253+ if e.err != nil {
254+ return nil, e.err
255+ }
256+ n := len(e.buf) - e.body
257+ if len(e.buf) > maxMessage || n > math.MaxUint32 {
258+ return nil, ErrTooLarge
259+ }
260+ binary.LittleEndian.PutUint32(e.buf[4:], uint32(n))
261+ // Reading the body back with the signature proves it matches: a bus
262+ // disconnects a client that sends a body that does not.
263+ r := Reader{buf: e.buf[e.body:]}
264+ r.Skip(e.sig)
265+ if !r.Done() {
266+ return nil, ErrInvalid
267+ }
268+ return e.buf, nil
269+}
A wire.go +622 -0
@@ -0,0 +1,622 @@
1+package zerobus
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "math"
7+ "strings"
8+ "unicode/utf8"
9+ "unsafe"
10+)
11+
12+const (
13+ maxMessage = 128 << 20 // the D-Bus limit for a whole message
14+ maxArray = 64 << 20 // the D-Bus limit for one array
15+ maxDepth = 64 // 32 array + 32 struct levels
16+)
17+
18+var (
19+ // ErrMalformed reports bytes that do not follow the D-Bus wire format.
20+ ErrMalformed = errors.New("zerobus: malformed message")
21+ // ErrTooLarge reports a message or array over the D-Bus size limits.
22+ ErrTooLarge = errors.New("zerobus: message too large")
23+ // ErrInvalid reports an outgoing value D-Bus does not allow, such as a
24+ // string with a NUL byte or a body that does not match its signature.
25+ ErrInvalid = errors.New("zerobus: invalid value")
26+)
27+
28+// alignOf returns the alignment of the type that starts with signature byte c.
29+func alignOf(c byte) int {
30+ switch c {
31+ case 'n', 'q':
32+ return 2
33+ case 'b', 'i', 'u', 'h', 's', 'o', 'a':
34+ return 4
35+ case 'x', 't', 'd', '(', '{':
36+ return 8
37+ }
38+ return 1 // y, g, v
39+}
40+
41+// pad rounds off up to a multiple of n, a power of two.
42+func pad(off, n int) int { return (off + n - 1) &^ (n - 1) }
43+
44+// sigEnd returns the index just after the single complete type that starts
45+// at sig[i], or -1 if there is none.
46+func sigEnd(sig string, i, depth int) int {
47+ if i >= len(sig) || depth > maxDepth {
48+ return -1
49+ }
50+ switch sig[i] {
51+ case 'y', 'b', 'n', 'q', 'i', 'u', 'x', 't', 'd', 'h', 's', 'o', 'g', 'v':
52+ return i + 1
53+ case 'a':
54+ if i+1 < len(sig) && sig[i+1] == '{' {
55+ return dictEnd(sig, i+1, depth+1)
56+ }
57+ return sigEnd(sig, i+1, depth+1)
58+ case '(':
59+ j := i + 1
60+ if j < len(sig) && sig[j] == ')' {
61+ return -1 // empty structs are not allowed
62+ }
63+ for j < len(sig) && sig[j] != ')' {
64+ if j = sigEnd(sig, j, depth+1); j < 0 {
65+ return -1
66+ }
67+ }
68+ if j >= len(sig) {
69+ return -1
70+ }
71+ return j + 1
72+ }
73+ return -1 // includes '{': a dict entry is only valid as an array element
74+}
75+
76+// dictEnd returns the index just after the dict entry type at sig[i] ('{').
77+// Its key must be a basic type and it holds exactly two types.
78+func dictEnd(sig string, i, depth int) int {
79+ j := i + 1
80+ if j >= len(sig) || !basic(sig[j]) {
81+ return -1
82+ }
83+ if j = sigEnd(sig, j+1, depth+1); j < 0 || j >= len(sig) || sig[j] != '}' {
84+ return -1
85+ }
86+ return j + 1
87+}
88+
89+func basic(c byte) bool {
90+ switch c {
91+ case 'y', 'b', 'n', 'q', 'i', 'u', 'x', 't', 'd', 'h', 's', 'o', 'g':
92+ return true
93+ }
94+ return false
95+}
96+
97+// validSignature reports whether sig is a list of complete types that fits
98+// in a D-Bus signature.
99+func validSignature(sig string) bool {
100+ if len(sig) > 255 {
101+ return false
102+ }
103+ for i := 0; i < len(sig); {
104+ if i = sigEnd(sig, i, 0); i < 0 {
105+ return false
106+ }
107+ }
108+ return true
109+}
110+
111+// validPath reports whether p is a valid object path.
112+func validPath(p string) bool {
113+ if p == "/" {
114+ return true
115+ }
116+ if len(p) < 2 || p[0] != '/' || p[len(p)-1] == '/' {
117+ return false
118+ }
119+ for i := 1; i < len(p); i++ {
120+ c := p[i]
121+ switch {
122+ case c == '/':
123+ if p[i-1] == '/' {
124+ return false
125+ }
126+ case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == '_':
127+ default:
128+ return false
129+ }
130+ }
131+ return true
132+}
133+
134+// validMember reports whether s is a valid member name: letters, digits and
135+// '_', not starting with a digit.
136+func validMember(s string) bool {
137+ if s == "" || len(s) > 255 || s[0] >= '0' && s[0] <= '9' {
138+ return false
139+ }
140+ for i := 0; i < len(s); i++ {
141+ if !nameChar(s[i]) {
142+ return false
143+ }
144+ }
145+ return true
146+}
147+
148+// nameChar reports whether c may appear in a member or interface name.
149+func nameChar(c byte) bool {
150+ return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_'
151+}
152+
153+// validInterface reports whether s is a valid interface or error name: two
154+// or more member-like elements separated by dots.
155+func validInterface(s string) bool {
156+ if len(s) > 255 {
157+ return false
158+ }
159+ n := 0
160+ for e := range strings.SplitSeq(s, ".") {
161+ if !validMember(e) {
162+ return false
163+ }
164+ n++
165+ }
166+ return n >= 2
167+}
168+
169+// validBusName reports whether s is a valid unique (":1.42") or well-known
170+// ("org.example.App") bus name.
171+func validBusName(s string) bool {
172+ if s == "" || len(s) > 255 {
173+ return false
174+ }
175+ unique := s[0] == ':'
176+ if unique {
177+ s = s[1:]
178+ }
179+ n := 0
180+ for e := range strings.SplitSeq(s, ".") {
181+ if e == "" || !unique && e[0] >= '0' && e[0] <= '9' {
182+ return false
183+ }
184+ for i := 0; i < len(e); i++ {
185+ if !nameChar(e[i]) && e[i] != '-' {
186+ return false
187+ }
188+ }
189+ n++
190+ }
191+ return n >= 2
192+}
193+
194+// Reader decodes D-Bus values in place.
195+//
196+// Strings and byte slices it returns point into the message buffer: they are
197+// valid only until the next Conn.ReadMessage. Copy them (strings.Clone,
198+// bytes.Clone) to keep them longer.
199+//
200+// Read values in the order of the message signature. Errors are sticky:
201+// after the first one, every read returns a zero value and Err reports it.
202+type Reader struct {
203+ buf []byte
204+ off int
205+ big bool
206+ err error
207+}
208+
209+// Err returns the first error the reader met, or nil.
210+func (r *Reader) Err() error { return r.err }
211+
212+// Done reports whether every byte was read without error.
213+func (r *Reader) Done() bool { return r.err == nil && r.off == len(r.buf) }
214+
215+func (r *Reader) fail() {
216+ if r.err == nil {
217+ r.err = ErrMalformed
218+ }
219+}
220+
221+// take aligns to align and returns the next size bytes.
222+func (r *Reader) take(align, size int) []byte {
223+ if r.err != nil {
224+ return nil
225+ }
226+ off := pad(r.off, align)
227+ if off > len(r.buf)-size {
228+ r.fail()
229+ return nil
230+ }
231+ r.off = off + size
232+ return r.buf[off:r.off]
233+}
234+
235+// Byte reads a BYTE (y).
236+func (r *Reader) Byte() byte {
237+ if b := r.take(1, 1); b != nil {
238+ return b[0]
239+ }
240+ return 0
241+}
242+
243+// Bool reads a BOOLEAN (b).
244+func (r *Reader) Bool() bool {
245+ v := r.Uint32()
246+ if v > 1 {
247+ r.fail()
248+ return false
249+ }
250+ return v == 1
251+}
252+
253+// Int16 reads an INT16 (n).
254+func (r *Reader) Int16() int16 { return int16(r.Uint16()) }
255+
256+// Uint16 reads a UINT16 (q).
257+func (r *Reader) Uint16() uint16 {
258+ b := r.take(2, 2)
259+ if b == nil {
260+ return 0
261+ }
262+ if r.big {
263+ return binary.BigEndian.Uint16(b)
264+ }
265+ return binary.LittleEndian.Uint16(b)
266+}
267+
268+// Int32 reads an INT32 (i).
269+func (r *Reader) Int32() int32 { return int32(r.Uint32()) }
270+
271+// Uint32 reads a UINT32 (u).
272+func (r *Reader) Uint32() uint32 {
273+ b := r.take(4, 4)
274+ if b == nil {
275+ return 0
276+ }
277+ if r.big {
278+ return binary.BigEndian.Uint32(b)
279+ }
280+ return binary.LittleEndian.Uint32(b)
281+}
282+
283+// UnixFD reads a UNIX_FD (h): an index into the fds sent with the message.
284+// zerobus does not receive file descriptors, so the index has nothing to
285+// point to; it is only read so the following values can be.
286+func (r *Reader) UnixFD() uint32 { return r.Uint32() }
287+
288+// Int64 reads an INT64 (x).
289+func (r *Reader) Int64() int64 { return int64(r.Uint64()) }
290+
291+// Uint64 reads a UINT64 (t).
292+func (r *Reader) Uint64() uint64 {
293+ b := r.take(8, 8)
294+ if b == nil {
295+ return 0
296+ }
297+ if r.big {
298+ return binary.BigEndian.Uint64(b)
299+ }
300+ return binary.LittleEndian.Uint64(b)
301+}
302+
303+// Float64 reads a DOUBLE (d).
304+func (r *Reader) Float64() float64 { return math.Float64frombits(r.Uint64()) }
305+
306+// Str reads a STRING (s). The result points into the message buffer.
307+func (r *Reader) Str() string { return r.str(int(r.Uint32())) }
308+
309+// ObjectPath reads an OBJECT_PATH (o). The result points into the message
310+// buffer.
311+func (r *Reader) ObjectPath() string { return r.Str() }
312+
313+// Signature reads a SIGNATURE (g). The result points into the message buffer.
314+func (r *Reader) Signature() string { return r.str(int(r.Byte())) }
315+
316+// str returns the next n bytes as a string and skips the NUL after them.
317+func (r *Reader) str(n int) string {
318+ if r.err != nil {
319+ return ""
320+ }
321+ if n < 0 || n > len(r.buf)-r.off-1 || r.buf[r.off+n] != 0 {
322+ r.fail()
323+ return ""
324+ }
325+ s := unsafe.String(unsafe.SliceData(r.buf[r.off:]), n)
326+ r.off += n + 1
327+ return s
328+}
329+
330+// Variant reads the signature of a VARIANT (v) and returns it. Read the
331+// value that follows with the matching method, or skip it with Skip.
332+func (r *Reader) Variant() string {
333+ sig := r.Signature()
334+ if r.err == nil && sigEnd(sig, 0, 0) != len(sig) {
335+ r.fail()
336+ return ""
337+ }
338+ return sig
339+}
340+
341+// Struct aligns the reader to the start of a STRUCT or DICT_ENTRY. Call it
342+// before reading the first field.
343+func (r *Reader) Struct() { r.take(8, 0) }
344+
345+// Array reads the header of an ARRAY whose element type starts with the
346+// signature byte elem, and returns the offset where the array ends. Read the
347+// elements while More returns true:
348+//
349+// end := r.Array('s')
350+// for r.More(end) {
351+// name := r.Str()
352+// }
353+func (r *Reader) Array(elem byte) int {
354+ n := r.Uint32()
355+ if r.err != nil {
356+ return r.off
357+ }
358+ if n > maxArray {
359+ r.fail()
360+ return r.off
361+ }
362+ off := pad(r.off, alignOf(elem))
363+ if off > len(r.buf)-int(n) {
364+ r.fail()
365+ return r.off
366+ }
367+ r.off = off
368+ return off + int(n)
369+}
370+
371+// More reports whether the array that ends at end has another element.
372+func (r *Reader) More(end int) bool {
373+ if r.err != nil {
374+ return false
375+ }
376+ if r.off < end {
377+ return true
378+ }
379+ if r.off > end {
380+ r.fail()
381+ }
382+ return false
383+}
384+
385+// SkipTo moves to end, the value returned by Array. Use it to leave an array
386+// before its last element.
387+func (r *Reader) SkipTo(end int) {
388+ if r.err != nil {
389+ return
390+ }
391+ if end < r.off || end > len(r.buf) {
392+ r.fail()
393+ return
394+ }
395+ r.off = end
396+}
397+
398+// ByteArray reads an ARRAY of BYTE (ay). The result points into the message
399+// buffer.
400+func (r *Reader) ByteArray() []byte {
401+ end := r.Array('y')
402+ if r.err != nil {
403+ return nil
404+ }
405+ b := r.buf[r.off:end:end]
406+ r.off = end
407+ return b
408+}
409+
410+// Skip reads past values of the given signature. Arrays are skipped by their
411+// length, without reading their elements.
412+func (r *Reader) Skip(sig string) { r.skip(sig, 0) }
413+
414+func (r *Reader) skip(sig string, depth int) {
415+ for i := 0; i < len(sig) && r.err == nil; {
416+ i = r.skipOne(sig, i, depth)
417+ }
418+}
419+
420+// skipOne skips the value of the type at sig[i] and returns the index of the
421+// next type.
422+func (r *Reader) skipOne(sig string, i, depth int) int {
423+ if depth > maxDepth {
424+ r.fail()
425+ return len(sig)
426+ }
427+ switch c := sig[i]; c {
428+ case 'y':
429+ r.take(1, 1)
430+ case 'n', 'q':
431+ r.take(2, 2)
432+ case 'b', 'i', 'u', 'h':
433+ r.take(4, 4)
434+ case 'x', 't', 'd':
435+ r.take(8, 8)
436+ case 's', 'o':
437+ r.Str()
438+ case 'g':
439+ r.Signature()
440+ case 'v':
441+ r.skip(r.Variant(), depth+1)
442+ case 'a':
443+ end := sigEnd(sig, i, depth)
444+ if end < 0 {
445+ r.fail()
446+ return len(sig)
447+ }
448+ r.SkipTo(r.Array(sig[i+1]))
449+ return end
450+ case '(', '{':
451+ closer := byte(')')
452+ if c == '{' {
453+ closer = '}'
454+ }
455+ r.Struct()
456+ j := i + 1
457+ for j < len(sig) && sig[j] != closer && r.err == nil {
458+ j = r.skipOne(sig, j, depth+1)
459+ }
460+ if j >= len(sig) {
461+ r.fail()
462+ return len(sig)
463+ }
464+ return j + 1
465+ default:
466+ r.fail()
467+ return len(sig)
468+ }
469+ return i + 1
470+}
471+
472+// Encoder appends D-Bus values to an outgoing message. Get one from
473+// Conn.NewCall, Conn.Signal, Conn.Reply or Conn.Error, write the body values
474+// in the order of the message signature, then call Conn.Send.
475+//
476+// Errors are sticky and reported by Conn.Send.
477+type Encoder struct {
478+ buf []byte
479+ body int // offset of the body; 0 when no message is being built
480+ sig string // body signature, checked by finish
481+ err error
482+}
483+
484+func (e *Encoder) invalid() {
485+ if e.err == nil {
486+ e.err = ErrInvalid
487+ }
488+}
489+
490+func (e *Encoder) pad(n int) {
491+ for len(e.buf)&(n-1) != 0 {
492+ e.buf = append(e.buf, 0)
493+ }
494+}
495+
496+// Byte writes a BYTE (y).
497+func (e *Encoder) Byte(v byte) { e.buf = append(e.buf, v) }
498+
499+// Bool writes a BOOLEAN (b).
500+func (e *Encoder) Bool(v bool) {
501+ var u uint32
502+ if v {
503+ u = 1
504+ }
505+ e.Uint32(u)
506+}
507+
508+// Int16 writes an INT16 (n).
509+func (e *Encoder) Int16(v int16) { e.Uint16(uint16(v)) }
510+
511+// Uint16 writes a UINT16 (q).
512+func (e *Encoder) Uint16(v uint16) {
513+ e.pad(2)
514+ e.buf = binary.LittleEndian.AppendUint16(e.buf, v)
515+}
516+
517+// Int32 writes an INT32 (i).
518+func (e *Encoder) Int32(v int32) { e.Uint32(uint32(v)) }
519+
520+// Uint32 writes a UINT32 (u).
521+func (e *Encoder) Uint32(v uint32) {
522+ e.pad(4)
523+ e.buf = binary.LittleEndian.AppendUint32(e.buf, v)
524+}
525+
526+// Int64 writes an INT64 (x).
527+func (e *Encoder) Int64(v int64) { e.Uint64(uint64(v)) }
528+
529+// Uint64 writes a UINT64 (t).
530+func (e *Encoder) Uint64(v uint64) {
531+ e.pad(8)
532+ e.buf = binary.LittleEndian.AppendUint64(e.buf, v)
533+}
534+
535+// Float64 writes a DOUBLE (d).
536+func (e *Encoder) Float64(v float64) { e.Uint64(math.Float64bits(v)) }
537+
538+// Str writes a STRING (s).
539+func (e *Encoder) Str(s string) {
540+ e.Uint32(uint32(len(s)))
541+ e.text(s)
542+}
543+
544+// ObjectPath writes an OBJECT_PATH (o).
545+func (e *Encoder) ObjectPath(p string) {
546+ if !validPath(p) {
547+ e.invalid()
548+ }
549+ e.Str(p)
550+}
551+
552+// Signature writes a SIGNATURE (g).
553+func (e *Encoder) Signature(sig string) {
554+ if !validSignature(sig) {
555+ e.invalid()
556+ }
557+ e.Byte(byte(len(sig)))
558+ e.text(sig)
559+}
560+
561+// text appends s and its NUL terminator.
562+func (e *Encoder) text(s string) {
563+ // The bus disconnects a client that sends a NUL or invalid UTF-8.
564+ if strings.IndexByte(s, 0) >= 0 || !utf8.ValidString(s) {
565+ e.invalid()
566+ }
567+ e.buf = append(e.buf, s...)
568+ e.buf = append(e.buf, 0)
569+}
570+
571+// Variant writes the signature of a VARIANT (v). Write its value next.
572+func (e *Encoder) Variant(sig string) {
573+ if sigEnd(sig, 0, 0) != len(sig) {
574+ e.invalid()
575+ }
576+ e.Signature(sig)
577+}
578+
579+// Struct aligns the encoder to the start of a STRUCT or DICT_ENTRY. Call it
580+// before writing the first field.
581+func (e *Encoder) Struct() { e.pad(8) }
582+
583+// ArrayMark is an array being written; pass it to EndArray.
584+type ArrayMark struct{ at, start int }
585+
586+// BeginArray starts an ARRAY whose element type starts with the signature
587+// byte elem. Write the elements, then call EndArray:
588+//
589+// a := e.BeginArray('s')
590+// e.Str("one")
591+// e.Str("two")
592+// e.EndArray(a)
593+func (e *Encoder) BeginArray(elem byte) ArrayMark {
594+ e.pad(4)
595+ at := len(e.buf)
596+ e.buf = append(e.buf, 0, 0, 0, 0)
597+ e.pad(alignOf(elem))
598+ return ArrayMark{at: at, start: len(e.buf)}
599+}
600+
601+// EndArray finishes the array started by BeginArray.
602+func (e *Encoder) EndArray(a ArrayMark) {
603+ n := len(e.buf) - a.start
604+ if n > maxArray {
605+ e.invalid()
606+ return
607+ }
608+ binary.LittleEndian.PutUint32(e.buf[a.at:], uint32(n))
609+}
610+
611+// ByteArray writes an ARRAY of BYTE (ay).
612+func (e *Encoder) ByteArray(b []byte) {
613+ e.Uint32(uint32(len(b)))
614+ e.buf = append(e.buf, b...)
615+}
616+
617+// SetFlags sets the flags of the message being built.
618+func (e *Encoder) SetFlags(f Flags) {
619+ if e.body != 0 {
620+ e.buf[2] = byte(f)
621+ }
622+}
@@ -0,0 +1,622 @@
1+package zerobus
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "math"
7+ "strings"
8+ "unicode/utf8"
9+ "unsafe"
10+)
11+
12+const (
13+ maxMessage = 128 << 20 // the D-Bus limit for a whole message
14+ maxArray = 64 << 20 // the D-Bus limit for one array
15+ maxDepth = 64 // 32 array + 32 struct levels
16+)
17+
18+var (
19+ // ErrMalformed reports bytes that do not follow the D-Bus wire format.
20+ ErrMalformed = errors.New("zerobus: malformed message")
21+ // ErrTooLarge reports a message or array over the D-Bus size limits.
22+ ErrTooLarge = errors.New("zerobus: message too large")
23+ // ErrInvalid reports an outgoing value D-Bus does not allow, such as a
24+ // string with a NUL byte or a body that does not match its signature.
25+ ErrInvalid = errors.New("zerobus: invalid value")
26+)
27+
28+// alignOf returns the alignment of the type that starts with signature byte c.
29+func alignOf(c byte) int {
30+ switch c {
31+ case 'n', 'q':
32+ return 2
33+ case 'b', 'i', 'u', 'h', 's', 'o', 'a':
34+ return 4
35+ case 'x', 't', 'd', '(', '{':
36+ return 8
37+ }
38+ return 1 // y, g, v
39+}
40+
41+// pad rounds off up to a multiple of n, a power of two.
42+func pad(off, n int) int { return (off + n - 1) &^ (n - 1) }
43+
44+// sigEnd returns the index just after the single complete type that starts
45+// at sig[i], or -1 if there is none.
46+func sigEnd(sig string, i, depth int) int {
47+ if i >= len(sig) || depth > maxDepth {
48+ return -1
49+ }
50+ switch sig[i] {
51+ case 'y', 'b', 'n', 'q', 'i', 'u', 'x', 't', 'd', 'h', 's', 'o', 'g', 'v':
52+ return i + 1
53+ case 'a':
54+ if i+1 < len(sig) && sig[i+1] == '{' {
55+ return dictEnd(sig, i+1, depth+1)
56+ }
57+ return sigEnd(sig, i+1, depth+1)
58+ case '(':
59+ j := i + 1
60+ if j < len(sig) && sig[j] == ')' {
61+ return -1 // empty structs are not allowed
62+ }
63+ for j < len(sig) && sig[j] != ')' {
64+ if j = sigEnd(sig, j, depth+1); j < 0 {
65+ return -1
66+ }
67+ }
68+ if j >= len(sig) {
69+ return -1
70+ }
71+ return j + 1
72+ }
73+ return -1 // includes '{': a dict entry is only valid as an array element
74+}
75+
76+// dictEnd returns the index just after the dict entry type at sig[i] ('{').
77+// Its key must be a basic type and it holds exactly two types.
78+func dictEnd(sig string, i, depth int) int {
79+ j := i + 1
80+ if j >= len(sig) || !basic(sig[j]) {
81+ return -1
82+ }
83+ if j = sigEnd(sig, j+1, depth+1); j < 0 || j >= len(sig) || sig[j] != '}' {
84+ return -1
85+ }
86+ return j + 1
87+}
88+
89+func basic(c byte) bool {
90+ switch c {
91+ case 'y', 'b', 'n', 'q', 'i', 'u', 'x', 't', 'd', 'h', 's', 'o', 'g':
92+ return true
93+ }
94+ return false
95+}
96+
97+// validSignature reports whether sig is a list of complete types that fits
98+// in a D-Bus signature.
99+func validSignature(sig string) bool {
100+ if len(sig) > 255 {
101+ return false
102+ }
103+ for i := 0; i < len(sig); {
104+ if i = sigEnd(sig, i, 0); i < 0 {
105+ return false
106+ }
107+ }
108+ return true
109+}
110+
111+// validPath reports whether p is a valid object path.
112+func validPath(p string) bool {
113+ if p == "/" {
114+ return true
115+ }
116+ if len(p) < 2 || p[0] != '/' || p[len(p)-1] == '/' {
117+ return false
118+ }
119+ for i := 1; i < len(p); i++ {
120+ c := p[i]
121+ switch {
122+ case c == '/':
123+ if p[i-1] == '/' {
124+ return false
125+ }
126+ case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == '_':
127+ default:
128+ return false
129+ }
130+ }
131+ return true
132+}
133+
134+// validMember reports whether s is a valid member name: letters, digits and
135+// '_', not starting with a digit.
136+func validMember(s string) bool {
137+ if s == "" || len(s) > 255 || s[0] >= '0' && s[0] <= '9' {
138+ return false
139+ }
140+ for i := 0; i < len(s); i++ {
141+ if !nameChar(s[i]) {
142+ return false
143+ }
144+ }
145+ return true
146+}
147+
148+// nameChar reports whether c may appear in a member or interface name.
149+func nameChar(c byte) bool {
150+ return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_'
151+}
152+
153+// validInterface reports whether s is a valid interface or error name: two
154+// or more member-like elements separated by dots.
155+func validInterface(s string) bool {
156+ if len(s) > 255 {
157+ return false
158+ }
159+ n := 0
160+ for e := range strings.SplitSeq(s, ".") {
161+ if !validMember(e) {
162+ return false
163+ }
164+ n++
165+ }
166+ return n >= 2
167+}
168+
169+// validBusName reports whether s is a valid unique (":1.42") or well-known
170+// ("org.example.App") bus name.
171+func validBusName(s string) bool {
172+ if s == "" || len(s) > 255 {
173+ return false
174+ }
175+ unique := s[0] == ':'
176+ if unique {
177+ s = s[1:]
178+ }
179+ n := 0
180+ for e := range strings.SplitSeq(s, ".") {
181+ if e == "" || !unique && e[0] >= '0' && e[0] <= '9' {
182+ return false
183+ }
184+ for i := 0; i < len(e); i++ {
185+ if !nameChar(e[i]) && e[i] != '-' {
186+ return false
187+ }
188+ }
189+ n++
190+ }
191+ return n >= 2
192+}
193+
194+// Reader decodes D-Bus values in place.
195+//
196+// Strings and byte slices it returns point into the message buffer: they are
197+// valid only until the next Conn.ReadMessage. Copy them (strings.Clone,
198+// bytes.Clone) to keep them longer.
199+//
200+// Read values in the order of the message signature. Errors are sticky:
201+// after the first one, every read returns a zero value and Err reports it.
202+type Reader struct {
203+ buf []byte
204+ off int
205+ big bool
206+ err error
207+}
208+
209+// Err returns the first error the reader met, or nil.
210+func (r *Reader) Err() error { return r.err }
211+
212+// Done reports whether every byte was read without error.
213+func (r *Reader) Done() bool { return r.err == nil && r.off == len(r.buf) }
214+
215+func (r *Reader) fail() {
216+ if r.err == nil {
217+ r.err = ErrMalformed
218+ }
219+}
220+
221+// take aligns to align and returns the next size bytes.
222+func (r *Reader) take(align, size int) []byte {
223+ if r.err != nil {
224+ return nil
225+ }
226+ off := pad(r.off, align)
227+ if off > len(r.buf)-size {
228+ r.fail()
229+ return nil
230+ }
231+ r.off = off + size
232+ return r.buf[off:r.off]
233+}
234+
235+// Byte reads a BYTE (y).
236+func (r *Reader) Byte() byte {
237+ if b := r.take(1, 1); b != nil {
238+ return b[0]
239+ }
240+ return 0
241+}
242+
243+// Bool reads a BOOLEAN (b).
244+func (r *Reader) Bool() bool {
245+ v := r.Uint32()
246+ if v > 1 {
247+ r.fail()
248+ return false
249+ }
250+ return v == 1
251+}
252+
253+// Int16 reads an INT16 (n).
254+func (r *Reader) Int16() int16 { return int16(r.Uint16()) }
255+
256+// Uint16 reads a UINT16 (q).
257+func (r *Reader) Uint16() uint16 {
258+ b := r.take(2, 2)
259+ if b == nil {
260+ return 0
261+ }
262+ if r.big {
263+ return binary.BigEndian.Uint16(b)
264+ }
265+ return binary.LittleEndian.Uint16(b)
266+}
267+
268+// Int32 reads an INT32 (i).
269+func (r *Reader) Int32() int32 { return int32(r.Uint32()) }
270+
271+// Uint32 reads a UINT32 (u).
272+func (r *Reader) Uint32() uint32 {
273+ b := r.take(4, 4)
274+ if b == nil {
275+ return 0
276+ }
277+ if r.big {
278+ return binary.BigEndian.Uint32(b)
279+ }
280+ return binary.LittleEndian.Uint32(b)
281+}
282+
283+// UnixFD reads a UNIX_FD (h): an index into the fds sent with the message.
284+// zerobus does not receive file descriptors, so the index has nothing to
285+// point to; it is only read so the following values can be.
286+func (r *Reader) UnixFD() uint32 { return r.Uint32() }
287+
288+// Int64 reads an INT64 (x).
289+func (r *Reader) Int64() int64 { return int64(r.Uint64()) }
290+
291+// Uint64 reads a UINT64 (t).
292+func (r *Reader) Uint64() uint64 {
293+ b := r.take(8, 8)
294+ if b == nil {
295+ return 0
296+ }
297+ if r.big {
298+ return binary.BigEndian.Uint64(b)
299+ }
300+ return binary.LittleEndian.Uint64(b)
301+}
302+
303+// Float64 reads a DOUBLE (d).
304+func (r *Reader) Float64() float64 { return math.Float64frombits(r.Uint64()) }
305+
306+// Str reads a STRING (s). The result points into the message buffer.
307+func (r *Reader) Str() string { return r.str(int(r.Uint32())) }
308+
309+// ObjectPath reads an OBJECT_PATH (o). The result points into the message
310+// buffer.
311+func (r *Reader) ObjectPath() string { return r.Str() }
312+
313+// Signature reads a SIGNATURE (g). The result points into the message buffer.
314+func (r *Reader) Signature() string { return r.str(int(r.Byte())) }
315+
316+// str returns the next n bytes as a string and skips the NUL after them.
317+func (r *Reader) str(n int) string {
318+ if r.err != nil {
319+ return ""
320+ }
321+ if n < 0 || n > len(r.buf)-r.off-1 || r.buf[r.off+n] != 0 {
322+ r.fail()
323+ return ""
324+ }
325+ s := unsafe.String(unsafe.SliceData(r.buf[r.off:]), n)
326+ r.off += n + 1
327+ return s
328+}
329+
330+// Variant reads the signature of a VARIANT (v) and returns it. Read the
331+// value that follows with the matching method, or skip it with Skip.
332+func (r *Reader) Variant() string {
333+ sig := r.Signature()
334+ if r.err == nil && sigEnd(sig, 0, 0) != len(sig) {
335+ r.fail()
336+ return ""
337+ }
338+ return sig
339+}
340+
341+// Struct aligns the reader to the start of a STRUCT or DICT_ENTRY. Call it
342+// before reading the first field.
343+func (r *Reader) Struct() { r.take(8, 0) }
344+
345+// Array reads the header of an ARRAY whose element type starts with the
346+// signature byte elem, and returns the offset where the array ends. Read the
347+// elements while More returns true:
348+//
349+// end := r.Array('s')
350+// for r.More(end) {
351+// name := r.Str()
352+// }
353+func (r *Reader) Array(elem byte) int {
354+ n := r.Uint32()
355+ if r.err != nil {
356+ return r.off
357+ }
358+ if n > maxArray {
359+ r.fail()
360+ return r.off
361+ }
362+ off := pad(r.off, alignOf(elem))
363+ if off > len(r.buf)-int(n) {
364+ r.fail()
365+ return r.off
366+ }
367+ r.off = off
368+ return off + int(n)
369+}
370+
371+// More reports whether the array that ends at end has another element.
372+func (r *Reader) More(end int) bool {
373+ if r.err != nil {
374+ return false
375+ }
376+ if r.off < end {
377+ return true
378+ }
379+ if r.off > end {
380+ r.fail()
381+ }
382+ return false
383+}
384+
385+// SkipTo moves to end, the value returned by Array. Use it to leave an array
386+// before its last element.
387+func (r *Reader) SkipTo(end int) {
388+ if r.err != nil {
389+ return
390+ }
391+ if end < r.off || end > len(r.buf) {
392+ r.fail()
393+ return
394+ }
395+ r.off = end
396+}
397+
398+// ByteArray reads an ARRAY of BYTE (ay). The result points into the message
399+// buffer.
400+func (r *Reader) ByteArray() []byte {
401+ end := r.Array('y')
402+ if r.err != nil {
403+ return nil
404+ }
405+ b := r.buf[r.off:end:end]
406+ r.off = end
407+ return b
408+}
409+
410+// Skip reads past values of the given signature. Arrays are skipped by their
411+// length, without reading their elements.
412+func (r *Reader) Skip(sig string) { r.skip(sig, 0) }
413+
414+func (r *Reader) skip(sig string, depth int) {
415+ for i := 0; i < len(sig) && r.err == nil; {
416+ i = r.skipOne(sig, i, depth)
417+ }
418+}
419+
420+// skipOne skips the value of the type at sig[i] and returns the index of the
421+// next type.
422+func (r *Reader) skipOne(sig string, i, depth int) int {
423+ if depth > maxDepth {
424+ r.fail()
425+ return len(sig)
426+ }
427+ switch c := sig[i]; c {
428+ case 'y':
429+ r.take(1, 1)
430+ case 'n', 'q':
431+ r.take(2, 2)
432+ case 'b', 'i', 'u', 'h':
433+ r.take(4, 4)
434+ case 'x', 't', 'd':
435+ r.take(8, 8)
436+ case 's', 'o':
437+ r.Str()
438+ case 'g':
439+ r.Signature()
440+ case 'v':
441+ r.skip(r.Variant(), depth+1)
442+ case 'a':
443+ end := sigEnd(sig, i, depth)
444+ if end < 0 {
445+ r.fail()
446+ return len(sig)
447+ }
448+ r.SkipTo(r.Array(sig[i+1]))
449+ return end
450+ case '(', '{':
451+ closer := byte(')')
452+ if c == '{' {
453+ closer = '}'
454+ }
455+ r.Struct()
456+ j := i + 1
457+ for j < len(sig) && sig[j] != closer && r.err == nil {
458+ j = r.skipOne(sig, j, depth+1)
459+ }
460+ if j >= len(sig) {
461+ r.fail()
462+ return len(sig)
463+ }
464+ return j + 1
465+ default:
466+ r.fail()
467+ return len(sig)
468+ }
469+ return i + 1
470+}
471+
472+// Encoder appends D-Bus values to an outgoing message. Get one from
473+// Conn.NewCall, Conn.Signal, Conn.Reply or Conn.Error, write the body values
474+// in the order of the message signature, then call Conn.Send.
475+//
476+// Errors are sticky and reported by Conn.Send.
477+type Encoder struct {
478+ buf []byte
479+ body int // offset of the body; 0 when no message is being built
480+ sig string // body signature, checked by finish
481+ err error
482+}
483+
484+func (e *Encoder) invalid() {
485+ if e.err == nil {
486+ e.err = ErrInvalid
487+ }
488+}
489+
490+func (e *Encoder) pad(n int) {
491+ for len(e.buf)&(n-1) != 0 {
492+ e.buf = append(e.buf, 0)
493+ }
494+}
495+
496+// Byte writes a BYTE (y).
497+func (e *Encoder) Byte(v byte) { e.buf = append(e.buf, v) }
498+
499+// Bool writes a BOOLEAN (b).
500+func (e *Encoder) Bool(v bool) {
501+ var u uint32
502+ if v {
503+ u = 1
504+ }
505+ e.Uint32(u)
506+}
507+
508+// Int16 writes an INT16 (n).
509+func (e *Encoder) Int16(v int16) { e.Uint16(uint16(v)) }
510+
511+// Uint16 writes a UINT16 (q).
512+func (e *Encoder) Uint16(v uint16) {
513+ e.pad(2)
514+ e.buf = binary.LittleEndian.AppendUint16(e.buf, v)
515+}
516+
517+// Int32 writes an INT32 (i).
518+func (e *Encoder) Int32(v int32) { e.Uint32(uint32(v)) }
519+
520+// Uint32 writes a UINT32 (u).
521+func (e *Encoder) Uint32(v uint32) {
522+ e.pad(4)
523+ e.buf = binary.LittleEndian.AppendUint32(e.buf, v)
524+}
525+
526+// Int64 writes an INT64 (x).
527+func (e *Encoder) Int64(v int64) { e.Uint64(uint64(v)) }
528+
529+// Uint64 writes a UINT64 (t).
530+func (e *Encoder) Uint64(v uint64) {
531+ e.pad(8)
532+ e.buf = binary.LittleEndian.AppendUint64(e.buf, v)
533+}
534+
535+// Float64 writes a DOUBLE (d).
536+func (e *Encoder) Float64(v float64) { e.Uint64(math.Float64bits(v)) }
537+
538+// Str writes a STRING (s).
539+func (e *Encoder) Str(s string) {
540+ e.Uint32(uint32(len(s)))
541+ e.text(s)
542+}
543+
544+// ObjectPath writes an OBJECT_PATH (o).
545+func (e *Encoder) ObjectPath(p string) {
546+ if !validPath(p) {
547+ e.invalid()
548+ }
549+ e.Str(p)
550+}
551+
552+// Signature writes a SIGNATURE (g).
553+func (e *Encoder) Signature(sig string) {
554+ if !validSignature(sig) {
555+ e.invalid()
556+ }
557+ e.Byte(byte(len(sig)))
558+ e.text(sig)
559+}
560+
561+// text appends s and its NUL terminator.
562+func (e *Encoder) text(s string) {
563+ // The bus disconnects a client that sends a NUL or invalid UTF-8.
564+ if strings.IndexByte(s, 0) >= 0 || !utf8.ValidString(s) {
565+ e.invalid()
566+ }
567+ e.buf = append(e.buf, s...)
568+ e.buf = append(e.buf, 0)
569+}
570+
571+// Variant writes the signature of a VARIANT (v). Write its value next.
572+func (e *Encoder) Variant(sig string) {
573+ if sigEnd(sig, 0, 0) != len(sig) {
574+ e.invalid()
575+ }
576+ e.Signature(sig)
577+}
578+
579+// Struct aligns the encoder to the start of a STRUCT or DICT_ENTRY. Call it
580+// before writing the first field.
581+func (e *Encoder) Struct() { e.pad(8) }
582+
583+// ArrayMark is an array being written; pass it to EndArray.
584+type ArrayMark struct{ at, start int }
585+
586+// BeginArray starts an ARRAY whose element type starts with the signature
587+// byte elem. Write the elements, then call EndArray:
588+//
589+// a := e.BeginArray('s')
590+// e.Str("one")
591+// e.Str("two")
592+// e.EndArray(a)
593+func (e *Encoder) BeginArray(elem byte) ArrayMark {
594+ e.pad(4)
595+ at := len(e.buf)
596+ e.buf = append(e.buf, 0, 0, 0, 0)
597+ e.pad(alignOf(elem))
598+ return ArrayMark{at: at, start: len(e.buf)}
599+}
600+
601+// EndArray finishes the array started by BeginArray.
602+func (e *Encoder) EndArray(a ArrayMark) {
603+ n := len(e.buf) - a.start
604+ if n > maxArray {
605+ e.invalid()
606+ return
607+ }
608+ binary.LittleEndian.PutUint32(e.buf[a.at:], uint32(n))
609+}
610+
611+// ByteArray writes an ARRAY of BYTE (ay).
612+func (e *Encoder) ByteArray(b []byte) {
613+ e.Uint32(uint32(len(b)))
614+ e.buf = append(e.buf, b...)
615+}
616+
617+// SetFlags sets the flags of the message being built.
618+func (e *Encoder) SetFlags(f Flags) {
619+ if e.body != 0 {
620+ e.buf[2] = byte(f)
621+ }
622+}
A wire_test.go +357 -0
@@ -0,0 +1,357 @@
1+package zerobus
2+
3+import (
4+ "errors"
5+ "testing"
6+)
7+
8+func TestSignatures(t *testing.T) {
9+ valid := []string{"", "s", "su", "a{sv}", "a(iiay)", "(s(ss))", "aa{s(ii)}", "a{oa{sa{sv}}}", "v", "g", "h"}
10+ invalid := []string{"a", "(", "()", "{sv}", "a{vs}", "a{s}", "a{sss}", "(s", "z", "a{(s)s}", "a{as}"}
11+ for _, s := range valid {
12+ if !validSignature(s) {
13+ t.Errorf("%q should be valid", s)
14+ }
15+ }
16+ for _, s := range invalid {
17+ if validSignature(s) {
18+ t.Errorf("%q should be invalid", s)
19+ }
20+ }
21+}
22+
23+func TestNames(t *testing.T) {
24+ for _, s := range []string{"org.example", "org.kde.StatusNotifierWatcher", "a_b.C1"} {
25+ if !validInterface(s) {
26+ t.Errorf("interface %q should be valid", s)
27+ }
28+ }
29+ for _, s := range []string{"", "org", "org.", ".org.a", "org.1a", "org.a-b"} {
30+ if validInterface(s) {
31+ t.Errorf("interface %q should be invalid", s)
32+ }
33+ }
34+ for _, s := range []string{":1.42", "org.freedesktop.StatusNotifierItem-799476-1", "org.a"} {
35+ if !validBusName(s) {
36+ t.Errorf("bus name %q should be valid", s)
37+ }
38+ }
39+ for _, s := range []string{"", ":", "org", "org..a", "org.1a", "org.a/b"} {
40+ if validBusName(s) {
41+ t.Errorf("bus name %q should be invalid", s)
42+ }
43+ }
44+ for _, s := range []string{"", "1a", "a.b", "a-b"} {
45+ if validMember(s) {
46+ t.Errorf("member %q should be invalid", s)
47+ }
48+ }
49+ var e Encoder
50+ e.begin(TypeMethodCall, 1, 0, "bad name", "/a", "a.b", "M", "", "")
51+ if _, err := e.finish(); !errors.Is(err, ErrInvalid) {
52+ t.Fatalf("bad destination accepted: %v", err)
53+ }
54+}
55+
56+func TestPaths(t *testing.T) {
57+ for _, p := range []string{"/", "/a", "/org/kde/StatusNotifierItem", "/a_1/B2"} {
58+ if !validPath(p) {
59+ t.Errorf("%q should be valid", p)
60+ }
61+ }
62+ for _, p := range []string{"", "a", "//", "/a/", "/a//b", "/a-b", "/a.b"} {
63+ if validPath(p) {
64+ t.Errorf("%q should be invalid", p)
65+ }
66+ }
67+}
68+
69+// build encodes a method call with every type, the way Conn does.
70+func build(e *Encoder) []byte {
71+ const sig = "ybnqiuxtdsogva{sv}ay(si)"
72+ e.begin(TypeMethodCall, 7, 0, "org.example", "/org/example", "org.example.I", "M", "", sig)
73+ e.Byte(0xAB)
74+ e.Bool(true)
75+ e.Int16(-2)
76+ e.Uint16(3)
77+ e.Int32(-4)
78+ e.Uint32(5)
79+ e.Int64(-6)
80+ e.Uint64(7)
81+ e.Float64(8.5)
82+ e.Str("héllo")
83+ e.ObjectPath("/a/b")
84+ e.Signature("a{sv}")
85+ e.Variant("u")
86+ e.Uint32(42)
87+ d := e.BeginArray('{')
88+ for _, k := range []string{"one", "two"} {
89+ e.Struct()
90+ e.Str(k)
91+ e.Variant("s")
92+ e.Str(k + "!")
93+ }
94+ e.EndArray(d)
95+ e.ByteArray([]byte{1, 2, 3})
96+ e.Struct()
97+ e.Str("x")
98+ e.Int32(9)
99+ b, err := e.finish()
100+ if err != nil {
101+ panic(err)
102+ }
103+ return b
104+}
105+
106+func TestRoundTrip(t *testing.T) {
107+ var e Encoder
108+ b := build(&e)
109+ n, err := frameLen(b)
110+ if err != nil || n != len(b) {
111+ t.Fatalf("frameLen = %d, %v; want %d", n, err, len(b))
112+ }
113+ var m Message
114+ if err := parse(b, &m); err != nil {
115+ t.Fatal(err)
116+ }
117+ if m.Type != TypeMethodCall || m.Serial != 7 || m.Path != "/org/example" || m.Interface != "org.example.I" ||
118+ m.Member != "M" || m.Destination != "org.example" || m.Signature != "ybnqiuxtdsogva{sv}ay(si)" {
119+ t.Fatalf("header: %+v", m)
120+ }
121+ r := m.Body()
122+ if r.Byte() != 0xAB || !r.Bool() || r.Int16() != -2 || r.Uint16() != 3 || r.Int32() != -4 || r.Uint32() != 5 ||
123+ r.Int64() != -6 || r.Uint64() != 7 || r.Float64() != 8.5 || r.Str() != "héllo" || r.ObjectPath() != "/a/b" ||
124+ r.Signature() != "a{sv}" {
125+ t.Fatalf("basic values: %v", r.Err())
126+ }
127+ if sig := r.Variant(); sig != "u" || r.Uint32() != 42 {
128+ t.Fatal("variant")
129+ }
130+ var keys []string
131+ end := r.Array('{')
132+ for r.More(end) {
133+ r.Struct()
134+ k := r.Str()
135+ if r.Variant() != "s" || r.Str() != k+"!" {
136+ t.Fatal("dict value")
137+ }
138+ keys = append(keys, k)
139+ }
140+ if len(keys) != 2 || keys[0] != "one" || keys[1] != "two" {
141+ t.Fatalf("keys %v", keys)
142+ }
143+ if ay := r.ByteArray(); len(ay) != 3 || ay[2] != 3 {
144+ t.Fatalf("ay %v", ay)
145+ }
146+ r.Struct()
147+ if r.Str() != "x" || r.Int32() != 9 || !r.Done() {
148+ t.Fatalf("struct: %v", r.Err())
149+ }
150+
151+ // Skip the same body by its signature.
152+ r = m.Body()
153+ r.Skip(m.Signature)
154+ if !r.Done() {
155+ t.Fatalf("skip: %v off=%d len=%d", r.Err(), r.off, len(r.buf))
156+ }
157+}
158+
159+func TestEmptyArrayAlignment(t *testing.T) {
160+ // An empty array of 8-aligned elements still pads to the element
161+ // alignment, and its length excludes that padding.
162+ var e Encoder
163+ e.begin(TypeSignal, 1, 0, "", "/a", "a.b", "C", "", "a(ii)u")
164+ a := e.BeginArray('(')
165+ e.EndArray(a)
166+ e.Uint32(1)
167+ b, err := e.finish()
168+ if err != nil {
169+ t.Fatal(err)
170+ }
171+ var m Message
172+ if err := parse(b, &m); err != nil {
173+ t.Fatal(err)
174+ }
175+ r := m.Body()
176+ end := r.Array('(')
177+ if r.More(end) || r.Uint32() != 1 || !r.Done() {
178+ t.Fatalf("got %v", r.Err())
179+ }
180+}
181+
182+func TestBigEndian(t *testing.T) {
183+ // A signal from a big-endian peer: path /a, interface a.b, member C,
184+ // body "u" = 0x01020304. The field array is 55 bytes, then 1 byte of
185+ // padding before the body.
186+ be := []byte{'B', 4, 0, 1, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 55,
187+ 1, 1, 'o', 0, 0, 0, 0, 2, '/', 'a', 0, 0, 0, 0, 0, 0,
188+ 2, 1, 's', 0, 0, 0, 0, 3, 'a', '.', 'b', 0, 0, 0, 0, 0,
189+ 3, 1, 's', 0, 0, 0, 0, 1, 'C', 0, 0, 0, 0, 0, 0, 0,
190+ 8, 1, 'g', 0, 1, 'u', 0, 0,
191+ 1, 2, 3, 4}
192+ n, err := frameLen(be)
193+ if err != nil || n != len(be) {
194+ t.Fatalf("frameLen %d %v want %d", n, err, len(be))
195+ }
196+ var m Message
197+ if err := parse(be, &m); err != nil {
198+ t.Fatal(err)
199+ }
200+ if m.Path != "/a" || m.Interface != "a.b" || m.Member != "C" || m.Body().Uint32() != 0x01020304 {
201+ t.Fatalf("%+v", m)
202+ }
203+}
204+
205+func TestEncoderRejects(t *testing.T) {
206+ cases := map[string]func(e *Encoder){
207+ "nul in string": func(e *Encoder) { e.Str("a\x00b") },
208+ "bad utf-8": func(e *Encoder) { e.Str("\xff") },
209+ "bad path": func(e *Encoder) { e.ObjectPath("a/b") },
210+ "missing value": func(e *Encoder) {},
211+ "wrong type": func(e *Encoder) { e.Byte(1) },
212+ "extra value": func(e *Encoder) { e.Str("a"); e.Str("b") },
213+ "bad variant sig": func(e *Encoder) { e.Variant("ss") },
214+ }
215+ for name, f := range cases {
216+ var e Encoder
217+ e.begin(TypeSignal, 1, 0, "", "/a", "a.b", "C", "", "s")
218+ f(&e)
219+ if _, err := e.finish(); !errors.Is(err, ErrInvalid) {
220+ t.Errorf("%s: err = %v, want ErrInvalid", name, err)
221+ }
222+ }
223+}
224+
225+func TestParseRejects(t *testing.T) {
226+ var e Encoder
227+ good := append([]byte(nil), build(&e)...)
228+ mutate := map[string]func(b []byte) []byte{
229+ "bad endian": func(b []byte) []byte { b[0] = 'x'; return b },
230+ "bad version": func(b []byte) []byte { b[3] = 2; return b },
231+ "zero serial": func(b []byte) []byte { b[8], b[9], b[10], b[11] = 0, 0, 0, 0; return b },
232+ "short body": func(b []byte) []byte { return b[:len(b)-1] },
233+ "huge body": func(b []byte) []byte { b[7] = 0xff; return b },
234+ "no member": func(b []byte) []byte { return dropField(b, fieldMember) },
235+ "bad padding": func(b []byte) []byte { return dirtyPadding(b) },
236+ }
237+ for name, f := range mutate {
238+ b := f(append([]byte(nil), good...))
239+ var m Message
240+ n, err := frameLen(b)
241+ if err == nil && n == len(b) {
242+ err = parse(b, &m)
243+ } else if err == nil {
244+ err = ErrMalformed
245+ }
246+ if err == nil {
247+ t.Errorf("%s: accepted", name)
248+ }
249+ }
250+}
251+
252+// dropField renames the header field code to an unknown one, so parse
253+// ignores it.
254+func dropField(b []byte, code byte) []byte {
255+ for i := 16; i < len(b)-1; i += 8 {
256+ if b[i] == code && b[i+1] == 1 {
257+ b[i] = 200
258+ return b
259+ }
260+ }
261+ return b
262+}
263+
264+// dirtyPadding sets the last padding byte before the body to a non-zero
265+// value.
266+func dirtyPadding(b []byte) []byte {
267+ fields := int(b[12]) | int(b[13])<<8
268+ if end := headerLen + fields; end%8 != 0 {
269+ b[pad(end, 8)-1] = 1
270+ }
271+ return b
272+}
273+
274+func TestReaderStickyErrors(t *testing.T) {
275+ r := Reader{buf: []byte{1, 0, 0}}
276+ if r.Uint32() != 0 || r.Err() == nil {
277+ t.Fatal("short read should fail")
278+ }
279+ if r.Byte() != 0 || r.Str() != "" {
280+ t.Fatal("reads after an error should return zero values")
281+ }
282+ // A string without its NUL.
283+ r = Reader{buf: []byte{1, 0, 0, 0, 'a', 'b'}}
284+ if r.Str() != "" || r.Err() == nil {
285+ t.Fatal("string without NUL should fail")
286+ }
287+ // An array longer than the buffer.
288+ r = Reader{buf: []byte{0xff, 0, 0, 0}}
289+ end := r.Array('y')
290+ if r.More(end) || r.Err() == nil {
291+ t.Fatal("oversized array should fail")
292+ }
293+}
294+
295+func TestEncodeParseNoAllocs(t *testing.T) {
296+ var e Encoder
297+ var m Message
298+ build(&e) // grow the buffer once
299+ allocs := testing.AllocsPerRun(100, func() {
300+ b := build(&e)
301+ if err := parse(b, &m); err != nil {
302+ t.Fatal(err)
303+ }
304+ r := m.Body()
305+ r.Skip(m.Signature)
306+ if !r.Done() {
307+ t.Fatal(r.Err())
308+ }
309+ })
310+ if allocs != 0 {
311+ t.Fatalf("%v allocations per message, want 0", allocs)
312+ }
313+}
314+
315+func FuzzParse(f *testing.F) {
316+ var e Encoder
317+ f.Add(append([]byte(nil), build(&e)...))
318+ f.Fuzz(func(t *testing.T, b []byte) {
319+ if len(b) < headerLen {
320+ return
321+ }
322+ n, err := frameLen(b)
323+ if err != nil || n > len(b) {
324+ return
325+ }
326+ var m Message
327+ if parse(b[:n], &m) != nil {
328+ return
329+ }
330+ r := m.Body()
331+ r.Skip(m.Signature)
332+ })
333+}
334+
335+func FuzzSkip(f *testing.F) {
336+ f.Add("a{sv}", []byte{8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 'a', 0, 1, 'u', 0, 0, 0, 0, 7, 0, 0, 0})
337+ f.Add("(v)", []byte{1, 'v', 0})
338+ f.Fuzz(func(t *testing.T, sig string, b []byte) {
339+ if !validSignature(sig) {
340+ return
341+ }
342+ r := Reader{buf: b}
343+ r.Skip(sig)
344+ })
345+}
346+
347+func BenchmarkEncodeParse(b *testing.B) {
348+ var e Encoder
349+ var m Message
350+ b.ReportAllocs()
351+ for b.Loop() {
352+ buf := build(&e)
353+ if err := parse(buf, &m); err != nil {
354+ b.Fatal(err)
355+ }
356+ }
357+}
@@ -0,0 +1,357 @@
1+package zerobus
2+
3+import (
4+ "errors"
5+ "testing"
6+)
7+
8+func TestSignatures(t *testing.T) {
9+ valid := []string{"", "s", "su", "a{sv}", "a(iiay)", "(s(ss))", "aa{s(ii)}", "a{oa{sa{sv}}}", "v", "g", "h"}
10+ invalid := []string{"a", "(", "()", "{sv}", "a{vs}", "a{s}", "a{sss}", "(s", "z", "a{(s)s}", "a{as}"}
11+ for _, s := range valid {
12+ if !validSignature(s) {
13+ t.Errorf("%q should be valid", s)
14+ }
15+ }
16+ for _, s := range invalid {
17+ if validSignature(s) {
18+ t.Errorf("%q should be invalid", s)
19+ }
20+ }
21+}
22+
23+func TestNames(t *testing.T) {
24+ for _, s := range []string{"org.example", "org.kde.StatusNotifierWatcher", "a_b.C1"} {
25+ if !validInterface(s) {
26+ t.Errorf("interface %q should be valid", s)
27+ }
28+ }
29+ for _, s := range []string{"", "org", "org.", ".org.a", "org.1a", "org.a-b"} {
30+ if validInterface(s) {
31+ t.Errorf("interface %q should be invalid", s)
32+ }
33+ }
34+ for _, s := range []string{":1.42", "org.freedesktop.StatusNotifierItem-799476-1", "org.a"} {
35+ if !validBusName(s) {
36+ t.Errorf("bus name %q should be valid", s)
37+ }
38+ }
39+ for _, s := range []string{"", ":", "org", "org..a", "org.1a", "org.a/b"} {
40+ if validBusName(s) {
41+ t.Errorf("bus name %q should be invalid", s)
42+ }
43+ }
44+ for _, s := range []string{"", "1a", "a.b", "a-b"} {
45+ if validMember(s) {
46+ t.Errorf("member %q should be invalid", s)
47+ }
48+ }
49+ var e Encoder
50+ e.begin(TypeMethodCall, 1, 0, "bad name", "/a", "a.b", "M", "", "")
51+ if _, err := e.finish(); !errors.Is(err, ErrInvalid) {
52+ t.Fatalf("bad destination accepted: %v", err)
53+ }
54+}
55+
56+func TestPaths(t *testing.T) {
57+ for _, p := range []string{"/", "/a", "/org/kde/StatusNotifierItem", "/a_1/B2"} {
58+ if !validPath(p) {
59+ t.Errorf("%q should be valid", p)
60+ }
61+ }
62+ for _, p := range []string{"", "a", "//", "/a/", "/a//b", "/a-b", "/a.b"} {
63+ if validPath(p) {
64+ t.Errorf("%q should be invalid", p)
65+ }
66+ }
67+}
68+
69+// build encodes a method call with every type, the way Conn does.
70+func build(e *Encoder) []byte {
71+ const sig = "ybnqiuxtdsogva{sv}ay(si)"
72+ e.begin(TypeMethodCall, 7, 0, "org.example", "/org/example", "org.example.I", "M", "", sig)
73+ e.Byte(0xAB)
74+ e.Bool(true)
75+ e.Int16(-2)
76+ e.Uint16(3)
77+ e.Int32(-4)
78+ e.Uint32(5)
79+ e.Int64(-6)
80+ e.Uint64(7)
81+ e.Float64(8.5)
82+ e.Str("héllo")
83+ e.ObjectPath("/a/b")
84+ e.Signature("a{sv}")
85+ e.Variant("u")
86+ e.Uint32(42)
87+ d := e.BeginArray('{')
88+ for _, k := range []string{"one", "two"} {
89+ e.Struct()
90+ e.Str(k)
91+ e.Variant("s")
92+ e.Str(k + "!")
93+ }
94+ e.EndArray(d)
95+ e.ByteArray([]byte{1, 2, 3})
96+ e.Struct()
97+ e.Str("x")
98+ e.Int32(9)
99+ b, err := e.finish()
100+ if err != nil {
101+ panic(err)
102+ }
103+ return b
104+}
105+
106+func TestRoundTrip(t *testing.T) {
107+ var e Encoder
108+ b := build(&e)
109+ n, err := frameLen(b)
110+ if err != nil || n != len(b) {
111+ t.Fatalf("frameLen = %d, %v; want %d", n, err, len(b))
112+ }
113+ var m Message
114+ if err := parse(b, &m); err != nil {
115+ t.Fatal(err)
116+ }
117+ if m.Type != TypeMethodCall || m.Serial != 7 || m.Path != "/org/example" || m.Interface != "org.example.I" ||
118+ m.Member != "M" || m.Destination != "org.example" || m.Signature != "ybnqiuxtdsogva{sv}ay(si)" {
119+ t.Fatalf("header: %+v", m)
120+ }
121+ r := m.Body()
122+ if r.Byte() != 0xAB || !r.Bool() || r.Int16() != -2 || r.Uint16() != 3 || r.Int32() != -4 || r.Uint32() != 5 ||
123+ r.Int64() != -6 || r.Uint64() != 7 || r.Float64() != 8.5 || r.Str() != "héllo" || r.ObjectPath() != "/a/b" ||
124+ r.Signature() != "a{sv}" {
125+ t.Fatalf("basic values: %v", r.Err())
126+ }
127+ if sig := r.Variant(); sig != "u" || r.Uint32() != 42 {
128+ t.Fatal("variant")
129+ }
130+ var keys []string
131+ end := r.Array('{')
132+ for r.More(end) {
133+ r.Struct()
134+ k := r.Str()
135+ if r.Variant() != "s" || r.Str() != k+"!" {
136+ t.Fatal("dict value")
137+ }
138+ keys = append(keys, k)
139+ }
140+ if len(keys) != 2 || keys[0] != "one" || keys[1] != "two" {
141+ t.Fatalf("keys %v", keys)
142+ }
143+ if ay := r.ByteArray(); len(ay) != 3 || ay[2] != 3 {
144+ t.Fatalf("ay %v", ay)
145+ }
146+ r.Struct()
147+ if r.Str() != "x" || r.Int32() != 9 || !r.Done() {
148+ t.Fatalf("struct: %v", r.Err())
149+ }
150+
151+ // Skip the same body by its signature.
152+ r = m.Body()
153+ r.Skip(m.Signature)
154+ if !r.Done() {
155+ t.Fatalf("skip: %v off=%d len=%d", r.Err(), r.off, len(r.buf))
156+ }
157+}
158+
159+func TestEmptyArrayAlignment(t *testing.T) {
160+ // An empty array of 8-aligned elements still pads to the element
161+ // alignment, and its length excludes that padding.
162+ var e Encoder
163+ e.begin(TypeSignal, 1, 0, "", "/a", "a.b", "C", "", "a(ii)u")
164+ a := e.BeginArray('(')
165+ e.EndArray(a)
166+ e.Uint32(1)
167+ b, err := e.finish()
168+ if err != nil {
169+ t.Fatal(err)
170+ }
171+ var m Message
172+ if err := parse(b, &m); err != nil {
173+ t.Fatal(err)
174+ }
175+ r := m.Body()
176+ end := r.Array('(')
177+ if r.More(end) || r.Uint32() != 1 || !r.Done() {
178+ t.Fatalf("got %v", r.Err())
179+ }
180+}
181+
182+func TestBigEndian(t *testing.T) {
183+ // A signal from a big-endian peer: path /a, interface a.b, member C,
184+ // body "u" = 0x01020304. The field array is 55 bytes, then 1 byte of
185+ // padding before the body.
186+ be := []byte{'B', 4, 0, 1, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 55,
187+ 1, 1, 'o', 0, 0, 0, 0, 2, '/', 'a', 0, 0, 0, 0, 0, 0,
188+ 2, 1, 's', 0, 0, 0, 0, 3, 'a', '.', 'b', 0, 0, 0, 0, 0,
189+ 3, 1, 's', 0, 0, 0, 0, 1, 'C', 0, 0, 0, 0, 0, 0, 0,
190+ 8, 1, 'g', 0, 1, 'u', 0, 0,
191+ 1, 2, 3, 4}
192+ n, err := frameLen(be)
193+ if err != nil || n != len(be) {
194+ t.Fatalf("frameLen %d %v want %d", n, err, len(be))
195+ }
196+ var m Message
197+ if err := parse(be, &m); err != nil {
198+ t.Fatal(err)
199+ }
200+ if m.Path != "/a" || m.Interface != "a.b" || m.Member != "C" || m.Body().Uint32() != 0x01020304 {
201+ t.Fatalf("%+v", m)
202+ }
203+}
204+
205+func TestEncoderRejects(t *testing.T) {
206+ cases := map[string]func(e *Encoder){
207+ "nul in string": func(e *Encoder) { e.Str("a\x00b") },
208+ "bad utf-8": func(e *Encoder) { e.Str("\xff") },
209+ "bad path": func(e *Encoder) { e.ObjectPath("a/b") },
210+ "missing value": func(e *Encoder) {},
211+ "wrong type": func(e *Encoder) { e.Byte(1) },
212+ "extra value": func(e *Encoder) { e.Str("a"); e.Str("b") },
213+ "bad variant sig": func(e *Encoder) { e.Variant("ss") },
214+ }
215+ for name, f := range cases {
216+ var e Encoder
217+ e.begin(TypeSignal, 1, 0, "", "/a", "a.b", "C", "", "s")
218+ f(&e)
219+ if _, err := e.finish(); !errors.Is(err, ErrInvalid) {
220+ t.Errorf("%s: err = %v, want ErrInvalid", name, err)
221+ }
222+ }
223+}
224+
225+func TestParseRejects(t *testing.T) {
226+ var e Encoder
227+ good := append([]byte(nil), build(&e)...)
228+ mutate := map[string]func(b []byte) []byte{
229+ "bad endian": func(b []byte) []byte { b[0] = 'x'; return b },
230+ "bad version": func(b []byte) []byte { b[3] = 2; return b },
231+ "zero serial": func(b []byte) []byte { b[8], b[9], b[10], b[11] = 0, 0, 0, 0; return b },
232+ "short body": func(b []byte) []byte { return b[:len(b)-1] },
233+ "huge body": func(b []byte) []byte { b[7] = 0xff; return b },
234+ "no member": func(b []byte) []byte { return dropField(b, fieldMember) },
235+ "bad padding": func(b []byte) []byte { return dirtyPadding(b) },
236+ }
237+ for name, f := range mutate {
238+ b := f(append([]byte(nil), good...))
239+ var m Message
240+ n, err := frameLen(b)
241+ if err == nil && n == len(b) {
242+ err = parse(b, &m)
243+ } else if err == nil {
244+ err = ErrMalformed
245+ }
246+ if err == nil {
247+ t.Errorf("%s: accepted", name)
248+ }
249+ }
250+}
251+
252+// dropField renames the header field code to an unknown one, so parse
253+// ignores it.
254+func dropField(b []byte, code byte) []byte {
255+ for i := 16; i < len(b)-1; i += 8 {
256+ if b[i] == code && b[i+1] == 1 {
257+ b[i] = 200
258+ return b
259+ }
260+ }
261+ return b
262+}
263+
264+// dirtyPadding sets the last padding byte before the body to a non-zero
265+// value.
266+func dirtyPadding(b []byte) []byte {
267+ fields := int(b[12]) | int(b[13])<<8
268+ if end := headerLen + fields; end%8 != 0 {
269+ b[pad(end, 8)-1] = 1
270+ }
271+ return b
272+}
273+
274+func TestReaderStickyErrors(t *testing.T) {
275+ r := Reader{buf: []byte{1, 0, 0}}
276+ if r.Uint32() != 0 || r.Err() == nil {
277+ t.Fatal("short read should fail")
278+ }
279+ if r.Byte() != 0 || r.Str() != "" {
280+ t.Fatal("reads after an error should return zero values")
281+ }
282+ // A string without its NUL.
283+ r = Reader{buf: []byte{1, 0, 0, 0, 'a', 'b'}}
284+ if r.Str() != "" || r.Err() == nil {
285+ t.Fatal("string without NUL should fail")
286+ }
287+ // An array longer than the buffer.
288+ r = Reader{buf: []byte{0xff, 0, 0, 0}}
289+ end := r.Array('y')
290+ if r.More(end) || r.Err() == nil {
291+ t.Fatal("oversized array should fail")
292+ }
293+}
294+
295+func TestEncodeParseNoAllocs(t *testing.T) {
296+ var e Encoder
297+ var m Message
298+ build(&e) // grow the buffer once
299+ allocs := testing.AllocsPerRun(100, func() {
300+ b := build(&e)
301+ if err := parse(b, &m); err != nil {
302+ t.Fatal(err)
303+ }
304+ r := m.Body()
305+ r.Skip(m.Signature)
306+ if !r.Done() {
307+ t.Fatal(r.Err())
308+ }
309+ })
310+ if allocs != 0 {
311+ t.Fatalf("%v allocations per message, want 0", allocs)
312+ }
313+}
314+
315+func FuzzParse(f *testing.F) {
316+ var e Encoder
317+ f.Add(append([]byte(nil), build(&e)...))
318+ f.Fuzz(func(t *testing.T, b []byte) {
319+ if len(b) < headerLen {
320+ return
321+ }
322+ n, err := frameLen(b)
323+ if err != nil || n > len(b) {
324+ return
325+ }
326+ var m Message
327+ if parse(b[:n], &m) != nil {
328+ return
329+ }
330+ r := m.Body()
331+ r.Skip(m.Signature)
332+ })
333+}
334+
335+func FuzzSkip(f *testing.F) {
336+ f.Add("a{sv}", []byte{8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 'a', 0, 1, 'u', 0, 0, 0, 0, 7, 0, 0, 0})
337+ f.Add("(v)", []byte{1, 'v', 0})
338+ f.Fuzz(func(t *testing.T, sig string, b []byte) {
339+ if !validSignature(sig) {
340+ return
341+ }
342+ r := Reader{buf: b}
343+ r.Skip(sig)
344+ })
345+}
346+
347+func BenchmarkEncodeParse(b *testing.B) {
348+ var e Encoder
349+ var m Message
350+ b.ReportAllocs()
351+ for b.Loop() {
352+ buf := build(&e)
353+ if err := parse(buf, &m); err != nil {
354+ b.Fatal(err)
355+ }
356+ }
357+}

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 3

↑↓ moveenter openesc close