Skip to content

Commit

Permalink
Add decoding option to specific how to decode tag 0 or 1 into any
Browse files Browse the repository at this point in the history
Adds a decoding option to specify how to decode CBOR tag 0 or 1 data item into an empty interface.
Based on the specified mode, Unmarshal can return a time.Time value or a time string in a specific format.

Signed-off-by: Suriyan Subbarayan [email protected]
  • Loading branch information
ssuriyan7 committed Mar 30, 2024
1 parent a35fe34 commit 219a19c
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 1 deletion.
55 changes: 54 additions & 1 deletion decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,30 @@ func (uttam UnrecognizedTagToAnyMode) valid() bool {
return uttam >= 0 && uttam < maxUnrecognizedTagToAny
}

// 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.
type TimeTagToAnyMode int

const (
// TimeTagToTime decodes CBOR tag 0 and 1 into a time.Time value
// when decoding tag 0 or 1 into an empty interface.
TimeTagToTime TimeTagToAnyMode = iota

// TimeTagToRFC3339 decodes CBOR tag 0 and 1 into a time string in RFC3339 format
// when decoding tag 0 or 1 into an empty interface.
TimeTagToRFC3339

// TimeTagToRFC3339Nano decodes CBOR tag 0 and 1 into a time string in RFC3339Nano format
// when decoding tag 0 or 1 into an empty interface.
TimeTagToRFC3339Nano

maxTimeTagToAnyMode
)

func (tttam TimeTagToAnyMode) valid() bool {
return tttam >= 0 && tttam < maxTimeTagToAnyMode
}

