1
//! Manipulate time periods (as used in the onion service system)
2

            
3
use std::{
4
    fmt::Display,
5
    time::{Duration, SystemTime},
6
};
7

            
8
use humantime::format_rfc3339_seconds;
9
use tor_units::IntegerMinutes;
10

            
11
use serde::{Deserialize, Serialize};
12

            
13
/// 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)]
24
pub struct TimePeriod {
25
    /// Index of the time periods that have passed since the unix epoch.
26
    pub(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.
30
    pub(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.
35
    pub(crate) epoch_offset_in_sec: u32,
36
}
37

            
38
/// Two [`TimePeriod`]s are ordered with respect to one another if they have the
39
/// same interval length and offset.
40
impl PartialOrd for TimePeriod {
41
4
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
42
4
        if self.length == other.length && self.epoch_offset_in_sec == other.epoch_offset_in_sec {
43
4
            Some(self.interval_num.cmp(&other.interval_num))
44
        } else {
45
            None
46
        }
47
4
    }
48
}
49

            
50
impl Display for TimePeriod {
51
118
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52
118
        write!(f, "#{} ", self.interval_num())?;
53
118
        match self.range() {
54
118
            Ok(r) => {
55
118
                let mins = self.length().as_minutes();
56
118
                write!(
57
118
                    f,
58
118
                    "{}..+{}:{:02}",
59
118
                    format_rfc3339_seconds(r.start),
60
118
                    mins / 60,
61
118
                    mins % 60
62
118
                )
63
            }
64
            Err(_) => write!(f, "overflow! {self:?}"),
65
        }
66
118
    }
67
}
68

            
69
impl 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.
80
21899
    pub fn new(
81
21899
        length: Duration,
82
21899
        when: SystemTime,
83
21899
        epoch_offset: Duration,
84
21899
    ) -> Result<Self, TimePeriodError> {
85
        // The algorithm here is specified in rend-spec-v3 section 2.2.1
86
21899
        let length_in_sec =
87
21899
            u32::try_from(length.as_secs()).map_err(|_| TimePeriodError::IntervalInvalid)?;
88
21899
        if length_in_sec % 60 != 0 || length.subsec_nanos() != 0 {
89
            return Err(TimePeriodError::IntervalInvalid);
90
21899
        }
91
21899
        let length_in_minutes = length_in_sec / 60;
92
21899
        let length = IntegerMinutes::new(length_in_minutes);
93
21899
        let epoch_offset_in_sec =
94
21899
            u32::try_from(epoch_offset.as_secs()).map_err(|_| TimePeriodError::OffsetInvalid)?;
95
21899
        let interval_num = when
96
21899
            .duration_since(SystemTime::UNIX_EPOCH + epoch_offset)
97
21899
            .map_err(|_| TimePeriodError::OutOfRange)?
98
21899
            .as_secs()
99
21899
            / u64::from(length_in_sec);
100
21899
        Ok(TimePeriod {
101
21899
            interval_num,
102
21899
            length,
103
21899
            epoch_offset_in_sec,
104
21899
        })
105
21899
    }
106

            
107
    /// 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.
116
714
    pub fn from_parts(length: u32, interval_num: u64, epoch_offset_in_sec: u32) -> Self {
117
714
        let length_in_sec = length * 60;
118
714

            
119
714
        Self {
120
714
            interval_num,
121
714
            length: length.into(),
122
714
            epoch_offset_in_sec,
123
714
        }
124
714
    }
125

            
126
    /// Return the time period after this one.
127
    ///
128
    /// Return None if this is the last representable time period.
129
21067
    pub fn next(&self) -> Option<Self> {
130
21067
        Some(TimePeriod {
131
21067
            interval_num: self.interval_num.checked_add(1)?,
132
            ..*self
133
        })
134
21067
    }
135
    /// Return the time period before this one.
136
    ///
137
    /// Return None if this is the first representable time period.
