httpdl: fix progress-reporter race on name/out; harden mirror & resume validation

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
This commit is contained in:
2026-06-20 23:28:26 +09:00
parent db73b51e0d
commit 65104ade92
8 changed files with 255 additions and 79 deletions

25
main.go
View File

@@ -100,9 +100,15 @@ func run(args []string) int {
eng := download.NewEngine(opts.Int("max-concurrent-downloads"))
uiCtx, uiCancel := context.WithCancel(context.Background())
uiDone := make(chan struct{})
if !opts.Bool("quiet") && !opts.Bool("dry-run") {
rep := progress.New(eng.Snapshot, opts.Bool("human-readable"))
go rep.Run(uiCtx)
go func() {
rep.Run(uiCtx)
close(uiDone)
}()
} else {
close(uiDone)
}
jobsRef.set(jobs)
@@ -129,7 +135,7 @@ func run(args []string) int {
}
uiCancel()
time.Sleep(20 * time.Millisecond) // let the renderer clear its line
<-uiDone // wait for the renderer's final frame before printing the OK/FAIL lines
saveCookies(jobs)
@@ -831,6 +837,11 @@ func makeLimiter(bps int64) *rate.Limiter {
return rate.NewLimiter(rate.Limit(bps), download.LimiterBurst(bps))
}
// maxSelectIndex bounds how far a --select-file range is expanded: a torrent has
// at most a few thousand files, so a degenerate spec like "1-9999999999" must not
// spin or balloon the map. Indexes past the real file count are ignored anyway.
const maxSelectIndex = 1 << 20
// parseSelect parses "1,3-5" into a set of 1-based file indexes.
func parseSelect(s string) map[int]bool {
s = strings.TrimSpace(s)
@@ -848,10 +859,14 @@ func parseSelect(s string) map[int]bool {
continue
}
b, _ := strconv.Atoi(strings.TrimSpace(hi))
if a < 1 {
a = 1
}
if b > maxSelectIndex {
b = maxSelectIndex
}
for i := a; i <= b; i++ {
if i > 0 {
out[i] = true
}
out[i] = true
}
}
return out