Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a decoding option to allow decoding byte string into time.Time. #524

Merged
merged 1 commit into from
Apr 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,23 @@ func (idm InfMode) valid() bool {
return idm >= 0 && idm < maxInfDecode
}

// ByteStringToTimeMode specifies the behavior when decoding a CBOR byte string into a Go time.Time.
type ByteStringToTimeMode int

const (
// ByteStringToTimeForbidden generates an error on an attempt to decode a CBOR byte string into a Go time.Time.
ByteStringToTimeForbidden ByteStringToTimeMode = iota

// ByteStringToTimeAllowed permits decoding a CBOR byte string into a Go time.Time.
ByteStringToTimeAllowed

maxByteStringToTimeMode
)

func (bttm ByteStringToTimeMode) valid() bool {
return bttm >= 0 && bttm < maxByteStringToTimeMode
}

// DecOptions specifies decoding options.
type DecOptions struct {
// DupMapKey specifies whether to enforce duplicate map key.
Expand Down Expand Up @@ -690,6 +707,9 @@ type DecOptions struct {
// Inf specifies how to decode floating-point values (major type 7, additional information
// 25 through 27) representing positive or negative infinity.
Inf InfMode

// ByteStringToTimeMode specifies the behavior when decoding a CBOR byte string into a Go time.Time.
ByteStringToTime ByteStringToTimeMode
}

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

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

dm := decMode{
dupMapKey: opts.DupMapKey,
timeTag: opts.TimeTag,
Expand All @@ -891,6 +915,7 @@ func (opts DecOptions) decMode() (*decMode, error) {
simpleValues: simpleValues,
nanDec: opts.NaN,
infDec: opts.Inf,
byteStringToTime: opts.ByteStringToTime,
}

return &dm, nil
Expand Down Expand Up @@ -966,6 +991,7 @@ type decMode struct {
simpleValues *SimpleValueRegistry
nanDec NaNMode
infDec InfMode
byteStringToTime ByteStringToTimeMode
}

var defaultDecMode, _ = DecOptions{}.decMode()
Expand Down Expand Up @@ -1002,6 +1028,7 @@ func (dm *decMode) DecOptions() DecOptions {
SimpleValues: simpleValues,
NaN: dm.nanDec,
Inf: dm.infDec,
ByteStringToTime: dm.byteStringToTime,
}
}

Expand Down Expand Up @@ -1470,6 +1497,16 @@ func (d *decoder) parseToTime() (time.Time, bool, error) {
}

switch t := d.nextCBORType(); t {
case cborTypeByteString:
if d.dm.byteStringToTime == ByteStringToTimeAllowed {
b, _ := d.parseByteString()
t, err := time.Parse(time.RFC3339, string(b))
if err != nil {
return time.Time{}, false, fmt.Errorf("cbor: cannot set %q for time.Time: %w", string(b), err)
}
return t, true, nil
}
return time.Time{}, false, &UnmarshalTypeError{CBORType: t.String(), GoType: typeTime.String()}
case cborTypeTextString:
s, err := d.parseTextString()
if err != nil {
Expand Down
86 changes: 86 additions & 0 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4921,6 +4921,7 @@ func TestDecOptions(t *testing.T) {
SimpleValues: simpleValues,
NaN: NaNDecodeForbidden,
Inf: InfDecodeForbidden,
ByteStringToTime: ByteStringToTimeAllowed,
}
ov := reflect.ValueOf(opts1)
for i := 0; i < ov.NumField(); i++ {
Expand Down Expand Up @@ -9426,3 +9427,88 @@ func TestInfDecMode(t *testing.T) {
})
}
}

func TestDecModeInvalidByteStringToTimeMode(t *testing.T) {
for _, tc := range []struct {
name string
opts DecOptions
wantErrorMsg string
}{
{
name: "below range of valid modes",
opts: DecOptions{ByteStringToTime: -1},
wantErrorMsg: "cbor: invalid ByteStringToTime -1",
},
{
name: "above range of valid modes",
opts: DecOptions{ByteStringToTime: 4},
wantErrorMsg: "cbor: invalid ByteStringToTime 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 TestDecModeByteStringToTime(t *testing.T) {
for _, tc := range []struct {
name string
opts DecOptions
in []byte
want time.Time
wantErrorMsg string
}{
{
name: "Unmarshal byte string to time.Time when ByteStringToTime is not set",
opts: DecOptions{},
in: hexDecode("54323031332D30332D32315432303A30343A30305A"), // '2013-03-21T20:04:00Z'
wantErrorMsg: "cbor: cannot unmarshal byte string into Go value of type time.Time",
},
{
name: "Unmarshal byte string to time.Time when ByteStringToTime is set to ByteStringToTimeAllowed",
opts: DecOptions{ByteStringToTime: ByteStringToTimeAllowed},
in: hexDecode("54323031332D30332D32315432303A30343A30305A"), // '2013-03-21T20:04:00Z'
want: time.Date(2013, 3, 21, 20, 4, 0, 0, time.UTC),
},
{
name: "Unmarshal byte string to time.Time with nano when ByteStringToTime is set to ByteStringToTimeAllowed",
opts: DecOptions{ByteStringToTime: ByteStringToTimeAllowed},
in: hexDecode("56323031332D30332D32315432303A30343A30302E355A"), // '2013-03-21T20:04:00.5Z'
want: time.Date(2013, 3, 21, 20, 4, 0, 500000000, time.UTC),
},
{
name: "Unmarshal a byte string that is not a valid RFC3339 timestamp to time.Time when ByteStringToTime is set to ByteStringToTimeAllowed",
opts: DecOptions{ByteStringToTime: ByteStringToTimeAllowed},
in: hexDecode("4B696E76616C696454657874"), // 'invalidText'
wantErrorMsg: `cbor: cannot set "invalidText" for time.Time: parsing time "invalidText" as "2006-01-02T15:04:05Z07:00": cannot parse "invalidText" as "2006"`,
},
{
name: "Unmarshal a byte string that is not a valid utf8 sequence to time.Time when ByteStringToTime is set to ByteStringToTimeAllowed",
opts: DecOptions{ByteStringToTime: ByteStringToTimeAllowed},
in: hexDecode("54323031338030332D32315432303A30343A30305A"), // "2013\x8003-21T20:04:00Z" -- the first hyphen of a valid RFC3339 string is replaced by a continuation byte
wantErrorMsg: `cbor: cannot set "2013\x8003-21T20:04:00Z" for time.Time: parsing time "2013\x8003-21T20:04:00Z" as "2006-01-02T15:04:05Z07:00": cannot parse "\x8003-21T20:04:00Z" as "-"`,
},
} {
t.Run(tc.name, func(t *testing.T) {
dm, err := tc.opts.DecMode()
if err != nil {
t.Fatal(err)
}

var got time.Time
if err := dm.Unmarshal(tc.in, &got); err != nil {
if tc.wantErrorMsg != err.Error() {
t.Errorf("unexpected error: got %v want %v", err, tc.wantErrorMsg)
}
} else {
compareNonFloats(t, tc.in, got, tc.want)
}
})
}
}
Loading