Repository: fxamacker/cbor (go)

Issue:
feature: Add option for decoding CBOR pos and neg integer to interface{} to not distinguish between uint and int
**Is your feature request related to a problem? Please describe.**
Some CBOR-based protocols (like COSE) use CBOR maps with positive/negative integers as map keys.  When decoding such maps to interface{}, client code needs to handle uint64 and int64 types separately.  It can be unnecessarily repetitive if the distinction isn't needed.

**Describe the solution you'd like**
Add a decoding option to decode CBOR positive/negative integers (major type 0 or 1) to interface{} as int64.  If value overflows int64, return UnmarshalTypeError.

This option only affects how CBOR integers (major type 0 and 1) decode to Go interface{}.

**Describe alternatives you've considered**
Continue as is.

Pull request:
Add options for decoding CBOR int to interface{}
#### Description (motivation)

New decoding option IntDec affects how CBOR integers (major type 0 and 1)
decodes to Go interface{}.

IntDec values:

* IntDecConvertNone: (default) make CBOR positive int (major type 0) decode
to uint64 value, and CBOR negative int (major type 1) decode to int64 value.

* IntDecConvertSigned: make CBOR positive/negative int (major type 0 and 1)
decode to int64 value.  If value overflows int64, UnmarshalTypeError is
returned.

This can reduce/simplify decoding code written by users. For example,
decoding COSE maps to interface{}.

Instead of users checking for Go int64 and uint64, they can just check
for Go int64.

Closes #216

#### Checklist (for code PR only, ignore for docs PR)

- [x] Include unit tests that cover the new code
- [x] Pass all unit tests 
- [x] Pass all 18 ci linters (golint, gosec, staticcheck, etc.)

Patch:
diff --git a/decode.go b/decode.go
index 079e682..d3855fb 100644
--- a/decode.go
+++ b/decode.go
@@ -210,6 +210,28 @@ func (tm TagsMode) valid() bool {
 	return tm < maxTagsMode
 }
 
