identity_core/common/
timestamp.rs

1// Copyright 2020-2022 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use core::convert::TryFrom;
5use core::fmt::Debug;
6use core::fmt::Display;
7use core::fmt::Formatter;
8use core::str::FromStr;
9use std::borrow::Borrow;
10use std::borrow::Cow;
11
12use serde;
13use serde::Deserialize;
14use serde::Serialize;
15use time::format_description::well_known::Rfc3339;
16use time::OffsetDateTime;
17use time::UtcOffset;
18
19use crate::error::Error;
20use crate::error::Result;
21
22/// A parsed Timestamp.
23#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
24#[repr(transparent)]
25#[serde(try_from = "ProvisionalTimestamp<'_>", into = "String")]
26pub struct Timestamp(OffsetDateTime);
27
28impl Timestamp {
29  /// Parses a `Timestamp` from the provided input string, normalized to UTC+00:00 with fractional
30  /// seconds truncated.
31  ///
32  /// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production).
33  pub fn parse(input: &str) -> Result<Self> {
34    let offset_date_time = OffsetDateTime::parse(input, &Rfc3339)
35      .map_err(time::Error::from)
36      .map_err(Error::InvalidTimestamp)?
37      .to_offset(UtcOffset::UTC);
38    Ok(Timestamp(truncate_fractional_seconds(offset_date_time)))
39  }
40
41  /// Creates a new `Timestamp` with the current date and time, normalized to UTC+00:00 with
42  /// fractional seconds truncated.
43  ///
44  /// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production).
45  #[cfg(all(
46    not(all(target_arch = "wasm32", not(target_os = "wasi"))),
47    not(feature = "custom_time")
48  ))]
49  pub fn now_utc() -> Self {
50    Self(truncate_fractional_seconds(OffsetDateTime::now_utc()))
51  }
52
53  /// Creates a new `Timestamp` with the current date and time, normalized to UTC+00:00 with
54  /// fractional seconds truncated.
55  ///
56  /// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production).
57  #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), not(feature = "custom_time")))]
58  pub fn now_utc() -> Self {
59    let milliseconds_since_unix_epoch: i64 = js_sys::Date::now() as i64;
60    let seconds: i64 = milliseconds_since_unix_epoch / 1000;
61    // expect is okay, we assume the current time is between 0AD and 9999AD
62    Self::from_unix(seconds).expect("Timestamp failed to convert system datetime")
63  }
64
65  /// Creates a new `Timestamp` with the current date and time, normalized to UTC+00:00 with
66  /// fractional seconds truncated.
67  ///
68  /// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production).
69  #[cfg(feature = "custom_time")]
70  pub fn now_utc() -> Self {
71    crate::custom_time::now_utc_custom()
72  }
73
74  /// Returns the `Timestamp` as an [RFC 3339](https://tools.ietf.org/html/rfc3339) `String`.
75  pub fn to_rfc3339(&self) -> String {
76    // expect is okay, constructors ensure RFC 3339 compatible timestamps.
77    // Making this fallible would break our interface such as From<Timestamp> for String.
78    self.0.format(&Rfc3339).expect("Timestamp incompatible with RFC 3339")
79  }
80
81  /// Returns the `Timestamp` as a Unix timestamp.
82  pub fn to_unix(&self) -> i64 {
83    self.0.unix_timestamp()
84  }
85
86  /// Creates a new `Timestamp` from the given Unix timestamp.
87  ///
88  /// The timestamp must be in the valid range for [RFC 3339](https://tools.ietf.org/html/rfc3339).
89  ///
90  /// # Errors
91  /// [`Error::InvalidTimestamp`] if `seconds` is outside of the interval [-62167219200,253402300799].
92  pub fn from_unix(seconds: i64) -> Result<Self> {
93    let offset_date_time = OffsetDateTime::from_unix_timestamp(seconds)
94      .map_err(time::error::Error::from)
95      .map_err(Error::InvalidTimestamp)?;
96
97    // Reject years outside of the range 0000AD - 9999AD per Rfc3339
98    // upfront to prevent conversion errors in to_rfc3339().
99    // https://datatracker.ietf.org/doc/html/rfc3339#section-1
100    if !(0..10_000).contains(&offset_date_time.year()) {
101      return Err(Error::InvalidTimestamp(time::error::Error::Format(
102        time::error::Format::InvalidComponent("invalid year"),
103      )));
104    }
105    Ok(Self(offset_date_time))
106  }
107
108  /// Computes `self + duration`
109  ///
110  /// Returns `None` if the operation leads to a timestamp not in the valid range for [RFC 3339](https://tools.ietf.org/html/rfc3339).
111  pub fn checked_add(self, duration: Duration) -> Option<Self> {
112    self
113      .0
114      .checked_add(duration.0)
115      .and_then(|offset_date_time| Self::from_unix(offset_date_time.unix_timestamp()).ok())
116  }
117
118  /// Computes `self - duration`
119  ///
120  /// Returns `None` if the operation leads to a timestamp not in the valid range for [RFC 3339](https://tools.ietf.org/html/rfc3339).
121  pub fn checked_sub(self, duration: Duration) -> Option<Self> {
122    self
123      .0
124      .checked_sub(duration.0)
125      .and_then(|offset_date_time| Self::from_unix(offset_date_time.unix_timestamp()).ok())
126  }
127}
128
129impl Default for Timestamp {
130  fn default() -> Self {
131    Self::now_utc()
132  }
133}
134
135impl Debug for Timestamp {
136  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
137    write!(f, "{:?}", self.to_rfc3339())
138  }
139}
140
141impl Display for Timestamp {
142  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
143    write!(f, "{}", self.to_rfc3339())
144  }
145}
146
147impl From<Timestamp> for String {
148  fn from(timestamp: Timestamp) -> Self {
149    timestamp.to_rfc3339()
150  }
151}
152
153impl TryFrom<&'_ str> for Timestamp {
154  type Error = Error;
155
156  fn try_from(string: &'_ str) -> Result<Self, Self::Error> {
157    Self::parse(string)
158  }
159}
160
161// This struct is only used to (potentially) avoid an allocation when deserializing a Timestamp. We cannot deserialize
162// via &str because that breaks serde_json::from_value which we use extensively in the Stronghold bindings.
163// This approach is inspired by https://crates.io/crates/serde_str_helpers, but we only use a subset of the functionality offered by that crate.
164#[derive(Deserialize)]
165struct ProvisionalTimestamp<'a>(#[serde(borrow)] Cow<'a, str>);
166
167impl<'a> TryFrom<ProvisionalTimestamp<'a>> for Timestamp {
168  type Error = Error;
169
170  fn try_from(value: ProvisionalTimestamp<'a>) -> Result<Self, Self::Error> {
171    Timestamp::parse(value.0.borrow())
172  }
173}
174
175impl TryFrom<String> for Timestamp {
176  type Error = Error;
177
178  fn try_from(string: String) -> Result<Self, Self::Error> {
179    Self::parse(&string)
180  }
181}
182
183impl FromStr for Timestamp {
184  type Err = Error;
185
186  fn from_str(string: &str) -> Result<Self, Self::Err> {
187    Self::parse(string)
188  }
189}
190
191/// Truncates an `OffsetDateTime` to the second.
192fn truncate_fractional_seconds(offset_date_time: OffsetDateTime) -> OffsetDateTime {
193  offset_date_time - time::Duration::nanoseconds(offset_date_time.nanosecond() as i64)
194}
195
196/// A span of time.
197///
198/// This type is typically used to increment or decrement a [`Timestamp`].
199#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
200#[repr(transparent)]
201pub struct Duration(time::Duration);
202
203// Re-expose a small subset of time::Duration and use u32 instead of i64
204// to disallow negative durations. This gives us the flexibility to migrate
205// the internal representation to e.g. std::time::Duration in the future
206// if required.
207impl Duration {
208  /// Create a new [`Duration`] with the given number of seconds.
209  pub const fn seconds(seconds: u32) -> Self {
210    Self(time::Duration::seconds(seconds as i64))
211  }
212  /// Create a new [`Duration`] with the given number of minutes.
213  pub const fn minutes(minutes: u32) -> Self {
214    Self(time::Duration::minutes(minutes as i64))
215  }
216
217  /// Create a new [`Duration`] with the given number of days.
218  pub const fn days(days: u32) -> Self {
219    Self(time::Duration::days(days as i64))
220  }
221
222  /// Create a new [`Duration`] with the given number of hours.
223  pub const fn hours(hours: u32) -> Self {
224    Self(time::Duration::hours(hours as i64))
225  }
226
227  /// Create a new [`Duration`] with the given number of weeks.
228  pub const fn weeks(weeks: u32) -> Self {
229    Self(time::Duration::weeks(weeks as i64))
230  }
231}
232
233#[cfg(test)]
234mod tests {
235  use crate::common::Timestamp;
236  use crate::convert::FromJson;
237  use crate::convert::ToJson;
238  use proptest::proptest;
239
240  use super::Duration;
241
242  // 0000-01-01T00:00:00Z
243  const FIRST_VALID_UNIX_TIMESTAMP: i64 = -62167219200;
244  // 9999-12-31T23:59:59Z
245  const LAST_VALID_UNIX_TIMESTAMP: i64 = 253402300799;
246
247  #[test]
248  fn test_parse_valid() {
249    let original = "2020-01-01T00:00:00Z";
250    let timestamp = Timestamp::parse(original).unwrap();
251
252    assert_eq!(timestamp.to_rfc3339(), original);
253
254    let original = "1980-01-01T12:34:56Z";
255    let timestamp = Timestamp::parse(original).unwrap();
256
257    assert_eq!(timestamp.to_rfc3339(), original);
258  }
259
260  #[test]
261  fn test_parse_valid_truncated() {
262    let original = "1980-01-01T12:34:56.789Z";
263    let expected = "1980-01-01T12:34:56Z";
264    let timestamp = Timestamp::parse(original).unwrap();
265
266    assert_eq!(timestamp.to_rfc3339(), expected);
267  }
268
269  #[test]
270  fn test_parse_valid_offset_normalised() {
271    let original = "1937-01-01T12:00:27.87+00:20";
272    let expected = "1937-01-01T11:40:27Z";
273    let timestamp = Timestamp::parse(original).unwrap();
274
275    assert_eq!(timestamp.to_rfc3339(), expected);
276  }
277
278  #[test]
279  fn test_checked_add() {
280    let timestamp = Timestamp::parse("1980-01-01T12:34:56Z").unwrap();
281    let second_later = timestamp.checked_add(Duration::seconds(1)).unwrap();
282    assert_eq!(second_later.to_rfc3339(), "1980-01-01T12:34:57Z");
283    let minute_later = timestamp.checked_add(Duration::minutes(1)).unwrap();
284    assert_eq!(minute_later.to_rfc3339(), "1980-01-01T12:35:56Z");
285    let hour_later = timestamp.checked_add(Duration::hours(1)).unwrap();
286    assert_eq!(hour_later.to_rfc3339(), "1980-01-01T13:34:56Z");
287    let day_later = timestamp.checked_add(Duration::days(1)).unwrap();
288    assert_eq!(day_later.to_rfc3339(), "1980-01-02T12:34:56Z");
289    let week_later = timestamp.checked_add(Duration::weeks(1)).unwrap();
290    assert_eq!(week_later.to_rfc3339(), "1980-01-08T12:34:56Z");
291
292    // check overflow
293    assert!(Timestamp::from_unix(LAST_VALID_UNIX_TIMESTAMP)
294      .unwrap()
295      .checked_add(Duration::seconds(1))
296      .is_none());
297  }
298
299  #[test]
300  fn test_checked_sub() {
301    let timestamp = Timestamp::parse("1980-01-01T12:34:56Z").unwrap();
302    let second_earlier = timestamp.checked_sub(Duration::seconds(1)).unwrap();
303    assert_eq!(second_earlier.to_rfc3339(), "1980-01-01T12:34:55Z");
304    let minute_earlier = timestamp.checked_sub(Duration::minutes(1)).unwrap();
305    assert_eq!(minute_earlier.to_rfc3339(), "1980-01-01T12:33:56Z");
306    let hour_earlier = timestamp.checked_sub(Duration::hours(1)).unwrap();
307    assert_eq!(hour_earlier.to_rfc3339(), "1980-01-01T11:34:56Z");
308    let day_earlier = timestamp.checked_sub(Duration::days(1)).unwrap();
309    assert_eq!(day_earlier.to_rfc3339(), "1979-12-31T12:34:56Z");
310    let week_earlier = timestamp.checked_sub(Duration::weeks(1)).unwrap();
311    assert_eq!(week_earlier.to_rfc3339(), "1979-12-25T12:34:56Z");
312
313    // check underflow
314    assert!(Timestamp::from_unix(FIRST_VALID_UNIX_TIMESTAMP)
315      .unwrap()
316      .checked_sub(Duration::seconds(1))
317      .is_none());
318  }
319
320  #[test]
321  fn test_from_unix_zero_to_rfc3339() {
322    let unix_epoch = Timestamp::from_unix(0).unwrap();
323    assert_eq!(unix_epoch.to_rfc3339(), "1970-01-01T00:00:00Z");
324  }
325
326  #[test]
327  fn test_from_unix_invalid_edge_cases() {
328    assert!(Timestamp::from_unix(LAST_VALID_UNIX_TIMESTAMP + 1).is_err());
329    assert!(Timestamp::from_unix(FIRST_VALID_UNIX_TIMESTAMP - 1).is_err());
330  }
331
332  #[test]
333  fn test_from_unix_to_rfc3339_boundaries() {
334    let beginning = Timestamp::from_unix(FIRST_VALID_UNIX_TIMESTAMP).unwrap();
335    let end = Timestamp::from_unix(LAST_VALID_UNIX_TIMESTAMP).unwrap();
336    assert_eq!(beginning.to_rfc3339(), "0000-01-01T00:00:00Z");
337    assert_eq!(end.to_rfc3339(), "9999-12-31T23:59:59Z");
338  }
339
340  proptest! {
341    #[test]
342    fn test_from_unix_to_rfc3339_valid_no_panic(seconds in FIRST_VALID_UNIX_TIMESTAMP..=LAST_VALID_UNIX_TIMESTAMP) {
343      let timestamp = Timestamp::from_unix(seconds).unwrap();
344      let expected_length = "dddd-dd-ddTdd:dd:ddZ".len();
345      assert_eq!(timestamp.to_rfc3339().len(), expected_length);
346    }
347  }
348
349  #[test]
350  #[should_panic = "InvalidTimestamp"]
351  fn test_parse_empty() {
352    Timestamp::parse("").unwrap();
353  }
354
355  #[test]
356  #[should_panic = "InvalidTimestamp"]
357  fn test_parse_invalid_date() {
358    Timestamp::parse("foo bar").unwrap();
359  }
360
361  #[test]
362  #[should_panic = "InvalidTimestamp"]
363  fn test_parse_invalid_fmt() {
364    Timestamp::parse("2020/01/01 03:30:16").unwrap();
365  }
366
367  #[test]
368  fn test_json_vec_roundtrip() {
369    let time1: Timestamp = Timestamp::now_utc();
370    let json: Vec<u8> = time1.to_json_vec().unwrap();
371    let time2: Timestamp = Timestamp::from_json_slice(&json).unwrap();
372
373    assert_eq!(time1, time2);
374  }
375
376  #[test]
377  fn test_json_value_roundtrip() {
378    let time1: Timestamp = Timestamp::now_utc();
379    let json: serde_json::Value = time1.to_json_value().unwrap();
380    let time2: Timestamp = Timestamp::from_json_value(json).unwrap();
381
382    assert_eq!(time1, time2);
383  }
384}