From 31ced983b5265532c67336685fe8071bb6e6d9d2 Mon Sep 17 00:00:00 2001 From: Naman-cell Date: Tue, 2 Dec 2025 14:45:48 -0500 Subject: [PATCH] Fix: Improve RFC3339 date parsing validation (Issue #2418) - Replace search() with fullmatch() for strict RFC3339 format validation - Provide clear, actionable error messages with expected format - Add input whitespace stripping before validation - Improve exception handling with specific ValueError messages - Add comprehensive test cases for invalid formats - Addresses reviewer feedback from PR #2420 Test: Existing tests pass + new test cases added --- kubernetes/base/config/dateutil.py | 28 ++++++++++++++----- kubernetes/base/config/dateutil_test.py | 36 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/kubernetes/base/config/dateutil.py b/kubernetes/base/config/dateutil.py index 972e003eba..287719f09f 100644 --- a/kubernetes/base/config/dateutil.py +++ b/kubernetes/base/config/dateutil.py @@ -49,18 +49,27 @@ def dst(self, dt): def parse_rfc3339(s): if isinstance(s, datetime.datetime): - # no need to parse it, just make sure it has a timezone. if not s.tzinfo: return s.replace(tzinfo=UTC) return s - groups = _re_rfc3339.search(s).groups() + + m = _re_rfc3339.fullmatch(s.strip()) + if m is None: + raise ValueError( + f"Invalid RFC3339 datetime: {s!r} " + "(expected YYYY-MM-DDTHH:MM:SS[.frac][Z|±HH:MM])" + ) + + groups = m.groups() dt = [0] * 7 for x in range(6): dt[x] = int(groups[x]) + us = 0 if groups[6] is not None: partial_sec = float(groups[6].replace(",", ".")) us = int(MICROSEC_PER_SEC * partial_sec) + tz = UTC if groups[7] is not None and groups[7] != 'Z' and groups[7] != 'z': tz_groups = _re_timezone.search(groups[7]).groups() @@ -71,10 +80,17 @@ def parse_rfc3339(s): if tz_groups[2]: minute = int(tz_groups[2]) tz = TimezoneInfo(hour, minute) - return datetime.datetime( - year=dt[0], month=dt[1], day=dt[2], - hour=dt[3], minute=dt[4], second=dt[5], - microsecond=us, tzinfo=tz) + + try: + return datetime.datetime( + year=dt[0], month=dt[1], day=dt[2], + hour=dt[3], minute=dt[4], second=dt[5], + microsecond=us, tzinfo=tz) + except ValueError as e: + raise ValueError( + f"Invalid date/time values in RFC3339 string {s!r}: {e}" + ) from e + def format_rfc3339(date_time): diff --git a/kubernetes/base/config/dateutil_test.py b/kubernetes/base/config/dateutil_test.py index 933360d9fb..25a57b98e7 100644 --- a/kubernetes/base/config/dateutil_test.py +++ b/kubernetes/base/config/dateutil_test.py @@ -66,3 +66,39 @@ def test_format_rfc3339(self): format_rfc3339(datetime(2017, 7, 25, 4, 44, 21, 0, TimezoneInfo(-2, 30))), "2017-07-25T07:14:21Z") + + def test_parse_rfc3339_invalid_formats(self): + """Test that invalid RFC3339 formats raise ValueError""" + invalid_inputs = [ + "2025-13-02T13:37:00Z", # Invalid month + "2025-12-32T13:37:00Z", # Invalid day + "2025-12-02T25:00:00Z", # Invalid hour + "2025-12-02T13:60:00Z", # Invalid minute + "2025-12-02T13:37:60Z", # Invalid second + "not-a-valid-date", # Completely invalid + "", # Empty string + "2025-12-02Z13:37:00", # Timezone before time + ] + + for invalid_input in invalid_inputs: + with self.assertRaises(ValueError): + parse_rfc3339(invalid_input) + + + + def test_parse_rfc3339_with_whitespace(self): + """Test that leading/trailing whitespace is handled""" + actual = parse_rfc3339(" 2017-07-25T04:44:21Z ") + expected = datetime(2017, 7, 25, 4, 44, 21, 0, UTC) + self.assertEqual(expected, actual) + + def test_parse_rfc3339_error_message_clarity(self): + """Test that error messages are clear and helpful""" + try: + parse_rfc3339("invalid-date-format") + except ValueError as e: + error_msg = str(e) + # Verify error message contains helpful information + self.assertIn("Invalid RFC3339", error_msg) + self.assertIn("YYYY-MM-DD", error_msg) + self.assertIn("expected", error_msg)