package config import ( "fmt" "strconv" "strings" "time" ) // Unlimited is the zero value used by both size and duration limits to mean // "no limit". Callers must therefore always compare against 0 before enforcing. const Unlimited = 0 var sizeUnits = []struct { suffix string mult int64 }{ {"KIB", 1 << 10}, {"MIB", 1 << 20}, {"GIB", 1 << 30}, {"TIB", 1 << 40}, {"KB", 1e3}, {"MB", 1e6}, {"GB", 1e9}, {"TB", 1e12}, {"K", 1 << 10}, {"M", 1 << 20}, {"G", 1 << 30}, {"T", 1 << 40}, {"B", 1}, } // ParseSize accepts "2GiB", "500MB", "1048576", "unlimited" or "0". Binary // units (KiB) and bare "K"/"M"/"G" are powers of 1024; "KB"/"MB"/"GB" are // powers of 1000, per SI. func ParseSize(s string) (int64, error) { t := strings.ToUpper(strings.TrimSpace(s)) if t == "" || t == "0" || t == "UNLIMITED" || t == "NONE" { return Unlimited, nil } mult := int64(1) for _, u := range sizeUnits { if strings.HasSuffix(t, u.suffix) { t = strings.TrimSpace(strings.TrimSuffix(t, u.suffix)) mult = u.mult break } } n, err := strconv.ParseFloat(t, 64) if err != nil { return 0, fmt.Errorf("invalid size %q", s) } if n < 0 { return 0, fmt.Errorf("invalid size %q: negative", s) } v := int64(n * float64(mult)) if v < 0 { return 0, fmt.Errorf("invalid size %q: overflow", s) } return v, nil } // FormatSize renders a *limit* for humans, where zero means "no limit". Use // FormatBytes for an actual quantity, where zero means zero. func FormatSize(n int64) string { if n == Unlimited { return "unlimited" } return FormatBytes(n) } // FormatBytes renders a byte count for humans. func FormatBytes(n int64) string { units := []string{"B", "KiB", "MiB", "GiB", "TiB"} f, i := float64(n), 0 for f >= 1024 && i < len(units)-1 { f /= 1024 i++ } if i == 0 { return fmt.Sprintf("%d B", n) } return strings.TrimSuffix(strconv.FormatFloat(f, 'f', 2, 64), ".00") + " " + units[i] } // ParseDuration extends time.ParseDuration with "d" (day) and "w" (week) // suffixes, and maps "never"/"0" to Unlimited. Durations are the one place the // stdlib's vocabulary is genuinely too small: "72h" is a poor way to say "3d". func ParseDuration(s string) (time.Duration, error) { t := strings.ToLower(strings.TrimSpace(s)) if t == "" || t == "0" || t == "never" || t == "unlimited" || t == "none" { return Unlimited, nil } // Expand a trailing d/w into hours so time.ParseDuration can finish the job. for suffix, hours := range map[string]float64{"d": 24, "w": 168} { if num, ok := strings.CutSuffix(t, suffix); ok { n, err := strconv.ParseFloat(num, 64) if err != nil { return 0, fmt.Errorf("invalid duration %q", s) } t = strconv.FormatFloat(n*hours, 'f', -1, 64) + "h" break } } d, err := time.ParseDuration(t) if err != nil { return 0, fmt.Errorf("invalid duration %q", s) } if d < 0 { return 0, fmt.Errorf("invalid duration %q: negative", s) } return d, nil } // FormatDuration is the human-facing inverse of ParseDuration. func FormatDuration(d time.Duration) string { switch { case d == Unlimited: return "never" case d%(24*time.Hour) == 0: return fmt.Sprintf("%dd", d/(24*time.Hour)) case d%time.Hour == 0: return fmt.Sprintf("%dh", d/time.Hour) default: return d.String() } }