diff options
| -rw-r--r-- | internal/pipewire/header.go | 73 | ||||
| -rw-r--r-- | internal/pipewire/header_test.go | 89 | ||||
| -rw-r--r-- | internal/pipewire/pipewire.go | 477 |
3 files changed, 639 insertions, 0 deletions
diff --git a/internal/pipewire/header.go b/internal/pipewire/header.go new file mode 100644 index 00000000..ceb4cde6 --- /dev/null +++ b/internal/pipewire/header.go @@ -0,0 +1,73 @@ +package pipewire + +import ( + "encoding/binary" + "errors" +) + +const ( + // HeaderSize is the fixed size of [Header]. + HeaderSize = 16 + // SizeMax is the largest value of [Header.Size] that can be represented in its 3-byte segment. + SizeMax = 0x00ffffff +) + +var ( + // ErrSizeRange indicates that the value of [Header.Size] cannot be represented in its 3-byte segment. + ErrSizeRange = errors.New("size out of range") + // ErrBadHeader indicates that the header slice does not have length [HeaderSize]. + ErrBadHeader = errors.New("incorrect header size") +) + +// A Header is the fixed-size message header described in protocol native. +type Header struct { + // The message id this is the destination resource/proxy id. + ID uint32 `json:"Id"` + // The opcode on the resource/proxy interface. + Opcode byte `json:"opcode"` + // The size of the payload and optional footer of the message. + // Note: this value is only 24 bits long in the format. + Size uint32 `json:"size"` + // An increasing sequence number for each message. + Sequence uint32 `json:"seq"` + // Number of file descriptors in this message. + FileCount uint32 `json:"n_fds"` +} + +// append appends the protocol native message header to data. +// +// Callers must perform bounds check on [Header.Size]. +func (h *Header) append(data []byte) []byte { + data = binary.NativeEndian.AppendUint32(data, h.ID) + data = binary.NativeEndian.AppendUint32(data, uint32(h.Opcode)<<24|h.Size) + data = binary.NativeEndian.AppendUint32(data, h.Sequence) + data = binary.NativeEndian.AppendUint32(data, h.FileCount) + return data +} + +// MarshalBinary encodes the protocol native message header. +func (h *Header) MarshalBinary() (data []byte, err error) { + if h.Size&^SizeMax != 0 { + return nil, ErrSizeRange + } + return h.append(make([]byte, 0, HeaderSize)), nil +} + +// unmarshalBinary decodes the protocol native message header. +func (h *Header) unmarshalBinary(data [HeaderSize]byte) { + h.ID = binary.NativeEndian.Uint32(data[0:4]) + h.Size = binary.NativeEndian.Uint32(data[4:8]) + h.Opcode = byte(h.Size >> 24) + h.Size &= SizeMax + h.Sequence = binary.NativeEndian.Uint32(data[8:]) + h.FileCount = binary.NativeEndian.Uint32(data[12:]) +} + +// UnmarshalBinary decodes the protocol native message header. +func (h *Header) UnmarshalBinary(data []byte) error { + if len(data) != HeaderSize { + return ErrBadHeader + } + h.unmarshalBinary(([HeaderSize]byte)(data)) + return nil +} diff --git a/internal/pipewire/header_test.go b/internal/pipewire/header_test.go new file mode 100644 index 00000000..d933598f --- /dev/null +++ b/internal/pipewire/header_test.go @@ -0,0 +1,89 @@ +package pipewire_test + +import ( + "reflect" + "testing" + + "hakurei.app/internal/pipewire" +) + +func TestHeader(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + data []byte + want pipewire.Header + }{ + {"PW_SECURITY_CONTEXT_METHOD_CREATE", []byte{ + // Id + 3, 0, 0, 0, + // size + 0xd8, 0, 0, + // opcode + 1, + // seq + 5, 0, 0, 0, + // n_fds + 2, 0, 0, 0, + }, pipewire.Header{ID: 3, Opcode: pipewire.PW_SECURITY_CONTEXT_METHOD_CREATE, + Size: 0xd8, Sequence: 5, FileCount: 2}}, + + {"PW_SECURITY_CONTEXT_METHOD_NUM", []byte{ + // Id + 0, 0, 0, 0, + // size + 0x28, 0, 0, + // opcode + 2, + // seq + 6, 0, 0, 0, + // n_fds + 0, 0, 0, 0, + }, pipewire.Header{ID: 0, Opcode: pipewire.PW_SECURITY_CONTEXT_METHOD_NUM, + Size: 0x28, Sequence: 6, FileCount: 0}}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + t.Run("decode", func(t *testing.T) { + t.Parallel() + + var got pipewire.Header + if err := got.UnmarshalBinary(tc.data); err != nil { + t.Fatalf("UnmarshalBinary: error = %v", err) + } + if got != tc.want { + t.Fatalf("UnmarshalBinary: %#v, want %#v", got, tc.want) + } + }) + + t.Run("encode", func(t *testing.T) { + t.Parallel() + + if got, err := tc.want.MarshalBinary(); err != nil { + t.Fatalf("MarshalBinary: error = %v", err) + } else if string(got) != string(tc.data) { + t.Fatalf("MarshalBinary: %#v, want %#v", got, tc.data) + } + }) + }) + } + + t.Run("size range", func(t *testing.T) { + t.Parallel() + + if _, err := (&pipewire.Header{Size: 0xff000000}).MarshalBinary(); !reflect.DeepEqual(err, pipewire.ErrSizeRange) { + t.Errorf("UnmarshalBinary: error = %v", err) + } + }) + + t.Run("header size", func(t *testing.T) { + t.Parallel() + + if err := (*pipewire.Header)(nil).UnmarshalBinary(nil); !reflect.DeepEqual(err, pipewire.ErrBadHeader) { + t.Errorf("UnmarshalBinary: error = %v", err) + } + }) +} diff --git a/internal/pipewire/pipewire.go b/internal/pipewire/pipewire.go new file mode 100644 index 00000000..a90811e5 --- /dev/null +++ b/internal/pipewire/pipewire.go @@ -0,0 +1,477 @@ +// Package pipewire provides a partial implementation of the PipeWire protocol native. +// +// This implementation is created based on black box analysis and very limited static +// analysis. The PipeWire documentation is vague and mostly nonexistent, and source code +// readability is not great due to frequent macro abuse, confusing and inconsistent naming +// schemes, almost complete absence of comments and the multiple layers of abstractions +// even internal to the library. The convoluted build system and frequent (mis)use of +// dlopen(3) further complicates static analysis efforts. +// +// Because of this, extreme care must be taken when reusing any code found in this package. +// While it is extensively tested to be correct for its role within Hakurei, remember that +// work is only done against PipeWire behaviour specific to this use case, and it is nearly +// impossible to guarantee that this interpretation of its behaviour is intended, or correct +// for any other uses of the protocol. +package pipewire + +/* pipewire/client.h */ + +const ( + PW_TYPE_INTERFACE_Client = PW_TYPE_INFO_INTERFACE_BASE + "Client" + PW_CLIENT_PERM_MASK = PW_PERM_RWXM + PW_VERSION_CLIENT = 3 + + PW_ID_CLIENT = 1 +) + +const ( + PW_CLIENT_CHANGE_MASK_PROPS = 1 << iota + + PW_CLIENT_CHANGE_MASK_ALL = 1<<iota - 1 +) + +const ( + PW_CLIENT_EVENT_INFO = iota + PW_CLIENT_EVENT_PERMISSIONS + PW_CLIENT_EVENT_NUM + + PW_VERSION_CLIENT_EVENTS = 0 +) + +const ( + PW_CLIENT_METHOD_ADD_LISTENER = iota + PW_CLIENT_METHOD_ERROR + PW_CLIENT_METHOD_UPDATE_PROPERTIES + PW_CLIENT_METHOD_GET_PERMISSIONS + PW_CLIENT_METHOD_UPDATE_PERMISSIONS + PW_CLIENT_METHOD_NUM + + PW_VERSION_CLIENT_METHODS = 0 +) + +/* pipewire/device.h */ + +const ( + PW_TYPE_INTERFACE_Device = PW_TYPE_INFO_INTERFACE_BASE + "Device" + PW_DEVICE_PERM_MASK = PW_PERM_RWXM + PW_VERSION_DEVICE = 3 +) + +const ( + PW_DEVICE_CHANGE_MASK_PROPS = 1 << iota + PW_DEVICE_CHANGE_MASK_PARAMS + + PW_DEVICE_CHANGE_MASK_ALL = 1<<iota - 1 +) + +const ( + PW_DEVICE_EVENT_INFO = iota + PW_DEVICE_EVENT_PARAM + PW_DEVICE_EVENT_NUM + + PW_VERSION_DEVICE_EVENTS = 0 +) + +const ( + PW_DEVICE_METHOD_ADD_LISTENER = iota + PW_DEVICE_METHOD_SUBSCRIBE_PARAMS + PW_DEVICE_METHOD_ENUM_PARAMS + PW_DEVICE_METHOD_SET_PARAM + PW_DEVICE_METHOD_NUM + + PW_VERSION_DEVICE_METHODS = 0 +) + +/* pipewire/core.h */ + +const ( + PW_TYPE_INTERFACE_Core = PW_TYPE_INFO_INTERFACE_BASE + "Core" + PW_TYPE_INTERFACE_Registry = PW_TYPE_INFO_INTERFACE_BASE + "Registry" + PW_CORE_PERM_MASK = PW_PERM_R | PW_PERM_X | PW_PERM_M + PW_VERSION_CORE = 4 + PW_VERSION_REGISTRY = 3 + + PW_DEFAULT_REMOTE = "pipewire-0" + PW_ID_CORE = 0 + PW_ID_ANY = uint32(0xffffffff) +) + +const ( + PW_CORE_CHANGE_MASK_PROPS = 1 << iota + + PW_CORE_CHANGE_MASK_ALL = 1<<iota - 1 +) + +const ( + PW_CORE_EVENT_INFO = iota + PW_CORE_EVENT_DONE + PW_CORE_EVENT_PING + PW_CORE_EVENT_ERROR + PW_CORE_EVENT_REMOVE_ID + PW_CORE_EVENT_BOUND_ID + PW_CORE_EVENT_ADD_MEM + PW_CORE_EVENT_REMOVE_MEM + PW_CORE_EVENT_BOUND_PROPS + PW_CORE_EVENT_NUM + + PW_VERSION_CORE_EVENTS = 1 +) + +const ( + PW_CORE_METHOD_ADD_LISTENER = iota + PW_CORE_METHOD_HELLO + PW_CORE_METHOD_SYNC + PW_CORE_METHOD_PONG + PW_CORE_METHOD_ERROR + PW_CORE_METHOD_GET_REGISTRY + PW_CORE_METHOD_CREATE_OBJECT + PW_CORE_METHOD_DESTROY + PW_CORE_METHOD_NUM + + PW_VERSION_CORE_METHODS = 0 +) + +const ( + PW_REGISTRY_EVENT_GLOBAL = iota + PW_REGISTRY_EVENT_GLOBAL_REMOVE + PW_REGISTRY_EVENT_NUM + + PW_VERSION_REGISTRY_EVENTS = 0 +) + +const ( + PW_REGISTRY_METHOD_ADD_LISTENER = iota + PW_REGISTRY_METHOD_BIND + PW_REGISTRY_METHOD_DESTROY + PW_REGISTRY_METHOD_NUM + + PW_VERSION_REGISTRY_METHODS = 0 +) + +/* pipewire/factory.h */ + +const ( + PW_TYPE_INTERFACE_Factory = PW_TYPE_INFO_INTERFACE_BASE + "Factory" + PW_FACTORY_PERM_MASK = PW_PERM_R | PW_PERM_M + PW_VERSION_FACTORY = 3 +) + +const ( + PW_FACTORY_CHANGE_MASK_PROPS = 1 << iota + + PW_FACTORY_CHANGE_MASK_ALL = 1<<iota - 1 +) + +const ( + PW_FACTORY_EVENT_INFO = iota + PW_FACTORY_EVENT_NUM + + PW_VERSION_FACTORY_EVENTS = 0 +) + +const ( + PW_FACTORY_METHOD_ADD_LISTENER = iota + PW_FACTORY_METHOD_NUM + + PW_VERSION_FACTORY_METHODS = 0 +) + +/* pipewire/link.h */ + +const ( + PW_TYPE_INTERFACE_Link = PW_TYPE_INFO_INTERFACE_BASE + "Link" + PW_LINK_PERM_MASK = PW_PERM_R | PW_PERM_X + PW_VERSION_LINK = 3 +) + +const ( + PW_LINK_STATE_ERROR = iota - 2 // the link is in error + PW_LINK_STATE_UNLINKED // the link is unlinked + PW_LINK_STATE_INIT // the link is initialized + PW_LINK_STATE_NEGOTIATING // the link is negotiating formats + PW_LINK_STATE_ALLOCATING // the link is allocating buffers + PW_LINK_STATE_PAUSED // the link is paused + PW_LINK_STATE_ACTIVE // the link is active +) + +const ( + PW_LINK_CHANGE_MASK_STATE = (1 << iota) + PW_LINK_CHANGE_MASK_FORMAT + PW_LINK_CHANGE_MASK_PROPS + + PW_LINK_CHANGE_MASK_ALL = 1<<iota - 1 +) + +const ( + PW_LINK_EVENT_INFO = iota + PW_LINK_EVENT_NUM + + PW_VERSION_LINK_EVENTS = 0 +) + +const ( + PW_LINK_METHOD_ADD_LISTENER = iota + PW_LINK_METHOD_NUM + + PW_VERSION_LINK_METHODS = 0 +) + +/* pipewire/module.h */ + +const ( + PW_TYPE_INTERFACE_Module = PW_TYPE_INFO_INTERFACE_BASE + "Module" + PW_MODULE_PERM_MASK = PW_PERM_R | PW_PERM_M + PW_VERSION_MODULE = 3 +) + +const ( + PW_MODULE_CHANGE_MASK_PROPS = 1 << iota + + PW_MODULE_CHANGE_MASK_ALL = 1<<iota - 1 +) + +const ( + PW_MODULE_EVENT_INFO = iota + PW_MODULE_EVENT_NUM + + PW_VERSION_MODULE_EVENTS = 0 +) + +const ( + PW_MODULE_METHOD_ADD_LISTENER = iota + PW_MODULE_METHOD_NUM + + PW_VERSION_MODULE_METHODS = 0 +) + +/* pipewire/node.h */ + +const ( + PW_TYPE_INTERFACE_Node = PW_TYPE_INFO_INTERFACE_BASE + "Node" + PW_NODE_PERM_MASK = PW_PERM_RWXML + PW_VERSION_NODE = 3 +) + +const ( + PW_NODE_STATE_ERROR = iota - 1 // error state + PW_NODE_STATE_CREATING // the node is being created + PW_NODE_STATE_SUSPENDED // the node is suspended, the device might be closed + PW_NODE_STATE_IDLE // the node is running but there is no active port + PW_NODE_STATE_RUNNING // the node is running +) + +const ( + PW_NODE_CHANGE_MASK_INPUT_PORTS = 1 << iota + PW_NODE_CHANGE_MASK_OUTPUT_PORTS + PW_NODE_CHANGE_MASK_STATE + PW_NODE_CHANGE_MASK_PROPS + PW_NODE_CHANGE_MASK_PARAMS + + PW_NODE_CHANGE_MASK_ALL = 1<<iota - 1 +) + +const ( + PW_NODE_EVENT_INFO = iota + PW_NODE_EVENT_PARAM + PW_NODE_EVENT_NUM + + PW_VERSION_NODE_EVENTS = 0 +) + +const ( + PW_NODE_METHOD_ADD_LISTENER = iota + PW_NODE_METHOD_SUBSCRIBE_PARAMS + PW_NODE_METHOD_ENUM_PARAMS + PW_NODE_METHOD_SET_PARAM + PW_NODE_METHOD_SEND_COMMAND + PW_NODE_METHOD_NUM + + PW_VERSION_NODE_METHODS = 0 +) + +/* pipewire/permission.h */ + +const ( + PW_PERM_R = 0400 // object can be seen and events can be received + PW_PERM_W = 0200 // methods can be called that modify the object + PW_PERM_X = 0100 // methods can be called on the object. The W flag must be present in order to call methods that modify the object. + PW_PERM_M = 0010 // metadata can be set on object, Since 0.3.9 + PW_PERM_L = 0020 // a link can be made between a node that doesn't have permission to see the other node, Since 0.3.77 + + PW_PERM_RW = PW_PERM_R | PW_PERM_W + PW_PERM_RWX = PW_PERM_RW | PW_PERM_X + PW_PERM_RWXM = PW_PERM_RWX | PW_PERM_M + PW_PERM_RWXML = PW_PERM_RWXM | PW_PERM_L + + PW_PERM_ALL = PW_PERM_RWXM + PW_PERM_INVALID uint32 = 0xffffffff +) + +/* pipewire/port.h */ + +const ( + PW_TYPE_INTERFACE_Port = PW_TYPE_INFO_INTERFACE_BASE + "Port" + PW_PORT_PERM_MASK = PW_PERM_R | PW_PERM_X | PW_PERM_M + PW_VERSION_PORT = 3 +) + +const ( + PW_PORT_CHANGE_MASK_PROPS = 1 << iota + PW_PORT_CHANGE_MASK_PARAMS + + PW_PORT_CHANGE_MASK_ALL = 1<<iota - 1 +) + +const ( + PW_PORT_EVENT_INFO = iota + PW_PORT_EVENT_PARAM + PW_PORT_EVENT_NUM + + PW_VERSION_PORT_EVENTS = 0 +) + +const ( + PW_PORT_METHOD_ADD_LISTENER = iota + PW_PORT_METHOD_SUBSCRIBE_PARAMS + PW_PORT_METHOD_ENUM_PARAMS + PW_PORT_METHOD_NUM + + PW_VERSION_PORT_METHODS = 0 +) + +/* pipewire/extensions/client-node.h */ + +const ( + PW_TYPE_INTERFACE_ClientNode = PW_TYPE_INFO_INTERFACE_BASE + "ClientNode" + PW_VERSION_CLIENT_NODE = 6 +) + +const ( + PW_CLIENT_NODE_EVENT_TRANSPORT = iota + PW_CLIENT_NODE_EVENT_SET_PARAM + PW_CLIENT_NODE_EVENT_SET_IO + PW_CLIENT_NODE_EVENT_EVENT + PW_CLIENT_NODE_EVENT_COMMAND + PW_CLIENT_NODE_EVENT_ADD_PORT + PW_CLIENT_NODE_EVENT_REMOVE_PORT + PW_CLIENT_NODE_EVENT_PORT_SET_PARAM + PW_CLIENT_NODE_EVENT_PORT_USE_BUFFERS + PW_CLIENT_NODE_EVENT_PORT_SET_IO + PW_CLIENT_NODE_EVENT_SET_ACTIVATION + PW_CLIENT_NODE_EVENT_PORT_SET_MIX_INFO + PW_CLIENT_NODE_EVENT_NUM + + PW_VERSION_CLIENT_NODE_EVENTS = 1 +) + +const ( + PW_CLIENT_NODE_METHOD_ADD_LISTENER = iota + PW_CLIENT_NODE_METHOD_GET_NODE + PW_CLIENT_NODE_METHOD_UPDATE + PW_CLIENT_NODE_METHOD_PORT_UPDATE + PW_CLIENT_NODE_METHOD_SET_ACTIVE + PW_CLIENT_NODE_METHOD_EVENT + PW_CLIENT_NODE_METHOD_PORT_BUFFERS + PW_CLIENT_NODE_METHOD_NUM + + PW_VERSION_CLIENT_NODE_METHODS = 0 +) + +const ( + PW_CLIENT_NODE_UPDATE_PARAMS = 1 << iota + PW_CLIENT_NODE_UPDATE_INFO +) + +const ( + PW_CLIENT_NODE_PORT_UPDATE_PARAMS = 1 << iota + PW_CLIENT_NODE_PORT_UPDATE_INFO +) + +/* pipewire/extensions/metadata.h */ + +const ( + PW_TYPE_INTERFACE_Metadata = PW_TYPE_INFO_INTERFACE_BASE + "Metadata" + PW_METADATA_PERM_MASK = PW_PERM_RWX + PW_VERSION_METADATA = 3 +) + +const ( + PW_METADATA_EVENT_PROPERTY = iota + PW_METADATA_EVENT_NUM + + PW_VERSION_METADATA_EVENTS = 0 +) + +const ( + PW_METADATA_METHOD_ADD_LISTENER = iota + PW_METADATA_METHOD_SET_PROPERTY + PW_METADATA_METHOD_CLEAR + PW_METADATA_METHOD_NUM + + PW_VERSION_METADATA_METHODS = 0 +) + +const ( + PW_KEY_METADATA_NAME = "metadata.name" + PW_KEY_METADATA_VALUES = "metadata.values" +) + +/* pipewire/extensions/profiler.h */ + +const ( + PW_TYPE_INTERFACE_Profiler = PW_TYPE_INFO_INTERFACE_BASE + "Profiler" + PW_VERSION_PROFILER = 3 + W_PROFILER_PERM_MASK = PW_PERM_R +) + +const ( + PW_PROFILER_EVENT_PROFILE = iota + PW_PROFILER_EVENT_NUM + + PW_VERSION_PROFILER_EVENTS = 0 +) + +const ( + PW_PROFILER_METHOD_ADD_LISTENER = iota + PW_PROFILER_METHOD_NUM + + PW_VERSION_PROFILER_METHODS = 0 +) + +const ( + PW_KEY_PROFILER_NAME = "profiler.name" +) + +/* pipewire/extensions/security-context.h */ + +const ( + PW_TYPE_INTERFACE_SecurityContext = PW_TYPE_INFO_INTERFACE_BASE + "SecurityContext" + PW_SECURITY_CONTEXT_PERM_MASK = PW_PERM_RWX + PW_VERSION_SECURITY_CONTEXT = 3 +) + +const ( + PW_SECURITY_CONTEXT_EVENT_NUM = iota + + PW_VERSION_SECURITY_CONTEXT_EVENTS = 0 +) + +const ( + PW_SECURITY_CONTEXT_METHOD_ADD_LISTENER = iota + PW_SECURITY_CONTEXT_METHOD_CREATE + PW_SECURITY_CONTEXT_METHOD_NUM + + PW_VERSION_SECURITY_CONTEXT_METHODS = 0 +) + +/* pipewire/type.h */ + +const ( + PW_TYPE_INFO_BASE = "PipeWire:" + + PW_TYPE_INFO_Object = PW_TYPE_INFO_BASE + "Object" + PW_TYPE_INFO_OBJECT_BASE = PW_TYPE_INFO_Object + ":" + + PW_TYPE_INFO_Interface = PW_TYPE_INFO_BASE + "Interface" + PW_TYPE_INFO_INTERFACE_BASE = PW_TYPE_INFO_Interface + ":" +) |
