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 c4c03dde60
commit e3505855c1
8 changed files with 255 additions and 79 deletions

View File

@@ -68,10 +68,17 @@ func ParsePorts(spec string) []int {
if err != nil {
continue
}
// Clamp to the valid port space before iterating: a parseable but oversized
// spec like "1-9999999999" would otherwise spin for ~10^10 iterations and
// hang startup, even though only [1,65535] can ever be appended.
if a < 1 {
a = 1
}
if b > 65535 {
b = 65535
}
for i := a; i <= b; i++ {
if i >= 1 && i <= 65535 {
ports = append(ports, i)
}
ports = append(ports, i)
}
}
return ports
@@ -306,14 +313,17 @@ func (d *Download) Run(ctx context.Context) (err error) {
return nil
}
if err := d.choose(t); err != nil {
return err
}
// --check-integrity: re-hash the existing on-disk data BEFORE arming the
// request loop, so already-good pieces are not re-requested from peers (aria2
// verifies first, then fetches only what is missing).
if d.opts.CheckIntegrity {
if err := t.VerifyDataContext(ctx); err != nil {
return err
}
}
if err := d.choose(t); err != nil {
return err
}
if err := d.wait(ctx, t); err != nil {
return err