internal/pipewire: improve protocol error messages
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m41s
Test / Sandbox (race detector) (push) Successful in 4m40s
Test / Hakurei (push) Successful in 4m45s
Test / Hpkg (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m13s
Test / Flake checks (push) Successful in 1m27s

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:
Ophestra 2025-12-04 03:14:37 +09:00
parent 69b1131d66
commit 0d3ae6cb23
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
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)