internal/pipewire: improve protocol error messages

These are mostly small formatting changes, with the biggest change being to UnexpectedEOFError where its kind is now described as part of the error type.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-12-04 03:14:37 +09:00
parent 69b1131d66
commit 0d3ae6cb23
2 changed files with 126 additions and 28 deletions

View File

@@ -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
}

View File

@@ -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)