Skip to main content

tor_checkable/
timed.rs

1//! Convenience implementation of a TimeBound object.
2
3use std::ops::{Bound, Deref, RangeBounds};
4use web_time_compat as time;
5
6/// A TimeBound object that is valid for a specified range of time.
7///
8/// The range is given as an argument, as in `t1..t2`.
9///
10/// The range is always treated as inclusive.
11///
12/// ```
13/// use web_time_compat::{SystemTime, SystemTimeExt, Duration};
14/// use tor_checkable::{Timebound, TimeValidityError, timed::TimerangeBound};
15///
16/// let now = SystemTime::get();
17/// let one_hour = Duration::new(3600, 0);
18///
19/// // This seven is only valid for another hour!
20/// let seven = TimerangeBound::new(7_u32, ..now+one_hour);
21///
22/// assert_eq!(seven.check_valid_at(&now).unwrap(), 7);
23///
24/// // That consumed the previous seven. Try another one.
25/// let seven = TimerangeBound::new(7_u32, ..now+one_hour);
26/// assert_eq!(seven.check_valid_at(&(now+2*one_hour)),
27///            Err(TimeValidityError::Expired(one_hour)));
28///
29/// ```
30#[derive(Debug, Clone)]
31#[cfg_attr(test, derive(Eq, PartialEq))]
32pub struct TimerangeBound<T> {
33    /// The underlying object, which we only want to expose if it is
34    /// currently timely.
35    obj: T,
36    /// If present, when the object first became valid.
37    start: Option<time::SystemTime>,
38    /// If present, when the object will no longer be valid.
39    end: Option<time::SystemTime>,
40}
41
42/// Helper: convert a Bound to its underlying value, if any.
43///
44/// This helper discards information about whether the bound was
45/// inclusive or exclusive.  However, since SystemTime has sub-second
46/// precision, we really don't care about what happens when the
47/// nanoseconds are equal to exactly 0.
48fn unwrap_bound(b: Bound<&'_ time::SystemTime>) -> Option<time::SystemTime> {
49    match b {
50        Bound::Included(x) => Some(*x),
51        Bound::Excluded(x) => Some(*x),
52        _ => None,
53    }
54}
55
56impl<T> TimerangeBound<T> {
57    /// Construct a new TimerangeBound object from a given object and range.
58    ///
59    /// Note that we do not distinguish between inclusive and
60    /// exclusive bounds: `x..y` and `x..=y` are treated the same
61    /// here - as an inclusive range.
62    pub fn new<U>(obj: T, range: U) -> Self
63    where
64        U: RangeBounds<time::SystemTime>,
65    {
66        let start = unwrap_bound(range.start_bound());
67        let end = unwrap_bound(range.end_bound());
68        Self { obj, start, end }
69    }
70
71    /// Construct a new TimerangeBound object from a given object, start time, and end time.
72    pub fn new_from_start_end(
73        obj: T,
74        start: Option<time::SystemTime>,
75        end: Option<time::SystemTime>,
76    ) -> Self {
77        Self { obj, start, end }
78    }
79
80    /// Adjust this time-range bound to tolerate an expiration time farther
81    /// in the future.
82    #[must_use]
83    pub fn extend_tolerance(self, d: time::Duration) -> Self {
84        let end = match self.end {
85            Some(t) => t.checked_add(d),
86            _ => None,
87        };
88        Self { end, ..self }
89    }
90    /// Adjust this time-range bound to tolerate an initial validity
91    /// time farther in the past.
92    #[must_use]
93    pub fn extend_pre_tolerance(self, d: time::Duration) -> Self {
94        let start = match self.start {
95            Some(t) => t.checked_sub(d),
96            _ => None,
97        };
98        Self { start, ..self }
99    }
100    /// Consume this [`TimerangeBound`], and return a new one with the same
101    /// bounds, applying `f` to its protected value.
102    ///
103    /// The caller must ensure that `f` does not make any assumptions about the
104    /// timeliness of the protected value, or leak any of its contents in
105    /// an inappropriate way.
106    #[must_use]
107    pub fn dangerously_map<F, U>(self, f: F) -> TimerangeBound<U>
108    where
109        F: FnOnce(T) -> U,
110    {
111        TimerangeBound {
112            obj: f(self.obj),
113            start: self.start,
114            end: self.end,
115        }
116    }
117
118    /// Consume this TimeRangeBound, and return its underlying time bounds and
119    /// object.
120    ///
121    /// The caller takes responsibility for making sure that the bounds are
122    /// actually checked.
123    pub fn dangerously_into_parts(
124        self,
125    ) -> (T, (Option<time::SystemTime>, Option<time::SystemTime>)) {
126        let bounds = self.bounds();
127
128        (self.obj, bounds)
129    }
130
131    /// Return a reference to the inner object of this TimeRangeBound, without
132    /// checking the time interval.
133    ///
134    /// The caller takes responsibility for making sure that nothing is actually
135    /// done with the inner object that would rely on the bounds being correct, until
136    /// the bounds are (eventually) checked.
137    pub fn dangerously_peek(&self) -> &T {
138        &self.obj
139    }
140
141    /// Return a `TimerangeBound` containing a reference
142    ///
143    /// This can be useful to call methods like `.check_valid_at`
144    /// without consuming the inner `T`.
145    pub fn as_ref(&self) -> TimerangeBound<&T> {
146        TimerangeBound {
147            obj: &self.obj,
148            start: self.start,
149            end: self.end,
150        }
151    }
152
153    /// Return a `TimerangeBound` containing a reference to `T`'s `Deref`
154    pub fn as_deref(&self) -> TimerangeBound<&T::Target>
155    where
156        T: Deref,
157    {
158        self.as_ref().dangerously_map(|t| &**t)
159    }
160
161    /// Return the underlying time bounds of this object.
162    pub fn bounds(&self) -> (Option<time::SystemTime>, Option<time::SystemTime>) {
163        (self.start, self.end)
164    }
165}
166
167impl<T> RangeBounds<time::SystemTime> for TimerangeBound<T> {
168    fn start_bound(&self) -> Bound<&time::SystemTime> {
169        self.start
170            .as_ref()
171            .map(Bound::Included)
172            .unwrap_or(Bound::Unbounded)
173    }
174
175    fn end_bound(&self) -> Bound<&time::SystemTime> {
176        self.end
177            .as_ref()
178            .map(Bound::Included)
179            .unwrap_or(Bound::Unbounded)
180    }
181}
182
183impl<T> crate::Timebound<T> for TimerangeBound<T> {
184    type Error = crate::TimeValidityError;
185
186    fn is_valid_at(&self, t: &time::SystemTime) -> Result<(), Self::Error> {
187        use crate::TimeValidityError;
188        if let Some(start) = self.start {
189            if let Ok(d) = start.duration_since(*t)
190                && d > time::Duration::ZERO
191            {
192                return Err(TimeValidityError::NotYetValid(d));
193            }
194        }
195
196        if let Some(end) = self.end {
197            if let Ok(d) = t.duration_since(end)
198                && d > time::Duration::ZERO
199            {
200                return Err(TimeValidityError::Expired(d));
201            }
202        }
203
204        Ok(())
205    }
206
207    fn dangerously_assume_timely(self) -> T {
208        self.obj
209    }
210}
211
212#[cfg(test)]
213mod test {
214    // @@ begin test lint list maintained by maint/add_warning @@
215    #![allow(clippy::bool_assert_comparison)]
216    #![allow(clippy::clone_on_copy)]
217    #![allow(clippy::dbg_macro)]
218    #![allow(clippy::mixed_attributes_style)]
219    #![allow(clippy::print_stderr)]
220    #![allow(clippy::print_stdout)]
221    #![allow(clippy::single_char_pattern)]
222    #![allow(clippy::unwrap_used)]
223    #![allow(clippy::unchecked_time_subtraction)]
224    #![allow(clippy::useless_vec)]
225    #![allow(clippy::needless_pass_by_value)]
226    #![allow(clippy::string_slice)] // See arti#2571
227    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
228    use super::*;
229    use crate::{TimeValidityError, Timebound};
230    use humantime::parse_rfc3339;
231    use web_time_compat::{Duration, SystemTime, SystemTimeExt};
232
233    #[test]
234    fn test_bounds() {
235        #![allow(clippy::unwrap_used)]
236        let one_day = Duration::new(86400, 0);
237        let mixminion_v0_0_1 = parse_rfc3339("2003-01-07T00:00:00Z").unwrap();
238        let tor_v0_0_2pre13 = parse_rfc3339("2003-10-19T00:00:00Z").unwrap();
239        let cussed_nougat = parse_rfc3339("2008-08-02T00:00:00Z").unwrap();
240        let tor_v0_4_4_5 = parse_rfc3339("2020-09-15T00:00:00Z").unwrap();
241        let today = parse_rfc3339("2020-09-22T00:00:00Z").unwrap();
242
243        let tr = TimerangeBound::new((), ..tor_v0_4_4_5);
244        assert_eq!(tr.start, None);
245        assert_eq!(tr.end, Some(tor_v0_4_4_5));
246        assert!(tr.is_valid_at(&mixminion_v0_0_1).is_ok());
247        assert!(tr.is_valid_at(&tor_v0_0_2pre13).is_ok());
248        assert_eq!(
249            tr.is_valid_at(&today),
250            Err(TimeValidityError::Expired(7 * one_day))
251        );
252
253        let tr = TimerangeBound::new((), tor_v0_0_2pre13..=tor_v0_4_4_5);
254        assert_eq!(tr.start, Some(tor_v0_0_2pre13));
255        assert_eq!(tr.end, Some(tor_v0_4_4_5));
256        assert_eq!(
257            tr.is_valid_at(&mixminion_v0_0_1),
258            Err(TimeValidityError::NotYetValid(285 * one_day))
259        );
260        assert!(tr.is_valid_at(&cussed_nougat).is_ok());
261        assert_eq!(
262            tr.is_valid_at(&today),
263            Err(TimeValidityError::Expired(7 * one_day))
264        );
265
266        let tr = tr
267            .extend_pre_tolerance(5 * one_day)
268            .extend_tolerance(2 * one_day);
269        assert_eq!(tr.start, Some(tor_v0_0_2pre13 - 5 * one_day));
270        assert_eq!(tr.end, Some(tor_v0_4_4_5 + 2 * one_day));
271
272        let tr = tr
273            .extend_pre_tolerance(Duration::MAX)
274            .extend_tolerance(Duration::MAX);
275        assert_eq!(tr.start, None);
276        assert_eq!(tr.end, None);
277
278        let tr = TimerangeBound::new((), tor_v0_4_4_5..);
279        assert_eq!(tr.start, Some(tor_v0_4_4_5));
280        assert_eq!(tr.end, None);
281        assert_eq!(
282            tr.is_valid_at(&cussed_nougat),
283            Err(TimeValidityError::NotYetValid(4427 * one_day))
284        );
285        assert!(tr.is_valid_at(&today).is_ok());
286    }
287
288    #[test]
289    fn test_checking() {
290        // West and East Germany reunified
291        let de = humantime::parse_rfc3339("1990-10-03T00:00:00Z").unwrap();
292        // Czechoslovakia separates into Czech Republic (Bohemia) & Slovakia
293        let cz_sk = humantime::parse_rfc3339("1993-01-01T00:00:00Z").unwrap();
294        // European Union created
295        let eu = humantime::parse_rfc3339("1993-11-01T00:00:00Z").unwrap();
296        // South Africa holds first free and fair elections
297        let za = humantime::parse_rfc3339("1994-04-27T00:00:00Z").unwrap();
298
299        // check_valid_at
300        let tr = TimerangeBound::new("Hello world", cz_sk..eu);
301        assert!(tr.check_valid_at(&za).is_err());
302
303        let tr = TimerangeBound::new("Hello world", cz_sk..za);
304        assert_eq!(tr.check_valid_at(&eu), Ok("Hello world"));
305
306        // check_valid_now
307        let tr = TimerangeBound::new("hello world", de..);
308        assert_eq!(tr.check_valid_now(), Ok("hello world"));
309
310        let tr = TimerangeBound::new("hello world", ..za);
311        assert!(tr.check_valid_now().is_err());
312
313        // Now try check_valid_at_opt() api
314        let tr = TimerangeBound::new("hello world", de..);
315        assert_eq!(tr.check_valid_at_opt(None), Ok("hello world"));
316        let tr = TimerangeBound::new("hello world", de..);
317        assert_eq!(
318            tr.check_valid_at_opt(Some(SystemTime::get())),
319            Ok("hello world")
320        );
321        let tr = TimerangeBound::new("hello world", ..za);
322        assert!(tr.check_valid_at_opt(None).is_err());
323
324        // edge cases
325        let tr = TimerangeBound::new("Hello world", de..eu);
326        let nano = Duration::from_nanos(1);
327        assert!(tr.is_valid_at(&(de - nano)).is_err());
328        assert!(tr.is_valid_at(&de).is_ok());
329        assert!(tr.is_valid_at(&(de + nano)).is_ok());
330        assert!(tr.is_valid_at(&(eu - nano)).is_ok());
331        assert!(tr.is_valid_at(&eu).is_ok());
332        assert!(tr.is_valid_at(&(eu + nano)).is_err());
333    }
334
335    #[test]
336    fn test_dangerous() {
337        let t1 = SystemTime::get();
338        let t2 = t1 + Duration::from_secs(60 * 525600);
339        let tr = TimerangeBound::new("cups of coffee", t1..=t2);
340
341        assert_eq!(tr.dangerously_peek(), &"cups of coffee");
342
343        let (a, b) = tr.dangerously_into_parts();
344        assert_eq!(a, "cups of coffee");
345        assert_eq!(b.0, Some(t1));
346        assert_eq!(b.1, Some(t2));
347    }
348
349    #[test]
350    fn test_map() {
351        let t1 = SystemTime::get();
352        let min = Duration::from_secs(60);
353
354        let tb = TimerangeBound::new(17_u32, t1..t1 + 5 * min);
355        let tb = tb.dangerously_map(|v| v * v);
356        assert!(tb.is_valid_at(&(t1 + 1 * min)).is_ok());
357        assert!(tb.is_valid_at(&(t1 + 10 * min)).is_err());
358
359        let val = tb.check_valid_at(&(t1 + 1 * min)).unwrap();
360        assert_eq!(val, 289);
361    }
362
363    #[test]
364    fn test_as_ref() {
365        let t1 = SystemTime::get();
366        let min = Duration::from_secs(60);
367
368        let tb1: TimerangeBound<String> = TimerangeBound::new("hi".into(), t1..t1 + 5 * min);
369        let tb2: TimerangeBound<&String> = tb1.as_ref();
370        let tb3: TimerangeBound<&str> = tb1.as_deref();
371        assert_eq!(tb1, tb2.dangerously_map(|s| s.clone()));
372        assert_eq!(tb1, tb3.dangerously_map(|s| s.to_owned()));
373    }
374}