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. // to encode an unsupported value type.
type UnsupportedTypeError struct{ Type reflect.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 // An UnsupportedSizeError is returned by [Marshal] when attempting
// to encode a value with its encoded size exceeding what could be // to encode a value with its encoded size exceeding what could be
// represented by the format. // represented by the format.
type UnsupportedSizeError int 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. // Marshal returns the PipeWire POD encoding of v.
func Marshal(v any) ([]byte, error) { func Marshal(v any) ([]byte, error) {
@@ -220,16 +220,43 @@ func (e *InvalidUnmarshalError) Error() string {
} }
if e.Type.Kind() != reflect.Pointer { 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() return "attempting to unmarshal to nil " + e.Type.String()
} }
// UnexpectedEOFError is returned when EOF was encountered in the middle of decoding POD data. // UnexpectedEOFError describes an unexpected EOF encountered in the middle of decoding POD data.
type UnexpectedEOFError struct{} 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) 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 // 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, // 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 { if n, err := UnmarshalNext(data, v); err != nil {
return err return err
} else if len(data) > int(n) { } else if len(data) > int(n) {
return &TrailingGarbageError{data[int(n):]} return TrailingGarbageError(data[int(n):])
} }
return nil return nil
@@ -261,25 +288,25 @@ func UnmarshalNext(data []byte, v any) (size Word, err error) {
// This is likely an unexported struct field. // This is likely an unexported struct field.
type UnmarshalSetError struct{ Type reflect.Type } 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 // A TrailingGarbageError describes extra bytes after decoding
// has completed during [Unmarshal]. // has completed during [Unmarshal].
type TrailingGarbageError struct{ Data []byte } type TrailingGarbageError []byte
func (e *TrailingGarbageError) Error() string { func (e TrailingGarbageError) Error() string {
if len(e.Data) < SizePrefix { if len(e) < SizePrefix {
return "got " + strconv.Itoa(len(e.Data)) + " bytes of trailing garbage" 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 // A StringTerminationError describes an incorrectly terminated string
// encountered during [Unmarshal]. // encountered during [Unmarshal].
type StringTerminationError struct{ Value byte } type StringTerminationError byte
func (e StringTerminationError) Error() string { 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. // 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 { if len(data) != 0 {
return &TrailingGarbageError{data} return TrailingGarbageError(data)
} }
return nil return nil
case reflect.Pointer: case reflect.Pointer:
if len(data) < SizePrefix { if len(data) < SizePrefix {
return UnexpectedEOFError{} return ErrEOFPrefix
} }
switch SPAKind(binary.NativeEndian.Uint32(data[SizeSPrefix:])) { switch SPAKind(binary.NativeEndian.Uint32(data[SizeSPrefix:])) {
case SPA_TYPE_None: case SPA_TYPE_None:
@@ -378,12 +405,12 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error {
// string size, one extra NUL byte // string size, one extra NUL byte
size := int(*wireSizeP) size := int(*wireSizeP)
if len(data) < size { if len(data) < size {
return UnexpectedEOFError{} return ErrEOFDataString
} }
// the serialised strings still include NUL termination // the serialised strings still include NUL termination
if data[size-1] != 0 { if data[size-1] != 0 {
return StringTerminationError{data[size-1]} return StringTerminationError(data[size-1])
} }
v.SetString(string(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]. // in data passed to [Unmarshal].
type InconsistentSizeError struct{ Prefix, Expect Word } type InconsistentSizeError struct{ Prefix, Expect Word }
func (e *InconsistentSizeError) Error() string { func (e InconsistentSizeError) Error() string {
return "unexpected size prefix: " + strconv.Itoa(int(e.Prefix)) + ", want " + strconv.Itoa(int(e.Expect)) 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 // An UnexpectedTypeError describes an unexpected type encountered
// in data passed to [Unmarshal]. // in data passed to [Unmarshal].
type UnexpectedTypeError struct{ Type, Expect SPAKind } type UnexpectedTypeError struct{ Type, Expect SPAKind }
func (u *UnexpectedTypeError) Error() string { func (e UnexpectedTypeError) Error() string {
return "received " + u.Type.String() + " for a value of type " + u.Expect.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. // unmarshalCheckTypeBounds performs bounds checks on data and validates the type and size prefixes.
// An expected size of zero skips further bounds checks. // An expected size of zero skips further bounds checks.
func unmarshalCheckTypeBounds(data *[]byte, t SPAKind, sizeP *Word) error { func unmarshalCheckTypeBounds(data *[]byte, t SPAKind, sizeP *Word) error {
if len(*data) < SizePrefix { if len(*data) < SizePrefix {
return UnexpectedEOFError{} return ErrEOFPrefix
} }
wantSize := *sizeP wantSize := *sizeP
@@ -422,15 +450,15 @@ func unmarshalCheckTypeBounds(data *[]byte, t SPAKind, sizeP *Word) error {
*sizeP = gotSize *sizeP = gotSize
if wantSize != 0 && gotSize != wantSize { if wantSize != 0 && gotSize != wantSize {
return &InconsistentSizeError{gotSize, wantSize} return InconsistentSizeError{gotSize, wantSize}
} }
if len(*data)-SizePrefix < int(gotSize) { if len(*data)-SizePrefix < int(gotSize) {
return UnexpectedEOFError{} return ErrEOFData
} }
gotType := SPAKind(binary.NativeEndian.Uint32((*data)[SizeSPrefix:])) gotType := SPAKind(binary.NativeEndian.Uint32((*data)[SizeSPrefix:]))
if gotType != t { if gotType != t {
return &UnexpectedTypeError{gotType, t} return UnexpectedTypeError{gotType, t}
} }
*data = (*data)[SizePrefix : gotSize+SizePrefix] *data = (*data)[SizePrefix : gotSize+SizePrefix]
@@ -541,7 +569,7 @@ func (d *SPADict) UnmarshalPOD(data []byte) (Word, error) {
} }
if len(data) != 0 { if len(data) != 0 {
return wireSize, &TrailingGarbageError{data} return wireSize, TrailingGarbageError(data)
} }
return wireSize, nil 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) { var benchmarkSample = func() (sample pipewire.CoreInfo) {
if err := sample.UnmarshalBinary(samplePWContainer[1][0][1]); err != nil { if err := sample.UnmarshalBinary(samplePWContainer[1][0][1]); err != nil {
panic(err) panic(err)