dbus: implement dbus_parse_address
All checks were successful
Tests / Go tests (push) Successful in 1m14s
Nix / NixOS tests (push) Successful in 7m36s

This parses D-Bus addresses according to spec. It does significantly fewer copies than dbus_parse_address.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-01-12 23:24:03 +09:00
parent ac543a1ce8
commit 1651eb06df
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
3 changed files with 360 additions and 0 deletions

186
dbus/address.go Normal file
View File

@ -0,0 +1,186 @@
package dbus
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"slices"
)
type AddrEntry struct {
Method string `json:"method"`
Values [][2]string `json:"values"`
}
// Parse parses D-Bus address according to
// https://dbus.freedesktop.org/doc/dbus-specification.html#addresses
func Parse(addr []byte) ([]AddrEntry, error) {
// Look for a semicolon
address := bytes.Split(bytes.TrimSuffix(addr, []byte{';'}), []byte{';'})
// Allocate for entries
v := make([]AddrEntry, len(address))
for i, s := range address {
var pairs [][]byte
// Look for the colon :
if method, list, ok := bytes.Cut(s, []byte{':'}); !ok {
return v, &BadAddressError{ErrNoColon, i, s, -1, nil}
} else {
pairs = bytes.Split(list, []byte{','})
v[i].Method = string(method)
v[i].Values = make([][2]string, len(pairs))
}
for j, pair := range pairs {
key, value, ok := bytes.Cut(pair, []byte{'='})
if !ok {
return v, &BadAddressError{ErrBadPairSep, i, s, j, pair}
}
if len(key) == 0 {
return v, &BadAddressError{ErrBadPairKey, i, s, j, pair}
}
if len(value) == 0 {
return v, &BadAddressError{ErrBadPairVal, i, s, j, pair}
}
v[i].Values[j][0] = string(key)
if val, errno := unescapeValue(value); errno != errSuccess {
return v, &BadAddressError{errno, i, s, j, pair}
} else {
v[i].Values[j][1] = string(val)
}
}
}
return v, nil
}
func unescapeValue(v []byte) (val []byte, errno ParseError) {
if l := len(v) - (bytes.Count(v, []byte{'%'}) * 2); l < 0 {
errno = ErrBadValLength
return
} else {
val = make([]byte, l)
}
var i, skip int
for iu, b := range v {
if skip > 0 {
skip--
continue
}
if ib := bytes.IndexByte([]byte("-_/.\\*"), b); ib != -1 { // - // _/.\*
goto opt
} else if b >= '0' && b <= '9' { // 0-9
goto opt
} else if b >= 'A' && b <= 'Z' { // A-Z
goto opt
} else if b >= 'a' && b <= 'z' { // a-z
goto opt
}
if b != '%' {
errno = ErrBadValByte
break
}
skip += 2
if iu+2 >= len(v) {
errno = ErrBadValHexLength
break
}
if c, err := hex.Decode(val[i:i+1], v[iu+1:iu+3]); err != nil {
if errors.As(err, new(hex.InvalidByteError)) {
errno = ErrBadValHexByte
break
}
// unreachable
panic(err.Error())
} else if c != 1 {
// unreachable
panic(fmt.Sprintf("invalid decode length %d", c))
}
i++
continue
opt:
val[i] = b
i++
}
return
}
type ParseError uint8
func (e ParseError) Error() string {
switch e {
case errSuccess:
panic("attempted to return success as error")
case ErrNoColon:
return "address does not contain a colon"
case ErrBadPairSep:
return "'=' character not found"
case ErrBadPairKey:
return "'=' character has no key preceding it"
case ErrBadPairVal:
return "'=' character has no value following it"
case ErrBadValLength:
return "unescaped value has impossible length"
case ErrBadValByte:
return "in D-Bus address, characters other than [-0-9A-Za-z_/.\\*] should have been escaped"
case ErrBadValHexLength:
return "in D-Bus address, percent character was not followed by two hex digits"
case ErrBadValHexByte:
return "in D-Bus address, percent character was followed by characters other than hex digits"
default:
return fmt.Sprintf("parse error %d", e)
}
}
const (
errSuccess ParseError = iota
ErrNoColon
ErrBadPairSep
ErrBadPairKey
ErrBadPairVal
ErrBadValLength
ErrBadValByte
ErrBadValHexLength
ErrBadValHexByte
)
type BadAddressError struct {
// error type
Type ParseError
// bad entry position
EntryPos int
// bad entry value
EntryVal []byte
// bad pair position
PairPos int
// bad pair value
PairVal []byte
}
func (a *BadAddressError) Is(err error) bool {
var b *BadAddressError
return errors.As(err, &b) && a.Type == b.Type &&
a.EntryPos == b.EntryPos && slices.Equal(a.EntryVal, b.EntryVal) &&
a.PairPos == b.PairPos && slices.Equal(a.PairVal, b.PairVal)
}
func (a *BadAddressError) Error() string {
return a.Type.Error()
}
func (a *BadAddressError) Unwrap() error {
return a.Type
}

View File

