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