diff --git a/util/sql3util/arg.go b/util/sql3util/arg.go index ddb8aa8..68fa8ab 100644 --- a/util/sql3util/arg.go +++ b/util/sql3util/arg.go @@ -97,13 +97,13 @@ func ParseTimeShift(s string) (years, months, days int, duration time.Duration, if ok = sign && len(s) >= 10 && s[7] == '-'; !ok { return // !ok } - if years, ok = parseInt(s[0:4]); !ok { + if years, ok = parseInt(s[0:4], 0); !ok { return // !ok } - if months, ok = parseInt(s[5:7]); !ok { + if months, ok = parseInt(s[5:7], 12); !ok { return // !ok } - if days, ok = parseInt(s[8:10]); !ok { + if days, ok = parseInt(s[8:10], 31); !ok { return // !ok } if len(s) == 10 { @@ -121,10 +121,10 @@ func ParseTimeShift(s string) (years, months, days int, duration time.Duration, } var hours, minutes int - if hours, ok = parseInt(s[0:2]); !ok { + if hours, ok = parseInt(s[0:2], 24); !ok { return } - if minutes, ok = parseInt(s[3:5]); !ok { + if minutes, ok = parseInt(s[3:5], 60); !ok { return } duration = time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute @@ -138,7 +138,7 @@ func ParseTimeShift(s string) (years, months, days int, duration time.Duration, // Seconds part: HH:MM:SS var seconds int - if seconds, ok = parseInt(s[6:8]); !ok { + if seconds, ok = parseInt(s[6:8], 60); !ok { return } duration += time.Duration(seconds) * time.Second @@ -153,7 +153,7 @@ func ParseTimeShift(s string) (years, months, days int, duration time.Duration, // Nanosecond part: HH:MM:SS.SSS var nanos int - if nanos, ok = parseInt(s[0:min(9, len(s))]); !ok { + if nanos, ok = parseInt(s[0:min(9, len(s))], 0); !ok { return } for i := len(s); i < 9; i++ { @@ -163,12 +163,12 @@ func ParseTimeShift(s string) (years, months, days int, duration time.Duration, // Subnanosecond part. if len(s) > 9 { - _, ok = parseInt(s[9:]) + _, ok = parseInt(s[9:], 0) } return } -func parseInt(s string) (i int, _ bool) { +func parseInt(s string, max int) (i int, _ bool) { for _, r := range []byte(s) { r -= '0' if r > 9 { @@ -176,5 +176,5 @@ func parseInt(s string) (i int, _ bool) { } i = i*10 + int(r) } - return i, true + return i, max == 0 || i < max } diff --git a/util/sql3util/arg_test.go b/util/sql3util/arg_test.go index a110cbc..87f0137 100644 --- a/util/sql3util/arg_test.go +++ b/util/sql3util/arg_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "github.com/ncruces/go-sqlite3" + _ "github.com/ncruces/go-sqlite3/embed" "github.com/ncruces/go-sqlite3/util/sql3util" ) @@ -63,26 +65,26 @@ func TestParseTimeShift(t *testing.T) { ok bool }{ {"", epoch, false}, - {"0001-12-30", epoch, false}, - {"+_001-12-30", epoch, false}, - {"+0001-_2-30", epoch.AddDate(1, 0, 0), false}, - {"+0001-12-_0", epoch.AddDate(1, 12, 0), false}, - {"+0001-12-30", epoch.AddDate(1, 12, 30), true}, - {"-0001-12-30", epoch.AddDate(-1, -12, -30), true}, - {"+0001-12-30T", epoch.AddDate(1, 12, 30), false}, - {"+0001-12-30 12", epoch.AddDate(1, 12, 30), false}, - {"+0001-12-30 _2:30", epoch.AddDate(1, 12, 30), false}, - {"+0001-12-30 12:_0", epoch.AddDate(1, 12, 30), false}, - {"+0001-12-30 12:30", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 30*time.Minute), true}, - {"+0001-12-30 12:30:", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 30*time.Minute), false}, - {"+0001-12-30 12:30:_0", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 30*time.Minute), false}, - {"+0001-12-30 12:30:60", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 31*time.Minute), true}, - {"+0001-12-30 12:30:60.", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 31*time.Minute), false}, - {"+0001-12-30 12:30:60._", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 31*time.Minute), false}, - {"+0001-12-30 12:30:60.1", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 31*time.Minute + 100*time.Millisecond), true}, - {"+0001-12-30 12:30:60.123456789_", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 31*time.Minute + 123456789), false}, - {"+0001-12-30 12:30:60.1234567890", epoch.AddDate(1, 12, 30).Add(12*time.Hour + 31*time.Minute + 123456789), true}, - {"-12:30:60.1234567890", epoch.Add(-12*time.Hour - 31*time.Minute - 123456789), true}, + {"0001-11-30", epoch, false}, + {"+_001-11-30", epoch, false}, + {"+0001-_1-30", epoch.AddDate(1, 0, 0), false}, + {"+0001-11-_0", epoch.AddDate(1, 11, 0), false}, + {"+0001-11-30", epoch.AddDate(1, 11, 30), true}, + {"-0001-11-30", epoch.AddDate(-1, -11, -30), true}, + {"+0001-11-30T", epoch.AddDate(1, 11, 30), false}, + {"+0001-11-30 12", epoch.AddDate(1, 11, 30), false}, + {"+0001-11-30 _2:30", epoch.AddDate(1, 11, 30), false}, + {"+0001-11-30 12:_0", epoch.AddDate(1, 11, 30), false}, + {"+0001-11-30 12:30", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute), true}, + {"+0001-11-30 12:30:", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute), false}, + {"+0001-11-30 12:30:_0", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute), false}, + {"+0001-11-30 12:30:59", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute + 59*time.Second), true}, + {"+0001-11-30 12:30:59.", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute + 59*time.Second), false}, + {"+0001-11-30 12:30:59._", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute + 59*time.Second), false}, + {"+0001-11-30 12:30:59.1", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute + 59*time.Second + 100*time.Millisecond), true}, + {"+0001-11-30 12:30:59.123456789_", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute + 59*time.Second + 123456789), false}, + {"+0001-11-30 12:30:59.1234567890", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute + 59*time.Second + 123456789), true}, + {"-12:30:59.1234567890", epoch.Add(-12*time.Hour - 30*time.Minute - 59*time.Second - 123456789), true}, } for _, tt := range tests { t.Run(tt.str, func(t *testing.T) { @@ -94,3 +96,72 @@ func TestParseTimeShift(t *testing.T) { }) } } + +func FuzzParseTimeShift(f *testing.F) { + f.Add("") + f.Add("0001-12-30") + f.Add("+_001-12-30") + f.Add("+0001-_2-30") + f.Add("+0001-12-_0") + f.Add("+0001-12-30") + f.Add("-0001-12-30") + f.Add("+0001-12-30T") + f.Add("+0001-12-30 12") + f.Add("+0001-12-30 _2:30") + f.Add("+0001-12-30 12:_0") + f.Add("+0001-12-30 12:30") + f.Add("+0001-12-30 12:30:") + f.Add("+0001-12-30 12:30:_0") + f.Add("+0001-12-30 12:30:60") + f.Add("+0001-12-30 12:30:60.") + f.Add("+0001-12-30 12:30:60._") + f.Add("+0001-12-30 12:30:60.1") + f.Add("+0001-12-30 12:30:60.123456789_") + f.Add("+0001-12-30 12:30:60.1234567890") + f.Add("-12:30:60.1234567890") + + c, err := sqlite3.Open(":memory:") + if err != nil { + f.Fatal(err) + } + defer c.Close() + + s, _, err := c.Prepare(`SELECT julianday('00:00', ?)`) + if err != nil { + f.Fatal(err) + } + defer s.Close() + + // Default SQLite date. + epoch := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + + f.Fuzz(func(t *testing.T, str string) { + years, months, days, duration, ok := sql3util.ParseTimeShift(str) + + // Account for a full 400 year cycle. + if years < -200 || years > +200 { + t.Skip() + } + // SQLite only tracks milliseconds. + if duration != duration.Truncate(time.Millisecond) { + t.Skip() + } + + if ok { + s.Reset() + s.BindText(1, str) + if !s.Step() { + t.Fail() + } + + got := epoch.AddDate(years, months, days).Add(duration) + + // Julian day introduces floating point inaccuracy. + want := s.ColumnTime(0, sqlite3.TimeFormatJulianDay) + want = want.Round(time.Millisecond) + if !got.Equal(want) { + t.Fatalf("with %q, got %v, want %v", str, got, want) + } + } + }) +}