From 7b27e141f5794eaf998343eb8b3fdb47c11299fa Mon Sep 17 00:00:00 2001 From: rafsaf Date: Wed, 30 Oct 2024 12:12:48 +0100 Subject: [PATCH 1/3] fix datetime leap day parsing warnings and add some tests Signed-off-by: rafsaf --- minio/time.py | 15 ++++- tests/unit/time_test.py | 145 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 tests/unit/time_test.py diff --git a/minio/time.py b/minio/time.py index 39a57c6d7..9b2554a0b 100644 --- a/minio/time.py +++ b/minio/time.py @@ -74,7 +74,20 @@ def from_http_header(value: str) -> datetime: f"time data {value} does not match HTTP header format") weekday = _WEEK_DAYS.index(value[0:3]) - day = datetime.strptime(value[4:8], " %d ").day + if value[4] != " " or value[7] != " ": + raise ValueError( + f"time data {value} does not match HTTP header format" + ) + if not value[5:7].isnumeric(): + raise ValueError( + f"time data {value} does not match HTTP header format" + ) + day = int(value[5:7]) + + if day < 1 or day > 31: + raise ValueError( + f"time data {value} does not match HTTP header format" + ) if value[8:11] not in _MONTHS: raise ValueError( diff --git a/tests/unit/time_test.py b/tests/unit/time_test.py new file mode 100644 index 000000000..1f5fc4655 --- /dev/null +++ b/tests/unit/time_test.py @@ -0,0 +1,145 @@ +import locale +import threading +import unittest +from contextlib import contextmanager +from datetime import datetime, timezone +from typing import Any, Generator + +from minio.time import from_http_header, to_http_header + +LOCALE_LOCK = threading.Lock() +LAST_MODIFIED_STR = "Mon, 02 Mar 2015 07:28:00 GMT" +LAST_MODIFIED_DATE = datetime( + year=2015, + month=3, + day=2, + hour=7, + minute=28, + second=0, + tzinfo=timezone.utc, +) + + +@contextmanager +def setlocale(name) -> Generator[str, Any, None]: + with LOCALE_LOCK: + saved = locale.setlocale(locale.LC_ALL) + try: + yield locale.setlocale(locale.LC_ALL, name) + finally: + locale.setlocale(locale.LC_ALL, saved) + + +class TimeutilsTest(unittest.TestCase): + def test_from_http_header_valid_headers(self) -> None: + for case in [ + ( + "Wed, 30 Oct 2024 09:35:00 GMT", + datetime( + year=2024, + month=10, + day=30, + hour=9, + minute=35, + second=0, + tzinfo=timezone.utc, + ), + ), + ( + "Tue, 29 Oct 2024 00:35:00 GMT", + datetime( + year=2024, + month=10, + day=29, + hour=0, + minute=35, + second=0, + tzinfo=timezone.utc, + ), + ), + ( + "Tue, 01 Oct 2024 22:35:22 GMT", + datetime( + year=2024, + month=10, + day=1, + hour=22, + minute=35, + second=22, + tzinfo=timezone.utc, + ), + ), + ( + "Mon, 30 Sep 2024 22:35:55 GMT", + datetime( + year=2024, + month=9, + day=30, + hour=22, + minute=35, + second=55, + tzinfo=timezone.utc, + ), + ), + ]: + with self.subTest(case=case): + self.assertEqual(from_http_header(case[0]), case[1]) + + def test_from_http_header_invalid_headers(self) -> None: + for case in [ + ("Wed, 30 Oct 2024 09:35:00 GMT ", "invalid length"), + ("Wet, 30 Oct 2024 09:35:00 GMT", "invalid weekday"), + ("Wed 30 Oct 2024 09:35:00 GMT", "no comma after weekday"), + ("Wed,30 Sep 2024 09:35:00 GMT ", "no space after weekday"), + ("Wed, 30Sep 2024 09:35:00 GMT ", "no space before month"), + ("Wed, 00 Sep 2024 09:35:00 GMT", "invalid day"), + ("Wed, 32 Sep 2024 09:35:00 GMT", "invalid day 2"), + ("Wed, ab Sep 2024 09:35:00 GMT", "invalid day 3"), + ("Wed, 30 Set 2024 09:35:00 GMT", "invalid month"), + ("Tue, 30 Set 2024 09:35:00 GMT", "name of day doesn't match"), + ]: + with self.subTest(case=case): + with self.assertRaises( + ValueError, + ) as exc: + from_http_header(case[0]) + self.assertEqual( + str(exc.exception), + f"time data {case[0]} does not match HTTP header format", + ) + + def test_from_http_header_default_locale(self) -> None: + result_datetime = from_http_header(LAST_MODIFIED_STR) + + self.assertEqual( + result_datetime, + LAST_MODIFIED_DATE, + ) + + def test_from_http_header_polish_locale(self) -> None: + try: + with setlocale("pl_PL.utf8"): + result_datetime = from_http_header(LAST_MODIFIED_STR) + + self.assertEqual( + result_datetime, + LAST_MODIFIED_DATE, + ) + except locale.Error: + self.skipTest("pl_PL.utf8 locale is not supported on this machine") + + def test_to_http_header_default_locale(self) -> None: + self.assertEqual( + to_http_header(LAST_MODIFIED_DATE), + LAST_MODIFIED_STR, + ) + + def test_to_http_header_polish_locale(self) -> None: + try: + with setlocale("pl_PL.utf8"): + self.assertEqual( + to_http_header(LAST_MODIFIED_DATE), + LAST_MODIFIED_STR, + ) + except locale.Error: + self.skipTest("pl_PL.utf8 locale is not supported on this machine") From bf3aa48a2dd1edd2883edd1e9be24d39d0bd6f66 Mon Sep 17 00:00:00 2001 From: Bala FA Date: Wed, 6 Nov 2024 14:26:44 +0530 Subject: [PATCH 2/3] Apply suggestions from code review --- minio/time.py | 9 --------- tests/unit/time_test.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/minio/time.py b/minio/time.py index 9b2554a0b..78c4c2856 100644 --- a/minio/time.py +++ b/minio/time.py @@ -78,17 +78,8 @@ def from_http_header(value: str) -> datetime: raise ValueError( f"time data {value} does not match HTTP header format" ) - if not value[5:7].isnumeric(): - raise ValueError( - f"time data {value} does not match HTTP header format" - ) day = int(value[5:7]) - if day < 1 or day > 31: - raise ValueError( - f"time data {value} does not match HTTP header format" - ) - if value[8:11] not in _MONTHS: raise ValueError( f"time data {value} does not match HTTP header format") diff --git a/tests/unit/time_test.py b/tests/unit/time_test.py index 1f5fc4655..b15708aec 100644 --- a/tests/unit/time_test.py +++ b/tests/unit/time_test.py @@ -1,3 +1,19 @@ +# -*- coding: utf-8 -*- +# MinIO Python Library for Amazon S3 Compatible Cloud Storage, +# (C) 2024 MinIO, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import locale import threading import unittest From 5bfa5cc5cc2ce98583723425148eb36e0c616118 Mon Sep 17 00:00:00 2001 From: Bala FA Date: Wed, 6 Nov 2024 14:36:50 +0530 Subject: [PATCH 3/3] Update tests/unit/time_test.py --- tests/unit/time_test.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/unit/time_test.py b/tests/unit/time_test.py index b15708aec..d392087ba 100644 --- a/tests/unit/time_test.py +++ b/tests/unit/time_test.py @@ -115,14 +115,7 @@ def test_from_http_header_invalid_headers(self) -> None: ("Tue, 30 Set 2024 09:35:00 GMT", "name of day doesn't match"), ]: with self.subTest(case=case): - with self.assertRaises( - ValueError, - ) as exc: - from_http_header(case[0]) - self.assertEqual( - str(exc.exception), - f"time data {case[0]} does not match HTTP header format", - ) + self.assertRaises(ValueError, from_http_header, case[0]) def test_from_http_header_default_locale(self) -> None: result_datetime = from_http_header(LAST_MODIFIED_STR)