Skip to content

Commit b97bfb0

Browse files
authored
[3.13] gh-152204: Validate date and time fields in _pydatetime.date{time}.fromisoformat (GH-152205) (#156368)
(cherry picked from commit e81960f)
1 parent 708b457 commit b97bfb0

3 files changed

Lines changed: 44 additions & 10 deletions

File tree

Lib/_pydatetime.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -332,19 +332,30 @@ def _find_isoformat_datetime_separator(dtstr):
332332
return 8
333333

334334

335+
def _read_isoformat_component(s, n):
336+
# The caller has verified the string is ASCII, so isdigit() matches only
337+
# the ASCII digits accepted by the C parser.
338+
if len(s) != n or not s.isdigit():
339+
raise ValueError("Invalid isoformat string")
340+
return int(s)
341+
342+
335343
def _parse_isoformat_date(dtstr):
336344
# It is assumed that this is an ASCII-only string of lengths 7, 8 or 10,
337345
# see the comment on Modules/_datetimemodule.c:_find_isoformat_datetime_separator
338346
if len(dtstr) not in (7, 8, 10):
339347
raise ValueError("Invalid isoformat string")
340-
year = int(dtstr[0:4])
348+
if not dtstr.isascii():
349+
raise ValueError("Invalid isoformat string")
350+
351+
year = _read_isoformat_component(dtstr[0:4], 4)
341352
has_sep = dtstr[4] == '-'
342353

343354
pos = 4 + has_sep
344355
if dtstr[pos:pos + 1] == "W":
345356
# YYYY-?Www-?D?
346357
pos += 1
347-
weekno = int(dtstr[pos:pos + 2])
358+
weekno = _read_isoformat_component(dtstr[pos:pos + 2], 2)
348359
pos += 2
349360

350361
dayno = 1
@@ -354,17 +365,17 @@ def _parse_isoformat_date(dtstr):
354365

355366
pos += has_sep
356367

357-
dayno = int(dtstr[pos:pos + 1])
368+
dayno = _read_isoformat_component(dtstr[pos:pos + 1], 1)
358369

359370
return list(_isoweek_to_gregorian(year, weekno, dayno))
360371
else:
361-
month = int(dtstr[pos:pos + 2])
372+
month = _read_isoformat_component(dtstr[pos:pos + 2], 2)
362373
pos += 2
363374
if (dtstr[pos:pos + 1] == "-") != has_sep:
364375
raise ValueError("Inconsistent use of dash separator")
365376

366377
pos += has_sep
367-
day = int(dtstr[pos:pos + 2])
378+
day = _read_isoformat_component(dtstr[pos:pos + 2], 2)
368379

369380
return [year, month, day]
370381

@@ -379,10 +390,7 @@ def _parse_hh_mm_ss_ff(tstr):
379390
time_comps = [0, 0, 0, 0]
380391
pos = 0
381392
for comp in range(0, 3):
382-
if (len_str - pos) < 2:
383-
raise ValueError("Incomplete time component")
384-
385-
time_comps[comp] = int(tstr[pos:pos+2])
393+
time_comps[comp] = _read_isoformat_component(tstr[pos:pos+2], 2)
386394

387395
pos += 2
388396
next_char = tstr[pos:pos+1]
@@ -403,7 +411,7 @@ def _parse_hh_mm_ss_ff(tstr):
403411
raise ValueError("Invalid microsecond component")
404412
else:
405413
pos += 1
406-
if not all(map(_is_ascii_digit, tstr[pos:])):
414+
if not tstr[pos:].isdigit():
407415
raise ValueError("Non-digit values in fraction")
408416

409417
len_remainder = len_str - pos
@@ -424,6 +432,8 @@ def _parse_isoformat_time(tstr):
424432
len_str = len(tstr)
425433
if len_str < 2:
426434
raise ValueError("Isoformat time too short")
435+
if not tstr.isascii():
436+
raise ValueError("Invalid isoformat string")
427437

428438
# This is equivalent to re.search('[+-Z]', tstr), but faster
429439
tz_pos = (tstr.find('-') + 1 or tstr.find('+') + 1 or tstr.find('Z') + 1)

Lib/test/datetimetester.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1949,6 +1949,15 @@ def test_fromisoformat_fails(self):
19491949
'10000-W25-1', # Invalid year
19501950
'2020-W25-0', # Invalid day-of-week
19511951
'2020-W25-8', # Invalid day-of-week
1952+
# gh-152204: each fixed-width field must be exactly N ASCII digits
1953+
'2020+12', # '+' in a basic-format field
1954+
'2020 12', # space in a basic-format field
1955+
'+020-06-15', # leading sign in the year
1956+
'202012+9', # '+' in the day field
1957+
'2020-W 5', # space in the week number
1958+
'2020061', # 7 chars: day slice reads a 1-character tail
1959+
'2020-W2', # 1-digit week number
1960+
'٢025-03-09', # Unicode characters
19521961
'2009\ud80002\ud80028', # Separators are surrogate codepoints
19531962
]
19541963

@@ -3396,6 +3405,15 @@ def test_fromisoformat_fails_datetime(self):
33963405
'2009-04-19T12:30:45.400 ', # Trailing space (gh-130959)
33973406
'2009-04-19T12:30:45. 400', # Space before fraction (gh-130959)
33983407
'2020-2020', # Ambiguous 9-char date portion
3408+
# gh-152204: each time field must be exactly N ASCII digits
3409+
'2020-12-12T0٥:02:03', # Unicode digit in the hour
3410+
'2020-12-12T01:0٥:03', # Unicode digit in the minute
3411+
'2020-12-12T01:02:0٥', # Unicode digit in the second
3412+
'2020-12-12T01:02:03.٥', # Unicode digit in the fraction
3413+
'2020-12-12T01:02:03.4_6', # underscore in the fraction
3414+
'2020-12-12T01:02:03+0٥:00', # Unicode digit in the tz hour
3415+
'2020-12-12T01:02:03+01:0٥', # Unicode digit in the tz minute
3416+
'20201212T0102٣٤', # Unicode digits in the basic-format time
33993417
'2009-04-19T12:30:45.+05:00', # Empty fraction before offset
34003418
'2009-04-19T12:30:45.-05:00', # Empty fraction before offset
34013419
'2009-04-19T12:30:45.Z', # Empty fraction before Z
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix the pure-Python implementations of :meth:`datetime.date.fromisoformat`,
2+
:meth:`datetime.time.fromisoformat` and :meth:`datetime.datetime.fromisoformat`
3+
silently accepting some malformed ISO 8601 strings, such as non-ASCII digits or
4+
a sign in a fixed-width field (for example ``'2020+12'`` or ``'20201212T0102٣٤'``).
5+
Each field is now required to be exactly *N* ASCII digits, matching the C
6+
implementation.

0 commit comments

Comments
 (0)