+// IntDecMode specifies which Go int type (int64 or uint64) should
+// be used when decoding CBOR int (major type 0 and 1) to Go interface{}.
+type IntDecMode int
+
+const (
+	// IntDecConvertNone affects how CBOR int (major type 0 and 1) decodes to Go interface{}.
+	// It makes CBOR positive int (major type 0) decode to uint64 value, and
+	// CBOR negative int (major type 1) decode to int64 value.
+	IntDecConvertNone IntDecMode = iota
+
+	// IntDecConvertSigned affects how CBOR int (major type 0 and 1) decodes to Go interface{}.
+	// It makes CBOR positive/negative int (major type 0 and 1) decode to int64 value.
+	// If value overflows int64, UnmarshalTypeError is returned.
+	IntDecConvertSigned
+
+	maxIntDec
+)
+
+func (idm IntDecMode) valid() bool {
+	return idm < maxIntDec
+}
+
 // DecOptions specifies decoding options.
 type DecOptions struct {
 	// DupMapKey specifies whether to enforce duplicate map key.
@@ -236,6 +258,10 @@ type DecOptions struct {
 
 	// TagsMd specifies whether to allow CBOR tags (major type 6).
 	TagsMd TagsMode
+
+	// IntDec specifies which Go integer type (int64 or uint64) to use
+	// when decoding CBOR int (major type 0 and 1) to Go interface{}.
+	IntDec IntDecMode
 }
 
 // DecMode returns DecMode with immutable options and no tags (safe for concurrency).
@@ -314,6 +340,9 @@ func (opts DecOptions) decMode() (*decMode, error) {
 	if !opts.TagsMd.valid() {
 		return nil, errors.New("cbor: invalid TagsMd " + strconv.Itoa(int(opts.TagsMd)))
 	}
+	if !opts.IntDec.valid() {
+		return nil, errors.New("cbor: invalid IntDec " + strconv.Itoa(int(opts.IntDec)))
+	}
 	if opts.MaxNestedLevels == 0 {
 		opts.MaxNestedLevels = 32
 	} else if opts.MaxNestedLevels < 4 || opts.MaxNestedLevels > 256 {
@@ -337,6 +366,7 @@ func (opts DecOptions) decMode() (*decMode, error) {
 		maxMapPairs:      opts.MaxMapPairs,
 		indefLength:      opts.IndefLength,
 		tagsMd:           opts.TagsMd,
+		intDec:           opts.IntDec,
 	}
 	return &dm, nil
 }
@@ -357,6 +387,7 @@ type decMode struct {
 	maxMapPairs      int
 	indefLength      IndefLengthMode
 	tagsMd           TagsMode
+	intDec           IntDecMode
 }
 
 var defaultDecMode, _ = DecOptions{}.decMode()
@@ -371,6 +402,7 @@ func (dm *decMode) DecOptions() DecOptions {
 		MaxMapPairs:      dm.maxMapPairs,
 		IndefLength:      dm.indefLength,
 		TagsMd:           dm.tagsMd,
+		IntDec:           dm.intDec,
 	}
 }
 
@@ -732,13 +764,23 @@ func (d *decodeState) parse() (interface{}, error) {
 	switch t {
 	case cborTypePositiveInt:
 		_, _, val := d.getHead()
-		return val, nil
+		if d.dm.intDec == IntDecConvertNone {
+			return val, nil
+		}
+		if val > math.MaxInt64 {
+			return nil, &UnmarshalTypeError{
+				Value:  t.String(),
+				Type:   reflect.TypeOf(int64(0)),
+				errMsg: strconv.FormatUint(val, 10) + " overflows Go's int64",
+			}
+		}
+		return int64(val), nil
 	case cborTypeNegativeInt:
 		_, _, val := d.getHead()
 		if val > math.MaxInt64 {
 			return nil, &UnmarshalTypeError{
 				Value:  t.String(),
-				Type:   reflect.TypeOf([]interface{}(nil)).Elem(),
+				Type:   reflect.TypeOf(int64(0)),
 				errMsg: "-1-" + strconv.FormatUint(val, 10) + " overflows Go's int64",
 			}
 		}


Test patch:
diff --git a/decode_test.go b/decode_test.go
index 0855d69..faa4c86 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -4201,3 +4201,62 @@ func TestDecTagsMdOption(t *testing.T) {
 		t.Errorf("DecModeWithSharedTags() returned error %q, want %q", err.Error(), wantErrorMsg)
 	}
 }
+
+func TestDecModeInvalidIntDec(t *testing.T) {
+	wantErrorMsg := "cbor: invalid IntDec 101"
+	_, err := DecOptions{IntDec: 101}.DecMode()
+	if err == nil {
+		t.Errorf("DecMode() didn't return an error")
+	} else if err.Error() != wantErrorMsg {
+		t.Errorf("DecMode() returned error %q, want %q", err.Error(), wantErrorMsg)
+	}
+}
+
+func TestIntDec(t *testing.T) {
+	dm, err := DecOptions{IntDec: IntDecConvertSigned}.DecMode()
+	if err != nil {
+		t.Errorf("DecMode() returned an error %+v", err)
+	}
+
+	testCases := []struct {
+		name         string
+		cborData     []byte
+		wantObj      interface{}
+		wantErrorMsg string
+	}{
+		{
+			name:     "CBOR pos int",
+			cborData: hexDecode("1a000f4240"),
+			wantObj:  int64(1000000),
+		},
+		{
+			name:         "CBOR pos int overflows int64",
+			cborData:     hexDecode("1bffffffffffffffff"),
+			wantErrorMsg: "18446744073709551615 overflows Go's int64",
+		},
+		{
+			name:     "CBOR neg int",
+			cborData: hexDecode("3903e7"),
+			wantObj:  int64(-1000),
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var v interface{}
+			err := dm.Unmarshal(tc.cborData, &v)
+			if err == nil {
+				if tc.wantErrorMsg != "" {
+					t.Errorf("Unmarshal(0x%x) didn't return an error, want %q", tc.cborData, tc.wantErrorMsg)
+				} else if !reflect.DeepEqual(v, tc.wantObj) {
+					t.Errorf("Unmarshal(0x%x) return %v (%T), want %v (%T)", tc.cborData, v, v, tc.wantObj, tc.wantObj)
+				}
+			} else {
+				if tc.wantErrorMsg == "" {
+					t.Errorf("Unmarshal(0x%x) returned error %q", tc.cborData, err)
+				} else if !strings.Contains(err.Error(), tc.wantErrorMsg) {
+					t.Errorf("Unmarshal(0x%x) returned error %q, want %q", tc.cborData, err.Error(), tc.wantErrorMsg)
+				}
+			}
+		})
+	}
+}