Skip to content

Fix str.expandtabs aborting on a tab size of zero - #8562

Merged
youknowone merged 1 commit into
RustPython:mainfrom
luantaraschi:fix/expandtabs-zero-tabsize
Aug 21, 2026
Merged

Fix str.expandtabs aborting on a tab size of zero#8562
youknowone merged 1 commit into
RustPython:mainfrom
luantaraschi:fix/expandtabs-zero-tabsize

Conversation

@luantaraschi

@luantaraschi luantaraschi commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

"a\tb".expandtabs(0) takes the interpreter down:

$ rustpython -c 'print("a\tb".expandtabs(0))'
thread 'main' panicked at library/alloc/src/raw_vec/mod.rs:28:5:
capacity overflow

CPython returns 'ab': with no width to advance to, the tabs come out and nothing else moves. expandtabs(-1) is the same, because ExpandTabsArgs::tabsize sends every negative value to 0.

rustpython_common::str::expandtabs walks the string with the tab stop in tab_size and the current column in col_count, and on a tab it does tab_size - col_count. Both start at zero, so the first character makes col_count 1 while tab_size stays 0, and the subtraction underflows. The run of spaces asked for next is usize::MAX, and the allocation aborts the process. That is why a tab has to follow something: "\ta".expandtabs(0) starts the tab at column 0, subtracts 0 from 0, and comes out right by accident.

BytesInner::expandtabs already returns early for this, filtering the tabs out. The string version now does the same thing.

Reachable from any Python code, no C API and no unusual build needed:

"\t".join(parts).expandtabs(user_supplied_width)

Checked against CPython 3.14.7 over 23 subjects by 10 tab sizes by str, bytes and bytearray, 460 cases. All 460 agree now. Without the change 42 of them abort the process, and the bytes half is not among them, which is where the shape of the fix came from.

Tests are in crates/common/src/str.rs next to the function and in extra_tests/snippets/builtin_str.py alongside the expandtabs overflow cases that landed in #8524. Both cover the zero and negative sizes, ASCII and non-ASCII, tabs after a newline and after a carriage return, and a set of ordinary tab sizes so a fix that reached too far would show up.

Lib/test/string_tests.py has no case for a zero tab size, so test_str and test_bytes pass either way. They still pass here, along with test_textwrap, 523 tests.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed expandtabs handling for zero tab sizes, preventing excessive memory allocation and process termination.
    • Zero tab sizes now remove tab characters consistently across strings, bytes, and bytearrays.
    • Preserved existing behavior for positive tab sizes, including multiline and multibyte text.
  • Tests

    • Added coverage for zero, negative, and positive tab sizes across varied input types and content.
    • Added coverage for Unicode-safe replacement using empty patterns.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: facf520a-6186-4010-82c9-56e8a01639e1

📥 Commits

Reviewing files that changed from the base of the PR and between a707729 and 410844f.

📒 Files selected for processing (1)
  • extra_tests/snippets/builtin_str.py

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

expandtabs now removes tabs for a zero tab size instead of allocating an oversized space run. Rust and Python tests cover zero, negative, and positive tab sizes. Python tests also cover empty-pattern replacement.

Changes

Expandtabs zero-tab-size handling

Layer / File(s) Summary
Zero tab-size behavior
crates/common/src/str.rs, extra_tests/snippets/builtin_str.py
expandtabs removes tab characters when tab_size is zero. Rust and Python tests cover zero, negative, and positive tab sizes across strings, bytes, bytearrays, Unicode content, and line boundaries.

Empty-pattern replacement coverage

Layer / File(s) Summary
Empty-pattern replacement regression coverage
extra_tests/snippets/builtin_str.py
Python tests cover empty-pattern replacement for ASCII, Unicode, empty inputs, insertion counts, and ordinary replacement behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 41084

This change prevents process termination for zero or negative tab sizes and adds focused coverage; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: youknowone

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: preventing str.expandtabs from aborting when the tab size is zero.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 2 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

`"a\tb".expandtabs(0)` panicked with a capacity overflow. CPython returns
`'ab'`: with no width to advance to, the tabs come out and nothing else
moves. `expandtabs(-1)` is the same call, since `ExpandTabsArgs::tabsize`
sends every negative value to 0.

`expandtabs` keeps the tab stop in `tab_size` and the current column in
`col_count`, and on a tab it does `tab_size - col_count`. With a tab size of
zero both start at 0, the first character makes `col_count` 1 while
`tab_size` stays 0, and the subtraction underflows. The run of spaces asked
for next is `usize::MAX`, and the allocation aborts the process. A tab has to
follow something on the line to reach it: `"\ta".expandtabs(0)` subtracts 0
from 0 and comes out right by accident.

`BytesInner::expandtabs` already returns early for this and filters the tabs
out. The string version now does the same.

Assisted-by: Claude Code:claude-opus-5
@luantaraschi
luantaraschi force-pushed the fix/expandtabs-zero-tabsize branch from a707729 to 410844f Compare August 21, 2026 07:47

@youknowone youknowone left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍

@youknowone
youknowone merged commit 6f542bf into RustPython:main Aug 21, 2026
25 of 27 checks passed
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.

2 participants