detached 455c41e

fix: build on 32-bit, check array elements and required header fields, and guard Handle reentrancy

Signatures limit array and struct nesting to 32 each. Received messages
without a signature must have an empty body. Call refuses messages that
get no reply. The module needs Go 1.24.
commit
455c41e14c8d0e7ca905779be20972ff73915307
author
brice · · 2026-10-08 17:46 UTC
parent
731ad81
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +379 -64
M .github/workflows/ci.yml +2 -0
@@ -22,8 +22,10 @@ jobs:
2222 - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends dbus
2323 - run: test -z "$(gofmt -l .)"
2424 - run: go vet ./...
25+ - run: GOARCH=386 go vet ./...
2526 - run: go test -race -count=1 ./...
2627 - name: Fuzz smoke
2728 run: |
2829 go test -run '^$' -fuzz '^FuzzParse$' -fuzztime 20s .
2930 go test -run '^$' -fuzz '^FuzzSkip$' -fuzztime 20s .
31+ go test -run '^$' -fuzz '^FuzzCheck$' -fuzztime 20s .
@@ -22,8 +22,10 @@ jobs:
22 - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends dbus22 - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends dbus
23 - run: test -z "$(gofmt -l .)"23 - run: test -z "$(gofmt -l .)"
24 - run: go vet ./...24 - run: go vet ./...
25+ - run: GOARCH=386 go vet ./...
25 - run: go test -race -count=1 ./...26 - run: go test -race -count=1 ./...
26 - name: Fuzz smoke27 - name: Fuzz smoke
27 run: |28 run: |
28 go test -run '^$' -fuzz '^FuzzParse$' -fuzztime 20s .29 go test -run '^$' -fuzz '^FuzzParse$' -fuzztime 20s .
29 go test -run '^$' -fuzz '^FuzzSkip$' -fuzztime 20s .30 go test -run '^$' -fuzz '^FuzzSkip$' -fuzztime 20s .
31+ go test -run '^$' -fuzz '^FuzzCheck$' -fuzztime 20s .
M README.md +3 -3
@@ -3,7 +3,7 @@
33 A small D-Bus client for Go that does not allocate once it is running.
44
55 ```sh
6-go get github.com/bnema/zerobus
6+go get github.com/bnema/zerobus # Go 1.24 or newer
77 ```
88
99 - **Zero allocations per message.** After the first few messages warm up its buffers, a call, a reply or a signal costs no heap allocation.
@@ -33,7 +33,7 @@ if err != nil {
3333 owner := m.Body().Str()
3434 ```
3535
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.
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). Before sending, zerobus reads the whole body back with its signature and checks names, paths and strings, so an invalid message returns `ErrInvalid` instead of getting the connection dropped by the bus.
3737
3838 ## Read values
3939
@@ -79,7 +79,7 @@ for {
7979 }
8080 ```
8181
82-`Call` reads messages until its reply arrives. Set `Conn.Handle` to receive the signals that arrive meanwhile; they are dropped otherwise.
82+`Call` reads messages until its reply arrives. Set `Conn.Handle` to receive the messages that arrive meanwhile; they are dropped otherwise. `Handle` may send (answer a call, emit a signal), but not wait: `Call` and `ReadMessage` return `ErrNested` inside it.
8383
8484 ## Answer calls
8585
@@ -3,7 +3,7 @@
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 ```sh5 ```sh
6-go get github.com/bnema/zerobus6+go get github.com/bnema/zerobus # Go 1.24 or newer
7 ```7 ```
8 8
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.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.
@@ -33,7 +33,7 @@ if err != nil {
33 owner := m.Body().Str()33 owner := m.Body().Str()
34 ```34 ```
35 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.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). Before sending, zerobus reads the whole body back with its signature and checks names, paths and strings, so an invalid message returns `ErrInvalid` instead of getting the connection dropped by the bus.
37 37
38 ## Read values38 ## Read values
39 39
@@ -79,7 +79,7 @@ for {
79 }79 }
80 ```80 ```
81 81
82-`Call` reads messages until its reply arrives. Set `Conn.Handle` to receive the signals that arrive meanwhile; they are dropped otherwise.82+`Call` reads messages until its reply arrives. Set `Conn.Handle` to receive the messages that arrive meanwhile; they are dropped otherwise. `Handle` may send (answer a call, emit a signal), but not wait: `Call` and `ReadMessage` return `ErrNested` inside it.
83 83
84 ## Answer calls84 ## Answer calls
85 85
M conn.go +39 -11
@@ -2,6 +2,7 @@ package zerobus
22
33 import (
44 "bytes"
5+ "encoding/binary"
56 "errors"
67 "fmt"
78 "io"
@@ -18,8 +19,12 @@ const (
1819 defaultBufSz = 16 << 10
1920 )
2021
21-// ErrClosed reports a read or send on a closed or broken connection.
22-var ErrClosed = errors.New("zerobus: connection closed")
22+var (
23+ // ErrClosed reports a read or send on a closed or broken connection.
24+ ErrClosed = errors.New("zerobus: connection closed")
25+ // ErrNested reports a Call or ReadMessage made from inside Conn.Handle.
26+ ErrNested = errors.New("zerobus: Call or ReadMessage inside Handle")
27+)
2328
2429 // Error is a D-Bus error reply.
2530 type Error struct {
@@ -49,10 +54,14 @@ type Conn struct {
4954 msg Message
5055 enc Encoder
5156 err error // sticky: the connection is unusable after it
57+ handling bool // inside Handle
5258
5359 // 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.
60+ // reply. It may be nil; those messages are then dropped.
61+ //
62+ // Handle may answer or emit messages with NewReply, NewError,
63+ // NewSignal and Send. It may not wait: Call and ReadMessage return
64+ // ErrNested inside it.
5665 Handle func(*Message)
5766 }
5867
@@ -65,7 +74,7 @@ func SessionBus() (*Conn, error) {
6574 if dir == "" {
6675 return nil, errors.New("zerobus: no session bus: DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR are not set")
6776 }
68- addr = "unix:path=" + dir + "/bus"
77+ return dialSocket(dir + "/bus")
6978 }
7079 return Dial(addr)
7180 }
@@ -124,6 +133,12 @@ func dialOne(addr string) (*Conn, error) {
124133 if sock == "" {
125134 return nil, fmt.Errorf("zerobus: address %q has no path or abstract socket", addr)
126135 }
136+ return dialSocket(sock)
137+}
138+
139+// dialSocket connects to the Unix socket sock ("@name" for an abstract
140+// one), authenticates, and registers with the bus.
141+func dialSocket(sock string) (*Conn, error) {
127142 uc, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: sock, Net: "unix"})
128143 if err != nil {
129144 return nil, fmt.Errorf("zerobus: %w", err)
@@ -278,18 +293,22 @@ func (c *Conn) Send() (uint32, error) {
278293 c.err = closedErr(err)
279294 return 0, c.err
280295 }
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
296+ return binary.LittleEndian.Uint32(b[8:]), nil
286297 }
287298
288299 // Call sends the method call being built and waits for its reply. Messages
289300 // that arrive meanwhile go to Handle. An error reply returns an *Error.
301+// A call flagged FlagNoReplyExpected gets no reply: send it with Send.
290302 //
291303 // The reply is valid until the next read, like ReadMessage's.
292304 func (c *Conn) Call() (*Message, error) {
305+ if c.handling {
306+ return nil, ErrNested
307+ }
308+ if c.enc.body != 0 && (Type(c.enc.buf[1]) != TypeMethodCall || Flags(c.enc.buf[2])&FlagNoReplyExpected != 0) {
309+ c.enc.body = 0
310+ return nil, ErrInvalid
311+ }
293312 serial, err := c.Send()
294313 if err != nil {
295314 return nil, err
@@ -306,11 +325,17 @@ func (c *Conn) Call() (*Message, error) {
306325 return m, nil
307326 }
308327 if c.Handle != nil {
309- c.Handle(m)
328+ c.handle(m)
310329 }
311330 }
312331 }
313332
333+func (c *Conn) handle(m *Message) {
334+ c.handling = true
335+ defer func() { c.handling = false }()
336+ c.Handle(m)
337+}
338+
314339 // replyError builds an *Error from an error reply. It copies the strings so
315340 // the error stays valid; this is the only allocation of an error path.
316341 func replyError(m *Message) *Error {
@@ -326,6 +351,9 @@ func replyError(m *Message) *Error {
326351 // ReadMessage blocks until the next message arrives. The message and its
327352 // strings are valid until the next call to ReadMessage or Call.
328353 func (c *Conn) ReadMessage() (*Message, error) {
354+ if c.handling {
355+ return nil, ErrNested
356+ }
329357 if c.err != nil {
330358 return nil, c.err
331359 }
@@ -2,6 +2,7 @@ package zerobus
2 2
3 import (3 import (
4 "bytes"4 "bytes"
5+ "encoding/binary"
5 "errors"6 "errors"
6 "fmt"7 "fmt"
7 "io"8 "io"
@@ -18,8 +19,12 @@ const (
18 defaultBufSz = 16 << 1019 defaultBufSz = 16 << 10
19 )20 )
20 21
21-// ErrClosed reports a read or send on a closed or broken connection.22+var (
22-var ErrClosed = errors.New("zerobus: connection closed")23+ // ErrClosed reports a read or send on a closed or broken connection.
24+ ErrClosed = errors.New("zerobus: connection closed")
25+ // ErrNested reports a Call or ReadMessage made from inside Conn.Handle.
26+ ErrNested = errors.New("zerobus: Call or ReadMessage inside Handle")
27+)
23 28
24 // Error is a D-Bus error reply.29 // Error is a D-Bus error reply.
25 type Error struct {30 type Error struct {
@@ -49,10 +54,14 @@ type Conn struct {
49 msg Message54 msg Message
50 enc Encoder55 enc Encoder
51 err error // sticky: the connection is unusable after it56 err error // sticky: the connection is unusable after it
57+ handling bool // inside Handle
52 58
53 // Handle receives the messages that arrive while Call waits for its59 // Handle receives the messages that arrive while Call waits for its
54- // reply. It may be nil; those messages are then dropped. It must not60+ // reply. It may be nil; those messages are then dropped.
55- // call Call, Send or ReadMessage.61+ //
62+ // Handle may answer or emit messages with NewReply, NewError,
63+ // NewSignal and Send. It may not wait: Call and ReadMessage return
64+ // ErrNested inside it.
56 Handle func(*Message)65 Handle func(*Message)
57 }66 }
58 67
@@ -65,7 +74,7 @@ func SessionBus() (*Conn, error) {
65 if dir == "" {74 if dir == "" {
66 return nil, errors.New("zerobus: no session bus: DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR are not set")75 return nil, errors.New("zerobus: no session bus: DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR are not set")
67 }76 }
68- addr = "unix:path=" + dir + "/bus"77+ return dialSocket(dir + "/bus")
69 }78 }
70 return Dial(addr)79 return Dial(addr)
71 }80 }
@@ -124,6 +133,12 @@ func dialOne(addr string) (*Conn, error) {
124 if sock == "" {133 if sock == "" {
125 return nil, fmt.Errorf("zerobus: address %q has no path or abstract socket", addr)134 return nil, fmt.Errorf("zerobus: address %q has no path or abstract socket", addr)
126 }135 }
136+ return dialSocket(sock)
137+}
138+
139+// dialSocket connects to the Unix socket sock ("@name" for an abstract
140+// one), authenticates, and registers with the bus.
141+func dialSocket(sock string) (*Conn, error) {
127 uc, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: sock, Net: "unix"})142 uc, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: sock, Net: "unix"})
128 if err != nil {143 if err != nil {
129 return nil, fmt.Errorf("zerobus: %w", err)144 return nil, fmt.Errorf("zerobus: %w", err)
@@ -278,18 +293,22 @@ func (c *Conn) Send() (uint32, error) {
278 c.err = closedErr(err)293 c.err = closedErr(err)
279 return 0, c.err294 return 0, c.err
280 }295 }
281- return readSerial(b), nil296+ return binary.LittleEndian.Uint32(b[8:]), 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 }297 }
287 298
288 // Call sends the method call being built and waits for its reply. Messages299 // 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.300 // that arrive meanwhile go to Handle. An error reply returns an *Error.
301+// A call flagged FlagNoReplyExpected gets no reply: send it with Send.
290 //302 //
291 // The reply is valid until the next read, like ReadMessage's.303 // The reply is valid until the next read, like ReadMessage's.
292 func (c *Conn) Call() (*Message, error) {304 func (c *Conn) Call() (*Message, error) {
305+ if c.handling {
306+ return nil, ErrNested
307+ }
308+ if c.enc.body != 0 && (Type(c.enc.buf[1]) != TypeMethodCall || Flags(c.enc.buf[2])&FlagNoReplyExpected != 0) {
309+ c.enc.body = 0
310+ return nil, ErrInvalid
311+ }
293 serial, err := c.Send()312 serial, err := c.Send()
294 if err != nil {313 if err != nil {
295 return nil, err314 return nil, err
@@ -306,11 +325,17 @@ func (c *Conn) Call() (*Message, error) {
306 return m, nil325 return m, nil
307 }326 }
308 if c.Handle != nil {327 if c.Handle != nil {
309- c.Handle(m)328+ c.handle(m)
310 }329 }
311 }330 }
312 }331 }
313 332
333+func (c *Conn) handle(m *Message) {
334+ c.handling = true
335+ defer func() { c.handling = false }()
336+ c.Handle(m)
337+}
338+
314 // replyError builds an *Error from an error reply. It copies the strings so339 // 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.340 // the error stays valid; this is the only allocation of an error path.
316 func replyError(m *Message) *Error {341 func replyError(m *Message) *Error {
@@ -326,6 +351,9 @@ func replyError(m *Message) *Error {
326 // ReadMessage blocks until the next message arrives. The message and its351 // ReadMessage blocks until the next message arrives. The message and its
327 // strings are valid until the next call to ReadMessage or Call.352 // strings are valid until the next call to ReadMessage or Call.
328 func (c *Conn) ReadMessage() (*Message, error) {353 func (c *Conn) ReadMessage() (*Message, error) {
354+ if c.handling {
355+ return nil, ErrNested
356+ }
329 if c.err != nil {357 if c.err != nil {
330 return nil, c.err358 return nil, c.err
331 }359 }
M conn_test.go +57 -0
@@ -156,6 +156,63 @@ func TestSignalAndMethodBetweenPeers(t *testing.T) {
156156 }
157157 }
158158
159+func TestHandleDuringCall(t *testing.T) {
160+ addr := privateBus(t)
161+ c := dial(t, addr)
162+ peer := dial(t, addr)
163+ if err := c.AddMatch("type='signal',interface='org.example.Ping'"); err != nil {
164+ t.Fatal(err)
165+ }
166+ peer.NewSignal("/a", "org.example.Ping", "Ping", "")
167+ if _, err := peer.Send(); err != nil {
168+ t.Fatal(err)
169+ }
170+ // A round trip from the peer proves the bus has routed the signal.
171+ peer.NewCall(busName, busPath, busName, "GetId", "")
172+ if _, err := peer.Call(); err != nil {
173+ t.Fatal(err)
174+ }
175+
176+ var got string
177+ var nested error
178+ c.Handle = func(m *Message) {
179+ if m.Member == "Ping" {
180+ got = strings.Clone(m.Member)
181+ _, nested = c.ReadMessage()
182+ // Sending from Handle is allowed.
183+ c.NewSignal("/a", "org.example.Pong", "Pong", "")
184+ if _, err := c.Send(); err != nil {
185+ t.Error(err)
186+ }
187+ }
188+ }
189+ // The signal is queued before the reply, so Call passes it to Handle.
190+ c.NewCall(busName, busPath, busName, "GetId", "")
191+ if _, err := c.Call(); err != nil {
192+ t.Fatal(err)
193+ }
194+ if got != "Ping" || !errors.Is(nested, ErrNested) {
195+ t.Fatalf("handled %q, nested read: %v", got, nested)
196+ }
197+}
198+
199+func TestCallRejectsNoReply(t *testing.T) {
200+ c := dial(t, privateBus(t))
201+ c.NewCall(busName, busPath, busName, "GetId", "").SetFlags(FlagNoReplyExpected)
202+ if _, err := c.Call(); !errors.Is(err, ErrInvalid) {
203+ t.Fatalf("err = %v, want ErrInvalid", err)
204+ }
205+ c.NewSignal("/a", "a.b", "C", "")
206+ if _, err := c.Call(); !errors.Is(err, ErrInvalid) {
207+ t.Fatalf("Call on a signal: %v, want ErrInvalid", err)
208+ }
209+ // The connection still works.
210+ c.NewCall(busName, busPath, busName, "GetId", "")
211+ if _, err := c.Call(); err != nil {
212+ t.Fatal(err)
213+ }
214+}
215+
159216 func TestCloseUnblocksRead(t *testing.T) {
160217 c := dial(t, privateBus(t))
161218 errc := make(chan error, 1)
@@ -156,6 +156,63 @@ func TestSignalAndMethodBetweenPeers(t *testing.T) {
156 }156 }
157 }157 }
158 158
159+func TestHandleDuringCall(t *testing.T) {
160+ addr := privateBus(t)
161+ c := dial(t, addr)
162+ peer := dial(t, addr)
163+ if err := c.AddMatch("type='signal',interface='org.example.Ping'"); err != nil {
164+ t.Fatal(err)
165+ }
166+ peer.NewSignal("/a", "org.example.Ping", "Ping", "")
167+ if _, err := peer.Send(); err != nil {
168+ t.Fatal(err)
169+ }
170+ // A round trip from the peer proves the bus has routed the signal.
171+ peer.NewCall(busName, busPath, busName, "GetId", "")
172+ if _, err := peer.Call(); err != nil {
173+ t.Fatal(err)
174+ }
175+
176+ var got string
177+ var nested error
178+ c.Handle = func(m *Message) {
179+ if m.Member == "Ping" {
180+ got = strings.Clone(m.Member)
181+ _, nested = c.ReadMessage()
182+ // Sending from Handle is allowed.
183+ c.NewSignal("/a", "org.example.Pong", "Pong", "")
184+ if _, err := c.Send(); err != nil {
185+ t.Error(err)
186+ }
187+ }
188+ }
189+ // The signal is queued before the reply, so Call passes it to Handle.
190+ c.NewCall(busName, busPath, busName, "GetId", "")
191+ if _, err := c.Call(); err != nil {
192+ t.Fatal(err)
193+ }
194+ if got != "Ping" || !errors.Is(nested, ErrNested) {
195+ t.Fatalf("handled %q, nested read: %v", got, nested)
196+ }
197+}
198+
199+func TestCallRejectsNoReply(t *testing.T) {
200+ c := dial(t, privateBus(t))
201+ c.NewCall(busName, busPath, busName, "GetId", "").SetFlags(FlagNoReplyExpected)
202+ if _, err := c.Call(); !errors.Is(err, ErrInvalid) {
203+ t.Fatalf("err = %v, want ErrInvalid", err)
204+ }
205+ c.NewSignal("/a", "a.b", "C", "")
206+ if _, err := c.Call(); !errors.Is(err, ErrInvalid) {
207+ t.Fatalf("Call on a signal: %v, want ErrInvalid", err)
208+ }
209+ // The connection still works.
210+ c.NewCall(busName, busPath, busName, "GetId", "")
211+ if _, err := c.Call(); err != nil {
212+ t.Fatal(err)
213+ }
214+}
215+
159 func TestCloseUnblocksRead(t *testing.T) {216 func TestCloseUnblocksRead(t *testing.T) {
160 c := dial(t, privateBus(t))217 c := dial(t, privateBus(t))
161 errc := make(chan error, 1)218 errc := make(chan error, 1)
A fakebus_test.go +112 -0
@@ -0,0 +1,112 @@
1+package zerobus
2+
3+import (
4+ "bufio"
5+ "errors"
6+ "net"
7+ "path/filepath"
8+ "strings"
9+ "testing"
10+)
11+
12+// fakeBus is a minimal bus on a Unix socket that answers the handshake and
13+// Hello, then writes the given frames to the client one byte at a time, so
14+// every message is split across reads.
15+func fakeBus(t *testing.T, authReply string, frames ...[]byte) string {
16+ t.Helper()
17+ sock := filepath.Join(t.TempDir(), "bus")
18+ l, err := net.Listen("unix", sock)
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ t.Cleanup(func() { _ = l.Close() })
23+ go func() {
24+ c, err := l.Accept()
25+ if err != nil {
26+ return
27+ }
28+ defer func() { _ = c.Close() }()
29+ r := bufio.NewReader(c)
30+ if _, err := r.ReadString('\n'); err != nil { // \0AUTH EXTERNAL ...
31+ return
32+ }
33+ var hello Encoder
34+ hello.begin(TypeMethodReturn, 1, 1, "", "", "", "", "", "s")
35+ hello.Str(":1.7")
36+ b, _ := hello.finish()
37+ // The OK line and the Hello reply arrive in the same write: the
38+ // client must keep the bytes after the line.
39+ if _, err := c.Write(append([]byte(authReply), b...)); err != nil {
40+ return
41+ }
42+ if _, err := r.ReadString('\n'); err != nil { // BEGIN
43+ return
44+ }
45+ for _, f := range frames {
46+ for i := range f {
47+ if _, err := c.Write(f[i : i+1]); err != nil {
48+ return
49+ }
50+ }
51+ }
52+ _, _ = r.ReadByte() // wait for the client to close
53+ }()
54+ return "unix:path=" + sock
55+}
56+
57+func signal(member string) []byte {
58+ var e Encoder
59+ e.begin(TypeSignal, 2, 0, "", "/a", "a.b", member, "", "s")
60+ e.Str(member)
61+ b, _ := e.finish()
62+ return append([]byte(nil), b...)
63+}
64+
65+func TestFakeBusSplitFrames(t *testing.T) {
66+ addr := fakeBus(t, "OK 0123456789abcdef\r\n", signal("One"), signal("Two"))
67+ c := dial(t, addr)
68+ if c.UniqueName() != ":1.7" {
69+ t.Fatalf("unique name %q", c.UniqueName())
70+ }
71+ for _, want := range []string{"One", "Two"} {
72+ m, err := c.ReadMessage()
73+ if err != nil {
74+ t.Fatal(err)
75+ }
76+ if m.Member != want || m.Body().Str() != want {
77+ t.Fatalf("got %q, want %q", m.Member, want)
78+ }
79+ }
80+}
81+
82+func TestFakeBusRejected(t *testing.T) {
83+ addr := fakeBus(t, "REJECTED EXTERNAL\r\n")
84+ if c, err := Dial(addr); err == nil {
85+ _ = c.Close()
86+ t.Fatal("rejected auth accepted")
87+ }
88+}
89+
90+func TestFakeBusMalformedIsSticky(t *testing.T) {
91+ bad := signal("Bad")
92+ bad[3] = 9 // protocol version
93+ c := dial(t, fakeBus(t, "OK 0123456789abcdef\r\n", bad))
94+ _, err := c.ReadMessage()
95+ if !errors.Is(err, ErrMalformed) {
96+ t.Fatalf("err = %v, want ErrMalformed", err)
97+ }
98+ if _, err := c.ReadMessage(); !errors.Is(err, ErrMalformed) {
99+ t.Fatalf("second read: %v, want the same error", err)
100+ }
101+}
102+
103+func TestSessionBusFromRuntimeDir(t *testing.T) {
104+ addr := fakeBus(t, "OK 0123456789abcdef\r\n")
105+ t.Setenv("DBUS_SESSION_BUS_ADDRESS", "")
106+ t.Setenv("XDG_RUNTIME_DIR", filepath.Dir(strings.TrimPrefix(addr, "unix:path=")))
107+ c, err := SessionBus()
108+ if err != nil {
109+ t.Fatal(err)
110+ }
111+ _ = c.Close()
112+}
@@ -0,0 +1,112 @@
1+package zerobus
2+
3+import (
4+ "bufio"
5+ "errors"
6+ "net"
7+ "path/filepath"
8+ "strings"
9+ "testing"
10+)
11+
12+// fakeBus is a minimal bus on a Unix socket that answers the handshake and
13+// Hello, then writes the given frames to the client one byte at a time, so
14+// every message is split across reads.
15+func fakeBus(t *testing.T, authReply string, frames ...[]byte) string {
16+ t.Helper()
17+ sock := filepath.Join(t.TempDir(), "bus")
18+ l, err := net.Listen("unix", sock)
19+ if err != nil {
20+ t.Fatal(err)
21+ }
22+ t.Cleanup(func() { _ = l.Close() })
23+ go func() {
24+ c, err := l.Accept()
25+ if err != nil {
26+ return
27+ }
28+ defer func() { _ = c.Close() }()
29+ r := bufio.NewReader(c)
30+ if _, err := r.ReadString('\n'); err != nil { // \0AUTH EXTERNAL ...
31+ return
32+ }
33+ var hello Encoder
34+ hello.begin(TypeMethodReturn, 1, 1, "", "", "", "", "", "s")
35+ hello.Str(":1.7")
36+ b, _ := hello.finish()
37+ // The OK line and the Hello reply arrive in the same write: the
38+ // client must keep the bytes after the line.
39+ if _, err := c.Write(append([]byte(authReply), b...)); err != nil {
40+ return
41+ }
42+ if _, err := r.ReadString('\n'); err != nil { // BEGIN
43+ return
44+ }
45+ for _, f := range frames {
46+ for i := range f {
47+ if _, err := c.Write(f[i : i+1]); err != nil {
48+ return
49+ }
50+ }
51+ }
52+ _, _ = r.ReadByte() // wait for the client to close
53+ }()
54+ return "unix:path=" + sock
55+}
56+
57+func signal(member string) []byte {
58+ var e Encoder
59+ e.begin(TypeSignal, 2, 0, "", "/a", "a.b", member, "", "s")
60+ e.Str(member)
61+ b, _ := e.finish()
62+ return append([]byte(nil), b...)
63+}
64+
65+func TestFakeBusSplitFrames(t *testing.T) {
66+ addr := fakeBus(t, "OK 0123456789abcdef\r\n", signal("One"), signal("Two"))
67+ c := dial(t, addr)
68+ if c.UniqueName() != ":1.7" {
69+ t.Fatalf("unique name %q", c.UniqueName())
70+ }
71+ for _, want := range []string{"One", "Two"} {
72+ m, err := c.ReadMessage()
73+ if err != nil {
74+ t.Fatal(err)
75+ }
76+ if m.Member != want || m.Body().Str() != want {
77+ t.Fatalf("got %q, want %q", m.Member, want)
78+ }
79+ }
80+}
81+
82+func TestFakeBusRejected(t *testing.T) {
83+ addr := fakeBus(t, "REJECTED EXTERNAL\r\n")
84+ if c, err := Dial(addr); err == nil {
85+ _ = c.Close()
86+ t.Fatal("rejected auth accepted")
87+ }
88+}
89+
90+func TestFakeBusMalformedIsSticky(t *testing.T) {
91+ bad := signal("Bad")
92+ bad[3] = 9 // protocol version
93+ c := dial(t, fakeBus(t, "OK 0123456789abcdef\r\n", bad))
94+ _, err := c.ReadMessage()
95+ if !errors.Is(err, ErrMalformed) {
96+ t.Fatalf("err = %v, want ErrMalformed", err)
97+ }
98+ if _, err := c.ReadMessage(); !errors.Is(err, ErrMalformed) {
99+ t.Fatalf("second read: %v, want the same error", err)
100+ }
101+}
102+
103+func TestSessionBusFromRuntimeDir(t *testing.T) {
104+ addr := fakeBus(t, "OK 0123456789abcdef\r\n")
105+ t.Setenv("DBUS_SESSION_BUS_ADDRESS", "")
106+ t.Setenv("XDG_RUNTIME_DIR", filepath.Dir(strings.TrimPrefix(addr, "unix:path=")))
107+ c, err := SessionBus()
108+ if err != nil {
109+ t.Fatal(err)
110+ }
111+ _ = c.Close()
112+}
M go.mod +1 -1
@@ -1,3 +1,3 @@
11 module github.com/bnema/zerobus
22
3-go 1.27
3+go 1.24
@@ -1,3 +1,3 @@
1 module github.com/bnema/zerobus1 module github.com/bnema/zerobus
2 2
3-go 1.273+go 1.24
M message.go +24 -12
@@ -1,9 +1,6 @@
11 package zerobus
22
3-import (
4- "encoding/binary"
5- "math"
6-)
3+import "encoding/binary"
74
85 // Type is the kind of a message.
96 type Type byte
@@ -175,7 +172,8 @@ func parse(b []byte, m *Message) error {
175172 }
176173 }
177174 m.body = b[start:]
178- if !validSignature(m.Signature) || m.Path != "" && !validPath(m.Path) {
175+ // No signature means an empty body.
176+ if m.Signature == "" && bodyLen != 0 || !validSignature(m.Signature) || m.Path != "" && !validPath(m.Path) {
179177 return ErrMalformed
180178 }
181179 switch m.Type {
@@ -205,11 +203,26 @@ func (e *Encoder) begin(t Type, serial, replySerial uint32, dest, path, iface, m
205203 e.buf = append(e.buf[:0], 'l', byte(t), 0, 1, 0, 0, 0, 0)
206204 e.buf = binary.LittleEndian.AppendUint32(e.buf, serial)
207205 e.body, e.sig, e.err = 0, sig, nil
208- // A bus disconnects a client that sends an invalid name.
206+ // A bus disconnects a client that sends an invalid name or leaves out a
207+ // field its message type requires.
209208 if dest != "" && !validBusName(dest) || iface != "" && !validInterface(iface) ||
210209 member != "" && !validMember(member) || errName != "" && !validInterface(errName) {
211210 e.invalid()
212211 }
212+ switch t {
213+ case TypeMethodCall:
214+ if path == "" || member == "" {
215+ e.invalid()
216+ }
217+ case TypeSignal:
218+ if path == "" || iface == "" || member == "" {
219+ e.invalid()
220+ }
221+ case TypeError:
222+ if errName == "" {
223+ e.invalid()
224+ }
225+ }
213226 fields := e.BeginArray('(')
214227 str := func(code byte, typ, v string) {
215228 if v == "" {
@@ -253,15 +266,14 @@ func (e *Encoder) finish() ([]byte, error) {
253266 if e.err != nil {
254267 return nil, e.err
255268 }
256- n := len(e.buf) - e.body
257- if len(e.buf) > maxMessage || n > math.MaxUint32 {
269+ if len(e.buf) > maxMessage {
258270 return nil, ErrTooLarge
259271 }
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.
272+ binary.LittleEndian.PutUint32(e.buf[4:], uint32(len(e.buf)-e.body))
273+ // Reading the whole body back with the signature proves it matches: a
274+ // bus disconnects a client that sends a body that does not.
263275 r := Reader{buf: e.buf[e.body:]}
264- r.Skip(e.sig)
276+ r.check(e.sig)
265277 if !r.Done() {
266278 return nil, ErrInvalid
267279 }
@@ -1,9 +1,6 @@
1 package zerobus1 package zerobus
2 2
3-import (3+import "encoding/binary"
4- "encoding/binary"
5- "math"
6-)
7 4
8 // Type is the kind of a message.5 // Type is the kind of a message.
9 type Type byte6 type Type byte
@@ -175,7 +172,8 @@ func parse(b []byte, m *Message) error {
175 }172 }
176 }173 }
177 m.body = b[start:]174 m.body = b[start:]
178- if !validSignature(m.Signature) || m.Path != "" && !validPath(m.Path) {175+ // No signature means an empty body.
176+ if m.Signature == "" && bodyLen != 0 || !validSignature(m.Signature) || m.Path != "" && !validPath(m.Path) {
179 return ErrMalformed177 return ErrMalformed
180 }178 }
181 switch m.Type {179 switch m.Type {
@@ -205,11 +203,26 @@ func (e *Encoder) begin(t Type, serial, replySerial uint32, dest, path, iface, m
205 e.buf = append(e.buf[:0], 'l', byte(t), 0, 1, 0, 0, 0, 0)203 e.buf = append(e.buf[:0], 'l', byte(t), 0, 1, 0, 0, 0, 0)
206 e.buf = binary.LittleEndian.AppendUint32(e.buf, serial)204 e.buf = binary.LittleEndian.AppendUint32(e.buf, serial)
207 e.body, e.sig, e.err = 0, sig, nil205 e.body, e.sig, e.err = 0, sig, nil
208- // A bus disconnects a client that sends an invalid name.206+ // A bus disconnects a client that sends an invalid name or leaves out a
207+ // field its message type requires.
209 if dest != "" && !validBusName(dest) || iface != "" && !validInterface(iface) ||208 if dest != "" && !validBusName(dest) || iface != "" && !validInterface(iface) ||
210 member != "" && !validMember(member) || errName != "" && !validInterface(errName) {209 member != "" && !validMember(member) || errName != "" && !validInterface(errName) {
211 e.invalid()210 e.invalid()
212 }211 }
212+ switch t {
213+ case TypeMethodCall:
214+ if path == "" || member == "" {
215+ e.invalid()
216+ }
217+ case TypeSignal:
218+ if path == "" || iface == "" || member == "" {
219+ e.invalid()
220+ }
221+ case TypeError:
222+ if errName == "" {
223+ e.invalid()
224+ }
225+ }
213 fields := e.BeginArray('(')226 fields := e.BeginArray('(')
214 str := func(code byte, typ, v string) {227 str := func(code byte, typ, v string) {
215 if v == "" {228 if v == "" {
@@ -253,15 +266,14 @@ func (e *Encoder) finish() ([]byte, error) {
253 if e.err != nil {266 if e.err != nil {
254 return nil, e.err267 return nil, e.err
255 }268 }
256- n := len(e.buf) - e.body269+ if len(e.buf) > maxMessage {
257- if len(e.buf) > maxMessage || n > math.MaxUint32 {
258 return nil, ErrTooLarge270 return nil, ErrTooLarge
259 }271 }
260- binary.LittleEndian.PutUint32(e.buf[4:], uint32(n))272+ binary.LittleEndian.PutUint32(e.buf[4:], uint32(len(e.buf)-e.body))
261- // Reading the body back with the signature proves it matches: a bus273+ // Reading the whole body back with the signature proves it matches: a
262- // disconnects a client that sends a body that does not.274+ // bus disconnects a client that sends a body that does not.
263 r := Reader{buf: e.buf[e.body:]}275 r := Reader{buf: e.buf[e.body:]}
264- r.Skip(e.sig)276+ r.check(e.sig)
265 if !r.Done() {277 if !r.Done() {
266 return nil, ErrInvalid278 return nil, ErrInvalid
267 }279 }
M wire.go +50 -26
@@ -12,7 +12,8 @@ import (
1212 const (
1313 maxMessage = 128 << 20 // the D-Bus limit for a whole message
1414 maxArray = 64 << 20 // the D-Bus limit for one array
15- maxDepth = 64 // 32 array + 32 struct levels
15+ maxNest = 32 // array or struct levels in one signature
16+ maxDepth = 64 // total levels in a value, variants included
1617 )
1718
1819 var (
@@ -43,8 +44,12 @@ func pad(off, n int) int { return (off + n - 1) &^ (n - 1) }
4344
4445 // sigEnd returns the index just after the single complete type that starts
4546 // 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 {
47+func sigEnd(sig string, i int) int { return typeEnd(sig, i, 0, 0) }
48+
49+// typeEnd is sigEnd with the array and struct nesting so far. The spec
50+// limits each to 32 levels; dict entries count as structs.
51+func typeEnd(sig string, i, arrays, structs int) int {
52+ if i >= len(sig) || arrays > maxNest || structs > maxNest {
4853 return -1
4954 }
5055 switch sig[i] {
@@ -52,16 +57,16 @@ func sigEnd(sig string, i, depth int) int {
5257 return i + 1
5358 case 'a':
5459 if i+1 < len(sig) && sig[i+1] == '{' {
55- return dictEnd(sig, i+1, depth+1)
60+ return dictEnd(sig, i+1, arrays+1, structs+1)
5661 }
57- return sigEnd(sig, i+1, depth+1)
62+ return typeEnd(sig, i+1, arrays+1, structs)
5863 case '(':
5964 j := i + 1
6065 if j < len(sig) && sig[j] == ')' {
6166 return -1 // empty structs are not allowed
6267 }
6368 for j < len(sig) && sig[j] != ')' {
64- if j = sigEnd(sig, j, depth+1); j < 0 {
69+ if j = typeEnd(sig, j, arrays, structs+1); j < 0 {
6570 return -1
6671 }
6772 }
@@ -75,12 +80,12 @@ func sigEnd(sig string, i, depth int) int {
7580
7681 // dictEnd returns the index just after the dict entry type at sig[i] ('{').
7782 // Its key must be a basic type and it holds exactly two types.
78-func dictEnd(sig string, i, depth int) int {
83+func dictEnd(sig string, i, arrays, structs int) int {
7984 j := i + 1
80- if j >= len(sig) || !basic(sig[j]) {
85+ if j >= len(sig) || !basic(sig[j]) || structs > maxNest {
8186 return -1
8287 }
83- if j = sigEnd(sig, j+1, depth+1); j < 0 || j >= len(sig) || sig[j] != '}' {
88+ if j = typeEnd(sig, j+1, arrays, structs); j < 0 || j >= len(sig) || sig[j] != '}' {
8489 return -1
8590 }
8691 return j + 1
@@ -101,7 +106,7 @@ func validSignature(sig string) bool {
101106 return false
102107 }
103108 for i := 0; i < len(sig); {
104- if i = sigEnd(sig, i, 0); i < 0 {
109+ if i = sigEnd(sig, i); i < 0 {
105110 return false
106111 }
107112 }
@@ -331,7 +336,7 @@ func (r *Reader) str(n int) string {
331336 // value that follows with the matching method, or skip it with Skip.
332337 func (r *Reader) Variant() string {
333338 sig := r.Signature()
334- if r.err == nil && sigEnd(sig, 0, 0) != len(sig) {
339+ if r.err == nil && sigEnd(sig, 0) != len(sig) {
335340 r.fail()
336341 return ""
337342 }
@@ -409,17 +414,22 @@ func (r *Reader) ByteArray() []byte {
409414
410415 // Skip reads past values of the given signature. Arrays are skipped by their
411416 // length, without reading their elements.
412-func (r *Reader) Skip(sig string) { r.skip(sig, 0) }
417+func (r *Reader) Skip(sig string) { r.skip(sig, 0, false) }
418+
419+// check reads every value of sig, array elements included, and fails on any
420+// value the bus would reject. It proves an outgoing body matches its
421+// signature.
422+func (r *Reader) check(sig string) { r.skip(sig, 0, true) }
413423
414-func (r *Reader) skip(sig string, depth int) {
424+func (r *Reader) skip(sig string, depth int, strict bool) {
415425 for i := 0; i < len(sig) && r.err == nil; {
416- i = r.skipOne(sig, i, depth)
426+ i = r.skipOne(sig, i, depth, strict)
417427 }
418428 }
419429
420430 // 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 {
431+// next type. In strict mode it also reads array elements and checks values.
432+func (r *Reader) skipOne(sig string, i, depth int, strict bool) int {
423433 if depth > maxDepth {
424434 r.fail()
425435 return len(sig)
@@ -429,24 +439,38 @@ func (r *Reader) skipOne(sig string, i, depth int) int {
429439 r.take(1, 1)
430440 case 'n', 'q':
431441 r.take(2, 2)
432- case 'b', 'i', 'u', 'h':
442+ case 'b':
443+ r.Bool()
444+ case 'i', 'u', 'h':
433445 r.take(4, 4)
434446 case 'x', 't', 'd':
435447 r.take(8, 8)
436448 case 's', 'o':
437- r.Str()
449+ s := r.Str()
450+ if strict && (!utf8.ValidString(s) || c == 'o' && !validPath(s)) {
451+ r.fail()
452+ }
438453 case 'g':
439- r.Signature()
454+ if s := r.Signature(); strict && !validSignature(s) {
455+ r.fail()
456+ }
440457 case 'v':
441- r.skip(r.Variant(), depth+1)
458+ r.skip(r.Variant(), depth+1, strict)
442459 case 'a':
443- end := sigEnd(sig, i, depth)
444- if end < 0 {
460+ next := sigEnd(sig, i)
461+ if next < 0 {
445462 r.fail()
446463 return len(sig)
447464 }
448- r.SkipTo(r.Array(sig[i+1]))
449- return end
465+ end := r.Array(sig[i+1])
466+ if !strict {
467+ r.SkipTo(end)
468+ return next
469+ }
470+ for r.More(end) {
471+ r.skipOne(sig, i+1, depth+1, true)
472+ }
473+ return next
450474 case '(', '{':
451475 closer := byte(')')
452476 if c == '{' {
@@ -455,7 +479,7 @@ func (r *Reader) skipOne(sig string, i, depth int) int {
455479 r.Struct()
456480 j := i + 1
457481 for j < len(sig) && sig[j] != closer && r.err == nil {
458- j = r.skipOne(sig, j, depth+1)
482+ j = r.skipOne(sig, j, depth+1, strict)
459483 }
460484 if j >= len(sig) {
461485 r.fail()
@@ -570,7 +594,7 @@ func (e *Encoder) text(s string) {
570594
571595 // Variant writes the signature of a VARIANT (v). Write its value next.
572596 func (e *Encoder) Variant(sig string) {
573- if sigEnd(sig, 0, 0) != len(sig) {
597+ if sigEnd(sig, 0) != len(sig) {
574598 e.invalid()
575599 }
576600 e.Signature(sig)
@@ -12,7 +12,8 @@ import (
12 const (12 const (
13 maxMessage = 128 << 20 // the D-Bus limit for a whole message13 maxMessage = 128 << 20 // the D-Bus limit for a whole message
14 maxArray = 64 << 20 // the D-Bus limit for one array14 maxArray = 64 << 20 // the D-Bus limit for one array
15- maxDepth = 64 // 32 array + 32 struct levels15+ maxNest = 32 // array or struct levels in one signature
16+ maxDepth = 64 // total levels in a value, variants included
16 )17 )
17 18
18 var (19 var (
@@ -43,8 +44,12 @@ func pad(off, n int) int { return (off + n - 1) &^ (n - 1) }
43 44
44 // sigEnd returns the index just after the single complete type that starts45 // sigEnd returns the index just after the single complete type that starts
45 // at sig[i], or -1 if there is none.46 // at sig[i], or -1 if there is none.
46-func sigEnd(sig string, i, depth int) int {47+func sigEnd(sig string, i int) int { return typeEnd(sig, i, 0, 0) }
47- if i >= len(sig) || depth > maxDepth {48+
49+// typeEnd is sigEnd with the array and struct nesting so far. The spec
50+// limits each to 32 levels; dict entries count as structs.
51+func typeEnd(sig string, i, arrays, structs int) int {
52+ if i >= len(sig) || arrays > maxNest || structs > maxNest {
48 return -153 return -1
49 }54 }
50 switch sig[i] {55 switch sig[i] {
@@ -52,16 +57,16 @@ func sigEnd(sig string, i, depth int) int {
52 return i + 157 return i + 1
53 case 'a':58 case 'a':
54 if i+1 < len(sig) && sig[i+1] == '{' {59 if i+1 < len(sig) && sig[i+1] == '{' {
55- return dictEnd(sig, i+1, depth+1)60+ return dictEnd(sig, i+1, arrays+1, structs+1)
56 }61 }
57- return sigEnd(sig, i+1, depth+1)62+ return typeEnd(sig, i+1, arrays+1, structs)
58 case '(':63 case '(':
59 j := i + 164 j := i + 1
60 if j < len(sig) && sig[j] == ')' {65 if j < len(sig) && sig[j] == ')' {
61 return -1 // empty structs are not allowed66 return -1 // empty structs are not allowed
62 }67 }
63 for j < len(sig) && sig[j] != ')' {68 for j < len(sig) && sig[j] != ')' {
64- if j = sigEnd(sig, j, depth+1); j < 0 {69+ if j = typeEnd(sig, j, arrays, structs+1); j < 0 {
65 return -170 return -1
66 }71 }
67 }72 }
@@ -75,12 +80,12 @@ func sigEnd(sig string, i, depth int) int {
75 80
76 // dictEnd returns the index just after the dict entry type at sig[i] ('{').81 // 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.82 // Its key must be a basic type and it holds exactly two types.
78-func dictEnd(sig string, i, depth int) int {83+func dictEnd(sig string, i, arrays, structs int) int {
79 j := i + 184 j := i + 1
80- if j >= len(sig) || !basic(sig[j]) {85+ if j >= len(sig) || !basic(sig[j]) || structs > maxNest {
81 return -186 return -1
82 }87 }
83- if j = sigEnd(sig, j+1, depth+1); j < 0 || j >= len(sig) || sig[j] != '}' {88+ if j = typeEnd(sig, j+1, arrays, structs); j < 0 || j >= len(sig) || sig[j] != '}' {
84 return -189 return -1
85 }90 }
86 return j + 191 return j + 1
@@ -101,7 +106,7 @@ func validSignature(sig string) bool {
101 return false106 return false
102 }107 }
103 for i := 0; i < len(sig); {108 for i := 0; i < len(sig); {
104- if i = sigEnd(sig, i, 0); i < 0 {109+ if i = sigEnd(sig, i); i < 0 {
105 return false110 return false
106 }111 }
107 }112 }
@@ -331,7 +336,7 @@ func (r *Reader) str(n int) string {
331 // value that follows with the matching method, or skip it with Skip.336 // value that follows with the matching method, or skip it with Skip.
332 func (r *Reader) Variant() string {337 func (r *Reader) Variant() string {
333 sig := r.Signature()338 sig := r.Signature()
334- if r.err == nil && sigEnd(sig, 0, 0) != len(sig) {339+ if r.err == nil && sigEnd(sig, 0) != len(sig) {
335 r.fail()340 r.fail()
336 return ""341 return ""
337 }342 }
@@ -409,17 +414,22 @@ func (r *Reader) ByteArray() []byte {
409 414
410 // Skip reads past values of the given signature. Arrays are skipped by their415 // Skip reads past values of the given signature. Arrays are skipped by their
411 // length, without reading their elements.416 // length, without reading their elements.
412-func (r *Reader) Skip(sig string) { r.skip(sig, 0) }417+func (r *Reader) Skip(sig string) { r.skip(sig, 0, false) }
418+
419+// check reads every value of sig, array elements included, and fails on any
420+// value the bus would reject. It proves an outgoing body matches its
421+// signature.
422+func (r *Reader) check(sig string) { r.skip(sig, 0, true) }
413 423
414-func (r *Reader) skip(sig string, depth int) {424+func (r *Reader) skip(sig string, depth int, strict bool) {
415 for i := 0; i < len(sig) && r.err == nil; {425 for i := 0; i < len(sig) && r.err == nil; {
416- i = r.skipOne(sig, i, depth)426+ i = r.skipOne(sig, i, depth, strict)
417 }427 }
418 }428 }
419 429
420 // skipOne skips the value of the type at sig[i] and returns the index of the430 // skipOne skips the value of the type at sig[i] and returns the index of the
421-// next type.431+// next type. In strict mode it also reads array elements and checks values.
422-func (r *Reader) skipOne(sig string, i, depth int) int {432+func (r *Reader) skipOne(sig string, i, depth int, strict bool) int {
423 if depth > maxDepth {433 if depth > maxDepth {
424 r.fail()434 r.fail()
425 return len(sig)435 return len(sig)
@@ -429,24 +439,38 @@ func (r *Reader) skipOne(sig string, i, depth int) int {
429 r.take(1, 1)439 r.take(1, 1)
430 case 'n', 'q':440 case 'n', 'q':
431 r.take(2, 2)441 r.take(2, 2)
432- case 'b', 'i', 'u', 'h':442+ case 'b':
443+ r.Bool()
444+ case 'i', 'u', 'h':
433 r.take(4, 4)445 r.take(4, 4)
434 case 'x', 't', 'd':446 case 'x', 't', 'd':
435 r.take(8, 8)447 r.take(8, 8)
436 case 's', 'o':448 case 's', 'o':
437- r.Str()449+ s := r.Str()
450+ if strict && (!utf8.ValidString(s) || c == 'o' && !validPath(s)) {
451+ r.fail()
452+ }
438 case 'g':453 case 'g':
439- r.Signature()454+ if s := r.Signature(); strict && !validSignature(s) {
455+ r.fail()
456+ }
440 case 'v':457 case 'v':
441- r.skip(r.Variant(), depth+1)458+ r.skip(r.Variant(), depth+1, strict)
442 case 'a':459 case 'a':
443- end := sigEnd(sig, i, depth)460+ next := sigEnd(sig, i)
444- if end < 0 {461+ if next < 0 {
445 r.fail()462 r.fail()
446 return len(sig)463 return len(sig)
447 }464 }
448- r.SkipTo(r.Array(sig[i+1]))465+ end := r.Array(sig[i+1])
449- return end466+ if !strict {
467+ r.SkipTo(end)
468+ return next
469+ }
470+ for r.More(end) {
471+ r.skipOne(sig, i+1, depth+1, true)
472+ }
473+ return next
450 case '(', '{':474 case '(', '{':
451 closer := byte(')')475 closer := byte(')')
452 if c == '{' {476 if c == '{' {
@@ -455,7 +479,7 @@ func (r *Reader) skipOne(sig string, i, depth int) int {
455 r.Struct()479 r.Struct()
456 j := i + 1480 j := i + 1
457 for j < len(sig) && sig[j] != closer && r.err == nil {481 for j < len(sig) && sig[j] != closer && r.err == nil {
458- j = r.skipOne(sig, j, depth+1)482+ j = r.skipOne(sig, j, depth+1, strict)
459 }483 }
460 if j >= len(sig) {484 if j >= len(sig) {
461 r.fail()485 r.fail()
@@ -570,7 +594,7 @@ func (e *Encoder) text(s string) {
570 594
571 // Variant writes the signature of a VARIANT (v). Write its value next.595 // Variant writes the signature of a VARIANT (v). Write its value next.
572 func (e *Encoder) Variant(sig string) {596 func (e *Encoder) Variant(sig string) {
573- if sigEnd(sig, 0, 0) != len(sig) {597+ if sigEnd(sig, 0) != len(sig) {
574 e.invalid()598 e.invalid()
575 }599 }
576 e.Signature(sig)600 e.Signature(sig)
M wire_test.go +91 -11
@@ -2,6 +2,7 @@ package zerobus
22
33 import (
44 "errors"
5+ "strings"
56 "testing"
67 )
78
@@ -18,6 +19,25 @@ func TestSignatures(t *testing.T) {
1819 t.Errorf("%q should be invalid", s)
1920 }
2021 }
22+ // The spec allows 32 nested arrays and 32 nested structs.
23+ nest := func(open, close string, n int) string {
24+ return strings.Repeat(open, n) + "y" + strings.Repeat(close, n)
25+ }
26+ for _, c := range []struct {
27+ sig string
28+ ok bool
29+ }{
30+ {nest("a", "", 32), true},
31+ {nest("a", "", 33), false},
32+ {nest("(", ")", 32), true},
33+ {nest("(", ")", 33), false},
34+ {nest("a(", ")", 32), true},
35+ {strings.Repeat("a{s", 33) + "y" + strings.Repeat("}", 33), false},
36+ } {
37+ if validSignature(c.sig) != c.ok {
38+ t.Errorf("validSignature(%d bytes) = %v, want %v", len(c.sig), !c.ok, c.ok)
39+ }
40+ }
2141 }
2242
2343 func TestNames(t *testing.T) {
@@ -46,10 +66,25 @@ func TestNames(t *testing.T) {
4666 t.Errorf("member %q should be invalid", s)
4767 }
4868 }
69+ for name, h := range map[string][4]string{ // dest, path, iface, member
70+ "bad destination": {"bad name", "/a", "a.b", "M"},
71+ "call no path": {"a.b", "", "a.b", "M"},
72+ "call no member": {"a.b", "/a", "a.b", ""},
73+ } {
74+ var e Encoder
75+ e.begin(TypeMethodCall, 1, 0, h[0], h[1], h[2], h[3], "", "")
76+ if _, err := e.finish(); !errors.Is(err, ErrInvalid) {
77+ t.Errorf("%s accepted: %v", name, err)
78+ }
79+ }
4980 var e Encoder
50- e.begin(TypeMethodCall, 1, 0, "bad name", "/a", "a.b", "M", "", "")
81+ e.begin(TypeSignal, 1, 0, "", "/a", "", "C", "", "")
5182 if _, err := e.finish(); !errors.Is(err, ErrInvalid) {
52- t.Fatalf("bad destination accepted: %v", err)
83+ t.Errorf("signal without interface accepted: %v", err)
84+ }
85+ e.begin(TypeError, 1, 1, "", "", "", "", "", "")
86+ if _, err := e.finish(); !errors.Is(err, ErrInvalid) {
87+ t.Errorf("error without name accepted: %v", err)
5388 }
5489 }
5590
@@ -211,10 +246,26 @@ func TestEncoderRejects(t *testing.T) {
211246 "wrong type": func(e *Encoder) { e.Byte(1) },
212247 "extra value": func(e *Encoder) { e.Str("a"); e.Str("b") },
213248 "bad variant sig": func(e *Encoder) { e.Variant("ss") },
214- }
249+ "wrong element": func(e *Encoder) {
250+ a := e.BeginArray('s')
251+ e.Uint32(77)
252+ e.EndArray(a)
253+ },
254+ "bad nested variant": func(e *Encoder) {
255+ a := e.BeginArray('v')
256+ e.Byte(1)
257+ e.Str("z")
258+ e.EndArray(a)
259+ },
260+ }
261+ sigs := map[string]string{"wrong element": "as", "bad nested variant": "av"}
215262 for name, f := range cases {
263+ sig := sigs[name]
264+ if sig == "" {
265+ sig = "s"
266+ }
216267 var e Encoder
217- e.begin(TypeSignal, 1, 0, "", "/a", "a.b", "C", "", "s")
268+ e.begin(TypeSignal, 1, 0, "", "/a", "a.b", "C", "", sig)
218269 f(&e)
219270 if _, err := e.finish(); !errors.Is(err, ErrInvalid) {
220271 t.Errorf("%s: err = %v, want ErrInvalid", name, err)
@@ -226,13 +277,14 @@ func TestParseRejects(t *testing.T) {
226277 var e Encoder
227278 good := append([]byte(nil), build(&e)...)
228279 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) },
280+ "bad endian": func(b []byte) []byte { b[0] = 'x'; return b },
281+ "bad version": func(b []byte) []byte { b[3] = 2; return b },
282+ "zero serial": func(b []byte) []byte { b[8], b[9], b[10], b[11] = 0, 0, 0, 0; return b },
283+ "short body": func(b []byte) []byte { return b[:len(b)-1] },
284+ "huge body": func(b []byte) []byte { b[7] = 0xff; return b },
285+ "no member": func(b []byte) []byte { return dropField(b, fieldMember) },
286+ "bad padding": func(b []byte) []byte { return dirtyPadding(b) },
287+ "no signature": func(b []byte) []byte { return dropField(b, fieldSignature) },
236288 }
237289 for name, f := range mutate {
238290 b := f(append([]byte(nil), good...))
@@ -332,6 +384,34 @@ func FuzzParse(f *testing.F) {
332384 })
333385 }
334386
387+// FuzzCheck checks that every body the encoder accepts parses back with
388+// its signature.
389+func FuzzCheck(f *testing.F) {
390+ 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})
391+ f.Add("as", []byte{4, 0, 0, 0, 77, 0, 0, 0})
392+ f.Fuzz(func(t *testing.T, sig string, body []byte) {
393+ if !validSignature(sig) {
394+ return
395+ }
396+ var e Encoder
397+ e.begin(TypeSignal, 1, 0, "", "/a", "a.b", "C", "", sig)
398+ e.buf = append(e.buf, body...)
399+ b, err := e.finish()
400+ if err != nil {
401+ return
402+ }
403+ var m Message
404+ if err := parse(b, &m); err != nil {
405+ t.Fatalf("accepted body does not parse: %v", err)
406+ }
407+ r := m.Body()
408+ r.check(m.Signature)
409+ if !r.Done() {
410+ t.Fatalf("accepted body does not match its signature: %v", r.Err())
411+ }
412+ })
413+}
414+
335415 func FuzzSkip(f *testing.F) {
336416 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})
337417 f.Add("(v)", []byte{1, 'v', 0})
@@ -2,6 +2,7 @@ package zerobus
2 2
3 import (3 import (
4 "errors"4 "errors"
5+ "strings"
5 "testing"6 "testing"
6 )7 )
7 8
@@ -18,6 +19,25 @@ func TestSignatures(t *testing.T) {
18 t.Errorf("%q should be invalid", s)19 t.Errorf("%q should be invalid", s)
19 }20 }
20 }21 }
22+ // The spec allows 32 nested arrays and 32 nested structs.
23+ nest := func(open, close string, n int) string {
24+ return strings.Repeat(open, n) + "y" + strings.Repeat(close, n)
25+ }
26+ for _, c := range []struct {
27+ sig string
28+ ok bool
29+ }{
30+ {nest("a", "", 32), true},
31+ {nest("a", "", 33), false},
32+ {nest("(", ")", 32), true},
33+ {nest("(", ")", 33), false},
34+ {nest("a(", ")", 32), true},
35+ {strings.Repeat("a{s", 33) + "y" + strings.Repeat("}", 33), false},
36+ } {
37+ if validSignature(c.sig) != c.ok {
38+ t.Errorf("validSignature(%d bytes) = %v, want %v", len(c.sig), !c.ok, c.ok)
39+ }
40+ }
21 }41 }
22 42
23 func TestNames(t *testing.T) {43 func TestNames(t *testing.T) {
@@ -46,10 +66,25 @@ func TestNames(t *testing.T) {
46 t.Errorf("member %q should be invalid", s)66 t.Errorf("member %q should be invalid", s)
47 }67 }
48 }68 }
69+ for name, h := range map[string][4]string{ // dest, path, iface, member
70+ "bad destination": {"bad name", "/a", "a.b", "M"},
71+ "call no path": {"a.b", "", "a.b", "M"},
72+ "call no member": {"a.b", "/a", "a.b", ""},
73+ } {
74+ var e Encoder
75+ e.begin(TypeMethodCall, 1, 0, h[0], h[1], h[2], h[3], "", "")
76+ if _, err := e.finish(); !errors.Is(err, ErrInvalid) {
77+ t.Errorf("%s accepted: %v", name, err)
78+ }
79+ }
49 var e Encoder80 var e Encoder
50- e.begin(TypeMethodCall, 1, 0, "bad name", "/a", "a.b", "M", "", "")81+ e.begin(TypeSignal, 1, 0, "", "/a", "", "C", "", "")
51 if _, err := e.finish(); !errors.Is(err, ErrInvalid) {82 if _, err := e.finish(); !errors.Is(err, ErrInvalid) {
52- t.Fatalf("bad destination accepted: %v", err)83+ t.Errorf("signal without interface accepted: %v", err)
84+ }
85+ e.begin(TypeError, 1, 1, "", "", "", "", "", "")
86+ if _, err := e.finish(); !errors.Is(err, ErrInvalid) {
87+ t.Errorf("error without name accepted: %v", err)
53 }88 }
54 }89 }
55 90
@@ -211,10 +246,26 @@ func TestEncoderRejects(t *testing.T) {
211 "wrong type": func(e *Encoder) { e.Byte(1) },246 "wrong type": func(e *Encoder) { e.Byte(1) },
212 "extra value": func(e *Encoder) { e.Str("a"); e.Str("b") },247 "extra value": func(e *Encoder) { e.Str("a"); e.Str("b") },
213 "bad variant sig": func(e *Encoder) { e.Variant("ss") },248 "bad variant sig": func(e *Encoder) { e.Variant("ss") },
214- }249+ "wrong element": func(e *Encoder) {
250+ a := e.BeginArray('s')
251+ e.Uint32(77)
252+ e.EndArray(a)
253+ },
254+ "bad nested variant": func(e *Encoder) {
255+ a := e.BeginArray('v')
256+ e.Byte(1)
257+ e.Str("z")
258+ e.EndArray(a)
259+ },
260+ }
261+ sigs := map[string]string{"wrong element": "as", "bad nested variant": "av"}
215 for name, f := range cases {262 for name, f := range cases {
263+ sig := sigs[name]
264+ if sig == "" {
265+ sig = "s"
266+ }
216 var e Encoder267 var e Encoder
217- e.begin(TypeSignal, 1, 0, "", "/a", "a.b", "C", "", "s")268+ e.begin(TypeSignal, 1, 0, "", "/a", "a.b", "C", "", sig)
218 f(&e)269 f(&e)
219 if _, err := e.finish(); !errors.Is(err, ErrInvalid) {270 if _, err := e.finish(); !errors.Is(err, ErrInvalid) {
220 t.Errorf("%s: err = %v, want ErrInvalid", name, err)271 t.Errorf("%s: err = %v, want ErrInvalid", name, err)
@@ -226,13 +277,14 @@ func TestParseRejects(t *testing.T) {
226 var e Encoder277 var e Encoder
227 good := append([]byte(nil), build(&e)...)278 good := append([]byte(nil), build(&e)...)
228 mutate := map[string]func(b []byte) []byte{279 mutate := map[string]func(b []byte) []byte{
229- "bad endian": func(b []byte) []byte { b[0] = 'x'; return b },280+ "bad endian": func(b []byte) []byte { b[0] = 'x'; return b },
230- "bad version": func(b []byte) []byte { b[3] = 2; return b },281+ "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 },282+ "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] },283+ "short body": func(b []byte) []byte { return b[:len(b)-1] },
233- "huge body": func(b []byte) []byte { b[7] = 0xff; return b },284+ "huge body": func(b []byte) []byte { b[7] = 0xff; return b },
234- "no member": func(b []byte) []byte { return dropField(b, fieldMember) },285+ "no member": func(b []byte) []byte { return dropField(b, fieldMember) },
235- "bad padding": func(b []byte) []byte { return dirtyPadding(b) },286+ "bad padding": func(b []byte) []byte { return dirtyPadding(b) },
287+ "no signature": func(b []byte) []byte { return dropField(b, fieldSignature) },
236 }288 }
237 for name, f := range mutate {289 for name, f := range mutate {
238 b := f(append([]byte(nil), good...))290 b := f(append([]byte(nil), good...))
@@ -332,6 +384,34 @@ func FuzzParse(f *testing.F) {
332 })384 })
333 }385 }
334 386
387+// FuzzCheck checks that every body the encoder accepts parses back with
388+// its signature.
389+func FuzzCheck(f *testing.F) {
390+ 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})
391+ f.Add("as", []byte{4, 0, 0, 0, 77, 0, 0, 0})
392+ f.Fuzz(func(t *testing.T, sig string, body []byte) {
393+ if !validSignature(sig) {
394+ return
395+ }
396+ var e Encoder
397+ e.begin(TypeSignal, 1, 0, "", "/a", "a.b", "C", "", sig)
398+ e.buf = append(e.buf, body...)
399+ b, err := e.finish()
400+ if err != nil {
401+ return
402+ }
403+ var m Message
404+ if err := parse(b, &m); err != nil {
405+ t.Fatalf("accepted body does not parse: %v", err)
406+ }
407+ r := m.Body()
408+ r.check(m.Signature)
409+ if !r.Done() {
410+ t.Fatalf("accepted body does not match its signature: %v", r.Err())
411+ }
412+ })
413+}
414+
335 func FuzzSkip(f *testing.F) {415 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})416 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})417 f.Add("(v)", []byte{1, 'v', 0})

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