Repository: fxamacker/cbor (go)

Issue:
feature: add option to decode to `*big.Int` instead of `big.Int` when decoding CBOR bignum into `any`
### What version of fxamacker/cbor are you using?
v2.5.0

### Does this issue reproduce with the latest release?
yes

### What OS and CPU architecture are you using (`go env`)?

<details><summary><code>go env</code> Output</summary><br><pre>
$ go env
GO111MODULE=''
GOARCH='amd64'
GOBIN='/Users/kgillette/usr/bin'
GOCACHE='/Users/kgillette/Library/Caches/go-build'
GOENV='/Users/kgillette/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/kgillette/go/pkg/mod'
GOOS='darwin'
GOPATH='/Users/kgillette/go'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/darwin_amd64'
GOVCS=''
GOVERSION='go1.21.0'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='clang'
CXX='clang++'
CGO_ENABLED='1'
GOMOD='/Users/kgillette/src/github.com/fxamacker/cbor/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/bh/9j45c9dj7ps89q48n_34m44c0000gn/T/go-build560026312=/tmp/go-build -gno-record-gcc-switches -fno-common'
</pre></details>

### What did you do?

Decode CBOR big integer into `any`:

https://go.dev/play/p/qDEgHDEJXum

### What did you expect to see?

The stored type would be `*big.Int`

### What did you see instead?

The stored type is `big.Int`

Context: *big.Int is designed to be pointerized: all `*big.Int` methods take and return pointers, and in general, code using Go big integers will have no reason to dereference (or deal with dereferenced) values.

Dealing with dereferenced big integers at depth can be a pain (such as when the big integers are arbitrarily nested within maps/slices), especially when interoperating with other code which expects `*big.Int`; this can require post-process recursive "fixups" of the data this library produces.

Furthermore, since a `big.Int` is not pointer-sized or smaller, storing it in an interface requires an additional heap allocation compared to storing the standard `*big.Int` in an interface (which requires no additional allocations).

Pull request description:
Add option to decode CBOR bignum to `interface{}` as `*big.Int`
Closes #444 

This PR adds decoding option `BigIntDecMode` which specifies how to decode CBOR bignum to Go `interface{}`.

- `BigIntDecodeValue` (default) makes CBOR bignum decode to `big.Int`.
- `BigIntDecodePointer` makes CBOR bignum decode to `*big.Int`.

Default behavior is unchanged to maintain backward compatibility.  However, using the new option is recommended as it improves speed, reduces memory use, and is more ergonomic.

### Benchmark Comparison

```
                  │ bench_decode_to_bigint.txt │     bench_decode_to_pbigint.txt      │
                  │           sec/op           │    sec/op     vs base                │
DecodeToBigInt-12                  238.9n ± 8%   196.2n ± 10%  -17.87% (p=0.000 n=20)

                  │ bench_decode_to_bigint.txt │    bench_decode_to_pbigint.txt     │
                  │            B/op            │    B/op     vs base                │
DecodeToBigInt-12                   144.0 ± 0%   112.0 ± 0%  -22.22% (p=0.000 n=20)

                  │ bench_decode_to_bigint.txt │    bench_decode_to_pbigint.txt     │
                  │         allocs/op          │ allocs/op   vs base                │
DecodeToBigInt-12                   5.000 ± 0%   4.000 ± 0%  -20.00% (p=0.000 n=20)
```

### Special Thanks

Many thanks to @extemporalgenome for opening issue #444!  :+1: 

