internal: relocate packages
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m32s

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-11-15 13:47:09 +09:00
parent 16e674782a
commit a91920310d
63 changed files with 61 additions and 61 deletions

193
internal/dbus/address.go Normal file
View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -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)
}
})
}
}

71
internal/dbus/config.go Normal file
View File

@@ -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
}

View File

@@ -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)
}
})
}
}

69
internal/dbus/dbus.go Normal file
View File

@@ -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]
}

170
internal/dbus/dbus_test.go Normal file
View File

@@ -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())
}
})
}
}

View File

@@ -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
}

188
internal/dbus/proc.go Normal file
View File

@@ -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)}
}
}

View File

@@ -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()) }

105
internal/dbus/proxy.go Normal file
View File

@@ -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}
}

View File

@@ -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
}()
)

View File

@@ -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
}

View File

@@ -0,0 +1,9 @@
{
"talk":[
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"filter":true
}

View File

@@ -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
}

View File

@@ -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
}