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 let hash = self.hash_key(key, execution)?;
387 let mut failure = None;
388 let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
389 equivalent(execution, key, candidate, &mut failure)
390 });
391 if let Some(reason) = failure {
392 return Err(reason);
393 }
394 Ok(match entry {
395 RawEntryMut::Occupied(entry) => Some(entry.into_mut()),
396 RawEntryMut::Vacant(_) => None,
397 })
398 }
399
400 /// Inserts a value fallibly, or replaces the value for an existing key.
401 /// This storage mutation runs in the collection's original resource
402 /// domain, including hashing, rehashing, allocation and copying work.
403 /// To mutate under a different domain, build a new collection there from
404 /// explicitly admitted borrowed entries; retained capacity is not rebound.
405 pub fn try_insert(&mut self, key: K, value: V) -> Result<Option<V>, MemoryError> {
406 let hash = self.hash_key(&key, &self.execution)?;
407 let mut failure = None;
408 let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
409 equivalent(&self.execution, &key, candidate, &mut failure)
410 });
411 if let Some(reason) = failure {
412 return Err(reason);
413 }
414 if let RawEntryMut::Occupied(entry) = entry {
415 let moves = (core::mem::size_of::<V>() as u64)
416 .checked_mul(2)
417 .ok_or(MemoryError::WorkExceeded)?;
418 self.execution.charge_work(moves)?;
419 return Ok(Some(core::mem::replace(entry.into_mut(), value)));
420 }
421 let key_work = self
422 .key_work
423 .checked_add(key.hash_work()?)
424 .ok_or(MemoryError::WorkExceeded)?;
425 if self.values.len() == self.values.capacity() {
426 // Include collision-dependent slot searches, not just key hashes.
427 self.execution.charge_work(self.rehash_work()?)?;
428 self.values.allocator().clear_failure();
429 self.values.try_reserve(1).map_err(|error| match error {
430 TryReserveError::CapacityOverflow => MemoryError::CapacityOverflow,
431 TryReserveError::AllocError { .. } => self.values.allocator().failure(),
432 })?;
433 self.maximum_capacity = self.maximum_capacity.max(self.values.capacity());
434 }
435 // Reuse the already computed hash after reserve. No key equality is
436 // needed: the first lookup established absence and resize preserves keys.
437 // raw_entry searches for a match; insert_hashed_nocheck subsequently
438 // searches for an insertion slot. Admit both control traversals.
439 self.execution.charge_work(
440 self.control_work()?
441 .checked_mul(2)
442 .and_then(|work| work.checked_add(core::mem::size_of::<(K, V)>() as u64))
443 .ok_or(MemoryError::WorkExceeded)?,
444 )?;
445 let RawEntryMut::Vacant(entry) = self.values.raw_entry_mut().from_hash(hash, |_| false)
446 else {
447 unreachable!("a predicate that always refuses cannot find an entry");
448 };
449 entry.insert_hashed_nocheck(hash, key, value);
450 self.key_work = key_work;
451 Ok(None)
452 }
453
454 /// Removes an entry, retaining table capacity and its storage charge.
455 /// The supplied execution domain pays for lookup and removal before mutation.
456 ///
457 /// # Examples
458 /// ```
459 /// # extern crate alloc;
460 /// # use alloc::sync::Arc;
461 /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
462 /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
463 /// # let mut map = memory.map();
464 /// # map.try_insert(1usize, 42)?;
465 /// let capacity_charge = memory.used();
466 /// assert_eq!(map.remove(&1, &memory)?, Some(42));
467 /// assert_eq!(memory.used(), capacity_charge);
468 /// # Ok::<(), MemoryError>(())
469 /// ```
470 pub fn remove(&mut self, key: &K, execution: &impl Work) -> Result<Option<V>, MemoryError> {
471 let hash = self.hash_key(key, execution)?;
472 let mut failure = None;
473 let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
474 equivalent(execution, key, candidate, &mut failure)
475 });
476 if let Some(reason) = failure {
477 return Err(reason);
478 }
479 let entry = match entry {
480 RawEntryMut::Occupied(entry) => {
481 // Erase examines the adjacent control groups; returning the
482 // pair moves its storage. Admit both before changing the table.
483 let work = (core::mem::size_of::<(K, V)>() as u64)
484 .checked_mul(2)
485 .and_then(|work| work.checked_add(2))
486 .ok_or(MemoryError::WorkExceeded)?;
487 let removed_work = entry.key().hash_work()?;
488 execution.charge_work(work)?;
489 let (_, value) = entry.remove_entry();
490 self.key_work -= removed_work;
491 Some(value)
492 }
493 RawEntryMut::Vacant(_) => None,
494 };
495 Ok(entry)
496 }
497}
498
499/// A synthetic match stops hashbrown immediately on exhaustion. The caller
500/// checks failure before exposing or mutating the matched entry, so no false
501/// equality escapes and no expensive comparison runs after a refusal.
502fn equivalent<K: HashWork + Borrow<Q>, Q: HashWork + ?Sized, W: Work>(
503 memory: &W,
504 query: &Q,
505 candidate: &K,
506 failure: &mut Option<MemoryError>,
507) -> bool {
508 let cost = query.hash_work().and_then(|query| {
509 candidate.hash_work().and_then(|candidate| {
510 query
511 .checked_add(candidate)
512 .ok_or(MemoryError::WorkExceeded)
513 })
514 });
515 let admitted = cost.and_then(|cost| memory.charge_work(cost));
516 if let Err(reason) = admitted {
517 *failure = Some(reason);
518 return true;
519 }
520 query == candidate.borrow()
521}