From 88ea5032f4588da7ebafdc262f5db64e561d76af Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Thu, 27 Apr 2023 12:03:48 +0100 Subject: [PATCH 01/14] hsclient: Build cached descriptor TimerangeBounds from descriptor lifetime. This makes `descriptor_ensure` refetch the descriptor if it has been cached for longer than `descriptor-lifetime` minutes. Signed-off-by: Gabriela Moldovan --- crates/tor-checkable/src/timed.rs | 18 +++++++---- crates/tor-hsclient/src/connect.rs | 50 +++++++++++++++++++---------- crates/tor-netdoc/src/doc/hsdesc.rs | 7 ++++ 3 files changed, 51 insertions(+), 24 deletions(-) diff --git a/crates/tor-checkable/src/timed.rs b/crates/tor-checkable/src/timed.rs index aed794541..d18051264 100644 --- a/crates/tor-checkable/src/timed.rs +++ b/crates/tor-checkable/src/timed.rs @@ -105,13 +105,9 @@ impl TimerangeBound { /// The caller takes responsibility for making sure that the bounds are /// actually checked. pub fn dangerously_into_parts(self) -> (T, (Bound, Bound)) { - ( - self.obj, - ( - self.start.map(Bound::Included).unwrap_or(Bound::Unbounded), - self.end.map(Bound::Included).unwrap_or(Bound::Unbounded), - ), - ) + let bounds = self.bounds(); + + (self.obj, bounds) } /// Return a reference to the inner object of this TimeRangeBound, without @@ -143,6 +139,14 @@ impl TimerangeBound { { self.as_ref().dangerously_map(|t| &**t) } + + /// Return the underlying time bounds of this object. + pub fn bounds(&self) -> (Bound, Bound) { + ( + self.start.map(Bound::Included).unwrap_or(Bound::Unbounded), + self.end.map(Bound::Included).unwrap_or(Bound::Unbounded), + ) + } } impl crate::Timebound for TimerangeBound { diff --git a/crates/tor-hsclient/src/connect.rs b/crates/tor-hsclient/src/connect.rs index c5b1fda31..0cc7f16fa 100644 --- a/crates/tor-hsclient/src/connect.rs +++ b/crates/tor-hsclient/src/connect.rs @@ -339,10 +339,18 @@ impl<'c, 'd, R: Runtime, M: MocksForConnect> Context<'c, 'd, R, M> { ) .map_err(DescriptorErrorDetail::from)?; - let unbounded_todo = Bound::Unbounded::; // TODO HS remove - let bound = (unbounded_todo, unbounded_todo); + // Cache the descriptor for the amount of time specified in the descriptor-lifetime field. + let upper_bound = Bound::Included( + now + Duration::try_from(hsdesc.lifetime()).map_err(|e| { + into_internal!( + "bad descriptor-lifetime (this descriptor should have failed to parse)" + )(e) + })?, + ); - Ok((hsdesc, bound)) + let bounds = (Bound::Unbounded, upper_bound); + + Ok((hsdesc, bounds)) } } @@ -571,21 +579,20 @@ mod test { secret_keys_builder.ks_hsc_desc_enc(sk); let secret_keys = secret_keys_builder.build().unwrap(); - let _got = AssertUnwindSafe( - Context::new( - &runtime, - &mocks, - netdir, - hsid, - &mut data, - secret_keys, - mocks.clone(), - ) - .unwrap() - .connect(), + let mut ctx = Context::new( + &runtime, + &mocks, + netdir, + hsid, + &mut data, + secret_keys, + mocks.clone(), ) - .catch_unwind() // TODO HS remove this and the AssertUnwindSafe - .await; + .unwrap(); + + let _got = AssertUnwindSafe(ctx.connect()) + .catch_unwind() // TODO HS remove this and the AssertUnwindSafe + .await; let (hs_blind_id_key, subcredential) = HsIdKey::try_from(hsid) .unwrap() @@ -613,6 +620,15 @@ mod test { format!("{:?}", Some(hsdesc)) ); + // Check how long the descriptor is valid for + let bounds = ctx.data.desc.as_ref().unwrap().bounds(); + assert_eq!(bounds.start_bound(), Bound::Unbounded); + Duration::try_from(mglobal.got_desc.as_ref().unwrap().lifetime()).unwrap(); + assert_eq!( + bounds.end_bound(), + Bound::Included(now + Duration::from_secs(test_data::TEST_LIFETIME_SECS_2)).as_ref() + ); + // TODO hs check the circuit in got is the one we gave out } diff --git a/crates/tor-netdoc/src/doc/hsdesc.rs b/crates/tor-netdoc/src/doc/hsdesc.rs index 6c5ad5b66..f32e394b3 100644 --- a/crates/tor-netdoc/src/doc/hsdesc.rs +++ b/crates/tor-netdoc/src/doc/hsdesc.rs @@ -271,6 +271,11 @@ impl HsDesc { .check_signature() .map_err(|e| e.into()) } + + /// Return the lifetime of this descriptor. + pub fn lifetime(&self) -> IntegerMinutes { + self.idx_info.lifetime + } } impl EncryptedHsDesc { @@ -394,6 +399,8 @@ pub mod test_data { // SDZNMD4RP4SCH4EYTTUZPFRZINNFWAOPPKZ6BINZAC7LREV24RBQ (base32) pub const TEST_SECKEY_2: [u8; 32] = hex!("90F2D60F917F2423F0989CE9979639435A5B01CF7AB3E0A1B900BEB892BAE443"); + /// The lifetime of the descriptor in seconds. + pub const TEST_LIFETIME_SECS_2: u64 = 180 * 60; } #[cfg(test)] From 919790c6320c7b62ff002733725919dd1a148f2c Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Wed, 3 May 2023 23:38:41 +0100 Subject: [PATCH 02/14] tor-checkable: Add a way to compute the intersection of 2 RangeBounds. This will be used for computing the final `TimerangeBound` of a `HsDesc` from the `TimerangeBound`s of its inner and outer layers. Signed-off-by: Gabriela Moldovan --- crates/tor-checkable/src/timed.rs | 71 +++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/crates/tor-checkable/src/timed.rs b/crates/tor-checkable/src/timed.rs index d18051264..cbb1affc4 100644 --- a/crates/tor-checkable/src/timed.rs +++ b/crates/tor-checkable/src/timed.rs @@ -1,5 +1,6 @@ //! Convenience implementation of a TimeBound object. +use std::cmp; use std::ops::{Bound, Deref, RangeBounds}; use std::time; @@ -174,6 +175,43 @@ impl crate::Timebound for TimerangeBound { } } +/// Compute the intersection of two `RangeBound`s. +/// +/// Returns a [TimeValidityError::Unspecified](crate::TimeValidityError::Unspecified) error if the +/// two ranges do not intersect. +pub fn intersect_bounds( + b1: U, + b2: U, +) -> Result<(Bound, Bound), crate::TimeValidityError> +where + U: RangeBounds, +{ + let b1_start = unwrap_bound(b1.start_bound()); + let b2_start = unwrap_bound(b2.start_bound()); + let b1_end = unwrap_bound(b1.end_bound()); + let b2_end = unwrap_bound(b2.end_bound()); + + let start = match (b1_start, b2_start) { + (Some(b1), Some(b2)) => Bound::Included(cmp::max(b1, b2)), + (Some(b), _) | (_, Some(b)) => Bound::Included(b), + _ => Bound::Unbounded, + }; + + let end = match (b1_end, b2_end) { + (Some(b1), Some(b2)) => Bound::Included(cmp::min(b1, b2)), + (Some(b), _) | (_, Some(b)) => Bound::Included(b), + _ => Bound::Unbounded, + }; + + match (unwrap_bound(start.as_ref()), unwrap_bound(end.as_ref())) { + (Some(start), Some(end)) if start > end => { + // If the two ranges don't intersect, return an error. + Err(crate::TimeValidityError::Unspecified) + } + _ => Ok((start, end)), + } +} + #[cfg(test)] mod test { // @@ begin test lint list maintained by maint/add_warning @@ @@ -316,4 +354,37 @@ mod test { assert_eq!(tb1, tb2.dangerously_map(|s| s.clone())); assert_eq!(tb1, tb3.dangerously_map(|s| s.to_owned())); } + + #[test] + fn test_intersect_bounds() { + const MIN: Duration = Duration::from_secs(60); + + // t1 - - t2 - + // t3 - - t4 + let t1 = SystemTime::now(); + let t2 = t1 + 2 * MIN; + + let t3 = t1 + MIN; + let t4 = t3 + 3 * MIN; + + let b1 = (Bound::Included(t1), Bound::Included(t2)); + let b2 = (Bound::Included(t3), Bound::Included(t4)); + let expected = (Bound::Included(t3), Bound::Included(t2)); + assert_eq!(intersect_bounds(b1, b2).unwrap(), expected); + assert_eq!(intersect_bounds(b2, b1).unwrap(), expected); + + // t1 - - t2 - - + // t3 - - t4 + let t3 = t2 + 2 * MIN; + let t4 = t3 + 3 * MIN; + let b2 = (Bound::Included(t3), Bound::Included(t4)); + assert_eq!( + intersect_bounds(b1, b2).unwrap_err(), + crate::TimeValidityError::Unspecified + ); + assert_eq!( + intersect_bounds(b2, b1).unwrap_err(), + crate::TimeValidityError::Unspecified + ); + } } From 92e0b6a0290578e89d502898957cbdb7bc970dea Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Wed, 3 May 2023 23:56:08 +0100 Subject: [PATCH 03/14] hsclient: Compute HsDesc validity time from the TimerangeBounds of its layers. This makes `descriptor_ensure` refetch the descriptor if either of its layers (inner or outer) expires. Signed-off-by: Gabriela Moldovan --- crates/tor-hsclient/src/connect.rs | 38 ++++++++++------------------- crates/tor-netdoc/src/doc/hsdesc.rs | 33 +++++++++++++------------ 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/crates/tor-hsclient/src/connect.rs b/crates/tor-hsclient/src/connect.rs index 0cc7f16fa..901951675 100644 --- a/crates/tor-hsclient/src/connect.rs +++ b/crates/tor-hsclient/src/connect.rs @@ -3,9 +3,7 @@ use std::time::Duration; -use std::ops::{Bound, RangeBounds}; use std::sync::Arc; -use std::time::SystemTime; use async_trait::async_trait; use educe::Educe; @@ -227,7 +225,7 @@ impl<'c, 'd, R: Runtime, M: MocksForConnect> Context<'c, 'd, R, M> { // https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1118#note_2894463 let mut attempts = hs_dirs.iter().cycle().take(MAX_TOTAL_ATTEMPTS); let mut errors = RetryError::in_attempt_to("retrieve hidden service descriptor"); - let (desc, bounds) = loop { + let desc = loop { let relay = match attempts.next() { Some(relay) => relay, None => { @@ -267,7 +265,7 @@ impl<'c, 'd, R: Runtime, M: MocksForConnect> Context<'c, 'd, R, M> { // Because the `HsDesc` must be owned by `data.desc`, // we must first wrap it in the TimerangeBound, // and then dangerously_assume_timely to get a reference out again. - let ret = self.data.desc.insert(TimerangeBound::new(desc, bounds)); + let ret = self.data.desc.insert(desc); Ok(ret.as_ref().dangerously_assume_timely()) } @@ -277,12 +275,12 @@ impl<'c, 'd, R: Runtime, M: MocksForConnect> Context<'c, 'd, R, M> { /// /// On success, returns the descriptor. /// - /// Also returns a `RangeBounds` which represents the descriptor's validity. - /// (This is separate, because the descriptor's validity at the current time *has* been checked,) + /// While the returned descriptor is `TimerangeBound`, its validity at the current time *has* + /// been checked. async fn descriptor_fetch_attempt( &self, hsdir: &Relay<'_>, - ) -> Result<(HsDesc, impl RangeBounds), DescriptorErrorDetail> { + ) -> Result, DescriptorErrorDetail> { let request = tor_dirclient::request::HsDescDownloadRequest::new(self.hs_blind_id); trace!( "hsdir for {}, trying {}/{}, request {:?} (http request {:?}", @@ -330,27 +328,14 @@ impl<'c, 'd, R: Runtime, M: MocksForConnect> Context<'c, 'd, R, M> { let now = self.runtime.wallclock(); - let hsdesc = HsDesc::parse_decrypt_validate( + HsDesc::parse_decrypt_validate( &desc_text, &self.hs_blind_id, now, &self.subcredential, hsc_desc_enc.as_ref().map(|(kp, ks)| (kp, *ks)), ) - .map_err(DescriptorErrorDetail::from)?; - - // Cache the descriptor for the amount of time specified in the descriptor-lifetime field. - let upper_bound = Bound::Included( - now + Duration::try_from(hsdesc.lifetime()).map_err(|e| { - into_internal!( - "bad descriptor-lifetime (this descriptor should have failed to parse)" - )(e) - })?, - ); - - let bounds = (Bound::Unbounded, upper_bound); - - Ok((hsdesc, bounds)) + .map_err(DescriptorErrorDetail::from) } } @@ -462,6 +447,7 @@ mod test { use super::*; use crate::*; use futures::FutureExt as _; + use std::ops::{Bound, RangeBounds}; use std::{iter, panic::AssertUnwindSafe}; use tokio_crate as tokio; use tor_async_utils::JoinReadWrite; @@ -609,7 +595,8 @@ mod test { &subcredential, Some((&pk, &sk)), ) - .unwrap(); + .unwrap() + .dangerously_assume_timely(); let mglobal = mocks.mglobal.lock().unwrap(); assert_eq!(mglobal.hsdirs_asked.len(), 1); @@ -623,10 +610,11 @@ mod test { // Check how long the descriptor is valid for let bounds = ctx.data.desc.as_ref().unwrap().bounds(); assert_eq!(bounds.start_bound(), Bound::Unbounded); - Duration::try_from(mglobal.got_desc.as_ref().unwrap().lifetime()).unwrap(); + + let desc_valid_until = humantime::parse_rfc3339("2023-02-11T20:00:00Z").unwrap(); assert_eq!( bounds.end_bound(), - Bound::Included(now + Duration::from_secs(test_data::TEST_LIFETIME_SECS_2)).as_ref() + Bound::Included(desc_valid_until).as_ref() ); // TODO hs check the circuit in got is the one we gave out diff --git a/crates/tor-netdoc/src/doc/hsdesc.rs b/crates/tor-netdoc/src/doc/hsdesc.rs index f32e394b3..b7b981e16 100644 --- a/crates/tor-netdoc/src/doc/hsdesc.rs +++ b/crates/tor-netdoc/src/doc/hsdesc.rs @@ -22,7 +22,7 @@ pub use desc_enc::DecryptionError; use crate::{NetdocErrorKind as EK, Result}; use tor_checkable::signed::{self, SignatureGated}; -use tor_checkable::timed::{self, TimerangeBound}; +use tor_checkable::timed::{self, intersect_bounds, TimerangeBound}; use tor_checkable::{SelfSigned, Timebound}; use tor_hscrypto::pk::{ HsBlindId, HsClientDescEncKey, HsClientDescEncSecretKey, HsIntroPtSessionIdKey, HsSvcNtorKey, @@ -262,19 +262,24 @@ impl HsDesc { valid_at: SystemTime, subcredential: &Subcredential, hsc_desc_enc: Option<(&HsClientDescEncKey, &HsClientDescEncSecretKey)>, - ) -> Result { - Self::parse(input, blinded_onion_id)? - .check_signature()? - .check_valid_at(&valid_at)? - .decrypt(subcredential, hsc_desc_enc)? - .check_valid_at(&valid_at)? - .check_signature() - .map_err(|e| e.into()) - } + ) -> Result> { + let unchecked_desc = Self::parse(input, blinded_onion_id)?.check_signature()?; - /// Return the lifetime of this descriptor. - pub fn lifetime(&self) -> IntegerMinutes { - self.idx_info.lifetime + let outer_desc_bounds = unchecked_desc.bounds(); + + let unchecked_desc = unchecked_desc + .check_valid_at(&valid_at)? + .decrypt(subcredential, hsc_desc_enc)?; + + let inner_desc_bounds = unchecked_desc.bounds(); + + let hsdesc = unchecked_desc + .check_valid_at(&valid_at)? + .check_signature()?; + + let range = intersect_bounds(outer_desc_bounds, inner_desc_bounds)?; + + Ok(TimerangeBound::new(hsdesc, range)) } } @@ -399,8 +404,6 @@ pub mod test_data { // SDZNMD4RP4SCH4EYTTUZPFRZINNFWAOPPKZ6BINZAC7LREV24RBQ (base32) pub const TEST_SECKEY_2: [u8; 32] = hex!("90F2D60F917F2423F0989CE9979639435A5B01CF7AB3E0A1B900BEB892BAE443"); - /// The lifetime of the descriptor in seconds. - pub const TEST_LIFETIME_SECS_2: u64 = 180 * 60; } #[cfg(test)] From 9dbf162c909b7ce7d07a94821c629931c028c65d Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Thu, 4 May 2023 19:39:28 +0100 Subject: [PATCH 04/14] netdoc: Do not consume EncryptedHsDesc when decrypting. `parse_decrypt_validate` will need to "peek" inside an encrypted descriptor (before validating it) to extract the `TimerangeBound` of the inner layer. This is needed to compute the intersection of the `TimerangeBound`s of both layers. Signed-off-by: Gabriela Moldovan --- crates/tor-netdoc/src/doc/hsdesc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tor-netdoc/src/doc/hsdesc.rs b/crates/tor-netdoc/src/doc/hsdesc.rs index b7b981e16..0ff849934 100644 --- a/crates/tor-netdoc/src/doc/hsdesc.rs +++ b/crates/tor-netdoc/src/doc/hsdesc.rs @@ -298,7 +298,7 @@ impl EncryptedHsDesc { // TODO hs: I'm not sure that taking `hsc_desc_enc` as an argument is correct. Instead, maybe // we should take a set of keys? pub fn decrypt( - self, + &self, subcredential: &Subcredential, hsc_desc_enc: Option<(&HsClientDescEncKey, &HsClientDescEncSecretKey)>, ) -> Result>> { From 69924275968c1bbb5567a800bbb3d8d99d5250f3 Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Fri, 5 May 2023 14:05:49 +0100 Subject: [PATCH 05/14] hsclient: descriptor_ensure no longer wraps the descriptor in TimerangeBound. `descriptor_fetch_attempt` now returns a `TimerangeBound` (and so does `parse_descript_validate`). Signed-off-by: Gabriela Moldovan --- crates/tor-hsclient/src/connect.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/tor-hsclient/src/connect.rs b/crates/tor-hsclient/src/connect.rs index 901951675..f7522ac13 100644 --- a/crates/tor-hsclient/src/connect.rs +++ b/crates/tor-hsclient/src/connect.rs @@ -262,9 +262,12 @@ impl<'c, 'd, R: Runtime, M: MocksForConnect> Context<'c, 'd, R, M> { // Store the bounded value in the cache for reuse, // but return a reference to the unwrapped `HsDesc`. // - // Because the `HsDesc` must be owned by `data.desc`, - // we must first wrap it in the TimerangeBound, + // The `HsDesc` must be owned by `data.desc`, + // so first add it to `data.desc`, // and then dangerously_assume_timely to get a reference out again. + // + // It is safe to dangerously_assume_timely, + // as descriptor_fetch_attempt has already checked the timeliness of the descriptor. let ret = self.data.desc.insert(desc); Ok(ret.as_ref().dangerously_assume_timely()) } From 872f5da4b250bb658bc90605d0934b1148a3a0b0 Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Fri, 5 May 2023 14:37:55 +0100 Subject: [PATCH 06/14] tor-basic-utils: Add RangeBoundsExt trait. Signed-off-by: Gabriela Moldovan --- crates/tor-basic-utils/src/lib.rs | 1 + crates/tor-basic-utils/src/rangebounds.rs | 200 ++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 crates/tor-basic-utils/src/rangebounds.rs diff --git a/crates/tor-basic-utils/src/lib.rs b/crates/tor-basic-utils/src/lib.rs index ddf6ebdb8..90342306e 100644 --- a/crates/tor-basic-utils/src/lib.rs +++ b/crates/tor-basic-utils/src/lib.rs @@ -44,6 +44,7 @@ use std::mem; pub mod iter; pub mod n_key_set; +pub mod rangebounds; pub mod retry; pub mod test_rng; diff --git a/crates/tor-basic-utils/src/rangebounds.rs b/crates/tor-basic-utils/src/rangebounds.rs new file mode 100644 index 000000000..058d58ed1 --- /dev/null +++ b/crates/tor-basic-utils/src/rangebounds.rs @@ -0,0 +1,200 @@ +//! This module exposes helpers for working with types that implement +//! [`RangeBounds`](std::ops::RangeBounds). + +use std::cmp::{self, Ord}; +use std::ops::{Bound, RangeBounds}; + +/// An extension trait for [`RangeBounds`](std::ops::RangeBounds). +pub trait RangeBoundsExt: RangeBounds { + /// Compute the intersection of two `RangeBound`s. + /// + /// In essence, this computes the intersection of the intervals described by bounds of the + /// two objects. + /// + /// Returns `None` if the intersection of the two ranges is the empty set. + fn intersect<'a, U: RangeBounds>( + &'a self, + other: &'a U, + ) -> Option<(Bound<&'a T>, Bound<&'a T>)>; +} + +impl RangeBoundsExt for R +where + R: RangeBounds, + T: Ord, +{ + fn intersect<'a, U: RangeBounds>( + &'a self, + other: &'a U, + ) -> Option<(Bound<&'a T>, Bound<&'a T>)> { + use Bound::*; + + let this_start = self.start_bound(); + let other_start = other.start_bound(); + let this_end = self.end_bound(); + let other_end = other.end_bound(); + + let start = bounds_max(this_start, other_start); + let end = bounds_min(this_end, other_end); + + match (start, end) { + (Included(start), Excluded(end)) if start == end => { + // The interval [n, n) = {} (empty set). + None + } + (Included(start), Included(end)) + | (Included(start), Excluded(end)) + | (Excluded(start), Included(end)) + | (Excluded(start), Excluded(end)) + if start > end => + { + // For any a > b, the intervals [a, b], [a, b), (a, b], (a, b) are empty. + None + } + _ => Some((start, end)), + } + } +} + +/// Return the largest of `b1` and `b2`. +/// +/// If one of the bounds is [Unbounded](Bound::Unbounded), the other will be returned. +fn bounds_max<'a, T: Ord>(b1: Bound<&'a T>, b2: Bound<&'a T>) -> Bound<&'a T> { + use Bound::*; + + match (b1, b2) { + (Included(b1), Included(b2)) => Included(cmp::max(b1, b2)), + (Excluded(b1), Excluded(b2)) => Excluded(cmp::max(b1, b2)), + + (Excluded(b1), Included(b2)) if b1 >= b2 => Excluded(b1), + (Excluded(_), Included(b2)) => Included(b2), + + (Included(b1), Excluded(b2)) if b2 >= b1 => Excluded(b2), + (Included(b1), Excluded(_)) => Included(b1), + + (b, Unbounded) | (Unbounded, b) => b, + } +} + +/// Return the smallest of `b1` and `b2`. +/// +/// If one of the bounds is [Unbounded](Bound::Unbounded), the other will be returned. +fn bounds_min<'a, T: Ord>(b1: Bound<&'a T>, b2: Bound<&'a T>) -> Bound<&'a T> { + use Bound::*; + + match (b1, b2) { + (Included(b1), Included(b2)) => Included(cmp::min(b1, b2)), + (Excluded(b1), Excluded(b2)) => Excluded(cmp::min(b1, b2)), + + (Excluded(b1), Included(b2)) if b1 <= b2 => Excluded(b1), + (Excluded(_), Included(b2)) => Included(b2), + + (Included(b1), Excluded(b2)) if b2 <= b1 => Excluded(b2), + (Included(b1), Excluded(_)) => Included(b1), + + (b, Unbounded) | (Unbounded, b) => b, + } +} + +#[cfg(test)] +mod test { + // @@ begin test lint list maintained by maint/add_warning @@ + #![allow(clippy::bool_assert_comparison)] + #![allow(clippy::clone_on_copy)] + #![allow(clippy::dbg_macro)] + #![allow(clippy::print_stderr)] + #![allow(clippy::print_stdout)] + #![allow(clippy::single_char_pattern)] + #![allow(clippy::unwrap_used)] + #![allow(clippy::unchecked_duration_subtraction)] + //! + use super::*; + use Bound::{Included as Incl, Excluded as Excl, Unbounded}; + + #[test] + fn no_overlap() { + #[allow(clippy::type_complexity)] + const NON_OVERLAPPING_RANGES: &[( + (Bound, Bound), + (Bound, Bound), + )] = &[ + // (1, 2) and (3, 4) + ((Excl(1), Excl(2)), (Excl(3), Excl(4))), + // (1, 2) and [2, 3] + ((Excl(1), Excl(2)), (Incl(3), Incl(4))), + // (-inf, 2) and [2, 3] + ((Unbounded, Excl(2)), (Incl(2), Incl(3))), + // (-inf, 2) and [2, inf) + ((Unbounded, Excl(2)), (Incl(2), Unbounded)), + ]; + + for (range1, range2) in NON_OVERLAPPING_RANGES { + let intersection1 = range1.intersect(range2); + let intersection2 = range2.intersect(range1); + + assert_eq!(intersection1, intersection2); + assert!(intersection1.is_none()); + } + } + + #[test] + fn intersect_unbounded_start() { + // (-inf, 3) + let range1 = (Unbounded, Excl(3)); + // [2, 5] + let range2 = (Incl(2), Incl(5)); + + let intersection1 = range1.intersect(&range2).unwrap(); + let intersection2 = range2.intersect(&range1).unwrap(); + + assert_eq!(intersection1, intersection2); + + // intersection = [2 3] + assert_eq!(intersection1.start_bound(), Bound::Included(&2)); + assert_eq!(intersection1.end_bound(), Bound::Excluded(&3)); + } + + #[test] + fn intersect_unbounded_end() { + // (8, inf) + let range1 = (Excl(8), Unbounded); + // [8, 20] + let range2 = (Incl(8), Incl(20)); + + let intersection1 = range1.intersect(&range2).unwrap(); + let intersection2 = range2.intersect(&range1).unwrap(); + + assert_eq!(intersection1, intersection2); + + // intersection = (8, 20] + assert_eq!(intersection1.start_bound(), Bound::Excluded(&8)); + assert_eq!(intersection1.end_bound(), Bound::Included(&20)); + } + + #[test] + fn combinatorial() { + for i in 0..10 { + for j in 0..10 { + for k in 0..10 { + for l in 0..10 { + let range1 = (Incl(i), Incl(j)); + let range2 = (Incl(k), Incl(l)); + + let intersection = range1.intersect(&range2); + + for witness in 0..10 { + let c1 = range1.contains(&witness); + let c2 = range2.contains(&witness); + let both_contain_witness = c1 && c2; + + if both_contain_witness { + // If both ranges contain `witness` they definitely intersect. + assert!(intersection.unwrap().contains(&witness)); + } + } + } + } + } + } + } +} From 11c39b5657bd2680201d1b897886222a06a4fdd6 Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Wed, 10 May 2023 19:15:43 +0100 Subject: [PATCH 07/14] tor-basic-utils: Add a helper function to deduplicate test code. Signed-off-by: Gabriela Moldovan --- crates/tor-basic-utils/src/rangebounds.rs | 48 ++++++++++++++--------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/crates/tor-basic-utils/src/rangebounds.rs b/crates/tor-basic-utils/src/rangebounds.rs index 058d58ed1..da376097c 100644 --- a/crates/tor-basic-utils/src/rangebounds.rs +++ b/crates/tor-basic-utils/src/rangebounds.rs @@ -109,7 +109,26 @@ mod test { #![allow(clippy::unchecked_duration_subtraction)] //! use super::*; - use Bound::{Included as Incl, Excluded as Excl, Unbounded}; + use std::fmt::Debug; + use Bound::{Excluded as Excl, Included as Incl, Unbounded}; + + /// A helper that computes the intersection of `range1` and `range2`. + /// + /// This function also asserts that the intersection operation is commutative. + fn intersect<'a, T, R: RangeBounds>( + range1: &'a R, + range2: &'a R, + ) -> Option<(Bound<&'a T>, Bound<&'a T>)> + where + T: PartialEq + Ord + Debug, + { + let intersection1 = range1.intersect(range2); + let intersection2 = range2.intersect(range1); + + assert_eq!(intersection1, intersection2); + + intersection1 + } #[test] fn no_overlap() { @@ -129,11 +148,8 @@ mod test { ]; for (range1, range2) in NON_OVERLAPPING_RANGES { - let intersection1 = range1.intersect(range2); - let intersection2 = range2.intersect(range1); - - assert_eq!(intersection1, intersection2); - assert!(intersection1.is_none()); + let intersection = intersect(range1, range2); + assert!(intersection.is_none()); } } @@ -144,14 +160,11 @@ mod test { // [2, 5] let range2 = (Incl(2), Incl(5)); - let intersection1 = range1.intersect(&range2).unwrap(); - let intersection2 = range2.intersect(&range1).unwrap(); - - assert_eq!(intersection1, intersection2); + let intersection = intersect(&range1, &range2).unwrap(); // intersection = [2 3] - assert_eq!(intersection1.start_bound(), Bound::Included(&2)); - assert_eq!(intersection1.end_bound(), Bound::Excluded(&3)); + assert_eq!(intersection.start_bound(), Bound::Included(&2)); + assert_eq!(intersection.end_bound(), Bound::Excluded(&3)); } #[test] @@ -161,14 +174,11 @@ mod test { // [8, 20] let range2 = (Incl(8), Incl(20)); - let intersection1 = range1.intersect(&range2).unwrap(); - let intersection2 = range2.intersect(&range1).unwrap(); - - assert_eq!(intersection1, intersection2); + let intersection = intersect(&range1, &range2).unwrap(); // intersection = (8, 20] - assert_eq!(intersection1.start_bound(), Bound::Excluded(&8)); - assert_eq!(intersection1.end_bound(), Bound::Included(&20)); + assert_eq!(intersection.start_bound(), Bound::Excluded(&8)); + assert_eq!(intersection.end_bound(), Bound::Included(&20)); } #[test] @@ -180,7 +190,7 @@ mod test { let range1 = (Incl(i), Incl(j)); let range2 = (Incl(k), Incl(l)); - let intersection = range1.intersect(&range2); + let intersection = intersect(&range1, &range2); for witness in 0..10 { let c1 = range1.contains(&witness); From 094287e6776926682ae09dabb1e7b1772c74d8d7 Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Fri, 5 May 2023 14:40:27 +0100 Subject: [PATCH 08/14] tor-checkable: Implement RangeBounds for TimerangeBound. By implementing `RangeBounds` for `TimerangeBound`, we get `RangeBoundsExt` for free. This will enable `parse_decrypt_validate` to easily compute the intersection of the `TimerangeBound`s its layers. Signed-off-by: Gabriela Moldovan --- crates/tor-basic-utils/src/rangebounds.rs | 4 ++-- crates/tor-checkable/src/timed.rs | 21 +++++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/tor-basic-utils/src/rangebounds.rs b/crates/tor-basic-utils/src/rangebounds.rs index da376097c..d28a30282 100644 --- a/crates/tor-basic-utils/src/rangebounds.rs +++ b/crates/tor-basic-utils/src/rangebounds.rs @@ -38,8 +38,8 @@ where let end = bounds_min(this_end, other_end); match (start, end) { - (Included(start), Excluded(end)) if start == end => { - // The interval [n, n) = {} (empty set). + (Excluded(start), Excluded(end)) | (Included(start), Excluded(end)) if start == end => { + // The interval (n, n) = [n, n) = {} (empty set). None } (Included(start), Included(end)) diff --git a/crates/tor-checkable/src/timed.rs b/crates/tor-checkable/src/timed.rs index cbb1affc4..99e239f71 100644 --- a/crates/tor-checkable/src/timed.rs +++ b/crates/tor-checkable/src/timed.rs @@ -143,10 +143,23 @@ impl TimerangeBound { /// Return the underlying time bounds of this object. pub fn bounds(&self) -> (Bound, Bound) { - ( - self.start.map(Bound::Included).unwrap_or(Bound::Unbounded), - self.end.map(Bound::Included).unwrap_or(Bound::Unbounded), - ) + (self.start_bound().cloned(), self.end_bound().cloned()) + } +} + +impl RangeBounds for TimerangeBound { + fn start_bound(&self) -> Bound<&time::SystemTime> { + self.start + .as_ref() + .map(Bound::Included) + .unwrap_or(Bound::Unbounded) + } + + fn end_bound(&self) -> Bound<&time::SystemTime> { + self.end + .as_ref() + .map(Bound::Included) + .unwrap_or(Bound::Unbounded) } } From f9c6cc11bb1e1e048ad4f22749de779d98db5a17 Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Fri, 5 May 2023 14:42:20 +0100 Subject: [PATCH 09/14] netdoc: Use the RangeBoundsExt impl of TimerangeBound. We can now get rid of the standalone `intersect_bounds` function. Signed-off-by: Gabriela Moldovan --- crates/tor-checkable/src/timed.rs | 71 ----------------------------- crates/tor-netdoc/Cargo.toml | 1 + crates/tor-netdoc/src/doc/hsdesc.rs | 35 +++++++++----- 3 files changed, 25 insertions(+), 82 deletions(-) diff --git a/crates/tor-checkable/src/timed.rs b/crates/tor-checkable/src/timed.rs index 99e239f71..e56bb8b8a 100644 --- a/crates/tor-checkable/src/timed.rs +++ b/crates/tor-checkable/src/timed.rs @@ -1,6 +1,5 @@ //! Convenience implementation of a TimeBound object. -use std::cmp; use std::ops::{Bound, Deref, RangeBounds}; use std::time; @@ -188,43 +187,6 @@ impl crate::Timebound for TimerangeBound { } } -/// Compute the intersection of two `RangeBound`s. -/// -/// Returns a [TimeValidityError::Unspecified](crate::TimeValidityError::Unspecified) error if the -/// two ranges do not intersect. -pub fn intersect_bounds( - b1: U, - b2: U, -) -> Result<(Bound, Bound), crate::TimeValidityError> -where - U: RangeBounds, -{ - let b1_start = unwrap_bound(b1.start_bound()); - let b2_start = unwrap_bound(b2.start_bound()); - let b1_end = unwrap_bound(b1.end_bound()); - let b2_end = unwrap_bound(b2.end_bound()); - - let start = match (b1_start, b2_start) { - (Some(b1), Some(b2)) => Bound::Included(cmp::max(b1, b2)), - (Some(b), _) | (_, Some(b)) => Bound::Included(b), - _ => Bound::Unbounded, - }; - - let end = match (b1_end, b2_end) { - (Some(b1), Some(b2)) => Bound::Included(cmp::min(b1, b2)), - (Some(b), _) | (_, Some(b)) => Bound::Included(b), - _ => Bound::Unbounded, - }; - - match (unwrap_bound(start.as_ref()), unwrap_bound(end.as_ref())) { - (Some(start), Some(end)) if start > end => { - // If the two ranges don't intersect, return an error. - Err(crate::TimeValidityError::Unspecified) - } - _ => Ok((start, end)), - } -} - #[cfg(test)] mod test { // @@ begin test lint list maintained by maint/add_warning @@ @@ -367,37 +329,4 @@ mod test { assert_eq!(tb1, tb2.dangerously_map(|s| s.clone())); assert_eq!(tb1, tb3.dangerously_map(|s| s.to_owned())); } - - #[test] - fn test_intersect_bounds() { - const MIN: Duration = Duration::from_secs(60); - - // t1 - - t2 - - // t3 - - t4 - let t1 = SystemTime::now(); - let t2 = t1 + 2 * MIN; - - let t3 = t1 + MIN; - let t4 = t3 + 3 * MIN; - - let b1 = (Bound::Included(t1), Bound::Included(t2)); - let b2 = (Bound::Included(t3), Bound::Included(t4)); - let expected = (Bound::Included(t3), Bound::Included(t2)); - assert_eq!(intersect_bounds(b1, b2).unwrap(), expected); - assert_eq!(intersect_bounds(b2, b1).unwrap(), expected); - - // t1 - - t2 - - - // t3 - - t4 - let t3 = t2 + 2 * MIN; - let t4 = t3 + 3 * MIN; - let b2 = (Bound::Included(t3), Bound::Included(t4)); - assert_eq!( - intersect_bounds(b1, b2).unwrap_err(), - crate::TimeValidityError::Unspecified - ); - assert_eq!( - intersect_bounds(b2, b1).unwrap_err(), - crate::TimeValidityError::Unspecified - ); - } } diff --git a/crates/tor-netdoc/Cargo.toml b/crates/tor-netdoc/Cargo.toml index 9dd4e601e..1ee6ce656 100644 --- a/crates/tor-netdoc/Cargo.toml +++ b/crates/tor-netdoc/Cargo.toml @@ -85,6 +85,7 @@ smallvec = "1.10" thiserror = "1" time = { version = "0.3", features = ["std", "parsing", "macros"] } tinystr = "0.7.0" +tor-basic-utils = { path = "../tor-basic-utils", version = "0.7.0" } tor-bytes = { path = "../tor-bytes", version = "0.7.0" } tor-cert = { path = "../tor-cert", version = "0.7.0" } tor-checkable = { path = "../tor-checkable", version = "0.5.0" } diff --git a/crates/tor-netdoc/src/doc/hsdesc.rs b/crates/tor-netdoc/src/doc/hsdesc.rs index 0ff849934..dce3240f0 100644 --- a/crates/tor-netdoc/src/doc/hsdesc.rs +++ b/crates/tor-netdoc/src/doc/hsdesc.rs @@ -18,11 +18,13 @@ mod middle; mod outer; pub use desc_enc::DecryptionError; +use tor_basic_utils::rangebounds::RangeBoundsExt; +use tor_error::internal; use crate::{NetdocErrorKind as EK, Result}; use tor_checkable::signed::{self, SignatureGated}; -use tor_checkable::timed::{self, intersect_bounds, TimerangeBound}; +use tor_checkable::timed::{self, TimerangeBound}; use tor_checkable::{SelfSigned, Timebound}; use tor_hscrypto::pk::{ HsBlindId, HsClientDescEncKey, HsClientDescEncSecretKey, HsIntroPtSessionIdKey, HsSvcNtorKey, @@ -265,21 +267,32 @@ impl HsDesc { ) -> Result> { let unchecked_desc = Self::parse(input, blinded_onion_id)?.check_signature()?; - let outer_desc_bounds = unchecked_desc.bounds(); + let (inner_desc, new_bounds) = { + // We use is_valid_at and dangerously_into_parts instead of check_valid_at because we + // need the time bounds of the outer layer (for computing the intersection with the + // time bounds of the inner layer). + unchecked_desc.is_valid_at(&valid_at)?; + // It's safe to use dangerously_into_parts() as we've just checked if unchecked_desc is + // valid at the current time + let (unchecked_desc, bounds) = unchecked_desc.dangerously_into_parts(); + let inner_timerangebound = unchecked_desc.decrypt(subcredential, hsc_desc_enc)?; - let unchecked_desc = unchecked_desc - .check_valid_at(&valid_at)? - .decrypt(subcredential, hsc_desc_enc)?; + let new_bounds = bounds + .intersect(&inner_timerangebound) + .map(|(b1, b2)| (b1.cloned(), b2.cloned())); - let inner_desc_bounds = unchecked_desc.bounds(); + (inner_timerangebound, new_bounds) + }; - let hsdesc = unchecked_desc - .check_valid_at(&valid_at)? - .check_signature()?; + let hsdesc = inner_desc.check_valid_at(&valid_at)?.check_signature()?; - let range = intersect_bounds(outer_desc_bounds, inner_desc_bounds)?; + // If we've reached this point, it means the descriptor is valid at specified time. This + // means the time bounds of the two layers definitely intersect, so new_bounds **must** be + // Some. It is a bug if new_bounds is None. + let new_bounds = new_bounds + .ok_or_else(|| internal!("failed to compute TimerangeBounds for a valid descriptor"))?; - Ok(TimerangeBound::new(hsdesc, range)) + Ok(TimerangeBound::new(hsdesc, new_bounds)) } } From 18c78c3ad3859f405ade0eef4fe90034795e3682 Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Wed, 10 May 2023 20:08:27 +0100 Subject: [PATCH 10/14] tor-basic-utils: Log the ranges/intersection on assertion failure. Signed-off-by: Gabriela Moldovan --- crates/tor-basic-utils/src/rangebounds.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/tor-basic-utils/src/rangebounds.rs b/crates/tor-basic-utils/src/rangebounds.rs index d28a30282..8c1736f36 100644 --- a/crates/tor-basic-utils/src/rangebounds.rs +++ b/crates/tor-basic-utils/src/rangebounds.rs @@ -139,17 +139,29 @@ mod test { )] = &[ // (1, 2) and (3, 4) ((Excl(1), Excl(2)), (Excl(3), Excl(4))), + // (1, 2) and (2, 3) + ((Excl(1), Excl(2)), (Excl(2), Excl(3))), + // (1, 2) and [2, 3) + ((Excl(1), Excl(2)), (Incl(2), Excl(3))), // (1, 2) and [2, 3] ((Excl(1), Excl(2)), (Incl(3), Incl(4))), // (-inf, 2) and [2, 3] ((Unbounded, Excl(2)), (Incl(2), Incl(3))), + // (-inf, 2) and (2, inf) + ((Unbounded, Excl(2)), (Excl(2), Unbounded)), // (-inf, 2) and [2, inf) ((Unbounded, Excl(2)), (Incl(2), Unbounded)), ]; for (range1, range2) in NON_OVERLAPPING_RANGES { let intersection = intersect(range1, range2); - assert!(intersection.is_none()); + assert!( + intersection.is_none(), + "{:?} and {:?} => {:?}", + range1, + range2, + intersection + ); } } From 9d4db38d4d499a80553b8705b170867cd7a43a20 Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Thu, 11 May 2023 11:50:37 +0100 Subject: [PATCH 11/14] tor-basic-utils: Assert witness is not part of the intersection. Signed-off-by: Gabriela Moldovan --- crates/tor-basic-utils/src/rangebounds.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/tor-basic-utils/src/rangebounds.rs b/crates/tor-basic-utils/src/rangebounds.rs index 8c1736f36..a4ced9008 100644 --- a/crates/tor-basic-utils/src/rangebounds.rs +++ b/crates/tor-basic-utils/src/rangebounds.rs @@ -212,6 +212,10 @@ mod test { if both_contain_witness { // If both ranges contain `witness` they definitely intersect. assert!(intersection.unwrap().contains(&witness)); + } else if let Some(intersection) = intersection { + // If one of them doesn't contain `witness`, `witness` is + // definitely not part of the intersection. + assert!(!intersection.contains(&witness)); } } } From 27ae57e54375475f2f035c8c14601ea0b74ff95e Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Thu, 11 May 2023 13:52:42 +0100 Subject: [PATCH 12/14] tor-basic-utils: Add rangebounds test with time ranges. Signed-off-by: Gabriela Moldovan --- crates/tor-basic-utils/src/rangebounds.rs | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/tor-basic-utils/src/rangebounds.rs b/crates/tor-basic-utils/src/rangebounds.rs index a4ced9008..069c2d217 100644 --- a/crates/tor-basic-utils/src/rangebounds.rs +++ b/crates/tor-basic-utils/src/rangebounds.rs @@ -110,6 +110,7 @@ mod test { //! use super::*; use std::fmt::Debug; + use std::time::{Duration, SystemTime}; use Bound::{Excluded as Excl, Included as Incl, Unbounded}; /// A helper that computes the intersection of `range1` and `range2`. @@ -193,6 +194,40 @@ mod test { assert_eq!(intersection.end_bound(), Bound::Included(&20)); } + #[test] + fn intersect_time_bounds() { + const MIN: Duration = Duration::from_secs(60); + + // time (relative to now): 0 1 2 3 + // | | | | + // [t1, t2]: [.......] + // [t3, t4]: [.......] + // intersection: [...] + let now = SystemTime::now(); + let t1 = now; + let t2 = now + 2 * MIN; + + let t3 = now + 1 * MIN; + let t4 = now + 3 * MIN; + + let b1 = (Bound::Included(t1), Bound::Included(t2)); + let b2 = (Bound::Included(t3), Bound::Included(t4)); + let expected = (Bound::Included(&t3), Bound::Included(&t2)); + assert_eq!(intersect(&b1, &b2).unwrap(), expected); + + // t1 - - t2 - - + // t3 - - t4 + // + // time (relative to now): 0 1 2 3 4 5 6 7 + // | | | | | | | | + // [t1, t2]: [.......] + // [t3, t4]: [............] + let t3 = now + 4 * MIN; + let t4 = now + 7 * MIN; + let b2 = (Bound::Included(t3), Bound::Included(t4)); + assert!(intersect(&b1, &b2).is_none()); + } + #[test] fn combinatorial() { for i in 0..10 { From ef53c4235d9487bcd4d75e2f0f1a139ddfda7a9c Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Thu, 11 May 2023 14:03:46 +0100 Subject: [PATCH 13/14] tor-basic-utils: Update combinatorial test to randomly choose an open or closed bound. Signed-off-by: Gabriela Moldovan --- crates/tor-basic-utils/src/rangebounds.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/tor-basic-utils/src/rangebounds.rs b/crates/tor-basic-utils/src/rangebounds.rs index 069c2d217..274d52ad8 100644 --- a/crates/tor-basic-utils/src/rangebounds.rs +++ b/crates/tor-basic-utils/src/rangebounds.rs @@ -131,6 +131,16 @@ mod test { intersection1 } + /// A helper for randomly generating either an inclusive or an exclusive bound with a + /// particular value. + fn random_bound(value: T) -> Bound { + if rand::random() { + Bound::Included(value) + } else { + Bound::Excluded(value) + } + } + #[test] fn no_overlap() { #[allow(clippy::type_complexity)] @@ -234,8 +244,8 @@ mod test { for j in 0..10 { for k in 0..10 { for l in 0..10 { - let range1 = (Incl(i), Incl(j)); - let range2 = (Incl(k), Incl(l)); + let range1 = (random_bound(i), random_bound(j)); + let range2 = (random_bound(k), random_bound(l)); let intersection = intersect(&range1, &range2); From c4def3cfca64a5848fb0b2be981ff8d1cfa75a9e Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Thu, 11 May 2023 14:16:37 +0100 Subject: [PATCH 14/14] tor-basic-utils: Add unbounded range (..) test. Signed-off-by: Gabriela Moldovan --- crates/tor-basic-utils/src/rangebounds.rs | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/tor-basic-utils/src/rangebounds.rs b/crates/tor-basic-utils/src/rangebounds.rs index 274d52ad8..4ebbfe780 100644 --- a/crates/tor-basic-utils/src/rangebounds.rs +++ b/crates/tor-basic-utils/src/rangebounds.rs @@ -204,6 +204,37 @@ mod test { assert_eq!(intersection.end_bound(), Bound::Included(&20)); } + #[test] + fn intersect_unbounded_range() { + #[allow(clippy::type_complexity)] + const RANGES: &[(Bound, Bound)] = &[ + // (1, 2) + (Excl(1), Excl(2)), + // (1, 2] + (Excl(1), Incl(2)), + // [1, 2] + (Incl(1), Incl(2)), + // [1, 2) + (Incl(1), Excl(2)), + // (1, inf) + (Excl(1), Unbounded), + // [1, inf) + (Incl(1), Unbounded), + // (-inf, 2) + (Unbounded, Excl(2)), + // (-inf, 2] + (Unbounded, Incl(2)), + ]; + + // The intersection of any interval I with (Unbounded, Unbounded) will be I. + let range1 = (Unbounded, Unbounded); + + for range2 in RANGES { + let range2 = (range2.0.as_ref(), range2.1.as_ref()); + assert_eq!(intersect(&range1, &range2).unwrap(), range2); + } + } + #[test] fn intersect_time_bounds() { const MIN: Duration = Duration::from_secs(60);