Skip to content

Commit 71c42b7

Browse files
donBarboserlend-aaslandpganssle
authored
gh-126883: Add check that timezone fields are in range for datetime.fromisoformat (#127242)
It was previously possible to specify things like `+00:90:00` which would be equivalent to `+01:30:00`, but is not a valid ISO8601 string. --------- Co-authored-by: Erlend E. Aasland <erlend.aasland@protonmail.com> Co-authored-by: Paul Ganssle <1377457+pganssle@users.noreply.github.com>
1 parent 8d490b3 commit 71c42b7

File tree

5 files changed

+63
-9
lines changed

5 files changed

+63
-9
lines changed

Lib/_pydatetime.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ def _parse_isoformat_time(tstr):
467467
hour, minute, second, microsecond = time_comps
468468
became_next_day = False
469469
error_from_components = False
470+
error_from_tz = None
470471
if (hour == 24):
471472
if all(time_comp == 0 for time_comp in time_comps[1:]):
472473
hour = 0
@@ -500,14 +501,22 @@ def _parse_isoformat_time(tstr):
500501
else:
501502
tzsign = -1 if tstr[tz_pos - 1] == '-' else 1
502503

503-
td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],
504-
seconds=tz_comps[2], microseconds=tz_comps[3])
505-
506-
tzi = timezone(tzsign * td)
504+
try:
505+
# This function is intended to validate datetimes, but because
506+
# we restrict time zones to ±24h, it serves here as well.
507+
_check_time_fields(hour=tz_comps[0], minute=tz_comps[1],
508+
second=tz_comps[2], microsecond=tz_comps[3],
509+
fold=0)
510+
except ValueError as e:
511+
error_from_tz = e
512+
else:
513+
td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],
514+
seconds=tz_comps[2], microseconds=tz_comps[3])
515+
tzi = timezone(tzsign * td)
507516

508517
time_comps.append(tzi)
509518

510-
return time_comps, became_next_day, error_from_components
519+
return time_comps, became_next_day, error_from_components, error_from_tz
511520

512521
# tuple[int, int, int] -> tuple[int, int, int] version of date.fromisocalendar
513522
def _isoweek_to_gregorian(year, week, day):
@@ -1633,9 +1642,21 @@ def fromisoformat(cls, time_string):
16331642
time_string = time_string.removeprefix('T')
16341643

16351644
try:
1636-
return cls(*_parse_isoformat_time(time_string)[0])
1637-
except Exception:
1638-
raise ValueError(f'Invalid isoformat string: {time_string!r}')
1645+
time_components, _, error_from_components, error_from_tz = (
1646+
_parse_isoformat_time(time_string)
1647+
)
1648+
except ValueError:
1649+
raise ValueError(
1650+
f'Invalid isoformat string: {time_string!r}') from None
1651+
else:
1652+
if error_from_tz:
1653+
raise error_from_tz
1654+
if error_from_components:
1655+
raise ValueError(
1656+
"Minute, second, and microsecond must be 0 when hour is 24"
1657+
)
1658+
1659+
return cls(*time_components)
16391660

