1//! Manipulate time periods (as used in the onion service system)
23use std::{
4 fmt::Display,
5 time::{Duration, SystemTime},
6};
78use humantime::format_rfc3339_seconds;
9use tor_units::IntegerMinutes;
1011use serde::{Deserialize, Serialize};
1213/// A period of time, as used in the onion service system.
14///
15/// A `TimePeriod` is defined as a duration (in seconds), and the number of such
16/// durations that have elapsed since a given offset from the Unix epoch. So
17/// for example, the interval "(86400 seconds length, 15 intervals, 12 hours
18/// offset)", covers `1970-01-16T12:00:00` up to but not including
19/// `1970-01-17T12:00:00`.
20///
21/// These time periods are used to derive a different `BlindedOnionIdKey` during
22/// each period from each `OnionIdKey`.
23#[derive(Deserialize, Serialize, Copy, Clone, Debug, Eq, PartialEq, Hash)]
24pub struct TimePeriod {
25/// Index of the time periods that have passed since the unix epoch.
26pub(crate) interval_num: u64,
27/// The length of a time period, in **minutes**.
28 ///
29 /// The spec admits only periods which are a whole number of minutes.
30pub(crate) length: IntegerMinutes<u32>,
31/// Our offset from the epoch, in seconds.
32 ///
33 /// This is the amount of time after the Unix epoch when our epoch begins,
34 /// rounded down to the nearest second.
35pub(crate) epoch_offset_in_sec: u32,
36}
3738/// Two [`TimePeriod`]s are ordered with respect to one another if they have the
39/// same interval length and offset.
40impl PartialOrd for TimePeriod {
41fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
42if self.length == other.length && self.epoch_offset_in_sec == other.epoch_offset_in_sec {
43Some(self.interval_num.cmp(&other.interval_num))
44 } else {
45None
46}
47 }
48}
4950impl Display for TimePeriod {
51fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52write!(f, "#{} ", self.interval_num())?;
53match self.range() {
54Ok(r) => {
55let mins = self.length().as_minutes();
56write!(
57 f,
58"{}..+{}:{:02}",
59 format_rfc3339_seconds(r.start),
60 mins / 60,
61 mins % 60
62)
63 }
64Err(_) => write!(f, "overflow! {self:?}"),
65 }
66 }
67}
6869impl TimePeriod {
70/// Construct a time period of a given `length` that contains `when`.
71 ///
72 /// The `length` value is rounded down to the nearest second,
73 /// and must then be a whole number of minutes.
74 ///
75 /// The `epoch_offset` value is the amount of time after the Unix epoch when
76 /// our epoch begins. It is also rounded down to the nearest second.
77 ///
78 /// Return None if the Duration is too large or too small, or if `when`
79 /// cannot be represented as a time period.
80pub fn new(
81 length: Duration,
82 when: SystemTime,
83 epoch_offset: Duration,
84 ) -> Result<Self, TimePeriodError> {
85// The algorithm here is specified in rend-spec-v3 section 2.2.1
86let length_in_sec =
87 u32::try_from(length.as_secs()).map_err(|_| TimePeriodError::IntervalInvalid)?;
88if length_in_sec % 60 != 0 || length.subsec_nanos() != 0 {
89return Err(TimePeriodError::IntervalInvalid);
90 }
91let length_in_minutes = length_in_sec / 60;
92let length = IntegerMinutes::new(length_in_minutes);
93let epoch_offset_in_sec =
94 u32::try_from(epoch_offset.as_secs()).map_err(|_| TimePeriodError::OffsetInvalid)?;
95let interval_num = when
96 .duration_since(SystemTime::UNIX_EPOCH + epoch_offset)
97 .map_err(|_| TimePeriodError::OutOfRange)?
98.as_secs()
99 / u64::from(length_in_sec);
100Ok(TimePeriod {
101 interval_num,
102 length,
103 epoch_offset_in_sec,
104 })
105 }
106107/// Compute the `TimePeriod`, given its length (in **minutes**), index (the number of time
108 /// periods that have passed since the unix epoch), and offset from the epoch (in seconds).
109 ///
110 /// The `epoch_offset_in_sec` value is the number of seconds after the Unix epoch when our
111 /// epoch begins, rounded down to the nearest second.
112 /// Note that this is *not* the time_t at which this *Time Period* begins.
113 ///
114 /// The returned TP begins at the time_t `interval_num * length * 60 + epoch_offset_in_sec`
115 /// and ends `length * 60` seconds later.
116pub fn from_parts(length: u32, interval_num: u64, epoch_offset_in_sec: u32) -> Self {
117let length_in_sec = length * 60;
118119Self {
120 interval_num,
121 length: length.into(),
122 epoch_offset_in_sec,
123 }
124 }
125126/// Return the time period after this one.
127 ///
128 /// Return None if this is the last representable time period.
129pub fn next(&self) -> Option<Self> {
130Some(TimePeriod {
131 interval_num: self.interval_num.checked_add(1)?,
132 ..*self
133})
134 }
135/// Return the time period before this one.
136 ///
137 /// Return None if this is the first representable time period.
138pub fn prev(&self) -> Option<Self> {
139Some(TimePeriod {
140 interval_num: self.interval_num.checked_sub(1)?,
141 ..*self
142})
143 }
144/// Return true if this time period contains `when`.
145 ///
146 /// # Limitations
147 ///
148 /// This function always returns false if the time period contains any times
149 /// that cannot be represented as a `SystemTime`.
150pub fn contains(&self, when: SystemTime) -> bool {
151match self.range() {
152Ok(r) => r.contains(&when),
153Err(_) => false,
154 }
155 }
156/// Return a range representing the [`SystemTime`] values contained within
157 /// this time period.
158 ///
159 /// Return None if this time period contains any times that can be
160 /// represented as a `SystemTime`.
161pub fn range(&self) -> Result<std::ops::Range<SystemTime>, TimePeriodError> {
162 (|| {
163let length_in_sec = u64::from(self.length.as_minutes()) * 60;
164let start_sec = length_in_sec.checked_mul(self.interval_num)?;
165let end_sec = start_sec.checked_add(length_in_sec)?;
166let epoch_offset = Duration::new(self.epoch_offset_in_sec.into(), 0);
167let start = (SystemTime::UNIX_EPOCH + epoch_offset)
168 .checked_add(Duration::from_secs(start_sec))?;
169let end = (SystemTime::UNIX_EPOCH + epoch_offset)
170 .checked_add(Duration::from_secs(end_sec))?;
171Some(start..end)
172 })()
173 .ok_or(TimePeriodError::OutOfRange)
174 }
175176/// Return the numeric index of this time period.
177 ///
178 /// This function should only be used when encoding the time period for
179 /// cryptographic purposes.
180pub fn interval_num(&self) -> u64 {
181self.interval_num
182 }
183184/// Return the length of this time period as a number of seconds.
185 ///
186 /// This function should only be used when encoding the time period for
187 /// cryptographic purposes.
188pub fn length(&self) -> IntegerMinutes<u32> {
189self.length
190 }
191192/// Return our offset from the epoch, in seconds.
193 ///
194 /// Note that this is *not* the start of the TP.
195 /// See `TimePeriod::from_parts`.
196pub fn epoch_offset_in_sec(&self) -> u32 {
197self.epoch_offset_in_sec
198 }
199}
200201/// An error that occurs when creating or manipulating a [`TimePeriod`]
202#[derive(Clone, Debug, thiserror::Error)]
203#[non_exhaustive]
204pub enum TimePeriodError {
205/// We couldn't represent the time period in the way we were trying to
206 /// represent it, since it outside of the range supported by the data type.
207#[error("Time period out was out of range")]
208OutOfRange,
209210/// The time period couldn't be constructed because its interval was
211 /// invalid.
212 ///
213 /// (We require that intervals are a multiple of 60 seconds, and that they
214 /// can be represented in a `u32`.)
215#[error("Invalid time period interval")]
216IntervalInvalid,
217218/// The time period couldn't be constructed because its offset was invalid.
219 ///
220 /// (We require that offsets can be represented in a `u32`.)
221#[error("Invalid time period offset")]
222OffsetInvalid,
223}
224225#[cfg(test)]
226mod test {
227// @@ begin test lint list maintained by maint/add_warning @@
228#![allow(clippy::bool_assert_comparison)]
229 #![allow(clippy::clone_on_copy)]
230 #![allow(clippy::dbg_macro)]
231 #![allow(clippy::mixed_attributes_style)]
232 #![allow(clippy::print_stderr)]
233 #![allow(clippy::print_stdout)]
234 #![allow(clippy::single_char_pattern)]
235 #![allow(clippy::unwrap_used)]
236 #![allow(clippy::unchecked_duration_subtraction)]
237 #![allow(clippy::useless_vec)]
238 #![allow(clippy::needless_pass_by_value)]
239//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
240241use super::*;
242use humantime::{parse_duration, parse_rfc3339};
243244/// Check reconstructing `period` from parts produces an identical `TimePeriod`.
245fn assert_eq_from_parts(period: TimePeriod) {
246assert_eq!(
247 period,
248 TimePeriod::from_parts(
249 period.length().as_minutes(),
250 period.interval_num(),
251 period.epoch_offset_in_sec()
252 )
253 );
254 }
255256#[test]
257fn check_testvec() {
258// Test case from C tor, taken from rend-spec.
259let offset = Duration::new(12 * 60 * 60, 0);
260let time = parse_rfc3339("2016-04-13T11:00:00Z").unwrap();
261let one_day = parse_duration("1day").unwrap();
262let period = TimePeriod::new(one_day, time, offset).unwrap();
263assert_eq!(period.interval_num, 16903);
264assert!(period.contains(time));
265 assert_eq_from_parts(period);
266267let time = parse_rfc3339("2016-04-13T11:59:59Z").unwrap();
268let period = TimePeriod::new(one_day, time, offset).unwrap();
269assert_eq!(period.interval_num, 16903); // still the same.
270assert!(period.contains(time));
271 assert_eq_from_parts(period);
272273assert_eq!(period.prev().unwrap().interval_num, 16902);
274assert_eq!(period.next().unwrap().interval_num, 16904);
275276let time2 = parse_rfc3339("2016-04-13T12:00:00Z").unwrap();
277let period2 = TimePeriod::new(one_day, time2, offset).unwrap();
278assert_eq!(period2.interval_num, 16904);
279assert!(period < period2);
280assert!(period2 > period);
281assert_eq!(period.next().unwrap(), period2);
282assert_eq!(period2.prev().unwrap(), period);
283assert!(period2.contains(time2));
284assert!(!period2.contains(time));
285assert!(!period.contains(time2));
286287assert_eq!(
288 period.range().unwrap(),
289 parse_rfc3339("2016-04-12T12:00:00Z").unwrap()
290 ..parse_rfc3339("2016-04-13T12:00:00Z").unwrap()
291 );
292assert_eq!(
293 period2.range().unwrap(),
294 parse_rfc3339("2016-04-13T12:00:00Z").unwrap()
295 ..parse_rfc3339("2016-04-14T12:00:00Z").unwrap()
296 );
297 assert_eq_from_parts(period2);
298 }
299}