From 95ccdc133018ce4459f73d4fdd2e1eac926badf4 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Mon, 24 Nov 2025 21:48:01 +0900 Subject: [PATCH] internal/pipewire: add type constants This change also centralises encoding testing. Signed-off-by: Ophestra --- internal/pipewire/header.go | 8 +- internal/pipewire/header_test.go | 53 +++++--------- internal/pipewire/pipewire.go | 4 +- internal/pipewire/pod.go | 121 +++++++++++++++++++++++++++++++ internal/pipewire/pod_test.go | 55 ++++++++++++++ 5 files changed, 200 insertions(+), 41 deletions(-) create mode 100644 internal/pipewire/pod.go create mode 100644 internal/pipewire/pod_test.go diff --git a/internal/pipewire/header.go b/internal/pipewire/header.go index ceb4cde..9963220 100644 --- a/internal/pipewire/header.go +++ b/internal/pipewire/header.go @@ -22,16 +22,16 @@ var ( // 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"` + ID Uint `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"` + Sequence Uint `json:"seq"` // Number of file descriptors in this message. - FileCount uint32 `json:"n_fds"` + FileCount Uint `json:"n_fds"` } // append appends the protocol native message header to data. @@ -39,7 +39,7 @@ type Header struct { // 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, Word(h.Opcode)<<24|h.Size) data = binary.NativeEndian.AppendUint32(data, h.Sequence) data = binary.NativeEndian.AppendUint32(data, h.FileCount) return data diff --git a/internal/pipewire/header_test.go b/internal/pipewire/header_test.go index d933598..8501dc1 100644 --- a/internal/pipewire/header_test.go +++ b/internal/pipewire/header_test.go @@ -10,11 +10,21 @@ import ( func TestHeader(t *testing.T) { t.Parallel() - testCases := []struct { - name string - data []byte - want pipewire.Header - }{ + encodingTestCases[pipewire.Header, *pipewire.Header]{ + {"PW_CORE_METHOD_HELLO", []byte{ + // Id + 0, 0, 0, 0, + // size + 0x18, 0, 0, + // opcode + 1, + // seq + 0, 0, 0, 0, + // n_fds + 0, 0, 0, 0, + }, pipewire.Header{ID: pipewire.PW_ID_CORE, Opcode: pipewire.PW_CORE_METHOD_HELLO, + Size: 0x18, Sequence: 0, FileCount: 0}, nil}, + {"PW_SECURITY_CONTEXT_METHOD_CREATE", []byte{ // Id 3, 0, 0, 0, @@ -27,7 +37,7 @@ func TestHeader(t *testing.T) { // n_fds 2, 0, 0, 0, }, pipewire.Header{ID: 3, Opcode: pipewire.PW_SECURITY_CONTEXT_METHOD_CREATE, - Size: 0xd8, Sequence: 5, FileCount: 2}}, + Size: 0xd8, Sequence: 5, FileCount: 2}, nil}, {"PW_SECURITY_CONTEXT_METHOD_NUM", []byte{ // Id @@ -41,35 +51,8 @@ func TestHeader(t *testing.T) { // 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) - } - }) - }) - } + Size: 0x28, Sequence: 6, FileCount: 0}, nil}, + }.run(t) t.Run("size range", func(t *testing.T) { t.Parallel() diff --git a/internal/pipewire/pipewire.go b/internal/pipewire/pipewire.go index a90811e..97eaba5 100644 --- a/internal/pipewire/pipewire.go +++ b/internal/pipewire/pipewire.go @@ -303,8 +303,8 @@ const ( 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 + PW_PERM_ALL = PW_PERM_RWXM + PW_PERM_INVALID Word = 0xffffffff ) /* pipewire/port.h */ diff --git a/internal/pipewire/pod.go b/internal/pipewire/pod.go new file mode 100644 index 0000000..5c960fb --- /dev/null +++ b/internal/pipewire/pod.go @@ -0,0 +1,121 @@ +package pipewire + +type ( + // A Word is a 32-bit unsigned integer. + // + // Values internal to a message appear to always be aligned to 32-bit boundary. + Word = uint32 + + // An Int is a signed integer the size of a PipeWire Word. + Int = int32 + // An Uint is an unsigned integer the size of a PipeWire Word. + Uint = Word +) + +/* Basic types */ +const ( + SPA_TYPE_START = 0x00000 + iota + SPA_TYPE_None + SPA_TYPE_Bool + SPA_TYPE_Id + SPA_TYPE_Int + SPA_TYPE_Long + SPA_TYPE_Float + SPA_TYPE_Double + SPA_TYPE_String + SPA_TYPE_Bytes + SPA_TYPE_Rectangle + SPA_TYPE_Fraction + SPA_TYPE_Bitmap + SPA_TYPE_Array + SPA_TYPE_Struct + SPA_TYPE_Object + SPA_TYPE_Sequence + SPA_TYPE_Pointer + SPA_TYPE_Fd + SPA_TYPE_Choice + SPA_TYPE_Pod + _SPA_TYPE_LAST /**< not part of ABI */ +) + +/* Pointers */ +const ( + SPA_TYPE_POINTER_START = 0x10000 + iota + SPA_TYPE_POINTER_Buffer + SPA_TYPE_POINTER_Meta + SPA_TYPE_POINTER_Dict + _SPA_TYPE_POINTER_LAST /**< not part of ABI */ +) + +/* Events */ +const ( + SPA_TYPE_EVENT_START = 0x20000 + iota + SPA_TYPE_EVENT_Device + SPA_TYPE_EVENT_Node + _SPA_TYPE_EVENT_LAST /**< not part of ABI */ +) + +/* Commands */ +const ( + SPA_TYPE_COMMAND_START = 0x30000 + iota + SPA_TYPE_COMMAND_Device + SPA_TYPE_COMMAND_Node + _SPA_TYPE_COMMAND_LAST /**< not part of ABI */ +) + +/* Objects */ +const ( + SPA_TYPE_OBJECT_START = 0x40000 + iota + SPA_TYPE_OBJECT_PropInfo + SPA_TYPE_OBJECT_Props + SPA_TYPE_OBJECT_Format + SPA_TYPE_OBJECT_ParamBuffers + SPA_TYPE_OBJECT_ParamMeta + SPA_TYPE_OBJECT_ParamIO + SPA_TYPE_OBJECT_ParamProfile + SPA_TYPE_OBJECT_ParamPortConfig + SPA_TYPE_OBJECT_ParamRoute + SPA_TYPE_OBJECT_Profiler + SPA_TYPE_OBJECT_ParamLatency + SPA_TYPE_OBJECT_ParamProcessLatency + SPA_TYPE_OBJECT_ParamTag + _SPA_TYPE_OBJECT_LAST /**< not part of ABI */ +) + +/* vendor extensions */ +const ( + SPA_TYPE_VENDOR_PipeWire = 0x02000000 + + SPA_TYPE_VENDOR_Other = 0x7f000000 +) + +const ( + SPA_TYPE_INFO_BASE = "Spa:" + + SPA_TYPE_INFO_Flags = SPA_TYPE_INFO_BASE + "Flags" + SPA_TYPE_INFO_FLAGS_BASE = SPA_TYPE_INFO_Flags + ":" + + SPA_TYPE_INFO_Enum = SPA_TYPE_INFO_BASE + "Enum" + SPA_TYPE_INFO_ENUM_BASE = SPA_TYPE_INFO_Enum + ":" + + SPA_TYPE_INFO_Pod = SPA_TYPE_INFO_BASE + "Pod" + SPA_TYPE_INFO_POD_BASE = SPA_TYPE_INFO_Pod + ":" + + SPA_TYPE_INFO_Struct = SPA_TYPE_INFO_POD_BASE + "Struct" + SPA_TYPE_INFO_STRUCT_BASE = SPA_TYPE_INFO_Struct + ":" + + SPA_TYPE_INFO_Object = SPA_TYPE_INFO_POD_BASE + "Object" + SPA_TYPE_INFO_OBJECT_BASE = SPA_TYPE_INFO_Object + ":" + + SPA_TYPE_INFO_Pointer = SPA_TYPE_INFO_BASE + "Pointer" + SPA_TYPE_INFO_POINTER_BASE = SPA_TYPE_INFO_Pointer + ":" + + SPA_TYPE_INFO_Interface = SPA_TYPE_INFO_POINTER_BASE + "Interface" + SPA_TYPE_INFO_INTERFACE_BASE = SPA_TYPE_INFO_Interface + ":" + + SPA_TYPE_INFO_Event = SPA_TYPE_INFO_OBJECT_BASE + "Event" + SPA_TYPE_INFO_EVENT_BASE = SPA_TYPE_INFO_Event + ":" + + SPA_TYPE_INFO_Command = SPA_TYPE_INFO_OBJECT_BASE + "Command" + SPA_TYPE_INFO_COMMAND_BASE = SPA_TYPE_INFO_Command + ":" +) diff --git a/internal/pipewire/pod_test.go b/internal/pipewire/pod_test.go new file mode 100644 index 0000000..a798286 --- /dev/null +++ b/internal/pipewire/pod_test.go @@ -0,0 +1,55 @@ +package pipewire_test + +import ( + "encoding" + "reflect" + "testing" +) + +type encodingTestCases[V any, S interface { + encoding.BinaryMarshaler + encoding.BinaryUnmarshaler + *V +}] []struct { + // Uninterpreted name of subtest. + name string + // Encoded data. + wantData []byte + // Value corresponding to wantData. + value V + // Expected decoding error. Skips encoding check if non-nil. + wantErr error +} + +// run runs all test cases as subtests of [testing.T]. +func (testCases encodingTestCases[V, S]) run(t *testing.T) { + t.Helper() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + t.Run("decode", func(t *testing.T) { + t.Parallel() + + var value V + if err := S(&value).UnmarshalBinary(tc.wantData); err != nil { + t.Fatalf("UnmarshalBinary: error = %v", err) + } + if !reflect.DeepEqual(&value, &tc.value) { + t.Fatalf("UnmarshalBinary: %#v, want %#v", value, tc.value) + } + }) + + t.Run("encode", func(t *testing.T) { + t.Parallel() + + if gotData, err := S(&tc.value).MarshalBinary(); err != nil { + t.Fatalf("MarshalBinary: error = %v", err) + } else if string(gotData) != string(tc.wantData) { + t.Fatalf("MarshalBinary: %#v, want %#v", gotData, tc.wantData) + } + }) + }) + } +}