detached 3fe3371

fix(html): avoid size-hint overflow for MaxInt generated-bytes limit

Cap the encode buffer hint at the limit instead of limit+1, which wrapped to a negative capacity when MaxGeneratedBytes was math.MaxInt. The oracle now reports malformed golden lines instead of panicking.
commit
3fe3371ef359bab4afde713a22b3fe697587a513
author
brice · · 2026-10-02 13:10 UTC
parent
2ef4850
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +16 -3
M html/renderer.go +4 -3
@@ -287,9 +287,10 @@ func (r *Renderer) buildUpdate(frame core.Frame, scratch []core.Cell, snapshot b
287287 // the exact encoded size. The previous conservative estimate could reject
288288 // compact updates well below the configured limit.
289289 func encodeBoundedUpdate(update Update, limit int) ([]byte, error) {
290- // The hint never exceeds limit+1, so an oversized update cannot reserve
291- // its full estimate before the limit rejects it.
292- encoded, err := appendUpdateJSON(make([]byte, 0, min(estimateUpdateJSONSize(update), limit+1)), update)
290+ // The hint never exceeds limit, so an oversized update cannot reserve its
291+ // full estimate before the limit rejects it. limit+1 would overflow for
292+ // math.MaxInt.
293+ encoded, err := appendUpdateJSON(make([]byte, 0, min(estimateUpdateJSONSize(update), limit)), update)
293294 if err != nil {
294295 return nil, fmt.Errorf("html: encode update: %w", err)
295296 }
@@ -287,9 +287,10 @@ func (r *Renderer) buildUpdate(frame core.Frame, scratch []core.Cell, snapshot b
287 // the exact encoded size. The previous conservative estimate could reject287 // the exact encoded size. The previous conservative estimate could reject
288 // compact updates well below the configured limit.288 // compact updates well below the configured limit.
289 func encodeBoundedUpdate(update Update, limit int) ([]byte, error) {289 func encodeBoundedUpdate(update Update, limit int) ([]byte, error) {
290- // The hint never exceeds limit+1, so an oversized update cannot reserve290+ // The hint never exceeds limit, so an oversized update cannot reserve its
291- // its full estimate before the limit rejects it.291+ // full estimate before the limit rejects it. limit+1 would overflow for
292- encoded, err := appendUpdateJSON(make([]byte, 0, min(estimateUpdateJSONSize(update), limit+1)), update)292+ // math.MaxInt.
293+ encoded, err := appendUpdateJSON(make([]byte, 0, min(estimateUpdateJSONSize(update), limit)), update)
293 if err != nil {294 if err != nil {
294 return nil, fmt.Errorf("html: encode update: %w", err)295 return nil, fmt.Errorf("html: encode update: %w", err)
295 }296 }
M html/renderer_test.go +9 -0
@@ -2,6 +2,7 @@ package html
22
33 import (
44 "errors"
5+ "math"
56 "os"
67 "testing"
78
@@ -127,6 +128,14 @@ func TestRendererEnforcesExactGeneratedByteLimit(t *testing.T) {
127128 require.ErrorContains(t, err, "generated update is")
128129 }
129130
131+func TestRendererAcceptsMaxIntGeneratedBytesLimit(t *testing.T) {
132+ renderer, err := New(Options{Limits: Limits{MaxGeneratedBytes: math.MaxInt}})
133+ require.NoError(t, err)
134+ prepared, err := renderer.Prepare(core.NewFrame(2, 1), nil, true, Cursor{})
135+ require.NoError(t, err)
136+ require.NoError(t, prepared.Commit())
137+}
138+
130139 func TestRendererUsesIncrementalDamageWithNormalCounts(t *testing.T) {
131140 frame := core.NewFrame(2, 2)
132141 renderer, err := New(Options{})
@@ -2,6 +2,7 @@ package html
2 2
3 import (3 import (
4 "errors"4 "errors"
5+ "math"
5 "os"6 "os"
6 "testing"7 "testing"
7 8
@@ -127,6 +128,14 @@ func TestRendererEnforcesExactGeneratedByteLimit(t *testing.T) {
127 require.ErrorContains(t, err, "generated update is")128 require.ErrorContains(t, err, "generated update is")
128 }129 }
129 130
131+func TestRendererAcceptsMaxIntGeneratedBytesLimit(t *testing.T) {
132+ renderer, err := New(Options{Limits: Limits{MaxGeneratedBytes: math.MaxInt}})
133+ require.NoError(t, err)
134+ prepared, err := renderer.Prepare(core.NewFrame(2, 1), nil, true, Cursor{})
135+ require.NoError(t, err)
136+ require.NoError(t, prepared.Commit())
137+}
138+
130 func TestRendererUsesIncrementalDamageWithNormalCounts(t *testing.T) {139 func TestRendererUsesIncrementalDamageWithNormalCounts(t *testing.T) {
131 frame := core.NewFrame(2, 2)140 frame := core.NewFrame(2, 2)
132 renderer, err := New(Options{})141 renderer, err := New(Options{})
M oracle_test.go +3 -0
@@ -644,6 +644,9 @@ func TestDifferentialOracle(t *testing.T) {
644644 }
645645 want := map[string][]string{}
646646 for _, line := range strings.Split(strings.TrimSpace(string(golden)), "\n") {
647+ if len(line) < 3 {
648+ t.Fatalf("malformed golden line %q (regenerate with -oracle.update on a trusted baseline)", line)
649+ }
647650 want[line[:2]] = append(want[line[:2]], line)
648651 }
649652 for i, digests := range got {
@@ -644,6 +644,9 @@ func TestDifferentialOracle(t *testing.T) {
644 }644 }
645 want := map[string][]string{}645 want := map[string][]string{}
646 for _, line := range strings.Split(strings.TrimSpace(string(golden)), "\n") {646 for _, line := range strings.Split(strings.TrimSpace(string(golden)), "\n") {
647+ if len(line) < 3 {
648+ t.Fatalf("malformed golden line %q (regenerate with -oracle.update on a trusted baseline)", line)
649+ }
647 want[line[:2]] = append(want[line[:2]], line)650 want[line[:2]] = append(want[line[:2]], line)
648 }651 }
649 for i, digests := range got {652 for i, digests := range got {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close