diff --git a/internal/pipewire/core_test.go b/internal/pipewire/core_test.go index 9d19be4..0c52c7e 100644 --- a/internal/pipewire/core_test.go +++ b/internal/pipewire/core_test.go @@ -128,15 +128,15 @@ func TestCorePing(t *testing.T) { // handmade sample {"sample", []byte{ /* size: rest of data */ 0x20, 0, 0, 0, - /* type: Struct */ pipewire.SPA_TYPE_Struct, 0, 0, 0, + /* type: Struct */ byte(pipewire.SPA_TYPE_Struct), 0, 0, 0, /* size: 4 bytes */ 4, 0, 0, 0, - /* type: Int */ pipewire.SPA_TYPE_Int, 0, 0, 0, + /* type: Int */ byte(pipewire.SPA_TYPE_Int), 0, 0, 0, /* value: -1 */ 0xff, 0xff, 0xff, 0xff, /* padding */ 0, 0, 0, 0, /* size: 4 bytes */ 4, 0, 0, 0, - /* type: Int */ pipewire.SPA_TYPE_Int, 0, 0, 0, + /* type: Int */ byte(pipewire.SPA_TYPE_Int), 0, 0, 0, /* value: 0 */ 0, 0, 0, 0, /* padding */ 0, 0, 0, 0, }, pipewire.CorePing{ @@ -257,15 +257,15 @@ func TestCorePong(t *testing.T) { // handmade sample {"sample", []byte{ /* size: rest of data */ 0x20, 0, 0, 0, - /* type: Struct */ pipewire.SPA_TYPE_Struct, 0, 0, 0, + /* type: Struct */ byte(pipewire.SPA_TYPE_Struct), 0, 0, 0, /* size: 4 bytes */ 4, 0, 0, 0, - /* type: Int */ pipewire.SPA_TYPE_Int, 0, 0, 0, + /* type: Int */ byte(pipewire.SPA_TYPE_Int), 0, 0, 0, /* value: -1 */ 0xff, 0xff, 0xff, 0xff, /* padding */ 0, 0, 0, 0, /* size: 4 bytes */ 4, 0, 0, 0, - /* type: Int */ pipewire.SPA_TYPE_Int, 0, 0, 0, + /* type: Int */ byte(pipewire.SPA_TYPE_Int), 0, 0, 0, /* value: 0 */ 0, 0, 0, 0, /* padding */ 0, 0, 0, 0, }, pipewire.CorePong{ diff --git a/internal/pipewire/pod.go b/internal/pipewire/pod.go index 13d2dc2..582d0a1 100644 --- a/internal/pipewire/pod.go +++ b/internal/pipewire/pod.go @@ -150,7 +150,7 @@ func marshalValueAppendRaw(data []byte, v reflect.Value) ([]byte, error) { if v.CanInterface() { switch c := v.Interface().(type) { case Fd: - data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_Fd) + data = SPA_TYPE_Fd.append(data) data = binary.NativeEndian.AppendUint64(data, uint64(c)) return data, nil } @@ -158,22 +158,22 @@ func marshalValueAppendRaw(data []byte, v reflect.Value) ([]byte, error) { switch v.Kind() { case reflect.Uint32: - data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_Id) + data = SPA_TYPE_Id.append(data) data = binary.NativeEndian.AppendUint32(data, Word(v.Uint())) return data, nil case reflect.Int32: - data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_Int) + data = SPA_TYPE_Int.append(data) data = binary.NativeEndian.AppendUint32(data, Word(v.Int())) return data, nil case reflect.Int64: - data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_Long) + data = SPA_TYPE_Long.append(data) data = binary.NativeEndian.AppendUint64(data, uint64(v.Int())) return data, nil case reflect.Struct: - data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_Struct) + data = SPA_TYPE_Struct.append(data) var err error for i := 0; i < v.NumField(); i++ { data, err = marshalValueAppend(data, v.Field(i)) @@ -185,13 +185,13 @@ func marshalValueAppendRaw(data []byte, v reflect.Value) ([]byte, error) { case reflect.Pointer: if v.IsNil() { - data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_None) + data = SPA_TYPE_None.append(data) return data, nil } return marshalValueAppendRaw(data, v.Elem()) case reflect.String: - data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_String) + data = SPA_TYPE_String.append(data) data = append(data, []byte(v.String())...) data = append(data, 0) return data, nil @@ -359,7 +359,7 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error { if len(data) < SizePrefix { return UnexpectedEOFError{} } - switch binary.NativeEndian.Uint32(data[SizeSPrefix:]) { + switch SPAKind(binary.NativeEndian.Uint32(data[SizeSPrefix:])) { case SPA_TYPE_None: v.SetZero() return nil @@ -404,15 +404,15 @@ func (e *InconsistentSizeError) Error() string { // An UnexpectedTypeError describes an unexpected type encountered // in data passed to [Unmarshal]. -type UnexpectedTypeError struct{ Type, Expect Word } +type UnexpectedTypeError struct{ Type, Expect SPAKind } func (u *UnexpectedTypeError) Error() string { - return "unexpected type: " + strconv.Itoa(int(u.Type)) + ", want " + strconv.Itoa(int(u.Expect)) + return "received " + u.Type.String() + " for a value of type " + u.Expect.String() } // unmarshalCheckTypeBounds performs bounds checks on data and validates the type and size prefixes. // An expected size of zero skips further bounds checks. -func unmarshalCheckTypeBounds(data *[]byte, t Word, sizeP *Word) error { +func unmarshalCheckTypeBounds(data *[]byte, t SPAKind, sizeP *Word) error { if len(*data) < SizePrefix { return UnexpectedEOFError{} } @@ -428,7 +428,7 @@ func unmarshalCheckTypeBounds(data *[]byte, t Word, sizeP *Word) error { return UnexpectedEOFError{} } - gotType := binary.NativeEndian.Uint32((*data)[SizeSPrefix:]) + gotType := SPAKind(binary.NativeEndian.Uint32((*data)[SizeSPrefix:])) if gotType != t { return &UnexpectedTypeError{gotType, t} } @@ -491,7 +491,7 @@ func (d *SPADict) Size() Word { // MarshalPOD satisfies [PODMarshaler] as [SPADict] violates the POD type system. func (d *SPADict) MarshalPOD(data []byte) ([]byte, error) { return appendInner(data, func(dataPrefix []byte) (data []byte, err error) { - data = binary.NativeEndian.AppendUint32(dataPrefix, SPA_TYPE_Struct) + data = SPA_TYPE_Struct.append(dataPrefix) if data, err = MarshalAppend(data, Int(len(*d))); err != nil { return } diff --git a/internal/pipewire/spa.go b/internal/pipewire/spa.go index 32d6986..a1014da 100644 --- a/internal/pipewire/spa.go +++ b/internal/pipewire/spa.go @@ -1,10 +1,24 @@ package pipewire +import ( + "encoding/binary" + "fmt" +) + +// A SPAKind describes the kind of data being encoded right after it. +// +// These do not always follow the same rules, and encoding/decoding +// is very much context-dependent. Callers should therefore not +// attempt to use these values directly and rely on [Marshal] and +// [Unmarshal] and their variants instead. +type SPAKind Word + /* Basic types */ const ( /* POD's can contain a number of basic SPA types: */ - SPA_TYPE_START = 0x00000 + iota + SPA_TYPE_START SPAKind = 0x00000 + iota + SPA_TYPE_None // No value or a NULL pointer. SPA_TYPE_Bool // A boolean value. SPA_TYPE_Id // An enumerated value. @@ -35,6 +49,60 @@ const ( _SPA_TYPE_LAST // not part of ABI ) +// append appends the representation of [SPAKind] to data and returns the appended slice. +func (kind SPAKind) append(data []byte) []byte { + return binary.NativeEndian.AppendUint32(data, Word(kind)) +} + +// String returns the name of the [SPAKind] for basic types. +func (kind SPAKind) String() string { + switch kind { + case SPA_TYPE_None: + return "None" + case SPA_TYPE_Bool: + return "Bool" + case SPA_TYPE_Id: + return "Id" + case SPA_TYPE_Int: + return "Int" + case SPA_TYPE_Long: + return "Long" + case SPA_TYPE_Float: + return "Float" + case SPA_TYPE_Double: + return "Double" + case SPA_TYPE_String: + return "String" + case SPA_TYPE_Bytes: + return "Bytes" + case SPA_TYPE_Rectangle: + return "Rectangle" + case SPA_TYPE_Fraction: + return "Fraction" + case SPA_TYPE_Bitmap: + return "Bitmap" + case SPA_TYPE_Array: + return "Array" + case SPA_TYPE_Struct: + return "Struct" + case SPA_TYPE_Object: + return "Object" + case SPA_TYPE_Sequence: + return "Sequence" + case SPA_TYPE_Pointer: + return "Pointer" + case SPA_TYPE_Fd: + return "Fd" + case SPA_TYPE_Choice: + return "Choice" + case SPA_TYPE_Pod: + return "Pod" + + default: + return fmt.Sprintf("invalid type field %#x", Word(kind)) + } +} + /* Pointers */ const ( SPA_TYPE_POINTER_START = 0x10000 + iota diff --git a/internal/pipewire/spa_test.go b/internal/pipewire/spa_test.go index 6bf7794..0a7d79a 100644 --- a/internal/pipewire/spa_test.go +++ b/internal/pipewire/spa_test.go @@ -3,10 +3,32 @@ package pipewire_test import ( _ "embed" "encoding/binary" + "testing" "hakurei.app/internal/pipewire" ) +func TestSPAKind(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + v pipewire.SPAKind + want string + }{ + {"invalid", 0xdeadbeef, "invalid type field 0xdeadbeef"}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if got := tc.v.String(); got != tc.want { + t.Errorf("String: %q, want %q", got, tc.want) + } + }) + } +} + // splitMessages splits concatenated messages into groups of // header, payload, footer of each individual message. // splitMessages panics on any decoding error.