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}