// DecOptions specifies decoding options.
type DecOptions struct {
// DupMapKey specifies whether to enforce duplicate map key.
Expand Down Expand Up @@ -564,6 +588,10 @@ type DecOptions struct {
// UnrecognizedTagToAny specifies how to decode unrecognized CBOR tag into an empty interface.
// Currently, recognized CBOR tag numbers are 0, 1, 2, 3, or registered by TagSet.
UnrecognizedTagToAny UnrecognizedTagToAnyMode

// 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
}

// DecMode returns DecMode with immutable options and no tags (safe for concurrency).
Expand Down Expand Up @@ -717,6 +745,10 @@ func (opts DecOptions) decMode() (*decMode, error) {
return nil, errors.New("cbor: invalid UnrecognizedTagToAnyMode " + strconv.Itoa(int(opts.UnrecognizedTagToAny)))
}

if !opts.TimeTagToAny.valid() {
return nil, errors.New("cbor: invalid TimeTagToAny " + strconv.Itoa(int(opts.TimeTagToAny)))
}

dm := decMode{
dupMapKey: opts.DupMapKey,
timeTag: opts.TimeTag,
Expand All @@ -736,6 +768,7 @@ func (opts DecOptions) decMode() (*decMode, error) {
byteStringToString: opts.ByteStringToString,
fieldNameByteString: opts.FieldNameByteString,
unrecognizedTagToAny: opts.UnrecognizedTagToAny,
timeTagToAny: opts.TimeTagToAny,
}

return &dm, nil
Expand Down Expand Up @@ -807,6 +840,7 @@ type decMode struct {
byteStringToString ByteStringToStringMode
fieldNameByteString FieldNameByteStringMode
unrecognizedTagToAny UnrecognizedTagToAnyMode
timeTagToAny TimeTagToAnyMode
}

var defaultDecMode, _ = DecOptions{}.decMode()
Expand All @@ -832,6 +866,7 @@ func (dm *decMode) DecOptions() DecOptions {
ByteStringToString: dm.byteStringToString,
FieldNameByteString: dm.fieldNameByteString,
UnrecognizedTagToAny: dm.unrecognizedTagToAny,
TimeTagToAny: dm.timeTagToAny,
}
}

Expand Down Expand Up @@ -1503,7 +1538,25 @@ func (d *decoder) parse(skipSelfDescribedTag bool) (interface{}, error) { //noli
case 0, 1:
d.off = tagOff
tm, _, err := d.parseToTime()
return tm, err
if err != nil {
return nil, err
}
switch d.dm.timeTagToAny {
case TimeTagToTime:
return tm, nil
case TimeTagToRFC3339:
if tagNum == 1 {
tm = tm.UTC()
}
return tm.Format(time.RFC3339), nil
case TimeTagToRFC3339Nano:
if tagNum == 1 {
tm = tm.UTC()
}
return tm.Format(time.RFC3339Nano), nil
default:
// not reachable
}
case 2:
b, _ := d.parseByteString()
bi := new(big.Int).SetBytes(b)
Expand Down
90 changes: 90 additions & 0 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4912,6 +4912,7 @@ func TestDecOptions(t *testing.T) {
ByteStringToString: ByteStringToStringAllowed,
FieldNameByteString: FieldNameByteStringAllowed,
UnrecognizedTagToAny: UnrecognizedTagContentToAny,
TimeTagToAny: TimeTagToRFC3339,
}
ov := reflect.ValueOf(opts1)
for i := 0; i < ov.NumField(); i++ {
Expand Down Expand Up @@ -8658,3 +8659,92 @@ func TestUnmarshalWithUnrecognizedTagToAnyModeForSharedTag(t *testing.T) {
func isCBORNil(data []byte) bool {
return len(data) > 0 && (data[0] == 0xf6 || data[0] == 0xf7)
}

func TestDecModeInvalidTimeTagToAnyMode(t *testing.T) {
for _, tc := range []struct {
name string
opts DecOptions
wantErrorMsg string
}{
{
name: "below range of valid modes",
opts: DecOptions{TimeTagToAny: -1},
wantErrorMsg: "cbor: invalid TimeTagToAny -1",
},
{
name: "above range of valid modes",
opts: DecOptions{TimeTagToAny: 4},
wantErrorMsg: "cbor: invalid TimeTagToAny 4",
},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := tc.opts.DecMode()
if err == nil {
t.Errorf("Expected non nil error from DecMode()")
} else if err.Error() != tc.wantErrorMsg {
t.Errorf("Expected error: %q, want: %q \n", tc.wantErrorMsg, err.Error())
}
})
}
}

func TestDecModeTimeTagToAny(t *testing.T) {
for _, tc := range []struct {
name string
opts DecOptions
in []byte
want interface{}
}{
{
name: "Unmarshal tag 0 data to time.Time when TimeTagToAny is not set",
opts: DecOptions{},
in: hexDecode("c074323031332d30332d32315432303a30343a30305a"),
want: time.Date(2013, 3, 21, 20, 4, 0, 0, time.UTC),
},
{
name: "Unmarshal tag 1 data to time.Time when TimeTagToAny is not set",
opts: DecOptions{},
in: hexDecode("c11a514b67b0"),
want: time.Date(2013, 3, 21, 20, 4, 0, 0, time.UTC),
},
{
name: "Unmarshal tag 0 data to RFC3339 string when TimeTagToAny is set",
opts: DecOptions{TimeTagToAny: TimeTagToRFC3339},
in: hexDecode("c074323031332d30332d32315432303a30343a30305a"),
want: "2013-03-21T20:04:00Z",
},
{
name: "Unmarshal tag 1 data to RFC3339 string when TimeTagToAny is set",
opts: DecOptions{TimeTagToAny: TimeTagToRFC3339},
in: hexDecode("c11a514b67b0"),
want: "2013-03-21T20:04:00Z",
},
{
name: "Unmarshal tag 0 data to RFC3339Nano string when TimeTagToAny is set",
opts: DecOptions{TimeTagToAny: TimeTagToRFC3339Nano},
in: hexDecode("c076323031332d30332d32315432303a30343a30302e355a"),
want: "2013-03-21T20:04:00.5Z",
},
{
name: "Unmarshal tag 1 data to RFC3339Nano string when TimeTagToAny is set",
opts: DecOptions{TimeTagToAny: TimeTagToRFC3339Nano},
in: hexDecode("c1fb41d452d9ec200000"),
want: "2013-03-21T20:04:00.5Z",
},
} {
t.Run(tc.name, func(t *testing.T) {
dm, err := tc.opts.DecMode()
if err != nil {
t.Fatal(err)
}

var got interface{}
if err := dm.Unmarshal(tc.in, &got); err != nil {
t.Errorf("unexpected error: %v", err)
}

compareNonFloats(t, tc.in, got, tc.want)

})
}
}

0 comments on commit 219a19c

Please sign in to comment.