Skip to content

Commit b885859

Browse files
authored
[3.14] gh-152204: Validate date and time fields in _pydatetime.date{time}.fromisoformat (GH-152205) (#156367)
(cherry picked from commit e81960f)
1 parent 7267c9b commit b885859

3 files changed

Lines changed: 44 additions & 11 deletions

File tree

Lib/_pydatetime.py

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

357357

358+
def _read_isoformat_component(s, n):
359+
# The caller has verified the string is ASCII, so isdigit() matches only
360+
# the ASCII digits accepted by the C parser.
361+
if len(s) != n or not s.isdigit():
362+
raise ValueError("Invalid isoformat string")
363+
return int(s)
364+
365+
358366
def _parse_isoformat_date(dtstr):
359367
# It is assumed that this is an ASCII-only string of lengths 7, 8 or 10,
360368
# see the comment on Modules/_datetimemodule.c:_find_isoformat_datetime_separator
361369
if len(dtstr) not in (7, 8, 10):
362370
raise ValueError("Invalid isoformat string")
363-
year = int(dtstr[0:4])
371+
if not dtstr.isascii():
372+
raise ValueError("Invalid isoformat string")
373+
374+
year = _read_isoformat_component(dtstr[0:4], 4)
364375
has_sep = dtstr[4] == '-'
365376

366377
pos = 4 + has_sep
367378
if dtstr[pos:pos + 1] == "W":
368379
# YYYY-?Www-?D?
369380
pos += 1
370-
weekno = int(dtstr[pos:pos + 2])
381+
weekno = _read_isoformat_component(dtstr[pos:pos + 2], 2)
371382
pos += 2
372383

373384
dayno = 1
@@ -377,17 +388,17 @@ def _parse_isoformat_date(dtstr):
377388

378389
pos += has_sep
379390

380-
dayno = int(dtstr[pos:pos + 1])
391+
dayno = _read_isoformat_component(dtstr[pos:pos + 1], 1)
381392

382393
return list(_isoweek_to_gregorian(year, weekno, dayno))
383394
else:
384-
month = int(dtstr[pos:pos + 2])
395+
month = _read_isoformat_component(dtstr[pos:pos + 2], 2)
385396
pos += 2
386397
if (dtstr[pos:pos + 1] == "-") != has_sep:
387398
raise ValueError("Inconsistent use of dash separator")
388399

389400
pos += has_sep
390-
day = int(dtstr[pos:pos + 2])
401+
day = _read_isoformat_component(dtstr[pos:pos + 2], 2)
391402

392403
return [year, month, day]
393404

@@ -402,10 +413,7 @@ def _parse_hh_mm_ss_ff(tstr):
402413
time_comps = [0, 0, 0, 0]
403414
pos = 0
404415
for comp in range(0, 3):
405-
if (len_str - pos) < 2:
406-
raise ValueError("Incomplete time component")
407-
408-
time_comps[comp] = int(tstr[pos:pos+2])
416+
time_comps[comp] = _read_isoformat_component(tstr[pos:pos+2], 2)
409417

410418
pos += 2
411419
next_char = tstr[pos:pos+1]
@@ -426,7 +434,7 @@ def _parse_hh_mm_ss_ff(tstr):
426434
raise ValueError("Invalid microsecond separator")
427435
else:
428436
pos += 1
429-
if not all(map(_is_ascii_digit, tstr[pos:])):
437+
if not tstr[pos:].isdigit():
430438
raise ValueError("Non-digit values in fraction")
431439

432440
len_remainder = len_str - pos
@@ -447,6 +455,8 @@ def _parse_isoformat_time(tstr):
447455
len_str = len(tstr)
448456
if len_str < 2:
449457
raise ValueError("Isoformat time too short")
458+
if not tstr.isascii():
459+
raise ValueError("Invalid isoformat string")
450460

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

Lib/test/datetimetester.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2100,7 +2100,15 @@ def test_fromisoformat_fails(self):
21002100
'10000-W25-1', # Invalid year
21012101
'2020-W25-0', # Invalid day-of-week
21022102
'2020-W25-8', # Invalid day-of-week
2103-
'٢025-03-09' # Unicode characters
2103+
# gh-152204: each fixed-width field must be exactly N ASCII digits
2104+
'2020+12', # '+' in a basic-format field
2105+
'2020 12', # space in a basic-format field
2106+
'+020-06-15', # leading sign in the year
2107+
'202012+9', # '+' in the day field
2108+
'2020-W 5', # space in the week number
2109+
'2020061', # 7 chars: day slice reads a 1-character tail
2110+
'2020-W2', # 1-digit week number
2111+
'٢025-03-09', # Unicode characters
21042112
'2009\ud80002\ud80028', # Separators are surrogate codepoints
21052113
]
21062114

@@ -3582,6 +3590,15 @@ def test_fromisoformat_fails_datetime(self):
35823590
'2009-04-19T12:30:45.400 ', # Trailing space (gh-130959)
35833591
'2009-04-19T12:30:45. 400', # Space before fraction (gh-130959)
35843592
'2020-2020', # Ambiguous 9-char date portion
3593+
# gh-152204: each time field must be exactly N ASCII digits
3594+
'2020-12-12T0٥:02:03', # Unicode digit in the hour
3595+
'2020-12-12T01:0٥:03', # Unicode digit in the minute
3596+
'2020-12-12T01:02:0٥', # Unicode digit in the second
3597+
'2020-12-12T01:02:03.٥', # Unicode digit in the fraction
3598+
'2020-12-12T01:02:03.4_6', # underscore in the fraction
3599+
'2020-12-12T01:02:03+0٥:00', # Unicode digit in the tz hour
3600+
'2020-12-12T01:02:03+01:0٥', # Unicode digit in the tz minute
3601+
'20201212T0102٣٤', # Unicode digits in the basic-format time
35853602
'2009-04-19T12:30:45.+05:00', # Empty fraction before offset
35863603
'2009-04-19T12:30:45.-05:00', # Empty fraction before offset
35873604
'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)