Skip to content

Commit 7e7e88f

Browse files
miss-islingtonserhiy-storchakaclaude
authored
[3.13] gh-156444: Fix a negated character set with IGNORECASE and LOCALE (GH-156445) (GH-156458)
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. (cherry picked from commit 6671d27) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9d85ddf commit 7e7e88f

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
@@ -1919,6 +1919,28 @@ def test_locale_flag(self):
19191919
self.assertRaises(ValueError, re.compile, b'(?a)', re.LOCALE)
19201920
self.assertRaises(re.PatternError, re.compile, b'(?aL)')
19211921

1922+
def test_locale_ignorecase_negated_set(self):
1923+
IL = re.LOCALE | re.IGNORECASE
1924+
# [bc] matches b'B', so [^bc] must not.
1925+
self.assertTrue(re.fullmatch(rb'[bc]', b'B', IL))
1926+
self.assertIsNone(re.fullmatch(rb'[^bc]', b'B', IL))
1927+
self.assertIsNone(re.fullmatch(rb'[^b-c]', b'C', IL))
1928+
self.assertIsNone(re.fullmatch(rb'[^bc]', b'c', IL))
1929+
self.assertTrue(re.fullmatch(rb'[^bc]', b'a', IL))
1930+
# A one-member set compiles to NOT_LITERAL_LOC_IGNORE.
1931+
self.assertIsNone(re.fullmatch(rb'[^b]', b'B', IL))
1932+
self.assertTrue(re.fullmatch(rb'[^b]', b'a', IL))
1933+
self.assertIsNone(re.fullmatch(rb'[^\wq]', b'Q', IL))
1934+
# A sparse set compiles to a bitmap instead of ranges.
1935+
self.assertTrue(re.fullmatch(rb'[ace]', b'C', IL))
1936+
self.assertIsNone(re.fullmatch(rb'[^ace]', b'C', IL))
1937+
self.assertTrue(re.fullmatch(rb'[^ace]', b'b', IL))
1938+
# An alternation folded into a set puts NEGATE in the middle of it.
1939+
self.assertIsNone(re.fullmatch(rb'(?:a|[^bc])', b'B', IL))
1940+
self.assertTrue(re.fullmatch(rb'(?:a|[^bc])', b'A', IL))
1941+
self.assertIsNone(re.fullmatch(rb'\w(?<!b)', b'B', IL))
1942+
self.assertTrue(re.fullmatch(rb'\w(?<!b)', b'A', IL))
1943+
19221944
def test_scoped_flags(self):
19231945
self.assertTrue(re.match(r'(?i:a)b', 'Ab'))
19241946
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
@@ -187,16 +187,71 @@ SRE(charset)(SRE_STATE* state, const SRE_CODE* set, SRE_CODE ch)
187187
}
188188
}
189189

190+
/* Like SRE(charset), but matches both locale cases of ch against every set
191+
member. Testing the whole set once per case would complement it before
192+
closing it under case instead of after, so that [^bc] matched b'B'.
193+
BIGCHARSET and RANGE_UNI_IGNORE are not handled: they never occur in a
194+
set of a bytes pattern. */
190195
LOCAL(int)
191196
SRE(charset_loc_ignore)(SRE_STATE* state, const SRE_CODE* set, SRE_CODE ch)
192197
{
193198
SRE_CODE lo, up;
194-
lo = sre_lower_locale(ch);
195-
if (SRE(charset)(state, set, lo))
196-
return 1;
199+
int ok = 1;
197200

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

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

0 commit comments

Comments
 (0)