Patch:
diff --git a/decode.go b/decode.go
index 09c325d..f7af4a2 100644
--- a/decode.go
+++ b/decode.go
@@ -361,7 +361,7 @@ const (
 	// FieldNameMatchingPreferCaseSensitive prefers to decode map items into struct fields whose names (or tag
 	// names) exactly match the item's key. If there is no such field, a map item will be decoded into a field whose
 	// name is a case-insensitive match for the item's key.
-	FieldNameMatchingPreferCaseSensitive = iota
+	FieldNameMatchingPreferCaseSensitive FieldNameMatchingMode = iota
 
 	// FieldNameMatchingCaseSensitive decodes map items only into a struct field whose name (or tag name) is an
 	// exact match for the item's key.
@@ -374,6 +374,25 @@ func (fnmm FieldNameMatchingMode) valid() bool {
 	return fnmm >= 0 && fnmm < maxFieldNameMatchingMode
 }
 
+// BigIntDecMode specifies how to decode CBOR bignum to Go interface{}.
+type BigIntDecMode int
+
+const (
+	// BigIntDecodeValue makes CBOR bignum decode to big.Int (instead of *big.Int)
+	// when unmarshalling into a Go interface{}.
+	BigIntDecodeValue BigIntDecMode = iota
+
+	// BigIntDecodePointer makes CBOR bignum decode to *big.Int when
+	// unmarshalling into a Go interface{}.
+	BigIntDecodePointer
+
+	maxBigIntDecMode
+)
+
+func (bidm BigIntDecMode) valid() bool {
+	return bidm >= 0 && bidm < maxBigIntDecMode
+}
+
 // DecOptions specifies decoding options.
 type DecOptions struct {
 	// DupMapKey specifies whether to enforce duplicate map key.
@@ -426,6 +445,9 @@ type DecOptions struct {
 
 	// FieldNameMatching specifies how string keys in CBOR maps are matched to Go struct field names.
 	FieldNameMatching FieldNameMatchingMode
+
+	// BigIntDec specifies how to decode CBOR bignum to Go interface{}.
+	BigIntDec BigIntDecMode
 }
 
 // DecMode returns DecMode with immutable options and no tags (safe for concurrency).
@@ -537,6 +559,9 @@ func (opts DecOptions) decMode() (*decMode, error) {
 	if !opts.FieldNameMatching.valid() {
 		return nil, errors.New("cbor: invalid FieldNameMatching " + strconv.Itoa(int(opts.FieldNameMatching)))
 	}
+	if !opts.BigIntDec.valid() {
+		return nil, errors.New("cbor: invalid BigIntDec " + strconv.Itoa(int(opts.BigIntDec)))
+	}
 	dm := decMode{
 		dupMapKey:         opts.DupMapKey,
 		timeTag:           opts.TimeTag,
@@ -551,6 +576,7 @@ func (opts DecOptions) decMode() (*decMode, error) {
 		defaultMapType:    opts.DefaultMapType,
 		utf8:              opts.UTF8,
 		fieldNameMatching: opts.FieldNameMatching,
+		bigIntDec:         opts.BigIntDec,
 	}
 	return &dm, nil
 }
@@ -616,6 +642,7 @@ type decMode struct {
 	defaultMapType    reflect.Type
 	utf8              UTF8Mode
 	fieldNameMatching FieldNameMatchingMode
+	bigIntDec         BigIntDecMode
 }
 
 var defaultDecMode, _ = DecOptions{}.decMode()
@@ -635,6 +662,7 @@ func (dm *decMode) DecOptions() DecOptions {
 		ExtraReturnErrors: dm.extraReturnErrors,
 		UTF8:              dm.utf8,
 		FieldNameMatching: dm.fieldNameMatching,
+		BigIntDec:         dm.bigIntDec,
 	}
 }
 
@@ -1184,6 +1212,10 @@ func (d *decoder) parse(skipSelfDescribedTag bool) (interface{}, error) { //noli
 			bi := new(big.Int).SetUint64(val)
 			bi.Add(bi, big.NewInt(1))
 			bi.Neg(bi)
+
+			if d.dm.bigIntDec == BigIntDecodePointer {
+				return bi, nil
+			}
 			return *bi, nil
 		}
 		nValue := int64(-1) ^ int64(val)
@@ -1208,12 +1240,20 @@ func (d *decoder) parse(skipSelfDescribedTag bool) (interface{}, error) { //noli
 		case 2:
 			b := d.parseByteString()
 			bi := new(big.Int).SetBytes(b)
+
+			if d.dm.bigIntDec == BigIntDecodePointer {
+				return bi, nil
+			}
 			return *bi, nil
 		case 3:
 			b := d.parseByteString()
 			bi := new(big.Int).SetBytes(b)
 			bi.Add(bi, big.NewInt(1))
 			bi.Neg(bi)
+
+			if d.dm.bigIntDec == BigIntDecodePointer {
+				return bi, nil
+			}
 			return *bi, nil
 		}
 


Test patch:
diff --git a/decode_test.go b/decode_test.go
index aa409a3..9231c8f 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -6263,3 +6263,105 @@ func TestDecodeFieldNameMatching(t *testing.T) {
 		})
 	}
 }
