Reject math/big.Int on encode and bignum tags on decode for CBOR.

Although CBOR can roundtrip arbitrary precision integers, they don't roundtrip properly through
Unstructured because Unstructured is limited to the dynamic numeric types int64 and float64. Rather
than serializing them (and potentially losing information), reject them explicitly with an error.
This commit is contained in:
Ben Luddy 2024-05-15 13:49:42 -04:00
parent fbaf9b0353
commit 61654e9f5d
No known key found for this signature in database
GPG Key ID: A6551E73A5974C30
5 changed files with 32 additions and 24 deletions

View File

@ -121,7 +121,6 @@ func TestAppendixA(t *testing.T) {
{
example: hex("c249010000000000000000"),
reject: "decoding tagged positive bigint value to interface{} can't reproduce this value without losing distinction between float and integer",
fixme: "decoding bigint to interface{} must not produce math/big.Int",
},
{
example: hex("3bffffffffffffffff"),
@ -130,7 +129,6 @@ func TestAppendixA(t *testing.T) {
{
example: hex("c349010000000000000000"),
reject: "-18446744073709551617 overflows int64 and falling back to float64 (as with JSON) loses distinction between float and integer",
fixme: "decoding negative bigint to interface{} must not produce math/big.Int",
},
{
example: hex("20"),

View File

@ -96,6 +96,10 @@ var Decode cbor.DecMode = func() cbor.DecMode {
// representation (RFC 8259 Section 6).
NaN: cbor.NaNDecodeForbidden,
Inf: cbor.InfDecodeForbidden,
// Reject the arbitrary-precision integer tags because they can't be faithfully
// roundtripped through the allowable Unstructured types.
BignumTag: cbor.BignumTagForbidden,
}.DecMode()
if err != nil {
panic(err)

View File

@ -739,29 +739,25 @@ func TestDecode(t *testing.T) {
group(t, "unsigned bignum", []test{
{
name: "rejected",
in: hex("c249010000000000000000"), // 2(18446744073709551616)
fixme: "decoding cbor data tagged with 2 produces big.Int instead of rejecting",
assertOnError: func(t *testing.T, e error) {
// TODO: Once this can pass, make the assertion stronger.
if e == nil {
t.Error("expected non-nil error")
name: "rejected",
in: hex("c249010000000000000000"), // 2(18446744073709551616)
assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "tag", Message: "bignum"}, e); diff != "" {
t.Errorf("unexpected error diff:\n%s", diff)
}
},
}),
},
})
group(t, "negative bignum", []test{
{
name: "rejected",
in: hex("c349010000000000000000"), // 3(-18446744073709551617)
fixme: "decoding cbor data tagged with 3 produces big.Int instead of rejecting",
assertOnError: func(t *testing.T, e error) {
// TODO: Once this can pass, make the assertion stronger.
if e == nil {
t.Error("expected non-nil error")
name: "rejected",
in: hex("c349010000000000000000"), // 3(-18446744073709551617)
assertOnError: assertOnConcreteError(func(t *testing.T, e *cbor.UnacceptableDataItemError) {
if diff := cmp.Diff(&cbor.UnacceptableDataItemError{CBORType: "tag", Message: "bignum"}, e); diff != "" {
t.Errorf("unexpected error diff:\n%s", diff)
}
},
}),
},
})

View File

@ -37,11 +37,10 @@ var Encode cbor.EncMode = func() cbor.EncMode {
NaNConvert: cbor.NaNConvertReject,
InfConvert: cbor.InfConvertReject,
// Prefer encoding math/big.Int to one of the 64-bit integer types if it fits. When
// later decoded into Unstructured, the set of allowable concrete numeric types is
// limited to int64 and float64, so the distinction between big integer and integer
// can't be preserved.
BigIntConvert: cbor.BigIntConvertShortest,
// Error on attempt to encode math/big.Int values, which can't be faithfully
// roundtripped through Unstructured in general (the dynamic numeric types allowed
// in Unstructured are limited to float64 and int64).
BigIntConvert: cbor.BigIntConvertReject,
// MarshalJSON for time.Time writes RFC3339 with nanos.
Time: cbor.TimeRFC3339Nano,

View File

@ -18,6 +18,8 @@ package modes_test
import (
"fmt"
"math/big"
"reflect"
"testing"
"github.com/fxamacker/cbor/v2"
@ -53,6 +55,15 @@ func TestEncode(t *testing.T) {
want: []byte{0xa1, 0x41, 0x41, 0x02}, // {"A": 2}
assertOnError: assertNilError,
},
{
name: "math/big.Int values are rejected",
in: big.NewInt(1),
assertOnError: assertOnConcreteError(func(t *testing.T, got *cbor.UnsupportedTypeError) {
if want := (&cbor.UnsupportedTypeError{Type: reflect.TypeFor[big.Int]()}); *want != *got {
t.Errorf("unexpected error, got %#v (%q), want %#v (%q)", got, got.Error(), want, want.Error())
}
}),
},
} {
encModes := tc.modes
if len(encModes) == 0 {
@ -68,7 +79,7 @@ func TestEncode(t *testing.T) {
t.Run(fmt.Sprintf("mode=%s/%s", modeName, tc.name), func(t *testing.T) {
out, err := encMode.Marshal(tc.in)
tc.assertOnError(t, err)
if diff := cmp.Diff(tc.want, out); diff != "" {
if diff := cmp.Diff(tc.want, out, cmp.Comparer(func(a, b reflect.Type) bool { return a == b })); diff != "" {
t.Errorf("unexpected output:\n%s", diff)
}
})