aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/system/pipewire_test.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-12-07 17:39:34 +0900
committerOphestra <cat@gensokyo.uk>2025-12-07 17:39:34 +0900
commit093e30c78872a5d8605de4b2a3aeb35abca63835 (patch)
tree15e87de000864f78f7c02a5c1f12ed829027b5d8 /internal/system/pipewire_test.go
parent1b17ccda914de78a88e1edbb42ce70d963d4b5a2 (diff)
internal/system: integrate PipeWire SecurityContext
Tests for this Op happens to be the best out of everything due to the robust infrastructure offered by internal/pipewire. This is now ready to use in internal/outcome for implementing #26. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/system/pipewire_test.go')
-rw-r--r--internal/system/pipewire_test.go487
1 files changed, 487 insertions, 0 deletions
diff --git a/internal/system/pipewire_test.go b/internal/system/pipewire_test.go
new file mode 100644
index 00000000..127e3f25
--- /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
+}