Fix every finding surfaced by the canonical .golangci.yml with golangci-lint v2.12.2 (refs #61), behavior-preserving throughout: - err113: dynamic errors replaced with package-level sentinels and %w wrapping; direct comparisons converted to errors.Is - goprintffuncname: printf-style helpers renamed with an f suffix (ui.Writer message methods, cli.ReportErrorf, database.Fatalf, vaultik stdoutf) and all call sites updated - revive: stuttering type names renamed (blob.Handler, blob.WithReader, blob.ChunkPosition, storage.URL, storage.Info), doc comments added, unused parameters blanked, package comments added - contextcheck/noctx: ctx threaded through blob.Packer (AddChunk/Flush/FinalizeBlob/PackChunks) and scanner call sites; context-aware exec and sql variants used - funlen/cyclop/gocognit/nestif/dupl: oversized or duplicated functions split into focused helpers across production and test code - paralleltest/tparallel/thelper/usetesting/testpackage: tests parallelized where safe (global log.Initialize kept in the serial phase), helpers marked, t.TempDir adopted, external test packages where only exported API is used - gosec: integer conversions clamped or justified, header timeouts added, remaining findings suppressed with per-site justifications - mnd/goconst/lll/wsl_v5/nlreturn/noinlineerr/errcheck and other mechanical findings fixed directly Remove the deprecated log.LogOptions alias (callers migrated to log.Options). make check is green.
127 lines
3.5 KiB
Go
127 lines
3.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Approximate lengths of the extended calendar units accepted by
|
|
// parseDuration.
|
|
const (
|
|
durationDay = 24 * time.Hour
|
|
durationWeek = 7 * durationDay
|
|
durationMonth = 30 * durationDay
|
|
durationYear = 365 * durationDay
|
|
)
|
|
|
|
var (
|
|
errNegativeDuration = errors.New("negative durations are not supported")
|
|
errInvalidDuration = errors.New("invalid duration format")
|
|
errUnknownTimeUnit = errors.New("unknown time unit")
|
|
)
|
|
|
|
// parseDuration parses duration strings. Supports standard Go duration format
|
|
// (e.g., "3h30m", "1h45m30s") as well as extended units:
|
|
// - d: days (e.g., "30d", "7d")
|
|
// - w: weeks (e.g., "2w", "4w")
|
|
// - mo: months (30 days) (e.g., "6mo", "1mo")
|
|
// - y: years (365 days) (e.g., "1y", "2y")
|
|
//
|
|
// Can combine units: "1y6mo", "2w3d", "1d12h30m"
|
|
func parseDuration(s string) (time.Duration, error) {
|
|
// First try standard Go duration parsing
|
|
d, err := time.ParseDuration(s)
|
|
if err == nil {
|
|
return d, nil
|
|
}
|
|
|
|
// Extended duration parsing
|
|
// Check for negative values
|
|
if strings.HasPrefix(strings.TrimSpace(s), "-") {
|
|
return 0, errNegativeDuration
|
|
}
|
|
|
|
// Pattern matches: number + unit, repeated
|
|
re := regexp.MustCompile(`(\d+(?:\.\d+)?)\s*([a-zA-Z]+)`)
|
|
matches := re.FindAllStringSubmatch(s, -1)
|
|
|
|
if len(matches) == 0 {
|
|
return 0, fmt.Errorf("%w: %q", errInvalidDuration, s)
|
|
}
|
|
|
|
var total time.Duration
|
|
|
|
for _, match := range matches {
|
|
valueStr := match[1]
|
|
unit := strings.ToLower(match[2])
|
|
|
|
value, err := strconv.ParseFloat(valueStr, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid number %q: %w", valueStr, err)
|
|
}
|
|
|
|
d, err := durationForUnit(value, unit)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
total += d
|
|
}
|
|
|
|
return total, nil
|
|
}
|
|
|
|
// durationForUnit converts a value with a (case-normalized) unit suffix
|
|
// into a time.Duration, accepting Go's standard units plus the extended
|
|
// calendar units.
|
|
func durationForUnit(value float64, unit string) (time.Duration, error) {
|
|
switch unit {
|
|
// Standard time units
|
|
case "ns", "nanosecond", "nanoseconds":
|
|
return time.Duration(value), nil
|
|
case "us", "µs", "microsecond", "microseconds":
|
|
return time.Duration(value * float64(time.Microsecond)), nil
|
|
case "ms", "millisecond", "milliseconds":
|
|
return time.Duration(value * float64(time.Millisecond)), nil
|
|
case "s", "sec", "second", "seconds":
|
|
return time.Duration(value * float64(time.Second)), nil
|
|
case "m", "min", "minute", "minutes":
|
|
return time.Duration(value * float64(time.Minute)), nil
|
|
case "h", "hr", "hour", "hours":
|
|
return time.Duration(value * float64(time.Hour)), nil
|
|
// Extended units
|
|
case "d", "day", "days":
|
|
return time.Duration(value * float64(durationDay)), nil
|
|
case "w", "week", "weeks":
|
|
return time.Duration(value * float64(durationWeek)), nil
|
|
case "mo", "month", "months":
|
|
// Using 30 days as approximation
|
|
return time.Duration(value * float64(durationMonth)), nil
|
|
case "y", "year", "years":
|
|
// Using 365 days as approximation
|
|
return time.Duration(value * float64(durationYear)), nil
|
|
default:
|
|
// Try parsing as standard Go duration unit
|
|
testStr := "1" + unit
|
|
|
|
_, err := time.ParseDuration(testStr)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%w: %q", errUnknownTimeUnit, unit)
|
|
}
|
|
|
|
// It's a valid Go duration unit, parse the full value
|
|
fullStr := fmt.Sprintf("%g%s", value, unit)
|
|
|
|
d, err := time.ParseDuration(fullStr)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid duration %q: %w", fullStr, err)
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
}
|