hst/dbus: move dbus config struct
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m12s
Test / Hpkg (push) Successful in 4m0s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Sandbox (race detector) (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m31s

This allows holding a xdg-dbus-proxy configuration without importing system/dbus.

It also makes more sense in the project structure since the config struct is part of the hst API however the rest of the implementation is not.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-10-07 19:01:18 +09:00
parent 3ce63e95d7
commit d23b4dc9e6
17 changed files with 185 additions and 251 deletions

View File

@@ -1,64 +1,50 @@
package dbus
import (
"encoding/json"
"errors"
"io"
"os"
"strings"
"hakurei.app/hst"
)
// ProxyPair is an upstream dbus address and a downstream socket path.
type ProxyPair [2]string
type Config struct {
// See set 'see' policy for NAME (--see=NAME)
See []string `json:"see"`
// Talk set 'talk' policy for NAME (--talk=NAME)
Talk []string `json:"talk"`
// Own set 'own' policy for NAME (--own=NAME)
Own []string `json:"own"`
// Call set RULE for calls on NAME (--call=NAME=RULE)
Call map[string]string `json:"call"`
// Broadcast set RULE for broadcasts from NAME (--broadcast=NAME=RULE)
Broadcast map[string]string `json:"broadcast"`
Log bool `json:"log,omitempty"`
Filter bool `json:"filter"`
}
func (c *Config) interfaces(yield func(string) bool) {
for _, iface := range c.See {
if !yield(iface) {
return
// interfacesAll returns an iterator over all interfaces specified in c.
func interfacesAll(c *hst.BusConfig) func(yield func(string) bool) {
return func(yield func(string) bool) {
for _, iface := range c.See {
if !yield(iface) {
return
}
}
}
for _, iface := range c.Talk {
if !yield(iface) {
return
for _, iface := range c.Talk {
if !yield(iface) {
return
}
}
}
for _, iface := range c.Own {
if !yield(iface) {
return
for _, iface := range c.Own {
if !yield(iface) {
return
}
}
}
for iface := range c.Call {
if !yield(iface) {
return
for iface := range c.Call {
if !yield(iface) {
return
}
}
}
for iface := range c.Broadcast {
if !yield(iface) {
return
for iface := range c.Broadcast {
if !yield(iface) {
return
}
}
}
}
func (c *Config) checkInterfaces(segment string) error {
for iface := range c.interfaces {
// checkInterfaces checks [hst.BusConfig] for invalid interfaces based on an undocumented check in xdg-dbus-error,
// returning [BadInterfaceError] if one is encountered.
func checkInterfaces(c *hst.BusConfig, segment string) error {
for iface := range interfacesAll(c) {
/*
xdg-dbus-proxy fails without output when this condition is not met:
char *dot = strrchr (filter->interface, '.');
@@ -83,7 +69,8 @@ func (c *Config) checkInterfaces(segment string) error {
return nil
}
func (c *Config) Args(bus ProxyPair) (args []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++
@@ -119,25 +106,9 @@ func (c *Config) Args(bus ProxyPair) (args []string) {
return
}
func (c *Config) Load(r io.Reader) error { return json.NewDecoder(r).Decode(&c) }
// NewConfigFromFile opens the target config file at path and parses its contents into *Config.
func NewConfigFromFile(path string) (*Config, error) {
if f, err := os.Open(path); err != nil {
return nil, err
} else {
c := new(Config)
err1 := c.Load(f)
err = f.Close()
return c, errors.Join(err1, err)
}
}
// NewConfig returns a reference to a Config struct with optional defaults.
// If id is an empty string own defaults are omitted.
func NewConfig(id string, defaults, mpris bool) (c *Config) {
c = &Config{
// 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),
@@ -158,5 +129,5 @@ func NewConfig(id string, defaults, mpris bool) (c *Config) {
}
}
return
return &c
}

View File

@@ -1,26 +1,24 @@
package dbus_test
import (
"errors"
"os"
"path"
"reflect"
"slices"
"strings"
"testing"
"hakurei.app/hst"
"hakurei.app/system/dbus"
)
func TestConfig_Args(t *testing.T) {
for _, tc := range makeTestCases() {
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) {
if got := tc.c.Args(tc.bus); !slices.Equal(got, tc.want) {
if got := dbus.Args(tc.c, tc.bus); !slices.Equal(got, tc.want) {
t.Errorf("Args(%q) = %v, want %v",
tc.bus,
got, tc.want)
@@ -29,74 +27,36 @@ func TestConfig_Args(t *testing.T) {
}
}
func TestNewConfigFromFile(t *testing.T) {
for _, tc := range makeTestCases() {
name := new(strings.Builder)
name.WriteString("parse configuration file for application ")
name.WriteString(tc.id)
if tc.wantErr {
name.WriteString(" with unexpected results")
}
samplePath := path.Join("testdata", tc.id+".json")
t.Run(name.String(), func(t *testing.T) {
got, err := dbus.NewConfigFromFile(samplePath)
if errors.Is(err, os.ErrNotExist) != tc.wantErrF {
t.Errorf("NewConfigFromFile(%q) error = %v, wantErrF %v",
samplePath,
err, tc.wantErrF)
return
}
if tc.wantErrF {
return
}
if !tc.wantErr && !reflect.DeepEqual(got, tc.c) {
t.Errorf("NewConfigFromFile(%q) got = %v, want %v",
samplePath,
got, tc.c)
}
if tc.wantErr && reflect.DeepEqual(got, tc.c) {
t.Errorf("NewConfigFromFile(%q) got = %v, wantErr %v",
samplePath,
got, tc.wantErr)
}
})
}
}
func TestNewConfig(t *testing.T) {
ids := [...]string{"org.chromium.Chromium", "dev.vencord.Vesktop"}
type newTestCase struct {
id string
args [2]bool
want *dbus.Config
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}, &dbus.Config{
newTestCase{"", [2]bool{false, false}, &hst.BusConfig{
Call: make(map[string]string),
Broadcast: make(map[string]string),
Filter: true,
}},
newTestCase{"", [2]bool{false, true}, &dbus.Config{
newTestCase{"", [2]bool{false, true}, &hst.BusConfig{
Call: make(map[string]string),
Broadcast: make(map[string]string),
Filter: true,
}},
newTestCase{"", [2]bool{true, false}, &dbus.Config{
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}, &dbus.Config{
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/*"},
@@ -105,24 +65,24 @@ func TestNewConfig(t *testing.T) {
)
for _, id := range ids {
tcs = append(tcs,
newTestCase{id, [2]bool{false, false}, &dbus.Config{
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}, &dbus.Config{
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}, &dbus.Config{
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}, &dbus.Config{
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.*": "*"},

View File

@@ -22,7 +22,7 @@ func TestFinalise(t *testing.T) {
err, syscall.EBADE)
}
for id, tc := range testCasePairs() {
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 {
@@ -105,7 +105,7 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
}
})
for id, tc := range testCasePairs() {
for id, tc := range testCasePairs {
// this test does not test errors
if tc[0].wantErr {
continue

View File

@@ -9,6 +9,7 @@ import (
"hakurei.app/container"
"hakurei.app/helper"
"hakurei.app/hst"
)
// ProxyName is the file name or path to the proxy program.
@@ -66,23 +67,23 @@ type Final struct {
}
// Finalise creates a checked argument writer for [Proxy].
func Finalise(sessionBus, systemBus ProxyPair, session, system *Config) (final *Final, err error) {
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 {
if err = checkInterfaces(session, "session"); err != nil {
return
}
args = append(args, session.Args(sessionBus)...)
args = append(args, Args(session, sessionBus)...)
}
if system != nil {
if err = system.checkInterfaces("system"); err != nil {
if err = checkInterfaces(system, "system"); err != nil {
return
}
args = append(args, system.Args(systemBus)...)
args = append(args, Args(system, systemBus)...)
}
final = &Final{Session: sessionBus, System: systemBus}

View File

@@ -1,9 +1,7 @@
package dbus_test
import (
"sync"
"hakurei.app/system/dbus"
"hakurei.app/hst"
)
const (
@@ -14,7 +12,7 @@ const (
var samples = []dbusTestCase{
{
"org.chromium.Chromium", &dbus.Config{
"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"},
@@ -45,7 +43,7 @@ var samples = []dbusTestCase{
},
},
{
"org.chromium.Chromium+", &dbus.Config{
"org.chromium.Chromium+", &hst.BusConfig{
See: nil,
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Own: nil,
@@ -66,7 +64,7 @@ var samples = []dbusTestCase{
},
{
"dev.vencord.Vesktop", &dbus.Config{
"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.*"},
@@ -89,7 +87,7 @@ var samples = []dbusTestCase{
},
{
"uk.gensokyo.CrashTestDummy", &dbus.Config{
"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.*"},
@@ -112,7 +110,7 @@ var samples = []dbusTestCase{
"--log"},
},
{
"uk.gensokyo.CrashTestDummy1", &dbus.Config{
"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.*"},
@@ -138,7 +136,7 @@ var samples = []dbusTestCase{
type dbusTestCase struct {
id string
c *dbus.Config
c *hst.BusConfig
wantErr bool
wantErrF bool
bus [2]string
@@ -146,83 +144,71 @@ type dbusTestCase struct {
}
var (
testCasesV []dbusTestCase
testCasePairsV map[string][2]dbusTestCase
testCasesExt = func() []dbusTestCase {
testCases := make([]dbusTestCase, len(samples)*2)
for i := range samples {
testCases[i] = samples[i]
testCaseOnce sync.Once
)
fi := &testCases[len(samples)+i]
*fi = samples[i]
func makeTestCases() []dbusTestCase {
testCaseOnce.Do(testCaseGenerate)
return testCasesV
}
// 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
}
func testCasePairs() map[string][2]dbusTestCase {
testCaseOnce.Do(testCaseGenerate)
return testCasePairsV
}
func injectNulls(t *[]string) {
f := make([]string, len(*t))
for i := range f {
f[i] = "\x00" + (*t)[i] + "\x00"
}
*t = f
}
func testCaseGenerate() {
// create null-injected test cases
testCasesV = make([]dbusTestCase, len(samples)*2)
for i := range samples {
testCasesV[i] = samples[i]
testCasesV[len(samples)+i] = samples[i]
testCasesV[len(samples)+i].c = new(dbus.Config)
*testCasesV[len(samples)+i].c = *samples[i].c
// inject nulls
fi := &testCasesV[len(samples)+i]
fi.wantErr = true
injectNulls(&fi.c.See)
injectNulls(&fi.c.Talk)
injectNulls(&fi.c.Own)
}
// enumerate test case pairs
var pc int
for _, tc := range samples {
if tc.id != "" {
pc++
}
}
testCasePairsV = make(map[string][2]dbusTestCase, pc)
for i, tc := range testCasesV {
if tc.id == "" {
continue
fi.c = new(hst.BusConfig)
*fi.c = *samples[i].c
injectNulls(&fi.c.See)
injectNulls(&fi.c.Talk)
injectNulls(&fi.c.Own)
}
return testCases
}()
// 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(testCasesV) && testCasesV[i+1].id[len(testCasesV[i+1].id)-1] == '+' {
// attach system bus config
ftp[1] = testCasesV[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")
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
}
k := tc.id
if tc.wantErr {
k = "malformed_" + k
// 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
}
testCasePairsV[k] = ftp
}
}
return pairs
}()
)