all: cut bloat found in the pike audit

Dead, vestigial, and over-built code with no behavior change (except the
bool-vocabulary trim, a deliberate behavior tightening):

- httpdl: drop the vestigial sort+copy in validSegs (work-stealing was
  removed in 270812d, so segments always tile [0,total) ascending now);
  validate order in place, which also rejects a scrambled sidecar. Inline
  the endOff() accessor to s.end.
- cli: shrink the boolean vocabulary to true/false, which is all that's
  accepted. The invented yes/no/1/0/on/off spellings are gone, so e.g.
  --enable-dht=yes (or yes in got.conf) is now a usage error.
- download: remove the dead, never-called Stat.Done() method.
- progress: drop speed(), a pure alias of humanSize; call humanSize directly.
- main: replace the jobsHolder mutex box with atomic.Pointer[[]job] for the
  forced-exit session save; the capability stays, the lock and type go.
- bt: collapse isAddrInUse's X||X (missinggo.IsAddrInUse is that exact
  string match); drop the now-unused missinggo import (tidy -> indirect).
- option: note that prealloc and falloc are identical here.
This commit is contained in:
2026-06-21 15:45:56 +09:00
parent 3eb2d5ffcb
commit 10672f3816
14 changed files with 47 additions and 75 deletions

View File

@@ -19,7 +19,6 @@ import (
"time" "time"
alog "github.com/anacrolix/log" alog "github.com/anacrolix/log"
missinggo "github.com/anacrolix/missinggo/v2"
"github.com/anacrolix/torrent" "github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo" "github.com/anacrolix/torrent/metainfo"
"github.com/hanbok/got/download" "github.com/hanbok/got/download"
@@ -172,10 +171,11 @@ func NewClient(cfg ClientConfig) (*torrent.Client, error) {
return cl, nil return cl, nil
} }
// isAddrInUse reports whether err is an "address already in use" bind failure, // isAddrInUse reports whether err is an "address already in use" bind failure.
// matching either missinggo's helper or the raw string the library wraps. // (anacrolix/missinggo's IsAddrInUse is this exact string match, so we just do
// it directly rather than carry the import for it.)
func isAddrInUse(err error) bool { func isAddrInUse(err error) bool {
return err != nil && (missinggo.IsAddrInUse(err) || strings.Contains(err.Error(), "address already in use")) return err != nil && strings.Contains(err.Error(), "address already in use")
} }
// Options are the per-download settings. // Options are the per-download settings.

View File

