]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-126883: Add check that timezone fields are in range for `datetime.fromisoformat...
authorSemyon Moroz <donbarbos@proton.me>
Mon, 19 May 2025 18:07:11 +0000 (18:07 +0000)
committerGitHub <noreply@github.com>
Mon, 19 May 2025 18:07:11 +0000 (14:07 -0400)
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>
Lib/_pydatetime.py
Lib/test/datetimetester.py
Misc/ACKS
Misc/NEWS.d/next/Library/2024-11-25-10-22-08.gh-issue-126883.MAEF7g.rst [new file with mode: 0644]
Modules/_datetimemodule.c

index 471e89c16a1c0eaf77dba7bafbf803bcc9e7a74a..71f619024e570d321c4db0c8053cb5b69866711d 100644 (file)
@@ -467,6 +467,7 @@ def _parse_isoformat_time(tstr):
     hour, minute, second, microsecond = time_comps
     became_next_day = False
     error_from_components = False
+    error_from_tz = None
     if (hour == 24):
         if all(time_comp == 0 for time_comp in time_comps[1:]):
             hour = 0
@@ -500,14 +501,22 @@ def _parse_isoformat_time(tstr):
         else:
             tzsign = -1 if tstr[tz_pos - 1] == '-' else 1
 
-            td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],
-                           seconds=tz_comps[2], microseconds=tz_comps[3])
-
-            tzi = timezone(tzsign * td)
+            try:
+                # This function is intended to validate datetimes, but because
+                # we restrict time zones to ±24h, it serves here as well.
+                _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)
 
-    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
 def _isoweek_to_gregorian(year, week, day):
@@ -1633,9 +1642,21 @@ class time:
         time_string = time_string.removeprefix('T')
 
         try:
-            return cls(*_parse_isoformat_time(time_string)[0])
-        except Exception:
-            raise ValueError(f'Invalid isoformat string: {time_string!r}')
+            time_components, _, error_from_components, error_from_tz = (
+                _parse_isoformat_time(time_string)
+            )
+        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):
         """Format using strftime().  The date part of the timestamp passed
@@ -1947,11 +1968,16 @@ class datetime(date):
 
         if tstr:
             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:
                 raise ValueError(
                     f'Invalid isoformat string: {date_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")
 
index d1882a310bbbb0c972a1b213cc29dc57fe091369..345698cfb5f1a41e97b7c6359e0b0d37d5f1ed08 100644 (file)
@@ -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 ',        # Trailing space (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:
@@ -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 ',            # Trailing space (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:
index 610dcf9f4238dee3845d1293453708cfb376d3f2..5653c52c9e354e872ba6d6f8223bd22278b89567 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1288,6 +1288,7 @@ Paul Moore
 Ross Moore
 Ben Morgan
 Emily Morehouse
+Semyon Moroz
 Derek Morr
 James A Morrison
 Martin Morrison
diff --git a/Misc/NEWS.d/next/Library/2024-11-25-10-22-08.gh-issue-126883.MAEF7g.rst b/Misc/NEWS.d/next/Library/2024-11-25-10-22-08.gh-issue-126883.MAEF7g.rst
new file mode 100644 (file)
index 0000000..5e3fa39
--- /dev/null
@@ -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.
index 9bba0e3354b26ba7c9144ff7b6b064d03866fdeb..313a72e3fe06680e52a6a9ee7c43fed18621782d 100644 (file)
@@ -1088,6 +1088,7 @@ parse_isoformat_time(const char *dtstr, size_t dtlen, int *hour, int *minute,
     //     -3:  Failed to parse time component
     //     -4:  Failed to parse time separator
     //     -5:  Malformed timezone string
+    //     -6:  Timezone fields are not in range
 
     const char *p = dtstr;
     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,
                            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);
     *tzmicrosecond *= tzsign;
 
@@ -5039,6 +5045,9 @@ time_fromisoformat(PyObject *cls, PyObject *tstr) {
                                   &tzoffset, &tzimicrosecond);
 
     if (rv < 0) {
+        if (rv == -6) {
+            goto error;
+        }
         goto invalid_string_error;
     }
 
@@ -5075,6 +5084,9 @@ invalid_iso_midnight:
 invalid_string_error:
     PyErr_Format(PyExc_ValueError, "Invalid isoformat string: %R", tstr);
     return NULL;
+
+error:
+    return NULL;
 }
 
 
@@ -5927,6 +5939,9 @@ datetime_fromisoformat(PyObject *cls, PyObject *dtstr)
         len -= (p - dt_ptr);
         rv = parse_isoformat_time(p, len, &hour, &minute, &second,
                                   &microsecond, &tzoffset, &tzusec);
+        if (rv == -6) {
+            goto error;
+        }
     }
     if (rv < 0) {
         goto invalid_string_error;