diff options
| author | tsne <tsne.dev@outlook.com> | 2026-07-06 16:52:20 +0200 |
|---|---|---|
| committer | tsne <tsne.dev@outlook.com> | 2026-07-06 19:21:15 +0200 |
| commit | 2c41acab4c97f584e8e199b31dc456194b8d7e6c (patch) | |
| tree | 42381deb6c4f6da0f32945432207a31b4d86664e /internal/cli/duration.go | |
| download | hopper-2c41acab4c97f584e8e199b31dc456194b8d7e6c.tar.gz | |
initial
Diffstat (limited to 'internal/cli/duration.go')
| -rw-r--r-- | internal/cli/duration.go | 136 |
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 +} |