From 3e87187c4c4eb43ed1c9dc1c0c6613e5bed1dc6c Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sun, 23 Nov 2025 16:20:35 +0900 Subject: [PATCH] internal/pipewire: implement message header Test cases are from interactions between pw-container and PipeWire. Results are validated against corresponding body. Signed-off-by: Ophestra --- internal/pipewire/header.go | 73 +++++ internal/pipewire/header_test.go | 89 ++++++ internal/pipewire/pipewire.go | 477 +++++++++++++++++++++++++++++++ 3 files changed, 639 insertions(+) create mode 100644 internal/pipewire/header.go create mode 100644 internal/pipewire/header_test.go create mode 100644 internal/pipewire/pipewire.go diff --git a/internal/pipewire/header.go b/internal/pipewire/header.go new file mode 100644 index 0000000..ceb4cde --- /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 0000000..d933598 --- /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 0000000..a90811e --- /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<