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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +379 -64
M .github/workflows/ci.yml +2 -0
| @@ -22,8 +22,10 @@ jobs: | |||
| 22 | 22 | - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends dbus | |
| 23 | 23 | - run: test -z "$(gofmt -l .)" | |
| 24 | 24 | - run: go vet ./... | |
| 25 | + | - run: GOARCH=386 go vet ./... | |
| 25 | 26 | - run: go test -race -count=1 ./... | |
| 26 | 27 | - name: Fuzz smoke | |
| 27 | 28 | run: | | |
| 28 | 29 | go test -run '^$' -fuzz '^FuzzParse$' -fuzztime 20s . | |
| 29 | 30 | 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 dbus | 22 | - 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 smoke | 27 | - 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 @@ | |||
| 3 | 3 | A small D-Bus client for Go that does not allocate once it is running. | |
| 4 | 4 | ||
| 5 | 5 | ```sh | |
| 6 | - | go get github.com/bnema/zerobus | |
| 6 | + | go get github.com/bnema/zerobus # Go 1.24 or newer | |
| 7 | 7 | ``` | |
| 8 | 8 | ||
| 9 | 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 | 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 | 38 | ## 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 | 84 | ## Answer calls | |
| 85 | 85 | ||
| @@ -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 | ```sh | 5 | ```sh | ||
| 6 | - | go get github.com/bnema/zerobus | 6 | + | 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 values | 38 | ## 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 calls | 84 | ## Answer calls | ||
| 85 | 85 | ||||
M conn.go +39 -11
| @@ -2,6 +2,7 @@ package zerobus | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "bytes" | |
| 5 | + | "encoding/binary" | |
| 5 | 6 | "errors" | |
| 6 | 7 | "fmt" | |
| 7 | 8 | "io" | |
| @@ -18,8 +19,12 @@ const ( | |||
| 18 | 19 | defaultBufSz = 16 << 10 | |
| 19 | 20 | ) | |
| 20 | 21 | ||
| 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 | + | ) | |
| 23 | 28 | ||
| 24 | 29 | // Error is a D-Bus error reply. | |
| 25 | 30 | type Error struct { | |
| @@ -49,10 +54,14 @@ type Conn struct { | |||
| 49 | 54 | msg Message | |
| 50 | 55 | enc Encoder | |
| 51 | 56 | err error // sticky: the connection is unusable after it | |
| 57 | + | handling bool // inside Handle | |
| 52 | 58 | ||
| 53 | 59 | // 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. | |
| 56 | 65 | Handle func(*Message) | |
| 57 | 66 | } | |
| 58 | 67 | ||
| @@ -65,7 +74,7 @@ func SessionBus() (*Conn, error) { | |||
| 65 | 74 | if dir == "" { | |
| 66 | 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 | 79 | return Dial(addr) | |
| 71 | 80 | } | |
| @@ -124,6 +133,12 @@ func dialOne(addr string) (*Conn, error) { | |||
| 124 | 133 | if sock == "" { | |
| 125 | 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 | 142 | uc, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: sock, Net: "unix"}) | |
| 128 | 143 | if err != nil { | |
| 129 | 144 | return nil, fmt.Errorf("zerobus: %w", err) | |
| @@ -278,18 +293,22 @@ func (c *Conn) Send() (uint32, error) { | |||
| 278 | 293 | c.err = closedErr(err) | |
| 279 | 294 | return 0, c.err | |
| 280 | 295 | } | |
| 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 | |
| 286 | 297 | } | |
| 287 | 298 | ||
| 288 | 299 | // Call sends the method call being built and waits for its reply. Messages | |
| 289 | 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 | 303 | // The reply is valid until the next read, like ReadMessage's. | |
| 292 | 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 | 312 | serial, err := c.Send() | |
| 294 | 313 | if err != nil { | |
| 295 | 314 | return nil, err | |
| @@ -306,11 +325,17 @@ func (c *Conn) Call() (*Message, error) { | |||
| 306 | 325 | return m, nil | |
| 307 | 326 | } | |
| 308 | 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 | 339 | // replyError builds an *Error from an error reply. It copies the strings so | |
| 315 | 340 | // the error stays valid; this is the only allocation of an error path. | |
| 316 | 341 | func replyError(m *Message) *Error { | |
| @@ -326,6 +351,9 @@ func replyError(m *Message) *Error { | |||
| 326 | 351 | // ReadMessage blocks until the next message arrives. The message and its | |
| 327 | 352 | // strings are valid until the next call to ReadMessage or Call. | |
| 328 | 353 | func (c *Conn) ReadMessage() (*Message, error) { | |
| 354 | + | if c.handling { | |
| 355 | + | return nil, ErrNested | |
| 356 | + | } | |
| 329 | 357 | if c.err != nil { | |
| 330 | 358 | return nil, c.err | |
| 331 | 359 | } | |
| @@ -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 << 10 | 19 | 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 Message | 54 | msg Message | ||
| 50 | enc Encoder | 55 | enc Encoder | ||
| 51 | err error // sticky: the connection is unusable after it | 56 | 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 its | 59 | // 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 | 60 | + | // 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.err | 294 | return 0, c.err | ||
| 280 | } | 295 | } | ||
| 281 | - | return readSerial(b), nil | 296 | + | 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. Messages | 299 | // 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, err | 314 | return nil, err | ||
| @@ -306,11 +325,17 @@ func (c *Conn) Call() (*Message, error) { | |||||
| 306 | return m, nil | 325 | 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 so | 339 | // 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 its | 351 | // 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.err | 358 | return nil, c.err | ||
| 331 | } | 359 | } | ||
M conn_test.go +57 -0
| @@ -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 | 216 | func TestCloseUnblocksRead(t *testing.T) { | |
| 160 | 217 | c := dial(t, privateBus(t)) | |
| 161 | 218 | 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 @@ | |||
| 1 | 1 | module github.com/bnema/zerobus | |
| 2 | 2 | ||
| 3 | - | go 1.27 | |
| 3 | + | go 1.24 | |
| @@ -1,3 +1,3 @@ | |||||
| 1 | module github.com/bnema/zerobus | 1 | module github.com/bnema/zerobus | ||
| 2 | 2 | ||||
| 3 | - | go 1.27 | 3 | + | go 1.24 |
M message.go +24 -12
| @@ -1,9 +1,6 @@ | |||
| 1 | 1 | package zerobus | |
| 2 | 2 | ||
| 3 | - | import ( | |
| 4 | - | "encoding/binary" | |
| 5 | - | "math" | |
| 6 | - | ) | |
| 3 | + | import "encoding/binary" | |
| 7 | 4 | ||
| 8 | 5 | // Type is the kind of a message. | |
| 9 | 6 | type Type byte | |
| @@ -175,7 +172,8 @@ func parse(b []byte, m *Message) error { | |||
| 175 | 172 | } | |
| 176 | 173 | } | |
| 177 | 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 | 177 | return ErrMalformed | |
| 180 | 178 | } | |
| 181 | 179 | switch m.Type { | |
| @@ -205,11 +203,26 @@ func (e *Encoder) begin(t Type, serial, replySerial uint32, dest, path, iface, m | |||
| 205 | 203 | e.buf = append(e.buf[:0], 'l', byte(t), 0, 1, 0, 0, 0, 0) | |
| 206 | 204 | e.buf = binary.LittleEndian.AppendUint32(e.buf, serial) | |
| 207 | 205 | 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 | 208 | if dest != "" && !validBusName(dest) || iface != "" && !validInterface(iface) || | |
| 210 | 209 | member != "" && !validMember(member) || errName != "" && !validInterface(errName) { | |
| 211 | 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 | 226 | fields := e.BeginArray('(') | |
| 214 | 227 | str := func(code byte, typ, v string) { | |
| 215 | 228 | if v == "" { | |
| @@ -253,15 +266,14 @@ func (e *Encoder) finish() ([]byte, error) { | |||
| 253 | 266 | if e.err != nil { | |
| 254 | 267 | return nil, e.err | |
| 255 | 268 | } | |
| 256 | - | n := len(e.buf) - e.body | |
| 257 | - | if len(e.buf) > maxMessage || n > math.MaxUint32 { | |
| 269 | + | if len(e.buf) > maxMessage { | |
| 258 | 270 | return nil, ErrTooLarge | |
| 259 | 271 | } | |
| 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. | |
| 263 | 275 | r := Reader{buf: e.buf[e.body:]} | |
| 264 | - | r.Skip(e.sig) | |
| 276 | + | r.check(e.sig) | |
| 265 | 277 | if !r.Done() { | |
| 266 | 278 | return nil, ErrInvalid | |
| 267 | 279 | } | |
| @@ -1,9 +1,6 @@ | |||||
| 1 | package zerobus | 1 | 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 byte | 6 | 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 ErrMalformed | 177 | 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, nil | 205 | 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.err | 267 | return nil, e.err | ||
| 255 | } | 268 | } | ||
| 256 | - | n := len(e.buf) - e.body | 269 | + | if len(e.buf) > maxMessage { |
| 257 | - | if len(e.buf) > maxMessage || n > math.MaxUint32 { | |||
| 258 | return nil, ErrTooLarge | 270 | 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 bus | 273 | + | // 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, ErrInvalid | 278 | return nil, ErrInvalid | ||
| 267 | } | 279 | } | ||
M wire.go +50 -26
| @@ -12,7 +12,8 @@ import ( | |||
| 12 | 12 | const ( | |
| 13 | 13 | maxMessage = 128 << 20 // the D-Bus limit for a whole message | |
| 14 | 14 | 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 | |
| 16 | 17 | ) | |
| 17 | 18 | ||
| 18 | 19 | var ( | |
| @@ -43,8 +44,12 @@ func pad(off, n int) int { return (off + n - 1) &^ (n - 1) } | |||
| 43 | 44 | ||
| 44 | 45 | // sigEnd returns the index just after the single complete type that starts | |
| 45 | 46 | // 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 { | |
| 48 | 53 | return -1 | |
| 49 | 54 | } | |
| 50 | 55 | switch sig[i] { | |
| @@ -52,16 +57,16 @@ func sigEnd(sig string, i, depth int) int { | |||
| 52 | 57 | return i + 1 | |
| 53 | 58 | case 'a': | |
| 54 | 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 | 63 | case '(': | |
| 59 | 64 | j := i + 1 | |
| 60 | 65 | if j < len(sig) && sig[j] == ')' { | |
| 61 | 66 | return -1 // empty structs are not allowed | |
| 62 | 67 | } | |
| 63 | 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 | 70 | return -1 | |
| 66 | 71 | } | |
| 67 | 72 | } | |
| @@ -75,12 +80,12 @@ func sigEnd(sig string, i, depth int) int { | |||
| 75 | 80 | ||
| 76 | 81 | // dictEnd returns the index just after the dict entry type at sig[i] ('{'). | |
| 77 | 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 | 84 | j := i + 1 | |
| 80 | - | if j >= len(sig) || !basic(sig[j]) { | |
| 85 | + | if j >= len(sig) || !basic(sig[j]) || structs > maxNest { | |
| 81 | 86 | 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 | 89 | return -1 | |
| 85 | 90 | } | |
| 86 | 91 | return j + 1 | |
| @@ -101,7 +106,7 @@ func validSignature(sig string) bool { | |||
| 101 | 106 | return false | |
| 102 | 107 | } | |
| 103 | 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 | 110 | return false | |
| 106 | 111 | } | |
| 107 | 112 | } | |
| @@ -331,7 +336,7 @@ func (r *Reader) str(n int) string { | |||
| 331 | 336 | // value that follows with the matching method, or skip it with Skip. | |
| 332 | 337 | func (r *Reader) Variant() string { | |
| 333 | 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 | 340 | r.fail() | |
| 336 | 341 | return "" | |
| 337 | 342 | } | |
| @@ -409,17 +414,22 @@ func (r *Reader) ByteArray() []byte { | |||
| 409 | 414 | ||
| 410 | 415 | // Skip reads past values of the given signature. Arrays are skipped by their | |
| 411 | 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 | 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 | 430 | // 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 { | |
| 423 | 433 | if depth > maxDepth { | |
| 424 | 434 | r.fail() | |
| 425 | 435 | return len(sig) | |
| @@ -429,24 +439,38 @@ func (r *Reader) skipOne(sig string, i, depth int) int { | |||
| 429 | 439 | r.take(1, 1) | |
| 430 | 440 | case 'n', 'q': | |
| 431 | 441 | r.take(2, 2) | |
| 432 | - | case 'b', 'i', 'u', 'h': | |
| 442 | + | case 'b': | |
| 443 | + | r.Bool() | |
| 444 | + | case 'i', 'u', 'h': | |
| 433 | 445 | r.take(4, 4) | |
| 434 | 446 | case 'x', 't', 'd': | |
| 435 | 447 | r.take(8, 8) | |
| 436 | 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 | 453 | case 'g': | |
| 439 | - | r.Signature() | |
| 454 | + | if s := r.Signature(); strict && !validSignature(s) { | |
| 455 | + | r.fail() | |
| 456 | + | } | |
| 440 | 457 | case 'v': | |
| 441 | - | r.skip(r.Variant(), depth+1) | |
| 458 | + | r.skip(r.Variant(), depth+1, strict) | |
| 442 | 459 | case 'a': | |
| 443 | - | end := sigEnd(sig, i, depth) | |
| 444 | - | if end < 0 { | |
| 460 | + | next := sigEnd(sig, i) | |
| 461 | + | if next < 0 { | |
| 445 | 462 | r.fail() | |
| 446 | 463 | return len(sig) | |
| 447 | 464 | } | |
| 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 | |
| 450 | 474 | case '(', '{': | |
| 451 | 475 | closer := byte(')') | |
| 452 | 476 | if c == '{' { | |
| @@ -455,7 +479,7 @@ func (r *Reader) skipOne(sig string, i, depth int) int { | |||
| 455 | 479 | r.Struct() | |
| 456 | 480 | j := i + 1 | |
| 457 | 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 | 484 | if j >= len(sig) { | |
| 461 | 485 | r.fail() | |
| @@ -570,7 +594,7 @@ func (e *Encoder) text(s string) { | |||
| 570 | 594 | ||
| 571 | 595 | // Variant writes the signature of a VARIANT (v). Write its value next. | |
| 572 | 596 | func (e *Encoder) Variant(sig string) { | |
| 573 | - | if sigEnd(sig, 0, 0) != len(sig) { | |
| 597 | + | if sigEnd(sig, 0) != len(sig) { | |
| 574 | 598 | e.invalid() | |
| 575 | 599 | } | |
| 576 | 600 | e.Signature(sig) | |
| @@ -12,7 +12,8 @@ import ( | |||||
| 12 | const ( | 12 | const ( | ||
| 13 | maxMessage = 128 << 20 // the D-Bus limit for a whole message | 13 | maxMessage = 128 << 20 // the D-Bus limit for a whole message | ||
| 14 | maxArray = 64 << 20 // the D-Bus limit for one array | 14 | 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 | |||
| 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 starts | 45 | // 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 -1 | 53 | 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 + 1 | 57 | 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 + 1 | 64 | j := i + 1 | ||
| 60 | if j < len(sig) && sig[j] == ')' { | 65 | if j < len(sig) && sig[j] == ')' { | ||
| 61 | return -1 // empty structs are not allowed | 66 | 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 -1 | 70 | 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 + 1 | 84 | j := i + 1 | ||
| 80 | - | if j >= len(sig) || !basic(sig[j]) { | 85 | + | if j >= len(sig) || !basic(sig[j]) || structs > maxNest { |
| 81 | return -1 | 86 | 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 -1 | 89 | return -1 | ||
| 85 | } | 90 | } | ||
| 86 | return j + 1 | 91 | return j + 1 | ||
| @@ -101,7 +106,7 @@ func validSignature(sig string) bool { | |||||
| 101 | return false | 106 | 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 false | 110 | 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 their | 415 | // 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 the | 430 | // 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 end | 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 | |||
| 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 + 1 | 480 | 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | + | "strings" | |
| 5 | 6 | "testing" | |
| 6 | 7 | ) | |
| 7 | 8 | ||
| @@ -18,6 +19,25 @@ func TestSignatures(t *testing.T) { | |||
| 18 | 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 | 43 | func TestNames(t *testing.T) { | |
| @@ -46,10 +66,25 @@ func TestNames(t *testing.T) { | |||
| 46 | 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 | 80 | var e Encoder | |
| 50 | - | e.begin(TypeMethodCall, 1, 0, "bad name", "/a", "a.b", "M", "", "") | |
| 81 | + | e.begin(TypeSignal, 1, 0, "", "/a", "", "C", "", "") | |
| 51 | 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 | 246 | "wrong type": func(e *Encoder) { e.Byte(1) }, | |
| 212 | 247 | "extra value": func(e *Encoder) { e.Str("a"); e.Str("b") }, | |
| 213 | 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 | 262 | for name, f := range cases { | |
| 263 | + | sig := sigs[name] | |
| 264 | + | if sig == "" { | |
| 265 | + | sig = "s" | |
| 266 | + | } | |
| 216 | 267 | 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 | 269 | f(&e) | |
| 219 | 270 | if _, err := e.finish(); !errors.Is(err, ErrInvalid) { | |
| 220 | 271 | t.Errorf("%s: err = %v, want ErrInvalid", name, err) | |
| @@ -226,13 +277,14 @@ func TestParseRejects(t *testing.T) { | |||
| 226 | 277 | var e Encoder | |
| 227 | 278 | good := append([]byte(nil), build(&e)...) | |
| 228 | 279 | 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) }, | |
| 236 | 288 | } | |
| 237 | 289 | for name, f := range mutate { | |
| 238 | 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 | 415 | func FuzzSkip(f *testing.F) { | |
| 336 | 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 | 417 | 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 Encoder | 80 | 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 Encoder | 267 | 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 Encoder | 277 | 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}) | ||