aboutsummaryrefslogtreecommitdiff
path: root/internal/cli/duration.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/cli/duration.go')
-rw-r--r--internal/cli/duration.go136
1 files changed, 136 insertions, 0 deletions
diff --git a/internal/cli/duration.go b/internal/cli/duration.go
new file mode 100644
index 0000000..ee40562
--- /dev/null
+++ b/internal/cli/duration.go
@@ -0,0 +1,136 @@
+package cli
+
+import (
+ "fmt"
+ "math"
+ "strings"
+ "time"
+
+ "github.com/spf13/pflag"
+)
+
+// Per-component upper bounds. A cache-safety grace window is measured in days
+// to a few years; anything past these is a typo or an attempt to overflow the
+// cutoff arithmetic. They are chosen so cutoff = now.AddDate(-y,-m,-d) can never
+// overflow the int year addition or the int64 range of time.Time (representable
+// to ~year 292 billion), so the destructive cutoff can never silently wrap.
+const (
+ maxDurationYears = 100000
+ maxDurationMonths = 100000 * 12
+ maxDurationDays = 100000 * 366
+)
+
+var _ pflag.Value = (*duration)(nil)
+
+// duration is a calendar-aware grace window . It holds a tuple
+// (years, months, days) rather than a time.Duration.
+type duration struct {
+ years int
+ months int
+ days int
+}
+
+func registerDurationFlag(flags *pflag.FlagSet, v *duration, name string, def string, usage string) {
+ if err := v.Set(def); err != nil {
+ panic(fmt.Sprintf("invalid default for --%s: %v", name, err))
+ }
+ flags.Var(v, name, usage)
+}
+
+// Type implements the `pflags.Value` interface.
+func (d *duration) Type() string {
+ return "duration"
+}
+
+// String implements the `pflags.Value` interface.
+func (d *duration) String() string {
+ sb := &strings.Builder{}
+ if d.years != 0 {
+ fmt.Fprintf(sb, "%dy", d.years)
+ }
+ if d.months != 0 {
+ fmt.Fprintf(sb, "%dm", d.months)
+ }
+ if d.days != 0 {
+ fmt.Fprintf(sb, "%dd", d.days)
+ }
+
+ if sb.Len() == 0 {
+ return "0d"
+ }
+ return sb.String()
+}
+
+// Set parses a combinable value like "1y2m10d", "7d" or "30d". Each component is
+// a non-negative integer followed by one of the units y, m (month), d, and each
+// unit may appear at most once. An empty value or a value with no components is
+// rejected.
+//
+// Set implements the `pflags.Value` interface.
+func (d *duration) Set(raw string) error {
+ parsed := duration{}
+ seen := map[byte]bool{}
+ components := 0
+
+ for i := 0; i < len(raw); {
+ c := raw[i]
+ if c < '0' || c > '9' {
+ return usageError{fmt.Sprintf("invalid duration %q", raw)}
+ }
+
+ n := 0
+ for i < len(raw) && raw[i] >= '0' && raw[i] <= '9' {
+ digit := int(raw[i] - '0')
+ if n > (math.MaxInt-digit)/10 {
+ return usageError{fmt.Sprintf("invalid duration %q (out of range)", raw)}
+ }
+ n = n*10 + digit
+ i++
+ }
+ if i >= len(raw) {
+ return usageError{fmt.Sprintf("invalid duration %q (number %d has no unit)", raw, n)}
+ }
+
+ unit := raw[i]
+ if seen[unit] {
+ return usageError{fmt.Sprintf("invalid duration %q (unit %q repeated)", raw, string(unit))}
+ }
+ switch unit {
+ case 'y':
+ if n > maxDurationYears {
+ return usageError{fmt.Sprintf("invalid duration %q (years exceeds %d)", raw, maxDurationYears)}
+ }
+ parsed.years = n
+ case 'm':
+ if n > maxDurationMonths {
+ return usageError{fmt.Sprintf("invalid duration %q (months exceeds %d)", raw, maxDurationMonths)}
+ }
+ parsed.months = n
+ case 'd':
+ if n > maxDurationDays {
+ return usageError{fmt.Sprintf("invalid duration %q (days exceeds %d)", raw, maxDurationDays)}
+ }
+ parsed.days = n
+ default:
+ return usageError{fmt.Sprintf("invalid duration %q (unknown unit %q)", raw, string(unit))}
+ }
+ seen[unit] = true
+ components++
+ i++
+ }
+
+ if components == 0 {
+ return usageError{fmt.Sprintf("invalid duration %q", raw)}
+ }
+
+ *d = parsed
+ return nil
+}
+
+func (d duration) cutoff(tm time.Time) time.Time {
+ return tm.AddDate(-d.years, -d.months, -d.days)
+}
+
+func (d duration) isZero() bool {
+ return d.years == 0 && d.months == 0 && d.days == 0
+}