structured_asn1/memory/map.rs
1//! Accounted storage and pre-admitted hashing for the existing hashbrown engine.
2
3use core::borrow::Borrow;
4use core::fmt;
5use core::hash::{BuildHasher, Hash};
6
7use hashbrown::hash_map::RawEntryMut;
8use hashbrown::{DefaultHashBuilder, HashMap, TryReserveError};
9
10use super::allocator::AccountAllocator;
11use super::{Memory, MemoryError, Work};
12
13#[cfg(test)]
14mod tests;
15
16/// A refused in-place filter, preserving the predicate's own cause.
17#[derive(Debug, PartialEq, Eq)]
18pub enum RetainError<E> {
19 /// The map could not admit its traversal or erase metadata.
20 Resource(MemoryError),
21 /// The caller's predicate refused further processing.
22 Predicate(E),
23}
24
25impl From<RetainError<MemoryError>> for MemoryError {
26 fn from(error: RetainError<MemoryError>) -> Self {
27 match error {
28 RetainError::Resource(reason) | RetainError::Predicate(reason) => reason,
29 }
30 }
31}
32
33/// A trusted key type's upper bound for hashing and equality work.
34///
35/// Implementations must cover all bytes examined by Hash and Eq. This is a
36/// contract for caller-provided code, not a value an untrusted document supplies.
37/// The bound must remain unchanged and successful while a key is stored, just
38/// as Hash and Eq must remain stable. Accounting retains this bound for rehashes.
39pub trait HashWork: Hash + Eq {
40 /// Work units for one hash/equality traversal, including constant overhead.
41 fn hash_work(&self) -> Result<u64, MemoryError>;
42}
43
44impl HashWork for usize {
45 fn hash_work(&self) -> Result<u64, MemoryError> {
46 Ok(1)
47 }
48}
49
50impl HashWork for u8 {
51 fn hash_work(&self) -> Result<u64, MemoryError> {
52 Ok(1)
53 }
54}
55
56impl HashWork for (usize, usize, usize) {
57 fn hash_work(&self) -> Result<u64, MemoryError> {
58 Ok(3)
59 }
60}
61
62impl HashWork for &[u8] {
63 fn hash_work(&self) -> Result<u64, MemoryError> {
64 Ok(self.len() as u64 + 1)
65 }
66}
67
68impl HashWork for str {
69 fn hash_work(&self) -> Result<u64, MemoryError> {
70 (self.len() as u64)
71 .checked_add(1)
72 .ok_or(MemoryError::WorkExceeded)
73 }
74}
75
76impl HashWork for crate::Oid<'_> {
77 fn hash_work(&self) -> Result<u64, MemoryError> {
78 Ok(self.as_bytes().len() as u64 + 1)
79 }
80}
81
82impl HashWork for (crate::Oid<'_>, crate::Oid<'_>) {
83 fn hash_work(&self) -> Result<u64, MemoryError> {
84 self.0
85 .hash_work()?
86 .checked_add(self.1.hash_work()?)
87 .ok_or(MemoryError::WorkExceeded)
88 }
89}
90
91/// A hash map whose backing allocation and operations share one resource domain.
92///
93/// Hashbrown supplies bucket/probing/SIMD machinery. Reservations include its
94/// control bytes and alignment, not merely the initialized entries. Hashing and
95/// actual collision comparisons are admitted before examining candidate keys.
96pub struct OwnedMap<K: HashWork, V, W: Work = Memory> {
97 values: HashMap<K, V, DefaultHashBuilder, AccountAllocator<W>>,
98 execution: W,
99 key_work: u64,
100 maximum_capacity: usize,
101}
102
103impl<K: HashWork + fmt::Debug, V: fmt::Debug, W: Work> fmt::Debug for OwnedMap<K, V, W> {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 self.values.fmt(f)
106 }
107}
108
109impl<K: HashWork, V, W: Work> OwnedMap<K, V, W> {
110 pub(super) fn new(execution: W) -> Self {
111 Self {
112 values: HashMap::with_hasher_in(
113 DefaultHashBuilder::default(),
114 AccountAllocator::new(execution.clone()),
115 ),
116 execution,
117 key_work: 0,
118 maximum_capacity: 0,
119 }
120 }
121
122 /// The shared account retained until the table is deallocated.
123 #[must_use]
124 pub fn memory(&self) -> &Memory {
125 self.execution.memory()
126 }
127
128 /// Number of initialized entries.
129 #[must_use]
130 pub fn len(&self) -> usize {
131 self.values.len()
132 }
133
134 /// Whether there are no entries.
135 #[must_use]
136 pub fn is_empty(&self) -> bool {
137 self.values.is_empty()
138 }
139
140 /// Borrowed entries after admitting a full control-bucket traversal in
141 /// the current execution domain, independently of retained storage.
142 /// Sparse holes count too. Partial consumption retains the conservative
143 /// prepayment; the opaque iterator exposes no cloning/restart operation.
144 ///
145 /// # Examples
146 /// ```
147 /// # extern crate alloc;
148 /// # use alloc::sync::Arc;
149 /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
150 /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
151 /// let mut map = memory.map();
152 /// map.try_insert(1usize, 42)?;
153 /// let mut entries = map.try_iter(&memory)?;
154 /// assert_eq!(entries.next(), Some((&1, &42)));
155 /// # Ok::<(), MemoryError>(())
156 /// ```
157 pub fn try_iter<'a, E: Work>(
158 &'a self,
159 execution: &E,
160 ) -> Result<impl Iterator<Item = (&'a K, &'a V)> + 'a + use<'a, K, V, W, E>, MemoryError> {
161 execution.charge_work(self.buckets()?)?;
162 Ok(self.values.iter())
163 }
164
165 /// Retains entries selected by a fallible predicate, without allocating.
166 ///
167 /// The supplied current execution admits the entire control-bucket scan,
168 /// including sparse holes, before the first predicate call. Every erase
169 /// separately admits its metadata work before removal. The predicate must
170 /// admit its own data-dependent work; arbitrary key/value destructors are
171 /// likewise caller-owned code. No key hashing or equality is performed.
172 ///
173 /// Entries are visited in unspecified order. On the first refusal no more
174 /// predicates or key-work metadata callbacks run; the engine finishes its
175 /// already admitted scan while retaining all remaining entries. Earlier
176 /// removals and predicate value edits, including an edit made by a failing
177 /// predicate, remain. Refusal never reports a successful partial filter.
178 ///
179 /// The original allocation, capacity charge and storage account remain
180 /// unchanged. Future growth still uses the map's original execution.
181 /// Filtering under another execution does not rebind that allocator.
182 ///
183 /// # Examples
184 /// ```
185 /// # extern crate alloc;
186 /// # use alloc::sync::Arc;
187 /// use structured_asn1::memory::{Memory, MemoryAccount};
188 /// let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
189 /// let mut map = memory.map();
190 /// map.try_insert(1usize, 10).unwrap();
191 /// map.try_insert(2usize, 20).unwrap();
192 /// map.try_retain(&memory, |key, value| {
193 /// *value += 1;
194 /// Ok::<_, core::convert::Infallible>(*key == 2)
195 /// }).unwrap();
196 /// assert_eq!(map.len(), 1);
197 /// assert_eq!(map.get(&2, &memory).unwrap(), Some(&21));
198 /// ```
199 pub fn try_retain<E>(
200 &mut self,
201 execution: &impl Work,
202 mut predicate: impl FnMut(&K, &mut V) -> Result<bool, E>,
203 ) -> Result<(), RetainError<E>> {
204 let buckets = self.buckets().map_err(RetainError::Resource)?;
205 execution
206 .charge_work(buckets)
207 .map_err(RetainError::Resource)?;
208 let mut failure = None;
209 let key_work = &mut self.key_work;
210 self.values.retain(|key, value| {
211 if failure.is_some() {
212 return true;
213 }
214 match predicate(key, value) {
215 Ok(true) => true,
216 Err(reason) => {
217 failure = Some(RetainError::Predicate(reason));
218 true
219 }
220 Ok(false) => {
221 // Pinned hashbrown 0.17.1 RawTableInner::erase loads the
222 // adjacent two control groups, then drops the pair in place.
223 // It neither hashes keys nor moves entries to another table.
224 if let Err(reason) = execution.charge_work(2) {
225 failure = Some(RetainError::Resource(reason));
226 return true;
227 }
228 let removed = match key.hash_work() {
229 Ok(work) => work,
230 Err(reason) => {
231 failure = Some(RetainError::Resource(reason));
232 return true;
233 }
234 };
235 // HashWork's stored bound is stable by contract. Update
236 // before hashbrown erases and invokes a possibly panicking Drop.
237 debug_assert!(*key_work >= removed);
238 *key_work -= removed;
239 false
240 }
241 }
242 });
243 match failure {
244 Some(reason) => Err(reason),
245 None => Ok(()),
246 }
247 }
248
249 fn buckets(&self) -> Result<u64, MemoryError> {
250 // Pinned hashbrown 0.17.1 raw::bucket_mask_to_capacity: tables with
251 // four/eight buckets keep one empty slot; larger tables use 7/8 of
252 // the buckets. Maximum capacity records each table immediately after
253 // reserve, before tombstones can reduce the public capacity value.
254 match self.maximum_capacity {
255 0 => Ok(0),
256 capacity @ 1..=7 => Ok(capacity as u64 + 1),
257 capacity => (capacity as u64 / 7)
258 .checked_mul(8)
259 .ok_or(MemoryError::WorkExceeded),
260 }
261 }
262
263 fn control_work(&self) -> Result<u64, MemoryError> {
264 let buckets = self.buckets()?;
265 // The pinned engine's narrowest control group is four bytes. Wider
266 // scalar/SIMD groups perform the same fixed group operations. One unit
267 // admits one group visit; triangular probing visits each group once.
268 Ok(buckets / 4)
269 }
270
271 fn rehash_work(&self) -> Result<u64, MemoryError> {
272 // reserve(1) can double the current buckets or rehash in place. Every
273 // retained key is hashed once; each slot search may visit all groups.
274 // In-place rehash additionally scans every bucket and swaps entries.
275 let buckets = self.buckets()?;
276 let groups = buckets.checked_mul(2).ok_or(MemoryError::WorkExceeded)? / 4;
277 let moves = (core::mem::size_of::<(K, V)>() as u64)
278 .checked_mul(2)
279 .ok_or(MemoryError::WorkExceeded)?;
280 groups
281 .checked_add(moves)
282 .and_then(|per_entry| per_entry.checked_mul(self.values.len() as u64))
283 .and_then(|work| work.checked_add(self.key_work))
284 .and_then(|work| {
285 buckets
286 .checked_mul(2)
287 .and_then(|scan| work.checked_add(scan))
288 })
289 .ok_or(MemoryError::WorkExceeded)
290 }
291
292 fn hash_key<Q: HashWork + ?Sized, E: Work>(
293 &self,
294 key: &Q,
295 execution: &E,
296 ) -> Result<u64, MemoryError> {
297 let work = key
298 .hash_work()?
299 .checked_add(self.control_work()?)
300 .ok_or(MemoryError::WorkExceeded)?;
301 execution.charge_work(work)?;
302 Ok(self.values.hasher().hash_one(key))
303 }
304
305 /// Looks up a key after admitting its complete possible probe work.
306 /// The current execution domain pays independently of stored capacity.
307 ///
308 /// # Examples
309 /// ```
310 /// # extern crate alloc;
311 /// # use alloc::sync::Arc;
312 /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
313 /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
314 /// # let mut map = memory.map();
315 /// # map.try_insert(1usize, 42)?;
316 /// assert_eq!(map.get(&1, &memory)?, Some(&42));
317 /// assert_eq!(map.get(&2, &memory)?, None);
318 /// # Ok::<(), MemoryError>(())
319 /// ```
320 pub fn get(&self, key: &K, execution: &impl Work) -> Result<Option<&V>, MemoryError> {
321 self.get_key_value_borrowed(key, execution)
322 .map(|entry| entry.map(|(_, value)| value))
323 }
324
325 /// Finds the canonical stored key and value using an allocation-free query.
326 ///
327 /// Owned and borrowed forms must have identical Hash/Eq semantics, as
328 /// required by [`Borrow`]. Current work admits query hashing, table probes
329 /// and every candidate comparison before executing them. The stored key's
330 /// [`HashWork`] bound must also cover its `Borrow<Q>` operation, which must
331 /// not allocate. A refusal returns `Err`, never successful absence or a
332 /// synthetic match. No table/account is rebound and no query key is copied.
333 ///
334 /// Returning the canonical key lets an interner clone its existing shared
335 /// owner without a second lookup or a temporary owned query allocation.
336 ///
337 /// ```
338 /// use std::sync::Arc;
339 /// use structured_asn1::memory::{Memory, MemoryAccount};
340 /// let storage = Memory::new(Arc::new(MemoryAccount::new(4096, 4096)));
341 /// let mut map = storage.map();
342 /// map.try_insert(7usize, "retained")?;
343 /// let query = Memory::new(Arc::new(MemoryAccount::new(0, 1024)));
344 /// assert_eq!(map.get_key_value_borrowed(&7, &query)?, Some((&7, &"retained")));
345 /// # Ok::<(), structured_asn1::memory::MemoryError>(())
346 /// ```
347 pub fn get_key_value_borrowed<Q: HashWork + ?Sized>(
348 &self,
349 key: &Q,
350 execution: &impl Work,
351 ) -> Result<Option<(&K, &V)>, MemoryError>
352 where
353 K: Borrow<Q>,
354 {
355 let hash = self.hash_key(key, execution)?;
356 let mut failure = None;
357 let entry = self.values.raw_entry().from_hash(hash, |candidate| {
358 equivalent(execution, key, candidate, &mut failure)
359 });
360 if let Some(reason) = failure {
361 return Err(reason);
362 }
363 Ok(entry)
364 }
365
366 /// Looks up an entry for mutation without exposing table growth.
367 /// Hashing, probing and equality use the supplied execution domain.
368 ///
369 /// # Examples
370 /// ```
371 /// # extern crate alloc;
372 /// # use alloc::sync::Arc;
373 /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
374 /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
375 /// # let mut map = memory.map();
376 /// # map.try_insert(1usize, 42)?;
377 /// if let Some(value) = map.get_mut(&1, &memory)? { *value = 7; }
378 /// assert_eq!(map.get(&1, &memory)?, Some(&7));
379 /// # Ok::<(), MemoryError>(())
380 /// ```
381 pub fn get_mut(
382 &mut self,
383 key: &K,
384 execution: &impl Work,
385 ) -> Result<Option<&mut V>, MemoryError> {
386 self.get_mut_borrowed(key, execution)
387 }
388
389 /// Finds a mutable value using an allocation-free borrowed query.
390 ///
391 /// Uses the same Hash/Eq/`Borrow` and [`HashWork`] requirements as
392 /// [`Self::get_key_value_borrowed`]. Current work admits hashing, probes and
393 /// each candidate comparison before exposing a mutable value. Refusal is
394 /// `Err`, never absence or a synthetic match. No owned query, second lookup
395 /// or table/account rebinding is performed.
396 pub fn get_mut_borrowed<Q: HashWork + ?Sized>(
397 &mut self,
398 key: &Q,
399 execution: &impl Work,
400 ) -> Result<Option<&mut V>, MemoryError>
401 where
402 K: Borrow<Q>,
403 {
404 let hash = self.hash_key(key, execution)?;
405 let mut failure = None;
406 let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
407 equivalent(execution, key, candidate, &mut failure)
408 });
409 if let Some(reason) = failure {
410 return Err(reason);
411 }
412 Ok(match entry {
413 RawEntryMut::Occupied(entry) => Some(entry.into_mut()),
414 RawEntryMut::Vacant(_) => None,
415 })
416 }
417
418 /// Inserts a value fallibly, or replaces the value for an existing key.
419 /// This storage mutation runs in the collection's original resource
420 /// domain, including hashing, rehashing, allocation and copying work.
421 /// To mutate under a different domain, build a new collection there from
422 /// explicitly admitted borrowed entries; retained capacity is not rebound.
423 pub fn try_insert(&mut self, key: K, value: V) -> Result<Option<V>, MemoryError> {
424 let hash = self.hash_key(&key, &self.execution)?;
425 let mut failure = None;
426 let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
427 equivalent(&self.execution, &key, candidate, &mut failure)
428 });
429 if let Some(reason) = failure {
430 return Err(reason);
431 }
432 if let RawEntryMut::Occupied(entry) = entry {
433 let moves = (core::mem::size_of::<V>() as u64)
434 .checked_mul(2)
435 .ok_or(MemoryError::WorkExceeded)?;
436 self.execution.charge_work(moves)?;
437 return Ok(Some(core::mem::replace(entry.into_mut(), value)));
438 }
439 let key_work = self
440 .key_work
441 .checked_add(key.hash_work()?)
442 .ok_or(MemoryError::WorkExceeded)?;
443 if self.values.len() == self.values.capacity() {
444 // Include collision-dependent slot searches, not just key hashes.
445 self.execution.charge_work(self.rehash_work()?)?;
446 self.values.allocator().clear_failure();
447 self.values.try_reserve(1).map_err(|error| match error {
448 TryReserveError::CapacityOverflow => MemoryError::CapacityOverflow,
449 TryReserveError::AllocError { .. } => self.values.allocator().failure(),
450 })?;
451 self.maximum_capacity = self.maximum_capacity.max(self.values.capacity());
452 }
453 // Reuse the already computed hash after reserve. No key equality is
454 // needed: the first lookup established absence and resize preserves keys.
455 // raw_entry searches for a match; insert_hashed_nocheck subsequently
456 // searches for an insertion slot. Admit both control traversals.
457 self.execution.charge_work(
458 self.control_work()?
459 .checked_mul(2)
460 .and_then(|work| work.checked_add(core::mem::size_of::<(K, V)>() as u64))
461 .ok_or(MemoryError::WorkExceeded)?,
462 )?;
463 let RawEntryMut::Vacant(entry) = self.values.raw_entry_mut().from_hash(hash, |_| false)
464 else {
465 unreachable!("a predicate that always refuses cannot find an entry");
466 };
467 entry.insert_hashed_nocheck(hash, key, value);
468 self.key_work = key_work;
469 Ok(None)
470 }
471
472 /// Removes an entry, retaining table capacity and its storage charge.
473 /// The supplied execution domain pays for lookup and removal before mutation.
474 ///
475 /// # Examples
476 /// ```
477 /// # extern crate alloc;
478 /// # use alloc::sync::Arc;
479 /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
480 /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
481 /// # let mut map = memory.map();
482 /// # map.try_insert(1usize, 42)?;
483 /// let capacity_charge = memory.used();
484 /// assert_eq!(map.remove(&1, &memory)?, Some(42));
485 /// assert_eq!(memory.used(), capacity_charge);
486 /// # Ok::<(), MemoryError>(())
487 /// ```
488 pub fn remove(&mut self, key: &K, execution: &impl Work) -> Result<Option<V>, MemoryError> {
489 self.remove_borrowed(key, execution)
490 }
491
492 /// Removes a value using an allocation-free borrowed query.
493 ///
494 /// Uses the same Hash/Eq/`Borrow` and [`HashWork`] requirements as
495 /// [`Self::get_key_value_borrowed`]. Current work admits lookup and erase
496 /// metadata/value movement before mutation. Refusal leaves the entry intact.
497 /// The original table capacity and allocator remain retained; the removed
498 /// key is dropped and the value transfers its independent ownership.
499 /// Arbitrary key/value destructor work remains caller-owned.
500 pub fn remove_borrowed<Q: HashWork + ?Sized>(
501 &mut self,
502 key: &Q,
503 execution: &impl Work,
504 ) -> Result<Option<V>, MemoryError>
505 where
506 K: Borrow<Q>,
507 {
508 let hash = self.hash_key(key, execution)?;
509 let mut failure = None;
510 let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
511 equivalent(execution, key, candidate, &mut failure)
512 });
513 if let Some(reason) = failure {
514 return Err(reason);
515 }
516 let entry = match entry {
517 RawEntryMut::Occupied(entry) => {
518 // Erase examines the adjacent control groups; returning the
519 // pair moves its storage. Admit both before changing the table.
520 let work = (core::mem::size_of::<(K, V)>() as u64)
521 .checked_mul(2)
522 .and_then(|work| work.checked_add(2))
523 .ok_or(MemoryError::WorkExceeded)?;
524 let removed_work = entry.key().hash_work()?;
525 execution.charge_work(work)?;
526 let (removed_key, value) = entry.remove_entry();
527 self.key_work -= removed_work;
528 // Settle metadata before caller-owned Drop can unwind.
529 drop(removed_key);
530 Some(value)
531 }
532 RawEntryMut::Vacant(_) => None,
533 };
534 Ok(entry)
535 }
536}
537
538/// A synthetic match stops hashbrown immediately on exhaustion. The caller
539/// checks failure before exposing or mutating the matched entry, so no false
540/// equality escapes and no expensive comparison runs after a refusal.
541fn equivalent<K: HashWork + Borrow<Q>, Q: HashWork + ?Sized, W: Work>(
542 memory: &W,
543 query: &Q,
544 candidate: &K,
545 failure: &mut Option<MemoryError>,
546) -> bool {
547 let cost = query.hash_work().and_then(|query| {
548 candidate.hash_work().and_then(|candidate| {
549 query
550 .checked_add(candidate)
551 .ok_or(MemoryError::WorkExceeded)
552 })
553 });
554 let admitted = cost.and_then(|cost| memory.charge_work(cost));
555 if let Err(reason) = admitted {
556 *failure = Some(reason);
557 return true;
558 }
559 query == candidate.borrow()
560}