Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,19 +355,30 @@ def _find_isoformat_datetime_separator(dtstr):
return 8


def _read_isoformat_component(s, n):
# The caller has verified the string is ASCII, so isdigit() matches only
# the ASCII digits accepted by the C parser.
if len(s) != n or not s.isdigit():
raise ValueError("Invalid isoformat string")
return int(s)


def _parse_isoformat_date(dtstr):
# It is assumed that this is an ASCII-only string of lengths 7, 8 or 10,
# see the comment on Modules/_datetimemodule.c:_find_isoformat_datetime_separator
if len(dtstr) not in (7, 8, 10):
raise ValueError("Invalid isoformat string")
year = int(dtstr[0:4])
if not dtstr.isascii():
raise ValueError("Invalid isoformat string")

year = _read_isoformat_component(dtstr[0:4], 4)
has_sep = dtstr[4] == '-'

pos = 4 + has_sep
if dtstr[pos:pos + 1] == "W":
# YYYY-?Www-?D?
pos += 1
weekno = int(dtstr[pos:pos + 2])
weekno = _read_isoformat_component(dtstr[pos:pos + 2], 2)
pos += 2

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

pos += has_sep

dayno = int(dtstr[pos:pos + 1])
dayno = _read_isoformat_component(dtstr[pos:pos + 1], 1)

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

pos += has_sep
day = int(dtstr[pos:pos + 2])
day = _read_isoformat_component(dtstr[pos:pos + 2], 2)

return [year, month, day]

Expand All @@ -402,10 +413,7 @@ def _parse_hh_mm_ss_ff(tstr):
time_comps = [0, 0, 0, 0]
pos = 0
for comp in range(0, 3):
if (len_str - pos) < 2:
raise ValueError("Incomplete time component")

time_comps[comp] = int(tstr[pos:pos+2])
time_comps[comp] = _read_isoformat_component(tstr[pos:pos+2], 2)

pos += 2
next_char = tstr[pos:pos+1]
Expand All @@ -426,7 +434,7 @@ def _parse_hh_mm_ss_ff(tstr):
raise ValueError("Invalid microsecond separator")
else:
pos += 1
if not all(map(_is_ascii_digit, tstr[pos:])):
if not tstr[pos:].isdigit():
raise ValueError("Non-digit values in fraction")

len_remainder = len_str - pos
Expand All @@ -447,6 +455,8 @@ def _parse_isoformat_time(tstr):
len_str = len(tstr)
if len_str < 2:
raise ValueError("Isoformat time too short")
if not tstr.isascii():
raise ValueError("Invalid isoformat string")

# This is equivalent to re.search('[+-Z]', tstr), but faster
tz_pos = (tstr.find('-') + 1 or tstr.find('+') + 1 or tstr.find('Z') + 1)
Expand Down
19 changes: 18 additions & 1 deletion Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -2106,7 +2106,15 @@ def test_fromisoformat_fails(self):
'10000-W25-1', # Invalid year
'2020-W25-0', # Invalid day-of-week
'2020-W25-8', # Invalid day-of-week
'٢025-03-09' # Unicode characters
# gh-152204: each fixed-width field must be exactly N ASCII digits
'2020+12', # '+' in a basic-format field
'2020 12', # space in a basic-format field
'+020-06-15', # leading sign in the year
'202012+9', # '+' in the day field
'2020-W 5', # space in the week day-of-week field
'2020061', # 7 chars: day slice reads a 1-character tail
'2020-W2', # 1-digit week number
'٢025-03-09', # Unicode characters
'2009\ud80002\ud80028', # Separators are surrogate codepoints
]

Expand Down Expand Up @@ -3758,6 +3766,15 @@ def test_fromisoformat_fails_datetime(self):
'2009-04-19T12:30:45-00:90:00', # Time zone field out from range
'2009-04-19T12:30:45-00:00:90', # Time zone field out from range
'2020-2020', # Ambiguous 9-char date portion
# gh-152204: each time field must be exactly N ASCII digits
'2020-12-12T0٥:02:03', # Unicode digit in the hour
'2020-12-12T01:0٥:03', # Unicode digit in the minute
'2020-12-12T01:02:0٥', # Unicode digit in the second
'2020-12-12T01:02:03.٥', # Unicode digit in the fraction
'2020-12-12T01:02:03.4_6', # underscore in the fraction
'2020-12-12T01:02:03+0٥:00', # Unicode digit in the tz hour
'2020-12-12T01:02:03+01:0٥', # Unicode digit in the tz minute
'20201212T0102٣٤', # Unicode digits in the basic-format time
'2009-04-19T12:30:45.+05:00', # Empty fraction before offset
'2009-04-19T12:30:45.-05:00', # Empty fraction before offset
'2009-04-19T12:30:45.Z', # Empty fraction before Z
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fix the pure-Python implementations of :meth:`datetime.date.fromisoformat`,
:meth:`datetime.time.fromisoformat` and :meth:`datetime.datetime.fromisoformat`
silently accepting some malformed ISO 8601 strings, such as non-ASCII digits or
a sign in a fixed-width field (for example ``'2020+12'`` or ``'20201212T0102٣٤'``).
Each field is now required to be exactly *N* ASCII digits, matching the C
implementation.
Loading