diff --git a/internal/system/dispatcher.go b/internal/system/dispatcher.go index 1641c3e..eae9247 100644 --- a/internal/system/dispatcher.go +++ b/internal/system/dispatcher.go @@ -10,6 +10,7 @@ import ( "hakurei.app/hst" "hakurei.app/internal/acl" "hakurei.app/internal/dbus" + "hakurei.app/internal/pipewire" "hakurei.app/internal/wayland" "hakurei.app/internal/xcb" ) @@ -47,8 +48,12 @@ type syscallDispatcher interface { // aclUpdate provides [acl.Update]. aclUpdate(name string, uid int, perms ...acl.Perm) error + // waylandNew provides [wayland.New]. waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (io.Closer, error) + // pipewireConnect provides [pipewire.Connect]. + pipewireConnect() (*pipewire.Context, error) + // xcbChangeHosts provides [xcb.ChangeHosts]. xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error @@ -84,6 +89,8 @@ func (k direct) waylandNew(displayPath, bindPath *check.Absolute, appID, instanc return wayland.New(displayPath, bindPath, appID, instanceID) } +func (k direct) pipewireConnect() (*pipewire.Context, error) { return pipewire.Connect(true) } + func (k direct) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error { return xcb.ChangeHosts(mode, family, address) } diff --git a/internal/system/dispatcher_test.go b/internal/system/dispatcher_test.go index ed16217..f69422c 100644 --- a/internal/system/dispatcher_test.go +++ b/internal/system/dispatcher_test.go @@ -14,6 +14,7 @@ import ( "hakurei.app/hst" "hakurei.app/internal/acl" "hakurei.app/internal/dbus" + "hakurei.app/internal/pipewire" "hakurei.app/internal/xcb" ) @@ -245,8 +246,16 @@ func (k *kstub) mkdir(name string, perm os.FileMode) error { func (k *kstub) chmod(name string, mode os.FileMode) error { k.Helper() - return k.Expects("chmod").Error( - stub.CheckArg(k.Stub, "name", name, 0), + expect := k.Expects("chmod") + + // translate ignored name + nameVal := any(name) + if _, ok := expect.Args[0].(ignoreValue); ok { + nameVal = ignoreValue{} + } + + return expect.Error( + stub.CheckArgReflect(k.Stub, "name", nameVal, 0), stub.CheckArg(k.Stub, "mode", mode, 1)) } @@ -273,8 +282,16 @@ func (k *kstub) println(v ...any) { func (k *kstub) aclUpdate(name string, uid int, perms ...acl.Perm) error { k.Helper() - return k.Expects("aclUpdate").Error( - stub.CheckArg(k.Stub, "name", name, 0), + expect := k.Expects("aclUpdate") + + // translate ignored name + nameVal := any(name) + if _, ok := expect.Args[0].(ignoreValue); ok { + nameVal = ignoreValue{} + } + + return expect.Error( + stub.CheckArgReflect(k.Stub, "name", nameVal, 0), stub.CheckArg(k.Stub, "uid", uid, 1), stub.CheckArgReflect(k.Stub, "perms", perms, 2)) } @@ -288,6 +305,12 @@ func (k *kstub) waylandNew(displayPath, bindPath *check.Absolute, appID, instanc stub.CheckArg(k.Stub, "instanceID", instanceID, 3)) } +func (k *kstub) pipewireConnect() (*pipewire.Context, error) { + k.Helper() + expect := k.Expects("pipewireConnect") + return expect.Ret.(func() *pipewire.Context)(), expect.Error() +} + func (k *kstub) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error { k.Helper() return k.Expects("xcbChangeHosts").Error( @@ -387,7 +410,18 @@ func (k *kstub) Verbose(v ...any) { func (k *kstub) Verbosef(format string, v ...any) { k.Helper() - if k.Expects("verbosef").Error( + expect := k.Expects("verbosef") + + // translate ignores in v + if want, ok := expect.Args[1].([]any); ok && len(v) == len(want) { + for i, a := range want { + if _, ok = a.(ignoreValue); ok { + v[i] = ignoreValue{} + } + } + } + + if expect.Error( stub.CheckArg(k.Stub, "format", format, 0), stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil { k.FailNow() diff --git a/internal/system/pipewire.go b/internal/system/pipewire.go new file mode 100644 index 0000000..45b1e8b --- /dev/null +++ b/internal/system/pipewire.go @@ -0,0 +1,99 @@ +package system + +import ( + "errors" + "fmt" + "io" + + "hakurei.app/container/check" + "hakurei.app/hst" + "hakurei.app/internal/acl" + "hakurei.app/internal/pipewire" +) + +// PipeWire maintains a pipewire socket with SecurityContext attached via [pipewire]. +// The socket stops accepting connections once the pipe referred to by sync is closed. +// The socket is pathname only and is destroyed on revert. +func (sys *I) PipeWire(dst *check.Absolute) *I { + sys.ops = append(sys.ops, &pipewireOp{nil, dst}) + return sys +} + +// pipewireOp implements [I.PipeWire]. +type pipewireOp struct { + scc io.Closer + dst *check.Absolute +} + +func (p *pipewireOp) Type() hst.Enablement { return Process } + +func (p *pipewireOp) apply(sys *I) (err error) { + var ctx *pipewire.Context + if ctx, err = sys.pipewireConnect(); err != nil { + return newOpError("pipewire", err, false) + } + defer func() { + if closeErr := ctx.Close(); closeErr != nil && err == nil { + err = newOpError("pipewire", closeErr, false) + } + }() + + sys.msg.Verbosef("pipewire pathname socket on %q", p.dst) + + var registry *pipewire.Registry + if registry, err = ctx.GetRegistry(); err != nil { + return newOpError("pipewire", err, false) + } else if err = ctx.GetCore().Sync(); err != nil { + return newOpError("pipewire", err, false) + } + + var securityContext *pipewire.SecurityContext + if securityContext, err = registry.GetSecurityContext(); err != nil { + return newOpError("pipewire", err, false) + } else if err = ctx.Roundtrip(); err != nil { + return newOpError("pipewire", err, false) + } + + if p.scc, err = securityContext.BindAndCreate(p.dst.String(), pipewire.SPADict{ + {Key: pipewire.PW_KEY_SEC_ENGINE, Value: "app.hakurei"}, + {Key: pipewire.PW_KEY_ACCESS, Value: "restricted"}, + }); err != nil { + return newOpError("pipewire", err, false) + } else if err = ctx.GetCore().Sync(); err != nil { + _ = p.scc.Close() + return newOpError("pipewire", err, false) + } + + if err = sys.chmod(p.dst.String(), 0); err != nil { + if closeErr := p.scc.Close(); closeErr != nil { + return newOpError("pipewire", errors.Join(err, closeErr), false) + } + return newOpError("pipewire", err, false) + } + + if err = sys.aclUpdate(p.dst.String(), sys.uid, acl.Read, acl.Write, acl.Execute); err != nil { + if closeErr := p.scc.Close(); closeErr != nil { + return newOpError("pipewire", errors.Join(err, closeErr), false) + } + return newOpError("pipewire", err, false) + } + + return nil +} + +func (p *pipewireOp) revert(sys *I, _ *Criteria) error { + if p.scc != nil { + sys.msg.Verbosef("hanging up pipewire socket on %q", p.dst) + return newOpError("pipewire", p.scc.Close(), true) + } + return nil +} + +func (p *pipewireOp) Is(o Op) bool { + target, ok := o.(*pipewireOp) + return ok && p != nil && target != nil && + p.dst.Is(target.dst) +} + +func (p *pipewireOp) Path() string { return p.dst.String() } +func (p *pipewireOp) String() string { return fmt.Sprintf("pipewire socket at %q", p.dst) } diff --git a/internal/system/pipewire_test.go b/internal/system/pipewire_test.go new file mode 100644 index 0000000..127e3f2 --- /dev/null +++ b/internal/system/pipewire_test.go @@ -0,0 +1,487 @@ +package system + +import ( + "fmt" + "os" + "path" + "syscall" + "testing" + + "hakurei.app/container/stub" + "hakurei.app/internal/acl" + "hakurei.app/internal/pipewire" +) + +func TestPipeWireOp(t *testing.T) { + t.Parallel() + + checkOpBehaviour(t, checkNoParallel, []opBehaviourTestCase{ + {"success", 0xbeef, 0xff, &pipewireOp{nil, + m(path.Join(t.TempDir(), "pipewire")), + }, []stub.Call{ + call("pipewireConnect", stub.ExpectArgs{}, func() *pipewire.Context { + if ctx, err := pipewire.New(&stubPipeWireConn{sendmsg: []string{ + + /* roundtrip 0 */ + + string([]byte{ + // header: Core::Hello + 0, 0, 0, 0, + 0x18, 0, 0, 1, + 0, 0, 0, 0, + 0, 0, 0, 0, + // Struct + 0x10, 0, 0, 0, + 0xe, 0, 0, 0, + // Int: version = 4 + 4, 0, 0, 0, + 4, 0, 0, 0, + 4, 0, 0, 0, + 0, 0, 0, 0, + + // header: Client::UpdateProperties + 1, 0, 0, 0, + 0x50, 0, 0, 2, + 1, 0, 0, 0, + 0, 0, 0, 0, + // Struct: spa_dict + 0x48, 0, 0, 0, + 0xe, 0, 0, 0, + // Struct: spa_dict_item + 0x40, 0, 0, 0, + 0xe, 0, 0, 0, + // Int: n_items + 4, 0, 0, 0, + 4, 0, 0, 0, + 1, 0, 0, 0, + 0, 0, 0, 0, + // String: key = "remote.intention" + 0x11, 0, 0, 0, + 8, 0, 0, 0, + 0x72, 0x65, 0x6d, 0x6f, + 0x74, 0x65, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, + 0, 0, 0, 0, + 0, 0, 0, 0, + // String: value = "manager" + 8, 0, 0, 0, + 8, 0, 0, 0, + 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0, + + // header: Core::GetRegistry + 0, 0, 0, 0, + 0x28, 0, 0, 5, + 2, 0, 0, 0, + 0, 0, 0, 0, + // Struct + 0x20, 0, 0, 0, + 0xe, 0, 0, 0, + // Int: version = 3 + 4, 0, 0, 0, + 4, 0, 0, 0, + 3, 0, 0, 0, + 0, 0, 0, 0, + // Int: new_id = 2 + 4, 0, 0, 0, + 4, 0, 0, 0, + 2, 0, 0, 0, + 0, 0, 0, 0, + // header: Core::Sync + 0, 0, 0, 0, + 0x28, 0, 0, 2, + 3, 0, 0, 0, + 0, 0, 0, 0, + // Struct + 0x20, 0, 0, 0, + 0xe, 0, 0, 0, + // Int: id = 0 + 4, 0, 0, 0, + 4, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + // Int: seq = 0x40000003 + 4, 0, 0, 0, + 4, 0, 0, 0, + 3, 0, 0, 0x40, + 0, 0, 0, 0, + }), + + /* roundtrip 1 */ + + string([]byte{ + // header: Registry::Bind + 2, 0, 0, 0, + 0x68, 0, 0, 1, + 4, 0, 0, 0, + 0, 0, 0, 0, + // Struct + 0x60, 0, 0, 0, + 0xe, 0, 0, 0, + // Int: id = 3 + 4, 0, 0, 0, + 4, 0, 0, 0, + 3, 0, 0, 0, + 0, 0, 0, 0, + // String: type = "PipeWire:Interface:SecurityContext" + 0x23, 0, 0, 0, + 8, 0, 0, 0, + 0x50, 0x69, 0x70, 0x65, + 0x57, 0x69, 0x72, 0x65, + 0x3a, 0x49, 0x6e, 0x74, + 0x65, 0x72, 0x66, 0x61, + 0x63, 0x65, 0x3a, 0x53, + 0x65, 0x63, 0x75, 0x72, + 0x69, 0x74, 0x79, 0x43, + 0x6f, 0x6e, 0x74, 0x65, + 0x78, 0x74, 0, 0, + 0, 0, 0, 0, + // Int: version = 3 + 4, 0, 0, 0, + 4, 0, 0, 0, + 3, 0, 0, 0, + 0, 0, 0, 0, + // Int: new_id = 3 + 4, 0, 0, 0, + 4, 0, 0, 0, + 3, 0, 0, 0, + 0, 0, 0, 0, + }), + + /* roundtrip 2 */ + + string([]byte{ + // header: SecurityContext::Create + 3, 0, 0, 0, + 0xa8, 0, 0, 1, + 5, 0, 0, 0, + 2, 0, 0, 0, + // Struct + 0xa0, 0, 0, 0, + 0xe, 0, 0, 0, + // Fd: listen_fd = 1 + 8, 0, 0, 0, + 0x12, 0, 0, 0, + 1, 0, 0, 0, + 0, 0, 0, 0, + // Fd: close_fd = 0 + 8, 0, 0, 0, + 0x12, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + // Struct: spa_dict + 0x78, 0, 0, 0, + 0xe, 0, 0, 0, + // Int: n_items = 2 + 4, 0, 0, 0, + 4, 0, 0, 0, + 2, 0, 0, 0, + 0, 0, 0, 0, + // String: key = "pipewire.sec.engine" + 0x14, 0, 0, 0, + 8, 0, 0, 0, + 0x70, 0x69, 0x70, 0x65, + 0x77, 0x69, 0x72, 0x65, + 0x2e, 0x73, 0x65, 0x63, + 0x2e, 0x65, 0x6e, 0x67, + 0x69, 0x6e, 0x65, 0, + 0, 0, 0, 0, + // String: value = "app.hakurei" + 0xc, 0, 0, 0, + 8, 0, 0, 0, + 0x61, 0x70, 0x70, 0x2e, + 0x68, 0x61, 0x6b, 0x75, + 0x72, 0x65, 0x69, 0, + 0, 0, 0, 0, + // String: key = "pipewire.access" + 0x10, 0, 0, 0, + 8, 0, 0, 0, + 0x70, 0x69, 0x70, 0x65, + 0x77, 0x69, 0x72, 0x65, + 0x2e, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0, + // String: value = "restricted" + 0xb, 0, 0, 0, + 8, 0, 0, 0, + 0x72, 0x65, 0x73, 0x74, + 0x72, 0x69, 0x63, 0x74, + 0x65, 0x64, 0, 0, + 0, 0, 0, 0, + // header: Core::Sync + 0, 0, 0, 0, + 0x28, 0, 0, 2, + 6, 0, 0, 0, + 0, 0, 0, 0, + // Struct + 0x20, 0, 0, 0, + 0xe, 0, 0, 0, + // Int: id = 0 + 4, 0, 0, 0, + 4, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + // Int: seq = 0x40000006 + 4, 0, 0, 0, + 4, 0, 0, 0, + 6, 0, 0, 0x40, + 0, 0, 0, 0, + }), + }, recvmsg: []*stubMessage{ + + /* roundtrip 0 */ + + {pipewire.PW_ID_CORE, nil, &pipewire.CoreInfo{ + ID: pipewire.PW_ID_CORE, + Cookie: -2069267610, + UserName: "alice", + HostName: "nixos", + Version: "1.4.7", + Name: "pipewire-0", + ChangeMask: pipewire.PW_CORE_CHANGE_MASK_PROPS, + Properties: &pipewire.SPADict{ + {Key: pipewire.PW_KEY_CONFIG_NAME, Value: "pipewire.conf"}, + {Key: pipewire.PW_KEY_APP_NAME, Value: "pipewire"}, + {Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pipewire"}, + {Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"}, + {Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1446"}, + {Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"}, + {Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"}, + {Key: pipewire.PW_KEY_CORE_DAEMON, Value: "true"}, + {Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"}, + {Key: pipewire.PW_KEY_OBJECT_ID, Value: "0"}, + {Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"}, + }, + }}, + {pipewire.PW_ID_CORE, nil, &pipewire.CoreBoundProps{ + ID: pipewire.PW_ID_CLIENT, + GlobalID: 34, + Properties: &pipewire.SPADict{ + {Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"}, + {Key: pipewire.PW_KEY_MODULE_ID, Value: "2"}, + {Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"}, + {Key: pipewire.PW_KEY_SEC_PID, Value: "1443"}, + {Key: pipewire.PW_KEY_SEC_UID, Value: "1000"}, + {Key: pipewire.PW_KEY_SEC_GID, Value: "100"}, + {Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"}, + }, + }}, + {pipewire.PW_ID_CLIENT, nil, &pipewire.ClientInfo{ + ID: 34, + ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS, + Properties: &pipewire.SPADict{ + {Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"}, + {Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-alice-1443"}, + {Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"}, + {Key: pipewire.PW_KEY_SEC_PID, Value: "1443"}, + {Key: pipewire.PW_KEY_SEC_UID, Value: "1000"}, + {Key: pipewire.PW_KEY_SEC_GID, Value: "100"}, + {Key: pipewire.PW_KEY_MODULE_ID, Value: "2"}, + {Key: pipewire.PW_KEY_OBJECT_ID, Value: "34"}, + {Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"}, + {Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"}, + {Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"}, + }, + }}, + {pipewire.PW_ID_CORE, nil, &pipewire.CoreDone{ + ID: -1, + Sequence: 0, + }}, + {2, nil, &pipewire.RegistryGlobal{ + ID: pipewire.PW_ID_CORE, + Permissions: pipewire.PW_CORE_PERM_MASK, + Type: pipewire.PW_TYPE_INTERFACE_Core, + Version: pipewire.PW_VERSION_CORE, + Properties: &pipewire.SPADict{ + {Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"}, + {Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"}, + }, + }}, + {2, nil, &pipewire.RegistryGlobal{ + ID: 1, + Permissions: pipewire.PW_MODULE_PERM_MASK, + Type: pipewire.PW_TYPE_INTERFACE_Module, + Version: pipewire.PW_VERSION_MODULE, + Properties: &pipewire.SPADict{ + {Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "1"}, + {Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-rt"}, + }, + }}, + {2, nil, &pipewire.RegistryGlobal{ + ID: 3, + Permissions: pipewire.PW_SECURITY_CONTEXT_PERM_MASK, + Type: pipewire.PW_TYPE_INTERFACE_SecurityContext, + Version: pipewire.PW_VERSION_SECURITY_CONTEXT, + Properties: &pipewire.SPADict{ + {Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "3"}, + }, + }}, + {2, nil, &pipewire.RegistryGlobal{ + ID: 2, + Permissions: pipewire.PW_MODULE_PERM_MASK, + Type: pipewire.PW_TYPE_INTERFACE_Module, + Version: pipewire.PW_VERSION_MODULE, + Properties: &pipewire.SPADict{ + {Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "2"}, + {Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-protocol-native"}, + }, + }}, + {pipewire.PW_ID_CORE, nil, &pipewire.CoreDone{ + ID: 0, + Sequence: pipewire.CoreSyncSequenceOffset + 3, + }}, + {2, nil, &pipewire.RegistryGlobal{ + ID: 4, + Permissions: pipewire.PW_CLIENT_PERM_MASK, + Type: pipewire.PW_TYPE_INTERFACE_Client, + Version: pipewire.PW_VERSION_CLIENT, + Properties: &pipewire.SPADict{ + {Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "4"}, + {Key: pipewire.PW_KEY_MODULE_ID, Value: "2"}, + {Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"}, + {Key: pipewire.PW_KEY_SEC_PID, Value: "1447"}, + {Key: pipewire.PW_KEY_SEC_UID, Value: "1000"}, + {Key: pipewire.PW_KEY_SEC_GID, Value: "100"}, + {Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"}, + {Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"}, + {Key: pipewire.PW_KEY_APP_NAME, Value: "WirePlumber"}, + }, + }}, + nil, + + /* roundtrip 1 */ + + {pipewire.PW_ID_CORE, nil, &pipewire.CoreBoundProps{ + ID: 3, + GlobalID: 3, + Properties: &pipewire.SPADict{ + {Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "3"}, + }, + }}, + nil, + + /* roundtrip 2 */ + + {pipewire.PW_ID_CORE, nil, &pipewire.CoreDone{ + ID: 0, + Sequence: pipewire.CoreSyncSequenceOffset + 6, + }}, + nil, + }}, pipewire.SPADict{ + {Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"}, + }); err != nil { + panic(err) + } else { + return ctx + } + }, nil), + call("verbosef", stub.ExpectArgs{"pipewire pathname socket on %q", []any{ignoreValue{}}}, nil, nil), + + call("chmod", stub.ExpectArgs{ignoreValue{}, os.FileMode(0)}, nil, nil), + call("aclUpdate", stub.ExpectArgs{ignoreValue{}, 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), + }, nil, []stub.Call{ + call("verbosef", stub.ExpectArgs{"hanging up pipewire socket on %q", []any{ignoreValue{}}}, nil, nil), + }, nil}, + }) + + checkOpsBuilder(t, "PipeWire", []opsBuilderTestCase{ + {"sample", 0xcafe, func(_ *testing.T, sys *I) { + sys.PipeWire(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire")) + }, []Op{&pipewireOp{nil, + m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"), + }}, stub.Expect{}}, + }) + + checkOpIs(t, []opIsTestCase{ + {"dst differs", &pipewireOp{nil, + m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7d/pipewire"), + }, &pipewireOp{nil, + m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"), + }, false}, + + {"equals", &pipewireOp{nil, + m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"), + }, &pipewireOp{nil, + m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"), + }, true}, + }) + + checkOpMeta(t, []opMetaTestCase{ + {"sample", &pipewireOp{nil, + m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"), + }, Process, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire", + `pipewire socket at "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"`}, + }) +} + +// stubMessage is a [pipewire.Message] prepared ahead of time. +type stubMessage struct { + // Proxy Id included in the [pipewire.Header]. + id pipewire.Int + // Footer optionally appended after the message body. + footer pipewire.KnownSize + // Known-good message prepared ahead of time. + m pipewire.Message +} + +// stubPipeWireConn implements [pipewire.Conn] and checks the behaviour of [pipewire.Context]. +type stubPipeWireConn struct { + // Marshaled and sent for Recvmsg. + recvmsg []*stubMessage + // Current position in recvmsg. + curRecvmsg int + // Current server seq number. + sequence pipewire.Int + + // Compared against calls to Sendmsg. + sendmsg []string + // Current position in sendmsg. + curSendmsg int +} + +// Recvmsg marshals and copies a stubMessage prepared ahead of time. +func (conn *stubPipeWireConn) Recvmsg(p, _ []byte, _ int) (n, _, recvflags int, err error) { + defer func() { conn.curRecvmsg++ }() + recvflags = syscall.MSG_CMSG_CLOEXEC + + if conn.recvmsg[conn.curRecvmsg] == nil { + err = syscall.EAGAIN + return + } + defer func() { conn.sequence++ }() + + if data, marshalErr := (pipewire.MessageEncoder{Message: conn.recvmsg[conn.curRecvmsg].m}).AppendMessage(nil, + conn.recvmsg[conn.curRecvmsg].id, + conn.sequence, + conn.recvmsg[conn.curRecvmsg].footer, + ); marshalErr != nil { + panic(marshalErr) + } else { + n = copy(p, data) + } + return +} + +// Sendmsg checks a client message against a known-good sample. +func (conn *stubPipeWireConn) Sendmsg(p, _ []byte, _ int) (n int, err error) { + defer func() { conn.curSendmsg++ }() + + n = len(p) + if string(p) != conn.sendmsg[conn.curSendmsg] { + err = fmt.Errorf("%#v, want %#v", p, []byte(conn.sendmsg[conn.curSendmsg])) + } + return +} + +// Close checks whether Recvmsg and Sendmsg has depleted all samples. +func (conn *stubPipeWireConn) Close() error { + if conn.curRecvmsg != len(conn.recvmsg) { + return fmt.Errorf("consumed %d recvmsg samples, want %d", conn.curRecvmsg, len(conn.recvmsg)) + } + + if conn.curSendmsg != len(conn.sendmsg) { + return fmt.Errorf("consumed %d sendmsg samples, want %d", conn.curSendmsg, len(conn.sendmsg)) + } + + return nil +}