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 }