mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
Fix str.expandtabs aborting on a tab size of zero (#8562)
`"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
This commit is contained in:
1 parent
c4549e1985
commit
cdb8a8dd83
2 files changed
+61
No files matched your search
@@ -743,6 +743,15 @@ pub mod levenshtein {
|
||||
/// Replace all tabs in a string with spaces, using the given tab size.
|
||||
#[must_use]
|
||||
pub fn expandtabs(input: &str, tab_size: usize) -> String {
|
||||
// A tab size of zero, which is also where a negative one lands, leaves no
|
||||
// column for a tab to advance to: the tabs come out and nothing else moves.
|
||||
// Going through the arithmetic anyway subtracts the current column from a
|
||||
// tab stop of zero and underflows on the first tab, so the width asked for
|
||||
// next is `usize::MAX`. The bytes version of this already returns here.
|
||||
if tab_size == 0 {
|
||||
return input.chars().filter(|ch| *ch != '\t').collect();
|
||||
}
|
||||
|
||||
let tab_stop = tab_size;
|
||||
let mut expanded_str = String::with_capacity(input.len());
|
||||
let mut tab_size = tab_stop;
|
||||
@@ -905,4 +914,28 @@ mod tests {
|
||||
let s = "0😀😃😄😁😆😅😂🤣9";
|
||||
assert_eq!(get_chars(s, 3..7), "😄😁😆😅");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expandtabs_with_zero_tab_size_drops_tabs() {
|
||||
// A tab that follows a character used to subtract that column from a
|
||||
// tab stop of zero, so the width of the run of spaces came out as
|
||||
// `usize::MAX` and the allocation aborted the process.
|
||||
assert_eq!(expandtabs("a\tb", 0), "ab");
|
||||
assert_eq!(expandtabs("ab\tcd\tef", 0), "abcdef");
|
||||
assert_eq!(expandtabs("a\nb\tc", 0), "a\nbc");
|
||||
assert_eq!(expandtabs("á\tb", 0), "áb");
|
||||
assert_eq!(expandtabs("\ta", 0), "a");
|
||||
assert_eq!(expandtabs("\t", 0), "");
|
||||
assert_eq!(expandtabs("", 0), "");
|
||||
assert_eq!(expandtabs("no tabs", 0), "no tabs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expandtabs_with_a_real_tab_size_is_unchanged() {
|
||||
assert_eq!(expandtabs("a\tb", 8), "a b");
|
||||
assert_eq!(expandtabs("a\tb", 1), "a b");
|
||||
assert_eq!(expandtabs("abcd\te", 4), "abcd e");
|
||||
assert_eq!(expandtabs("a\nb\tc", 4), "a\nb c");
|
||||
assert_eq!(expandtabs("\ta", 4), " a");
|
||||
}
|
||||
}
|
||||
@@ -951,3 +951,31 @@ def test_replace_empty_pattern():
|
||||
|
||||
|
||||
test_replace_empty_pattern()
|
||||
|
||||
|
||||
def test_expandtabs_zero_tabsize():
|
||||
# With no width to advance to, the tabs come out and nothing else moves.
|
||||
# A tab that followed a character used to ask for a run of usize::MAX
|
||||
# spaces and take the interpreter down with it.
|
||||
for tabsize in (0, -1, -8):
|
||||
assert "a\tb".expandtabs(tabsize) == "ab"
|
||||
assert "ab\tcd\tef".expandtabs(tabsize) == "abcdef"
|
||||
assert "a\nb\tc".expandtabs(tabsize) == "a\nbc"
|
||||
assert "a\r\nb\tc".expandtabs(tabsize) == "a\r\nbc"
|
||||
assert "á\tb".expandtabs(tabsize) == "áb"
|
||||
assert "😀\tb".expandtabs(tabsize) == "😀b"
|
||||
assert "\ta".expandtabs(tabsize) == "a"
|
||||
assert "\t".expandtabs(tabsize) == ""
|
||||
assert "".expandtabs(tabsize) == ""
|
||||
assert "no tabs".expandtabs(tabsize) == "no tabs"
|
||||
assert b"a\tb".expandtabs(tabsize) == b"ab"
|
||||
assert bytearray(b"a\tb").expandtabs(tabsize) == bytearray(b"ab")
|
||||
|
||||
# A tab size that is actually there keeps working.
|
||||
assert "a\tb".expandtabs(8) == "a b"
|
||||
assert "a\tb".expandtabs(1) == "a b"
|
||||
assert "abcd\te".expandtabs(4) == "abcd e"
|
||||
assert "a\nb\tc".expandtabs(4) == "a\nb c"
|
||||
|
||||
|
||||
test_expandtabs_zero_tabsize()
|
||||
Reference in new issue
Block a user