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};
11
12/// A trusted key type's upper bound for hashing and equality work.
13///
14/// Implementations must cover all bytes examined by Hash and Eq. This is a
15/// contract for caller-provided code, not a value an untrusted document supplies.
16/// The bound must remain unchanged and successful while a key is stored, just
17/// as Hash and Eq must remain stable. Accounting retains this bound for rehashes.
18pub trait HashWork: Hash + Eq {
19 /// Work units for one hash/equality traversal, including constant overhead.
20 fn hash_work(&self) -> Result<u64, MemoryError>;
21}
22
23impl HashWork for usize {
24 fn hash_work(&self) -> Result<u64, MemoryError> {
25 Ok(1)
26 }
27}
28
29impl HashWork for (usize, usize, usize) {
30 fn hash_work(&self) -> Result<u64, MemoryError> {
31 Ok(3)
32 }
33}
34
35impl HashWork for &[u8] {
36 fn hash_work(&self) -> Result<u64, MemoryError> {
37 Ok(self.len() as u64 + 1)
38 }
39}
40
41impl HashWork for crate::Oid<'_> {
42 fn hash_work(&self) -> Result<u64, MemoryError> {
43 Ok(self.as_bytes().len() as u64 + 1)
44 }
45}
46
47impl HashWork for (crate::Oid<'_>, crate::Oid<'_>) {
48 fn hash_work(&self) -> Result<u64, MemoryError> {
49 self.0
50 .hash_work()?
51 .checked_add(self.1.hash_work()?)
52 .ok_or(MemoryError::WorkExceeded)
53 }
54}
55
56/// A hash map whose backing allocation and operations share one resource domain.
57///
58/// Hashbrown supplies bucket/probing/SIMD machinery. Reservations include its
59/// control bytes and alignment, not merely the initialized entries. Hashing and
60/// actual collision comparisons are admitted before examining candidate keys.
61pub struct OwnedMap<K: HashWork, V> {
62 values: HashMap<K, V, DefaultHashBuilder, AccountAllocator>,
63 memory: Memory,
64 key_work: u64,
65 maximum_capacity: usize,
66}
67
68impl<K: HashWork + fmt::Debug, V: fmt::Debug> fmt::Debug for OwnedMap<K, V> {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 self.values.fmt(f)
71 }
72}
73
74impl<K: HashWork, V> OwnedMap<K, V> {
75 pub(super) fn new(memory: Memory) -> Self {
76 Self {
77 values: HashMap::with_hasher_in(
78 DefaultHashBuilder::default(),
79 AccountAllocator::new(memory.clone()),
80 ),
81 memory,
82 key_work: 0,
83 maximum_capacity: 0,
84 }
85 }
86
87 /// The shared account retained until the table is deallocated.
88 #[must_use]
89 pub fn memory(&self) -> &Memory {
90 &self.memory
91 }
92
93 /// Number of initialized entries.
94 #[must_use]
95 pub fn len(&self) -> usize {
96 self.values.len()
97 }
98
99 /// Whether there are no entries.
100 #[must_use]
101 pub fn is_empty(&self) -> bool {
102 self.values.is_empty()
103 }
104
105 /// Borrowed entries after admitting a full control-bucket traversal in
106 /// the current execution domain, independently of retained storage.
107 /// Sparse holes count too. Partial consumption retains the conservative
108 /// prepayment; the opaque iterator exposes no cloning/restart operation.
109 ///
110 /// # Examples
111 /// ```
112 /// # extern crate alloc;
113 /// # use alloc::sync::Arc;
114 /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
115 /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
116 /// let mut map = memory.map();
117 /// map.try_insert(1usize, 42)?;
118 /// let mut entries = map.try_iter(&memory)?;
119 /// assert_eq!(entries.next(), Some((&1, &42)));
120 /// # Ok::<(), MemoryError>(())
121 /// ```
122 pub fn try_iter<'a>(
123 &'a self,
124 execution: &Memory,
125 ) -> Result<impl Iterator<Item = (&'a K, &'a V)> + use<'a, K, V>, MemoryError> {
126 execution.charge_work(self.buckets()?)?;
127 Ok(self.values.iter())
128 }
129
130 fn buckets(&self) -> Result<u64, MemoryError> {
131 // Pinned hashbrown 0.16.1 raw::bucket_mask_to_capacity: tables with
132 // four/eight buckets keep one empty slot; larger tables use 7/8 of
133 // the buckets. Maximum capacity records each table immediately after
134 // reserve, before tombstones can reduce the public capacity value.
135 match self.maximum_capacity {
136 0 => Ok(0),
137 capacity @ 1..=7 => Ok(capacity as u64 + 1),
138 capacity => (capacity as u64 / 7)
139 .checked_mul(8)
140 .ok_or(MemoryError::WorkExceeded),
141 }
142 }
143
144 fn control_work(&self) -> Result<u64, MemoryError> {
145 let buckets = self.buckets()?;
146 // The pinned engine's narrowest control group is four bytes. Wider
147 // scalar/SIMD groups perform the same fixed group operations. One unit
148 // admits one group visit; triangular probing visits each group once.
149 Ok(buckets / 4)
150 }
151
152 fn rehash_work(&self) -> Result<u64, MemoryError> {
153 // reserve(1) can double the current buckets or rehash in place. Every
154 // retained key is hashed once; each slot search may visit all groups.
155 // In-place rehash additionally scans every bucket and swaps entries.
156 let buckets = self.buckets()?;
157 let groups = buckets.checked_mul(2).ok_or(MemoryError::WorkExceeded)? / 4;
158 let moves = (core::mem::size_of::<(K, V)>() as u64)
159 .checked_mul(2)
160 .ok_or(MemoryError::WorkExceeded)?;
161 groups
162 .checked_add(moves)
163 .and_then(|per_entry| per_entry.checked_mul(self.values.len() as u64))
164 .and_then(|work| work.checked_add(self.key_work))
165 .and_then(|work| {
166 buckets
167 .checked_mul(2)
168 .and_then(|scan| work.checked_add(scan))
169 })
170 .ok_or(MemoryError::WorkExceeded)
171 }
172
173 fn hash_key(&self, key: &K, execution: &Memory) -> Result<u64, MemoryError> {
174 let work = key
175 .hash_work()?
176 .checked_add(self.control_work()?)
177 .ok_or(MemoryError::WorkExceeded)?;
178 execution.charge_work(work)?;
179 Ok(self.values.hasher().hash_one(key))
180 }
181
182 /// Looks up a key after admitting its complete possible probe work.
183 /// The current execution domain pays independently of stored capacity.
184 ///
185 /// # Examples
186 /// ```
187 /// # extern crate alloc;
188 /// # use alloc::sync::Arc;
189 /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
190 /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
191 /// # let mut map = memory.map();
192 /// # map.try_insert(1usize, 42)?;
193 /// assert_eq!(map.get(&1, &memory)?, Some(&42));
194 /// assert_eq!(map.get(&2, &memory)?, None);
195 /// # Ok::<(), MemoryError>(())
196 /// ```
197 pub fn get(&self, key: &K, execution: &Memory) -> Result<Option<&V>, MemoryError> {
198 let hash = self.hash_key(key, execution)?;
199 let mut failure = None;
200 let entry = self.values.raw_entry().from_hash(hash, |candidate| {
201 equivalent(execution, key, candidate, &mut failure)
202 });
203 if let Some(reason) = failure {
204 return Err(reason);
205 }
206 Ok(entry.map(|(_, value)| value))
207 }
208
209 /// Looks up an entry for mutation without exposing table growth.
210 /// Hashing, probing and equality use the supplied execution domain.
211 ///
212 /// # Examples
213 /// ```
214 /// # extern crate alloc;
215 /// # use alloc::sync::Arc;
216 /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
217 /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
218 /// # let mut map = memory.map();
219 /// # map.try_insert(1usize, 42)?;
220 /// if let Some(value) = map.get_mut(&1, &memory)? { *value = 7; }
221 /// assert_eq!(map.get(&1, &memory)?, Some(&7));
222 /// # Ok::<(), MemoryError>(())
223 /// ```
224 pub fn get_mut(&mut self, key: &K, execution: &Memory) -> Result<Option<&mut V>, MemoryError> {
225 let hash = self.hash_key(key, execution)?;
226 let mut failure = None;
227 let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
228 equivalent(execution, key, candidate, &mut failure)
229 });
230 if let Some(reason) = failure {
231 return Err(reason);
232 }
233 Ok(match entry {
234 RawEntryMut::Occupied(entry) => Some(entry.into_mut()),
235 RawEntryMut::Vacant(_) => None,
236 })
237 }
238
239 /// Inserts a value fallibly, or replaces the value for an existing key.
240 /// This storage mutation runs in the collection's original resource
241 /// domain, including hashing, rehashing, allocation and copying work.
242 /// To mutate under a different domain, build a new collection there from
243 /// explicitly admitted borrowed entries; retained capacity is not rebound.
244 pub fn try_insert(&mut self, key: K, value: V) -> Result<Option<V>, MemoryError> {
245 let hash = self.hash_key(&key, &self.memory)?;
246 let mut failure = None;
247 let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
248 equivalent(&self.memory, &key, candidate, &mut failure)
249 });
250 if let Some(reason) = failure {
251 return Err(reason);
252 }
253 if let RawEntryMut::Occupied(entry) = entry {
254 return Ok(Some(core::mem::replace(entry.into_mut(), value)));
255 }
256 let key_work = self
257 .key_work
258 .checked_add(key.hash_work()?)
259 .ok_or(MemoryError::WorkExceeded)?;
260 if self.values.len() == self.values.capacity() {
261 // Include collision-dependent slot searches, not just key hashes.
262 self.memory().charge_work(self.rehash_work()?)?;
263 self.values.allocator().clear_failure();
264 self.values.try_reserve(1).map_err(|error| match error {
265 TryReserveError::CapacityOverflow => MemoryError::CapacityOverflow,
266 TryReserveError::AllocError { .. } => self.values.allocator().failure(),
267 })?;
268 self.maximum_capacity = self.maximum_capacity.max(self.values.capacity());
269 }
270 // Reuse the already computed hash after reserve. No key equality is
271 // needed: the first lookup established absence and resize preserves keys.
272 // raw_entry searches for a match; insert_hashed_nocheck subsequently
273 // searches for an insertion slot. Admit both control traversals.
274 self.memory.charge_work(
275 self.control_work()?
276 .checked_mul(2)
277 .ok_or(MemoryError::WorkExceeded)?,
278 )?;
279 let RawEntryMut::Vacant(entry) = self.values.raw_entry_mut().from_hash(hash, |_| false)
280 else {
281 unreachable!("a predicate that always refuses cannot find an entry");
282 };
283 entry.insert_hashed_nocheck(hash, key, value);
284 self.key_work = key_work;
285 Ok(None)
286 }
287
288 /// Removes an entry, retaining table capacity and its storage charge.
289 /// The supplied execution domain pays for lookup and removal before mutation.
290 ///
291 /// # Examples
292 /// ```
293 /// # extern crate alloc;
294 /// # use alloc::sync::Arc;
295 /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
296 /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
297 /// # let mut map = memory.map();
298 /// # map.try_insert(1usize, 42)?;
299 /// let capacity_charge = memory.used();
300 /// assert_eq!(map.remove(&1, &memory)?, Some(42));
301 /// assert_eq!(memory.used(), capacity_charge);
302 /// # Ok::<(), MemoryError>(())
303 /// ```
304 pub fn remove(&mut self, key: &K, execution: &Memory) -> Result<Option<V>, MemoryError> {
305 let hash = self.hash_key(key, execution)?;
306 let mut failure = None;
307 let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
308 equivalent(execution, key, candidate, &mut failure)
309 });
310 if let Some(reason) = failure {
311 return Err(reason);
312 }
313 let entry = match entry {
314 RawEntryMut::Occupied(entry) => {
315 // Erase examines the adjacent control groups; returning the
316 // pair moves its storage. Admit both before changing the table.
317 let work = (core::mem::size_of::<(K, V)>() as u64)
318 .checked_mul(2)
319 .and_then(|work| work.checked_add(2))
320 .ok_or(MemoryError::WorkExceeded)?;
321 let removed_work = entry.key().hash_work()?;
322 execution.charge_work(work)?;
323 let (_, value) = entry.remove_entry();
324 self.key_work -= removed_work;
325 Some(value)
326 }
327 RawEntryMut::Vacant(_) => None,
328 };
329 Ok(entry)
330 }
331}
332
333/// A synthetic match stops hashbrown immediately on exhaustion. The caller
334/// checks failure before exposing or mutating the matched entry, so no false
335/// equality escapes and no expensive comparison runs after a refusal.
336fn equivalent<K: HashWork>(
337 memory: &Memory,
338 query: &K,
339 candidate: &K,
340 failure: &mut Option<MemoryError>,
341) -> bool {
342 let cost = query.hash_work().and_then(|query| {
343 candidate.hash_work().and_then(|candidate| {
344 query
345 .checked_add(candidate)
346 .ok_or(MemoryError::WorkExceeded)
347 })
348 });
349 let admitted = cost.and_then(|cost| memory.charge_work(cost));
350 if let Err(reason) = admitted {
351 *failure = Some(reason);
352 return true;
353 }
354 query == candidate
355}