webauthn_rp

WebAuthn Level 3 RP library.
git clone https://git.philomathiclife.com/repos/webauthn_rp
Log | Files | Refs | README

commit 242d7971fba90bff1b6d03e3e5f18f220b11d3ff
parent 18befe4fbcacf8eb89fafca076dabca42e489526
Author: Zack Newman <zack@philomathiclife.com>
Date:   Fri, 20 Mar 2026 21:36:55 -0600

update deps and lints. address lints. improve maxlenhashmap api

Diffstat:
MCargo.toml | 107+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/hash/hash_map.rs | 586+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/hash/hash_set.rs | 342+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/lib.rs | 16++++++++++------
Msrc/request.rs | 21+++++++--------------
Msrc/request/register.rs | 23+++++++++++------------
Msrc/response.rs | 12++++--------
Msrc/response/register.rs | 26++++++++++++--------------
8 files changed, 842 insertions(+), 291 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -9,59 +9,60 @@ license = "MIT OR Apache-2.0" name = "webauthn_rp" readme = "README.md" repository = "https://git.philomathiclife.com/repos/webauthn_rp/" -rust-version = "1.89.0" +rust-version = "1.94.0" version = "0.4.0+spec-3" [lints.rust] -ambiguous_negative_literals = { level = "deny", priority = -1 } -closure_returning_async_block = { level = "deny", priority = -1 } -deprecated_safe = { level = "deny", priority = -1 } -deref_into_dyn_supertrait = { level = "deny", priority = -1 } -ffi_unwind_calls = { level = "deny", priority = -1 } -future_incompatible = { level = "deny", priority = -1 } -#fuzzy_provenance_casts = { level = "deny", priority = -1 } -impl_trait_redundant_captures = { level = "deny", priority = -1 } -keyword_idents = { level = "deny", priority = -1 } -let_underscore = { level = "deny", priority = -1 } -linker_messages = { level = "deny", priority = -1 } -#lossy_provenance_casts = { level = "deny", priority = -1 } -macro_use_extern_crate = { level = "deny", priority = -1 } -meta_variable_misuse = { level = "deny", priority = -1 } -missing_copy_implementations = { level = "deny", priority = -1 } -missing_debug_implementations = { level = "deny", priority = -1 } -missing_docs = { level = "deny", priority = -1 } -#multiple_supertrait_upcastable = { level = "deny", priority = -1 } -#must_not_suspend = { level = "deny", priority = -1 } -non_ascii_idents = { level = "deny", priority = -1 } -#non_exhaustive_omitted_patterns = { level = "deny", priority = -1 } -nonstandard_style = { level = "deny", priority = -1 } -redundant_imports = { level = "deny", priority = -1 } -redundant_lifetimes = { level = "deny", priority = -1 } -refining_impl_trait = { level = "deny", priority = -1 } -rust_2018_compatibility = { level = "deny", priority = -1 } -rust_2018_idioms = { level = "deny", priority = -1 } -rust_2021_compatibility = { level = "deny", priority = -1 } -rust_2024_compatibility = { level = "deny", priority = -1 } -single_use_lifetimes = { level = "deny", priority = -1 } -#supertrait_item_shadowing_definition = { level = "deny", priority = -1 } -#supertrait_item_shadowing_usage = { level = "deny", priority = -1 } -trivial_casts = { level = "deny", priority = -1 } -trivial_numeric_casts = { level = "deny", priority = -1 } -unit_bindings = { level = "deny", priority = -1 } -unknown_or_malformed_diagnostic_attributes = { level = "deny", priority = -1 } -unnameable_types = { level = "deny", priority = -1 } -#unqualified_local_imports = { level = "deny", priority = -1 } -unreachable_pub = { level = "deny", priority = -1 } -unsafe_code = { level = "deny", priority = -1 } -unstable_features = { level = "deny", priority = -1 } +deprecated-safe = { level = "deny", priority = -1 } +future-incompatible = { level = "deny", priority = -1 } +keyword-idents = { level = "deny", priority = -1 } +let-underscore = { level = "deny", priority = -1 } +nonstandard-style = { level = "deny", priority = -1 } +refining-impl-trait = { level = "deny", priority = -1 } +rust-2018-compatibility = { level = "deny", priority = -1 } +rust-2018-idioms = { level = "deny", priority = -1 } +rust-2021-compatibility = { level = "deny", priority = -1 } +rust-2024-compatibility = { level = "deny", priority = -1 } +unknown-or-malformed-diagnostic-attributes = { level = "deny", priority = -1 } unused = { level = "deny", priority = -1 } -unused_crate_dependencies = { level = "deny", priority = -1 } -unused_import_braces = { level = "deny", priority = -1 } -unused_lifetimes = { level = "deny", priority = -1 } -unused_qualifications = { level = "deny", priority = -1 } -unused_results = { level = "deny", priority = -1 } -variant_size_differences = { level = "deny", priority = -1 } warnings = { level = "deny", priority = -1 } +ambiguous-negative-literals = { level = "deny", priority = -1 } +closure-returning-async-block = { level = "deny", priority = -1 } +deprecated-in-future = { level = "deny", priority = -1 } +deref-into-dyn-supertrait = { level = "deny", priority = -1 } +ffi-unwind-calls = { level = "deny", priority = -1 } +#fuzzy-provenance-casts = { level = "deny", priority = -1 } +impl-trait-redundant-captures = { level = "deny", priority = -1 } +linker-messages = { level = "deny", priority = -1 } +#lossy-provenance-casts = { level = "deny", priority = -1 } +macro-use-extern-crate = { level = "deny", priority = -1 } +meta-variable-misuse = { level = "deny", priority = -1 } +missing-copy-implementations = { level = "deny", priority = -1 } +missing-debug-implementations = { level = "deny", priority = -1 } +missing-docs = { level = "deny", priority = -1 } +#multiple-supertrait-upcastable = { level = "deny", priority = -1 } +#must-not-suspend = { level = "deny", priority = -1 } +non-ascii-idents = { level = "deny", priority = -1 } +#non-exhaustive-omitted-patterns = { level = "deny", priority = -1 } +redundant-imports = { level = "deny", priority = -1 } +redundant-lifetimes = { level = "deny", priority = -1 } +#resolving-to-items-shadowing-supertrait-items = { level = "deny", priority = -1 } +#shadowing-supertrait-items = { level = "deny", priority = -1 } +single-use-lifetimes = { level = "deny", priority = -1 } +trivial-casts = { level = "deny", priority = -1 } +trivial-numeric-casts = { level = "deny", priority = -1 } +unit-bindings = { level = "deny", priority = -1 } +unnameable-types = { level = "deny", priority = -1 } +#unqualified-local-imports = { level = "deny", priority = -1 } +unreachable-pub = { level = "deny", priority = -1 } +unsafe-code = { level = "deny", priority = -1 } +unstable-features = { level = "deny", priority = -1 } +unused-crate-dependencies = { level = "deny", priority = -1 } +unused-import-braces = { level = "deny", priority = -1 } +unused-lifetimes = { level = "deny", priority = -1 } +unused-qualifications = { level = "deny", priority = -1 } +unused-results = { level = "deny", priority = -1 } +variant-size-differences = { level = "deny", priority = -1 } [lints.clippy] cargo = { level = "deny", priority = -1 } @@ -76,6 +77,7 @@ suspicious = { level = "deny", priority = -1 } # Noisy, opinionated, and likely don't prevent bugs or improve APIs. arbitrary_source_item_ordering = "allow" blanket_clippy_restriction_lints = "allow" +decimal_bitwise_operands = "allow" exhaustive_enums = "allow" exhaustive_structs = "allow" implicit_return = "allow" @@ -93,6 +95,9 @@ single_call_fn = "allow" single_char_lifetime_names = "allow" unseparated_literal_suffix = "allow" +[lints.rustdoc] +all = { level = "deny", priority = -1 } + [package.metadata.docs.rs] all-features = true default-target = "x86_64-unknown-linux-gnu" @@ -110,20 +115,20 @@ targets = [ ] [dependencies] -base64url_nopad = { version = "0.1.2", default-features = false } +base64url_nopad = { version = "0.1.4", default-features = false } ed25519-dalek = { version = "2.2.0", default-features = false } hashbrown = { version = "0.16.1", default-features = false } p256 = { version = "0.13.2", default-features = false, features = ["ecdsa"] } p384 = { version = "0.13.1", default-features = false, features = ["ecdsa"] } precis-profiles = { version = "0.1.13", default-features = false } -rand = { version = "0.9.2", default-features = false, features = ["thread_rng"] } +rand = { version = "0.10.0", default-features = false, features = ["thread_rng"] } rsa = { version = "0.9.10", default-features = false, features = ["sha2"] } serde = { version = "1.0.228", default-features = false, features = ["alloc"], optional = true } serde_json = { version = "1.0.149", default-features = false, features = ["alloc"], optional = true } url = { version = "2.5.8", default-features = false } [dev-dependencies] -base64url_nopad = { version = "0.1.2", default-features = false, features = ["alloc"] } +base64url_nopad = { version = "0.1.4", default-features = false, features = ["alloc"] } ed25519-dalek = { version = "2.2.0", default-features = false, features = ["alloc", "pkcs8"] } p256 = { version = "0.13.2", default-features = false, features = ["pem"] } p384 = { version = "0.13.1", default-features = false, features = ["pkcs8"] } diff --git a/src/hash/hash_map.rs b/src/hash/hash_map.rs @@ -6,7 +6,7 @@ use hashbrown::{ Equivalent, TryReserveError, hash_map::{Drain, Entry, EntryRef, ExtractIf, HashMap, IterMut, OccupiedError, ValuesMut}, }; -#[cfg(not(feature = "serializable_server_state"))] +#[cfg(any(doc, not(feature = "serializable_server_state")))] use std::time::Instant; #[cfg(feature = "serializable_server_state")] use std::time::SystemTime; @@ -121,27 +121,230 @@ impl<K, V, S> MaxLenHashMap<K, V, S> { } impl<K: TimedCeremony, V, S> MaxLenHashMap<K, V, S> { /// Removes all expired ceremonies. + /// + /// `None` is returned iff at least one expired ceremony was removed; otherwise returns the earliest + /// expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead. + #[cfg_attr(docsrs, doc(auto_cfg = false))] + #[cfg(any(doc, not(feature = "serializable_server_state")))] #[inline] - pub fn remove_expired_ceremonies(&mut self) { + pub fn remove_expired_ceremonies(&mut self) -> Option<Instant> { // Even though it's more accurate to check the current `Instant` for each ceremony, we elect to capture // the `Instant` we begin iteration for performance reasons. It's unlikely an appreciable amount of // additional ceremonies would be removed. - #[cfg(not(feature = "serializable_server_state"))] let now = Instant::now(); - #[cfg(feature = "serializable_server_state")] + let mut some = true; + let mut expiry_min = None; + self.retain(|k, _| { + let expiry = k.expiration(); + if expiry >= now { + match expiry_min { + None => expiry_min = Some(expiry), + Some(ref mut e) if expiry < *e => *e = expiry, + _ => {} + } + true + } else { + some = false; + false + } + }); + if some { expiry_min } else { None } + } + /// Removes all expired ceremonies. + /// + /// `None` is returned iff at least one expired ceremony was removed; otherwise returns the earliest + /// expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead. + #[cfg(all(not(doc), feature = "serializable_server_state"))] + #[inline] + pub fn remove_expired_ceremonies(&mut self) -> Option<SystemTime> { + // Even though it's more accurate to check the current `SystemTime` for each ceremony, we elect to capture + // the `SystemTime` we begin iteration for performance reasons. It's unlikely an appreciable amount of + // additional ceremonies would be removed. let now = SystemTime::now(); - self.retain(|v, _| v.expiration() >= now); + let mut some = true; + let mut expiry_min = None; + self.retain(|k, _| { + let expiry = k.expiration(); + if expiry >= now { + match expiry_min { + None => expiry_min = Some(expiry), + Some(ref mut e) if expiry < *e => *e = expiry, + _ => {} + } + true + } else { + some = false; + false + } + }); + if some { expiry_min } else { None } } /// Removes the first encountered expired ceremony. + /// + /// `None` is returned iff an expired ceremony was removed; otherwise returns the earliest + /// expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead. + #[cfg_attr(docsrs, doc(auto_cfg = false))] + #[cfg(any(doc, not(feature = "serializable_server_state")))] #[inline] - pub fn remove_first_expired_ceremony(&mut self) { - #[cfg(not(feature = "serializable_server_state"))] + pub fn remove_first_expired_ceremony(&mut self) -> Option<Instant> { + // Even though it's more accurate to check the current `Instant` for each ceremony, we elect to capture + // the `Instant` we begin iteration for performance reasons. It's unlikely an appreciable amount of + // additional ceremonies would be removed. let now = Instant::now(); - #[cfg(feature = "serializable_server_state")] + let mut expiry_min = None; + self.0 + .extract_if(|k, _| { + let expiry = k.expiration(); + if expiry < now { + true + } else { + match expiry_min { + None => expiry_min = Some(expiry), + Some(ref mut e) if expiry < *e => *e = expiry, + _ => {} + } + false + } + }) + .next() + .map_or(expiry_min, |_| None) + } + /// Removes the first encountered expired ceremony. + /// + /// `None` is returned iff an expired ceremony was removed; otherwise returns the earliest + /// expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead. + #[cfg(all(not(doc), feature = "serializable_server_state"))] + #[inline] + pub fn remove_first_expired_ceremony(&mut self) -> Option<SystemTime> { + // Even though it's more accurate to check the current `SystemTime` for each ceremony, we elect to capture + // the `SystemTime` we begin iteration for performance reasons. It's unlikely an appreciable amount of + // additional ceremonies would be removed. let now = SystemTime::now(); - drop(self.0.extract_if(|k, _| k.expiration() < now).next()); + let mut expiry_min = None; + self.0 + .extract_if(|k, _| { + let expiry = k.expiration(); + if expiry < now { + true + } else { + match expiry_min { + None => expiry_min = Some(expiry), + Some(ref mut e) if expiry < *e => *e = expiry, + _ => {} + } + false + } + }) + .next() + .map_or(expiry_min, |_| None) } } +/// Signifies the consequences of [`MaxLenHashMap::insert`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Insert<V> { + /// The entry was successfully inserted. + Success, + /// The value replaced the contained value. + Previous(V), + /// The key does not exist, but there was no available capacity to insert the entry. + CapacityFull, +} +/// Error returned from [`MaxLenHashMap::try_insert`]. +#[derive(Debug)] +pub enum FullCapOccupiedErr<'a, K, V, S> { + /// Error when the key already exists. + Occupied(OccupiedError<'a, K, V, S>), + /// Error when there is no available capacity and the key does not exist. + CapacityFull, +} +/// Error returned from [`MaxLenHashMap::try_insert_remove_expired`] and +/// [`MaxLenHashMap::try_insert_remove_all_expired`]. +#[derive(Debug)] +pub enum FullCapRemoveExpiredOccupiedErr<'a, K, V, S> { + /// Error when the key already exists. + Occupied(OccupiedError<'a, K, V, S>), + /// Error when there was is no available capacity to insert the entry and no expired + /// [`TimedCeremony`]s could be removed. + /// + /// The contained `Instant` is the earliest expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is contained instead. + #[cfg_attr(docsrs, doc(auto_cfg = false))] + #[cfg(any(doc, not(feature = "serializable_server_state")))] + CapacityFull(Instant), + /// Error when there was is no available capacity to insert the entry and no expired + /// [`TimedCeremony`]s could be removed. + /// + /// The contained `Instant` is the earliest expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is contained instead. + #[cfg(all(not(doc), feature = "serializable_server_state"))] + CapacityFull(SystemTime), +} +/// Signifies the consequences of [`MaxLenHashMap::insert_remove_expired`] and +/// [`MaxLenHashMap::insert_remove_all_expired`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum InsertRemoveExpired<V> { + /// The entry was successfully inserted. + Success, + /// The value replaced the contained value. + Previous(V), + /// The key does not exist, but there was no available capacity to insert the entry and no expired + /// [`TimedCeremony`]s could be removed. + /// + /// The contained `Instant` is the earliest expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is contained instead. + #[cfg_attr(docsrs, doc(auto_cfg = false))] + #[cfg(any(doc, not(feature = "serializable_server_state")))] + CapacityFull(Instant), + /// The value does not exist, but there was no available capacity to insert it and no expired + /// [`TimedCeremony`]s that could be removed. + /// + /// The contained `Instant` is the earliest expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is contained instead. + #[cfg(all(not(doc), feature = "serializable_server_state"))] + CapacityFull(SystemTime), +} +/// Signifies the consequences of [`MaxLenHashMap::entry`] and [`MaxLenHashMap::entry_ref`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EntryStatus<E> { + /// The entry was successfully grabbed. + Success(E), + /// The capacity was full, and there was no value where the entry would be. + CapacityFull, +} +/// Signifies the consequences of [`MaxLenHashMap::entry_remove_expired`] and +/// [`MaxLenHashMap::entry_remove_all_expired`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EntryStatusRemoveExpired<E> { + /// The entry was successfully grabbed. + Success(E), + /// The capacity was full, and there was no value where the entry would be. + /// + /// The contained `Instant` is the earliest expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is contained instead. + #[cfg_attr(docsrs, doc(auto_cfg = false))] + #[cfg(any(doc, not(feature = "serializable_server_state")))] + CapacityFull(Instant), + /// The capacity was full, and there was no value where the entry would be. + /// + /// The contained `Instant` is the earliest expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is contained instead. + #[cfg(all(not(doc), feature = "serializable_server_state"))] + CapacityFull(SystemTime), +} impl<K: Eq + Hash, V, S: BuildHasher> MaxLenHashMap<K, V, S> { /// [`HashMap::with_hasher`] using `hasher` followed by [`HashMap::try_reserve`] using `2 * max_len`. /// @@ -202,172 +405,239 @@ impl<K: Eq + Hash, V, S: BuildHasher> MaxLenHashMap<K, V, S> { } /// [`HashMap::try_insert`]. /// - /// `Ok(None)` is returned iff [`HashMap::len`] `==` [`Self::max_len`] and `key` does not already exist in - /// the map. - /// /// # Errors /// - /// Errors iff [`HashMap::insert`] does. + /// Errors iff [`HashMap::try_insert`] does or there is no available capacity to insert the entry. #[inline] pub fn try_insert( &mut self, key: K, value: V, - ) -> Result<Option<&mut V>, OccupiedError<'_, K, V, S>> { + ) -> Result<&mut V, FullCapOccupiedErr<'_, K, V, S>> { let full = self.0.len() == self.1; match self.0.entry(key) { - Entry::Occupied(entry) => Err(OccupiedError { entry, value }), + Entry::Occupied(entry) => { + Err(FullCapOccupiedErr::Occupied(OccupiedError { entry, value })) + } Entry::Vacant(ent) => { if full { - Ok(None) + Err(FullCapOccupiedErr::CapacityFull) } else { - Ok(Some(ent.insert(value))) + Ok(ent.insert(value)) } } } } /// [`HashMap::insert`]. - /// - /// `None` is returned iff [`HashMap::len`] `==` [`Self::max_len`] and `key` does not already exist in the - /// map. #[inline] - pub fn insert(&mut self, k: K, v: V) -> Option<Option<V>> { + pub fn insert(&mut self, k: K, v: V) -> Insert<V> { let full = self.0.len() == self.1; match self.0.entry(k) { - Entry::Occupied(mut ent) => Some(Some(ent.insert(v))), + Entry::Occupied(mut ent) => Insert::Previous(ent.insert(v)), Entry::Vacant(ent) => { if full { - None + Insert::CapacityFull } else { _ = ent.insert(v); - Some(None) + Insert::Success } } } } /// [`HashMap::entry`]. - /// - /// `None` is returned iff [`HashMap::len`] `==` [`Self::max_len`] and `key` does not already exist in the - /// map. #[inline] - pub fn entry(&mut self, key: K) -> Option<Entry<'_, K, V, S>> { + pub fn entry(&mut self, key: K) -> EntryStatus<Entry<'_, K, V, S>> { let full = self.0.len() == self.1; match self.0.entry(key) { - ent @ Entry::Occupied(_) => Some(ent), + ent @ Entry::Occupied(_) => EntryStatus::Success(ent), ent @ Entry::Vacant(_) => { if full { - None + EntryStatus::CapacityFull } else { - Some(ent) + EntryStatus::Success(ent) } } } } /// [`HashMap::entry_ref`]. - /// - /// `None` is returned iff [`HashMap::len`] `==` [`Self::max_len`] and `key` does not already exist in the - /// map. #[inline] pub fn entry_ref<'a, 'b, Q: Equivalent<K> + Hash + ?Sized>( &'a mut self, key: &'b Q, - ) -> Option<EntryRef<'a, 'b, K, Q, V, S>> { + ) -> EntryStatus<EntryRef<'a, 'b, K, Q, V, S>> { let full = self.0.len() == self.1; match self.0.entry_ref(key) { - ent @ EntryRef::Occupied(_) => Some(ent), + ent @ EntryRef::Occupied(_) => EntryStatus::Success(ent), ent @ EntryRef::Vacant(_) => { if full { - None + EntryStatus::CapacityFull } else { - Some(ent) + EntryStatus::Success(ent) } } } } } impl<K: Eq + Hash + TimedCeremony, V, S: BuildHasher> MaxLenHashMap<K, V, S> { - /// [`Self::insert`] except the first encountered expired ceremony is removed in the event [`Self::max_len`] + /// [`Self::try_insert`] except the first encountered expired ceremony is removed in the event [`Self::max_len`] /// entries have been added. + /// + /// # Errors + /// + /// Errors iff [`HashMap::try_insert`] does after removing the first expired ceremony. #[inline] - pub fn insert_remove_expired(&mut self, k: K, v: V) -> Option<Option<V>> { + pub fn try_insert_remove_expired( + &mut self, + key: K, + value: V, + ) -> Result<&mut V, FullCapRemoveExpiredOccupiedErr<'_, K, V, S>> { if self.0.len() == self.1 { - #[cfg(not(feature = "serializable_server_state"))] - let now = Instant::now(); - #[cfg(feature = "serializable_server_state")] - let now = SystemTime::now(); - if self - .0 - .extract_if(|exp, _| exp.expiration() < now) - .next() - .is_some() - { - Some(self.0.insert(k, v)) - } else if let Entry::Occupied(mut ent) = self.0.entry(k) { - Some(Some(ent.insert(v))) - } else { - None + match self.remove_first_expired_ceremony() { + None => self + .0 + .try_insert(key, value) + .map_err(FullCapRemoveExpiredOccupiedErr::Occupied), + Some(exp) => { + if let Entry::Occupied(entry) = self.0.entry(key) { + Err(FullCapRemoveExpiredOccupiedErr::Occupied(OccupiedError { + entry, + value, + })) + } else { + Err(FullCapRemoveExpiredOccupiedErr::CapacityFull(exp)) + } + } } } else { - Some(self.0.insert(k, v)) + self.0 + .try_insert(key, value) + .map_err(FullCapRemoveExpiredOccupiedErr::Occupied) } } - /// [`Self::insert`] except all expired ceremonies are removed in the event [`Self::max_len`] entries have + /// [`Self::try_insert`] except all expired ceremonies are removed in the event [`Self::max_len`] entries have /// been added. + /// + /// # Errors + /// + /// Errors iff [`HashMap::try_insert`] does after removing all expired ceremonies. #[inline] - pub fn insert_remove_all_expired(&mut self, k: K, v: V) -> Option<Option<V>> { + pub fn try_insert_remove_all_expired( + &mut self, + key: K, + value: V, + ) -> Result<&mut V, FullCapRemoveExpiredOccupiedErr<'_, K, V, S>> { if self.0.len() == self.1 { - self.remove_expired_ceremonies(); + match self.remove_expired_ceremonies() { + None => self + .0 + .try_insert(key, value) + .map_err(FullCapRemoveExpiredOccupiedErr::Occupied), + Some(exp) => { + if let Entry::Occupied(entry) = self.0.entry(key) { + Err(FullCapRemoveExpiredOccupiedErr::Occupied(OccupiedError { + entry, + value, + })) + } else { + Err(FullCapRemoveExpiredOccupiedErr::CapacityFull(exp)) + } + } + } + } else { + self.0 + .try_insert(key, value) + .map_err(FullCapRemoveExpiredOccupiedErr::Occupied) } + } + /// [`Self::insert`] except the first encountered expired ceremony is removed in the event [`Self::max_len`] + /// entries have been added. + #[inline] + pub fn insert_remove_expired(&mut self, k: K, v: V) -> InsertRemoveExpired<V> { if self.0.len() == self.1 { - if let Entry::Occupied(mut ent) = self.0.entry(k) { - Some(Some(ent.insert(v))) - } else { - None + match self.remove_first_expired_ceremony() { + None => self.0.insert(k, v).map_or_else( + || InsertRemoveExpired::Success, + InsertRemoveExpired::Previous, + ), + Some(exp) => { + if let Entry::Occupied(mut ent) = self.0.entry(k) { + InsertRemoveExpired::Previous(ent.insert(v)) + } else { + InsertRemoveExpired::CapacityFull(exp) + } + } + } + } else { + self.0.insert(k, v).map_or_else( + || InsertRemoveExpired::Success, + InsertRemoveExpired::Previous, + ) + } + } + /// [`Self::insert`] except all expired ceremonies are removed in the event [`Self::max_len`] entries have + /// been added. + #[inline] + pub fn insert_remove_all_expired(&mut self, k: K, v: V) -> InsertRemoveExpired<V> { + if self.0.len() == self.1 { + match self.remove_expired_ceremonies() { + None => self.0.insert(k, v).map_or_else( + || InsertRemoveExpired::Success, + InsertRemoveExpired::Previous, + ), + Some(exp) => { + if let Entry::Occupied(mut ent) = self.0.entry(k) { + InsertRemoveExpired::Previous(ent.insert(v)) + } else { + InsertRemoveExpired::CapacityFull(exp) + } + } } } else { - Some(self.0.insert(k, v)) + self.0.insert(k, v).map_or_else( + || InsertRemoveExpired::Success, + InsertRemoveExpired::Previous, + ) } } /// [`Self::entry`] except the first encountered expired ceremony is removed in the event [`Self::max_len`] /// entries have been added. #[inline] - pub fn entry_remove_expired(&mut self, key: K) -> Option<Entry<'_, K, V, S>> { + pub fn entry_remove_expired(&mut self, key: K) -> EntryStatusRemoveExpired<Entry<'_, K, V, S>> { if self.0.len() == self.1 { - #[cfg(not(feature = "serializable_server_state"))] - let now = Instant::now(); - #[cfg(feature = "serializable_server_state")] - let now = SystemTime::now(); - if self - .0 - .extract_if(|v, _| v.expiration() < now) - .next() - .is_some() - { - Some(self.0.entry(key)) - } else if let ent @ Entry::Occupied(_) = self.0.entry(key) { - Some(ent) - } else { - None + match self.remove_first_expired_ceremony() { + None => EntryStatusRemoveExpired::Success(self.0.entry(key)), + Some(exp) => { + if let ent @ Entry::Occupied(_) = self.0.entry(key) { + EntryStatusRemoveExpired::Success(ent) + } else { + EntryStatusRemoveExpired::CapacityFull(exp) + } + } } } else { - Some(self.0.entry(key)) + EntryStatusRemoveExpired::Success(self.0.entry(key)) } } /// [`Self::entry`] except all expired ceremonies are removed in the event [`Self::max_len`] entries have /// been added. #[inline] - pub fn entry_remove_all_expired(&mut self, key: K) -> Option<Entry<'_, K, V, S>> { - if self.0.len() == self.1 { - self.remove_expired_ceremonies(); - } + pub fn entry_remove_all_expired( + &mut self, + key: K, + ) -> EntryStatusRemoveExpired<Entry<'_, K, V, S>> { if self.0.len() == self.1 { - if let ent @ Entry::Occupied(_) = self.0.entry(key) { - Some(ent) - } else { - None + match self.remove_expired_ceremonies() { + None => EntryStatusRemoveExpired::Success(self.0.entry(key)), + Some(exp) => { + if let ent @ Entry::Occupied(_) = self.0.entry(key) { + EntryStatusRemoveExpired::Success(ent) + } else { + EntryStatusRemoveExpired::CapacityFull(exp) + } + } } } else { - Some(self.0.entry(key)) + EntryStatusRemoveExpired::Success(self.0.entry(key)) } } /// [`Self::entry_ref`] except the first encoutered expired ceremony is removed in the event [`Self::max_len`] @@ -376,26 +646,20 @@ impl<K: Eq + Hash + TimedCeremony, V, S: BuildHasher> MaxLenHashMap<K, V, S> { pub fn entry_ref_remove_expired<'a, 'b, Q: Equivalent<K> + Hash + ?Sized>( &'a mut self, key: &'b Q, - ) -> Option<EntryRef<'a, 'b, K, Q, V, S>> { + ) -> EntryStatusRemoveExpired<EntryRef<'a, 'b, K, Q, V, S>> { if self.0.len() == self.1 { - #[cfg(not(feature = "serializable_server_state"))] - let now = Instant::now(); - #[cfg(feature = "serializable_server_state")] - let now = SystemTime::now(); - if self - .0 - .extract_if(|v, _| v.expiration() < now) - .next() - .is_some() - { - Some(self.0.entry_ref(key)) - } else if let ent @ EntryRef::Occupied(_) = self.0.entry_ref(key) { - Some(ent) - } else { - None + match self.remove_first_expired_ceremony() { + None => EntryStatusRemoveExpired::Success(self.0.entry_ref(key)), + Some(exp) => { + if let ent @ EntryRef::Occupied(_) = self.0.entry_ref(key) { + EntryStatusRemoveExpired::Success(ent) + } else { + EntryStatusRemoveExpired::CapacityFull(exp) + } + } } } else { - Some(self.0.entry_ref(key)) + EntryStatusRemoveExpired::Success(self.0.entry_ref(key)) } } /// [`Self::entry_ref`] except all expired ceremonies are removed in the event [`Self::max_len`] entries have @@ -404,18 +668,20 @@ impl<K: Eq + Hash + TimedCeremony, V, S: BuildHasher> MaxLenHashMap<K, V, S> { pub fn entry_ref_remove_all_expired<'a, 'b, Q: Equivalent<K> + Hash + ?Sized>( &'a mut self, key: &'b Q, - ) -> Option<EntryRef<'a, 'b, K, Q, V, S>> { - if self.0.len() == self.1 { - self.remove_expired_ceremonies(); - } + ) -> EntryStatusRemoveExpired<EntryRef<'a, 'b, K, Q, V, S>> { if self.0.len() == self.1 { - if let ent @ EntryRef::Occupied(_) = self.0.entry_ref(key) { - Some(ent) - } else { - None + match self.remove_expired_ceremonies() { + None => EntryStatusRemoveExpired::Success(self.0.entry_ref(key)), + Some(exp) => { + if let ent @ EntryRef::Occupied(_) = self.0.entry_ref(key) { + EntryStatusRemoveExpired::Success(ent) + } else { + EntryStatusRemoveExpired::CapacityFull(exp) + } + } } } else { - Some(self.0.entry_ref(key)) + EntryStatusRemoveExpired::Success(self.0.entry_ref(key)) } } } @@ -431,3 +697,91 @@ impl<K, V, S> From<MaxLenHashMap<K, V, S>> for HashMap<K, V, S> { value.0 } } +#[cfg(test)] +mod tests { + use super::{Equivalent, Insert, InsertRemoveExpired, MaxLenHashMap, TimedCeremony}; + use core::hash::{Hash, Hasher}; + #[cfg(not(feature = "serializable_server_state"))] + use std::time::Instant; + #[cfg(feature = "serializable_server_state")] + use std::time::SystemTime; + #[derive(Clone, Copy)] + struct Ceremony { + id: usize, + #[cfg(not(feature = "serializable_server_state"))] + exp: Instant, + #[cfg(feature = "serializable_server_state")] + exp: SystemTime, + } + impl Default for Ceremony { + fn default() -> Self { + Self { + id: 0, + #[cfg(not(feature = "serializable_server_state"))] + exp: Instant::now(), + #[cfg(feature = "serializable_server_state")] + exp: SystemTime::now(), + } + } + } + impl PartialEq for Ceremony { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } + } + impl Eq for Ceremony {} + impl Hash for Ceremony { + fn hash<H: Hasher>(&self, state: &mut H) { + self.id.hash(state); + } + } + impl TimedCeremony for Ceremony { + #[cfg(not(feature = "serializable_server_state"))] + fn expiration(&self) -> Instant { + self.exp + } + #[cfg(feature = "serializable_server_state")] + fn expiration(&self) -> SystemTime { + self.exp + } + } + impl Equivalent<Ceremony> for usize { + #[inline] + fn equivalent(&self, key: &Ceremony) -> bool { + *self == key.id + } + } + #[test] + fn hash_map_insert_removed() { + const REQ_MAX_LEN: usize = 8; + let mut map = MaxLenHashMap::new(REQ_MAX_LEN); + let cap = map.as_ref().capacity(); + let max_len = map.max_len(); + assert_eq!(cap >> 1u8, max_len); + assert!(max_len >= REQ_MAX_LEN); + let mut cer = Ceremony::default(); + for i in 0..max_len { + assert!(map.as_ref().capacity() <= cap); + cer.id = i; + assert_eq!(map.insert(cer, i), Insert::Success); + } + assert!(map.as_ref().capacity() <= cap); + assert_eq!(map.as_ref().len(), max_len); + for i in 0..max_len { + assert!(map.as_ref().contains_key(&i)); + } + cer.id = cap; + assert_eq!( + map.insert_remove_expired(cer, 10), + InsertRemoveExpired::Success + ); + assert!(map.as_ref().capacity() <= cap); + assert_eq!(map.as_ref().len(), max_len); + let mut counter = 0; + for i in 0..max_len { + counter += usize::from(map.as_ref().contains_key(&i)); + } + assert_eq!(counter, max_len - 1); + assert!(map.as_ref().contains_key(&(max_len - 1))); + } +} diff --git a/src/hash/hash_set.rs b/src/hash/hash_set.rs @@ -6,7 +6,7 @@ use hashbrown::{ Equivalent, TryReserveError, hash_set::{Drain, Entry, ExtractIf, HashSet}, }; -#[cfg(not(feature = "serializable_server_state"))] +#[cfg(any(doc, not(feature = "serializable_server_state")))] use std::time::Instant; #[cfg(feature = "serializable_server_state")] use std::time::SystemTime; @@ -107,27 +107,208 @@ impl<T, S> MaxLenHashSet<T, S> { } impl<T: TimedCeremony, S> MaxLenHashSet<T, S> { /// Removes all expired ceremonies. + /// + /// `None` is returned iff at least one expired ceremony was removed; otherwise returns the earliest + /// expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead. + #[cfg_attr(docsrs, doc(auto_cfg = false))] + #[cfg(any(doc, not(feature = "serializable_server_state")))] #[inline] - pub fn remove_expired_ceremonies(&mut self) { + pub fn remove_expired_ceremonies(&mut self) -> Option<Instant> { // Even though it's more accurate to check the current `Instant` for each ceremony, we elect to capture // the `Instant` we begin iteration for performance reasons. It's unlikely an appreciable amount of // additional ceremonies would be removed. - #[cfg(not(feature = "serializable_server_state"))] let now = Instant::now(); - #[cfg(feature = "serializable_server_state")] + let mut some = true; + let mut expiry_min = None; + self.retain(|v| { + let expiry = v.expiration(); + if expiry >= now { + match expiry_min { + None => expiry_min = Some(expiry), + Some(ref mut e) if expiry < *e => *e = expiry, + _ => (), + } + true + } else { + some = false; + false + } + }); + if some { expiry_min } else { None } + } + /// Removes all expired ceremonies. + /// + /// `None` is returned iff at least one expired ceremony was removed; otherwise returns the earliest + /// expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead. + #[cfg(all(not(doc), feature = "serializable_server_state"))] + #[inline] + pub fn remove_expired_ceremonies(&mut self) -> Option<SystemTime> { + // Even though it's more accurate to check the current `SystemTime` for each ceremony, we elect to capture + // the `SystemTime` we begin iteration for performance reasons. It's unlikely an appreciable amount of + // additional ceremonies would be removed. let now = SystemTime::now(); - self.retain(|v| v.expiration() >= now); + let mut some = true; + let mut expiry_min = None; + self.retain(|v| { + let expiry = v.expiration(); + if expiry >= now { + match expiry_min { + None => expiry_min = Some(expiry), + Some(ref mut e) if expiry < *e => *e = expiry, + _ => (), + } + true + } else { + some = false; + false + } + }); + if some { expiry_min } else { None } } /// Removes the first encountered expired ceremony. + /// + /// `None` is returned iff an expired ceremony was removed; otherwise returns the earliest + /// expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead. + #[cfg_attr(docsrs, doc(auto_cfg = false))] + #[cfg(any(doc, not(feature = "serializable_server_state")))] #[inline] - pub fn remove_first_expired_ceremony(&mut self) { - #[cfg(not(feature = "serializable_server_state"))] + pub fn remove_first_expired_ceremony(&mut self) -> Option<Instant> { + // Even though it's more accurate to check the current `Instant` for each ceremony, we elect to capture + // the `Instant` we begin iteration for performance reasons. It's unlikely an appreciable amount of + // additional ceremonies would be removed. let now = Instant::now(); - #[cfg(feature = "serializable_server_state")] + let mut expiry_min = None; + self.0 + .extract_if(|v| { + let expiry = v.expiration(); + if expiry < now { + true + } else { + match expiry_min { + None => expiry_min = Some(expiry), + Some(ref mut e) if expiry < *e => *e = expiry, + _ => (), + } + false + } + }) + .next() + .map_or(expiry_min, |_| None) + } + /// Removes the first encountered expired ceremony. + /// + /// `None` is returned iff an expired ceremony was removed; otherwise returns the earliest + /// expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead. + #[cfg(all(not(doc), feature = "serializable_server_state"))] + #[inline] + pub fn remove_first_expired_ceremony(&mut self) -> Option<SystemTime> { + // Even though it's more accurate to check the current `SystemTime` for each ceremony, we elect to capture + // the `SystemTime` we begin iteration for performance reasons. It's unlikely an appreciable amount of + // additional ceremonies would be removed. let now = SystemTime::now(); - drop(self.0.extract_if(|v| v.expiration() < now).next()); + let mut expiry_min = None; + self.0 + .extract_if(|v| { + let expiry = v.expiration(); + if expiry < now { + true + } else { + match expiry_min { + None => expiry_min = Some(expiry), + Some(ref mut e) if expiry < *e => *e = expiry, + _ => (), + } + false + } + }) + .next() + .map_or(expiry_min, |_| None) } } +/// Signifies the consequences of [`MaxLenHashSet::insert`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Insert { + /// The value was successfully inserted. + Success, + /// The value was not inserted since it already existed. + Duplicate, + /// The value does not exist, but there was no available capacity to insert it. + CapacityFull, +} +/// Signifies the consequences of [`MaxLenHashSet::replace`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Replace<T> { + /// The value was inserted. + Insert, + /// The value replaced the contained value. + Previous(T), + /// The value does not exist, but there was no available capacity to insert it. + CapacityFull, +} +/// Signifies the consequences of [`MaxLenHashSet::insert_remove_expired`] and +/// [`MaxLenHashSet::insert_remove_all_expired`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum InsertRemoveExpired { + /// The value was successfully inserted. + Success, + /// The value was not inserted since it already existed. + Duplicate, + /// The value does not exist, but there was no available capacity to insert it and no expired + /// [`TimedCeremony`]s that could be removed. + /// + /// The contained `Instant` is the earliest expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is contained instead. + #[cfg_attr(docsrs, doc(auto_cfg = false))] + #[cfg(any(doc, not(feature = "serializable_server_state")))] + CapacityFull(Instant), + /// The value does not exist, but there was no available capacity to insert it and no expired + /// [`TimedCeremony`]s could be removed. + /// + /// The contained `Instant` is the earliest expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is contained instead. + #[cfg(all(not(doc), feature = "serializable_server_state"))] + CapacityFull(SystemTime), +} +/// Signifies the consequences of [`MaxLenHashSet::entry`]. +#[derive(Debug)] +pub enum EntryStatus<'a, T, S> { + /// The `Entry` was successfully grabbed. + Success(Entry<'a, T, S>), + /// The capacity was full, and there was no value where the `Entry` would be. + CapacityFull, +} +/// Signifies the consequences of [`MaxLenHashSet::entry_remove_expired`] and +/// [`MaxLenHashSet::entry_remove_all_expired`]. +#[derive(Debug)] +pub enum EntryStatusRemoveExpired<'a, T, S> { + /// The `Entry` was successfully grabbed. + Success(Entry<'a, T, S>), + /// The capacity was full, and there was no value where the `Entry` would be. + /// + /// The contained `Instant` is the earliest expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is contained instead. + #[cfg_attr(docsrs, doc(auto_cfg = false))] + #[cfg(any(doc, not(feature = "serializable_server_state")))] + CapacityFull(Instant), + /// The capacity was full, and there was no value where the `Entry` would be. + /// + /// The contained `Instant` is the earliest expiration. + /// + /// Note when `serializable_server_state` is enabled, [`SystemTime`] is contained instead. + #[cfg(all(not(doc), feature = "serializable_server_state"))] + CapacityFull(SystemTime), +} impl<T: Eq + Hash, S: BuildHasher> MaxLenHashSet<T, S> { /// [`HashSet::with_hasher`] using `hasher` followed by [`HashSet::try_reserve`] using `2 * max_len`. /// @@ -186,54 +367,50 @@ impl<T: Eq + Hash, S: BuildHasher> MaxLenHashSet<T, S> { self.0.take(value) } /// [`HashSet::insert`]. - /// - /// `None` is returned iff [`HashSet::len`] `==` [`Self::max_len`] and `value` does not already exist in the - /// set. #[inline] - pub fn insert(&mut self, value: T) -> Option<bool> { + pub fn insert(&mut self, value: T) -> Insert { let full = self.0.len() == self.1; if let Entry::Vacant(ent) = self.0.entry(value) { if full { - None + Insert::CapacityFull } else { _ = ent.insert(); - Some(true) + Insert::Success } } else { - Some(false) + Insert::Duplicate } } /// [`HashSet::replace`]. - /// - /// `None` is returned iff [`HashSet::len`] `==` [`Self::max_len`] and `value` does not already exist in the - /// set. + #[expect(clippy::unreachable, reason = "want to crash when there is a bug")] #[inline] - pub fn replace(&mut self, value: T) -> Option<Option<T>> { + pub fn replace(&mut self, value: T) -> Replace<T> { // Ideally we would use the Entry API to avoid searching multiple times, but one can't while also using // `replace` since there is no `OccupiedEntry::replace`. if self.0.contains(&value) { - Some(self.0.replace(value)) + Replace::Previous( + self.0 + .replace(value) + .unwrap_or_else(|| unreachable!("there is a bug in HashSet::replace")), + ) } else if self.0.len() == self.1 { - None + Replace::CapacityFull } else { _ = self.0.insert(value); - Some(None) + Replace::Insert } } /// [`HashSet::entry`]. - /// - /// `None` is returned iff [`HashSet::len`] `==` [`Self::max_len`] and `value` does not already exist in the - /// set. #[inline] - pub fn entry(&mut self, value: T) -> Option<Entry<'_, T, S>> { + pub fn entry(&mut self, value: T) -> EntryStatus<'_, T, S> { let full = self.0.len() == self.1; match self.0.entry(value) { - ent @ Entry::Occupied(_) => Some(ent), + ent @ Entry::Occupied(_) => EntryStatus::Success(ent), ent @ Entry::Vacant(_) => { if full { - None + EntryStatus::CapacityFull } else { - Some(ent) + EntryStatus::Success(ent) } } } @@ -243,69 +420,93 @@ impl<T: Eq + Hash + TimedCeremony, S: BuildHasher> MaxLenHashSet<T, S> { /// [`Self::insert`] except the first encountered expired ceremony is removed in the event [`Self::max_len`] /// items have been added. #[inline] - pub fn insert_remove_expired(&mut self, value: T) -> Option<bool> { + pub fn insert_remove_expired(&mut self, value: T) -> InsertRemoveExpired { if self.0.len() == self.1 { - #[cfg(not(feature = "serializable_server_state"))] - let now = Instant::now(); - #[cfg(feature = "serializable_server_state")] - let now = SystemTime::now(); - if self.0.extract_if(|v| v.expiration() < now).next().is_some() { - Some(self.0.insert(value)) - } else { - self.0.contains(&value).then_some(false) + match self.remove_first_expired_ceremony() { + None => { + if self.0.insert(value) { + InsertRemoveExpired::Success + } else { + InsertRemoveExpired::Duplicate + } + } + Some(exp) => { + if self.0.contains(&value) { + InsertRemoveExpired::Duplicate + } else { + InsertRemoveExpired::CapacityFull(exp) + } + } } + } else if self.0.insert(value) { + InsertRemoveExpired::Success } else { - Some(self.0.insert(value)) + InsertRemoveExpired::Duplicate } } /// [`Self::insert`] except all expired ceremones are removed in the event [`Self::max_len`] items have /// been added. #[inline] - pub fn insert_remove_all_expired(&mut self, value: T) -> Option<bool> { - if self.0.len() == self.1 { - self.remove_expired_ceremonies(); - } + pub fn insert_remove_all_expired(&mut self, value: T) -> InsertRemoveExpired { if self.0.len() == self.1 { - self.0.contains(&value).then_some(false) + match self.remove_expired_ceremonies() { + None => { + if self.0.insert(value) { + InsertRemoveExpired::Success + } else { + InsertRemoveExpired::Duplicate + } + } + Some(exp) => { + if self.0.contains(&value) { + InsertRemoveExpired::Duplicate + } else { + InsertRemoveExpired::CapacityFull(exp) + } + } + } + } else if self.0.insert(value) { + InsertRemoveExpired::Success } else { - Some(self.0.insert(value)) + InsertRemoveExpired::Duplicate } } /// [`Self::entry`] except the first encountered expired ceremony is removed in the event [`Self::max_len`] /// items have been added. #[inline] - pub fn entry_remove_expired(&mut self, value: T) -> Option<Entry<'_, T, S>> { + pub fn entry_remove_expired(&mut self, value: T) -> EntryStatusRemoveExpired<'_, T, S> { if self.0.len() == self.1 { - #[cfg(not(feature = "serializable_server_state"))] - let now = Instant::now(); - #[cfg(feature = "serializable_server_state")] - let now = SystemTime::now(); - if self.0.extract_if(|v| v.expiration() < now).next().is_some() { - Some(self.0.entry(value)) - } else if let ent @ Entry::Occupied(_) = self.0.entry(value) { - Some(ent) - } else { - None + match self.remove_first_expired_ceremony() { + None => EntryStatusRemoveExpired::Success(self.0.entry(value)), + Some(exp) => { + if let ent @ Entry::Occupied(_) = self.0.entry(value) { + EntryStatusRemoveExpired::Success(ent) + } else { + EntryStatusRemoveExpired::CapacityFull(exp) + } + } } } else { - Some(self.0.entry(value)) + EntryStatusRemoveExpired::Success(self.0.entry(value)) } } /// [`Self::entry`] except all expired ceremones are removed in the event [`Self::max_len`] items have /// been added. #[inline] - pub fn entry_remove_all_expired(&mut self, value: T) -> Option<Entry<'_, T, S>> { + pub fn entry_remove_all_expired(&mut self, value: T) -> EntryStatusRemoveExpired<'_, T, S> { if self.0.len() == self.1 { - self.remove_expired_ceremonies(); - } - if self.0.len() == self.1 { - if let ent @ Entry::Occupied(_) = self.0.entry(value) { - Some(ent) - } else { - None + match self.remove_expired_ceremonies() { + None => EntryStatusRemoveExpired::Success(self.0.entry(value)), + Some(exp) => { + if let ent @ Entry::Occupied(_) = self.0.entry(value) { + EntryStatusRemoveExpired::Success(ent) + } else { + EntryStatusRemoveExpired::CapacityFull(exp) + } + } } } else { - Some(self.0.entry(value)) + EntryStatusRemoveExpired::Success(self.0.entry(value)) } } } @@ -323,7 +524,7 @@ impl<T, S> From<MaxLenHashSet<T, S>> for HashSet<T, S> { } #[cfg(test)] mod tests { - use super::{Equivalent, MaxLenHashSet, TimedCeremony}; + use super::{Equivalent, Insert, InsertRemoveExpired, MaxLenHashSet, TimedCeremony}; use core::hash::{Hash, Hasher}; #[cfg(not(feature = "serializable_server_state"))] use std::time::Instant; @@ -370,6 +571,7 @@ mod tests { } } impl Equivalent<Ceremony> for usize { + #[inline] fn equivalent(&self, key: &Ceremony) -> bool { *self == key.id } @@ -386,7 +588,7 @@ mod tests { for i in 0..max_len { assert!(set.as_ref().capacity() <= cap); cer.id = i; - assert_eq!(set.insert(cer), Some(true)); + assert_eq!(set.insert(cer), Insert::Success); } assert!(set.as_ref().capacity() <= cap); assert_eq!(set.as_ref().len(), max_len); @@ -394,7 +596,7 @@ mod tests { assert!(set.as_ref().contains(&i)); } cer.id = cap; - assert_eq!(set.insert_remove_expired(cer), Some(true)); + assert_eq!(set.insert_remove_expired(cer), InsertRemoveExpired::Success); assert!(set.as_ref().capacity() <= cap); assert_eq!(set.as_ref().len(), max_len); let mut counter = 0; diff --git a/src/lib.rs b/src/lib.rs @@ -22,7 +22,7 @@ //! AuthenticatedCredential64, DiscoverableAuthentication64, DiscoverableAuthenticationServerState, //! DiscoverableCredentialRequestOptions, CredentialCreationOptions64, RegisteredCredential64, //! Registration, RegistrationServerState64, -//! hash::hash_set::MaxLenHashSet, +//! hash::hash_set::{InsertRemoveExpired, MaxLenHashSet}, //! request::{ //! PublicKeyCredentialDescriptor, RpId, //! auth::AuthenticationVerificationOptions, @@ -123,7 +123,7 @@ //! .unwrap_or_else(|_e| { //! unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error") //! }); -//! if reg_ceremonies.insert_remove_all_expired(server).is_some_and(convert::identity) +//! if matches!(reg_ceremonies.insert_remove_all_expired(server), InsertRemoveExpired::Success) //! { //! Ok(serde_json::to_vec(&client) //! .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState64::serialize"))) @@ -175,7 +175,7 @@ //! .unwrap_or_else(|_e| { //! unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error") //! }); -//! if reg_ceremonies.insert_remove_all_expired(server).is_some_and(convert::identity) +//! if matches!(reg_ceremonies.insert_remove_all_expired(server), InsertRemoveExpired::Success) //! { //! Ok(serde_json::to_vec(&client) //! .unwrap_or_else(|_e| unreachable!("bug in RegistrationClientState64::serialize"))) @@ -220,7 +220,7 @@ //! .unwrap_or_else(|_e| { //! unreachable!("we don't manually mutate the options and we assume the server clock is functioning; thus this won't error") //! }); -//! if auth_ceremonies.insert_remove_all_expired(server).is_some_and(convert::identity) +//! if matches!(auth_ceremonies.insert_remove_all_expired(server), InsertRemoveExpired::Success) //! { //! Ok(serde_json::to_vec(&client).unwrap_or_else(|_e| { //! unreachable!("bug in DiscoverableAuthenticationClientState::serialize") @@ -499,9 +499,13 @@ clippy::multiple_crate_versions, reason = "RustCrypto hasn't updated rand yet" )] +#![expect( + clippy::doc_paragraphs_missing_punctuation, + reason = "false positive for crate documentation having image links" +)] #![cfg_attr(docsrs, feature(doc_cfg))] -#[cfg(not(any(feature = "custom", all(feature = "bin", feature = "serde"))))] -compile_error!("'custom' must be enabled or both 'bin' and 'serde' must be enabled"); +//#[cfg(not(any(feature = "custom", all(feature = "bin", feature = "serde"))))] +//compile_error!("'custom' must be enabled or both 'bin' and 'serde' must be enabled"); #[cfg(all(doc, feature = "serde"))] use crate::request::register::ser::{ PublicKeyCredentialCreationOptionsOwned, PublicKeyCredentialUserEntityOwned, diff --git a/src/request.rs b/src/request.rs @@ -44,7 +44,7 @@ use url::Url as Uri; /// ``` /// # use core::convert; /// # use webauthn_rp::{ -/// # hash::hash_set::MaxLenHashSet, +/// # hash::hash_set::{InsertRemoveExpired, MaxLenHashSet}, /// # request::{ /// # auth::{AllowedCredentials, DiscoverableCredentialRequestOptions, NonDiscoverableCredentialRequestOptions}, /// # register::UserHandle64, @@ -56,9 +56,7 @@ use url::Url as Uri; /// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap(); /// let mut ceremonies = MaxLenHashSet::new(128); /// let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID).start_ceremony()?; -/// assert!( -/// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity) -/// ); +/// assert_eq!(ceremonies.insert_remove_all_expired(server), InsertRemoveExpired::Success); /// # #[cfg(feature = "custom")] /// let mut ceremonies_2 = MaxLenHashSet::new(128); /// # #[cfg(feature = "serde")] @@ -70,9 +68,7 @@ use url::Url as Uri; /// let (server_2, client_2) = /// NonDiscoverableCredentialRequestOptions::second_factor(RP_ID, creds).start_ceremony()?; /// # #[cfg(feature = "custom")] -/// assert!( -/// ceremonies_2.insert_remove_all_expired(server_2).map_or(false, convert::identity) -/// ); +/// assert_eq!(ceremonies_2.insert_remove_all_expired(server_2), InsertRemoveExpired::Success); /// # #[cfg(all(feature = "custom", feature = "serde"))] /// assert!(serde_json::to_string(&client_2).is_ok()); /// /// Extract `UserHandle` from session cookie. @@ -107,7 +103,7 @@ pub mod error; /// ``` /// # use core::convert; /// # use webauthn_rp::{ -/// # hash::hash_set::MaxLenHashSet, +/// # hash::hash_set::{InsertRemoveExpired, MaxLenHashSet}, /// # request::{ /// # register::{ /// # CredentialCreationOptions, DisplayName, PublicKeyCredentialUserEntity, UserHandle, USER_HANDLE_MAX_LEN, UserHandle64, @@ -130,9 +126,7 @@ pub mod error; /// let (server, client) = CredentialCreationOptions::passkey(RP_ID, user.clone(), creds) /// .start_ceremony()?; /// # #[cfg(feature = "custom")] -/// assert!( -/// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity) -/// ); +/// assert_eq!(ceremonies.insert_remove_all_expired(server), InsertRemoveExpired::Success); /// # #[cfg(all(feature = "serde", feature = "custom"))] /// assert!(serde_json::to_string(&client).is_ok()); /// # #[cfg(feature = "custom")] @@ -141,9 +135,7 @@ pub mod error; /// let (server_2, client_2) = /// CredentialCreationOptions::second_factor(RP_ID, user, creds_2).start_ceremony()?; /// # #[cfg(feature = "custom")] -/// assert!( -/// ceremonies.insert_remove_all_expired(server_2).map_or(false, convert::identity) -/// ); +/// assert_eq!(ceremonies.insert_remove_all_expired(server_2), InsertRemoveExpired::Success); /// # #[cfg(all(feature = "serde", feature = "custom"))] /// assert!(serde_json::to_string(&client_2).is_ok()); /// /// Extract `UserHandle` from session cookie or storage if this is not the first credential registered. @@ -1666,6 +1658,7 @@ pub trait TimedCeremony { /// Returns the `Instant` the ceremony expires. /// /// Note when `serializable_server_state` is enabled, [`SystemTime`] is returned instead. + #[cfg_attr(docsrs, doc(auto_cfg = false))] #[cfg(any(doc, not(feature = "serializable_server_state")))] fn expiration(&self) -> Instant; /// Returns the `SystemTime` the ceremony expires. diff --git a/src/request/register.rs b/src/request/register.rs @@ -1074,6 +1074,7 @@ impl Default for Extension<'_, '_> { } #[cfg(test)] impl PartialEq for Extension<'_, '_> { + #[inline] fn eq(&self, other: &Self) -> bool { self.cred_props == other.cred_props && self.cred_protect == other.cred_protect @@ -2429,6 +2430,16 @@ pub struct RegistrationServerState<const USER_LEN: usize> { user_id: UserHandle<USER_LEN>, } impl<const USER_LEN: usize> RegistrationServerState<USER_LEN> { + #[cfg(all(test, feature = "custom", feature = "serializable_server_state"))] + fn is_eq(&self, other: &Self) -> bool { + self.mediation == other.mediation + && self.challenge == other.challenge + && self.pub_key_cred_params == other.pub_key_cred_params + && self.authenticator_selection == other.authenticator_selection + && self.extensions == other.extensions + && self.expiration == other.expiration + && self.user_id == other.user_id + } /// Verifies `response` is valid based on `self` consuming `self` and returning a `RegisteredCredential` that /// borrows the necessary data from `response`. /// @@ -2570,18 +2581,6 @@ impl<const USER_LEN: usize> RegistrationServerState<USER_LEN> { }) } } -#[cfg(all(test, feature = "custom", feature = "serializable_server_state"))] -impl<const USER_LEN: usize> RegistrationServerState<USER_LEN> { - fn is_eq(&self, other: &Self) -> bool { - self.mediation == other.mediation - && self.challenge == other.challenge - && self.pub_key_cred_params == other.pub_key_cred_params - && self.authenticator_selection == other.authenticator_selection - && self.extensions == other.extensions - && self.expiration == other.expiration - && self.user_id == other.user_id - } -} impl<const USER_LEN: usize> TimedCeremony for RegistrationServerState<USER_LEN> { #[cfg(any(doc, not(feature = "serializable_server_state")))] #[inline] diff --git a/src/response.rs b/src/response.rs @@ -30,7 +30,7 @@ use ser_relaxed::SerdeJsonErr; /// ```no_run /// # use core::convert; /// # use webauthn_rp::{ -/// # hash::hash_set::MaxLenHashSet, +/// # hash::hash_set::{InsertRemoveExpired, MaxLenHashSet}, /// # request::{auth::{error::InvalidTimeout, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, AuthenticationVerificationOptions}, register::{UserHandle, USER_HANDLE_MAX_LEN, UserHandle64}, BackupReq, RpId}, /// # response::{auth::{error::AuthCeremonyErr, DiscoverableAuthentication64}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, ClientExtensionsOutputsStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKeyOwned, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId}, /// # AuthenticatedCredential, CredentialErr @@ -74,9 +74,7 @@ use ser_relaxed::SerdeJsonErr; /// const RP_ID: &RpId = &RpId::from_static_domain("example.com").unwrap(); /// let mut ceremonies = MaxLenHashSet::new(128); /// let (server, client) = DiscoverableCredentialRequestOptions::passkey(RP_ID).start_ceremony()?; -/// assert!( -/// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity) -/// ); +/// assert_eq!(ceremonies.insert_remove_all_expired(server), InsertRemoveExpired::Success); /// # #[cfg(feature = "serde")] /// let authentication = serde_json::from_str::<DiscoverableAuthentication64>(get_authentication_json(client).as_str())?; /// # #[cfg(feature = "serde")] @@ -144,7 +142,7 @@ pub mod error; /// ```no_run /// # use core::convert; /// # use webauthn_rp::{ -/// # hash::hash_set::MaxLenHashSet, +/// # hash::hash_set::{InsertRemoveExpired, MaxLenHashSet}, /// # request::{register::{error::CreationOptionsErr, CredentialCreationOptions, DisplayName, PublicKeyCredentialUserEntity, RegistrationClientState, UserHandle, UserHandle64, USER_HANDLE_MAX_LEN, RegistrationVerificationOptions}, PublicKeyCredentialDescriptor, RpId}, /// # response::{register::{error::RegCeremonyErr, Registration}, error::CollectedClientDataErr, CollectedClientData}, /// # RegisteredCredential @@ -189,9 +187,7 @@ pub mod error; /// # #[cfg(feature = "custom")] /// let (server, client) = CredentialCreationOptions::passkey(RP_ID, user, creds).start_ceremony()?; /// # #[cfg(feature = "custom")] -/// assert!( -/// ceremonies.insert_remove_all_expired(server).map_or(false, convert::identity) -/// ); +/// assert_eq!(ceremonies.insert_remove_all_expired(server), InsertRemoveExpired::Success); /// # #[cfg(all(feature = "serde_relaxed", feature = "custom"))] /// let registration = serde_json::from_str::<Registration>(get_registration_json(client).as_str())?; /// let ver_opts = RegistrationVerificationOptions::<&str, &str>::default(); diff --git a/src/response/register.rs b/src/response/register.rs @@ -2736,6 +2736,18 @@ impl<'a> FromCbor<'a> for AttestationFormat<'a> { } } impl<'a> AttestationObject<'a> { + /// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids). + #[inline] + #[must_use] + pub const fn attestation(&self) -> AttestationFormat<'a> { + self.attestation + } + /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data). + #[inline] + #[must_use] + pub const fn auth_data(&self) -> &AuthenticatorData<'a> { + &self.auth_data + } /// Deserializes `data` based on the /// [attestation object layout](https://www.w3.org/TR/webauthn-3/#attestation-object) /// returning [`Self`] and the index within `data` that the authenticator data portion @@ -2840,20 +2852,6 @@ pub struct AttestationObject<'a> { /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data). auth_data: AuthenticatorData<'a>, } -impl<'a> AttestationObject<'a> { - /// [Attestation statement format identifiers](https://www.w3.org/TR/webauthn-3/#sctn-attstn-fmt-ids). - #[inline] - #[must_use] - pub const fn attestation(&self) -> AttestationFormat<'a> { - self.attestation - } - /// [Authenticator data](https://www.w3.org/TR/webauthn-3/#authenticator-data). - #[inline] - #[must_use] - pub const fn auth_data(&self) -> &AuthenticatorData<'a> { - &self.auth_data - } -} impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AttestationObject<'b> { type Error = AttestationObjectErr; /// Deserializes `value` based on the