From a052a932fe7861c40867c66fdad824f6ba3626c0 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Mon, 25 Nov 2024 06:21:23 +0400 Subject: [PATCH 1/8] Add check that timezone fields are in range --- Lib/_pydatetime.py | 43 ++++++++++++++++++++++++++++++++------- Modules/_datetimemodule.c | 15 ++++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index ed01670cfece43..9b68823fb042ec 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -463,9 +463,10 @@ def _parse_isoformat_time(tstr): time_comps = _parse_hh_mm_ss_ff(timestr) - hour, minute, second, microsecond = time_comps + hour = time_comps[0] 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 @@ -499,6 +500,13 @@ def _parse_isoformat_time(tstr): else: tzsign = -1 if tstr[tz_pos - 1] == '-' else 1 + try: + _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 + td = timedelta(hours=tz_comps[0], minutes=tz_comps[1], seconds=tz_comps[2], microseconds=tz_comps[3]) @@ -506,7 +514,7 @@ def _parse_isoformat_time(tstr): 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): @@ -1625,9 +1633,21 @@ def fromisoformat(cls, time_string): 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 @@ -1939,13 +1959,22 @@ def fromisoformat(cls, date_string): 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") + raise ValueError( + "minute, second, and microsecond must be 0 when hour is 24" + ) if became_next_day: year, month, day = date_components diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index b1102984cb5e9e..5a730a8e0d36f6 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1070,6 +1070,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; @@ -1116,6 +1117,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; @@ -5001,6 +5007,9 @@ time_fromisoformat(PyObject *cls, PyObject *tstr) { &tzoffset, &tzimicrosecond); if (rv < 0) { + if (rv == -6) { + goto error; + } goto invalid_string_error; } @@ -5037,6 +5046,9 @@ time_fromisoformat(PyObject *cls, PyObject *tstr) { invalid_string_error: PyErr_Format(PyExc_ValueError, "Invalid isoformat string: %R", tstr); return NULL; + +error: + return NULL; } @@ -5880,6 +5892,9 @@ datetime_fromisoformat(PyObject *cls, PyObject *dtstr) len -= (p - dt_ptr); rv = parse_isoformat_time(p, len, &hour, &minute, &second, µsecond, &tzoffset, &tzusec); + if (rv == -6) { + goto error; + } } if (rv < 0) { goto invalid_string_error; From 5e2ddf8b0f57e4cab7d356b23039796b8d70a6fc Mon Sep 17 00:00:00 2001 From: donBarbos Date: Mon, 25 Nov 2024 06:22:21 +0400 Subject: [PATCH 2/8] Add use cases in Lib/test/datetimetester.py --- Lib/test/datetimetester.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 25a3015c4e19ce..701a0d5584ccbc 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3492,12 +3492,14 @@ def test_fromisoformat_fails_datetime(self): '2009-04-19T12:30:45.123456-05:00a', # Extra text '2009-04-19T12:30:45.123-05:00a', # Extra text '2009-04-19T12:30:45-05:00a', # Extra text - '2009-04-19T24:00:00.000001', # Has non-zero microseconds on 24:00 - '2009-04-19T24:00:01.000000', # Has non-zero seconds on 24:00 - '2009-04-19T24:01:00.000000', # Has non-zero minutes on 24:00 - '2009-04-32T24:00:00.000000', # Day is invalid before wrapping due to 24:00 - '2009-13-01T24:00:00.000000', # Month is invalid before wrapping due to 24:00 - '9999-12-31T24:00:00.000000', # Year is invalid after wrapping due to 24:00 + '2009-04-19T24:00:00.000001', # Has non-zero microseconds on 24:00 + '2009-04-19T24:00:01.000000', # Has non-zero seconds on 24:00 + '2009-04-19T24:01:00.000000', # Has non-zero minutes on 24:00 + '2009-04-32T24:00:00.000000', # Day is invalid before wrapping due to 24:00 + '2009-13-01T24:00:00.000000', # Month is invalid before wrapping due to 24:00 + '9999-12-31T24:00:00.000000', # Year is invalid after wrapping due to 24:00 + '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: @@ -4656,6 +4658,11 @@ def test_fromisoformat_fails(self): '12:30:45.123456+12:00:30a', # Extra at end of full time '12.5', # Decimal mark at end of hour '12:30,5', # Decimal mark at end of minute + '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: From 72d4701213ce57f6b90f202e03c8a771b2149312 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Mon, 25 Nov 2024 10:28:26 +0400 Subject: [PATCH 3/8] Add self to acknowledgements and news blurb --- Misc/ACKS | 1 + .../Library/2024-11-25-10-22-08.gh-issue-126883.MAEF7g.rst | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-11-25-10-22-08.gh-issue-126883.MAEF7g.rst diff --git a/Misc/ACKS b/Misc/ACKS index 08cd293eac3835..5584bf33fe10e0 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1274,6 +1274,7 @@ Paul Moore Ross Moore Ben Morgan Emily Morehouse +Semyon "donBarbos" 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 index 00000000000000..d399af7ffb99c8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-11-25-10-22-08.gh-issue-126883.MAEF7g.rst @@ -0,0 +1,4 @@ +Add check that timezone fields are in range for +:meth:`datetime.datetime.fromisoformat` and +:meth:`datetime.time.fromisoformat`. Patch by Semyon "donBarbos" Moroz +(donbarbos@proton.me) From 763ec67507b1d7e435fa484a484c5c0120fb3bcf Mon Sep 17 00:00:00 2001 From: donBarbos Date: Mon, 25 Nov 2024 18:29:34 +0400 Subject: [PATCH 4/8] Update for correct definition of exception --- Lib/_pydatetime.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 9b68823fb042ec..00a83447b33816 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -506,11 +506,10 @@ def _parse_isoformat_time(tstr): fold=0) except ValueError as e: error_from_tz = e - - td = timedelta(hours=tz_comps[0], minutes=tz_comps[1], - seconds=tz_comps[2], microseconds=tz_comps[3]) - - tzi = timezone(tzsign * td) + 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) From cc0d774f3e3989ec20b2c9eb0880e60a99b8d643 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Wed, 5 Feb 2025 08:38:01 +0000 Subject: [PATCH 5/8] Update Lib/_pydatetime.py Co-authored-by: Erlend E. Aasland --- Lib/_pydatetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 00a83447b33816..cd055df20162d7 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -463,7 +463,7 @@ def _parse_isoformat_time(tstr): time_comps = _parse_hh_mm_ss_ff(timestr) - hour = time_comps[0] + hour = time_comps[0] became_next_day = False error_from_components = False error_from_tz = None From 9adfc6e890918f87b9611fb12f6af2fc61587f7e Mon Sep 17 00:00:00 2001 From: donBarbos Date: Wed, 5 Feb 2025 12:45:09 +0400 Subject: [PATCH 6/8] Revert style changes --- Lib/_pydatetime.py | 4 +--- Lib/test/datetimetester.py | 12 ++++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index cd055df20162d7..2fb8cdece04a9e 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -1971,9 +1971,7 @@ def fromisoformat(cls, date_string): 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" - ) + raise ValueError("minute, second, and microsecond must be 0 when hour is 24") if became_next_day: year, month, day = date_components diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 701a0d5584ccbc..1837d8aebaf688 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3492,12 +3492,12 @@ def test_fromisoformat_fails_datetime(self): '2009-04-19T12:30:45.123456-05:00a', # Extra text '2009-04-19T12:30:45.123-05:00a', # Extra text '2009-04-19T12:30:45-05:00a', # Extra text - '2009-04-19T24:00:00.000001', # Has non-zero microseconds on 24:00 - '2009-04-19T24:00:01.000000', # Has non-zero seconds on 24:00 - '2009-04-19T24:01:00.000000', # Has non-zero minutes on 24:00 - '2009-04-32T24:00:00.000000', # Day is invalid before wrapping due to 24:00 - '2009-13-01T24:00:00.000000', # Month is invalid before wrapping due to 24:00 - '9999-12-31T24:00:00.000000', # Year is invalid after wrapping due to 24:00 + '2009-04-19T24:00:00.000001', # Has non-zero microseconds on 24:00 + '2009-04-19T24:00:01.000000', # Has non-zero seconds on 24:00 + '2009-04-19T24:01:00.000000', # Has non-zero minutes on 24:00 + '2009-04-32T24:00:00.000000', # Day is invalid before wrapping due to 24:00 + '2009-13-01T24:00:00.000000', # Month is invalid before wrapping due to 24:00 + '9999-12-31T24:00:00.000000', # Year is invalid after wrapping due to 24:00 '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 ] From 8079a72541842c19f3150263e3fffc0a33bb4ba3 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Tue, 25 Feb 2025 02:13:50 +0400 Subject: [PATCH 7/8] Make diff a little bit less --- Lib/_pydatetime.py | 12 +++++------- Misc/ACKS | 2 +- .../2024-11-25-10-22-08.gh-issue-126883.MAEF7g.rst | 3 +-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index c7ab0f51467c65..6283791ab1f07f 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -463,7 +463,7 @@ def _parse_isoformat_time(tstr): time_comps = _parse_hh_mm_ss_ff(timestr) - hour = time_comps[0] + hour, minute, second, microsecond = time_comps became_next_day = False error_from_components = False error_from_tz = None @@ -1961,12 +1961,10 @@ def fromisoformat(cls, date_string): if tstr: try: - ( - time_components, - became_next_day, - error_from_components, - error_from_tz, - ) = _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 diff --git a/Misc/ACKS b/Misc/ACKS index 412d79b2766b4e..84e1bb35fce49f 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1281,7 +1281,7 @@ Paul Moore Ross Moore Ben Morgan Emily Morehouse -Semyon "donBarbos" Moroz +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 index d399af7ffb99c8..5e3fa39acf1979 100644 --- 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 @@ -1,4 +1,3 @@ Add check that timezone fields are in range for :meth:`datetime.datetime.fromisoformat` and -:meth:`datetime.time.fromisoformat`. Patch by Semyon "donBarbos" Moroz -(donbarbos@proton.me) +:meth:`datetime.time.fromisoformat`. Patch by Semyon Moroz. From 5580d17ed083da8b570f24f885776a88d4618a7b Mon Sep 17 00:00:00 2001 From: Semyon Moroz Date: Mon, 19 May 2025 15:39:59 +0000 Subject: [PATCH 8/8] Apply suggestions from code review Co-authored-by: Paul Ganssle <1377457+pganssle@users.noreply.github.com> --- Lib/_pydatetime.py | 4 +++- Lib/test/datetimetester.py | 2 ++ Modules/_datetimemodule.c | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index e84f3091c688aa..4465b69a2e580f 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -502,6 +502,8 @@ def _parse_isoformat_time(tstr): tzsign = -1 if tstr[tz_pos - 1] == '-' else 1 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) @@ -1647,7 +1649,7 @@ def fromisoformat(cls, time_string): raise error_from_tz if error_from_components: raise ValueError( - "minute, second, and microsecond must be 0 when hour is 24" + "Minute, second, and microsecond must be 0 when hour is 24" ) return cls(*time_components) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index a7a28202a6c881..56cd940cde241d 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3569,6 +3569,8 @@ def test_fromisoformat_fails_datetime(self): '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: diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 03211b70fa5e1d..313a72e3fe0668 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1135,7 +1135,7 @@ 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 + // Check if timezone fields are in range if (check_time_args(tzhour, tzminute, tzsecond, *tzmicrosecond, 0) < 0) { return -6; }