Findings from a Rob-Pike-lens review (bugs/races/network), each verified against the code before fixing: - httpdl: name/out are now atomic.Pointer[string] -- the engine publishes a download to the reporter before Run resolves the name, so Stat raced the write (bt already did this) - httpdl: a malformed --proxy fails loudly instead of silently bypassing it - httpdl: a 206 must carry a matching Content-Range; a 200 in segmented mode is fatal so it fails over instead of burning the retry budget - httpdl: single-stream mirror failover validates the range before appending; ErrTooSlow only when the error is a real ctx cancellation - httpdl: idle guard tracks progress by timestamp (no Reset/Stop race, no sticky fired flag) - httpdl/control: reject a resume file whose segments don't tile [0,total) - bt: clamp the listen-port range; verify on-disk data before choosing pieces under --check-integrity - cli: reject size overflow; show --seed-time=MIN; clamp --select-file range - progress/engine/main: clamp ETA against int64 overflow; show queued downloads as waiting; join the reporter on exit instead of a 20ms sleep
116 lines
3.6 KiB
Go
116 lines
3.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Options is the resolved configuration: a flat name->value map plus a record
|
|
// of which names were explicitly set (so callers can tell a default from a
|
|
// chosen value). Values are kept as strings end-to-end and converted only at
|
|
// the point of use by the typed getters below.
|
|
type Options struct {
|
|
vals map[string]string
|
|
set map[string]bool
|
|
}
|
|
|
|
func newOptions() *Options {
|
|
return &Options{vals: map[string]string{}, set: map[string]bool{}}
|
|
}
|
|
|
|
// IsSet reports whether name was given on the command line or in the config
|
|
// (as opposed to coming from the built-in default).
|
|
func (o *Options) IsSet(name string) bool { return o.set[name] }
|
|
|
|
// Str returns the raw string value (empty if unset and no default).
|
|
func (o *Options) Str(name string) string { return o.vals[name] }
|
|
|
|
// boolWords is the single accepted vocabulary for boolean options, mapping each
|
|
// recognised spelling to its truth value. The Bool reader, truthy, and
|
|
// validate() all consult it, so exactly the words that validate are honoured
|
|
// (no "accepted by the reader but rejected by validate" surprises like --x=on).
|
|
var boolWords = map[string]bool{
|
|
"true": true, "yes": true, "1": true, "on": true,
|
|
"false": false, "no": false, "0": false, "off": false,
|
|
}
|
|
|
|
// boolWord reports a value's truth and whether it is a recognised boolean word.
|
|
func boolWord(s string) (val, ok bool) {
|
|
val, ok = boolWords[strings.ToLower(strings.TrimSpace(s))]
|
|
return val, ok
|
|
}
|
|
|
|
// Bool reports whether the value is a truthy boolean word.
|
|
func (o *Options) Bool(name string) bool {
|
|
val, _ := boolWord(o.vals[name])
|
|
return val
|
|
}
|
|
|
|
// Int returns the value as an int, or 0 if empty/invalid.
|
|
func (o *Options) Int(name string) int { return int(o.Int64(name)) }
|
|
|
|
// Int64 returns the value as an int64, or 0 if empty/invalid.
|
|
func (o *Options) Int64(name string) int64 {
|
|
n, _ := strconv.ParseInt(strings.TrimSpace(o.vals[name]), 10, 64)
|
|
return n
|
|
}
|
|
|
|
// Float returns the value as a float64, or 0 if empty/invalid.
|
|
func (o *Options) Float(name string) float64 {
|
|
f, _ := strconv.ParseFloat(strings.TrimSpace(o.vals[name]), 64)
|
|
return f
|
|
}
|
|
|
|
// Size returns a byte count parsed from a value like "20M" or "512K".
|
|
func (o *Options) Size(name string) int64 {
|
|
n, _ := parseSize(o.vals[name])
|
|
return n
|
|
}
|
|
|
|
// List returns a repeatable option's accumulated values.
|
|
func (o *Options) List(name string) []string {
|
|
v := o.vals[name]
|
|
if v == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(v, "\n")
|
|
}
|
|
|
|
// parseSize converts a size string into a byte count: the first 'K'/'k' or
|
|
// 'M'/'m' in the string selects the multiplier (1024 or 1024*1024) and
|
|
// everything from that byte on is discarded; with no such unit the whole string
|
|
// is the byte count. There is no gigabyte unit, so "1G" is rejected; "1Mi" and
|
|
// "10MB" are 1M and 10M (the trailing bytes are dropped). An empty string is 0
|
|
// bytes; a negative value is rejected.
|
|
func parseSize(s string) (int64, error) {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return 0, nil
|
|
}
|
|
mult := int64(1)
|
|
if i := strings.IndexAny(s, "KkMm"); i >= 0 {
|
|
if c := s[i]; c == 'M' || c == 'm' {
|
|
mult = 1 << 20
|
|
} else {
|
|
mult = 1 << 10
|
|
}
|
|
s = s[:i]
|
|
}
|
|
n, err := strconv.ParseInt(strings.TrimSpace(s), 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("bad size %q", s)
|
|
}
|
|
// Size parsing rejects negative sizes outright.
|
|
if n < 0 {
|
|
return 0, fmt.Errorf("negative size %q", s)
|
|
}
|
|
// Reject a value whose unit multiply would overflow int64 and silently wrap to
|
|
// a bogus (positive or negative) byte count.
|
|
if mult > 1 && n > math.MaxInt64/mult {
|
|
return 0, fmt.Errorf("size %q too large", s)
|
|
}
|
|
return n * mult, nil
|
|
}
|