From 2e1a0f4cff6f5dd9145b7270701947faaa7f3d4a Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Fri, 3 Apr 2026 23:19:47 -0400 Subject: [PATCH] fix: Swapcase must handle multibyte expansions `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 b31dc6ccc9d..74564278925 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 12f97ac619d..b852e678ace 100644 --- a/extra_tests/snippets/builtin_str.py +++ b/extra_tests/snippets/builtin_str.py @@ -235,6 +235,8 @@ 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"