aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--internal/pipewire/core.go79
-rw-r--r--internal/pipewire/core_test.go22
-rw-r--r--internal/pipewire/header.go6
-rw-r--r--internal/pipewire/pipewire.go66
-rw-r--r--internal/pipewire/pod.go228
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