From eb1c21027840381c82b856ee099a620db38b6681 Mon Sep 17 00:00:00 2001 From: Jie Luo Date: Wed, 6 Dec 2023 11:36:15 -0800 Subject: [PATCH] BREAKING CHANGE in v26: check if Timestamp is valid. Seconds should be in range [-62135596800, 253402300799] Nanos should be in range [0, 999999999] PiperOrigin-RevId: 588493865 --- .../protobuf/internal/json_format_test.py | 9 ++- .../protobuf/internal/well_known_types.py | 56 +++++++++++++++---- .../internal/well_known_types_test.py | 24 +++----- 3 files changed, 59 insertions(+), 30 deletions(-) diff --git a/python/google/protobuf/internal/json_format_test.py b/python/google/protobuf/internal/json_format_test.py index 15b376c181d93..6c4d2dac0b9ca 100644 --- a/python/google/protobuf/internal/json_format_test.py +++ b/python/google/protobuf/internal/json_format_test.py @@ -1060,7 +1060,14 @@ def testInvalidTimestamp(self): json_format.Parse, text, message) # Time bigger than maximum time. message.value.seconds = 253402300800 - self.assertRaisesRegex(OverflowError, 'date value out of range', + self.assertRaisesRegex(json_format.SerializeToJsonError, + 'Timestamp is not valid', + json_format.MessageToJson, message) + # Nanos smaller than 0 + message.value.seconds = 0 + message.value.nanos = -1 + self.assertRaisesRegex(json_format.SerializeToJsonError, + 'Timestamp is not valid', json_format.MessageToJson, message) # Lower case t does not accept. text = '{"value": "0001-01-01t00:00:00Z"}' diff --git a/python/google/protobuf/internal/well_known_types.py b/python/google/protobuf/internal/well_known_types.py index 5727bc98c2c06..835e443eff764 100644 --- a/python/google/protobuf/internal/well_known_types.py +++ b/python/google/protobuf/internal/well_known_types.py @@ -33,6 +33,8 @@ _MICROS_PER_SECOND = 1000000 _SECONDS_PER_DAY = 24 * 3600 _DURATION_SECONDS_MAX = 315576000000 +_TIMESTAMP_SECONDS_MIN = -62135596800 +_TIMESTAMP_SECONDS_MAX = 253402300799 _EPOCH_DATETIME_NAIVE = datetime.datetime(1970, 1, 1, tzinfo=None) _EPOCH_DATETIME_AWARE = _EPOCH_DATETIME_NAIVE.replace( @@ -85,10 +87,10 @@ def ToJsonString(self): and uses 3, 6 or 9 fractional digits as required to represent the exact time. Example of the return format: '1972-01-01T10:00:20.021Z' """ - nanos = self.nanos % _NANOS_PER_SECOND - total_sec = self.seconds + (self.nanos - nanos) // _NANOS_PER_SECOND - seconds = total_sec % _SECONDS_PER_DAY - days = (total_sec - seconds) // _SECONDS_PER_DAY + _CheckTimestampValid(self.seconds, self.nanos) + nanos = self.nanos + seconds = self.seconds % _SECONDS_PER_DAY + days = (self.seconds - seconds) // _SECONDS_PER_DAY dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(days, seconds) result = dt.isoformat() @@ -166,6 +168,7 @@ def FromJsonString(self, value): else: seconds += (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 # Set seconds and nanos + _CheckTimestampValid(seconds, nanos) self.seconds = int(seconds) self.nanos = int(nanos) @@ -175,39 +178,53 @@ def GetCurrentTime(self): def ToNanoseconds(self): """Converts Timestamp to nanoseconds since epoch.""" + _CheckTimestampValid(self.seconds, self.nanos) return self.seconds * _NANOS_PER_SECOND + self.nanos def ToMicroseconds(self): """Converts Timestamp to microseconds since epoch.""" + _CheckTimestampValid(self.seconds, self.nanos) return (self.seconds * _MICROS_PER_SECOND + self.nanos // _NANOS_PER_MICROSECOND) def ToMilliseconds(self): """Converts Timestamp to milliseconds since epoch.""" + _CheckTimestampValid(self.seconds, self.nanos) return (self.seconds * _MILLIS_PER_SECOND + self.nanos // _NANOS_PER_MILLISECOND) def ToSeconds(self): """Converts Timestamp to seconds since epoch.""" + _CheckTimestampValid(self.seconds, self.nanos) return self.seconds def FromNanoseconds(self, nanos): """Converts nanoseconds since epoch to Timestamp.""" - self.seconds = nanos // _NANOS_PER_SECOND - self.nanos = nanos % _NANOS_PER_SECOND + seconds = nanos // _NANOS_PER_SECOND + nanos = nanos % _NANOS_PER_SECOND + _CheckTimestampValid(seconds, nanos) + self.seconds = seconds + self.nanos = nanos def FromMicroseconds(self, micros): """Converts microseconds since epoch to Timestamp.""" - self.seconds = micros // _MICROS_PER_SECOND - self.nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND + seconds = micros // _MICROS_PER_SECOND + nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND + _CheckTimestampValid(seconds, nanos) + self.seconds = seconds + self.nanos = nanos def FromMilliseconds(self, millis): """Converts milliseconds since epoch to Timestamp.""" - self.seconds = millis // _MILLIS_PER_SECOND - self.nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND + seconds = millis // _MILLIS_PER_SECOND + nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND + _CheckTimestampValid(seconds, nanos) + self.seconds = seconds + self.nanos = nanos def FromSeconds(self, seconds): """Converts seconds since epoch to Timestamp.""" + _CheckTimestampValid(seconds, 0) self.seconds = seconds self.nanos = 0 @@ -229,6 +246,7 @@ def ToDatetime(self, tzinfo=None): # https://github.com/python/cpython/issues/109849) or full range (on some # platforms, see https://github.com/python/cpython/issues/110042) of # datetime. + _CheckTimestampValid(self.seconds, self.nanos) delta = datetime.timedelta( seconds=self.seconds, microseconds=_RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND), @@ -252,8 +270,22 @@ def FromDatetime(self, dt): # manipulated into a long value of seconds. During the conversion from # struct_time to long, the source date in UTC, and so it follows that the # correct transformation is calendar.timegm() - self.seconds = calendar.timegm(dt.utctimetuple()) - self.nanos = dt.microsecond * _NANOS_PER_MICROSECOND + seconds = calendar.timegm(dt.utctimetuple()) + nanos = dt.microsecond * _NANOS_PER_MICROSECOND + _CheckTimestampValid(seconds, nanos) + self.seconds = seconds + self.nanos = nanos + + +def _CheckTimestampValid(seconds, nanos): + if seconds < _TIMESTAMP_SECONDS_MIN or seconds > _TIMESTAMP_SECONDS_MAX: + raise ValueError( + 'Timestamp is not valid: Seconds {0} must be in range ' + '[-62135596800, 253402300799].'.format(seconds)) + if nanos < 0 or nanos >= _NANOS_PER_SECOND: + raise ValueError( + 'Timestamp is not valid: Nanos {} must be in a range ' + '[0, 999999].'.format(nanos)) class Duration(object): diff --git a/python/google/protobuf/internal/well_known_types_test.py b/python/google/protobuf/internal/well_known_types_test.py index 37d20491735f1..da273166eb970 100644 --- a/python/google/protobuf/internal/well_known_types_test.py +++ b/python/google/protobuf/internal/well_known_types_test.py @@ -352,27 +352,15 @@ def testTimezoneAwareMinDatetimeConversion(self): ) def testNanosOneSecond(self): - # TODO: b/301980950 - Test error behavior instead once ToDatetime validates - # that nanos are in expected range. tz = _TZ_PACIFIC ts = timestamp_pb2.Timestamp(nanos=1_000_000_000) - self.assertEqual(ts.ToDatetime(), datetime.datetime(1970, 1, 1, 0, 0, 1)) - self.assertEqual( - ts.ToDatetime(tz), datetime.datetime(1969, 12, 31, 16, 0, 1, tzinfo=tz) - ) + self.assertRaisesRegex(ValueError, 'Timestamp is not valid', + ts.ToDatetime) def testNanosNegativeOneSecond(self): - # TODO: b/301980950 - Test error behavior instead once ToDatetime validates - # that nanos are in expected range. - tz = _TZ_PACIFIC ts = timestamp_pb2.Timestamp(nanos=-1_000_000_000) - self.assertEqual( - ts.ToDatetime(), datetime.datetime(1969, 12, 31, 23, 59, 59) - ) - self.assertEqual( - ts.ToDatetime(tz), - datetime.datetime(1969, 12, 31, 15, 59, 59, tzinfo=tz), - ) + self.assertRaisesRegex(ValueError, 'Timestamp is not valid', + ts.ToDatetime) def testTimedeltaConversion(self): message = duration_pb2.Duration() @@ -421,8 +409,10 @@ def testInvalidTimestamp(self): self.assertRaisesRegex(ValueError, 'year (0 )?is out of range', message.FromJsonString, '0000-01-01T00:00:00Z') message.seconds = 253402300800 - self.assertRaisesRegex(OverflowError, 'date value out of range', + self.assertRaisesRegex(ValueError, 'Timestamp is not valid', message.ToJsonString) + self.assertRaisesRegex(ValueError, 'Timestamp is not valid', + message.FromSeconds, -62135596801) def testInvalidDuration(self): message = duration_pb2.Duration()