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
25 changes: 19 additions & 6 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 @@ -405,7 +416,7 @@ def _parse_hh_mm_ss_ff(tstr):
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 Down Expand Up @@ -447,6 +458,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
26 changes: 12 additions & 14 deletions 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', # '+' accepted in a basic-format field
'2020 12', # space accepted 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,10 +3766,9 @@ 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
'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
'2009-04-19T12:30:45,+05:00', # Empty fraction (comma) before offset
'2009-04-19T12:30:4٥', # Unicode digit in the seconds
'20201212T0102٣٤', # Unicode digits in the time (gh-152204)
'2009-04-19T12:30:45+0٥:00', # Unicode digit in the timezone
]

for bad_str in bad_strs:
Expand Down Expand Up @@ -4123,11 +4130,6 @@ def test_strftime_special(self):
self.assertEqual(t.strftime('\0'*1000), '\0'*1000)
self.assertEqual(t.strftime('\0%I%p%Z\0%X'), f'\0{s1}\0{s2}')
self.assertEqual(t.strftime('%I%p%Z\0%X\0'), f'{s1}\0{s2}\0')
# gh-152305: the year directives must not raise on a time.
for directive, expected in (('%Y', '1900'), ('%G', '1900'),
('%C', '19'), ('%F', '1900-01-01')):
with self.subTest(directive=directive):
self.assertEqual(t.strftime(directive), expected)

def test_format(self):
t = self.theclass(1, 2, 3, 4)
Expand Down Expand Up @@ -5038,10 +5040,6 @@ def test_fromisoformat_fails(self):
'24:01:00.000000', # Has non-zero minutes on 24:00
'12:30:45+00:90:00', # Time zone field out from range
'12:30:45+00:00:90', # Time zone field out from range
'12:30:45.+05:00', # Empty fraction before offset
'12:30:45.-05:00', # Empty fraction before offset
'12:30:45.Z', # Empty fraction before Z
'12:30:45,+05:00', # Empty fraction (comma) before offset
]

for bad_str in bad_strs:
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