+
+func TestInvalidBigIntDecMode(t *testing.T) {
+	for _, tc := range []struct {
+		name         string
+		opts         DecOptions
+		wantErrorMsg string
+	}{
+		{
+			name:         "below range of valid modes",
+			opts:         DecOptions{BigIntDec: -1},
+			wantErrorMsg: "cbor: invalid BigIntDec -1",
+		},
+		{
+			name:         "above range of valid modes",
+			opts:         DecOptions{BigIntDec: 101},
+			wantErrorMsg: "cbor: invalid BigIntDec 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 TestDecodeBignumToEmptyInterface(t *testing.T) {
+	decOptionsDecodeToBigIntValue := DecOptions{BigIntDec: BigIntDecodeValue}
+	decOptionsDecodeToBigIntPointer := DecOptions{BigIntDec: BigIntDecodePointer}
+
+	cborDataPositiveBignum := hexDecode("c249010000000000000000") // positive bignum: 18446744073709551616
+	pbn, _ := new(big.Int).SetString("18446744073709551616", 10)
+
+	cborDataNegativeBignum := hexDecode("c349010000000000000000") // negative bignum: -18446744073709551617
+	nbn, _ := new(big.Int).SetString("-18446744073709551617", 10)
+
+	cborDataLargeNegativeInt := hexDecode("3bffffffffffffffff") // -18446744073709551616
+	ni, _ := new(big.Int).SetString("-18446744073709551616", 10)
+
+	testCases := []struct {
+		name      string
+		opts      DecOptions
+		cborData  []byte
+		wantValue interface{}
+	}{
+		{
+			name:      "decode positive bignum to big.Int",
+			opts:      decOptionsDecodeToBigIntValue,
+			cborData:  cborDataPositiveBignum,
+			wantValue: *pbn,
+		},
+		{
+			name:      "decode negative bignum to big.Int",
+			opts:      decOptionsDecodeToBigIntValue,
+			cborData:  cborDataNegativeBignum,
+			wantValue: *nbn,
+		},
+		{
+			name:      "decode large negative int to big.Int",
+			opts:      decOptionsDecodeToBigIntValue,
+			cborData:  cborDataLargeNegativeInt,
+			wantValue: *ni,
+		},
+		{
+			name:      "decode positive bignum to *big.Int",
+			opts:      decOptionsDecodeToBigIntPointer,
+			cborData:  cborDataPositiveBignum,
+			wantValue: pbn,
+		},
+		{
+			name:      "decode negative bignum to *big.Int",
+			opts:      decOptionsDecodeToBigIntPointer,
+			cborData:  cborDataNegativeBignum,
+			wantValue: nbn,
+		},
+		{
+			name:      "decode large negative int to *big.Int",
+			opts:      decOptionsDecodeToBigIntPointer,
+			cborData:  cborDataLargeNegativeInt,
+			wantValue: ni,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			decMode, _ := tc.opts.DecMode()
+
+			var v interface{}
+			err := decMode.Unmarshal(tc.cborData, &v)
+			if err != nil {
+				t.Errorf("Unmarshal(0x%x) to empty interface returned error %v", tc.cborData, err)
+			} else {
+				if !reflect.DeepEqual(v, tc.wantValue) {
+					t.Errorf("Unmarshal(0x%x) = %v (%T), want %v (%T)", tc.cborData, v, v, tc.wantValue, tc.wantValue)
+				}
+			}
+		})
+	}
+}