From 219a19cc05a63edee79696acbcd67cc77450b764 Mon Sep 17 00:00:00 2001 From: Suriyan S Date: Sat, 16 Mar 2024 17:47:17 -0400 Subject: [PATCH] Add decoding option to specific how to decode tag 0 or 1 into any 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 suriyansub710@gmail.com --- decode.go | 55 +++++++++++++++++++++++++++++- decode_test.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) diff --git a/decode.go b/decode.go index c2b5e008..0c8b398c 100644 --- a/decode.go +++ b/decode.go @@ -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. @@ -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). @@ -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, @@ -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 @@ -807,6 +840,7 @@ type decMode struct { byteStringToString ByteStringToStringMode fieldNameByteString FieldNameByteStringMode unrecognizedTagToAny UnrecognizedTagToAnyMode + timeTagToAny TimeTagToAnyMode } var defaultDecMode, _ = DecOptions{}.decMode() @@ -832,6 +866,7 @@ func (dm *decMode) DecOptions() DecOptions { ByteStringToString: dm.byteStringToString, FieldNameByteString: dm.fieldNameByteString, UnrecognizedTagToAny: dm.unrecognizedTagToAny, + TimeTagToAny: dm.timeTagToAny, } } @@ -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) diff --git a/decode_test.go b/decode_test.go index 54da82f3..9bc4c881 100644 --- a/decode_test.go +++ b/decode_test.go @@ -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++ { @@ -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) + + }) + } +}