Skip to main content

tor_checkable/
timed.rs

1//! Convenience implementation of a TimeBound object.
2
3use crate::{TimeBound, TimeValidityError};
4use itertools::chain;
5use std::ops::{Bound, Deref, RangeBounds};
6use web_time_compat as time;
7
8/// A `TimeBound` object that is valid for a specified range of time.
9///
10/// The range is given as an argument, as in `t1..t2`.
11///
12/// The range is always treated as inclusive.
13///
14/// **Non-invariant**: it is possible for the start to be after the end.
15/// In that case, it's simply never valid: either expired, or too soon, or both.
16///
17/// `TimeRangeBound<()>` aka `TimeRange` is sometimes used as a representation of a time range,
18/// for example, the return value from [`TimeBound::bounds`].
19///
20/// ```
21/// use web_time_compat::{SystemTime, SystemTimeExt, Duration};
22/// use tor_checkable::{TimeBound, TimeValidityError, timed::TimeRangeBound};
23///
24/// let now = SystemTime::get();
25/// let one_hour = Duration::new(3600, 0);
26///
27/// // This seven is only valid for another hour!
28/// let seven = TimeRangeBound::new(7_u32, ..now+one_hour);
29///
30/// assert_eq!(seven.if_valid_at(&now).unwrap(), 7);
31///
32/// // That consumed the previous seven. Try another one.
33/// let seven = TimeRangeBound::new(7_u32, ..now+one_hour);
34/// assert_eq!(seven.if_valid_at(&(now+2*one_hour)),
35///            Err(TimeValidityError::Expired(one_hour)));
36///
37/// ```
38#[derive(Debug, Clone)]
39#[cfg_attr(test, derive(Eq, PartialEq))]
40pub struct TimeRangeBound<T> {
41    /// The underlying object, which we only want to expose if it is
42    /// currently timely.
43    obj: T,
44    /// If present, when the object first became valid.
45    start: Option<time::SystemTime>,
46    /// If present, when the object will no longer be valid.
47    end: Option<time::SystemTime>,
48}
49
50/// Validity time range.
51///
52/// We use `TimeRangeBound<()>` to represent just a validity range.
53//
54// We could have a separate `TimeBounds` struct but it would have to have
55// many of the same constructors, accessors, etc.
56pub type TimeRange = TimeRangeBound<()>;
57
58/// Deprecated compatibility alias for [`TimeRangeBound`]
59#[deprecated = "use the new name, TimeRangeBound, instead"]
60pub type TimerangeBound<T> = TimeRangeBound<T>;
61
62/// Helper: convert a Bound to its underlying value, if any.
63///
64/// This helper discards information about whether the bound was
65/// inclusive or exclusive.  However, since SystemTime has sub-second
66/// precision, we really don't care about what happens when the
67/// nanoseconds are equal to exactly 0.
68fn unwrap_bound(b: Bound<&'_ time::SystemTime>) -> Option<time::SystemTime> {
69    match b {
70        Bound::Included(x) => Some(*x),
71        Bound::Excluded(x) => Some(*x),
72        _ => None,
73    }
74}
75
76impl<T> TimeRangeBound<T> {
77    /// Construct a new TimeRangeBound object from a given object and range.
78    ///
79    /// Note that we do not distinguish between inclusive and
80    /// exclusive bounds: `x..y` and `x..=y` are treated the same
81    /// here - as an inclusive range.
82    ///
83    /// Use `TimeRange::new_range` to create a `TimeRange` aka a `TimeRangeBound<()>`.
84    pub fn new<U>(obj: T, range: U) -> Self
85    where
86        U: RangeBounds<time::SystemTime>,
87    {
88        let start = unwrap_bound(range.start_bound());
89        let end = unwrap_bound(range.end_bound());
90        Self { obj, start, end }
91    }
92
93    /// Construct a new TimeRangeBound object from a given object, start time, and end time.
94    pub fn new_from_start_end(
95        obj: T,
96        start: Option<time::SystemTime>,
97        end: Option<time::SystemTime>,
98    ) -> Self {
99        Self { obj, start, end }
100    }
101
102    /// Adjust this time-range bound to tolerate an initial validity
103    /// time farther in the past.
104    #[must_use]
105    pub fn extend_start_bound(self, d: time::Duration) -> Self {
106        let start = match self.start {
107            Some(t) => t.checked_sub(d),
108            _ => None,
109        };
110        Self { start, ..self }
111    }
112    /// Adjust this time-range bound to tolerate an expiration time farther
113    /// in the future.
114    #[must_use]
115    pub fn extend_end_bound(self, d: time::Duration) -> Self {
116        let end = match self.end {
117            Some(t) => t.checked_add(d),
118            _ => None,
119        };
120        Self { end, ..self }
121    }
122
123    /// Deprecated alias for `extend_start_bound`
124    #[deprecated = "use extend_start_bound instead"]
125    #[must_use]
126    pub fn extend_pre_tolerance(self, d: time::Duration) -> Self {
127        self.extend_start_bound(d)
128    }
129    /// Deprecated alias for `extend_end_bound`
130    #[deprecated = "use extend_end_bound instead"]
131    #[must_use]
132    pub fn extend_tolerance(self, d: time::Duration) -> Self {
133        self.extend_end_bound(d)
134    }
135
136    /// Consume this [`TimeRangeBound`], and return a new one with the same
137    /// bounds, applying `f` to its protected value.
138    ///
139    /// The caller must ensure that `f` does not make any assumptions about the
140    /// timeliness of the protected value, or leak any of its contents in
141    /// an inappropriate way.
142    #[must_use]
143    pub fn dangerously_map<F, U>(self, f: F) -> TimeRangeBound<U>
144    where
145        F: FnOnce(T) -> U,
146    {
147        TimeRangeBound {
148            obj: f(self.obj),
149            start: self.start,
150            end: self.end,
151        }
152    }
153
154    /// Consume this TimeRangeBound, and return its underlying time bounds and
155    /// object.
156    ///
157    /// The caller takes responsibility for making sure that the bounds are
158    /// actually checked.
159    pub fn dangerously_into_parts(self) -> (T, TimeRange) {
160        let bounds = self.bounds();
161
162        (self.obj, bounds)
163    }
164
165    /// Return a reference to the inner object of this TimeRangeBound, without
166    /// checking the time interval.
167    ///
168    /// The caller takes responsibility for making sure that nothing is actually
169    /// done with the inner object that would rely on the bounds being correct, until
170    /// the bounds are (eventually) checked.
171    pub fn dangerously_peek(&self) -> &T {
172        &self.obj
173    }
174
175    /// Return a `TimeRangeBound` containing a reference
176    ///
177    /// This can be useful to call methods like `.check_valid_at`
178    /// without consuming the inner `T`.
179    pub fn as_ref(&self) -> TimeRangeBound<&T> {
180        TimeRangeBound {
181            obj: &self.obj,
182            start: self.start,
183            end: self.end,
184        }
185    }
186
187    /// Return a `TimeRangeBound` containing a reference to `T`'s `Deref`
188    pub fn as_deref(&self) -> TimeRangeBound<&T::Target>
189    where
190        T: Deref,
191    {
192        self.as_ref().dangerously_map(|t| &**t)
193    }
194
195    /// Return the underlying time bounds of this object.
196    pub fn bounds_start_end(&self) -> (Option<time::SystemTime>, Option<time::SystemTime>) {
197        (self.start, self.end)
198    }
199
200    /// Narrow the bounds of `self` to the overlap with `bounds`
201    ///
202    /// If the bounds conflict (ie, if the intersection is empty),
203    /// simply yields a `TimeRangeBound` that is never valid.
204    ///
205    /// (This is unlike `tor_basic_utils::rangebounds::RangeBoundsExt::intersect`
206    /// which *is* implemented for `TimeRange` via [`RangeBounds`]:
207    /// `intersect` insists on returning a well-formed range,
208    /// whereas `TimeRangeBound` can be empty if `start > end`.)
209    // (we can't make the ref to tor_basic_utils a doc link since that's not in scope!)
210    pub fn intersect_bounds(&mut self, bounds: TimeRange) {
211        self.start = chain!(self.start, bounds.start()).max();
212        self.end = chain!(self.end, bounds.end()).min();
213    }
214
215    /// Process multiple `TimeBound`s, intersecting their validity ranges
216    ///
217    /// Within `logic`, [`TimeBound::unwrap_with`] can be used,
218    /// for unwrapping [`TimeBound`]s.
219    ///
220    /// Those time bounds are accumulated within the [`TimeRangeBoundBuilder`],
221    /// and when `logic` returns, they are applied to its result.
222    ///
223    /// This allows multiple time-bound components of (a Tor protocol element)
224    /// to be conveniently processed into an overall return value.
225    ///
226    /// The API is intended to prevent accidentally forgetting to check
227    /// or process one of the time bounds; `TimeRangeBoundBuilder` is
228    /// an alternative to manual use of `dangerously_*` and `intersect`.
229    ///
230    /// # CORRECTNESS
231    ///
232    /// Everything that needs to be bound to the time range must be returned
233    /// only as part of the return value from `logic`.
234    ///
235    /// It is the caller's responsibility not to smuggle out
236    /// values whose validity time has not been checked
237    /// out via mutable captures in `logic`, global variables, etc.
238    ///
239    /// Likewise, if `logic` returns `Err`, this must mean that callers don't treat
240    /// the data as valid or successful.  I.e. `Error` must really be an error,
241    /// and not be used as a way to smuggle out potentially-out-of-time-range data.
242    ///
243    /// # Example
244    ///
245    /// ```
246    /// use humantime::parse_rfc3339;
247    /// use tor_checkable::{TimeBound as _, TimeRangeBound};
248    ///
249    /// // Fake document.  A real document would involve signature verification too.
250    /// struct Data {}
251    /// struct FakeDoc { data: Data, sig: TimeRangeBound<()>, }
252    /// impl FakeDoc {
253    ///     fn parse(_dummy: &str) -> TimeRangeBound<Self> {
254    ///         let t = |s| parse_rfc3339(s).unwrap();
255    ///         let sig = TimeRangeBound::new((), ..=t("2001-01-01T00:00:01Z"));
256    ///         let doc = FakeDoc { data: Data {}, sig };
257    ///         TimeRangeBound::new(doc, ..=t("2000-01-01T00:00:01Z"))
258    ///     }
259    /// }
260    ///
261    /// // Demo usage of TimeBoundRangeBuilder, in verification function
262    /// fn parse_verify(input: &str) -> Result<TimeRangeBound<Data>, ()> {
263    ///     let parsed = FakeDoc::parse(input); // real parser would be fallible
264    ///     TimeRangeBound::build_intersect(move |times| {
265    ///         let FakeDoc { data, sig } = parsed.unwrap_with(times);
266    ///         let _: () = sig.unwrap_with(times); // would verify signature too
267    ///         Ok(data)
268    ///     })
269    /// }
270    ///
271    /// assert_eq!(
272    ///     parse_verify("dummy").unwrap().bounds().end(),
273    ///     Some(parse_rfc3339("2000-01-01T00:00:01Z").unwrap()),
274    /// );
275    /// ```
276    pub fn build_intersect<Error, Logic>(logic: Logic) -> Result<Self, Error>
277    where
278        Logic: FnOnce(&mut TimeRangeBoundBuilder) -> Result<T, Error>,
279    {
280        let mut builder = TimeRangeBoundBuilder(TimeRange::new_range(..));
281        let output = logic(&mut builder)?;
282        Ok(builder.0.apply_to(output))
283    }
284}
285
286impl TimeRange {
287    /// Create a new `TimeRange` from a `std::ops::RangeBounds`
288    pub fn new_range<U>(range: U) -> Self
289    where
290        U: RangeBounds<time::SystemTime>,
291    {
292        Self::new((), range)
293    }
294
295    /// Applies this `TimeRange` to a value, protecting it
296    pub fn apply_to<T>(self, t: T) -> TimeRangeBound<T> {
297        TimeRangeBound::new(t, self.bounds())
298    }
299
300    /// Get the start of the validity period
301    ///
302    /// `None` means there is no start: the object has been valid forever.
303    ///
304    /// Provided only for `TimeRange`; to call on a general [`TimeRangeBound<T>`],
305    /// write `.bounds().start()`.
306    pub fn start(&self) -> Option<time::SystemTime> {
307        self.start
308    }
309
310    /// Get the end of the validity period
311    ///
312    /// `None` means there is no end: the object will been valid forever.
313    /// This is normally a mistake.
314    ///
315    /// Provided only for `TimeRange`; to call on a general [`TimeRangeBound<T>`],
316    /// write `.bounds().end()`.
317    //
318    // We could forbid the lack of an expiry time,
319    // but it would make everything much less consistent.
320    pub fn end(&self) -> Option<time::SystemTime> {
321        self.end
322    }
323}
324
325/// Accumulator used by `TimeRangeBounds::build_intersect`
326///
327/// Provided to the user's `logic` callback by [`TimeRangeBound::build_intersect`]
328///
329/// There is no other way to obtain a `TimeRangeBoundBuilder`.
330// ^ this property allows the API to prevent accidental drops of time bounds.
331pub struct TimeRangeBoundBuilder(TimeRange);
332
333impl TimeRangeBoundBuilder {
334    /// Handle a `TimeBound`, ensuring its validity range will be honoured
335    ///
336    /// This is equivalent to [`TimeBound::unwrap_with`],
337    /// which is normally more convenient.
338    ///
339    /// # CORRECTNESS
340    ///
341    /// See [`TimeBound::unwrap_with`] and [`TimeRangeBound::build_intersect`].
342    pub fn incorporate_unwrap<Component: TimeBound>(
343        &mut self,
344        component: Component,
345    ) -> Component::Inner {
346        self.intersect_bounds(component.bounds());
347        // Correctness: we include the component's bounds in `self`,
348        // so that when the whole `build` function returns, those bounds will be re-applied.
349        component.dangerously_assume_timely()
350    }
351
352    /// Narrow the bounds of `self` to the overlap with `bounds`
353    ///
354    /// Equivalent to `.as_mut_range().intersect_bounds()`.
355    pub fn intersect_bounds(&mut self, bounds: TimeRange) {
356        self.as_mut_range().intersect_bounds(bounds);
357    }
358
359    /// Mutably access the being-built time range.
360    ///
361    /// This range is the intersection of all the ranges
362    /// from calls to `incorporate_unwrap` and
363    /// `intersect_bounds`.
364    ///
365    /// # CORRECTNESS
366    ///
367    /// Normally it is only correct to narrow the range, not widen it.
368    /// Getting the time range right is the responsibility of the caller.
369    ///
370    /// Consider [`intersect_bounds`](TimeRangeBoundBuilder::intersect_bounds) instead.
371    pub fn as_mut_range(&mut self) -> &mut TimeRange {
372        &mut self.0
373    }
374}
375
376impl<T> RangeBounds<time::SystemTime> for TimeRangeBound<T> {
377    fn start_bound(&self) -> Bound<&time::SystemTime> {
378        self.start
379            .as_ref()
380            .map(Bound::Included)
381            .unwrap_or(Bound::Unbounded)
382    }
383
384    fn end_bound(&self) -> Bound<&time::SystemTime> {
385        self.end
386            .as_ref()
387            .map(Bound::Included)
388            .unwrap_or(Bound::Unbounded)
389    }
390}
391
392/// Implement `From<$R> for TimeRange` via `new_range`
393macro_rules! impl_from_range { { $R:ty } => {
394    impl From<$R> for TimeRange {
395        fn from(r: $R) -> TimeRange {
396            TimeRange::new_range(r)
397        }
398    }
399} }
400
401// We don't implement trivial-seeming `From`/`Into` conversions from non-inclusive ranges,
402// since strictly speaking we don't preserve the semantics.
403// They can still be converted manually with `new_range`.
404impl_from_range! { std::ops::RangeFrom<time::SystemTime> }
405impl_from_range! { std::ops::RangeFull }
406impl_from_range! { std::ops::RangeInclusive<time::SystemTime> }
407impl_from_range! { std::ops::RangeToInclusive<time::SystemTime> }
408
409impl<T> crate::TimeBound for TimeRangeBound<T> {
410    type Inner = T;
411
412    fn bounds(&self) -> TimeRange {
413        TimeRangeBound {
414            obj: (),
415            start: self.start,
416            end: self.end,
417        }
418    }
419
420    fn check_valid_at(&self, t: &time::SystemTime) -> Result<(), TimeValidityError> {
421        use crate::TimeValidityError;
422        if let Some(start) = self.start {
423            if let Ok(d) = start.duration_since(*t)
424                && d > time::Duration::ZERO
425            {
426                return Err(TimeValidityError::NotYetValid(d));
427            }
428        }
429
430        if let Some(end) = self.end {
431            if let Ok(d) = t.duration_since(end)
432                && d > time::Duration::ZERO
433            {
434                return Err(TimeValidityError::Expired(d));
435            }
436        }
437
438        Ok(())
439    }
440
441    fn dangerously_assume_timely(self) -> T {
442        self.obj
443    }
444}
445
446#[cfg(test)]
447mod test {
448    // @@ begin test lint list maintained by maint/add_warning @@
449    #![allow(clippy::bool_assert_comparison)]
450    #![allow(clippy::clone_on_copy)]
451    #![allow(clippy::dbg_macro)]
452    #![allow(clippy::mixed_attributes_style)]
453    #![allow(clippy::print_stderr)]
454    #![allow(clippy::print_stdout)]
455    #![allow(clippy::single_char_pattern)]
456    #![allow(clippy::unwrap_used)]
457    #![allow(clippy::unchecked_time_subtraction)]
458    #![allow(clippy::useless_vec)]
459    #![allow(clippy::needless_pass_by_value)]
460    #![allow(clippy::string_slice)] // See arti#2571
461    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
462    use super::*;
463    use crate::{TimeBound, TimeValidityError};
464    use humantime::parse_rfc3339;
465    use tor_basic_utils::rangebounds::RangeBoundsExt as _;
466    use web_time_compat::{Duration, SystemTime, SystemTimeExt};
467
468    #[test]
469    fn test_bounds() {
470        #![allow(clippy::unwrap_used)]
471        let one_day = Duration::new(86400, 0);
472        let mixminion_v0_0_1 = parse_rfc3339("2003-01-07T00:00:00Z").unwrap();
473        let tor_v0_0_2pre13 = parse_rfc3339("2003-10-19T00:00:00Z").unwrap();
474        let cussed_nougat = parse_rfc3339("2008-08-02T00:00:00Z").unwrap();
475        let tor_v0_4_4_5 = parse_rfc3339("2020-09-15T00:00:00Z").unwrap();
476        let today = parse_rfc3339("2020-09-22T00:00:00Z").unwrap();
477
478        let tr = TimeRangeBound::new((), ..tor_v0_4_4_5);
479        assert_eq!(tr.start, None);
480        assert_eq!(tr.end, Some(tor_v0_4_4_5));
481        assert!(tr.check_valid_at(&mixminion_v0_0_1).is_ok());
482        assert!(tr.check_valid_at(&tor_v0_0_2pre13).is_ok());
483        assert_eq!(
484            tr.check_valid_at(&today),
485            Err(TimeValidityError::Expired(7 * one_day))
486        );
487
488        let tr = TimeRangeBound::new((), tor_v0_0_2pre13..=tor_v0_4_4_5);
489        assert_eq!(tr.start, Some(tor_v0_0_2pre13));
490        assert_eq!(tr.end, Some(tor_v0_4_4_5));
491        assert_eq!(
492            tr.check_valid_at(&mixminion_v0_0_1),
493            Err(TimeValidityError::NotYetValid(285 * one_day))
494        );
495        assert!(tr.check_valid_at(&cussed_nougat).is_ok());
496        assert_eq!(
497            tr.check_valid_at(&today),
498            Err(TimeValidityError::Expired(7 * one_day))
499        );
500
501        let tr = tr
502            .extend_start_bound(5 * one_day)
503            .extend_end_bound(2 * one_day);
504        assert_eq!(tr.start, Some(tor_v0_0_2pre13 - 5 * one_day));
505        assert_eq!(tr.end, Some(tor_v0_4_4_5 + 2 * one_day));
506
507        let tr = tr
508            .extend_start_bound(Duration::MAX)
509            .extend_end_bound(Duration::MAX);
510        assert_eq!(tr.start, None);
511        assert_eq!(tr.end, None);
512
513        let tr = TimeRangeBound::new((), tor_v0_4_4_5..);
514        assert_eq!(tr.start, Some(tor_v0_4_4_5));
515        assert_eq!(tr.end, None);
516        assert_eq!(
517            tr.check_valid_at(&cussed_nougat),
518            Err(TimeValidityError::NotYetValid(4427 * one_day))
519        );
520        assert!(tr.check_valid_at(&today).is_ok());
521    }
522
523    #[test]
524    fn test_checking() {
525        // West and East Germany reunified
526        let de = humantime::parse_rfc3339("1990-10-03T00:00:00Z").unwrap();
527        // Czechoslovakia separates into Czech Republic (Bohemia) & Slovakia
528        let cz_sk = humantime::parse_rfc3339("1993-01-01T00:00:00Z").unwrap();
529        // European Union created
530        let eu = humantime::parse_rfc3339("1993-11-01T00:00:00Z").unwrap();
531        // South Africa holds first free and fair elections
532        let za = humantime::parse_rfc3339("1994-04-27T00:00:00Z").unwrap();
533
534        // check_valid_at
535        let tr = TimeRangeBound::new("Hello world", cz_sk..eu);
536        assert!(tr.if_valid_at(&za).is_err());
537
538        let tr = TimeRangeBound::new("Hello world", cz_sk..za);
539        assert_eq!(tr.if_valid_at(&eu), Ok("Hello world"));
540
541        // check_valid_now
542        #[allow(clippy::disallowed_methods)]
543        {
544            let tr = TimeRangeBound::new("hello world", de..);
545            assert_eq!(tr.if_valid_now(), Ok("hello world"));
546
547            let tr = TimeRangeBound::new("hello world", ..za);
548            assert!(tr.if_valid_now().is_err());
549        }
550
551        // Now try check_valid_at_opt() api
552        let tr = TimeRangeBound::new("hello world", de..);
553        #[allow(deprecated)]
554        {
555            assert_eq!(tr.check_valid_at_opt(None), Ok("hello world"));
556            let tr = TimeRangeBound::new("hello world", de..);
557            assert_eq!(
558                tr.check_valid_at_opt(Some(SystemTime::get())),
559                Ok("hello world")
560            );
561            let tr = TimeRangeBound::new("hello world", ..za);
562            assert!(tr.check_valid_at_opt(None).is_err());
563        }
564
565        // edge cases
566        let tr = TimeRangeBound::new("Hello world", de..eu);
567        let nano = Duration::from_nanos(1);
568        assert!(tr.check_valid_at(&(de - nano)).is_err());
569        assert!(tr.check_valid_at(&de).is_ok());
570        assert!(tr.check_valid_at(&(de + nano)).is_ok());
571        assert!(tr.check_valid_at(&(eu - nano)).is_ok());
572        assert!(tr.check_valid_at(&eu).is_ok());
573        assert!(tr.check_valid_at(&(eu + nano)).is_err());
574    }
575
576    #[test]
577    fn test_dangerous() {
578        let t1 = SystemTime::get();
579        let t2 = t1 + Duration::from_secs(60 * 525600);
580        let tr = TimeRangeBound::new("cups of coffee", t1..=t2);
581
582        assert_eq!(tr.dangerously_peek(), &"cups of coffee");
583
584        let (a, b) = tr.dangerously_into_parts();
585        assert_eq!(a, "cups of coffee");
586        assert_eq!(b.start(), Some(t1));
587        assert_eq!(b.end(), Some(t2));
588    }
589
590    #[test]
591    fn test_map() {
592        let t1 = SystemTime::get();
593        let min = Duration::from_secs(60);
594
595        let tb = TimeRangeBound::new(17_u32, t1..t1 + 5 * min);
596        let tb = tb.dangerously_map(|v| v * v);
597        assert!(tb.check_valid_at(&(t1 + 1 * min)).is_ok());
598        assert!(tb.check_valid_at(&(t1 + 10 * min)).is_err());
599
600        let val = tb.if_valid_at(&(t1 + 1 * min)).unwrap();
601        assert_eq!(val, 289);
602    }
603
604    #[test]
605    fn test_as_ref() {
606        let t1 = SystemTime::get();
607        let min = Duration::from_secs(60);
608
609        let tb1: TimeRangeBound<String> = TimeRangeBound::new("hi".into(), t1..t1 + 5 * min);
610        let tb2: TimeRangeBound<&String> = tb1.as_ref();
611        let tb3: TimeRangeBound<&str> = tb1.as_deref();
612        assert_eq!(tb1, tb2.dangerously_map(|s| s.clone()));
613        assert_eq!(tb1, tb3.dangerously_map(|s| s.to_owned()));
614    }
615
616    #[test]
617    fn test_intersect_bounds() {
618        // we use tor-basic-utils's intersect as a reference implementation
619        let bounds = || {
620            chain!(
621                [None],
622                (0..=10)
623                    .map(|days| {
624                        parse_rfc3339("2000-01-01T00:00:01Z").unwrap()
625                            + Duration::from_secs(days * 86400)
626                    })
627                    .map(Some),
628            )
629        };
630
631        for a_start in bounds() {
632            for a_end in bounds() {
633                for b_start in bounds() {
634                    for b_end in bounds() {
635                        let mut a = TimeRange::new_from_start_end((), a_start, a_end);
636                        let b = TimeRange::new_from_start_end((), b_start, b_end);
637                        let exp = a.intersect(&b).map(TimeRange::new_range);
638                        a.intersect_bounds(b);
639                        if let Some(exp) = exp {
640                            assert_eq!(a, exp);
641                        } else {
642                            assert!(a.start() > a.end());
643                        }
644                    }
645                }
646            }
647        }
648    }
649}