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