Skip to content

Commit 4f77929

Browse files
anandoleecopybara-github
authored andcommitted
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: 594119545
1 parent 75455ea commit 4f77929

File tree

3 files changed

+60
-30
lines changed

3 files changed

+60
-30
lines changed

python/google/protobuf/internal/json_format_test.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1060,7 +1060,14 @@ def testInvalidTimestamp(self):
10601060
json_format.Parse, text, message)
10611061
# Time bigger than maximum time.
10621062
message.value.seconds = 253402300800
1063-
self.assertRaisesRegex(OverflowError, 'date value out of range',
1063+
self.assertRaisesRegex(json_format.SerializeToJsonError,
1064+
'Timestamp is not valid',
1065+
json_format.MessageToJson, message)
1066+
# Nanos smaller than 0
1067+
message.value.seconds = 0
1068+
message.value.nanos = -1
1069+
self.assertRaisesRegex(json_format.SerializeToJsonError,
1070+
'Timestamp is not valid',
10641071
json_format.MessageToJson, message)
10651072
# Lower case t does not accept.
10661073
text = '{"value": "0001-01-01t00:00:00Z"}'

python/google/protobuf/internal/well_known_types.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import calendar
2121
import collections.abc
2222
import datetime
23+
import warnings
2324

2425
from google.protobuf.internal import field_mask
2526

@@ -33,6 +34,8 @@
3334
_MICROS_PER_SECOND = 1000000
3435
_SECONDS_PER_DAY = 24 * 3600
3536
_DURATION_SECONDS_MAX = 315576000000
37+
_TIMESTAMP_SECONDS_MIN = -62135596800
38+
_TIMESTAMP_SECONDS_MAX = 253402300799
3639

3740
_EPOCH_DATETIME_NAIVE = datetime.datetime(1970, 1, 1, tzinfo=None)
3841
_EPOCH_DATETIME_AWARE = _EPOCH_DATETIME_NAIVE.replace(
@@ -85,10 +88,10 @@ def ToJsonString(self):
8588
and uses 3, 6 or 9 fractional digits as required to represent the
8689
exact time. Example of the return format: '1972-01-01T10:00:20.021Z'
8790
"""
88-
nanos = self.nanos % _NANOS_PER_SECOND
89-
total_sec = self.seconds + (self.nanos - nanos) // _NANOS_PER_SECOND
90-
seconds = total_sec % _SECONDS_PER_DAY
91-
days = (total_sec - seconds) // _SECONDS_PER_DAY
91+
_CheckTimestampValid(self.seconds, self.nanos)
92+
nanos = self.nanos
93+
seconds = self.seconds % _SECONDS_PER_DAY
94+
days = (self.seconds - seconds) // _SECONDS_PER_DAY
9295
dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(days, seconds)
9396

9497
result = dt.isoformat()
@@ -166,6 +169,7 @@ def FromJsonString(self, value):
166169
else:
167170
seconds += (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60
168171
# Set seconds and nanos
172+
_CheckTimestampValid(seconds, nanos)
169173
self.seconds = int(seconds)
170174
self.nanos = int(nanos)
171175

@@ -175,39 +179,53 @@ def GetCurrentTime(self):
175179

176180
def ToNanoseconds(self):
177181
"""Converts Timestamp to nanoseconds since epoch."""
182+
_CheckTimestampValid(self.seconds, self.nanos)
178183
return self.seconds * _NANOS_PER_SECOND + self.nanos
179184

180185
def ToMicroseconds(self):
181186
"""Converts Timestamp to microseconds since epoch."""
187+
_CheckTimestampValid(self.seconds, self.nanos)
182188
return (self.seconds * _MICROS_PER_SECOND +
183189
self.nanos // _NANOS_PER_MICROSECOND)
184190

185191
def ToMilliseconds(self):
186192
"""Converts Timestamp to milliseconds since epoch."""
193+
_CheckTimestampValid(self.seconds, self.nanos)
187194
return (self.seconds * _MILLIS_PER_SECOND +
188195
self.nanos // _NANOS_PER_MILLISECOND)
189196

190197
def ToSeconds(self):
191198
"""Converts Timestamp to seconds since epoch."""
199+
_CheckTimestampValid(self.seconds, self.nanos)
192200
return self.seconds
193201

194202
def FromNanoseconds(self, nanos):
195203
"""Converts nanoseconds since epoch to Timestamp."""
196-
self.seconds = nanos // _NANOS_PER_SECOND
197-
self.nanos = nanos % _NANOS_PER_SECOND
204+
seconds = nanos // _NANOS_PER_SECOND
205+
nanos = nanos % _NANOS_PER_SECOND
206+
_CheckTimestampValid(seconds, nanos)
207+
self.seconds = seconds
208+
self.nanos = nanos
198209

199210
def FromMicroseconds(self, micros):
200211
"""Converts microseconds since epoch to Timestamp."""
201-
self.seconds = micros // _MICROS_PER_SECOND
202-
self.nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND
212+
seconds = micros // _MICROS_PER_SECOND
213+
nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND
214+
_CheckTimestampValid(seconds, nanos)
215+
self.seconds = seconds
216+
self.nanos = nanos
203217

204218
def FromMilliseconds(self, millis):
205219
"""Converts milliseconds since epoch to Timestamp."""
206-
self.seconds = millis // _MILLIS_PER_SECOND
207-
self.nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND
220+
seconds = millis // _MILLIS_PER_SECOND
221+
nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND
222+
_CheckTimestampValid(seconds, nanos)
223+
self.seconds = seconds
224+
self.nanos = nanos
208225

209226
def FromSeconds(self, seconds):
210227
"""Converts seconds since epoch to Timestamp."""
228+
_CheckTimestampValid(seconds, 0)
211229
self.seconds = seconds
212230
self.nanos = 0
213231

@@ -229,6 +247,7 @@ def ToDatetime(self, tzinfo=None):
229247
# https://github.com/python/cpython/issues/109849) or full range (on some
230248
# platforms, see https://github.com/python/cpython/issues/110042) of
231249
# datetime.
250+
_CheckTimestampValid(self.seconds, self.nanos)
232251
delta = datetime.timedelta(
233252
seconds=self.seconds,
234253
microseconds=_RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND),
@@ -252,8 +271,22 @@ def FromDatetime(self, dt):
252271
# manipulated into a long value of seconds. During the conversion from
253272
# struct_time to long, the source date in UTC, and so it follows that the
254273
# correct transformation is calendar.timegm()
255-
self.seconds = calendar.timegm(dt.utctimetuple())
256-
self.nanos = dt.microsecond * _NANOS_PER_MICROSECOND
274+
seconds = calendar.timegm(dt.utctimetuple())
275+
nanos = dt.microsecond * _NANOS_PER_MICROSECOND
276+
_CheckTimestampValid(seconds, nanos)
277+
self.seconds = seconds
278+
self.nanos = nanos
279+
280+
281+
def _CheckTimestampValid(seconds, nanos):
282+
if seconds < _TIMESTAMP_SECONDS_MIN or seconds > _TIMESTAMP_SECONDS_MAX:
283+
raise ValueError(
284+
'Timestamp is not valid: Seconds {0} must be in range '
285+
'[-62135596800, 253402300799].'.format(seconds))
286+
if nanos < 0 or nanos >= _NANOS_PER_SECOND:
287+
raise ValueError(
288+
'Timestamp is not valid: Nanos {} must be in a range '
289+
'[0, 999999].'.format(nanos))
257290

258291

259292
class Duration(object):

python/google/protobuf/internal/well_known_types_test.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -352,27 +352,15 @@ def testTimezoneAwareMinDatetimeConversion(self):
352352
)
353353

354354
def testNanosOneSecond(self):
355-
# TODO: b/301980950 - Test error behavior instead once ToDatetime validates
356-
# that nanos are in expected range.
357355
tz = _TZ_PACIFIC
358356
ts = timestamp_pb2.Timestamp(nanos=1_000_000_000)
359-
self.assertEqual(ts.ToDatetime(), datetime.datetime(1970, 1, 1, 0, 0, 1))
360-
self.assertEqual(
361-
ts.ToDatetime(tz), datetime.datetime(1969, 12, 31, 16, 0, 1, tzinfo=tz)
362-
)
357+
self.assertRaisesRegex(ValueError, 'Timestamp is not valid',
358+
ts.ToDatetime)
363359

364360
def testNanosNegativeOneSecond(self):
365-
# TODO: b/301980950 - Test error behavior instead once ToDatetime validates
366-
# that nanos are in expected range.
367-
tz = _TZ_PACIFIC
368361
ts = timestamp_pb2.Timestamp(nanos=-1_000_000_000)
369-
self.assertEqual(
370-
ts.ToDatetime(), datetime.datetime(1969, 12, 31, 23, 59, 59)
371-
)
372-
self.assertEqual(
373-
ts.ToDatetime(tz),
374-
datetime.datetime(1969, 12, 31, 15, 59, 59, tzinfo=tz),
375-
)
362+
self.assertRaisesRegex(ValueError, 'Timestamp is not valid',
363+
ts.ToDatetime)
376364

377365
def testTimedeltaConversion(self):
378366
message = duration_pb2.Duration()
@@ -421,8 +409,10 @@ def testInvalidTimestamp(self):
421409
self.assertRaisesRegex(ValueError, 'year (0 )?is out of range',
422410
message.FromJsonString, '0000-01-01T00:00:00Z')
423411
message.seconds = 253402300800
424-
self.assertRaisesRegex(OverflowError, 'date value out of range',
412+
self.assertRaisesRegex(ValueError, 'Timestamp is not valid',
425413
message.ToJsonString)
414+
self.assertRaisesRegex(ValueError, 'Timestamp is not valid',
415+
message.FromSeconds, -62135596801)
426416

427417
def testInvalidDuration(self):
428418
message = duration_pb2.Duration()

0 commit comments

Comments
 (0)