diff --git a/decode.go b/decode.go index f0ec3b1..d0eb615 100644 --- a/decode.go +++ b/decode.go @@ -144,13 +144,23 @@ func parseDateTime(b []byte) (time.Time, error) { return time.Time{}, unstable.NewParserError(b, "extra bytes at the end of the timezone") } + // Normalize leap seconds (second=60) to second=59 to prevent overflow + // when Go's time.Date normalizes the time. This is necessary because + // time.Date(9999, 12, 31, 23, 59, 60, 0, UTC) normalizes to year 10000, + // which is outside the valid TOML date range (0000-9999). + // See: https://github.com/pelletier/go-toml/issues/1015 + second := dt.Second + if second == 60 { + second = 59 + } + t := time.Date( dt.Year, time.Month(dt.Month), dt.Day, dt.Hour, dt.Minute, - dt.Second, + second, dt.Nanosecond, zone) diff --git a/localtime.go b/localtime.go index a856bfd..669a9a7 100644 --- a/localtime.go +++ b/localtime.go @@ -94,7 +94,13 @@ type LocalDateTime struct { // AsTime converts d into a specific time instance in zone. func (d LocalDateTime) AsTime(zone *time.Location) time.Time { - return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, d.Second, d.Nanosecond, zone) + // Normalize leap seconds (second=60) to second=59 to prevent overflow + // when Go's time.Date normalizes the time. + second := d.Second + if second == 60 { + second = 59 + } + return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, second, d.Nanosecond, zone) } // String returns RFC 3339 representation of d. diff --git a/unmarshaler_test.go b/unmarshaler_test.go index 14f3db9..d643788 100644 --- a/unmarshaler_test.go +++ b/unmarshaler_test.go @@ -4242,3 +4242,66 @@ func TestIssue995_SliceNonEmpty_UsesLastElement(t *testing.T) { t.Fatalf("unexpected values in allowlists: %v", got) } } + +// TestLeapSecondRoundTrip tests that leap seconds (second=60) don't cause +// year overflow issues during round-trip marshaling. This reproduces OSS-Fuzz +// issue 472183443. +func TestLeapSecondRoundTrip(t *testing.T) { + // This is the test case from OSS-Fuzz issue #1015 + input := []byte("s=9999-12-31 23:59:60z") + + var v interface{} + err := toml.Unmarshal(input, &v) + assert.NoError(t, err) + + // Marshal back to TOML + encoded, err := toml.Marshal(v) + assert.NoError(t, err) + + // Unmarshal again - this should not fail with year overflow + var v2 interface{} + err = toml.Unmarshal(encoded, &v2) + assert.NoError(t, err) +} + +// TestLeapSecondVariants tests various leap second edge cases +func TestLeapSecondVariants(t *testing.T) { + testCases := []struct { + name string + input string + }{ + { + name: "leap second with UTC offset datetime", + input: "s=9999-12-31 23:59:60z", + }, + { + name: "leap second with positive offset", + input: "s=9999-12-31 23:59:60+00:00", + }, + { + name: "leap second with negative offset", + input: "s=9999-12-31 23:59:60-05:00", + }, + { + name: "leap second earlier in year", + input: "s=2015-06-30 23:59:60z", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var v interface{} + err := toml.Unmarshal([]byte(tc.input), &v) + assert.NoError(t, err) + + // Marshal back to TOML + encoded, err := toml.Marshal(v) + assert.NoError(t, err) + + // Unmarshal again - this should not fail + var v2 interface{} + err = toml.Unmarshal(encoded, &v2) + assert.NoError(t, err) + }) + } +}