mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
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>
This commit is contained in:
parent
8d490b3687
commit
71c42b778d
5 changed files with 63 additions and 9 deletions
|
@ -467,6 +467,7 @@ def _parse_isoformat_time(tstr):
|
||||||
hour, minute, second, microsecond = time_comps
|
hour, minute, second, microsecond = time_comps
|
||||||
became_next_day = False
|
became_next_day = False
|
||||||
error_from_components = False
|
error_from_components = False
|
||||||
|
error_from_tz = None
|
||||||
if (hour == 24):
|
if (hour == 24):
|
||||||
if all(time_comp == 0 for time_comp in time_comps[1:]):
|
if all(time_comp == 0 for time_comp in time_comps[1:]):
|
||||||
hour = 0
|
hour = 0
|
||||||
|
@ -500,14 +501,22 @@ def _parse_isoformat_time(tstr):
|
||||||
else:
|
else:
|
||||||
tzsign = -1 if tstr[tz_pos - 1] == '-' else 1
|
tzsign = -1 if tstr[tz_pos - 1] == '-' else 1
|
||||||
|
|
||||||
td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],
|
try:
|
||||||
seconds=tz_comps[2], microseconds=tz_comps[3])
|
# This function is intended to validate datetimes, but because
|
||||||
|
# we restrict time zones to ±24h, it serves here as well.
|
||||||
tzi = timezone(tzsign * td)
|
_check_time_fields(hour=tz_comps[0], minute=tz_comps[1],
|
||||||
|
second=tz_comps[2], microsecond=tz_comps[3],
|
||||||
|
fold=0)
|
||||||
|
except ValueError as e:
|
||||||
|
error_from_tz = e
|
||||||
|
else:
|
||||||
|
td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],
|
||||||
|
seconds=tz_comps[2], microseconds=tz_comps[3])
|
||||||
|
tzi = timezone(tzsign * td)
|
||||||
|
|
||||||
time_comps.append(tzi)
|
time_comps.append(tzi)
|
||||||
|
|
||||||
return time_comps, became_next_day, error_from_components
|
return time_comps, became_next_day, error_from_components, error_from_tz
|
||||||
|
|
||||||
# tuple[int, int, int] -> tuple[int, int, int] version of date.fromisocalendar
|
# tuple[int, int, int] -> tuple[int, int, int] version of date.fromisocalendar
|
||||||
def _isoweek_to_gregorian(year, week, day):
|
def _isoweek_to_gregorian(year, week, day):
|
||||||
|
@ -1633,9 +1642,21 @@ class time:
|
||||||
time_string = time_string.removeprefix('T')
|
time_string = time_string.removeprefix('T')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return cls(*_parse_isoformat_time(time_string)[0])
|
time_components, _, error_from_components, error_from_tz = (
|
||||||
except Exception:
|
_parse_isoformat_time(time_string)
|
||||||
raise ValueError(f'Invalid isoformat string: {time_string!r}')
|
)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
f'Invalid isoformat string: {time_string!r}') from None
|
||||||
|
else:
|
||||||
|
if error_from_tz:
|
||||||
|
raise error_from_tz
|
||||||
|
if error_from_components:
|
||||||
|
raise ValueError(
|
||||||
|
"Minute, second, and microsecond must be 0 when hour is 24"
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(*time_components)
|
||||||
|
|
||||||
def strftime(self, format):
|
def strftime(self, format):
|
||||||
"""Format using strftime(). The date part of the timestamp passed
|
"""Format using strftime(). The date part of the timestamp passed
|
||||||
|
@ -1947,11 +1968,16 @@ class datetime(date):
|
||||||
|
|
||||||
if tstr:
|
if tstr:
|
||||||
try:
|
try:
|
||||||
time_components, became_next_day, error_from_components = _parse_isoformat_time(tstr)
|
(time_components,
|
||||||
|
became_next_day,
|
||||||
|
error_from_components,
|
||||||
|
error_from_tz) = _parse_isoformat_time(tstr)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Invalid isoformat string: {date_string!r}') from None
|
f'Invalid isoformat string: {date_string!r}') from None
|
||||||
else:
|
else:
|
||||||
|
if error_from_tz:
|
||||||
|
raise error_from_tz
|
||||||
if error_from_components:
|
if error_from_components:
|
||||||
raise ValueError("minute, second, and microsecond must be 0 when hour is 24")
|
raise ValueError("minute, second, and microsecond must be 0 when hour is 24")
|
||||||
|
|
||||||
|
|
|
@ -3571,6 +3571,10 @@ class TestDateTime(TestDate):
|
||||||
'2009-04-19T12:30:45.400 +02:30', # Space between ms and timezone (gh-130959)
|
'2009-04-19T12:30:45.400 +02:30', # Space between ms and timezone (gh-130959)
|
||||||
'2009-04-19T12:30:45.400 ', # Trailing space (gh-130959)
|
'2009-04-19T12:30:45.400 ', # Trailing space (gh-130959)
|
||||||
'2009-04-19T12:30:45. 400', # Space before fraction (gh-130959)
|
'2009-04-19T12:30:45. 400', # Space before fraction (gh-130959)
|
||||||
|
'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
|
||||||
|
'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
|
||||||
]
|
]
|
||||||
|
|
||||||
for bad_str in bad_strs:
|
for bad_str in bad_strs:
|
||||||
|
@ -4795,6 +4799,11 @@ class TestTimeTZ(TestTime, TZInfoBase, unittest.TestCase):
|
||||||
'12:30:45.400 +02:30', # Space between ms and timezone (gh-130959)
|
'12:30:45.400 +02:30', # Space between ms and timezone (gh-130959)
|
||||||
'12:30:45.400 ', # Trailing space (gh-130959)
|
'12:30:45.400 ', # Trailing space (gh-130959)
|
||||||
'12:30:45. 400', # Space before fraction (gh-130959)
|
'12:30:45. 400', # Space before fraction (gh-130959)
|
||||||
|
'24:00:00.000001', # Has non-zero microseconds on 24:00
|
||||||
|
'24:00:01.000000', # Has non-zero seconds on 24:00
|
||||||
|
'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
|
||||||
]
|
]
|
||||||
|
|
||||||
for bad_str in bad_strs:
|
for bad_str in bad_strs:
|
||||||
|
|
|
@ -1288,6 +1288,7 @@ Paul Moore
|
||||||
Ross Moore
|
Ross Moore
|
||||||
Ben Morgan
|
Ben Morgan
|
||||||
Emily Morehouse
|
Emily Morehouse
|
||||||
|
Semyon Moroz
|
||||||
Derek Morr
|
Derek Morr
|
||||||
James A Morrison
|
James A Morrison
|
||||||
Martin Morrison
|
Martin Morrison
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Add check that timezone fields are in range for
|
||||||
|
:meth:`datetime.datetime.fromisoformat` and
|
||||||
|
:meth:`datetime.time.fromisoformat`. Patch by Semyon Moroz.
|
|
@ -1088,6 +1088,7 @@ parse_isoformat_time(const char *dtstr, size_t dtlen, int *hour, int *minute,
|
||||||
// -3: Failed to parse time component
|
// -3: Failed to parse time component
|
||||||
// -4: Failed to parse time separator
|
// -4: Failed to parse time separator
|
||||||
// -5: Malformed timezone string
|
// -5: Malformed timezone string
|
||||||
|
// -6: Timezone fields are not in range
|
||||||
|
|
||||||
const char *p = dtstr;
|
const char *p = dtstr;
|
||||||
const char *p_end = dtstr + dtlen;
|
const char *p_end = dtstr + dtlen;
|
||||||
|
@ -1134,6 +1135,11 @@ parse_isoformat_time(const char *dtstr, size_t dtlen, int *hour, int *minute,
|
||||||
rv = parse_hh_mm_ss_ff(tzinfo_pos, p_end, &tzhour, &tzminute, &tzsecond,
|
rv = parse_hh_mm_ss_ff(tzinfo_pos, p_end, &tzhour, &tzminute, &tzsecond,
|
||||||
tzmicrosecond);
|
tzmicrosecond);
|
||||||
|
|
||||||
|
// Check if timezone fields are in range
|
||||||
|
if (check_time_args(tzhour, tzminute, tzsecond, *tzmicrosecond, 0) < 0) {
|
||||||
|
return -6;
|
||||||
|
}
|
||||||
|
|
||||||
*tzoffset = tzsign * ((tzhour * 3600) + (tzminute * 60) + tzsecond);
|
*tzoffset = tzsign * ((tzhour * 3600) + (tzminute * 60) + tzsecond);
|
||||||
*tzmicrosecond *= tzsign;
|
*tzmicrosecond *= tzsign;
|
||||||
|
|
||||||
|
@ -5039,6 +5045,9 @@ time_fromisoformat(PyObject *cls, PyObject *tstr) {
|
||||||
&tzoffset, &tzimicrosecond);
|
&tzoffset, &tzimicrosecond);
|
||||||
|
|
||||||
if (rv < 0) {
|
if (rv < 0) {
|
||||||
|
if (rv == -6) {
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
goto invalid_string_error;
|
goto invalid_string_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5075,6 +5084,9 @@ invalid_iso_midnight:
|
||||||
invalid_string_error:
|
invalid_string_error:
|
||||||
PyErr_Format(PyExc_ValueError, "Invalid isoformat string: %R", tstr);
|
PyErr_Format(PyExc_ValueError, "Invalid isoformat string: %R", tstr);
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
error:
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -5927,6 +5939,9 @@ datetime_fromisoformat(PyObject *cls, PyObject *dtstr)
|
||||||
len -= (p - dt_ptr);
|
len -= (p - dt_ptr);
|
||||||
rv = parse_isoformat_time(p, len, &hour, &minute, &second,
|
rv = parse_isoformat_time(p, len, &hour, &minute, &second,
|
||||||
µsecond, &tzoffset, &tzusec);
|
µsecond, &tzoffset, &tzusec);
|
||||||
|
if (rv == -6) {
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (rv < 0) {
|
if (rv < 0) {
|
||||||
goto invalid_string_error;
|
goto invalid_string_error;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue