From bf9f0b268d2c741b5a4f1d91877856df255a66bf Mon Sep 17 00:00:00 2001 From: Ben Luddy Date: Tue, 2 Apr 2024 14:51:51 -0400 Subject: [PATCH] Allow rejection of NaN and Inf float values on encode and decode. Signed-off-by: Ben Luddy --- decode.go | 115 ++++++++++++ decode_test.go | 484 +++++++++++++++++++++++++++++++++++++++++++++++++ encode.go | 14 +- encode_test.go | 46 ++++- 4 files changed, 649 insertions(+), 10 deletions(-) diff --git a/decode.go b/decode.go index 0c8b398c..7942ba9a 100644 --- a/decode.go +++ b/decode.go @@ -208,6 +208,18 @@ func (e *UnknownFieldError) Error() string { return fmt.Sprintf("cbor: found unknown field at map element index %d", e.Index) } +// UnacceptableDataItemError is returned when unmarshaling a CBOR input that contains a data item +// that is not acceptable to a specific CBOR-based application protocol ("invalid or unexpected" as +// described in RFC 8949 Section 5 Paragraph 3). +type UnacceptableDataItemError struct { + CBORType string + Message string +} + +func (e UnacceptableDataItemError) Error() string { + return fmt.Sprintf("cbor: data item of cbor type %s is not accepted by protocol: %s", e.CBORType, e.Message) +} + // DupMapKeyMode specifies how to enforce duplicate map key. Two map keys are considered duplicates if: // 1. When decoding into a struct, both keys match the same struct field. The keys are also // considered duplicates if neither matches any field and decoding to interface{} would produce @@ -496,6 +508,43 @@ func (tttam TimeTagToAnyMode) valid() bool { return tttam >= 0 && tttam < maxTimeTagToAnyMode } +// NaNDecodeMode specifies how to decode floating-point values (major type 7, additional information +// 25 through 27) representing NaN (not-a-number). +type NaNDecodeMode int + +const ( + // NaNDecodeAccept will decode NaN values to Go float32 or float64. + NaNDecodeAccept NaNDecodeMode = iota + + // NaNDecodeReject will return an UnacceptableDataItemError on an attempt to decode a NaN value. + NaNDecodeReject + + maxNaNDecode +) + +func (ndm NaNDecodeMode) valid() bool { + return ndm >= 0 && ndm < maxNaNDecode +} + +// InfDecodeMode specifies how to decode floating-point values (major type 7, additional information +// 25 through 27) representing positive or negative infinity. +type InfDecodeMode int + +const ( + // InfDecodeAccept will decode infinite values to Go float32 or float64. + InfDecodeAccept InfDecodeMode = iota + + // InfDecodeReject will return an UnacceptableDataItemError on an attempt to decode an + // infinite value. + InfDecodeReject + + maxInfDecode +) + +func (idm InfDecodeMode) valid() bool { + return idm >= 0 && idm < maxInfDecode +} + // DecOptions specifies decoding options. type DecOptions struct { // DupMapKey specifies whether to enforce duplicate map key. @@ -592,6 +641,14 @@ type DecOptions struct { // TimeTagToAnyMode specifies how to decode CBOR tag 0 and 1 into an empty interface (any). // Based on the specified mode, Unmarshal can return a time.Time value or a time string in a specific format. TimeTagToAny TimeTagToAnyMode + + // NaNDec specifies how to decode floating-point values (major type 7, additional + // information 25 through 27) representing NaN (not-a-number). + NaNDec NaNDecodeMode + + // InfDec specifies how to decode floating-point values (major type 7, additional + // information 25 through 27) representing positive or negative infinity. + InfDec InfDecodeMode } // DecMode returns DecMode with immutable options and no tags (safe for concurrency). @@ -661,6 +718,7 @@ const ( maxMaxNestedLevels = 65535 ) +//nolint:gocyclo // Each option comes with some manageable boilerplate func (opts DecOptions) decMode() (*decMode, error) { if !opts.DupMapKey.valid() { return nil, errors.New("cbor: invalid DupMapKey " + strconv.Itoa(int(opts.DupMapKey))) @@ -749,6 +807,14 @@ func (opts DecOptions) decMode() (*decMode, error) { return nil, errors.New("cbor: invalid TimeTagToAny " + strconv.Itoa(int(opts.TimeTagToAny))) } + if !opts.NaNDec.valid() { + return nil, errors.New("cbor: invalid NaNDec " + strconv.Itoa(int(opts.NaNDec))) + } + + if !opts.InfDec.valid() { + return nil, errors.New("cbor: invalid InfDec " + strconv.Itoa(int(opts.InfDec))) + } + dm := decMode{ dupMapKey: opts.DupMapKey, timeTag: opts.TimeTag, @@ -769,6 +835,8 @@ func (opts DecOptions) decMode() (*decMode, error) { fieldNameByteString: opts.FieldNameByteString, unrecognizedTagToAny: opts.UnrecognizedTagToAny, timeTagToAny: opts.TimeTagToAny, + nanDec: opts.NaNDec, + infDec: opts.InfDec, } return &dm, nil @@ -841,6 +909,8 @@ type decMode struct { fieldNameByteString FieldNameByteStringMode unrecognizedTagToAny UnrecognizedTagToAnyMode timeTagToAny TimeTagToAnyMode + nanDec NaNDecodeMode + infDec InfDecodeMode } var defaultDecMode, _ = DecOptions{}.decMode() @@ -867,6 +937,8 @@ func (dm *decMode) DecOptions() DecOptions { FieldNameByteString: dm.fieldNameByteString, UnrecognizedTagToAny: dm.unrecognizedTagToAny, TimeTagToAny: dm.timeTagToAny, + NaNDec: dm.nanDec, + InfDec: dm.infDec, } } @@ -1181,12 +1253,21 @@ func (d *decoder) parseToValue(v reflect.Value, tInfo *typeInfo) error { //nolin switch ai { case 25: f := float64(float16.Frombits(uint16(val)).Float32()) + if err := d.acceptableFloat(f); err != nil { + return err + } return fillFloat(t, f, v) case 26: f := float64(math.Float32frombits(uint32(val))) + if err := d.acceptableFloat(f); err != nil { + return err + } return fillFloat(t, f, v) case 27: f := math.Float64frombits(val) + if err := d.acceptableFloat(f); err != nil { + return err + } return fillFloat(t, f, v) default: // ai <= 24 switch ai { @@ -1373,10 +1454,19 @@ func (d *decoder) parseToTime() (time.Time, bool, error) { switch ai { case 25: f = float64(float16.Frombits(uint16(val)).Float32()) + if err := d.acceptableFloat(f); err != nil { + return time.Time{}, false, err + } case 26: f = float64(math.Float32frombits(uint32(val))) + if err := d.acceptableFloat(f); err != nil { + return time.Time{}, false, err + } case 27: f = math.Float64frombits(val) + if err := d.acceptableFloat(f); err != nil { + return time.Time{}, false, err + } default: return time.Time{}, false, &UnmarshalTypeError{CBORType: t.String(), GoType: typeTime.String()} } @@ -1617,12 +1707,21 @@ func (d *decoder) parse(skipSelfDescribedTag bool) (interface{}, error) { //noli return nil, nil case 25: f := float64(float16.Frombits(uint16(val)).Float32()) + if err := d.acceptableFloat(f); err != nil { + return nil, err + } return f, nil case 26: f := float64(math.Float32frombits(uint32(val))) + if err := d.acceptableFloat(f); err != nil { + return nil, err + } return f, nil case 27: f := math.Float64frombits(val) + if err := d.acceptableFloat(f); err != nil { + return nil, err + } return f, nil } case cborTypeArray: @@ -1641,6 +1740,22 @@ func (d *decoder) parse(skipSelfDescribedTag bool) (interface{}, error) { //noli return nil, nil } +func (d *decoder) acceptableFloat(f float64) error { + switch { + case d.dm.nanDec == NaNDecodeReject && math.IsNaN(f): + return &UnacceptableDataItemError{ + CBORType: cborTypePrimitives.String(), + Message: "floating-point NaN", + } + case d.dm.infDec == InfDecodeReject && math.IsInf(f, 0): + return &UnacceptableDataItemError{ + CBORType: cborTypePrimitives.String(), + Message: "floating-point infinity", + } + } + return nil +} + // parseByteString parses a CBOR encoded byte string. The returned byte slice // may be backed directly by the input. The second return value will be true if // and only if the slice is backed by a copy of the input. Callers are diff --git a/decode_test.go b/decode_test.go index 9bc4c881..e55b7502 100644 --- a/decode_test.go +++ b/decode_test.go @@ -4913,6 +4913,8 @@ func TestDecOptions(t *testing.T) { FieldNameByteString: FieldNameByteStringAllowed, UnrecognizedTagToAny: UnrecognizedTagContentToAny, TimeTagToAny: TimeTagToRFC3339, + NaNDec: NaNDecodeReject, + InfDec: InfDecodeReject, } ov := reflect.ValueOf(opts1) for i := 0; i < ov.NumField(); i++ { @@ -8748,3 +8750,485 @@ func TestDecModeTimeTagToAny(t *testing.T) { }) } } + +func TestDecModeInvalidNaNDec(t *testing.T) { + for _, tc := range []struct { + name string + opts DecOptions + wantErrorMsg string + }{ + { + name: "below range of valid modes", + opts: DecOptions{NaNDec: -1}, + wantErrorMsg: "cbor: invalid NaNDec -1", + }, + { + name: "above range of valid modes", + opts: DecOptions{NaNDec: 101}, + wantErrorMsg: "cbor: invalid NaNDec 101", + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.opts.DecMode() + if err == nil { + t.Errorf("DecMode() didn't return an error") + } else if err.Error() != tc.wantErrorMsg { + t.Errorf("DecMode() returned error %q, want %q", err.Error(), tc.wantErrorMsg) + } + }) + } +} + +func TestNaNDecMode(t *testing.T) { + for _, tc := range []struct { + name string + opt NaNDecodeMode + src []byte + dst interface{} + reject bool + }{ + { + opt: NaNDecodeReject, + src: hexDecode("f90000"), // 0.0 + dst: new(interface{}), + reject: false, + }, + { + opt: NaNDecodeReject, + src: hexDecode("f90000"), // 0.0 + dst: new(float32), + reject: false, + }, + { + opt: NaNDecodeReject, + src: hexDecode("f90000"), // 0.0 + dst: new(float64), + reject: false, + }, + { + opt: NaNDecodeReject, + src: hexDecode("f90000"), // 0.0 + dst: new(time.Time), + reject: false, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fa47c35000"), // 100000.0 + dst: new(interface{}), + reject: false, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fa47c35000"), // 100000.0 + dst: new(float32), + reject: false, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fa47c35000"), // 100000.0 + dst: new(float64), + reject: false, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fa47c35000"), // 100000.0 + dst: new(time.Time), + reject: false, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fb3ff199999999999a"), // 1.1 + dst: new(interface{}), + reject: false, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fb3ff199999999999a"), // 1.1 + dst: new(float32), + reject: false, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fb3ff199999999999a"), // 1.1 + dst: new(float64), + reject: false, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fb3ff199999999999a"), // 1.1 + dst: new(time.Time), + reject: false, + }, + { + opt: NaNDecodeReject, + src: hexDecode("f97e00"), + dst: new(interface{}), + reject: true, + }, + { + opt: NaNDecodeReject, + src: hexDecode("f97e00"), + dst: new(float32), + reject: true, + }, + { + opt: NaNDecodeReject, + src: hexDecode("f97e00"), + dst: new(float64), + reject: true, + }, + { + opt: NaNDecodeReject, + src: hexDecode("f97e00"), + dst: new(time.Time), + reject: true, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fa7fc00000"), + dst: new(interface{}), + reject: true, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fa7fc00000"), + dst: new(float32), + reject: true, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fa7fc00000"), + dst: new(float64), + reject: true, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fa7fc00000"), + dst: new(time.Time), + reject: true, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fb7ff8000000000000"), + dst: new(interface{}), + reject: true, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fb7ff8000000000000"), + dst: new(float32), + reject: true, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fb7ff8000000000000"), + dst: new(float64), + reject: true, + }, + { + opt: NaNDecodeReject, + src: hexDecode("fb7ff8000000000000"), + dst: new(time.Time), + reject: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + dm, err := DecOptions{NaNDec: tc.opt}.DecMode() + if err != nil { + t.Fatal(err) + } + want := &UnacceptableDataItemError{ + CBORType: cborTypePrimitives.String(), + Message: "floating-point NaN", + } + if got := dm.Unmarshal(tc.src, tc.dst); got != nil { + if tc.reject { + if !reflect.DeepEqual(want, got) { + t.Errorf("want error: %v, got error: %v", want, got) + } + } else { + t.Errorf("unexpected error: %v", err) + } + } else if tc.reject { + t.Error("unexpected nil error") + } + }) + } +} + +func TestDecModeInvalidInfDec(t *testing.T) { + for _, tc := range []struct { + name string + opts DecOptions + wantErrorMsg string + }{ + { + name: "below range of valid modes", + opts: DecOptions{InfDec: -1}, + wantErrorMsg: "cbor: invalid InfDec -1", + }, + { + name: "above range of valid modes", + opts: DecOptions{InfDec: 101}, + wantErrorMsg: "cbor: invalid InfDec 101", + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.opts.DecMode() + if err == nil { + t.Errorf("DecMode() didn't return an error") + } else if err.Error() != tc.wantErrorMsg { + t.Errorf("DecMode() returned error %q, want %q", err.Error(), tc.wantErrorMsg) + } + }) + } +} + +func TestInfDecMode(t *testing.T) { + for _, tc := range []struct { + name string + opt InfDecodeMode + src []byte + dst interface{} + reject bool + }{ + { + opt: InfDecodeReject, + src: hexDecode("f90000"), // 0.0 + dst: new(interface{}), + reject: false, + }, + { + opt: InfDecodeReject, + src: hexDecode("f90000"), // 0.0 + dst: new(float32), + reject: false, + }, + { + opt: InfDecodeReject, + src: hexDecode("f90000"), // 0.0 + dst: new(float64), + reject: false, + }, + { + opt: InfDecodeReject, + src: hexDecode("f90000"), // 0.0 + dst: new(time.Time), + reject: false, + }, + { + opt: InfDecodeReject, + src: hexDecode("fa47c35000"), // 100000.0 + dst: new(interface{}), + reject: false, + }, + { + opt: InfDecodeReject, + src: hexDecode("fa47c35000"), // 100000.0 + dst: new(float32), + reject: false, + }, + { + opt: InfDecodeReject, + src: hexDecode("fa47c35000"), // 100000.0 + dst: new(float64), + reject: false, + }, + { + opt: InfDecodeReject, + src: hexDecode("fa47c35000"), // 100000.0 + dst: new(time.Time), + reject: false, + }, + { + opt: InfDecodeReject, + src: hexDecode("fb3ff199999999999a"), // 1.1 + dst: new(interface{}), + reject: false, + }, + { + opt: InfDecodeReject, + src: hexDecode("fb3ff199999999999a"), // 1.1 + dst: new(float32), + reject: false, + }, + { + opt: InfDecodeReject, + src: hexDecode("fb3ff199999999999a"), // 1.1 + dst: new(float64), + reject: false, + }, + { + opt: InfDecodeReject, + src: hexDecode("fb3ff199999999999a"), // 1.1 + dst: new(time.Time), + reject: false, + }, + { + opt: InfDecodeReject, + src: hexDecode("f97c00"), // Infinity + dst: new(interface{}), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("f97c00"), // Infinity + dst: new(float32), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("f97c00"), // Infinity + dst: new(float64), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("f97c00"), // Infinity + dst: new(time.Time), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("f9fc00"), // -Infinity + dst: new(interface{}), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("f9fc00"), // -Infinity + dst: new(float32), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("f9fc00"), // -Infinity + dst: new(float64), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("f9fc00"), // -Infinity + dst: new(time.Time), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("fa7f800000"), // Infinity + dst: new(interface{}), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("fa7f800000"), // Infinity + dst: new(float32), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("fa7f800000"), // Infinity + dst: new(float64), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("fa7f800000"), // Infinity + dst: new(time.Time), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("faff800000"), // -Infinity + dst: new(interface{}), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("faff800000"), // -Infinity + dst: new(float32), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("faff800000"), // -Infinity + dst: new(float64), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("faff800000"), // -Infinity + dst: new(time.Time), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("fb7ff0000000000000"), // Infinity + dst: new(interface{}), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("fb7ff0000000000000"), // Infinity + dst: new(float32), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("fb7ff0000000000000"), // Infinity + dst: new(float64), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("fb7ff0000000000000"), // Infinity + dst: new(time.Time), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("fbfff0000000000000"), // -Infinity + dst: new(interface{}), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("fbfff0000000000000"), // -Infinity + dst: new(float32), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("fbfff0000000000000"), // -Infinity + dst: new(float64), + reject: true, + }, + { + opt: InfDecodeReject, + src: hexDecode("fbfff0000000000000"), // -Infinity + dst: new(time.Time), + reject: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + dm, err := DecOptions{InfDec: tc.opt}.DecMode() + if err != nil { + t.Fatal(err) + } + want := &UnacceptableDataItemError{ + CBORType: cborTypePrimitives.String(), + Message: "floating-point infinity", + } + if got := dm.Unmarshal(tc.src, tc.dst); got != nil { + if tc.reject { + if !reflect.DeepEqual(want, got) { + t.Errorf("want error: %v, got error: %v", want, got) + } + } else { + t.Errorf("unexpected error: %v", err) + } + } else if tc.reject { + t.Error("unexpected nil error") + } + }) + } +} diff --git a/encode.go b/encode.go index f04559fe..d716d77b 100644 --- a/encode.go +++ b/encode.go @@ -240,6 +240,9 @@ const ( // NaN payload. NaNConvertQuiet + // NaNConvertReject returns UnsupportedValueError on attempts to encode a NaN value. + NaNConvertReject + maxNaNConvert ) @@ -258,6 +261,9 @@ const ( // InfConvertNone never converts (used by CTAP2 Canonical CBOR). InfConvertNone + // InfConvertReject returns UnsupportedValueError on attempts to encode an infinite value. + InfConvertReject + maxInfConvert ) @@ -908,7 +914,10 @@ func encodeFloat(e *encoderBuffer, em *encMode, v reflect.Value) error { func encodeInf(e *encoderBuffer, em *encMode, v reflect.Value) error { f64 := v.Float() - if em.infConvert == InfConvertFloat16 { + switch em.infConvert { + case InfConvertReject: + return &UnsupportedValueError{msg: "floating-point infinity"} + case InfConvertFloat16: if f64 > 0 { e.Write(cborPositiveInfinity) } else { @@ -935,6 +944,9 @@ func encodeNaN(e *encoderBuffer, em *encMode, v reflect.Value) error { f32 := float32NaNFromReflectValue(v) return encodeFloat32(e, f32) + case NaNConvertReject: + return &UnsupportedValueError{msg: "floating-point NaN"} + default: // NaNConvertPreserveSignal, NaNConvertQuiet if v.Kind() == reflect.Float64 { f64 := v.Float() diff --git a/encode_test.go b/encode_test.go index 2ed416bd..5eb4f6e4 100644 --- a/encode_test.go +++ b/encode_test.go @@ -3004,7 +3004,7 @@ func TestInfConvert(t *testing.T) { t.Run(tc.name, func(t *testing.T) { em, err := tc.opts.EncMode() if err != nil { - t.Errorf("EncMode() returned an error %v", err) + t.Fatalf("EncMode() returned an error %v", err) } b, err := em.Marshal(tc.v) if err != nil { @@ -3013,6 +3013,23 @@ func TestInfConvert(t *testing.T) { t.Errorf("Marshal(%v) = 0x%x, want 0x%x", tc.v, b, tc.wantCborData) } }) + var vName string + switch v := tc.v.(type) { + case float32: + vName = fmt.Sprintf("0x%x", math.Float32bits(v)) + case float64: + vName = fmt.Sprintf("0x%x", math.Float64bits(v)) + } + t.Run("reject inf "+vName, func(t *testing.T) { + em, err := EncOptions{InfConvert: InfConvertReject}.EncMode() + if err != nil { + t.Fatalf("EncMode() returned an error %v", err) + } + want := &UnsupportedValueError{msg: "floating-point infinity"} + if _, got := em.Marshal(tc.v); !reflect.DeepEqual(want, got) { + t.Errorf("expected Marshal(%v) to return error: %v, got: %v", tc.v, want, got) + } + }) } } @@ -3318,6 +3335,13 @@ func TestNaNConvert(t *testing.T) { }}, } for _, tc := range testCases { + var vName string + switch v := tc.v.(type) { + case float32: + vName = fmt.Sprintf("0x%x", math.Float32bits(v)) + case float64: + vName = fmt.Sprintf("0x%x", math.Float64bits(v)) + } for _, convert := range tc.convert { var convertName string switch convert.opt.NaNConvert { @@ -3330,18 +3354,11 @@ func TestNaNConvert(t *testing.T) { case NaNConvertQuiet: convertName = "ConvertQuiet" } - var vName string - switch v := tc.v.(type) { - case float32: - vName = fmt.Sprintf("0x%x", math.Float32bits(v)) - case float64: - vName = fmt.Sprintf("0x%x", math.Float64bits(v)) - } name := convertName + "_" + vName t.Run(name, func(t *testing.T) { em, err := convert.opt.EncMode() if err != nil { - t.Errorf("EncMode() returned an error %v", err) + t.Fatalf("EncMode() returned an error %v", err) } b, err := em.Marshal(tc.v) if err != nil { @@ -3351,6 +3368,17 @@ func TestNaNConvert(t *testing.T) { } }) } + + t.Run("ConvertReject_"+vName, func(t *testing.T) { + em, err := EncOptions{NaNConvert: NaNConvertReject}.EncMode() + if err != nil { + t.Fatalf("EncMode() returned an error %v", err) + } + want := &UnsupportedValueError{msg: "floating-point NaN"} + if _, got := em.Marshal(tc.v); !reflect.DeepEqual(want, got) { + t.Errorf("expected Marshal(%v) to return error: %v, got: %v", tc.v, want, got) + } + }) } }