From 6493f9a31d82e610a693a1d3ecd6c76a5656ad46 Mon Sep 17 00:00:00 2001 From: Joshua Megnauth <48846352+joshuamegnauth54@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:29:30 -0400 Subject: [PATCH] fix: Swapcase must handle multibyte expansions (#7559) `swapcase` used `to_ascii_lowercase` and uppercase to swap cases. This is fine for ASCII, but code points may expand into multiple bytes which leads to incorrect case swaps for some languages. The fix is to use `to_lowercase` and `to_uppercase` instead. Unfortunately, this leads to a realloc in `swapcase` when bytes are expanded. Part of #7526. --- crates/vm/src/builtins/str.rs | 6 +++--- extra_tests/snippets/builtin_str.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/vm/src/builtins/str.rs b/crates/vm/src/builtins/str.rs index b31dc6ccc..745642789 100644 --- a/crates/vm/src/builtins/str.rs +++ b/crates/vm/src/builtins/str.rs @@ -1045,11 +1045,11 @@ impl PyStr { let mut swapped_str = Wtf8Buf::with_capacity(self.data.len()); for c_orig in self.as_wtf8().code_points() { let c = c_orig.to_char_lossy(); - // to_uppercase returns an iterator, to_ascii_uppercase returns the char + // to_uppercase returns an iterator because case changes may be multiple bytes if c.is_lowercase() { - swapped_str.push_char(c.to_ascii_uppercase()); + swapped_str.extend(c.to_uppercase()); } else if c.is_uppercase() { - swapped_str.push_char(c.to_ascii_lowercase()); + swapped_str.extend(c.to_lowercase()); } else { swapped_str.push(c_orig); } diff --git a/extra_tests/snippets/builtin_str.py b/extra_tests/snippets/builtin_str.py index 12f97ac61..b852e678a 100644 --- a/extra_tests/snippets/builtin_str.py +++ b/extra_tests/snippets/builtin_str.py @@ -235,6 +235,8 @@ assert "유니코드".isidentifier() assert not "😂".isidentifier() assert not "123".isidentifier() +assert "Σίσυφος".swapcase() == "σΊΣΥΦΟΣ" + # String Formatting assert "{} {}".format(1, 2) == "1 2" assert "{0} {1}".format(2, 3) == "2 3"