diff --git a/internal/pipewire/pod.go b/internal/pipewire/pod.go index 582d0a1..523322f 100644 --- a/internal/pipewire/pod.go +++ b/internal/pipewire/pod.go @@ -87,14 +87,14 @@ type PODMarshaler interface { // to encode an unsupported value type. type UnsupportedTypeError struct{ Type reflect.Type } -func (e *UnsupportedTypeError) Error() string { return "unsupported type: " + e.Type.String() } +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)) } +func (e UnsupportedSizeError) Error() string { return "size " + strconv.Itoa(int(e)) + " out of range" } // Marshal returns the PipeWire POD encoding of v. func Marshal(v any) ([]byte, error) { @@ -220,16 +220,43 @@ func (e *InvalidUnmarshalError) Error() string { } if e.Type.Kind() != reflect.Pointer { - return "attempting to unmarshal to non-pointer type: " + e.Type.String() + return "attempting to unmarshal to non-pointer type " + e.Type.String() } return "attempting to unmarshal to nil " + e.Type.String() } -// UnexpectedEOFError is returned when EOF was encountered in the middle of decoding POD data. -type UnexpectedEOFError struct{} +// UnexpectedEOFError describes an unexpected EOF encountered in the middle of decoding POD data. +type UnexpectedEOFError uintptr + +const ( + // ErrEOFPrefix is returned when unexpectedly encountering EOF + // decoding the fixed-size POD prefix. + ErrEOFPrefix UnexpectedEOFError = iota + // ErrEOFData is returned when unexpectedly encountering EOF + // establishing POD data bounds. + ErrEOFData + // ErrEOFDataString is returned when unexpectedly encountering EOF + // establishing POD [String] bounds. + ErrEOFDataString +) func (UnexpectedEOFError) Unwrap() error { return io.ErrUnexpectedEOF } -func (UnexpectedEOFError) Error() string { return "unexpected EOF decoding POD data" } +func (e UnexpectedEOFError) Error() string { + var suffix string + switch e { + case ErrEOFPrefix: + suffix = "decoding fixed-size POD prefix" + case ErrEOFData: + suffix = "establishing POD data bounds" + case ErrEOFDataString: + suffix = "establishing POD String bounds" + + default: + return "unexpected EOF" + } + + return "unexpected EOF " + suffix +} // Unmarshal parses the PipeWire POD encoded data and stores the result // in the value pointed to by v. If v is nil or not a pointer, @@ -238,7 +265,7 @@ func Unmarshal(data []byte, v any) error { if n, err := UnmarshalNext(data, v); err != nil { return err } else if len(data) > int(n) { - return &TrailingGarbageError{data[int(n):]} + return TrailingGarbageError(data[int(n):]) } return nil @@ -261,25 +288,25 @@ func UnmarshalNext(data []byte, v any) (size Word, err error) { // This is likely an unexported struct field. type UnmarshalSetError struct{ Type reflect.Type } -func (u *UnmarshalSetError) Error() string { return "cannot set: " + u.Type.String() } +func (u *UnmarshalSetError) Error() string { return "cannot set " + u.Type.String() } // A TrailingGarbageError describes extra bytes after decoding // has completed during [Unmarshal]. -type TrailingGarbageError struct{ Data []byte } +type TrailingGarbageError []byte -func (e *TrailingGarbageError) Error() string { - if len(e.Data) < SizePrefix { - return "got " + strconv.Itoa(len(e.Data)) + " bytes of trailing garbage" +func (e TrailingGarbageError) Error() string { + if len(e) < SizePrefix { + return "got " + strconv.Itoa(len(e)) + " bytes of trailing garbage" } - return "data has extra values starting with type " + strconv.Itoa(int(binary.NativeEndian.Uint32(e.Data[SizeSPrefix:]))) + return "data has extra values starting with " + SPAKind(binary.NativeEndian.Uint32(e[SizeSPrefix:])).String() } // A StringTerminationError describes an incorrectly terminated string // encountered during [Unmarshal]. -type StringTerminationError struct{ Value byte } +type StringTerminationError byte func (e StringTerminationError) Error() string { - return "got byte " + strconv.Itoa(int(e.Value)) + " instead of NUL" + return "got byte " + strconv.Itoa(int(e)) + " instead of NUL" } // unmarshalValue implements [Unmarshal] on [reflect.Value] without compensating for prefix and padding size. @@ -351,13 +378,13 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error { } if len(data) != 0 { - return &TrailingGarbageError{data} + return TrailingGarbageError(data) } return nil case reflect.Pointer: if len(data) < SizePrefix { - return UnexpectedEOFError{} + return ErrEOFPrefix } switch SPAKind(binary.NativeEndian.Uint32(data[SizeSPrefix:])) { case SPA_TYPE_None: @@ -378,12 +405,12 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error { // string size, one extra NUL byte size := int(*wireSizeP) if len(data) < size { - return UnexpectedEOFError{} + return ErrEOFDataString } // the serialised strings still include NUL termination if data[size-1] != 0 { - return StringTerminationError{data[size-1]} + return StringTerminationError(data[size-1]) } v.SetString(string(data[:size-1])) @@ -398,23 +425,24 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error { // 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)) +func (e InconsistentSizeError) Error() string { + return "prefix claims size " + strconv.Itoa(int(e.Prefix)) + + " for a " + strconv.Itoa(int(e.Expect)) + "-byte long segment" } // An UnexpectedTypeError describes an unexpected type encountered // in data passed to [Unmarshal]. type UnexpectedTypeError struct{ Type, Expect SPAKind } -func (u *UnexpectedTypeError) Error() string { - return "received " + u.Type.String() + " for a value of type " + u.Expect.String() +func (e UnexpectedTypeError) Error() string { + return "received " + e.Type.String() + " for a value of type " + e.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 SPAKind, sizeP *Word) error { if len(*data) < SizePrefix { - return UnexpectedEOFError{} + return ErrEOFPrefix } wantSize := *sizeP @@ -422,15 +450,15 @@ func unmarshalCheckTypeBounds(data *[]byte, t SPAKind, sizeP *Word) error { *sizeP = gotSize if wantSize != 0 && gotSize != wantSize { - return &InconsistentSizeError{gotSize, wantSize} + return InconsistentSizeError{gotSize, wantSize} } if len(*data)-SizePrefix < int(gotSize) { - return UnexpectedEOFError{} + return ErrEOFData } gotType := SPAKind(binary.NativeEndian.Uint32((*data)[SizeSPrefix:])) if gotType != t { - return &UnexpectedTypeError{gotType, t} + return UnexpectedTypeError{gotType, t} } *data = (*data)[SizePrefix : gotSize+SizePrefix] @@ -541,7 +569,7 @@ func (d *SPADict) UnmarshalPOD(data []byte) (Word, error) { } if len(data) != 0 { - return wireSize, &TrailingGarbageError{data} + return wireSize, TrailingGarbageError(data) } return wireSize, nil } diff --git a/internal/pipewire/pod_test.go b/internal/pipewire/pod_test.go index 629bedc..3ae1e3e 100644 --- a/internal/pipewire/pod_test.go +++ b/internal/pipewire/pod_test.go @@ -69,6 +69,76 @@ func (testCases encodingTestCases[V, S]) run(t *testing.T) { } } +func TestPODErrors(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + err error + want string + }{ + {"UnsupportedTypeError", &pipewire.UnsupportedTypeError{ + Type: reflect.TypeFor[any](), + }, "unsupported type interface {}"}, + + {"UnsupportedSizeError", pipewire.UnsupportedSizeError(pipewire.SizeMax + 1), "size 16777216 out of range"}, + + {"InvalidUnmarshalError untyped nil", new(pipewire.InvalidUnmarshalError), "attempting to unmarshal to nil"}, + {"InvalidUnmarshalError non-pointer", &pipewire.InvalidUnmarshalError{ + Type: reflect.TypeFor[uintptr](), + }, "attempting to unmarshal to non-pointer type uintptr"}, + {"InvalidUnmarshalError nil", &pipewire.InvalidUnmarshalError{ + Type: reflect.TypeFor[*uintptr](), + }, "attempting to unmarshal to nil *uintptr"}, + + {"UnexpectedEOFError ErrEOFPrefix", pipewire.ErrEOFPrefix, "unexpected EOF decoding fixed-size POD prefix"}, + {"UnexpectedEOFError ErrEOFData", pipewire.ErrEOFData, "unexpected EOF establishing POD data bounds"}, + {"UnexpectedEOFError ErrEOFDataString", pipewire.ErrEOFDataString, "unexpected EOF establishing POD String bounds"}, + {"UnexpectedEOFError invalid", pipewire.UnexpectedEOFError(0xbad), "unexpected EOF"}, + + {"UnmarshalSetError", &pipewire.UnmarshalSetError{ + Type: reflect.TypeFor[*uintptr](), + }, "cannot set *uintptr"}, + + {"TrailingGarbageError short", make(pipewire.TrailingGarbageError, 1<<3-1), "got 7 bytes of trailing garbage"}, + {"TrailingGarbageError String", pipewire.TrailingGarbageError{ + /* size: */ 0, 0, 0, 0, + /* type: */ byte(pipewire.SPA_TYPE_String), 0, 0, 0, + }, "data has extra values starting with String"}, + {"TrailingGarbageError invalid", pipewire.TrailingGarbageError{ + /* size: */ 0, 0, 0, 0, + /* type: */ 0xff, 0xff, 0xff, 0xff, + /* garbage: */ 0, + }, "data has extra values starting with invalid type field 0xffffffff"}, + + {"StringTerminationError", pipewire.StringTerminationError(0xff), "got byte 255 instead of NUL"}, + + {"InconsistentSizeError", pipewire.InconsistentSizeError{ + Prefix: 0xbad, + Expect: 0xff, + }, "prefix claims size 2989 for a 255-byte long segment"}, + + {"UnexpectedTypeError zero", pipewire.UnexpectedTypeError{}, "received invalid type field 0x0 for a value of type invalid type field 0x0"}, + {"UnexpectedTypeError", pipewire.UnexpectedTypeError{ + Type: pipewire.SPA_TYPE_String, + Expect: pipewire.SPA_TYPE_Array, + }, "received String for a value of type Array"}, + {"UnexpectedTypeError invalid", pipewire.UnexpectedTypeError{ + Type: 0xdeadbeef, + Expect: pipewire.SPA_TYPE_Long, + }, "received invalid type field 0xdeadbeef for a value of type Long"}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if got := tc.err.Error(); got != tc.want { + t.Errorf("Error: %q, want %q", got, tc.want) + } + }) + } +} + var benchmarkSample = func() (sample pipewire.CoreInfo) { if err := sample.UnmarshalBinary(samplePWContainer[1][0][1]); err != nil { panic(err)