From 33980c792d40803e8d4c68bd92d9fd869fb861fa Mon Sep 17 00:00:00 2001 From: Samuel Merritt Date: Fri, 29 Aug 2014 15:48:38 -0700 Subject: [PATCH] Fix last_modified_date_to_timestamp on non-UTC systems Before, we were calling datetime.datetime.strftime('%s.%f') to convert a datetime to epoch seconds + microseconds. However, the '%s' format isn't actually part of Python's library. Rather, Python passes that on to the system C library, which is typically glibc. Now, glibc takes the '%s' format and helpfully* applies the current timezone as an offset. This gives bogus results on machines where UTC is not the system timezone. (Yes, some people really do that.) For example: >>> import os >>> from swift.common import utils >>> os.environ['TZ'] = 'PST8PDT,M3.2.0,M11.1.0' >>> float(utils.last_modified_date_to_timestamp('1970-01-01T00:00:00.000000')) 28800.0 >>> That timestamp should obviously be 0. This patch replaces the strftime() call with datetime arithmetic, which is entirely in Python so the system timezone doesn't mess it up. * unhelpfully Change-Id: I56855acd79a5d8f2c98a771fa9fd2729e4f490b1 --- swift/common/utils.py | 19 ++++++++++++++----- test/unit/common/test_utils.py | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/swift/common/utils.py b/swift/common/utils.py index a008a344eb..4dacae24fb 100644 --- a/swift/common/utils.py +++ b/swift/common/utils.py @@ -696,16 +696,25 @@ def normalize_timestamp(timestamp): return Timestamp(timestamp).normal +EPOCH = datetime.datetime(1970, 1, 1) + + def last_modified_date_to_timestamp(last_modified_date_str): """ Convert a last modified date (like you'd get from a container listing, e.g. 2014-02-28T23:22:36.698390) to a float. """ - return Timestamp( - datetime.datetime.strptime( - last_modified_date_str, '%Y-%m-%dT%H:%M:%S.%f' - ).strftime('%s.%f') - ) + start = datetime.datetime.strptime(last_modified_date_str, + '%Y-%m-%dT%H:%M:%S.%f') + delta = start - EPOCH + # TODO(sam): after we no longer support py2.6, this expression can + # simplify to Timestamp(delta.total_seconds()). + # + # This calculation is based on Python 2.7's Modules/datetimemodule.c, + # function delta_to_microseconds(), but written in Python. + return Timestamp(delta.days * 86400 + + delta.seconds + + delta.microseconds / 1000000.0) def normalize_delete_at_timestamp(timestamp): diff --git a/test/unit/common/test_utils.py b/test/unit/common/test_utils.py index 1ea3a2dcec..7093249ae8 100644 --- a/test/unit/common/test_utils.py +++ b/test/unit/common/test_utils.py @@ -859,6 +859,24 @@ class TestUtils(unittest.TestCase): real = utils.last_modified_date_to_timestamp(last_modified) self.assertEqual(real, ts, "failed for %s" % last_modified) + def test_last_modified_date_to_timestamp_when_system_not_UTC(self): + try: + old_tz = os.environ.get('TZ') + # Western Argentina Summer Time. Found in glibc manual; this + # timezone always has a non-zero offset from UTC, so this test is + # always meaningful. + os.environ['TZ'] = 'WART4WARST,J1/0,J365/25' + + self.assertEqual(utils.last_modified_date_to_timestamp( + '1970-01-01T00:00:00.000000'), + 0.0) + + finally: + if old_tz is not None: + os.environ['TZ'] = old_tz + else: + os.environ.pop('TZ') + def test_backwards(self): # Test swift.common.utils.backward