diff --git a/internal/pipewire/client.go b/internal/pipewire/client.go index 0dce269..0847ac0 100644 --- a/internal/pipewire/client.go +++ b/internal/pipewire/client.go @@ -46,6 +46,14 @@ type ClientInfo struct { Props *SPADict } +// Size satisfies [KnownSize] with a value computed at runtime. +func (c *ClientInfo) Size() Word { + return SizePrefix + + Size(SizeInt) + + Size(SizeLong) + + c.Props.Size() +} + // MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal]. func (c *ClientInfo) MarshalBinary() ([]byte, error) { return Marshal(c) } @@ -58,6 +66,9 @@ type ClientUpdateProperties struct { Props *SPADict } +// Size satisfies [KnownSize] with a value computed at runtime. +func (c *ClientUpdateProperties) Size() Word { return SizePrefix + c.Props.Size() } + // MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal]. func (c *ClientUpdateProperties) MarshalBinary() ([]byte, error) { return Marshal(c) } diff --git a/internal/pipewire/core.go b/internal/pipewire/core.go index 407ea95..1ef2f79 100644 --- a/internal/pipewire/core.go +++ b/internal/pipewire/core.go @@ -102,6 +102,19 @@ type CoreInfo struct { Props *SPADict } +// Size satisfies [KnownSize] with a value computed at runtime. +func (c *CoreInfo) Size() Word { + return SizePrefix + + Size(SizeInt) + + Size(SizeInt) + + SizeString[Word](c.UserName) + + SizeString[Word](c.HostName) + + SizeString[Word](c.Version) + + SizeString[Word](c.Name) + + Size(SizeLong) + + c.Props.Size() +} + // MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal]. func (c *CoreInfo) MarshalBinary() ([]byte, error) { return Marshal(c) } @@ -116,6 +129,9 @@ type CoreDone struct { Sequence Int } +// Size satisfies [KnownSize] with a constant value. +func (c *CoreDone) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) } + // MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal]. func (c *CoreDone) MarshalBinary() ([]byte, error) { return Marshal(c) } @@ -133,6 +149,14 @@ type CoreBoundProps struct { Props *SPADict } +// Size satisfies [KnownSize] with a value computed at runtime. +func (c *CoreBoundProps) Size() Word { + return SizePrefix + + Size(SizeInt) + + Size(SizeInt) + + c.Props.Size() +} + // MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal]. func (c *CoreBoundProps) MarshalBinary() ([]byte, error) { return Marshal(c) } @@ -145,6 +169,9 @@ type CoreHello struct { Version Int } +// Size satisfies [KnownSize] with a constant value. +func (c *CoreHello) Size() Word { return SizePrefix + Size(SizeInt) } + // MarshalBinary satisfies [encoding.BinaryMarshaler] via [MarshalAppend]. func (c *CoreHello) MarshalBinary() ([]byte, error) { return MarshalAppend(make([]byte, 0, 24), c) @@ -168,6 +195,9 @@ type CoreSync struct { Sequence Int } +// Size satisfies [KnownSize] with a constant value. +func (c *CoreSync) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) } + // MarshalBinary satisfies [encoding.BinaryMarshaler] via [MarshalAppend]. func (c *CoreSync) MarshalBinary() ([]byte, error) { return MarshalAppend(make([]byte, 0, 40), c) @@ -191,6 +221,9 @@ type CoreGetRegistry struct { NewID Int } +// Size satisfies [KnownSize] with a constant value. +func (c *CoreGetRegistry) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) } + // MarshalBinary satisfies [encoding.BinaryMarshaler] via [MarshalAppend]. func (c *CoreGetRegistry) MarshalBinary() ([]byte, error) { return MarshalAppend(make([]byte, 0, 40), c) diff --git a/internal/pipewire/pod.go b/internal/pipewire/pod.go index 97ba4d0..80c798e 100644 --- a/internal/pipewire/pod.go +++ b/internal/pipewire/pod.go @@ -44,11 +44,11 @@ const ( SizePrefix = SizeSPrefix + SizeTPrefix // SizeId is the fixed, unpadded size of a [SPA_TYPE_Id] value. - SizeId = 4 + SizeId Word = 4 // SizeInt is the fixed, unpadded size of a [SPA_TYPE_Int] value. - SizeInt = 4 + SizeInt Word = 4 // SizeLong is the fixed, unpadded size of a [SPA_TYPE_Long] value. - SizeLong = 8 + SizeLong Word = 8 ) /* Basic types */ @@ -86,6 +86,24 @@ const ( _SPA_TYPE_LAST // not part of ABI ) +// A KnownSize value has known POD encoded size before serialisation. +type KnownSize interface { + // Size returns the POD encoded size of the receiver. + Size() Word +} + +// PaddingSize returns the padding size corresponding to a wire size. +func PaddingSize[W Word | int](wireSize W) W { return (SizeAlign - (wireSize)%SizeAlign) % SizeAlign } + +// PaddedSize returns the padded size corresponding to a wire size. +func PaddedSize[W Word | int](wireSize W) W { return wireSize + PaddingSize(wireSize) } + +// Size returns prefixed and padded size corresponding to a wire size. +func Size[W Word | int](wireSize W) W { return SizePrefix + PaddedSize(wireSize) } + +// SizeString returns prefixed and padded size corresponding to a string. +func SizeString[W Word | int](s string) W { return Size(W(len(s)) + 1) } + // PODMarshaler is the interface implemented by an object that can // marshal itself into PipeWire POD encoding. type PODMarshaler interface { @@ -125,14 +143,13 @@ func appendInner(data []byte, f func(data []byte) ([]byte, error)) ([]byte, erro } size := len(rData) - len(data) + SizeSPrefix - paddingSize := (SizeAlign - (size)%SizeAlign) % SizeAlign // compensated for size and type prefix wireSize := size - SizePrefix if wireSize > math.MaxUint32 { return data, UnsupportedSizeError(wireSize) } binary.NativeEndian.PutUint32(rData[len(data)-SizeSPrefix:len(data)], Word(wireSize)) - rData = append(rData, make([]byte, paddingSize)...) + rData = append(rData, make([]byte, PaddingSize(size))...) return rData, nil } @@ -242,7 +259,7 @@ func UnmarshalNext(data []byte, v any) (size Word, err error) { } err = unmarshalValue(data, rv.Elem(), &size) // prefix and padding size - size += SizePrefix + (SizeAlign-(size)%SizeAlign)%SizeAlign + size = Size(size) return } @@ -325,9 +342,8 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error { if err := unmarshalValue(data, v.Field(i), &fieldWireSize); err != nil { return err } - paddingSize := (SizeAlign - (fieldWireSize)%SizeAlign) % SizeAlign // bounds check completed in successful call to unmarshalValue - data = data[SizePrefix+fieldWireSize+paddingSize:] + data = data[Size(fieldWireSize):] } if len(data) != 0 { @@ -441,6 +457,17 @@ type SPADict struct { Items []SPADictItem } +// Size satisfies [KnownSize] with a value computed at runtime. +func (d *SPADict) Size() Word { + // struct prefix, NItems value + size := SizePrefix + int(Size(SizeInt)) + for i := range d.Items { + size += SizeString[int](d.Items[i].Key) + size += SizeString[int](d.Items[i].Value) + } + return Word(size) +} + // MarshalPOD satisfies [PODMarshaler] as [SPADict] violates the POD type system. func (d *SPADict) MarshalPOD() ([]byte, error) { return appendInner(nil, func(dataPrefix []byte) (data []byte, err error) { diff --git a/internal/pipewire/pod_test.go b/internal/pipewire/pod_test.go index a39f961..28e789f 100644 --- a/internal/pipewire/pod_test.go +++ b/internal/pipewire/pod_test.go @@ -5,11 +5,14 @@ import ( "encoding/json" "reflect" "testing" + + "hakurei.app/internal/pipewire" ) type encodingTestCases[V any, S interface { encoding.BinaryMarshaler encoding.BinaryUnmarshaler + *V }] []struct { // Uninterpreted name of subtest. @@ -51,6 +54,14 @@ func (testCases encodingTestCases[V, S]) run(t *testing.T) { t.Fatalf("MarshalBinary: %#v, want %#v", gotData, tc.wantData) } }) + + if s, ok := any(&tc.value).(pipewire.KnownSize); ok { + t.Run("size", func(t *testing.T) { + if got := int(s.Size()); got != len(tc.wantData) { + t.Errorf("Size: %d, want %d", got, len(tc.wantData)) + } + }) + } }) } }