diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-11-24 23:29:06 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-11-25 01:20:30 +0900 |
| commit | 5bcafcf73421cfdd8c80a0a0daec9ac09d8784e2 (patch) | |
| tree | 25b72f78f9e2d0a4aa42ca691f08dcdc5bed078b | |
| parent | 9f7b0c2f46305f8a321791d6fcedc3df7f966783 (diff) | |
internal/pipewire: implement Core::Hello
This implements enough types to correctly marshal and unmarshal Core::Hello.
Signed-off-by: Ophestra <cat@gensokyo.uk>
| -rw-r--r-- | internal/pipewire/core.go | 79 | ||||
| -rw-r--r-- | internal/pipewire/core_test.go | 22 | ||||
| -rw-r--r-- | internal/pipewire/header.go | 6 | ||||
| -rw-r--r-- | internal/pipewire/pipewire.go | 66 | ||||
| -rw-r--r-- | internal/pipewire/pod.go | 228 |
5 files changed, 329 insertions, 72 deletions
diff --git a/internal/pipewire/core.go b/internal/pipewire/core.go new file mode 100644 index 00000000..a833009f --- /dev/null +++ b/internal/pipewire/core.go @@ -0,0 +1,79 @@ +package pipewire + +/* 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 = Word(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 +) + +// CoreHello is the first message sent by a client. +type CoreHello struct { + // version number of the client; PW_VERSION_CORE + Version Int +} + +// MarshalBinary satisfies [encoding.BinaryMarshaler] via [MarshalAppend]. +func (c *CoreHello) MarshalBinary() ([]byte, error) { return MarshalAppend(make([]byte, 0, 24), c) } + +// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal]. +func (c *CoreHello) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) } diff --git a/internal/pipewire/core_test.go b/internal/pipewire/core_test.go new file mode 100644 index 00000000..c271feaf --- /dev/null +++ b/internal/pipewire/core_test.go @@ -0,0 +1,22 @@ +package pipewire_test + +import ( + "testing" + + "hakurei.app/internal/pipewire" +) + +func TestCoreHello(t *testing.T) { + encodingTestCases[pipewire.CoreHello, *pipewire.CoreHello]{ + {"sample", []byte{ + 0x10, 0, 0, 0, + 0xe, 0, 0, 0, + 4, 0, 0, 0, + 4, 0, 0, 0, + 4, 0, 0, 0, + 0, 0, 0, 0, + }, pipewire.CoreHello{ + Version: pipewire.PW_VERSION_CORE, + }, nil}, + }.run(t) +} diff --git a/internal/pipewire/header.go b/internal/pipewire/header.go index 99632208..3ab22d49 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 Uint `json:"Id"` + ID Word `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 Uint `json:"seq"` + Sequence Word `json:"seq"` // Number of file descriptors in this message. - FileCount Uint `json:"n_fds"` + FileCount Word `json:"n_fds"` } // append appends the protocol native message header to data. diff --git a/internal/pipewire/pipewire.go b/internal/pipewire/pipewire.go index 97eaba50..4b03a543 100644 --- a/internal/pipewire/pipewire.go +++ b/internal/pipewire/pipewire.go @@ -82,72 +82,6 @@ const ( 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 ( diff --git a/internal/pipewire/pod.go b/internal/pipewire/pod.go index 7c01543d..e81d81bc 100644 --- a/internal/pipewire/pod.go +++ b/internal/pipewire/pod.go @@ -1,15 +1,33 @@ package pipewire +import ( + "encoding/binary" + "io" + "math" + "reflect" + "strconv" +) + 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. + // A Bool is a boolean value representing SPA_TYPE_Bool. + Bool = bool + // An Int is a signed integer value representing SPA_TYPE_Int. Int = int32 - // An Uint is an unsigned integer the size of a PipeWire Word. - Uint = Word + // A Long is a signed integer value representing SPA_TYPE_Long. + Long = int64 + // A Float is a floating point value representing SPA_TYPE_Float. + Float = float32 + // A Double is a floating point value representing SPA_TYPE_Double. + Double = float64 + // A String is a string value representing SPA_TYPE_String. + String = string + // Bytes is a byte slice representing SPA_TYPE_Bytes. + Bytes = []byte ) /* Basic types */ @@ -47,6 +65,210 @@ const ( _SPA_TYPE_LAST // not part of ABI ) +// An UnsupportedTypeError is returned by [Marshal] when attempting +// to encode an unsupported value type. +type UnsupportedTypeError struct{ Type reflect.Type } + +func (e *UnsupportedTypeError) Error() string { return "unsupported type: " + e.Type.String() } + +// An UnsupportedSizeError is returned by [Marshal] when attempting +// to encode a value with its encoded size exceeding what could be +// represented by the format. +type UnsupportedSizeError int + +func (e UnsupportedSizeError) Error() string { return "size out of range: " + strconv.Itoa(int(e)) } + +// Marshal returns the PipeWire POD encoding of v. +func Marshal(v any) ([]byte, error) { return MarshalAppend(make([]byte, 0), v) } + +// MarshalAppend appends the PipeWire POD encoding of v to data. +func MarshalAppend(data []byte, v any) ([]byte, error) { + return marshalValueAppend(data, reflect.ValueOf(v)) +} + +// marshalValueAppendRaw implements [MarshalAppend] on [reflect.Value]. +func marshalValueAppend(data []byte, v reflect.Value) ([]byte, error) { + data = append(data, make([]byte, 4)...) + + rData, err := marshalValueAppendRaw(data, v) + if err != nil { + return data, err + } + + size := len(rData) - len(data) + 4 + paddingSize := (8 - (size)%8) % 8 + // compensated for size and type prefix + wireSize := size - 8 + if wireSize > math.MaxUint32 { + return data, UnsupportedSizeError(wireSize) + } + binary.NativeEndian.PutUint32(rData[len(data)-4:len(data)], Word(wireSize)) + rData = append(rData, make([]byte, paddingSize)...) + + return rData, nil +} + +// marshalValueAppendRaw implements [MarshalAppend] on [reflect.Value] without the size prefix. +func marshalValueAppendRaw(data []byte, v reflect.Value) ([]byte, error) { + switch v.Kind() { + + case reflect.Int32: + data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_Int) + data = binary.NativeEndian.AppendUint32(data, Word(v.Int())) + return data, nil + + case reflect.Struct: + data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_Struct) + var err error + for i := 0; i < v.NumField(); i++ { + data, err = marshalValueAppend(data, v.Field(i)) + if err != nil { + return data, err + } + } + return data, nil + + case reflect.Pointer: + if v.IsNil() { + data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_None) + return data, nil + } + return marshalValueAppendRaw(data, v.Elem()) + + default: + return data, &UnsupportedTypeError{v.Type()} + } +} + +// An InvalidUnmarshalError describes an invalid argument passed to [Unmarshal]. +// (The argument to [Unmarshal] must be a non-nil pointer.) +type InvalidUnmarshalError struct{ Type reflect.Type } + +func (e *InvalidUnmarshalError) Error() string { + if e.Type == nil { + return "attempting to unmarshal to nil" + } + + if e.Type.Kind() != reflect.Pointer { + return "attempting to unmarshal to non-pointer type: " + e.Type.String() + } + return "attempting to unmarshal to nil " + e.Type.String() +} + +// Unmarshal parses the JSON-encoded data and stores the result +// in the value pointed to by v. If v is nil or not a pointer, +// Unmarshal returns an [InvalidUnmarshalError]. +func Unmarshal(data []byte, v any) error { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Pointer || rv.IsNil() { + return &InvalidUnmarshalError{reflect.TypeOf(v)} + } + return unmarshalValue(data, rv.Elem(), new(Word)) +} + +// UnmarshalSetError describes a value that cannot be set during [Unmarshal]. +// This is likely an unexported struct field. +type UnmarshalSetError struct{ Type reflect.Type } + +func (u *UnmarshalSetError) Error() string { return "cannot set: " + u.Type.String() } + +// unmarshalValue implements [Unmarshal] on [reflect.Value]. +func unmarshalValue(data []byte, v reflect.Value, sizeP *Word) error { + switch v.Kind() { + + case reflect.Int32: + *sizeP = 4 + if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Int, sizeP); err != nil { + return err + } + if !v.CanSet() { + return &UnmarshalSetError{v.Type()} + } + v.SetInt(int64(binary.NativeEndian.Uint32(data))) + return nil + + case reflect.Struct: + if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Struct, sizeP); err != nil { + return err + } + + var fieldWireSize Word + for i := 0; i < v.NumField(); i++ { + if err := unmarshalValue(data, v.Field(i), &fieldWireSize); err != nil { + return err + } + paddingSize := (8 - (fieldWireSize)%8) % 8 + // already bounds checked by the successful unmarshalValue call + data = data[8+fieldWireSize+paddingSize:] + } + return nil + + case reflect.Pointer: + if !v.CanSet() { + return &UnmarshalSetError{v.Type()} + } + + if len(data) < 8 { + return io.ErrUnexpectedEOF + } + switch binary.NativeEndian.Uint32(data[4:]) { + case SPA_TYPE_None: + v.SetZero() + return nil + + default: + v.Set(reflect.New(v.Type().Elem())) + return unmarshalValue(data, v.Elem(), sizeP) + } + + default: + return &UnsupportedTypeError{v.Type()} + } +} + +// An InconsistentSizeError describes an inconsistent size prefix encountered +// in data passed to [Unmarshal]. +type InconsistentSizeError struct{ Prefix, Expect Word } + +func (e *InconsistentSizeError) Error() string { + return "unexpected size prefix: " + strconv.Itoa(int(e.Prefix)) + ", want " + strconv.Itoa(int(e.Expect)) +} + +// An UnexpectedTypeError describes an unexpected type encountered +// in data passed to [Unmarshal]. +type UnexpectedTypeError struct{ Type, Expect Word } + +func (u *UnexpectedTypeError) Error() string { + return "unexpected type: " + strconv.Itoa(int(u.Type)) + ", want " + strconv.Itoa(int(u.Expect)) +} + +// 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 { + if len(*data) < 8 { + return io.ErrUnexpectedEOF + } + + wantSize := *sizeP + gotSize := binary.NativeEndian.Uint32(*data) + *sizeP = gotSize + + if wantSize != 0 && gotSize != wantSize { + return &InconsistentSizeError{gotSize, wantSize} + } + if len(*data)-8 < int(wantSize) { + return io.ErrUnexpectedEOF + } + + gotType := binary.NativeEndian.Uint32((*data)[4:]) + if gotType != t { + return &UnexpectedTypeError{gotType, t} + } + + *data = (*data)[8:] + return nil +} + /* Pointers */ const ( SPA_TYPE_POINTER_START = 0x10000 + iota |
