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 }