From 915a7810d7379deee17ac1a948c9569cbb523c7d Mon Sep 17 00:00:00 2001 From: Qian Qiao Date: Mon, 2 May 2022 03:28:54 +0000 Subject: [PATCH 1/4] Fixed integer overflow in NumericDate.MarshalJSON The original issue was caused by the fact that if we use a very large unix timestamp. The resulting value from time.UnixNano overflows a int64, as documented here: https://pkg.go.dev/time#Time.UnixNano. This patch works around the issue by calculating the second part and nanosecond part separately, taking the second part from time.Unix, and the nanosecond part from time.Nanosecond and then adding the results together. --- types.go | 3 ++- types_test.go | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/types.go b/types.go index 2c647fd2..720e3af8 100644 --- a/types.go +++ b/types.go @@ -53,7 +53,8 @@ func (date NumericDate) MarshalJSON() (b []byte, err error) { if TimePrecision < time.Second { prec = int(math.Log10(float64(time.Second) / float64(TimePrecision))) } - f := float64(date.Truncate(TimePrecision).UnixNano()) / float64(time.Second) + trancatedDate := date.Truncate(TimePrecision) + f := float64(trancatedDate.Unix()) + float64(trancatedDate.Nanosecond()) / float64(time.Second) return []byte(strconv.FormatFloat(f, 'f', prec, 64)), nil } diff --git a/types_test.go b/types_test.go index bc28605a..deffd439 100644 --- a/types_test.go +++ b/types_test.go @@ -79,14 +79,19 @@ func TestNumericDate_MarshalJSON(t *testing.T) { }{ {time.Unix(5243700879, 0), "5243700879", time.Second}, {time.Unix(5243700879, 0), "5243700879.000", time.Millisecond}, - {time.Unix(5243700879, 0), "5243700879.000001", time.Microsecond}, - {time.Unix(5243700879, 0), "5243700879.000000954", time.Nanosecond}, + {time.Unix(5243700879, 0), "5243700879.000000", time.Microsecond}, + {time.Unix(5243700879, 0), "5243700879.000000000", time.Nanosecond}, // {time.Unix(4239425898, 0), "4239425898", time.Second}, {time.Unix(4239425898, 0), "4239425898.000", time.Millisecond}, {time.Unix(4239425898, 0), "4239425898.000000", time.Microsecond}, {time.Unix(4239425898, 0), "4239425898.000000000", time.Nanosecond}, // + {time.Unix(253402271999, 0), "253402271999", time.Second}, + {time.Unix(253402271999, 0), "253402271999.000", time.Millisecond}, + {time.Unix(253402271999, 0), "253402271999.000000", time.Microsecond}, + {time.Unix(253402271999, 0), "253402271999.000000000", time.Nanosecond}, + // {time.Unix(0, 1644285000210402000), "1644285000", time.Second}, {time.Unix(0, 1644285000210402000), "1644285000.210", time.Millisecond}, {time.Unix(0, 1644285000210402000), "1644285000.210402", time.Microsecond}, From eb2e058ac18a1a3b651fc94e288870fafe05e3de Mon Sep 17 00:00:00 2001 From: Qian Qiao Date: Tue, 3 May 2022 02:11:04 +0000 Subject: [PATCH 2/4] Supporting even larger timestamps We now format the whole part and the decimal part separately. Since the whole part is by definition, we need not covert it to a float, hence the whole part can simply be formatted as an integer. The and since by definition of the `Time.Nanoseconds()`, the return value is between 0-999999999, only this part needs to be formatted separately. We then combine whole + decimals[1:] to form the final result. This allows us to correctly format the maximun timestamp that is allowed by Go. --- types.go | 6 ++++-- types_test.go | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/types.go b/types.go index 720e3af8..92c51b9d 100644 --- a/types.go +++ b/types.go @@ -54,9 +54,11 @@ func (date NumericDate) MarshalJSON() (b []byte, err error) { prec = int(math.Log10(float64(time.Second) / float64(TimePrecision))) } trancatedDate := date.Truncate(TimePrecision) - f := float64(trancatedDate.Unix()) + float64(trancatedDate.Nanosecond()) / float64(time.Second) + whole := strconv.FormatInt(trancatedDate.Unix(), 10) + decimal := strconv.FormatFloat(float64(trancatedDate.Nanosecond())/float64(time.Second), 'f', prec, 64) + f := append([]byte(whole), []byte(decimal)[1:]...) - return []byte(strconv.FormatFloat(f, 'f', prec, 64)), nil + return f, nil } // UnmarshalJSON is an implementation of the json.RawMessage interface and deserializses a diff --git a/types_test.go b/types_test.go index deffd439..b26c2bef 100644 --- a/types_test.go +++ b/types_test.go @@ -2,6 +2,7 @@ package jwt_test import ( "encoding/json" + "math" "testing" "time" @@ -95,12 +96,22 @@ func TestNumericDate_MarshalJSON(t *testing.T) { {time.Unix(0, 1644285000210402000), "1644285000", time.Second}, {time.Unix(0, 1644285000210402000), "1644285000.210", time.Millisecond}, {time.Unix(0, 1644285000210402000), "1644285000.210402", time.Microsecond}, - {time.Unix(0, 1644285000210402000), "1644285000.210402012", time.Nanosecond}, + {time.Unix(0, 1644285000210402000), "1644285000.210402000", time.Nanosecond}, // {time.Unix(0, 1644285315063096000), "1644285315", time.Second}, {time.Unix(0, 1644285315063096000), "1644285315.063", time.Millisecond}, {time.Unix(0, 1644285315063096000), "1644285315.063096", time.Microsecond}, - {time.Unix(0, 1644285315063096000), "1644285315.063096046", time.Nanosecond}, + {time.Unix(0, 1644285315063096000), "1644285315.063096000", time.Nanosecond}, + // Maximum time that a go time.Time can represent + {time.Unix(math.MaxInt64, 999999999), "9223372036854775807", time.Second}, + {time.Unix(math.MaxInt64, 999999999), "9223372036854775807.999", time.Millisecond}, + {time.Unix(math.MaxInt64, 999999999), "9223372036854775807.999999", time.Microsecond}, + {time.Unix(math.MaxInt64, 999999999), "9223372036854775807.999999999", time.Nanosecond}, + // Strange precisions + {time.Unix(math.MaxInt64, 999999999), "9223372036854775807", time.Second}, + {time.Unix(math.MaxInt64, 999999999), "9223372036854775756", time.Minute}, + {time.Unix(math.MaxInt64, 999999999), "9223372036854774016", time.Hour}, + {time.Unix(math.MaxInt64, 999999999), "9223372036854745216", 24 * time.Hour}, } for i, tc := range tt { From 585fa254657377abdc3f0543b58fde96113df93e Mon Sep 17 00:00:00 2001 From: Qian Qiao Date: Mon, 30 May 2022 19:17:25 +0000 Subject: [PATCH 3/4] Added source code comments to document the reason for the approach used to marshal times. Improved variable naming and fixed typos. --- types.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/types.go b/types.go index 92c51b9d..252e6aca 100644 --- a/types.go +++ b/types.go @@ -53,12 +53,22 @@ func (date NumericDate) MarshalJSON() (b []byte, err error) { if TimePrecision < time.Second { prec = int(math.Log10(float64(time.Second) / float64(TimePrecision))) } - trancatedDate := date.Truncate(TimePrecision) - whole := strconv.FormatInt(trancatedDate.Unix(), 10) - decimal := strconv.FormatFloat(float64(trancatedDate.Nanosecond())/float64(time.Second), 'f', prec, 64) - f := append([]byte(whole), []byte(decimal)[1:]...) - - return f, nil + truncatedDate := date.Truncate(TimePrecision) + + // For very large timestamps, UnixNano would overflow an int64, but this + // function requires nanosecond level precision, so we have to use the + // following technique to get round the issue: + // 1. Take the normal unix timestamp to form the seconds number part, + // 2. Take the result of the Nanosecond function, which retuns the offset + // within the second of the particular unix time instance, to form the + // decimal part of the result + // 3. Concatenate the seconds and the decimal part to produce the final result + seconds := strconv.FormatInt(truncatedDate.Unix(), 10) + nanosecondsOffset := strconv.FormatFloat(float64(truncatedDate.Nanosecond())/float64(time.Second), 'f', prec, 64) + + output := append([]byte(seconds), []byte(nanosecondsOffset)[1:]...) + + return output, nil } // UnmarshalJSON is an implementation of the json.RawMessage interface and deserializses a From 769fec999041307398a5407c0e71b97f6646f08a Mon Sep 17 00:00:00 2001 From: Qian Qiao Date: Mon, 30 May 2022 19:20:58 +0000 Subject: [PATCH 4/4] Improved the wording of the comments --- types.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/types.go b/types.go index 252e6aca..ac8e140e 100644 --- a/types.go +++ b/types.go @@ -58,11 +58,12 @@ func (date NumericDate) MarshalJSON() (b []byte, err error) { // For very large timestamps, UnixNano would overflow an int64, but this // function requires nanosecond level precision, so we have to use the // following technique to get round the issue: - // 1. Take the normal unix timestamp to form the seconds number part, + // 1. Take the normal unix timestamp to form the whole number part of the + // output, // 2. Take the result of the Nanosecond function, which retuns the offset // within the second of the particular unix time instance, to form the - // decimal part of the result - // 3. Concatenate the seconds and the decimal part to produce the final result + // decimal part of the output + // 3. Concatenate them to produce the final result seconds := strconv.FormatInt(truncatedDate.Unix(), 10) nanosecondsOffset := strconv.FormatFloat(float64(truncatedDate.Nanosecond())/float64(time.Second), 'f', prec, 64)