Skip to content

Commit 49ff84b

Browse files
committed
gh-158252: Match Unicode whitespace in the C strptime parser
Py_ISSPACE excludes ASCII separators U+001C through U+001F, which the Python parser treats as whitespace. Consuming one as a literal can leave a space for the next numeric field, changing how adjacent fields split. Use Py_UNICODE_ISSPACE and add regression tests for all four public entry points. These cases returned different dates or times before the fix.
1 parent 79115de commit 49ff84b

3 files changed

Lines changed: 36 additions & 4 deletions

File tree

‎Lib/test/datetimetester.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,13 @@ def test_strptime_whitespace(self):
12811281
fmt = format_space.join(('%Y', '%m', '%d'))
12821282
self.assertEqual(date.strptime(text, fmt), date(2024, 2, 29))
12831283

1284+
def test_strptime_whitespace_before_adjacent_fields(self):
1285+
for separator in ('\x1c', '\x1d', '\x1e', '\x1f'):
1286+
with self.subTest(separator=separator):
1287+
text = f'2024{separator} 112'
1288+
fmt = f'%Y{separator}%d%m'
1289+
self.assertEqual(date.strptime(text, fmt), date(2024, 2, 11))
1290+
12841291
def test_strptime_single_digit(self):
12851292
# bpo-34903: Check that single digit dates are allowed.
12861293
strptime = date.strptime
@@ -3202,6 +3209,14 @@ def test_strptime_whitespace(self):
32023209
self.assertEqual(self.theclass.strptime(text, fmt),
32033210
self.theclass(2024, 2, 29, 12, 34, 56))
32043211

3212+
def test_strptime_whitespace_before_adjacent_fields(self):
3213+
for separator in ('\x1c', '\x1d', '\x1e', '\x1f'):
3214+
with self.subTest(separator=separator):
3215+
text = f'2024{separator} 112 34{separator} 123'
3216+
fmt = f'%Y{separator}%d%m %M{separator}%H%S'
3217+
self.assertEqual(self.theclass.strptime(text, fmt),
3218+
self.theclass(2024, 2, 11, 12, 34, 3))
3219+
32053220
def test_strptime_numeric_fallback(self):
32063221
cases = (
32073222
('2024\0-02-29', '%Y\0-%m-%d', (2024, 2, 29)),
@@ -4435,6 +4450,14 @@ def test_strptime_whitespace(self):
44354450
self.assertEqual(self.theclass.strptime(text, fmt),
44364451
self.theclass(12, 34, 56))
44374452

4453+
def test_strptime_whitespace_before_adjacent_fields(self):
4454+
for separator in ('\x1c', '\x1d', '\x1e', '\x1f'):
4455+
with self.subTest(separator=separator):
4456+
text = f'34{separator} 123'
4457+
fmt = f'%M{separator}%H%S'
4458+
self.assertEqual(self.theclass.strptime(text, fmt),
4459+
self.theclass(12, 34, 3))
4460+
44384461
def test_strptime_tz(self):
44394462
strptime = self.theclass.strptime
44404463
self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE)

‎Lib/test/test_time.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,14 @@ def test_strptime_whitespace(self):
382382
self.assertEqual(time.strptime(text, fmt),
383383
(2024, 2, 29, 12, 34, 56, 3, 60, -1))
384384

385+
def test_strptime_whitespace_before_adjacent_fields(self):
386+
for separator in ('\x1c', '\x1d', '\x1e', '\x1f'):
387+
with self.subTest(separator=separator):
388+
text = f'2024{separator} 112 34{separator} 123'
389+
fmt = f'%Y{separator}%d%m %M{separator}%H%S'
390+
self.assertEqual(time.strptime(text, fmt),
391+
(2024, 2, 11, 12, 34, 3, 6, 42, -1))
392+
385393
def test_strptime_numeric_calendar(self):
386394
cases = (
387395
('0001-01-01', 1, 1, 1, 0, 1),

‎Python/_strptime.c‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,17 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
111111

112112
for (Py_ssize_t i = 0; i < fmt_length; i++) {
113113
unsigned char c = fmt[i];
114-
if (Py_ISSPACE(c)) {
115-
if (pos == length || !Py_ISSPACE(data[pos])) {
114+
/* Match regex \s, including ASCII separators U+001C through U+001F. */
115+
if (Py_UNICODE_ISSPACE(c)) {
116+
if (pos == length || !Py_UNICODE_ISSPACE(data[pos])) {
116117
return 0;
117118
}
118-
while (i + 1 < fmt_length && Py_ISSPACE(fmt[i + 1])) {
119+
while (i + 1 < fmt_length && Py_UNICODE_ISSPACE(fmt[i + 1])) {
119120
i++;
120121
}
121122
do {
122123
pos++;
123-
} while (pos < length && Py_ISSPACE(data[pos]));
124+
} while (pos < length && Py_UNICODE_ISSPACE(data[pos]));
124125
continue;
125126
}
126127
if (c != '%') {

0 commit comments

Comments
 (0)