https://github.com/python/cpython/commit/6671d27327dd0518c502aaac9d1b2e079d80ef05 commit: 6671d27327dd0518c502aaac9d1b2e079d80ef05 branch: main author: Serhiy Storchaka <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-08-27T14:11:33+03:00 summary:
gh-156444: Fix a negated character set with IGNORECASE and LOCALE (GH-156445) charset_loc_ignore() tested the whole set once per locale case and took the disjunction, which complements a set before closing it under case instead of after: [bc] matched b'B', but so did [^bc]. Match both cases of the character against every set member instead. Co-authored-by: Claude Opus 5 (1M context) <[email protected]> files: A Misc/NEWS.d/next/Library/2026-08-27-08-31-29.gh-issue-156444.klcySy.rst M Lib/test/test_re.py M Modules/_sre/sre_lib.h diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py index d086fd521e73b59..ff106c1b3415668 100644 --- a/Lib/test/test_re.py +++ b/Lib/test/test_re.py @@ -2145,6 +2145,28 @@ def test_locale_flag(self): self.assertRaises(ValueError, re.compile, b'(?a)', re.LOCALE) self.assertRaises(re.PatternError, re.compile, b'(?aL)') + def test_locale_ignorecase_negated_set(self): + IL = re.LOCALE | re.IGNORECASE + # [bc] matches b'B', so [^bc] must not. + self.assertTrue(re.fullmatch(rb'[bc]', b'B', IL)) + self.assertIsNone(re.fullmatch(rb'[^bc]', b'B', IL)) + self.assertIsNone(re.fullmatch(rb'[^b-c]', b'C', IL)) + self.assertIsNone(re.fullmatch(rb'[^bc]', b'c', IL)) + self.assertTrue(re.fullmatch(rb'[^bc]', b'a', IL)) + # A one-member set compiles to NOT_LITERAL_LOC_IGNORE. + self.assertIsNone(re.fullmatch(rb'[^b]', b'B', IL)) + self.assertTrue(re.fullmatch(rb'[^b]', b'a', IL)) + self.assertIsNone(re.fullmatch(rb'[^\wq]', b'Q', IL)) + # A sparse set compiles to a bitmap instead of ranges. + self.assertTrue(re.fullmatch(rb'[ace]', b'C', IL)) + self.assertIsNone(re.fullmatch(rb'[^ace]', b'C', IL)) + self.assertTrue(re.fullmatch(rb'[^ace]', b'b', IL)) + # An alternation folded into a set puts NEGATE in the middle of it. + self.assertIsNone(re.fullmatch(rb'(?:a|[^bc])', b'B', IL)) + self.assertTrue(re.fullmatch(rb'(?:a|[^bc])', b'A', IL)) + self.assertIsNone(re.fullmatch(rb'\w(?<!b)', b'B', IL)) + self.assertTrue(re.fullmatch(rb'\w(?<!b)', b'A', IL)) + def test_scoped_flags(self): self.assertTrue(re.match(r'(?i:a)b', 'Ab')) self.assertIsNone(re.match(r'(?i:a)b', 'aB')) diff --git a/Misc/NEWS.d/next/Library/2026-08-27-08-31-29.gh-issue-156444.klcySy.rst b/Misc/NEWS.d/next/Library/2026-08-27-08-31-29.gh-issue-156444.klcySy.rst new file mode 100644 index 000000000000000..cae73e534c47ddd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-27-08-31-29.gh-issue-156444.klcySy.rst @@ -0,0 +1,3 @@ +Fix matching of a negated character set in a bytes pattern compiled with both +:const:`re.IGNORECASE` and :const:`re.LOCALE`. The case closure is now applied +to the members of the set, so that ``[^bc]`` no longer matches ``b'B'``. diff --git a/Modules/_sre/sre_lib.h b/Modules/_sre/sre_lib.h index 444cd39d2fed288..fbdbaefb399193d 100644 --- a/Modules/_sre/sre_lib.h +++ b/Modules/_sre/sre_lib.h @@ -175,16 +175,71 @@ SRE(charset)(SRE_STATE* state, const SRE_CODE* set, SRE_CODE ch) } } +/* Like SRE(charset), but matches both locale cases of ch against every set + member. Testing the whole set once per case would complement it before + closing it under case instead of after, so that [^bc] matched b'B'. + BIGCHARSET and RANGE_UNI_IGNORE are not handled: they never occur in a + set of a bytes pattern. */ LOCAL(int) SRE(charset_loc_ignore)(SRE_STATE* state, const SRE_CODE* set, SRE_CODE ch) { SRE_CODE lo, up; - lo = sre_lower_locale(ch); - if (SRE(charset)(state, set, lo)) - return 1; + int ok = 1; + lo = sre_lower_locale(ch); up = sre_upper_locale(ch); - return up != lo && SRE(charset)(state, set, up); + if (up == lo) + return SRE(charset)(state, set, lo); + + for (;;) { + switch (*set++) { + + case SRE_OP_FAILURE: + return !ok; + + case SRE_OP_LITERAL: + /* <LITERAL> <code> */ + if (lo == set[0] || up == set[0]) + return ok; + set++; + break; + + case SRE_OP_CATEGORY: + /* <CATEGORY> <code> */ + if (sre_category(set[0], (int) lo) || + sre_category(set[0], (int) up)) + return ok; + set++; + break; + + case SRE_OP_CHARSET: + /* <CHARSET> <bitmap> */ + if ((lo < 256 && (set[lo/SRE_CODE_BITS] + & (1u << (lo & (SRE_CODE_BITS-1))))) || + (up < 256 && (set[up/SRE_CODE_BITS] + & (1u << (up & (SRE_CODE_BITS-1)))))) + return ok; + set += 256/SRE_CODE_BITS; + break; + + case SRE_OP_RANGE: + /* <RANGE> <lower> <upper> */ + if ((set[0] <= lo && lo <= set[1]) || + (set[0] <= up && up <= set[1])) + return ok; + set += 2; + break; + + case SRE_OP_NEGATE: + ok = !ok; + break; + + default: + /* internal error -- there's not much we can do about it + here, so let's just pretend it didn't match... */ + return 0; + } + } } LOCAL(Py_ssize_t) SRE(match)(SRE_STATE* state, const SRE_CODE* pattern, int toplevel); _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
