Skip to content

Commit

Permalink
Add support for unassigned/reserved CBOR simple values (#370)
Browse files Browse the repository at this point in the history
Add a SimpleValue type which is distinct from Go's numeric types.

Add support for properly encoding and decoding all simple values,
including 252 unassigned/reserved simple values.

Improve support for simple values as map keys by making them
distinct from uint64 values 0-255.

CBOR simple values are a subset of major type 7
that is not floating-point.

Only 4 simple values were previously supported by this codec:
- false
- true
- null
- undefined

The other 252 simple values are unassigned or reserved by IANA.
  • Loading branch information
fxamacker committed Nov 14, 2022
1 parent 3ce4b02 commit 7704fa5
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 18 deletions.
24 changes: 16 additions & 8 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -731,14 +731,7 @@ func (d *decoder) parseToValue(v reflect.Value, tInfo *typeInfo) error { //nolin
return fillTextString(t, b, v)
case cborTypePrimitives:
_, ai, val := d.getHead()
if ai < 20 || ai == 24 {
return fillPositiveInt(t, val, v)
}
switch ai {
case 20, 21:
return fillBool(t, ai == 21, v)
case 22, 23:
return fillNil(t, v)
case 25:
f := float64(float16.Frombits(uint16(val)).Float32())
return fillFloat(t, f, v)
Expand All @@ -748,7 +741,22 @@ func (d *decoder) parseToValue(v reflect.Value, tInfo *typeInfo) error { //nolin
case 27:
f := math.Float64frombits(val)
return fillFloat(t, f, v)
default: // ai <= 24
// Decode simple values (including false, true, null, and undefined)
if tInfo.nonPtrType == typeSimpleValue {
v.SetUint(val)
return nil
}
switch ai {
case 20, 21:
return fillBool(t, ai == 21, v)
case 22, 23:
return fillNil(t, v)
default:
return fillPositiveInt(t, val, v)
}
}

case cborTypeTag:
_, _, tagNum := d.getHead()
switch tagNum {
Expand Down Expand Up @@ -1034,7 +1042,7 @@ func (d *decoder) parse(skipSelfDescribedTag bool) (interface{}, error) { //noli
case cborTypePrimitives:
_, ai, val := d.getHead()
if ai < 20 || ai == 24 {
return val, nil
return SimpleValue(val), nil
}
switch ai {
case 20, 21:
Expand Down
20 changes: 10 additions & 10 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,31 +431,31 @@ var unmarshalTests = []unmarshalTest{
{
hexDecode("f4"),
false,
[]interface{}{false},
[]interface{}{false, SimpleValue(20)},
[]reflect.Type{typeUint8, typeUint16, typeUint32, typeUint64, typeInt8, typeInt16, typeInt32, typeInt64, typeFloat32, typeFloat64, typeByteArray, typeByteSlice, typeString, typeIntSlice, typeMapStringInt, typeTag, typeRawTag, typeBigInt},
},
{
hexDecode("f5"),
true,
[]interface{}{true},
[]interface{}{true, SimpleValue(21)},
[]reflect.Type{typeUint8, typeUint16, typeUint32, typeUint64, typeInt8, typeInt16, typeInt32, typeInt64, typeFloat32, typeFloat64, typeByteArray, typeByteSlice, typeString, typeIntSlice, typeMapStringInt, typeTag, typeRawTag, typeBigInt},
},
{
hexDecode("f6"),
nil,
[]interface{}{false, uint(0), uint8(0), uint16(0), uint32(0), uint64(0), int(0), int8(0), int16(0), int32(0), int64(0), float32(0.0), float64(0.0), "", []byte(nil), []int(nil), []string(nil), map[string]int(nil), time.Time{}, bigIntOrPanic("0"), Tag{}, RawTag{}},
[]interface{}{SimpleValue(22), false, uint(0), uint8(0), uint16(0), uint32(0), uint64(0), int(0), int8(0), int16(0), int32(0), int64(0), float32(0.0), float64(0.0), "", []byte(nil), []int(nil), []string(nil), map[string]int(nil), time.Time{}, bigIntOrPanic("0"), Tag{}, RawTag{}},
nil,
},
{
hexDecode("f7"),
nil,
[]interface{}{false, uint(0), uint8(0), uint16(0), uint32(0), uint64(0), int(0), int8(0), int16(0), int32(0), int64(0), float32(0.0), float64(0.0), "", []byte(nil), []int(nil), []string(nil), map[string]int(nil), time.Time{}, bigIntOrPanic("0"), Tag{}, RawTag{}},
[]interface{}{SimpleValue(23), false, uint(0), uint8(0), uint16(0), uint32(0), uint64(0), int(0), int8(0), int16(0), int32(0), int64(0), float32(0.0), float64(0.0), "", []byte(nil), []int(nil), []string(nil), map[string]int(nil), time.Time{}, bigIntOrPanic("0"), Tag{}, RawTag{}},
nil,
},
{
hexDecode("f0"),
uint64(16),
[]interface{}{uint8(16), uint16(16), uint32(16), uint64(16), uint(16), int8(16), int16(16), int32(16), int64(16), int(16), float32(16), float64(16), bigIntOrPanic("16")},
SimpleValue(16),
[]interface{}{SimpleValue(16), uint8(16), uint16(16), uint32(16), uint64(16), uint(16), int8(16), int16(16), int32(16), int64(16), int(16), float32(16), float64(16), bigIntOrPanic("16")},
[]reflect.Type{typeByteSlice, typeString, typeBool, typeByteArray, typeIntSlice, typeMapStringInt, typeTag, typeRawTag},
},
// This example is not well-formed because Simple value (with 5-bit value 24) must be >= 32.
Expand All @@ -471,14 +471,14 @@ var unmarshalTests = []unmarshalTest{
*/
{
hexDecode("f820"),
uint64(32),
[]interface{}{uint8(32), uint16(32), uint32(32), uint64(32), uint(32), int8(32), int16(32), int32(32), int64(32), int(32), float32(32), float64(32), bigIntOrPanic("32")},
SimpleValue(32),
[]interface{}{SimpleValue(32), uint8(32), uint16(32), uint32(32), uint64(32), uint(32), int8(32), int16(32), int32(32), int64(32), int(32), float32(32), float64(32), bigIntOrPanic("32")},
[]reflect.Type{typeByteSlice, typeString, typeBool, typeByteArray, typeIntSlice, typeMapStringInt, typeTag, typeRawTag},
},
{
hexDecode("f8ff"),
uint64(255),
[]interface{}{uint8(255), uint16(255), uint32(255), uint64(255), uint(255), int16(255), int32(255), int64(255), int(255), float32(255), float64(255), bigIntOrPanic("255")},
SimpleValue(255),
[]interface{}{SimpleValue(255), uint8(255), uint16(255), uint32(255), uint64(255), uint(255), int16(255), int32(255), int64(255), int(255), float32(255), float64(255), bigIntOrPanic("255")},
[]reflect.Type{typeByteSlice, typeString, typeBool, typeByteArray, typeIntSlice, typeMapStringInt, typeTag, typeRawTag},
},
// More testcases not covered by https://tools.ietf.org/html/rfc7049#appendix-A.
Expand Down
10 changes: 10 additions & 0 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,14 @@ func encodeTag(e *encoderBuffer, em *encMode, v reflect.Value) error {
return nil
}

func encodeSimpleValue(e *encoderBuffer, em *encMode, v reflect.Value) error {
if b := em.encTagBytes(v.Type()); b != nil {
e.Write(b)
}
encodeHead(e, byte(cborTypePrimitives), v.Uint())
return nil
}

func encodeHead(e *encoderBuffer, t byte, n uint64) {
if n <= 23 {
e.WriteByte(t | byte(n))
Expand Down Expand Up @@ -1290,6 +1298,8 @@ func getEncodeFuncInternal(t reflect.Type) (encodeFunc, isEmptyFunc) {
return getEncodeIndirectValueFunc(t), isEmptyPtr
}
switch t {
case typeSimpleValue:
return encodeSimpleValue, isEmptyUint
case typeTag:
return encodeTag, alwaysNotEmpty
case typeTime:
Expand Down
63 changes: 63 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ var marshalTests = []marshalTest{
{hexDecode("f4"), []interface{}{false}},
{hexDecode("f5"), []interface{}{true}},
{hexDecode("f6"), []interface{}{nil, []byte(nil), []int(nil), map[uint]bool(nil), (*int)(nil), io.Reader(nil)}},
// simple values
{hexDecode("e0"), []interface{}{SimpleValue(0)}},
{hexDecode("f0"), []interface{}{SimpleValue(16)}},
{hexDecode("f820"), []interface{}{SimpleValue(32)}},
{hexDecode("f8ff"), []interface{}{SimpleValue(255)}},
// nan, positive and negative inf
{hexDecode("f97c00"), []interface{}{math.Inf(1)}},
{hexDecode("f97e00"), []interface{}{math.NaN()}},
Expand Down Expand Up @@ -3590,3 +3595,61 @@ func TestMarshalNegBigInt(t *testing.T) {
})
}
}

func TestStructWithSimpleValueFields(t *testing.T) {
type T struct {
SV1 SimpleValue `cbor:",omitempty"` // omit empty
SV2 SimpleValue
}

v1 := T{}
want1 := []byte{0xa1, 0x63, 0x53, 0x56, 0x32, 0xe0}

v2 := T{SV1: SimpleValue(1), SV2: SimpleValue(255)}
want2 := []byte{
0xa2,
0x63, 0x53, 0x56, 0x31, 0xe1,
0x63, 0x53, 0x56, 0x32, 0xf8, 0xff,
}

em, _ := EncOptions{}.EncMode()
dm, _ := DecOptions{}.DecMode()
tests := []roundTripTest{
{"default values", v1, want1},
{"non-default values", v2, want2}}
testRoundTrip(t, tests, em, dm)
}

func TestMapWithSimpleValueKey(t *testing.T) {
data := []byte{0xa2, 0x00, 0x00, 0xe0, 0x00} // {0: 0, simple(0): 0}

// Decode CBOR map with positive integer 0 and simple value 0 as keys.
// No map key duplication is detected because keys are of different CBOR types.
// RFC 8949 Section 5.6.1 says "a simple value 2 is not equivalent to an integer 2".
decOpts := DecOptions{
DupMapKey: DupMapKeyEnforcedAPF, // duplicated key not allowed
}
decMode, _ := decOpts.DecMode()

var v map[interface{}]interface{}
err := decMode.Unmarshal(data, &v)
if err != nil {
t.Errorf("Unmarshal(0x%x) returned error %v", data, err)
}

// Encode decoded Go map.
encOpts := EncOptions{
Sort: SortBytewiseLexical,
}
encMode, _ := encOpts.EncMode()

encodedData, err := encMode.Marshal(v)
if err != nil {
t.Errorf("Marshal(%v) returned error %v", v, err)
}

// Test roundtrip produces identical CBOR data.
if !bytes.Equal(data, encodedData) {
t.Errorf("Marshal(%v) = 0x%x, want 0x%x", v, encodedData, data)
}
}
17 changes: 17 additions & 0 deletions simplevalue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package cbor

import "reflect"

// SimpleValue represents CBOR simple value.
// CBOR simple value is:
// * an extension point like CBOR tag.
// * a subset of CBOR major type 7 that isn't floating-point.
// * "identified by a number between 0 and 255, but distinct from that number itself".
// For example, "a simple value 2 is not equivalent to an integer 2" as a CBOR map key.
// CBOR simple values identified by 20..23 are: "false", "true" , "null", and "undefined".
// Other CBOR simple values are currently unassigned/reserved by IANA.
type SimpleValue uint8

var (
typeSimpleValue = reflect.TypeOf(SimpleValue(0))
)

0 comments on commit 7704fa5

Please sign in to comment.