diff --git a/loopdb/sql_test.go b/loopdb/sql_test.go index b30ea1b..831cafd 100644 --- a/loopdb/sql_test.go +++ b/loopdb/sql_test.go @@ -328,8 +328,10 @@ func TestIssue615(t *testing.T) { // Create a faulty loopout swap. destAddr := test.GetDestAddr(t, 0) - faultyTime, err := parseSqliteTimeStamp("55563-06-27 02:09:24 +0000 UTC") - require.NoError(t, err) + // Corresponds to 55563-06-27 02:09:24 +0000 UTC. + faultyTime := time.Unix(1691247002964, 0) + + t.Log(faultyTime.Unix()) unrestrictedSwap := LoopOutContract{ SwapContract: SwapContract{ @@ -362,7 +364,7 @@ func TestIssue615(t *testing.T) { SwapPublicationDeadline: faultyTime, } - err = sqlDB.CreateLoopOut(ctxb, testPreimage.Hash(), &unrestrictedSwap) + err := sqlDB.CreateLoopOut(ctxb, testPreimage.Hash(), &unrestrictedSwap) require.NoError(t, err) // This should fail because of the faulty timestamp. @@ -394,30 +396,54 @@ func TestTimeConversions(t *testing.T) { }, { timeString: "2018-11-01 00:00:01.10000 +0000 UTC", - expectedTime: time.Date(2018, 11, 1, 0, 0, 1, 0, time.UTC), + expectedTime: time.Date(2018, 11, 1, 0, 0, 1, 100000000, time.UTC), }, { timeString: "2053-12-29T02:40:44.269009408Z", expectedTime: time.Date( - 2053, 12, 29, 2, 40, 44, 0, time.UTC, + time.Now().Year(), 12, 29, 2, 40, 44, 269009408, time.UTC, ), }, { timeString: "55563-06-27 02:09:24 +0000 UTC", expectedTime: time.Date( - 55563, 6, 27, 2, 9, 24, 0, time.UTC, + time.Now().Year(), 6, 27, 2, 9, 24, 0, time.UTC, ), }, { timeString: "2172-03-11 10:01:11.849906176 +0000 UTC", expectedTime: time.Date( - 2172, 3, 11, 10, 1, 11, 0, time.UTC, + time.Now().Year(), 3, 11, 10, 1, 11, 849906176, time.UTC, + ), + }, + { + timeString: "2023-08-04 16:07:49 +0800 CST", + expectedTime: time.Date( + 2023, 8, 4, 8, 7, 49, 0, time.UTC, + ), + }, + { + timeString: "2023-08-04 16:07:49 -0700 MST", + expectedTime: time.Date( + 2023, 8, 4, 23, 7, 49, 0, time.UTC, + ), + }, + { + timeString: "2023-08-04T16:07:49+08:00", + expectedTime: time.Date( + 2023, 8, 4, 8, 7, 49, 0, time.UTC, + ), + }, + { + timeString: "2023-08-04T16:07:49+08:00", + expectedTime: time.Date( + 2023, 8, 4, 8, 7, 49, 0, time.UTC, ), }, } for _, test := range tests { - time, err := parseTimeStamp(test.timeString) + time, err := fixTimeStamp(test.timeString) require.NoError(t, err) require.Equal(t, test.expectedTime, time) } diff --git a/loopdb/sqlite.go b/loopdb/sqlite.go index f8e22bd..87bf9fa 100644 --- a/loopdb/sqlite.go +++ b/loopdb/sqlite.go @@ -3,6 +3,7 @@ package loopdb import ( "context" "database/sql" + "errors" "fmt" "net/url" "path/filepath" @@ -247,19 +248,25 @@ func (b *BaseDB) FixFaultyTimestamps(ctx context.Context) error { defer tx.Rollback() //nolint: errcheck for _, swap := range loopOutSwaps { - faultyTime, err := parseTimeStamp(swap.PublicationDeadline) + + // Get the year of the timestamp. + year, err := getTimeStampYear(swap.PublicationDeadline) if err != nil { return err } - // Skip if the time is not faulty. - if !isMilisecondsTime(faultyTime.Unix()) { + // Skip if the year is not in the future. + thisYear := time.Now().Year() + if year <= thisYear { continue } + fixedTime, err := fixTimeStamp(swap.PublicationDeadline) + if err != nil { + return err + } + // Update the faulty time to a valid time. - secs := faultyTime.Unix() / 1000 - correctTime := time.Unix(secs, 0) _, err = tx.ExecContext( ctx, ` UPDATE @@ -269,7 +276,7 @@ func (b *BaseDB) FixFaultyTimestamps(ctx context.Context) error { WHERE swap_hash = $2; `, - correctTime, swap.Hash, + fixedTime, swap.Hash, ) if err != nil { return err @@ -308,120 +315,84 @@ func (r *SqliteTxOptions) ReadOnly() bool { return r.readOnly } -// parseTimeStamp tries to parse a timestamp string with both the +// fixTimeStamp tries to parse a timestamp string with both the // parseSqliteTimeStamp and parsePostgresTimeStamp functions. // If both fail, it returns an error. -func parseTimeStamp(dateTimeStr string) (time.Time, error) { - t, err := parseSqliteTimeStamp(dateTimeStr) +func fixTimeStamp(dateTimeStr string) (time.Time, error) { + year, err := getTimeStampYear(dateTimeStr) if err != nil { - t, err = parsePostgresTimeStamp(dateTimeStr) - if err != nil { - return time.Time{}, err - } - } - - return t, nil -} - -// parseSqliteTimeStamp parses a timestamp string in the format of -// "YYYY-MM-DD HH:MM:SS +0000 UTC" and returns a time.Time value. -// NOTE: we can't use time.Parse() because it doesn't support having years -// with more than 4 digits. -func parseSqliteTimeStamp(dateTimeStr string) (time.Time, error) { - // Split the date and time parts. - parts := strings.Fields(strings.TrimSpace(dateTimeStr)) - if len(parts) < 2 { - return time.Time{}, fmt.Errorf("invalid timestamp format: %v", - dateTimeStr) - } - - datePart, timePart := parts[0], parts[1] - - return parseTimeParts(datePart, timePart) -} - -// parseSqliteTimeStamp parses a timestamp string in the format of -// "YYYY-MM-DDTHH:MM:SSZ" and returns a time.Time value. -// NOTE: we can't use time.Parse() because it doesn't support having years -// with more than 4 digits. -func parsePostgresTimeStamp(dateTimeStr string) (time.Time, error) { - // Split the date and time parts. - parts := strings.Split(dateTimeStr, "T") - if len(parts) != 2 { - return time.Time{}, fmt.Errorf("invalid timestamp format: %v", - dateTimeStr) + return time.Time{}, err } - datePart, timePart := parts[0], strings.TrimSuffix(parts[1], "Z") - - return parseTimeParts(datePart, timePart) -} - -// parseTimeParts takes a datePart string in the format of "YYYY-MM-DD" and -// a timePart string in the format of "HH:MM:SS" and returns a time.Time value. -func parseTimeParts(datePart, timePart string) (time.Time, error) { - // Parse the date. - dateParts := strings.Split(datePart, "-") - if len(dateParts) != 3 { - return time.Time{}, fmt.Errorf("invalid date format: %v", - datePart) + // If the year is in the future. It was a faulty timestamp. + thisYear := time.Now().Year() + if year > thisYear { + dateTimeStr = strings.Replace( + dateTimeStr, + fmt.Sprintf("%d", year), + fmt.Sprintf("%d", thisYear), + 1, + ) } - year, err := strconv.Atoi(dateParts[0]) + parsedTime, err := parseLayouts(defaultLayouts(), dateTimeStr) if err != nil { - return time.Time{}, err + return time.Time{}, fmt.Errorf("unable to parse timestamp %v: %v", + dateTimeStr, err) } - month, err := strconv.Atoi(dateParts[1]) - if err != nil { - return time.Time{}, err - } + return parsedTime.UTC(), nil +} - day, err := strconv.Atoi(dateParts[2]) - if err != nil { - return time.Time{}, err +// parseLayouts parses time based on a list of provided layouts. +// If layouts is empty list or nil, the error with unknown layout will be returned. +func parseLayouts(layouts []string, dateTime string) (time.Time, error) { + for _, layout := range layouts { + parsedTime, err := time.Parse(layout, dateTime) + if err == nil { + return parsedTime, nil + } } - // Parse the time. - timeParts := strings.Split(timePart, ":") - if len(timeParts) != 3 { - return time.Time{}, fmt.Errorf("invalid time format: %v", - timePart) - } + return time.Time{}, errors.New("unknown layout") +} - hour, err := strconv.Atoi(timeParts[0]) - if err != nil { - return time.Time{}, err +// defaultLayouts returns a default list of ALL supported layouts. +// This function returns new copy of a slice. +func defaultLayouts() []string { + return []string{ + "2006-01-02 15:04:05.99999 -0700 MST", // Custom sqlite layout. + time.RFC3339Nano, + time.RFC3339, + time.RFC1123Z, + time.RFC1123, + time.RFC850, + time.RFC822Z, + time.RFC822, + time.Layout, + time.RubyDate, + time.UnixDate, + time.ANSIC, + time.StampNano, + time.StampMicro, + time.StampMilli, + time.Stamp, + time.Kitchen, } +} - minute, err := strconv.Atoi(timeParts[1]) - if err != nil { - return time.Time{}, err +// getTimeStampYear returns the year of a timestamp string. +func getTimeStampYear(dateTimeStr string) (int, error) { + parts := strings.Split(dateTimeStr, "-") + if len(parts) < 1 { + return 0, fmt.Errorf("invalid timestamp format: %v", + dateTimeStr) } - // Parse the seconds and ignore the fractional part. - secondParts := strings.Split(timeParts[2], ".") - - second, err := strconv.Atoi(secondParts[0]) + year, err := strconv.Atoi(parts[0]) if err != nil { - return time.Time{}, err + return 0, fmt.Errorf("unable to parse year: %v", err) } - // Construct a time.Time value. - return time.Date( - year, time.Month(month), day, hour, minute, second, 0, time.UTC, - ), nil -} - -// isMilisecondsTime returns true if the unix timestamp is likely in -// milliseconds. -func isMilisecondsTime(unixTimestamp int64) bool { - length := len(fmt.Sprintf("%d", unixTimestamp)) - if length >= 13 { - // Likely a millisecond timestamp - return true - } else { - // Likely a second timestamp - return false - } + return year, nil }