16401661
def strftime(self, format):
16411662
"""Format using strftime(). The date part of the timestamp passed
@@ -1947,11 +1968,16 @@ def fromisoformat(cls, date_string):
19471968

19481969
if tstr:
19491970
try:
1950-
time_components, became_next_day, error_from_components = _parse_isoformat_time(tstr)
1971+
(time_components,
1972+
became_next_day,
1973+
error_from_components,
1974+
error_from_tz) = _parse_isoformat_time(tstr)
19511975
except ValueError:
19521976
raise ValueError(
19531977
f'Invalid isoformat string: {date_string!r}') from None
19541978
else:
1979+
if error_from_tz:
1980+
raise error_from_tz
19551981
if error_from_components:
19561982
raise ValueError("minute, second, and microsecond must be 0 when hour is 24")
19571983

Lib/test/datetimetester.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3571,6 +3571,10 @@ def test_fromisoformat_fails_datetime(self):
35713571
'2009-04-19T12:30:45.400 +02:30', # Space between ms and timezone (gh-130959)
35723572
'2009-04-19T12:30:45.400 ', # Trailing space (gh-130959)
35733573
'2009-04-19T12:30:45. 400', # Space before fraction (gh-130959)
3574+
'2009-04-19T12:30:45+00:90:00', # Time zone field out from range
3575+
'2009-04-19T12:30:45+00:00:90', # Time zone field out from range
3576+
'2009-04-19T12:30:45-00:90:00', # Time zone field out from range
3577+
'2009-04-19T12:30:45-00:00:90', # Time zone field out from range
35743578
]
35753579

35763580
for bad_str in bad_strs:
@@ -4795,6 +4799,11 @@ def test_fromisoformat_fails(self):
47954799
'12:30:45.400 +02:30', # Space between ms and timezone (gh-130959)
47964800
'12:30:45.400 ', # Trailing space (gh-130959)
47974801
'12:30:45. 400', # Space before fraction (gh-130959)
4802+
'24:00:00.000001', # Has non-zero microseconds on 24:00
4803+
'24:00:01.000000', # Has non-zero seconds on 24:00
4804+
'24:01:00.000000', # Has non-zero minutes on 24:00
4805+
'12:30:45+00:90:00', # Time zone field out from range
4806+
'12:30:45+00:00:90', # Time zone field out from range
47984807
]
47994808

48004809
for bad_str in bad_strs:

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,6 +1288,7 @@ Paul Moore
12881288
Ross Moore
12891289
Ben Morgan
12901290
Emily Morehouse
1291+
Semyon Moroz
12911292
Derek Morr
12921293
James A Morrison
12931294
Martin Morrison
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add check that timezone fields are in range for
2+
:meth:`datetime.datetime.fromisoformat` and
3+
:meth:`datetime.time.fromisoformat`. Patch by Semyon Moroz.

Modules/_datetimemodule.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,7 @@ parse_isoformat_time(const char *dtstr, size_t dtlen, int *hour, int *minute,
10881088
// -3: Failed to parse time component
10891089
// -4: Failed to parse time separator
10901090
// -5: Malformed timezone string
1091+
// -6: Timezone fields are not in range
10911092

10921093
const char *p = dtstr;
10931094
const char *p_end = dtstr + dtlen;
@@ -1134,6 +1135,11 @@ parse_isoformat_time(const char *dtstr, size_t dtlen, int *hour, int *minute,
11341135
rv = parse_hh_mm_ss_ff(tzinfo_pos, p_end, &tzhour, &tzminute, &tzsecond,
11351136
tzmicrosecond);
11361137

1138+
// Check if timezone fields are in range
1139+
if (check_time_args(tzhour, tzminute, tzsecond, *tzmicrosecond, 0) < 0) {
1140+
return -6;
1141+
}
1142+
11371143
*tzoffset = tzsign * ((tzhour * 3600) + (tzminute * 60) + tzsecond);
11381144
*tzmicrosecond *= tzsign;
11391145

@@ -5039,6 +5045,9 @@ time_fromisoformat(PyObject *cls, PyObject *tstr) {
50395045
&tzoffset, &tzimicrosecond);
50405046

50415047
if (rv < 0) {
5048+
if (rv == -6) {
5049+
goto error;
5050+
}
50425051
goto invalid_string_error;
50435052
}
50445053

@@ -5075,6 +5084,9 @@ time_fromisoformat(PyObject *cls, PyObject *tstr) {
50755084
invalid_string_error:
50765085
PyErr_Format(PyExc_ValueError, "Invalid isoformat string: %R", tstr);
50775086
return NULL;
5087+
5088+
error:
5089+
return NULL;
50785090
}
50795091

50805092

@@ -5927,6 +5939,9 @@ datetime_fromisoformat(PyObject *cls, PyObject *dtstr)
59275939
len -= (p - dt_ptr);
59285940
rv = parse_isoformat_time(p, len, &hour, &minute, &second,
59295941
&microsecond, &tzoffset, &tzusec);
5942+
if (rv == -6) {
5943+
goto error;
5944+
}
59305945
}
59315946
if (rv < 0) {
59325947
goto invalid_string_error;

0 commit comments

Comments
 (0)