From 3cd538fad689a8eb7014fb6e4e19401c6a4eb05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Thu, 17 Sep 2026 21:29:12 +0200 Subject: [PATCH] fix: reject an out-of-range date-time offset Assisted-by: GLM 5.3 Flash --- datetime.go | 14 ++++++++++++++ interpres_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/datetime.go b/datetime.go index 6686f84..6710b1d 100644 --- a/datetime.go +++ b/datetime.go @@ -6,6 +6,7 @@ package interpres import ( "fmt" "regexp" + "strconv" "strings" "time" ) @@ -78,6 +79,12 @@ var dateTimeShape = regexp.MustCompile( `|^\d{2}:\d{2}:\d{2}(\.\d+)?$`, ) +// offsetBounds extracts the numeric offset of a date-time. The ABNF bounds it +// to 00:00 through 23:59, but time.Parse accepts values outside that range +// and rolls them over (for example "+00:60" becomes "+01:00"), so the bounds +// are enforced here. +var offsetBounds = regexp.MustCompile(`([+-])(\d{2}):(\d{2})$`) + // parseDateTime classifies and parses a bare token as a TOML date-time value. // It returns the decoded value (time.Time, LocalDateTime, LocalDate, or // LocalTime) and whether the token was a date-time at all. @@ -91,6 +98,13 @@ func parseDateTime(tok string) (any, bool) { if !dateTimeShape.MatchString(tok) { return nil, false } + if m := offsetBounds.FindStringSubmatch(tok); m != nil { + hour, _ := strconv.Atoi(m[2]) + minute, _ := strconv.Atoi(m[3]) + if hour > 23 || minute > 59 { + return nil, false + } + } // The ABNF accepts lowercase "t"/"z"; time.Parse only matches uppercase. norm := strings.ToUpper(tok) for _, layout := range offsetDateTimeLayouts { diff --git a/interpres_test.go b/interpres_test.go index 2dc047f..7dc416f 100644 --- a/interpres_test.go +++ b/interpres_test.go @@ -380,6 +380,34 @@ func TestRejectsInvalidNumbers(t *testing.T) { } } +func TestParseRejectsOffsetOutOfRange(t *testing.T) { + for _, tok := range []string{ + "1979-05-27T07:32:00+00:60", + "1979-05-27T07:32:00-00:99", + "1979-05-27T07:32:00+24:00", + "1979-05-27T07:32:00+99:99", + } { + if _, err := Parse([]byte("v = " + tok + "\n")); err == nil { + t.Errorf("%q: expected an error, got none", tok) + } + } +} + +func TestParseAcceptsOffsetBounds(t *testing.T) { + tree, err := Parse([]byte("a = 1979-05-27T07:32:00+23:59\nb = 1979-05-27T07:32:00-23:59\n")) + if err != nil { + t.Fatalf("parse: %v", err) + } + a := tree["a"].(time.Time) + if _, offset := a.Zone(); offset != 23*3600+59*60 { + t.Fatalf("a offset = %d, want %d", offset, 23*3600+59*60) + } + b := tree["b"].(time.Time) + if _, offset := b.Zone(); offset != -(23*3600 + 59*60) { + t.Fatalf("b offset = %d", offset) + } +} + func TestAcceptsNumberEdgeCases(t *testing.T) { cases := map[string]any{ "0": int64(0),