aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/system/dbus/address.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/system/dbus/address.go')
-rw-r--r--internal/system/dbus/address.go193
1 files changed, 193 insertions, 0 deletions
diff --git a/internal/system/dbus/address.go b/internal/system/dbus/address.go
new file mode 100644
index 00000000..edee7e20
--- /dev/null
+++ b/internal/system/dbus/address.go
@@ -0,0 +1,193 @@
+package dbus
+
+import (
+ "bytes"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "slices"
+)
+
+type AddrEntry struct {
+ Method string `json:"method"`
+ Values [][2]string `json:"values"`
+}
+
+// EqualAddrEntries returns whether two slices of [AddrEntry] are equal.
+func EqualAddrEntries(entries, target []AddrEntry) bool {
+ return slices.EqualFunc(entries, target, func(a AddrEntry, b AddrEntry) bool {
+ return a.Method == b.Method && slices.Equal(a.Values, b.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
+}