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:
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