138
21067
    pub fn prev(&self) -> Option<Self> {
139
21067
        Some(TimePeriod {
140
21067
            interval_num: self.interval_num.checked_sub(1)?,
141
            ..*self
142
        })
143
21067
    }
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`.
150
10
    pub fn contains(&self, when: SystemTime) -> bool {
151
10
        match self.range() {
152
10
            Ok(r) => r.contains(&when),
153
            Err(_) => false,
154
        }
155
10
    }
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`.
161
83853
    pub fn range(&self) -> Result<std::ops::Range<SystemTime>, TimePeriodError> {
162
83853
        (|| {
163
83853
            let length_in_sec = u64::from(self.length.as_minutes()) * 60;
164
83853
            let start_sec = length_in_sec.checked_mul(self.interval_num)?;
165
83853
            let end_sec = start_sec.checked_add(length_in_sec)?;
166
83853
            let epoch_offset = Duration::new(self.epoch_offset_in_sec.into(), 0);
167
83853
            let start = (SystemTime::UNIX_EPOCH + epoch_offset)
168
83853
                .checked_add(Duration::from_secs(start_sec))?;
169
83853
            let end = (SystemTime::UNIX_EPOCH + epoch_offset)
170
83853
                .checked_add(Duration::from_secs(end_sec))?;
171
83853
            Some(start..end)
172
83853
        })()
173
83853
        .ok_or(TimePeriodError::OutOfRange)
174
83853
    }
175

            
176
    /// 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.
180
120838
    pub fn interval_num(&self) -> u64 {
181
120838
        self.interval_num
182
120838
    }
183

            
184
    /// 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.
188
120661
    pub fn length(&self) -> IntegerMinutes<u32> {
189
120661
        self.length
190
120661
    }
191

            
192
    /// 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`.
196
360
    pub fn epoch_offset_in_sec(&self) -> u32 {
197
360
        self.epoch_offset_in_sec
198
360
    }
199
}
200

            
201
/// An error that occurs when creating or manipulating a [`TimePeriod`]
202
#[derive(Clone, Debug, thiserror::Error)]
203
#[non_exhaustive]
204
pub 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")]
208
    OutOfRange,
209

            
210
    /// 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")]
216
    IntervalInvalid,
217

            
218
    /// 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")]
222
    OffsetInvalid,
223
}
224

            
225
#[cfg(test)]
226
mod 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 @@ -->
240

            
241
    use super::*;
242
    use humantime::{parse_duration, parse_rfc3339};
243

            
244
    /// Check reconstructing `period` from parts produces an identical `TimePeriod`.
245
    fn assert_eq_from_parts(period: TimePeriod) {
246
        assert_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
    }
255

            
256
    #[test]
257
    fn check_testvec() {
258
        // Test case from C tor, taken from rend-spec.
259
        let offset = Duration::new(12 * 60 * 60, 0);
260
        let time = parse_rfc3339("2016-04-13T11:00:00Z").unwrap();
261
        let one_day = parse_duration("1day").unwrap();
262
        let period = TimePeriod::new(one_day, time, offset).unwrap();
263
        assert_eq!(period.interval_num, 16903);
264
        assert!(period.contains(time));
265
        assert_eq_from_parts(period);
266

            
267
        let time = parse_rfc3339("2016-04-13T11:59:59Z").unwrap();
268
        let period = TimePeriod::new(one_day, time, offset).unwrap();
269
        assert_eq!(period.interval_num, 16903); // still the same.
270
        assert!(period.contains(time));
271
        assert_eq_from_parts(period);
272

            
273
        assert_eq!(period.prev().unwrap().interval_num, 16902);
274
        assert_eq!(period.next().unwrap().interval_num, 16904);
275

            
276
        let time2 = parse_rfc3339("2016-04-13T12:00:00Z").unwrap();
277
        let period2 = TimePeriod::new(one_day, time2, offset).unwrap();
278
        assert_eq!(period2.interval_num, 16904);
279
        assert!(period < period2);
280
        assert!(period2 > period);
281
        assert_eq!(period.next().unwrap(), period2);
282
        assert_eq!(period2.prev().unwrap(), period);
283
        assert!(period2.contains(time2));
284
        assert!(!period2.contains(time));
285
        assert!(!period.contains(time2));
286

            
287
        assert_eq!(
288
            period.range().unwrap(),
289
            parse_rfc3339("2016-04-12T12:00:00Z").unwrap()
290
                ..parse_rfc3339("2016-04-13T12:00:00Z").unwrap()
291
        );
292
        assert_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
}