@@ -122,7 +122,7 @@ var options = []Opt{
{Long: "allow-overwrite", Kind: Bool, Default: "false", Help: "overwrite an existing file", Tag: Advanced}, {Long: "allow-overwrite", Kind: Bool, Default: "false", Help: "overwrite an existing file", Tag: Advanced},
{Long: "auto-file-renaming", Kind: Bool, Default: "true", Help: "rename file (.1, .2, ...) if it already exists", Tag: Advanced}, {Long: "auto-file-renaming", Kind: Bool, Default: "true", Help: "rename file (.1, .2, ...) if it already exists", Tag: Advanced},
{Long: "human-readable", Kind: Bool, Default: "true", Help: "show sizes as Ki/Mi/Gi", Tag: Advanced}, {Long: "human-readable", Kind: Bool, Default: "true", Help: "show sizes as Ki/Mi/Gi", Tag: Advanced},
{Long: "file-allocation", Short: 'a', Kind: Enum, Default: "prealloc", Enum: []string{"none", "prealloc", "trunc", "falloc"}, Help: "how to allocate disk space", Tag: Advanced}, {Long: "file-allocation", Short: 'a', Kind: Enum, Default: "prealloc", Enum: []string{"none", "prealloc", "trunc", "falloc"}, Help: "how to reserve disk space (prealloc and falloc are identical here)", Tag: Advanced},
{Long: "dry-run", Kind: Bool, Default: "false", Help: "check that the file is available but do not download it", Tag: Advanced}, {Long: "dry-run", Kind: Bool, Default: "false", Help: "check that the file is available but do not download it", Tag: Advanced},
{Long: "stop", Kind: Int, Default: "0", Min: 0, Help: "stop the program after N seconds (0 = off)", Tag: Advanced}, {Long: "stop", Kind: Int, Default: "0", Min: 0, Help: "stop the program after N seconds (0 = off)", Tag: Advanced},
{Long: "disable-ipv6", Kind: Bool, Default: "false", Help: "disable IPv6 (force IPv4-only connections)", Tag: Advanced}, {Long: "disable-ipv6", Kind: Bool, Default: "false", Help: "disable IPv6 (force IPv4-only connections)", Tag: Advanced},

View File

@@ -27,13 +27,14 @@ func (o *Options) IsSet(name string) bool { return o.set[name] }
// Str returns the raw string value (empty if unset and no default). // Str returns the raw string value (empty if unset and no default).
func (o *Options) Str(name string) string { return o.vals[name] } func (o *Options) Str(name string) string { return o.vals[name] }
// boolWords is the single accepted vocabulary for boolean options, mapping each // boolWords is the accepted vocabulary for boolean options. aria2 takes only
// recognised spelling to its truth value. The Bool reader, truthy, and // true/false (and rejects everything else), so we match it rather than inventing
// validate() all consult it, so exactly the words that validate are honoured // extra spellings. boolWord is the single consult point — the Bool reader,
// (no "accepted by the reader but rejected by validate" surprises like --x=on). // validate(), and the no-conf bootstrap all go through it — so exactly the words
// that validate are honoured.
var boolWords = map[string]bool{ var boolWords = map[string]bool{
"true": true, "yes": true, "1": true, "on": true, "true": true,
"false": false, "no": false, "0": false, "off": false, "false": false,
} }
// boolWord reports a value's truth and whether it is a recognised boolean word. // boolWord reports a value's truth and whether it is a recognised boolean word.

View File

@@ -139,13 +139,17 @@ func TestValidateFloatNonNegative(t *testing.T) {
func TestValidateBool(t *testing.T) { func TestValidateBool(t *testing.T) {
b := byLong["enable-dht"] b := byLong["enable-dht"]
for _, ok := range []string{"true", "false", "yes", "no", "1", "0", "on", "off"} { // aria2 accepts only true/false; the invented yes/no/1/0/on/off spellings
// were removed, so they must now fail validation.
for _, ok := range []string{"true", "false"} {
if err := validate(b, ok); err != nil { if err := validate(b, ok); err != nil {
t.Errorf("validate bool %q: %v", ok, err) t.Errorf("validate bool %q: %v", ok, err)
} }
} }
if err := validate(b, "flase"); err == nil { for _, bad := range []string{"yes", "no", "1", "0", "on", "off", "flase"} {
t.Errorf("validate bool flase: want error") if err := validate(b, bad); err == nil {
t.Errorf("validate bool %q: want error (only true/false accepted)", bad)
}
} }
} }

View File

@@ -55,9 +55,6 @@ type Stat struct {
Seeders int // connected seeders (BitTorrent only) Seeders int // connected seeders (BitTorrent only)
} }
// Done reports whether the download has reached a terminal state.
func (s Stat) Done() bool { return s.Status == Complete || s.Status == Errored }
// A Download is one logical job: a URL, a torrent, a magnet. Run blocks in its // A Download is one logical job: a URL, a torrent, a magnet. Run blocks in its
// own goroutine until the work finishes, fails, or ctx is cancelled. Stat may // own goroutine until the work finishes, fails, or ctx is cancelled. Stat may
// be called concurrently at any time and must not block; implementations back // be called concurrently at any time and must not block; implementations back

2
go.mod
View File

@@ -4,7 +4,6 @@ go 1.25
require ( require (
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb
github.com/anacrolix/missinggo/v2 v2.10.0
github.com/anacrolix/torrent v1.61.0 github.com/anacrolix/torrent v1.61.0
golang.org/x/sys v0.38.0 golang.org/x/sys v0.38.0
golang.org/x/term v0.37.0 golang.org/x/term v0.37.0
@@ -22,6 +21,7 @@ require (
github.com/anacrolix/go-libutp v1.3.2 // indirect github.com/anacrolix/go-libutp v1.3.2 // indirect
github.com/anacrolix/missinggo v1.3.0 // indirect github.com/anacrolix/missinggo v1.3.0 // indirect
github.com/anacrolix/missinggo/perf v1.0.0 // indirect github.com/anacrolix/missinggo/perf v1.0.0 // indirect
github.com/anacrolix/missinggo/v2 v2.10.0 // indirect
github.com/anacrolix/mmsg v1.0.1 // indirect github.com/anacrolix/mmsg v1.0.1 // indirect
github.com/anacrolix/multiless v0.4.0 // indirect github.com/anacrolix/multiless v0.4.0 // indirect
github.com/anacrolix/stm v0.5.0 // indirect github.com/anacrolix/stm v0.5.0 // indirect

View File

@@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"sync/atomic" "sync/atomic"
) )
@@ -34,7 +33,7 @@ func controlPath(out string) string { return out + ".got" }
func snapshot(url string, total int64, etag, lastmod string, segs []seg) control { func snapshot(url string, total int64, etag, lastmod string, segs []seg) control {
c := control{URL: url, Total: total, ETag: etag, LastModified: lastmod, Segs: make([]segState, len(segs))} c := control{URL: url, Total: total, ETag: etag, LastModified: lastmod, Segs: make([]segState, len(segs))}
for i := range segs { for i := range segs {
c.Segs[i] = segState{segs[i].start, segs[i].endOff(), atomic.LoadInt64(&segs[i].written)} c.Segs[i] = segState{segs[i].start, segs[i].end, atomic.LoadInt64(&segs[i].written)}
} }
return c return c
} }
@@ -88,16 +87,15 @@ func loadControl(out, url string, total int64, etag, lastmod string) *control {
} }
// validSegs reports whether segs cover [0,total) with no gap or overlap and a // validSegs reports whether segs cover [0,total) with no gap or overlap and a
// sane written count for each. The recorded order is not sorted (a steal appends // sane written count for each. got always writes segments in ascending start
// a tail), so we check coverage on a sorted copy. // order, so coverage is checked in place; an out-of-order sidecar (only possible
// from a hand-edited or corrupted file) is rejected, which restarts cleanly.
func validSegs(segs []segState, total int64) bool { func validSegs(segs []segState, total int64) bool {
if total <= 0 || len(segs) == 0 { if total <= 0 || len(segs) == 0 {
return false return false
} }
sorted := append([]segState(nil), segs...)
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Start < sorted[j].Start })
var next int64 var next int64
for _, s := range sorted { for _, s := range segs {
length := s.End - s.Start + 1 length := s.End - s.Start + 1
if s.Start != next || s.End < s.Start || s.Written < 0 || s.Written > length { if s.Start != next || s.End < s.Start || s.Written < 0 || s.Written > length {
return false return false

View File

@@ -758,7 +758,7 @@ func (d *Download) fetchSeg(ctx context.Context, f *os.File, s *seg, total int64
func (d *Download) fetchOnce(ctx context.Context, f *os.File, s *seg, uri string, total int64) error { func (d *Download) fetchOnce(ctx context.Context, f *os.File, s *seg, uri string, total int64) error {
reqCtx, cancel := context.WithCancel(ctx) reqCtx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
req, err := d.request(reqCtx, uri, fmt.Sprintf("bytes=%d-%d", s.offset(), s.endOff())) req, err := d.request(reqCtx, uri, fmt.Sprintf("bytes=%d-%d", s.offset(), s.end))
if err != nil { if err != nil {
return err return err
} }

View File

@@ -16,7 +16,6 @@ type seg struct {
} }
func (s *seg) length() int64 { return s.end - s.start + 1 } func (s *seg) length() int64 { return s.end - s.start + 1 }
func (s *seg) endOff() int64 { return s.end }
func (s *seg) done() bool { return atomic.LoadInt64(&s.written) >= s.length() } func (s *seg) done() bool { return atomic.LoadInt64(&s.written) >= s.length() }
func (s *seg) addWritten(n int64) { atomic.AddInt64(&s.written, n) } func (s *seg) addWritten(n int64) { atomic.AddInt64(&s.written, n) }
func (s *seg) progress() int64 { return atomic.LoadInt64(&s.written) } func (s *seg) progress() int64 { return atomic.LoadInt64(&s.written) }

View File

@@ -201,7 +201,7 @@ func tilesCover(t *testing.T, segs []seg, total int64) {
if s.start != next { if s.start != next {
t.Fatalf("segment gap/overlap: next byte %d, got start %d", next, s.start) t.Fatalf("segment gap/overlap: next byte %d, got start %d", next, s.start)
} }
next = s.endOff() + 1 next = s.end + 1
} }
if next != total { if next != total {
t.Fatalf("segments cover %d bytes, want %d", next, total) t.Fatalf("segments cover %d bytes, want %d", next, total)

41
main.go
View File

@@ -17,7 +17,7 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync/atomic"
"syscall" "syscall"
"time" "time"
@@ -94,8 +94,12 @@ func run(args []string) int {
t := time.AfterFunc(time.Duration(stop)*time.Second, cancel) t := time.AfterFunc(time.Duration(stop)*time.Second, cancel)
defer t.Stop() defer t.Stop()
} }
jobsRef := &jobsHolder{} var jobsRef atomic.Pointer[[]job]
go handleSignals(cancel, sessionFile, jobsRef) // publish snapshots the slice header (j is a by-value copy) and stores a
// pointer to it, so the signal handler can read the current jobs lock-free
// without racing the `jobs = append(...)` reassignment in the follow pass.
publish := func(j []job) { jobsRef.Store(&j) }
go handleSignals(cancel, sessionFile, &jobsRef)
eng := download.NewEngine(opts.Int("max-concurrent-downloads")) eng := download.NewEngine(opts.Int("max-concurrent-downloads"))
@@ -111,7 +115,7 @@ func run(args []string) int {
close(uiDone) close(uiDone)
} }
jobsRef.set(jobs) publish(jobs)
results := runJobs(ctx, eng, jobs) results := runJobs(ctx, eng, jobs)
// --follow-torrent: a .torrent fetched over HTTP becomes a BitTorrent // --follow-torrent: a .torrent fetched over HTTP becomes a BitTorrent
@@ -125,7 +129,7 @@ func run(args []string) int {
// Index space, so the sort below restores input order deterministically. // Index space, so the sort below restores input order deterministically.
nInitial := len(jobs) nInitial := len(jobs)
jobs = append(jobs, follow...) jobs = append(jobs, follow...)
jobsRef.set(jobs) publish(jobs)
followResults := runJobs(ctx, eng, follow) followResults := runJobs(ctx, eng, follow)
for i := range followResults { for i := range followResults {
followResults[i].Index += nInitial followResults[i].Index += nInitial
@@ -153,25 +157,6 @@ func run(args []string) int {
return report(results) return report(results)
} }
// jobsHolder lets the signal handler reach the latest job list for a
// best-effort session save on a forced (second-Ctrl-C) exit.
type jobsHolder struct {
mu sync.Mutex
jobs []job
}
func (h *jobsHolder) set(jobs []job) {
h.mu.Lock()
h.jobs = jobs
h.mu.Unlock()
}
func (h *jobsHolder) get() []job {
h.mu.Lock()
defer h.mu.Unlock()
return h.jobs
}
// job pairs a download with the source string that produced it, so we can write // job pairs a download with the source string that produced it, so we can write
// the still-unfinished ones back to a session file. // the still-unfinished ones back to a session file.
type job struct { type job struct {
@@ -810,14 +795,18 @@ func progressNote(s download.Stat) string {
// TERM) cancels the graceful context so run() can wind down and save normally; // TERM) cancels the graceful context so run() can wind down and save normally;
// a second forces exit, but first writes the session best-effort so the resume // a second forces exit, but first writes the session best-effort so the resume
// list survives a force-quit. // list survives a force-quit.
func handleSignals(cancel context.CancelFunc, sessionFile string, jobs *jobsHolder) { func handleSignals(cancel context.CancelFunc, sessionFile string, jobs *atomic.Pointer[[]job]) {
sig := make(chan os.Signal, 2) sig := make(chan os.Signal, 2)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM) signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
<-sig // first: cancel the graceful context <-sig // first: cancel the graceful context
cancel() cancel()
<-sig // second: force exit <-sig // second: force exit
if sessionFile != "" { if sessionFile != "" {
if err := saveSession(sessionFile, jobs.get()); err != nil { var js []job // nil until the first publish; saveSession handles an empty list
if p := jobs.Load(); p != nil {
js = *p
}
if err := saveSession(sessionFile, js); err != nil {
fmt.Fprintln(os.Stderr, "got:", err) fmt.Fprintln(os.Stderr, "got:", err)
} }
} }

View File

@@ -41,12 +41,6 @@ func humanSize(n int64, human bool) string {
return fmt.Sprintf("%.0f%ciB", val, suffix) return fmt.Sprintf("%.0f%ciB", val, suffix)
} }
// speed formats a bytes-per-second rate. The DL:/UL: fields show the bare
// abbreviated size with no "/s" suffix.
func speed(bytesPerSec int64, human bool) string {
return humanSize(bytesPerSec, human)
}
// secfmt renders a duration as "1h2m3s", appending each unit only when it is // secfmt renders a duration as "1h2m3s", appending each unit only when it is
// nonzero (but still showing seconds when the whole input is 0): // nonzero (but still showing seconds when the whole input is 0):
// 3600s->"1h", 120s->"2m", 3720s->"1h2m". A non-positive or absurd // 3600s->"1h", 120s->"2m", 3720s->"1h2m". A non-positive or absurd

View File

@@ -53,16 +53,6 @@ func TestSecfmt(t *testing.T) {
} }
} }
func TestSpeedNoSuffix(t *testing.T) {
// The DL:/UL: fields show the bare abbreviated size with no "/s".
if got := speed(1536, true); got != "1.5KiB" {
t.Errorf("speed(1536,true) = %q, want %q", got, "1.5KiB")
}
if got := speed(2048, false); got != "2048B" {
t.Errorf("speed(2048,false) = %q, want %q", got, "2048B")
}
}
func TestPercent(t *testing.T) { func TestPercent(t *testing.T) {
if got := percent(50, 100); got != 50 { if got := percent(50, 100); got != 50 {
t.Errorf("percent(50,100) = %d, want 50", got) t.Errorf("percent(50,100) = %d, want 50", got)

View File

@@ -147,10 +147,10 @@ func (r *Reporter) lineOne(s download.Stat) string {
fmt.Fprintf(&b, " SD:%d", s.Seeders) fmt.Fprintf(&b, " SD:%d", s.Seeders)
} }
if !seeding { if !seeding {
fmt.Fprintf(&b, " DL:%s", speed(dl, r.human)) fmt.Fprintf(&b, " DL:%s", humanSize(dl, r.human))
} }
if seeding || ul > 0 { if seeding || ul > 0 {
fmt.Fprintf(&b, " UL:%s", speed(ul, r.human)) fmt.Fprintf(&b, " UL:%s", humanSize(ul, r.human))
} }
if !seeding && s.Total > 0 && dl > 0 { if !seeding && s.Total > 0 && dl > 0 {
fmt.Fprintf(&b, " ETA:%s", secfmt(etaDuration(s.Total-s.Completed, dl))) fmt.Fprintf(&b, " ETA:%s", secfmt(etaDuration(s.Total-s.Completed, dl)))
@@ -227,7 +227,7 @@ func (r *Reporter) summary(stats []download.Stat) string {
stalled++ stalled++
} }
} }
line := fmt.Sprintf("%d active DL:%s UL:%s", len(stats)-waiting, speed(totDL, r.human), speed(totUL, r.human)) line := fmt.Sprintf("%d active DL:%s UL:%s", len(stats)-waiting, humanSize(totDL, r.human), humanSize(totUL, r.human))
if waiting > 0 { if waiting > 0 {
line += fmt.Sprintf(" (%d waiting)", waiting) line += fmt.Sprintf(" (%d waiting)", waiting)
} }
@@ -254,13 +254,13 @@ func (r *Reporter) rowMetric(s download.Stat, dl, ul int64) (metric, tail string
case download.Waiting: case download.Waiting:
return "queued", "" return "queued", ""
case download.Seeding: case download.Seeding:
return "seeding", "UL:" + speed(ul, r.human) return "seeding", "UL:" + humanSize(ul, r.human)
case download.Complete: case download.Complete:
return "done", "" return "done", ""
case download.Errored: case download.Errored:
return "failed", "" return "failed", ""
default: default:
metric = "DL:" + speed(dl, r.human) metric = "DL:" + humanSize(dl, r.human)
switch { switch {
case dl > 0 && s.Total > 0: case dl > 0 && s.Total > 0:
tail = secfmt(etaDuration(s.Total-s.Completed, dl)) tail = secfmt(etaDuration(s.Total-s.Completed, dl))