Skip to content

Commit 6671d27

Browse files
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) <noreply@anthropic.com>
1 parent 79f41dd commit 6671d27

3 files changed

Lines changed: 84 additions & 4 deletions

File tree

Lib/test/test_re.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2145,6 +2145,28 @@ def test_locale_flag(self):
21452145
self.assertRaises(ValueError, re.compile, b'(?a)', re.LOCALE)
21462146
self.assertRaises(re.PatternError, re.compile, b'(?aL)')
21472147

2148+
def test_locale_ignorecase_negated_set(self):
2149+
IL = re.LOCALE | re.IGNORECASE
2150+
# [bc] matches b'B', so [^bc] must not.
2151+
self.assertTrue(re.fullmatch(rb'[bc]', b'B', IL))
2152+
self.assertIsNone(re.fullmatch(rb'[^bc]', b'B', IL))
2153+
self.assertIsNone(re.fullmatch(rb'[^b-c]', b'C', IL))
2154+
self.assertIsNone(re.fullmatch(rb'[^bc]', b'c', IL))
2155+
self.assertTrue(re.fullmatch(rb'[^bc]', b'a', IL))
2156+
# A one-member set compiles to NOT_LITERAL_LOC_IGNORE.
2157+
self.assertIsNone(re.fullmatch(rb'[^b]', b'B', IL))
2158+
self.assertTrue(re.fullmatch(rb'[^b]', b'a', IL))
2159+
self.assertIsNone(re.fullmatch(rb'[^\wq]', b'Q', IL))
2160+
# A sparse set compiles to a bitmap instead of ranges.
2161+
self.assertTrue(re.fullmatch(rb'[ace]', b'C', IL))
2162+
self.assertIsNone(re.fullmatch(rb'[^ace]', b'C', IL))
2163+
self.assertTrue(re.fullmatch(rb'[^ace]', b'b', IL))
2164+
# An alternation folded into a set puts NEGATE in the middle of it.
2165+
self.assertIsNone(re.fullmatch(rb'(?:a|[^bc])', b'B', IL))
2166+
self.assertTrue(re.fullmatch(rb'(?:a|[^bc])', b'A', IL))
2167+
self.assertIsNone(re.fullmatch(rb'\w(?<!b)', b'B', IL))
2168+
self.assertTrue(re.fullmatch(rb'\w(?<!b)', b'A', IL))
2169+
21482170
def test_scoped_flags(self):
21492171
self.assertTrue(re.match(r'(?i:a)b', 'Ab'))
21502172
self.assertIsNone(re.match(r'(?i:a)b', 'aB'))
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix matching of a negated character set in a bytes pattern compiled with both
2+
:const:`re.IGNORECASE` and :const:`re.LOCALE`. The case closure is now applied
3+
to the members of the set, so that ``[^bc]`` no longer matches ``b'B'``.

Modules/_sre/sre_lib.h

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,16 +175,71 @@ SRE(charset)(SRE_STATE* state, const SRE_CODE* set, SRE_CODE ch)
175175
}
176176
}
177177

178+
/* Like SRE(charset), but matches both locale cases of ch against every set
179+
member. Testing the whole set once per case would complement it before
180+
closing it under case instead of after, so that [^bc] matched b'B'.
181+
BIGCHARSET and RANGE_UNI_IGNORE are not handled: they never occur in a
182+
set of a bytes pattern. */
178183
LOCAL(int)
179184
SRE(charset_loc_ignore)(SRE_STATE* state, const SRE_CODE* set, SRE_CODE ch)
180185
{
181186
SRE_CODE lo, up;
182-
lo = sre_lower_locale(ch);
183-
if (SRE(charset)(state, set, lo))
184-
return 1;
187+
int ok = 1;
185188

189+
lo = sre_lower_locale(ch);
186190
up = sre_upper_locale(ch);
187-
return up != lo && SRE(charset)(state, set, up);
191+
if (up == lo)
192+
return SRE(charset)(state, set, lo);
193+
194+
for (;;) {
195+
switch (*set++) {
196+
197+
case SRE_OP_FAILURE:
198+
return !ok;
199+
200+
case SRE_OP_LITERAL:
201+
/* <LITERAL> <code> */
202+
if (lo == set[0] || up == set[0])
203+
return ok;
204+
set++;
205+
break;
206+
207+
case SRE_OP_CATEGORY:
208+
/* <CATEGORY> <code> */
209+
if (sre_category(set[0], (int) lo) ||
210+
sre_category(set[0], (int) up))
211+
return ok;
212+
set++;
213+
break;
214+
215+
case SRE_OP_CHARSET:
216+
/* <CHARSET> <bitmap> */
217+
if ((lo < 256 && (set[lo/SRE_CODE_BITS]
218+
& (1u << (lo & (SRE_CODE_BITS-1))))) ||
219+
(up < 256 && (set[up/SRE_CODE_BITS]
220+
& (1u << (up & (SRE_CODE_BITS-1))))))
221+
return ok;
222+
set += 256/SRE_CODE_BITS;
223+
break;
224+
225+
case SRE_OP_RANGE:
226+
/* <RANGE> <lower> <upper> */
227+
if ((set[0] <= lo && lo <= set[1]) ||
228+
(set[0] <= up && up <= set[1]))
229+
return ok;
230+
set += 2;
231+
break;
232+
233+
case SRE_OP_NEGATE:
234+
ok = !ok;
235+
break;
236+
237+
default:
238+
/* internal error -- there's not much we can do about it
239+
here, so let's just pretend it didn't match... */
240+
return 0;
241+
}
242+
}
188243
}
189244

190245
LOCAL(Py_ssize_t) SRE(match)(SRE_STATE* state, const SRE_CODE* pattern, int toplevel);

0 commit comments

Comments
 (0)