Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions internal/ui/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,20 +247,23 @@ func (p *Printer) renderSideBySide(a, b string) {
}

sep := " │ "
// Clamp once so the divider's strings.Repeat shares truncPad's bound; an
// unclamped huge Width would panic there with "makeslice: len out of range".
width := min(p.Width, maxColWidth)
// Header and divider must share the exact column geometry of the data
// rows: each side is `width` columns, joined by sep (" │ "). The divider
// mirrors sep as "─┼─" so the ┼ lands directly under every │.
// Pad the header text manually (not via %-*s) so it stays correct even
// when paint wraps it in ANSI escapes, which %-*s would miscount.
fmt.Fprintf(p.Out, " %s%s%s\n", //nolint:errcheck
p.paint(th.Meta, truncPad("current", p.Width)),
p.paint(th.Meta, truncPad("current", width)),
sep,
p.paint(th.Meta, "proposed"))
fmt.Fprintf(p.Out, " %s\n", p.paint(th.Meta, //nolint:errcheck
strings.Repeat("─", p.Width)+"─┼─"+strings.Repeat("─", p.Width)))
strings.Repeat("─", width)+"─┼─"+strings.Repeat("─", width)))
for _, r := range rows {
left := truncPad(r.l, p.Width)
right := truncPad(r.r, p.Width)
left := truncPad(r.l, width)
right := truncPad(r.r, width)
if r.lc != (Style{}) {
left = p.paint(r.lc, left)
}
Expand Down Expand Up @@ -371,13 +374,21 @@ func withTypeName(src, name string) string {
return first + "\n" + rest
}

// maxColWidth caps the per-side column width. It is far wider than any real
// terminal and keeps padding allocations bounded: an unclamped huge width
// (e.g. -width=1<<62) would make FillRight/strings.Repeat attempt a
// multi-gigabyte allocation and panic with "makeslice: len out of range".
const maxColWidth = 1 << 16

// truncPad fits s into exactly w display cells: right-padded with spaces when
// shorter, or truncated with a trailing "…" when longer. Width is measured in
// terminal cells via runewidth, so CJK and other wide runes count as two.
// Widths above maxColWidth are clamped.
func truncPad(s string, w int) string {
if w <= 0 {
return ""
}
w = min(w, maxColWidth)
// expand tabs to 4 spaces for stable columns
s = strings.ReplaceAll(s, "\t", " ")
// runewidth.Truncate fits s into w cells, appending "…" (and reserving its
Expand Down
22 changes: 22 additions & 0 deletions internal/ui/unicode_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ui

import (
"bytes"
"math"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -34,6 +36,26 @@ func TestTruncPadWideRunes(t *testing.T) {
assert.Equal(t, "世界…", truncPad("世界世", 5))
}

// truncPad must be total: an absurd width (e.g. a user-supplied
// -width=4611686018427387904) must clamp instead of panicking with
// "makeslice: len out of range" inside FillRight. Found by fuzzing
// (ClusterFuzzLite, #99); the pre-#94 hand-rolled loop panicked identically.
func TestTruncPadHugeWidth(t *testing.T) {
res := truncPad("x", math.MaxInt)
assert.Equal(t, maxColWidth, len(res), "pads to the clamped maximum")
assert.Equal(t, "x", res[:1])
}

// The side-by-side divider (strings.Repeat) must survive a huge Width too.
func TestRenderSideBySideHugeWidth(t *testing.T) {
var buf bytes.Buffer
p := &Printer{Out: &buf, Width: math.MaxInt}

assert.NotPanics(t, func() {
p.renderSideBySide("a\nb", "a\nc")
})
}

func TestIndent(t *testing.T) {
// Without trailing newline
assert.Equal(t, " a\n b", indent("a\nb", " "))
Expand Down
Loading