Repository: fxamacker/cbor
Pull request #540 (2024-05-23 17:06:04)

Fix invalid RFC 3339 in TimeTagToAny (unreleased new feature)
<!--
Thank you for your interest in contributing to fxamacker/cbor!
-->

### Description

`TimeTagToAny` is an unreleased new option that was added by PR 496:
- #496 

Unfortunately, `TimeTagToAny` can produce strings that are not valid RFC 3339 timestamps through its use of (time.Time).Format, whose signature does not allow it to return errors.

<!-- For code contributions, please complete all the items below this line. -->
<!-- For documentation-only contributions, please delete everything below this line. -->

#### PR Was Proposed and Welcomed in Currently Open Issue

- [ ] This PR was proposed and welcomed by maintainer(s) in issue #___
- [ ] Closes or Updates Issue #___

#### 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 lint checks in CI (goimports, gosec, staticcheck, etc.)
- [x] Sign each commit with your real name and email.  
      Last line of each commit message should be in this format:  
      Signed-off-by: Firstname Lastname <firstname.lastname@example.com>
- [x] Certify the Developer's Certificate of Origin 1.1
      (see next section).

#### Certify the Developer's Certificate of Origin 1.1

- [x] By marking this item as completed, I certify 
      the Developer Certificate of Origin 1.1.

```
Developer Certificate of Origin
Version 1.1

Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA

Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
```




Patch:
diff --git a/decode.go b/decode.go
index ae4f365..66599e1 100644
--- a/decode.go
+++ b/decode.go
@@ -1890,12 +1890,24 @@ func (d *decoder) parse(skipSelfDescribedTag bool) (interface{}, error) { //noli
 				if tagNum == 1 {
 					tm = tm.UTC()
 				}
-				return tm.Format(time.RFC3339), nil
+				// Formats to RFC3339 and errors on time.Time values that cannot be
+				// represented by RFC3339.
+				text, err := tm.Truncate(time.Second).MarshalText()
+				if err != nil {
+					return nil, err
+				}
+				return string(text), nil
 			case TimeTagToRFC3339Nano:
 				if tagNum == 1 {
 					tm = tm.UTC()
 				}
-				return tm.Format(time.RFC3339Nano), nil
+				// Formats to RFC3339 with subsecond precision and errors on
+				// time.Time values that cannot be represented by RFC3339.
+				text, err := tm.MarshalText()
+				if err != nil {
+					return nil, err
+				}
+				return string(text), nil
 			default:
 				// not reachable
 			}


Test patch:
diff --git a/decode_test.go b/decode_test.go
index 7e05f30..440f53e 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -8856,10 +8856,11 @@ func TestDecModeInvalidTimeTagToAnyMode(t *testing.T) {
 
 func TestDecModeTimeTagToAny(t *testing.T) {
 	for _, tc := range []struct {
-		name string
-		opts DecOptions
-		in   []byte
-		want interface{}
+		name           string
+		opts           DecOptions
+		in             []byte
+		want           interface{}
+		wantErrMessage string
 	}{
 		{
 			name: "Unmarshal tag 0 data to time.Time when TimeTagToAny is not set",
@@ -8897,6 +8898,30 @@ func TestDecModeTimeTagToAny(t *testing.T) {
 			in:   hexDecode("c1fb41d452d9ec200000"),
 			want: "2013-03-21T20:04:00.5Z",
 		},
+		{
+			name:           "error under TimeTagToRFC3339 when tag 0 contains an invalid RFC3339 timestamp",
+			opts:           DecOptions{TimeTagToAny: TimeTagToRFC3339},
+			in:             hexDecode("c07731303030302D30332D32315432303A30343A30302E355A"), // 0("10000-03-21T20:04:00.5Z")
+			wantErrMessage: `cbor: cannot set 10000-03-21T20:04:00.5Z for time.Time: parsing time "10000-03-21T20:04:00.5Z" as "2006-01-02T15:04:05Z07:00": cannot parse "0-03-21T20:04:00.5Z" as "-"`,
+		},
+		{
+			name:           "error under TimeTagToRFC3339Nano when tag 0 contains an invalid RFC3339 timestamp",
+			opts:           DecOptions{TimeTagToAny: TimeTagToRFC3339Nano},
+			in:             hexDecode("c07731303030302D30332D32315432303A30343A30302E355A"), // 0("10000-03-21T20:04:00.5Z")
+			wantErrMessage: `cbor: cannot set 10000-03-21T20:04:00.5Z for time.Time: parsing time "10000-03-21T20:04:00.5Z" as "2006-01-02T15:04:05Z07:00": cannot parse "0-03-21T20:04:00.5Z" as "-"`,
+		},
+		{
+			name:           "error under TimeTagToRFC3339 when tag 1 represents a time that can't be represented by valid RFC3339",
+			opts:           DecOptions{TimeTagToAny: TimeTagToRFC3339},
+			in:             hexDecode("c11b0000003afff44181"), // 1(253402300801)
+			wantErrMessage: "Time.MarshalText: year outside of range [0,9999]",
+		},
+		{
+			name:           "error under TimeTagToRFC3339Nano when tag 1 represents a time that can't be represented by valid RFC3339",
+			opts:           DecOptions{TimeTagToAny: TimeTagToRFC3339Nano},
+			in:             hexDecode("c11b0000003afff44181"), // 1(253402300801)
+			wantErrMessage: "Time.MarshalText: year outside of range [0,9999]",
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
 			dm, err := tc.opts.DecMode()
@@ -8906,7 +8931,13 @@ func TestDecModeTimeTagToAny(t *testing.T) {
 
 			var got interface{}
 			if err := dm.Unmarshal(tc.in, &got); err != nil {
-				t.Errorf("unexpected error: %v", err)
+				if tc.wantErrMessage == "" {
+					t.Fatalf("unexpected error: %v", err)
+				} else if gotErrMessage := err.Error(); tc.wantErrMessage != gotErrMessage {
+					t.Fatalf("want error %q, got %q", tc.wantErrMessage, gotErrMessage)
+				}
+			} else if tc.wantErrMessage != "" {
+				t.Fatalf("got nil error, want %q", tc.wantErrMessage)
 			}
 
 			compareNonFloats(t, tc.in, got, tc.want)