aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/dbus
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-11-15 13:47:09 +0900
committerOphestra <cat@gensokyo.uk>2025-11-15 13:58:34 +0900
commita91920310d20e1fc472dea044fce8fd9438835df (patch)
tree378cba47d8586e798130a2855f91a685e05244a3 /internal/dbus
parent16e674782abbda8c85eac2e7d0c5c22e8cfbf5de (diff)
internal: relocate packages
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/dbus')
-rw-r--r--internal/dbus/address.go193
-rw-r--r--internal/dbus/address_escape_test.go59
-rw-r--r--internal/dbus/address_test.go123
-rw-r--r--internal/dbus/config.go71
-rw-r--r--internal/dbus/config_test.go124
-rw-r--r--internal/dbus/dbus.go69
-rw-r--r--internal/dbus/dbus_test.go170
-rw-r--r--internal/dbus/export_test.go15
-rw-r--r--internal/dbus/proc.go188
-rw-r--r--internal/dbus/proc_test.go11
-rw-r--r--internal/dbus/proxy.go105
-rw-r--r--internal/dbus/samples_test.go214
-rw-r--r--internal/dbus/testdata/dev.vencord.Vesktop.json18
-rw-r--r--internal/dbus/testdata/org.chromium.Chromium+.json9
-rw-r--r--internal/dbus/testdata/org.chromium.Chromium.json24
-rw-r--r--internal/dbus/testdata/uk.gensokyo.CrashTestDummy.json21
16 files changed, 1414 insertions, 0 deletions
diff --git a/internal/dbus/address.go b/internal/dbus/address.go
new file mode 100644
index 00000000..edee7e20
--- /dev/null
+++ b/internal/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
+}
diff --git a/internal/dbus/address_escape_test.go b/internal/dbus/address_escape_test.go
new file mode 100644
index 00000000..4ef48f8b
--- /dev/null
+++ b/internal/dbus/address_escape_test.go
@@ -0,0 +1,59 @@
+package dbus
+
+import (
+ "testing"
+)
+
+func TestUnescapeValue(t *testing.T) {
+ t.Parallel()
+
+ 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) {
+ t.Parallel()
+
+ 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)
+ }
+ })
+ }
+}
diff --git a/internal/dbus/address_test.go b/internal/dbus/address_test.go
new file mode 100644
index 00000000..7517aa1d
--- /dev/null
+++ b/internal/dbus/address_test.go
@@ -0,0 +1,123 @@
+package dbus_test
+
+import (
+ "errors"
+ "reflect"
+ "testing"
+
+ "hakurei.app/internal/dbus"
+)
+
+func TestParse(t *testing.T) {
+ t.Parallel()
+
+ 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) {
+ t.Parallel()
+
+ 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)
+ }
+ })
+ }
+}
diff --git a/internal/dbus/config.go b/internal/dbus/config.go
new file mode 100644
index 00000000..2a09ee90
--- /dev/null
+++ b/internal/dbus/config.go
@@ -0,0 +1,71 @@
+package dbus
+
+import (
+ "hakurei.app/hst"
+)
+
+// ProxyPair is an upstream dbus address and a downstream socket path.
+type ProxyPair [2]string
+
+// Args returns the xdg-dbus-proxy arguments equivalent of [hst.BusConfig].
+func Args(c *hst.BusConfig, bus ProxyPair) (args []string) {
+ argc := 2 + len(c.See) + len(c.Talk) + len(c.Own) + len(c.Call) + len(c.Broadcast)
+ if c.Log {
+ argc++
+ }
+ if c.Filter {
+ argc++
+ }
+
+ args = make([]string, 0, argc)
+ args = append(args, bus[0], bus[1])
+ if c.Filter {
+ args = append(args, "--filter")
+ }
+ for _, name := range c.See {
+ args = append(args, "--see="+name)
+ }
+ for _, name := range c.Talk {
+ args = append(args, "--talk="+name)
+ }
+ for _, name := range c.Own {
+ args = append(args, "--own="+name)
+ }
+ for name, rule := range c.Call {
+ args = append(args, "--call="+name+"="+rule)
+ }
+ for name, rule := range c.Broadcast {
+ args = append(args, "--broadcast="+name+"="+rule)
+ }
+ if c.Log {
+ args = append(args, "--log")
+ }
+
+ return
+}
+
+// NewConfig returns the address of a new [hst.BusConfig] with optional defaults.
+func NewConfig(id string, defaults, mpris bool) *hst.BusConfig {
+ c := hst.BusConfig{
+ Call: make(map[string]string),
+ Broadcast: make(map[string]string),
+
+ Filter: true,
+ }
+
+ if defaults {
+ c.Talk = []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}
+
+ c.Call["org.freedesktop.portal.*"] = "*"
+ c.Broadcast["org.freedesktop.portal.*"] = "@/org/freedesktop/portal/*"
+
+ if id != "" {
+ c.Own = []string{id + ".*"}
+ if mpris {
+ c.Own = append(c.Own, "org.mpris.MediaPlayer2."+id+".*")
+ }
+ }
+ }
+
+ return &c
+}
diff --git a/internal/dbus/config_test.go b/internal/dbus/config_test.go
new file mode 100644
index 00000000..8509007a
--- /dev/null
+++ b/internal/dbus/config_test.go
@@ -0,0 +1,124 @@
+package dbus_test
+
+import (
+ "reflect"
+ "slices"
+ "strings"
+ "testing"
+
+ "hakurei.app/hst"
+ "hakurei.app/internal/dbus"
+)
+
+func TestConfigArgs(t *testing.T) {
+ t.Parallel()
+
+ for _, tc := range testCasesExt {
+ if tc.wantErr {
+ // args does not check for nulls
+ continue
+ }
+
+ t.Run("build arguments for "+tc.id, func(t *testing.T) {
+ t.Parallel()
+
+ if got := dbus.Args(tc.c, tc.bus); !slices.Equal(got, tc.want) {
+ t.Errorf("Args: %v, want %v", got, tc.want)
+ }
+ })
+ }
+}
+
+func TestNewConfig(t *testing.T) {
+ t.Parallel()
+ ids := [...]string{"org.chromium.Chromium", "dev.vencord.Vesktop"}
+
+ type newTestCase struct {
+ id string
+ args [2]bool
+ want *hst.BusConfig
+ }
+
+ // populate tests from IDs in generic tests
+ tcs := make([]newTestCase, 0, (len(ids)+1)*4)
+ // tests for defaults without id
+ tcs = append(tcs,
+ newTestCase{"", [2]bool{false, false}, &hst.BusConfig{
+ Call: make(map[string]string),
+ Broadcast: make(map[string]string),
+ Filter: true,
+ }},
+ newTestCase{"", [2]bool{false, true}, &hst.BusConfig{
+ Call: make(map[string]string),
+ Broadcast: make(map[string]string),
+ Filter: true,
+ }},
+ newTestCase{"", [2]bool{true, false}, &hst.BusConfig{
+ Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"},
+ Call: map[string]string{"org.freedesktop.portal.*": "*"},
+ Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
+ Filter: true,
+ }},
+ newTestCase{"", [2]bool{true, true}, &hst.BusConfig{
+ Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"},
+ Call: map[string]string{"org.freedesktop.portal.*": "*"},
+ Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
+ Filter: true,
+ }},
+ )
+ for _, id := range ids {
+ tcs = append(tcs,
+ newTestCase{id, [2]bool{false, false}, &hst.BusConfig{
+ Call: make(map[string]string),
+ Broadcast: make(map[string]string),
+ Filter: true,
+ }},
+ newTestCase{id, [2]bool{false, true}, &hst.BusConfig{
+ Call: make(map[string]string),
+ Broadcast: make(map[string]string),
+ Filter: true,
+ }},
+ newTestCase{id, [2]bool{true, false}, &hst.BusConfig{
+ Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"},
+ Own: []string{id + ".*"},
+ Call: map[string]string{"org.freedesktop.portal.*": "*"},
+ Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
+ Filter: true,
+ }},
+ newTestCase{id, [2]bool{true, true}, &hst.BusConfig{
+ Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"},
+ Own: []string{id + ".*", "org.mpris.MediaPlayer2." + id + ".*"},
+ Call: map[string]string{"org.freedesktop.portal.*": "*"},
+ Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
+ Filter: true,
+ }},
+ )
+ }
+
+ for _, tc := range tcs {
+ name := new(strings.Builder)
+ name.WriteString("create new configuration struct")
+
+ if tc.args[0] {
+ name.WriteString(" with builtin defaults")
+ if tc.args[1] {
+ name.WriteString(" (mpris)")
+ }
+ }
+
+ if tc.id != "" {
+ name.WriteString(" for application ID ")
+ name.WriteString(tc.id)
+ }
+
+ t.Run(name.String(), func(t *testing.T) {
+ t.Parallel()
+
+ if gotC := dbus.NewConfig(tc.id, tc.args[0], tc.args[1]); !reflect.DeepEqual(gotC, tc.want) {
+ t.Errorf("NewConfig(%q, %t, %t) = %v, want %v",
+ tc.id, tc.args[0], tc.args[1],
+ gotC, tc.want)
+ }
+ })
+ }
+}
diff --git a/internal/dbus/dbus.go b/internal/dbus/dbus.go
new file mode 100644
index 00000000..9ea89f51
--- /dev/null
+++ b/internal/dbus/dbus.go
@@ -0,0 +1,69 @@
+// Package dbus wraps xdg-dbus-proxy and implements configuration and sandboxing of the underlying helper process.
+package dbus
+
+import (
+ "fmt"
+ "os"
+ "sync"
+)
+
+const (
+ /*
+ SessionBusAddress is the name of the environment variable where the address of the login session message bus is given in.
+
+ If that variable is not set, applications may also try to read the address from the X Window System root window property _DBUS_SESSION_BUS_ADDRESS.
+ The root window property must have type STRING. The environment variable should have precedence over the root window property.
+
+ The address of the login session message bus is given in the DBUS_SESSION_BUS_ADDRESS environment variable.
+ If DBUS_SESSION_BUS_ADDRESS is not set, or if it's set to the string "autolaunch:",
+ the system should use platform-specific methods of locating a running D-Bus session server,
+ or starting one if a running instance cannot be found.
+ Note that this mechanism is not recommended for attempting to determine if a daemon is running.
+ It is inherently racy to attempt to make this determination, since the bus daemon may be started just before or just after the determination is made.
+ Therefore, it is recommended that applications do not try to make this determination for their functionality purposes, and instead they should attempt to start the server.
+
+ This package diverges from the specification, as the caller is unlikely to be an X client, or be in a position to autolaunch a dbus server.
+ So a fallback address with a socket located in the well-known default XDG_RUNTIME_DIR formatting is used.
+ */
+ SessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
+
+ /*
+ SystemBusAddress is the name of the environment variable where the address of the system message bus is given in.
+
+ If that variable is not set, applications should try to connect to the well-known address unix:path=/var/run/dbus/system_bus_socket.
+ Implementations of the well-known system bus should listen on an address that will result in that connection being successful.
+ */
+ SystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
+
+ // FallbackSystemBusAddress is used when [SystemBusAddress] is not set.
+ FallbackSystemBusAddress = "unix:path=/var/run/dbus/system_bus_socket"
+)
+
+var (
+ address [2]string
+ addressOnce sync.Once
+)
+
+// Address returns the session and system bus addresses copied from environment,
+// or appropriate fallback values if they are not set.
+func Address() (session, system string) {
+ addressOnce.Do(func() {
+ // resolve upstream session bus address
+ if addr, ok := os.LookupEnv(SessionBusAddress); !ok {
+ // fall back to default format
+ address[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid())
+ } else {
+ address[0] = addr
+ }
+
+ // resolve upstream system bus address
+ if addr, ok := os.LookupEnv(SystemBusAddress); !ok {
+ // fall back to default hardcoded value
+ address[1] = FallbackSystemBusAddress
+ } else {
+ address[1] = addr
+ }
+ })
+
+ return address[0], address[1]
+}
diff --git a/internal/dbus/dbus_test.go b/internal/dbus/dbus_test.go
new file mode 100644
index 00000000..5d6e4c40
--- /dev/null
+++ b/internal/dbus/dbus_test.go
@@ -0,0 +1,170 @@
+package dbus_test
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "syscall"
+ "testing"
+ "time"
+
+ "hakurei.app/internal/dbus"
+ "hakurei.app/internal/helper"
+ "hakurei.app/message"
+)
+
+func TestFinalise(t *testing.T) {
+ if _, err := dbus.Finalise(dbus.ProxyPair{}, dbus.ProxyPair{}, nil, nil); !errors.Is(err, syscall.EBADE) {
+ t.Errorf("Finalise: error = %v, want %v",
+ err, syscall.EBADE)
+ }
+
+ for id, tc := range testCasePairs {
+ t.Run("create final for "+id, func(t *testing.T) {
+ var wt io.WriterTo
+ if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr {
+ t.Errorf("Finalise: error = %v, wantErr %v",
+ err, tc[0].wantErr)
+ return
+ } else {
+ wt = v
+ }
+
+ // rest of the tests happen for sealed instances
+ if tc[0].wantErr {
+ return
+ }
+
+ // build null-terminated string from wanted args
+ want := new(strings.Builder)
+ args := append(tc[0].want, tc[1].want...)
+ for _, arg := range args {
+ want.WriteString(arg)
+ want.WriteByte(0)
+ }
+
+ got := new(strings.Builder)
+ if _, err := wt.WriteTo(got); err != nil {
+ t.Errorf("WriteTo: error = %v", err)
+ }
+
+ if want.String() != got.String() {
+ t.Errorf("Seal: %q, want %q",
+ got.String(), want.String())
+ }
+ })
+ }
+}
+
+func TestProxyStartWaitCloseString(t *testing.T) {
+ t.Run("sandbox", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, true) })
+ t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) })
+}
+
+const (
+ stubProxyTimeout = 5 * time.Second
+)
+
+func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
+ {
+ oldWaitDelay := helper.WaitDelay
+ helper.WaitDelay = 16 * time.Second
+ t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
+ }
+
+ {
+ proxyName := dbus.ProxyName
+ dbus.ProxyName = os.Args[0]
+ t.Cleanup(func() { dbus.ProxyName = proxyName })
+ }
+
+ var p *dbus.Proxy
+
+ t.Run("string for nil proxy", func(t *testing.T) {
+ want := "(invalid dbus proxy)"
+ if got := p.String(); got != want {
+ t.Errorf("String: %q, want %q",
+ got, want)
+ }
+ })
+
+ t.Run("invalid start", func(t *testing.T) {
+ if !useSandbox {
+ p = dbus.NewDirect(t.Context(), message.New(nil), nil, nil)
+ } else {
+ p = dbus.New(t.Context(), message.New(nil), nil, nil)
+ }
+
+ if err := p.Start(); !errors.Is(err, syscall.ENOTRECOVERABLE) {
+ t.Errorf("Start: error = %q, wantErr %q", err, syscall.ENOTRECOVERABLE)
+ return
+ }
+ })
+
+ for id, tc := range testCasePairs {
+ // this test does not test errors
+ if tc[0].wantErr {
+ continue
+ }
+
+ t.Run("proxy for "+id, func(t *testing.T) {
+ var final *dbus.Final
+ t.Run("finalise", func(t *testing.T) {
+ if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); err != nil {
+ t.Errorf("Finalise: error = %v, wantErr %v", err, tc[0].wantErr)
+ return
+ } else {
+ final = v
+ }
+ })
+
+ ctx, cancel := context.WithTimeout(t.Context(), stubProxyTimeout)
+ defer cancel()
+ output := new(strings.Builder)
+ if !useSandbox {
+ p = dbus.NewDirect(ctx, message.New(nil), final, output)
+ } else {
+ p = dbus.New(ctx, message.New(nil), final, output)
+ }
+
+ { // check invalid wait behaviour
+ wantErr := "dbus: not started"
+ if err := p.Wait(); err == nil || err.Error() != wantErr {
+ t.Errorf("Wait: error = %v, wantErr %v", err, wantErr)
+ }
+ }
+
+ { // check string behaviour
+ want := "(unused dbus proxy)"
+ if got := p.String(); got != want {
+ t.Errorf("String: %q, want %q", got, want)
+ return
+ }
+ }
+
+ if err := p.Start(); err != nil {
+ t.Fatalf("Start: error = %v", err)
+ }
+
+ { // check running string behaviour
+ wantSubstr := fmt.Sprintf("%s --args=3 --fd=4", os.Args[0])
+ if useSandbox {
+ wantSubstr = `argv: ["xdg-dbus-proxy" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf`
+ }
+ if got := p.String(); !strings.Contains(got, wantSubstr) {
+ t.Errorf("String: %q, want %q",
+ got, wantSubstr)
+ return
+ }
+ }
+
+ p.Close()
+ if err := p.Wait(); err != nil {
+ t.Errorf("Wait: error = %v\noutput: %s", err, output.String())
+ }
+ })
+ }
+}
diff --git a/internal/dbus/export_test.go b/internal/dbus/export_test.go
new file mode 100644
index 00000000..bc24f3f3
--- /dev/null
+++ b/internal/dbus/export_test.go
@@ -0,0 +1,15 @@
+package dbus
+
+import (
+ "context"
+ "io"
+
+ "hakurei.app/message"
+)
+
+// NewDirect returns a new instance of [Proxy] with its sandbox disabled.
+func NewDirect(ctx context.Context, msg message.Msg, final *Final, output io.Writer) *Proxy {
+ p := New(ctx, msg, final, output)
+ p.useSandbox = false
+ return p
+}
diff --git a/internal/dbus/proc.go b/internal/dbus/proc.go
new file mode 100644
index 00000000..c934e89e
--- /dev/null
+++ b/internal/dbus/proc.go
@@ -0,0 +1,188 @@
+package dbus
+
+import (
+ "context"
+ "errors"
+ "os"
+ "os/exec"
+ "strconv"
+ "syscall"
+
+ "hakurei.app/container"
+ "hakurei.app/container/check"
+ "hakurei.app/container/seccomp"
+ "hakurei.app/container/std"
+ "hakurei.app/internal/helper"
+ "hakurei.app/ldd"
+)
+
+// Start starts and configures a D-Bus proxy process.
+func (p *Proxy) Start() error {
+ if p.final == nil || p.final.WriterTo == nil {
+ return syscall.ENOTRECOVERABLE
+ }
+
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ p.pmu.Lock()
+ defer p.pmu.Unlock()
+
+ if p.cancel != nil || p.cause != nil {
+ return errors.New("dbus: already started")
+ }
+
+ ctx, cancel := context.WithCancelCause(p.ctx)
+
+ if !p.useSandbox {
+ p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) {
+ if p.output != nil {
+ cmd.Stdout, cmd.Stderr = p.output, p.output
+ }
+ cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+ cmd.Env = make([]string, 0)
+ }, nil)
+ } else {
+ var toolPath *check.Absolute
+ if a, err := check.NewAbs(p.name); err != nil {
+ if p.name, err = exec.LookPath(p.name); err != nil {
+ return err
+ } else if toolPath, err = check.NewAbs(p.name); err != nil {
+ return err
+ }
+ } else {
+ toolPath = a
+ }
+
+ var libPaths []*check.Absolute
+ if entries, err := ldd.Resolve(ctx, p.msg, toolPath); err != nil {
+ return err
+ } else {
+ libPaths = ldd.Path(entries)
+ }
+
+ p.helper = helper.New(
+ ctx, p.msg, toolPath, "xdg-dbus-proxy",
+ p.final, true,
+ argF, func(z *container.Container) {
+ z.SeccompFlags |= seccomp.AllowMultiarch
+ z.SeccompPresets |= std.PresetStrict
+ z.Hostname = "hakurei-dbus"
+ if p.output != nil {
+ z.Stdout, z.Stderr = p.output, p.output
+ }
+
+ // these lib paths are unpredictable, so mount them first so they cannot cover anything
+ for _, name := range libPaths {
+ z.Bind(name, name, 0)
+ }
+
+ // upstream bus directories
+ upstreamPaths := make([]*check.Absolute, 0, 2)
+ for _, addr := range [][]AddrEntry{p.final.SessionUpstream, p.final.SystemUpstream} {
+ for _, ent := range addr {
+ if ent.Method != "unix" {
+ continue
+ }
+ for _, pair := range ent.Values {
+ if pair[0] != "path" {
+ continue
+ }
+ if a, err := check.NewAbs(pair[1]); err != nil {
+ continue
+ } else {
+ upstreamPaths = append(upstreamPaths, a.Dir())
+ }
+ }
+ }
+ }
+ check.SortAbs(upstreamPaths)
+ upstreamPaths = check.CompactAbs(upstreamPaths)
+ for _, name := range upstreamPaths {
+ z.Bind(name, name, 0)
+ }
+ z.HostNet = len(upstreamPaths) == 0
+ z.HostAbstract = z.HostNet
+
+ // parent directories of bind paths
+ sockDirPaths := make([]*check.Absolute, 0, 2)
+ if a, err := check.NewAbs(p.final.Session[1]); err == nil {
+ sockDirPaths = append(sockDirPaths, a.Dir())
+ }
+ if a, err := check.NewAbs(p.final.System[1]); err == nil {
+ sockDirPaths = append(sockDirPaths, a.Dir())
+ }
+ check.SortAbs(sockDirPaths)
+ sockDirPaths = check.CompactAbs(sockDirPaths)
+ for _, name := range sockDirPaths {
+ z.Bind(name, name, std.BindWritable)
+ }
+
+ // xdg-dbus-proxy bin path
+ binPath := toolPath.Dir()
+ z.Bind(binPath, binPath, 0)
+ }, nil)
+ }
+
+ if err := p.helper.Start(); err != nil {
+ cancel(err)
+ p.helper = nil
+ return err
+ }
+
+ p.cancel, p.cause = cancel, func() error { return context.Cause(ctx) }
+ return nil
+}
+
+var proxyClosed = errors.New("proxy closed")
+
+// Wait blocks until xdg-dbus-proxy exits and releases resources.
+func (p *Proxy) Wait() error {
+ p.mu.RLock()
+ defer p.mu.RUnlock()
+
+ p.pmu.RLock()
+ if p.helper == nil || p.cancel == nil || p.cause == nil {
+ p.pmu.RUnlock()
+ return errors.New("dbus: not started")
+ }
+
+ var errs [3]error
+
+ errs[0] = p.helper.Wait()
+ if errors.Is(errs[0], context.Canceled) &&
+ errors.Is(p.cause(), proxyClosed) {
+ errs[0] = nil
+ }
+ p.pmu.RUnlock()
+
+ // ensure socket removal so ephemeral directory is empty at revert
+ if err := os.Remove(p.final.Session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
+ errs[1] = err
+ }
+ if p.final.System[1] != "" {
+ if err := os.Remove(p.final.System[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
+ errs[2] = err
+ }
+ }
+
+ return errors.Join(errs[:]...)
+}
+
+// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy.
+func (p *Proxy) Close() {
+ p.pmu.Lock()
+ defer p.pmu.Unlock()
+
+ if p.cancel == nil {
+ panic("dbus: not started")
+ }
+ p.cancel(proxyClosed)
+}
+
+func argF(argsFd, statFd int) []string {
+ if statFd == -1 {
+ return []string{"--args=" + strconv.Itoa(argsFd)}
+ } else {
+ return []string{"--args=" + strconv.Itoa(argsFd), "--fd=" + strconv.Itoa(statFd)}
+ }
+}
diff --git a/internal/dbus/proc_test.go b/internal/dbus/proc_test.go
new file mode 100644
index 00000000..544856ed
--- /dev/null
+++ b/internal/dbus/proc_test.go
@@ -0,0 +1,11 @@
+package dbus_test
+
+import (
+ "os"
+ "testing"
+
+ "hakurei.app/container"
+ "hakurei.app/internal/helper"
+)
+
+func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) }
diff --git a/internal/dbus/proxy.go b/internal/dbus/proxy.go
new file mode 100644
index 00000000..9de49c41
--- /dev/null
+++ b/internal/dbus/proxy.go
@@ -0,0 +1,105 @@
+package dbus
+
+import (
+ "context"
+ "io"
+ "sync"
+ "syscall"
+
+ "hakurei.app/hst"
+ "hakurei.app/internal/helper"
+ "hakurei.app/message"
+)
+
+// ProxyName is the file name or path to the proxy program.
+// Overriding ProxyName will only affect Proxy instance created after the change.
+var ProxyName = "xdg-dbus-proxy"
+
+// Proxy holds the state of a xdg-dbus-proxy process, and should never be copied.
+type Proxy struct {
+ helper helper.Helper
+ ctx context.Context
+ msg message.Msg
+
+ cancel context.CancelCauseFunc
+ cause func() error
+
+ final *Final
+ output io.Writer
+ useSandbox bool
+
+ name string
+
+ mu, pmu sync.RWMutex
+}
+
+func (p *Proxy) String() string {
+ if p == nil {
+ return "(invalid dbus proxy)"
+ }
+
+ p.mu.RLock()
+ defer p.mu.RUnlock()
+
+ if p.helper != nil {
+ return p.helper.String()
+ }
+
+ return "(unused dbus proxy)"
+}
+
+// Final describes the outcome of a proxy configuration.
+type Final struct {
+ Session, System ProxyPair
+ // parsed upstream address
+ SessionUpstream, SystemUpstream []AddrEntry
+ io.WriterTo
+}
+
+// Finalise creates a checked argument writer for [Proxy].
+func Finalise(sessionBus, systemBus ProxyPair, session, system *hst.BusConfig) (final *Final, err error) {
+ if session == nil && system == nil {
+ return nil, syscall.EBADE
+ }
+
+ var args []string
+ if session != nil {
+ if err = session.CheckInterfaces("session"); err != nil {
+ return
+ }
+ args = append(args, Args(session, sessionBus)...)
+ }
+ if system != nil {
+ if err = system.CheckInterfaces("system"); err != nil {
+ return
+ }
+ args = append(args, Args(system, systemBus)...)
+ }
+
+ final = &Final{Session: sessionBus, System: systemBus}
+
+ final.WriterTo, err = helper.NewCheckedArgs(args...)
+ if err != nil {
+ return
+ }
+
+ if session != nil {
+ final.SessionUpstream, err = Parse([]byte(final.Session[0]))
+ if err != nil {
+ return
+ }
+ }
+ if system != nil {
+ final.SystemUpstream, err = Parse([]byte(final.System[0]))
+ if err != nil {
+ return
+ }
+ }
+
+ return
+}
+
+// New returns a new instance of [Proxy].
+func New(ctx context.Context, msg message.Msg, final *Final, output io.Writer) *Proxy {
+ return &Proxy{name: ProxyName, ctx: ctx, msg: msg, final: final, output: output, useSandbox: true}
+}
diff --git a/internal/dbus/samples_test.go b/internal/dbus/samples_test.go
new file mode 100644
index 00000000..7db3d6b9
--- /dev/null
+++ b/internal/dbus/samples_test.go
@@ -0,0 +1,214 @@
+package dbus_test
+
+import (
+ "hakurei.app/hst"
+)
+
+const (
+ sampleHostPath = "/tmp/bus"
+ sampleHostAddr = "unix:path=" + sampleHostPath
+ sampleBindPath = "/tmp/proxied_bus"
+)
+
+var samples = []dbusTestCase{
+ {
+ "org.chromium.Chromium", &hst.BusConfig{
+ See: nil,
+ Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver",
+ "org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"},
+ Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
+ "org.mpris.MediaPlayer2.chromium.*"},
+ Call: map[string]string{"org.freedesktop.portal.*": "*"},
+ Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
+ Log: false,
+ Filter: true,
+ }, false, false,
+ [2]string{sampleHostAddr, sampleBindPath},
+ []string{
+ sampleHostAddr,
+ sampleBindPath,
+ "--filter",
+ "--talk=org.freedesktop.Notifications",
+ "--talk=org.freedesktop.FileManager1",
+ "--talk=org.freedesktop.ScreenSaver",
+ "--talk=org.freedesktop.secrets",
+ "--talk=org.kde.kwalletd5",
+ "--talk=org.kde.kwalletd6",
+ "--talk=org.gnome.SessionManager",
+ "--own=org.chromium.Chromium.*",
+ "--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
+ "--own=org.mpris.MediaPlayer2.chromium.*",
+ "--call=org.freedesktop.portal.*=*",
+ "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
+ },
+ },
+ {
+ "org.chromium.Chromium+", &hst.BusConfig{
+ See: nil,
+ Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
+ Own: nil,
+ Call: nil,
+ Broadcast: nil,
+ Log: false,
+ Filter: true,
+ }, false, false,
+ [2]string{sampleHostAddr, sampleBindPath},
+ []string{
+ sampleHostAddr,
+ sampleBindPath,
+ "--filter",
+ "--talk=org.bluez",
+ "--talk=org.freedesktop.Avahi",
+ "--talk=org.freedesktop.UPower",
+ },
+ },
+
+ {
+ "dev.vencord.Vesktop", &hst.BusConfig{
+ See: nil,
+ Talk: []string{"org.freedesktop.Notifications", "org.kde.StatusNotifierWatcher"},
+ Own: []string{"dev.vencord.Vesktop.*", "org.mpris.MediaPlayer2.dev.vencord.Vesktop.*"},
+ Call: map[string]string{"org.freedesktop.portal.*": "*"},
+ Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
+ Log: false,
+ Filter: true,
+ }, false, false,
+ [2]string{sampleHostAddr, sampleBindPath},
+ []string{
+ sampleHostAddr,
+ sampleBindPath,
+ "--filter",
+ "--talk=org.freedesktop.Notifications",
+ "--talk=org.kde.StatusNotifierWatcher",
+ "--own=dev.vencord.Vesktop.*",
+ "--own=org.mpris.MediaPlayer2.dev.vencord.Vesktop.*",
+ "--call=org.freedesktop.portal.*=*",
+ "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*"},
+ },
+
+ {
+ "uk.gensokyo.CrashTestDummy", &hst.BusConfig{
+ See: []string{"uk.gensokyo.CrashTestDummy1"},
+ Talk: []string{"org.freedesktop.Notifications"},
+ Own: []string{"uk.gensokyo.CrashTestDummy.*", "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*"},
+ Call: map[string]string{"org.freedesktop.portal.*": "*"},
+ Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
+ Log: true,
+ Filter: true,
+ }, false, false,
+ [2]string{sampleHostAddr, sampleBindPath},
+ []string{
+ sampleHostAddr,
+ sampleBindPath,
+ "--filter",
+ "--see=uk.gensokyo.CrashTestDummy1",
+ "--talk=org.freedesktop.Notifications",
+ "--own=uk.gensokyo.CrashTestDummy.*",
+ "--own=org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*",
+ "--call=org.freedesktop.portal.*=*",
+ "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
+ "--log"},
+ },
+ {
+ "uk.gensokyo.CrashTestDummy1", &hst.BusConfig{
+ See: []string{"uk.gensokyo.CrashTestDummy"},
+ Talk: []string{"org.freedesktop.Notifications"},
+ Own: []string{"uk.gensokyo.CrashTestDummy1.*", "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy1.*"},
+ Call: map[string]string{"org.freedesktop.portal.*": "*"},
+ Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
+ Log: true,
+ Filter: true,
+ }, false, true,
+ [2]string{sampleHostAddr, sampleBindPath},
+ []string{
+ sampleHostAddr,
+ sampleBindPath,
+ "--filter",
+ "--see=uk.gensokyo.CrashTestDummy",
+ "--talk=org.freedesktop.Notifications",
+ "--own=uk.gensokyo.CrashTestDummy1.*",
+ "--own=org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy1.*",
+ "--call=org.freedesktop.portal.*=*",
+ "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
+ "--log"},
+ },
+}
+
+type dbusTestCase struct {
+ id string
+ c *hst.BusConfig
+ wantErr bool
+ wantErrF bool
+ bus [2]string
+ want []string
+}
+
+var (
+ testCasesExt = func() []dbusTestCase {
+ testCases := make([]dbusTestCase, len(samples)*2)
+ for i := range samples {
+ testCases[i] = samples[i]
+
+ fi := &testCases[len(samples)+i]
+ *fi = samples[i]
+
+ // create null-injected test cases
+ fi.wantErr = true
+ injectNulls := func(t *[]string) {
+ f := make([]string, len(*t))
+ for i := range f {
+ f[i] = "\x00" + (*t)[i] + "\x00"
+ }
+ *t = f
+ }
+
+ fi.c = new(hst.BusConfig)
+ *fi.c = *samples[i].c
+ injectNulls(&fi.c.See)
+ injectNulls(&fi.c.Talk)
+ injectNulls(&fi.c.Own)
+ }
+ return testCases
+ }()
+
+ testCasePairs = func() map[string][2]dbusTestCase {
+ // enumerate test case pairs
+ var pc int
+ for _, tc := range samples {
+ if tc.id != "" {
+ pc++
+ }
+ }
+ pairs := make(map[string][2]dbusTestCase, pc)
+ for i, tc := range testCasesExt {
+ if tc.id == "" {
+ continue
+ }
+
+ // skip already enumerated system bus test
+ if tc.id[len(tc.id)-1] == '+' {
+ continue
+ }
+
+ ftp := [2]dbusTestCase{tc}
+
+ // system proxy tests always place directly after its user counterpart with id ending in +
+ if i+1 < len(testCasesExt) && testCasesExt[i+1].id[len(testCasesExt[i+1].id)-1] == '+' {
+ // attach system bus config
+ ftp[1] = testCasesExt[i+1]
+
+ // check for misplaced/mismatching tests
+ if ftp[0].wantErr != ftp[1].wantErr || ftp[0].id+"+" != ftp[1].id {
+ panic("mismatching session/system pairing")
+ }
+ }
+
+ k := tc.id
+ if tc.wantErr {
+ k = "malformed_" + k
+ }
+ pairs[k] = ftp
+ }
+ return pairs
+ }()
+)
diff --git a/internal/dbus/testdata/dev.vencord.Vesktop.json b/internal/dbus/testdata/dev.vencord.Vesktop.json
new file mode 100644
index 00000000..05259774
--- /dev/null
+++ b/internal/dbus/testdata/dev.vencord.Vesktop.json
@@ -0,0 +1,18 @@
+{
+ "talk":[
+ "org.freedesktop.Notifications",
+ "org.kde.StatusNotifierWatcher"
+ ],
+ "own":[
+ "dev.vencord.Vesktop.*",
+ "org.mpris.MediaPlayer2.dev.vencord.Vesktop.*"
+ ],
+ "call":{
+ "org.freedesktop.portal.*":"*"
+ },
+ "broadcast":{
+ "org.freedesktop.portal.*":"@/org/freedesktop/portal/*"
+ },
+
+ "filter":true
+}
diff --git a/internal/dbus/testdata/org.chromium.Chromium+.json b/internal/dbus/testdata/org.chromium.Chromium+.json
new file mode 100644
index 00000000..0a27d719
--- /dev/null
+++ b/internal/dbus/testdata/org.chromium.Chromium+.json
@@ -0,0 +1,9 @@
+{
+ "talk":[
+ "org.bluez",
+ "org.freedesktop.Avahi",
+ "org.freedesktop.UPower"
+ ],
+
+ "filter":true
+} \ No newline at end of file
diff --git a/internal/dbus/testdata/org.chromium.Chromium.json b/internal/dbus/testdata/org.chromium.Chromium.json
new file mode 100644
index 00000000..be027f39
--- /dev/null
+++ b/internal/dbus/testdata/org.chromium.Chromium.json
@@ -0,0 +1,24 @@
+{
+ "talk":[
+ "org.freedesktop.Notifications",
+ "org.freedesktop.FileManager1",
+ "org.freedesktop.ScreenSaver",
+ "org.freedesktop.secrets",
+ "org.kde.kwalletd5",
+ "org.kde.kwalletd6",
+ "org.gnome.SessionManager"
+ ],
+ "own":[
+ "org.chromium.Chromium.*",
+ "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
+ "org.mpris.MediaPlayer2.chromium.*"
+ ],
+ "call":{
+ "org.freedesktop.portal.*":"*"
+ },
+ "broadcast":{
+ "org.freedesktop.portal.*":"@/org/freedesktop/portal/*"
+ },
+
+ "filter":true
+}
diff --git a/internal/dbus/testdata/uk.gensokyo.CrashTestDummy.json b/internal/dbus/testdata/uk.gensokyo.CrashTestDummy.json
new file mode 100644
index 00000000..43272f67
--- /dev/null
+++ b/internal/dbus/testdata/uk.gensokyo.CrashTestDummy.json
@@ -0,0 +1,21 @@
+{
+ "see": [
+ "uk.gensokyo.CrashTestDummy1"
+ ],
+ "talk":[
+ "org.freedesktop.Notifications"
+ ],
+ "own":[
+ "uk.gensokyo.CrashTestDummy.*",
+ "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*"
+ ],
+ "call":{
+ "org.freedesktop.portal.*":"*"
+ },
+ "broadcast":{
+ "org.freedesktop.portal.*":"@/org/freedesktop/portal/*"
+ },
+
+ "log": true,
+ "filter":true
+}