From cf25c2235d7fc8fa5cdd28cebac43aca31c2baaf Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 08:23:03 +0200 Subject: [PATCH] fix: do not corrupt state when Add/Set overshoots max Add64 checked currentNum > max only after already mutating currentNum and currentBytes, so a single call that overshoots max returned an error but left the bar stuck: currentNum frozen above max, percent stuck above 100%, and every later Add/Set call failing with the same error forever, since the top-of-function currentNum < max guard could never pass again. Check the bound before mutating state instead, so an overshooting call is rejected as a whole and leaves currentNum/currentBytes/currentPercent exactly as they were. The now-unreachable post-hoc check is removed. Add TestAddExceedsMaxDoesNotCorruptState covering the corrupted-state case and that the bar keeps accepting valid progress afterwards. --- progressbar.go | 7 ++++--- progressbar_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/progressbar.go b/progressbar.go index 4060005..79a8e6a 100644 --- a/progressbar.go +++ b/progressbar.go @@ -741,6 +741,10 @@ func (p *ProgressBar) Add64(num int64) error { return errors.New("max must be greater than 0") } + if !p.config.ignoreLength && p.state.currentNum+num > p.config.max { + return errors.New("current number exceeds max") + } + if p.state.currentNum < p.config.max { if p.config.ignoreLength { p.state.currentNum = (p.state.currentNum + num) % p.config.max @@ -774,9 +778,6 @@ func (p *ProgressBar) Add64(num int64) error { updateBar := p.state.currentPercent != p.state.lastPercent && p.state.currentPercent > 0 p.state.lastPercent = p.state.currentPercent - if p.state.currentNum > p.config.max { - return errors.New("current number exceeds max") - } // always update if show bytes/second or its/second if updateBar || p.config.showIterationsPerSecond || p.config.showIterationsCount { diff --git a/progressbar_test.go b/progressbar_test.go index acc7bbe..9ba3ac0 100644 --- a/progressbar_test.go +++ b/progressbar_test.go @@ -420,6 +420,34 @@ func TestBar(t *testing.T) { } } +func TestAddExceedsMaxDoesNotCorruptState(t *testing.T) { + bar := NewOptions(100, OptionSetWriter(io.Discard)) + + if err := bar.Add(95); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if err := bar.Add(10); err == nil { + t.Error("should have an error for adding past max") + } + + // A rejected Add must leave state untouched, not partially applied. + if bar.state.currentNum != 95 { + t.Errorf("expected currentNum to stay at 95 after a rejected Add, got %d", bar.state.currentNum) + } + if bar.state.currentPercent != 95 { + t.Errorf("expected currentPercent to stay at 95 after a rejected Add, got %d", bar.state.currentPercent) + } + + // The bar must still accept further, non-overshooting progress afterwards. + if err := bar.Add(5); err != nil { + t.Errorf("bar should still accept progress after a rejected Add, got error: %v", err) + } + if bar.state.currentNum != 100 { + t.Errorf("expected currentNum to reach 100, got %d", bar.state.currentNum) + } +} + func TestState(t *testing.T) { bar := NewOptions(100, OptionSetWidth(10)) bar.Reset()