@ -0,0 +1,55 @@
package dbus
import (
"testing"
)
func TestUnescapeValue(t *testing.T) {
testCases := []struct {
value string
want string
wantErr ParseError
}{
// upstream test cases
{value: "abcde", want: "abcde"},
{value: "", want: ""},
{value: "%20%20", want: " "},
{value: "%24", want: "$"},
{value: "%25", want: "%"},
{value: "abc%24", want: "abc$"},
{value: "%24abc", want: "$abc"},
{value: "abc%24abc", want: "abc$abc"},
{value: "/", want: "/"},
{value: "-", want: "-"},
{value: "_", want: "_"},
{value: "A", want: "A"},
{value: "I", want: "I"},
{value: "Z", want: "Z"},
{value: "a", want: "a"},
{value: "i", want: "i"},
{value: "z", want: "z"},
/* Bug: https://bugs.freedesktop.org/show_bug.cgi?id=53499 */
{value: "%c3%b6", want: "\xc3\xb6"},
{value: "%a", wantErr: ErrBadValHexLength},
{value: "%q", wantErr: ErrBadValHexLength},
{value: "%az", wantErr: ErrBadValHexByte},
{value: "%%", wantErr: ErrBadValLength},
{value: "%$$", wantErr: ErrBadValHexByte},
{value: "abc%a", wantErr: ErrBadValHexLength},
{value: "%axyz", wantErr: ErrBadValHexByte},
{value: "%", wantErr: ErrBadValLength},
{value: "$", wantErr: ErrBadValByte},
{value: " ", wantErr: ErrBadValByte},
}
for _, tc := range testCases {
t.Run("unescape "+tc.value, func(t *testing.T) {
if got, errno := unescapeValue([]byte(tc.value)); errno != tc.wantErr {
t.Errorf("unescapeValue() errno = %v, wantErr %v", errno, tc.wantErr)
} else if tc.wantErr == errSuccess && string(got) != tc.want {
t.Errorf("unescapeValue() = %q, want %q", got, tc.want)
}
})
}
}

119
dbus/address_test.go Normal file
View File

@ -0,0 +1,119 @@
package dbus_test
import (
"errors"
"reflect"
"testing"
"git.gensokyo.uk/security/fortify/dbus"
)
func TestParse(t *testing.T) {
testCases := []struct {
name string
addr string
want []dbus.AddrEntry
wantErr error
}{
{
name: "simple session unix",
addr: "unix:path=/run/user/1971/bus",
want: []dbus.AddrEntry{{
Method: "unix",
Values: [][2]string{{"path", "/run/user/1971/bus"}},
}},
},
{
name: "simple upper escape",
addr: "debug:name=Test,cat=cute,escaped=%c3%b6",
want: []dbus.AddrEntry{{
Method: "debug",
Values: [][2]string{
{"name", "Test"},
{"cat", "cute"},
{"escaped", "\xc3\xb6"},
},
}},
},
{
name: "simple bad escape",
addr: "debug:name=%",
wantErr: &dbus.BadAddressError{Type: dbus.ErrBadValLength,
EntryPos: 0, EntryVal: []byte("debug:name=%"), PairPos: 0, PairVal: []byte("name=%")},
},
// upstream test cases
{
name: "full address success",
addr: "unix:path=/tmp/foo;debug:name=test,sliff=sloff;",
want: []dbus.AddrEntry{
{Method: "unix", Values: [][2]string{{"path", "/tmp/foo"}}},
{Method: "debug", Values: [][2]string{{"name", "test"}, {"sliff", "sloff"}}},
},
},
{
name: "empty address",
addr: "",
wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon,
EntryVal: []byte{}, PairPos: -1},
},
{
name: "no body",
addr: "foo",
wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon,
EntryPos: 0, EntryVal: []byte("foo"), PairPos: -1},
},
{
name: "no pair separator",
addr: "foo:bar",
wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep,
EntryPos: 0, EntryVal: []byte("foo:bar"), PairPos: 0, PairVal: []byte("bar")},
},
{
name: "no pair separator multi pair",
addr: "foo:bar,baz",
wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep,
EntryPos: 0, EntryVal: []byte("foo:bar,baz"), PairPos: 0, PairVal: []byte("bar")},
},
{
name: "no pair separator single valid pair",
addr: "foo:bar=foo,baz",
wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep,
EntryPos: 0, EntryVal: []byte("foo:bar=foo,baz"), PairPos: 1, PairVal: []byte("baz")},
},
{
name: "no body single valid address",
addr: "foo:bar=foo;baz",
wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon,
EntryPos: 1, EntryVal: []byte("baz"), PairPos: -1},
},
{
name: "no key",
addr: "foo:=foo",
wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairKey,
EntryPos: 0, EntryVal: []byte("foo:=foo"), PairPos: 0, PairVal: []byte("=foo")},
},
{
name: "no value",
addr: "foo:foo=",
wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairVal,
EntryPos: 0, EntryVal: []byte("foo:foo="), PairPos: 0, PairVal: []byte("foo=")},
},
{
name: "no pair separator single valid pair trailing",
addr: "foo:foo,bar=baz",
wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep,
EntryPos: 0, EntryVal: []byte("foo:foo,bar=baz"), PairPos: 0, PairVal: []byte("foo")},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got, err := dbus.Parse([]byte(tc.addr)); !errors.Is(err, tc.wantErr) {
t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr)
} else if tc.wantErr == nil && !reflect.DeepEqual(got, tc.want) {
t.Errorf("Parse() = %#v, want %#v", got, tc.want)
}
})
}
}