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/// 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, W: Work = Memory> {
62 values: HashMap<K, V, DefaultHashBuilder, AccountAllocator<W>>,
63 execution: W,
64 key_work: u64,
65 maximum_capacity: usize,
66}
67
68impl<K: HashWork + fmt::Debug, V: fmt::Debug, W: Work> fmt::Debug for OwnedMap<K, V, W> {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 self.values.fmt(f)
71 }
72}
73
74impl<K: HashWork, V, W: Work> OwnedMap<K, V, W> {
75 pub(super) fn new(execution: W) -> Self {
76 Self {
77 values: HashMap::with_hasher_in(
78 DefaultHashBuilder::default(),
79 AccountAllocator::new(execution.clone()),
80 ),
81 execution,
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.execution.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, E: Work>(
123 &'a self,
124 execution: &E,
125 ) -> Result<impl Iterator<Item = (&'a K, &'a V)> + 'a + use<'a, K, V, W, E>, 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.17.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<E: Work>(&self, key: &K, execution: &E) -> 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: &impl Work) -> 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(
225 &mut self,
226 key: &K,
227 execution: &impl Work,
228 ) -> Result<Option<&mut V>, MemoryError> {
229 let hash = self.hash_key(key, execution)?;
230 let mut failure = None;
231 let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
232 equivalent(execution, key, candidate, &mut failure)
233 });
234 if let Some(reason) = failure {
235 return Err(reason);
236 }
237 Ok(match entry {
238 RawEntryMut::Occupied(entry) => Some(entry.into_mut()),
239 RawEntryMut::Vacant(_) => None,
240 })
241 }
242
243 /// Inserts a value fallibly, or replaces the value for an existing key.
244 /// This storage mutation runs in the collection's original resource
245 /// domain, including hashing, rehashing, allocation and copying work.
246 /// To mutate under a different domain, build a new collection there from
247 /// explicitly admitted borrowed entries; retained capacity is not rebound.
248 pub fn try_insert(&mut self, key: K, value: V) -> Result<Option<V>, MemoryError> {
249 let hash = self.hash_key(&key, &self.execution)?;
250 let mut failure = None;
251 let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
252 equivalent(&self.execution, &key, candidate, &mut failure)
253 });
254 if let Some(reason) = failure {
255 return Err(reason);
256 }
257 if let RawEntryMut::Occupied(entry) = entry {
258 let moves = (core::mem::size_of::<V>() as u64)
259 .checked_mul(2)
260 .ok_or(MemoryError::WorkExceeded)?;
261 self.execution.charge_work(moves)?;
262 return Ok(Some(core::mem::replace(entry.into_mut(), value)));
263 }
264 let key_work = self
265 .key_work
266 .checked_add(key.hash_work()?)
267 .ok_or(MemoryError::WorkExceeded)?;
268 if self.values.len() == self.values.capacity() {
269 // Include collision-dependent slot searches, not just key hashes.
270 self.execution.charge_work(self.rehash_work()?)?;
271 self.values.allocator().clear_failure();
272 self.values.try_reserve(1).map_err(|error| match error {
273 TryReserveError::CapacityOverflow => MemoryError::CapacityOverflow,
274 TryReserveError::AllocError { .. } => self.values.allocator().failure(),
275 })?;
276 self.maximum_capacity = self.maximum_capacity.max(self.values.capacity());
277 }
278 // Reuse the already computed hash after reserve. No key equality is
279 // needed: the first lookup established absence and resize preserves keys.
280 // raw_entry searches for a match; insert_hashed_nocheck subsequently
281 // searches for an insertion slot. Admit both control traversals.
282 self.execution.charge_work(
283 self.control_work()?
284 .checked_mul(2)
285 .and_then(|work| work.checked_add(core::mem::size_of::<(K, V)>() as u64))
286 .ok_or(MemoryError::WorkExceeded)?,
287 )?;
288 let RawEntryMut::Vacant(entry) = self.values.raw_entry_mut().from_hash(hash, |_| false)
289 else {
290 unreachable!("a predicate that always refuses cannot find an entry");
291 };
292 entry.insert_hashed_nocheck(hash, key, value);
293 self.key_work = key_work;
294 Ok(None)
295 }
296
297 /// Removes an entry, retaining table capacity and its storage charge.
298 /// The supplied execution domain pays for lookup and removal before mutation.
299 ///
300 /// # Examples
301 /// ```
302 /// # extern crate alloc;
303 /// # use alloc::sync::Arc;
304 /// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
305 /// # let memory = Memory::new(Arc::new(MemoryAccount::new(1024, 1024)));
306 /// # let mut map = memory.map();
307 /// # map.try_insert(1usize, 42)?;
308 /// let capacity_charge = memory.used();
309 /// assert_eq!(map.remove(&1, &memory)?, Some(42));
310 /// assert_eq!(memory.used(), capacity_charge);
311 /// # Ok::<(), MemoryError>(())
312 /// ```
313 pub fn remove(&mut self, key: &K, execution: &impl Work) -> Result<Option<V>, MemoryError> {
314 let hash = self.hash_key(key, execution)?;
315 let mut failure = None;
316 let entry = self.values.raw_entry_mut().from_hash(hash, |candidate| {
317 equivalent(execution, key, candidate, &mut failure)
318 });
319 if let Some(reason) = failure {
320 return Err(reason);
321 }
322 let entry = match entry {
323 RawEntryMut::Occupied(entry) => {
324 // Erase examines the adjacent control groups; returning the
325 // pair moves its storage. Admit both before changing the table.
326 let work = (core::mem::size_of::<(K, V)>() as u64)
327 .checked_mul(2)
328 .and_then(|work| work.checked_add(2))
329 .ok_or(MemoryError::WorkExceeded)?;
330 let removed_work = entry.key().hash_work()?;
331 execution.charge_work(work)?;
332 let (_, value) = entry.remove_entry();
333 self.key_work -= removed_work;
334 Some(value)
335 }
336 RawEntryMut::Vacant(_) => None,
337 };
338 Ok(entry)
339 }
340}
341
342/// A synthetic match stops hashbrown immediately on exhaustion. The caller
343/// checks failure before exposing or mutating the matched entry, so no false
344/// equality escapes and no expensive comparison runs after a refusal.
345fn equivalent<K: HashWork, W: Work>(
346 memory: &W,
347 query: &K,
348 candidate: &K,
349 failure: &mut Option<MemoryError>,
350) -> bool {
351 let cost = query.hash_work().and_then(|query| {
352 candidate.hash_work().and_then(|candidate| {
353 query
354 .checked_add(candidate)
355 .ok_or(MemoryError::WorkExceeded)
356 })
357 });
358 let admitted = cost.and_then(|cost| memory.charge_work(cost));
359 if let Err(reason) = admitted {
360 *failure = Some(reason);
361 return true;
362 }
363 query == candidate
364}