Skip to content

Fix capacity overflow panic wrapping a wide char at narrow terminal width - #3886

Open
vjymisal0 wants to merge 3 commits into
sharkdp:masterfrom
vjymisal0:fix/wide-char-wrap-capacity-overflow
Open

Fix capacity overflow panic wrapping a wide char at narrow terminal width#3886
vjymisal0 wants to merge 3 commits into
sharkdp:masterfrom
vjymisal0:fix/wide-char-wrap-capacity-overflow

Conversation

@vjymisal0

Copy link
Copy Markdown

Bug

bat --terminal-width 1 --wrap character aborts with capacity overflow (exit 101) when a line contains a character wider than the terminal (a double-width CJK char or emoji) and a background is painted on the line (--highlight-line, or a theme/style that fills the row background). Reported in #3844.

Root cause

In InteractivePrinter::print_line (src/printer.rs), the character-wrapping branch accumulates cursor by each chunk's display width. When a single character is wider than the remaining max_width, it still gets pushed onto line_buf and its full width added to cursor at flush time — so cursor can end up greater than cursor_max (the terminal width). The end-of-line background fill then computes:

ansi_style.paint(" ".repeat(cursor_max - cursor))

cursor_max - cursor underflows (usize), producing a repeat count near usize::MAX, which aborts inside str::repeat.

This is the same defect class as the --style=snip width-1 panic fixed in #3804 — that fix clamps a different overflow site in the same function; this one is the last remaining unclamped subtraction of the same shape.

Fix

Clamp with saturating_sub, matching the pattern already used a few lines above (line ~789) for the no-wrap branch:

ansi_style.paint(" ".repeat(cursor_max.saturating_sub(cursor)))

When the cursor has overshot the terminal width, the background fill is now empty instead of underflowing — matching the "clamp to empty" behavior bat already uses elsewhere for this exact class of bug.

Testing

Reproduced the panic on current master before the fix:

$ printf '\U0001F4E6\U0001F4E6\n' | bat --highlight-line 1 --terminal-width 1 --wrap character --color always --paging never --theme OneHalfDark
thread 'main' panicked at src/printer.rs:961:49:
attempt to subtract with overflow

After the fix, the same command exits 0 with no panic.

Added a regression test, wide_char_wrap_at_terminal_width_one_with_highlight_does_not_panic, covering this exact case. Ran the full integration suite (cargo test --test integration_tests): 230 passed, 0 failed. Also ran cargo clippy --lib with no new warnings.

Added a changelog entry under Bugfixes per CONTRIBUTING.md.

Closes #3844

…idth

InteractivePrinter::print_line's wrapping branch advances `cursor` by a
chunk's full display width even when that chunk (a double-width CJK
character or emoji) is wider than `cursor_max` (the terminal width).
When a background is painted on the line (e.g. via --highlight-line),
the end-of-line fill computed `" ".repeat(cursor_max - cursor)`, which
underflows when cursor > cursor_max, aborting with "capacity overflow".

Reproduced on current master:

  printf '\U0001F4E6\U0001F4E6\n' | bat --highlight-line 1 \
    --terminal-width 1 --wrap character --color always \
    --paging never --theme OneHalfDark
  # thread 'main' panicked at src/printer.rs:961:49:
  # attempt to subtract with overflow

Fix clamps the fill width with saturating_sub, matching the pattern
already used a few lines above (line 789-793) for the no-wrap branch
and the earlier width-1 snip fix (sharkdp#3804). When cursor exceeds
cursor_max, the background fill is now empty instead of underflowing.

Added a regression test (wide_char_wrap_at_terminal_width_one_with_highlight_does_not_panic)
covering the exact repro. Full integration suite (230 tests) and
clippy pass locally.

Closes sharkdp#3844
Copilot AI lite review requested due to automatic review settings August 10, 2026 15:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@xhon-pelushi xhon-pelushi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked out this branch and built it to verify the fix (cargo build --release, then reproduced both the original panic — confirmed fixed — and the case below).

The description says this is "the last remaining unclamped subtraction of the same shape," but there's a second one in the same function that this doesn't cover: max_width = cursor_max - cursor at printer.rs:829 (a few lines above the one you fixed, in the same char-wrapping branch). That line only runs once per Text chunk from the region/style iterator — for a line with a single style span (like your repro, a bare emoji line with no syntax highlighting boundaries) it only ever evaluates at cursor == 0, so it's safe. But cursor isn't reset between chunks, only inside the wrap-triggered branch further down. So a line with multiple style spans, where an earlier span already leaves cursor overshooting cursor_max (exactly the state your fix's own scenario produces), hits this second raw subtraction on the next span and panics the same way.

Repro against your branch (built from this PR, not master):

$ printf 'x = "📦" + y\n' > multispan.py
$ bat --highlight-line 1 --terminal-width 1 --wrap character --color always --paging never --theme OneHalfDark multispan.py
thread 'main' panicked at src/printer.rs:829:49:
attempt to subtract with overflow

Backtrace confirms the exact site:

17: <bat::printer::InteractivePrinter as bat::printer::Printer>::print_line
    at src/printer.rs:829:49

This is pre-existing on master too (I checked — that line is untouched by your diff), so it's not a regression you introduced, just a second instance of the bug class your PR is otherwise a correct, well-tested fix for. Given the PR's stated goal is closing out this whole class of panic (referencing #3804 as the prior instance), seems worth clamping this one too — cursor_max.saturating_sub(cursor) in the same style as your fix — while you're in this function, rather than leaving a known second repro for a follow-up issue.

Everything else about the change (the fix at line 968, the CHANGELOG entry, the regression test) looks correct and I confirmed it against the emoji-only single-span case as described.

@xhon-pelushi

Copy link
Copy Markdown

One correction to my own review, found during independent re-verification: I implied the second unguarded site (`printer.rs:829`) needs `--highlight-line` like the one this PR fixes. It doesn't — line 829 has no `background_color` guard, so it panics on a multi-style-span line (e.g. ordinary syntax-highlighted code) with no highlight flag at all. That makes it more broadly reachable than the site this PR patches, which strengthens the case for fixing both together rather than just the one this PR targets.

@xhon-pelushi xhon-pelushi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c162d403 covers the second site — thanks. Rebuilt the PR head and re-ran the cases:

case result
the original #3844 repro (--highlight-line=1 --terminal-width=1 --wrap=character, double-width emoji) exits 0
a multi-style-span line (var x = "📦" + y;) at --terminal-width=1, no --highlight-line exits 0 (panicked before this commit)
same line with --highlight-line=1 exits 0
ZWJ sequence (👨‍👩‍👦) in a string literal at width 1 and width 2 exits 0

Both cursor_max - cursor subtractions are now saturating_sub. The third cursor_max-relative subtraction in that function, cursor_max - cursor_total + 1 at line 797, is already inside an explicit if cursor_total <= cursor_max, so I do not think anything is left unguarded here.

Also checked the guard does not quietly break wrapping: var x = "📦" + y; at --terminal-width=8 --wrap=character still emits three wrapped lines with the content intact, so the max_width == 0 path recovers on the first wrap (cursor = 0; max_width = cursor_max;) rather than looping or dropping text.

Looks good to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bat panics (capacity overflow) on --terminal-width 1 wrapping a double-width char with a background

4 participants