1use core::{
4 convert::{TryFrom, TryInto},
5 fmt,
6 ops::{Add, Sub},
7 str::FromStr,
8 time::Duration,
9};
10
11use cometbft_proto::{google::protobuf::Timestamp, serializers::timestamp, Protobuf};
12use serde::{Deserialize, Serialize};
13use time::{
14 format_description::well_known::Rfc3339,
15 macros::{datetime, offset},
16 OffsetDateTime, PrimitiveDateTime,
17};
18
19use crate::{error::Error, prelude::*};
20
21#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
38#[serde(try_from = "Timestamp", into = "Timestamp")]
39pub struct Time(PrimitiveDateTime);
40
41impl Protobuf<Timestamp> for Time {}
42
43impl TryFrom<Timestamp> for Time {
44 type Error = Error;
45
46 fn try_from(value: Timestamp) -> Result<Self, Error> {
47 let nanos = value
48 .nanos
49 .try_into()
50 .map_err(|_| Error::timestamp_nanos_out_of_range())?;
51 Self::from_unix_timestamp(value.seconds, nanos)
52 }
53}
54
55impl From<Time> for Timestamp {
56 fn from(value: Time) -> Self {
57 let t = value.0.assume_utc();
58 let seconds = t.unix_timestamp();
59 let nanos = t.nanosecond() as i32;
62 Timestamp { seconds, nanos }
63 }
64}
65
66impl Time {
67 #[cfg(feature = "clock")]
68 pub fn now() -> Time {
69 OffsetDateTime::now_utc().try_into().unwrap()
70 }
71
72 fn from_utc(t: OffsetDateTime) -> Result<Self, Error> {
76 debug_assert_eq!(t.offset(), offset!(UTC));
77 match t.year() {
78 1..=9999 => Ok(Self(PrimitiveDateTime::new(t.date(), t.time()))),
79 _ => Err(Error::date_out_of_range()),
80 }
81 }
82
83 pub fn unix_epoch() -> Self {
85 Self(datetime!(1970-01-01 00:00:00))
86 }
87
88 pub fn from_unix_timestamp(secs: i64, nanos: u32) -> Result<Self, Error> {
89 if nanos > 999_999_999 {
90 return Err(Error::timestamp_nanos_out_of_range());
91 }
92 let total_nanos = secs as i128 * 1_000_000_000 + nanos as i128;
93 match OffsetDateTime::from_unix_timestamp_nanos(total_nanos) {
94 Ok(odt) => Self::from_utc(odt),
95 _ => Err(Error::timestamp_conversion()),
96 }
97 }
98
99 pub fn duration_since(&self, other: Time) -> Result<Duration, Error> {
102 let duration = self.0.assume_utc() - other.0.assume_utc();
103 duration
104 .try_into()
105 .map_err(|_| Error::duration_out_of_range())
106 }
107
108 pub fn parse_from_rfc3339(s: &str) -> Result<Self, Error> {
110 let date = OffsetDateTime::parse(s, &Rfc3339)
111 .map_err(Error::time_parse)?
112 .to_offset(offset!(UTC));
113 Self::from_utc(date)
114 }
115
116 pub fn to_rfc3339(&self) -> String {
118 timestamp::to_rfc3339_nanos(self.0.assume_utc())
119 }
120
121 pub fn unix_timestamp(&self) -> i64 {
123 self.0.assume_utc().unix_timestamp()
124 }
125
126 pub fn unix_timestamp_nanos(&self) -> i128 {
128 self.0.assume_utc().unix_timestamp_nanos()
129 }
130
131 pub fn checked_add(self, duration: Duration) -> Option<Self> {
133 let duration = duration.try_into().ok()?;
134 let t = self.0.checked_add(duration)?;
135 Self::from_utc(t.assume_utc()).ok()
136 }
137
138 pub fn checked_sub(self, duration: Duration) -> Option<Self> {
140 let duration = duration.try_into().ok()?;
141 let t = self.0.checked_sub(duration)?;
142 Self::from_utc(t.assume_utc()).ok()
143 }
144
145 pub fn before(&self, other: Time) -> bool {
147 self.0.assume_utc() < other.0.assume_utc()
148 }
149
150 pub fn after(&self, other: Time) -> bool {
152 self.0.assume_utc() > other.0.assume_utc()
153 }
154}
155
156impl fmt::Display for Time {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
158 timestamp::fmt_as_rfc3339_nanos(self.0.assume_utc(), f)
159 }
160}
161
162impl FromStr for Time {
163 type Err = Error;
164
165 fn from_str(s: &str) -> Result<Self, Self::Err> {
166 Self::parse_from_rfc3339(s)
167 }
168}
169
170impl TryFrom<OffsetDateTime> for Time {
171 type Error = Error;
172
173 fn try_from(t: OffsetDateTime) -> Result<Time, Error> {
174 Self::from_utc(t.to_offset(offset!(UTC)))
175 }
176}
177
178impl From<Time> for OffsetDateTime {
179 fn from(t: Time) -> OffsetDateTime {
180 t.0.assume_utc()
181 }
182}
183
184impl Add<Duration> for Time {
185 type Output = Result<Self, Error>;
186
187 fn add(self, rhs: Duration) -> Self::Output {
188 let duration = rhs.try_into().map_err(|_| Error::duration_out_of_range())?;
189 let t = self
190 .0
191 .checked_add(duration)
192 .ok_or_else(Error::duration_out_of_range)?;
193 Self::from_utc(t.assume_utc())
194 }
195}
196
197impl Sub<Duration> for Time {
198 type Output = Result<Self, Error>;
199
200 fn sub(self, rhs: Duration) -> Self::Output {
201 let duration = rhs.try_into().map_err(|_| Error::duration_out_of_range())?;
202 let t = self
203 .0
204 .checked_sub(duration)
205 .ok_or_else(Error::duration_out_of_range)?;
206 Self::from_utc(t.assume_utc())
207 }
208}
209
210pub trait ParseTimestamp {
212 fn parse_timestamp(&self) -> Result<Time, Error>;
214}
215
216#[cfg(test)]
217mod tests {
218 use cometbft_pbt_gen as pbt;
219 use proptest::{prelude::*, sample::select};
220 use time::{Date, Month::*};
221
222 use super::*;
223 use crate::error::ErrorDetail;
224
225 fn particular_rfc3339_timestamps() -> impl Strategy<Value = String> {
227 let strs: Vec<String> = vec![
228 "0001-01-01T00:00:00Z",
229 "9999-12-31T23:59:59.999999999Z",
230 "2020-09-14T16:33:54.21191421Z",
231 "2020-09-14T16:33:00Z",
232 "2020-09-14T16:33:00.1Z",
233 "2020-09-14T16:33:00.211914212Z",
234 "1970-01-01T00:00:00Z",
235 "2021-01-07T20:25:56.0455760Z",
236 "2021-01-07T20:25:57.039219Z",
237 "2021-01-07T20:25:58.03562100Z",
238 "2021-01-07T20:25:59.000955200Z",
239 "2021-01-07T20:26:04.0121030Z",
240 "2021-01-07T20:26:05.005096Z",
241 "2021-01-07T20:26:09.08488400Z",
242 "2021-01-07T20:26:11.0875340Z",
243 "2021-01-07T20:26:12.078268Z",
244 "2021-01-07T20:26:13.08074100Z",
245 "2021-01-07T20:26:15.079663000Z",
246 ]
247 .into_iter()
248 .map(String::from)
249 .collect();
250
251 select(strs)
252 }
253
254 fn particular_datetimes_out_of_range() -> impl Strategy<Value = OffsetDateTime> {
255 let dts = vec![
256 datetime!(0000-12-31 23:59:59.999999999 UTC),
257 datetime!(0001-01-01 00:00:00.999999999 +00:00:01),
258 Date::from_calendar_date(-1, October, 9)
259 .unwrap()
260 .midnight()
261 .assume_utc(),
262 ];
263 select(dts)
264 }
265
266 proptest! {
267 #[test]
268 fn can_parse_rfc3339_timestamps(stamp in pbt::time::arb_protobuf_safe_rfc3339_timestamp()) {
269 prop_assert!(stamp.parse::<Time>().is_ok())
270 }
271
272 #[test]
273 fn serde_from_value_is_the_inverse_of_to_value_within_reasonable_time_range(
274 datetime in pbt::time::arb_protobuf_safe_datetime()
275 ) {
276 let time: Time = datetime.try_into().unwrap();
279 let json_encoded_time = serde_json::to_value(time).unwrap();
280 let decoded_time: Time = serde_json::from_value(json_encoded_time).unwrap();
281 prop_assert_eq!(time, decoded_time);
282 }
283
284 #[test]
285 fn serde_of_rfc3339_timestamps_is_safe(
286 stamp in prop_oneof![
287 pbt::time::arb_protobuf_safe_rfc3339_timestamp(),
288 particular_rfc3339_timestamps(),
289 ]
290 ) {
291 let time: Time = stamp.parse().unwrap();
296 let json_encoded_time = serde_json::to_value(time).unwrap();
297 let decoded_time: Time = serde_json::from_value(json_encoded_time).unwrap();
298 prop_assert_eq!(time, decoded_time);
299 }
300
301 #[test]
302 fn conversion_unix_timestamp_is_safe(
303 stamp in prop_oneof![
304 pbt::time::arb_protobuf_safe_rfc3339_timestamp(),
305 particular_rfc3339_timestamps(),
306 ]
307 ) {
308 let time: Time = stamp.parse().unwrap();
309 let timestamp = time.unix_timestamp();
310 let parsed = Time::from_unix_timestamp(timestamp, 0).unwrap();
311 prop_assert_eq!(timestamp, parsed.unix_timestamp());
312 }
313
314 #[test]
315 fn conversion_from_datetime_succeeds_for_4_digit_ce_years(
316 datetime in prop_oneof![
317 pbt::time::arb_datetime_with_offset(),
318 particular_datetimes_out_of_range(),
319 ]
320 ) {
321 let res: Result<Time, _> = datetime.try_into();
322 match datetime.to_offset(offset!(UTC)).year() {
323 1 ..= 9999 => {
324 let t = res.unwrap();
325 let dt_converted_back: OffsetDateTime = t.into();
326 assert_eq!(dt_converted_back, datetime);
327 }
328 _ => {
329 let e = res.unwrap_err();
330 assert!(matches!(e.detail(), ErrorDetail::DateOutOfRange(_)))
331 }
332 }
333 }
334
335 #[test]
336 fn from_unix_timestamp_rejects_out_of_range_nanos(
337 datetime in pbt::time::arb_protobuf_safe_datetime(),
338 nanos in 1_000_000_000 ..= u32::MAX,
339 ) {
340 let secs = datetime.unix_timestamp();
341 let res = Time::from_unix_timestamp(secs, nanos);
342 let e = res.unwrap_err();
343 assert!(matches!(e.detail(), ErrorDetail::TimestampNanosOutOfRange(_)))
344 }
345 }
346
347 fn duration_from_nanos(whole_nanos: u128) -> Duration {
348 let secs: u64 = (whole_nanos / 1_000_000_000).try_into().unwrap();
349 let nanos = (whole_nanos % 1_000_000_000) as u32;
350 Duration::new(secs, nanos)
351 }
352
353 prop_compose! {
354 fn args_for_regular_add()
355 (t in pbt::time::arb_protobuf_safe_datetime())
356 (
357 t in Just(t),
358 d_nanos in 0 ..= (pbt::time::max_protobuf_time() - t).whole_nanoseconds() as u128,
359 ) -> (OffsetDateTime, Duration)
360 {
361 (t, duration_from_nanos(d_nanos))
362 }
363 }
364
365 prop_compose! {
366 fn args_for_regular_sub()
367 (t in pbt::time::arb_protobuf_safe_datetime())
368 (
369 t in Just(t),
370 d_nanos in 0 ..= (t - pbt::time::min_protobuf_time()).whole_nanoseconds() as u128,
371 ) -> (OffsetDateTime, Duration)
372 {
373 (t, duration_from_nanos(d_nanos))
374 }
375 }
376
377 prop_compose! {
378 fn args_for_overflowed_add()
379 (t in pbt::time::arb_protobuf_safe_datetime())
380 (
381 t in Just(t),
382 d_nanos in (
383 (pbt::time::max_protobuf_time() - t).whole_nanoseconds() as u128 + 1
384 ..=
385 Duration::MAX.as_nanos()
386 ),
387 ) -> (OffsetDateTime, Duration)
388 {
389 (t, duration_from_nanos(d_nanos))
390 }
391 }
392
393 prop_compose! {
394 fn args_for_overflowed_sub()
395 (t in pbt::time::arb_protobuf_safe_datetime())
396 (
397 t in Just(t),
398 d_nanos in (
399 (t - pbt::time::min_protobuf_time()).whole_nanoseconds() as u128 + 1
400 ..=
401 Duration::MAX.as_nanos()
402 ),
403 ) -> (OffsetDateTime, Duration)
404 {
405 (t, duration_from_nanos(d_nanos))
406 }
407 }
408
409 proptest! {
410 #[test]
411 fn checked_add_regular((dt, d) in args_for_regular_add()) {
412 let t: Time = dt.try_into().unwrap();
413 let t = t.checked_add(d).unwrap();
414 let res: OffsetDateTime = t.into();
415 assert_eq!(res, dt + d);
416 }
417
418 #[test]
419 fn checked_sub_regular((dt, d) in args_for_regular_sub()) {
420 let t: Time = dt.try_into().unwrap();
421 let t = t.checked_sub(d).unwrap();
422 let res: OffsetDateTime = t.into();
423 assert_eq!(res, dt - d);
424 }
425
426 #[test]
427 fn checked_add_overflow((dt, d) in args_for_overflowed_add()) {
428 let t: Time = dt.try_into().unwrap();
429 assert_eq!(t.checked_add(d), None);
430 }
431
432 #[test]
433 fn checked_sub_overflow((dt, d) in args_for_overflowed_sub()) {
434 let t: Time = dt.try_into().unwrap();
435 assert_eq!(t.checked_sub(d), None);
436 }
437 }
438}