subetha_cxc/shared_umbra_pointer.rs
1//! `SharedUmbraPointer<T>` - cross-process content-prefixed pointer.
2//!
3//! The cross-process lift of `subetha_pointers::UmbraPointer<T>`. The
4//! mechanical change is one field swap:
5//!
6//! ```text
7//! in-process: target: *const T (8 bytes, address-space-bound)
8//! cross-proc: target: OffsetPtr<T> (4 bytes, byte-stable)
9//! ```
10//!
11//! Everything else stays identical: 16-byte slot, u32 prefix at the
12//! same offset, SIMD-friendly array layout for prefix-shortcircuit
13//! scans, prefix derived from content (first 4 bytes or hash).
14//!
15//! # Why a separate primitive
16//!
17//! A `*const T` is process-local: it indexes the heap of the
18//! constructing process. Writing one into an MMF and reading it from
19//! another process gives a wild pointer. `OffsetPtr<T>` is an index
20//! into a `SharedRegion<T>` - every process resolves it via its own
21//! mapping's base pointer.
22//!
23//! # Pod-safety
24//!
25//! `SharedUmbraPointer<T>` is `Copy + repr(C, align(16))` with no
26//! Drop side effects. It can live inside any other MMF container
27//! (`SharedVec`, `SharedHashMap`, `SharedBTreeMap`, …) and be read
28//! in any process holding the matching region.
29//!
30//! # Composition pattern
31//!
32//! ```text
33//! SharedRegion<T> owns the underlying T values
34//! SharedVec<SharedUmbraPointer<T>> stores prefix-prefixed handles
35//! scan callers filter by prefix in-register;
36//! only on prefix match do they resolve
37//! the OffsetPtr through the region
38//! ```
39//!
40//! The architectural win is identical to the in-process Umbra: 95 %
41//! of prefix mismatches reject without paying the cache miss to
42//! load the underlying T from the region MMF.
43
44use std::collections::hash_map::DefaultHasher;
45use std::hash::{Hash, Hasher};
46use std::marker::PhantomData;
47
48use crate::shared_region::{OffsetPtr, RegionError, SharedRegion};
49
50/// 16-byte cross-process content-prefixed pointer.
51///
52/// Layout is fixed and PoD so SIMD scans over an array see a stable
53/// prefix-byte position.
54///
55/// ```text
56/// offset 0 : OffsetPtr<T> (u32 index; NIL = u32::MAX)
57/// offset 4 : u32 prefix
58/// offset 8 : u8 ext_tag (0 = unset; 1..=255 = registered)
59/// offset 9 : [u8; 7] ext_payload (interpretation per tag)
60/// offset 16 : end
61/// ```
62///
63/// # User-addressable extension bytes
64///
65/// Bytes 8..16 are a TAG (1 byte) + PAYLOAD (7 bytes) that callers
66/// can use to attach typed metadata to the pointer. Access via the
67/// [`UmbraExtension`] trait + `set_ext` / `ext` methods:
68///
69/// - **Guard 1 (compile-time size)**: `set_ext<E>` and `ext<E>`
70/// both monomorphize a const-assertion that
71/// `size_of::<E>() <= 7`. Larger types fail to compile.
72/// - **Guard 2 (runtime tag)**: each `UmbraExtension` declares a
73/// unique `TAG: u8` constant. `ext<E>()` returns `None` if the
74/// pointer's tag does not match `E::TAG`, preventing two
75/// consumers from interpreting the same bytes differently.
76/// - **Guard 3 (type bound)**: `E: Copy + 'static` ensures no
77/// Drop side effects and no lifetimes to manage.
78#[repr(C, align(16))]
79#[derive(Debug)]
80pub struct SharedUmbraPointer<T: Copy + 'static> {
81 /// Index of the target slot in some `SharedRegion<T>`. The
82 /// region itself is held by the caller; this pointer is just
83 /// the cross-process-stable address.
84 pub target: OffsetPtr<T>,
85 /// 4-byte content prefix derived from the target's bytes (or a
86 /// 4-byte hash). Constant for the lifetime of the pointer.
87 pub prefix: u32,
88 /// User extension tag. 0 means "no extension set"; non-zero
89 /// values are caller-defined per `UmbraExtension::TAG`.
90 ext_tag: u8,
91 /// User extension payload. Interpretation depends on `ext_tag`.
92 /// Access via `set_ext` / `ext` for typed safety.
93 ext_payload: [u8; 7],
94 _phantom: PhantomData<T>,
95}
96
97/// Marker trait for user-defined extension types stored in
98/// SharedUmbraPointer's reserved bytes. Each implementor declares
99/// a unique TAG so different consumers don't misinterpret each
100/// other's payloads.
101///
102/// # Implementor responsibility
103///
104/// `TAG` MUST be globally unique across all `UmbraExtension`
105/// implementations that may be present in the same shared memory.
106/// Two implementations sharing a TAG value will silently
107/// misinterpret each other's payloads. Reserve TAG values in your
108/// application by registering them in a central location (e.g. a
109/// doc comment listing claimed tags).
110///
111/// TAG 0 is reserved for "no extension set".
112pub trait UmbraExtension: Copy + 'static {
113 const TAG: u8;
114}
115
116/// Compile-time size guard. Monomorphization of `CHECK` triggers a
117/// `const` assertion that the extension fits in 7 bytes.
118struct ExtSizeCheck<E>(PhantomData<E>);
119impl<E> ExtSizeCheck<E> {
120 const CHECK: () = assert!(
121 std::mem::size_of::<E>() <= 7,
122 "UmbraExtension type must fit in 7 bytes (1 byte reserved for tag)",
123 );
124}
125
126impl<T: Copy + 'static> Clone for SharedUmbraPointer<T> {
127 fn clone(&self) -> Self { *self }
128}
129impl<T: Copy + 'static> Copy for SharedUmbraPointer<T> {}
130
131impl<T: Copy + 'static> PartialEq for SharedUmbraPointer<T> {
132 /// Full equality: same target AND same prefix. Use
133 /// `prefix_eq` for the fast-path prefix-only check.
134 fn eq(&self, other: &Self) -> bool {
135 self.target == other.target && self.prefix == other.prefix
136 }
137}
138impl<T: Copy + 'static> Eq for SharedUmbraPointer<T> {}
139
140impl<T: Copy + 'static> Default for SharedUmbraPointer<T> {
141 /// NIL pointer with zero prefix. Zero-bytes representation,
142 /// safe to write into freshly-zeroed MMF storage.
143 fn default() -> Self { Self::NIL }
144}
145
146impl<T: Copy + 'static> SharedUmbraPointer<T> {
147 /// Direction signature of `SharedUmbraPointer<T>`. Engages the
148 /// `K_content_prefix` axis (4-byte prefix stored at slot for
149 /// short-circuit equality before MMF deref).
150 pub const SIGNATURE: subetha_core::AxisMask = subetha_core::AxisMask::from_axes(
151 &[subetha_core::Axis::ContentPrefix],
152 );
153
154 /// NIL sentinel: target is `OffsetPtr::NIL` and prefix is 0;
155 /// extension tag is 0 (unset). Equivalent to a freshly-zeroed
156 /// 16-byte slot.
157 pub const NIL: Self = Self {
158 target: OffsetPtr::NIL,
159 prefix: 0,
160 ext_tag: 0,
161 ext_payload: [0; 7],
162 _phantom: PhantomData,
163 };
164
165 /// Construct from an existing region-allocated OffsetPtr and a
166 /// caller-computed prefix. Extension is unset (tag=0).
167 #[inline]
168 pub const fn new(target: OffsetPtr<T>, prefix: u32) -> Self {
169 Self {
170 target, prefix,
171 ext_tag: 0,
172 ext_payload: [0; 7],
173 _phantom: PhantomData,
174 }
175 }
176
177 /// Write a typed extension. Sets the tag to `E::TAG` and copies
178 /// the value bytes into the payload. Caller guarantees
179 /// `E::TAG` is globally unique.
180 pub fn set_ext<E: UmbraExtension>(&mut self, value: E) {
181 // Monomorphization-time size check: fails to compile if
182 // size_of::<E>() exceeds 7.
183 let _check: () = ExtSizeCheck::<E>::CHECK;
184 self.ext_tag = E::TAG;
185 self.ext_payload = [0; 7];
186 // SAFETY: E: Copy + 'static (no Drop, no lifetimes); we
187 // write size_of::<E>() bytes (<= 7) into the 7-byte
188 // payload. The cast to *const u8 is a standard byte-copy.
189 let bytes = unsafe {
190 std::slice::from_raw_parts(
191 &value as *const E as *const u8,
192 std::mem::size_of::<E>(),
193 )
194 };
195 self.ext_payload[..bytes.len()].copy_from_slice(bytes);
196 }
197
198 /// Read a typed extension. Returns `None` if no extension is
199 /// set (tag=0) OR if the stored tag does not match `E::TAG`.
200 ///
201 /// # Safety
202 ///
203 /// Even with tag validation, this is `unsafe` because the
204 /// tag-uniqueness contract is on the caller. Two
205 /// `UmbraExtension` implementations sharing a TAG value will
206 /// silently misinterpret each other's payloads. The payload
207 /// bytes must also be a valid representation of `E` (relevant
208 /// for enums with restricted discriminants).
209 pub unsafe fn ext<E: UmbraExtension>(&self) -> Option<E> {
210 let _check: () = ExtSizeCheck::<E>::CHECK;
211 if self.ext_tag == 0 || self.ext_tag != E::TAG {
212 return None;
213 }
214 // SAFETY: tag validated; size compile-time-bounded;
215 // E: Copy + 'static. Read first size_of::<E>() bytes from
216 // payload as E.
217 let mut buf = [0u8; 7];
218 buf.copy_from_slice(&self.ext_payload);
219 Some(unsafe { std::ptr::read(buf.as_ptr() as *const E) })
220 }
221
222 /// Clear the extension. Tag and payload set to 0.
223 pub fn clear_ext(&mut self) {
224 self.ext_tag = 0;
225 self.ext_payload = [0; 7];
226 }
227
228 /// The current extension tag (0 = unset).
229 #[inline]
230 pub fn ext_tag(&self) -> u8 { self.ext_tag }
231
232 /// Raw byte access to the extension payload. Use this for
233 /// debugging or when interfacing with untyped consumers.
234 #[inline]
235 pub fn ext_payload_raw(&self) -> &[u8; 7] { &self.ext_payload }
236
237 /// Allocate `value` in `region` and build a pointer whose prefix
238 /// is the first 4 bytes of the in-memory representation of T
239 /// (little-endian native). Useful when T's first bytes are a
240 /// meaningful key field (row IDs, packet headers).
241 pub fn from_region_alloc_content_prefix(
242 region: &SharedRegion<T>, value: T,
243 ) -> Result<Self, RegionError> {
244 let prefix = content_prefix_of(&value);
245 let ptr = region.allocate(value)?;
246 Ok(Self::new(ptr, prefix))
247 }
248
249 /// Allocate `value` in `region` and build a pointer whose prefix
250 /// is the low 32 bits of `std::hash::DefaultHasher` applied to
251 /// `value`. Near-perfect rejection rate; requires `T: Hash`.
252 pub fn from_region_alloc_hash_prefix(
253 region: &SharedRegion<T>, value: T,
254 ) -> Result<Self, RegionError>
255 where T: Hash,
256 {
257 let prefix = hash_prefix_of(&value);
258 let ptr = region.allocate(value)?;
259 Ok(Self::new(ptr, prefix))
260 }
261
262 /// Allocate `value` in `region` and build a pointer with an
263 /// explicit caller-supplied prefix.
264 pub fn from_region_alloc(
265 region: &SharedRegion<T>, value: T, prefix: u32,
266 ) -> Result<Self, RegionError> {
267 let ptr = region.allocate(value)?;
268 Ok(Self::new(ptr, prefix))
269 }
270
271 /// True when target is NIL. Prefix may still be non-zero.
272 #[inline]
273 pub fn is_nil(&self) -> bool { self.target.is_nil() }
274
275 /// Prefix-only comparison. Single in-register check; does NOT
276 /// touch the region MMF. Use as the first step in a staged
277 /// equality check.
278 #[inline]
279 pub fn prefix_eq(&self, other: &Self) -> bool {
280 self.prefix == other.prefix
281 }
282
283 /// Compare against a literal query prefix. Same semantics as
284 /// `prefix_eq` against a constructed SharedUmbraPointer.
285 #[inline]
286 pub fn matches_prefix(&self, query: u32) -> bool {
287 self.prefix == query
288 }
289
290 /// Resolve the target through `region`. Costs one MMF read.
291 /// Only call after a successful prefix check unless you really
292 /// need the value.
293 pub fn resolve(&self, region: &SharedRegion<T>) -> Result<T, RegionError> {
294 region.get(self.target)
295 }
296}
297
298/// Compute the content prefix (first 4 bytes of T's in-memory
299/// representation, padded with zero if T is smaller than 4 bytes).
300#[inline]
301fn content_prefix_of<T: Copy>(value: &T) -> u32 {
302 let mut buf = [0u8; 4];
303 let n = std::mem::size_of::<T>().min(4);
304 unsafe {
305 std::ptr::copy_nonoverlapping(
306 value as *const T as *const u8,
307 buf.as_mut_ptr(),
308 n,
309 );
310 }
311 u32::from_le_bytes(buf)
312}
313
314/// Compute the hash prefix (low 32 bits of DefaultHasher(value)).
315#[inline]
316fn hash_prefix_of<T: Hash>(value: &T) -> u32 {
317 let mut h = DefaultHasher::new();
318 value.hash(&mut h);
319 h.finish() as u32
320}
321
322const _: () = {
323 assert!(std::mem::size_of::<SharedUmbraPointer<u64>>() == 16);
324 assert!(std::mem::align_of::<SharedUmbraPointer<u64>>() == 16);
325};
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 fn tmp(name: &str) -> std::path::PathBuf {
332 let mut p = std::env::temp_dir();
333 let pid = std::process::id();
334 p.push(format!("subetha-umbra-{name}-{pid}.bin"));
335 p
336 }
337
338 #[test]
339 fn layout_is_exactly_16_bytes() {
340 assert_eq!(std::mem::size_of::<SharedUmbraPointer<u64>>(), 16);
341 assert_eq!(std::mem::align_of::<SharedUmbraPointer<u64>>(), 16);
342 }
343
344 #[test]
345 fn nil_is_all_zero() {
346 let n: SharedUmbraPointer<u64> = SharedUmbraPointer::NIL;
347 assert!(n.is_nil());
348 assert_eq!(n.prefix, 0);
349 let default: SharedUmbraPointer<u64> = SharedUmbraPointer::default();
350 assert_eq!(default, n);
351 }
352
353 #[test]
354 fn prefix_eq_does_not_touch_region() {
355 // Two SharedUmbraPointers with the SAME prefix but
356 // different (invalid) OffsetPtr indices. prefix_eq returns
357 // true without resolving either target. matches_prefix
358 // against the same query prefix likewise.
359 let a: SharedUmbraPointer<u64> = SharedUmbraPointer::new(
360 OffsetPtr::new(7), 0xDEAD_BEEF,
361 );
362 let b: SharedUmbraPointer<u64> = SharedUmbraPointer::new(
363 OffsetPtr::new(99), 0xDEAD_BEEF,
364 );
365 let c: SharedUmbraPointer<u64> = SharedUmbraPointer::new(
366 OffsetPtr::new(0), 0xCAFE_BABE,
367 );
368 assert!(a.prefix_eq(&b));
369 assert!(!a.prefix_eq(&c));
370 assert!(a.matches_prefix(0xDEAD_BEEF));
371 assert!(!a.matches_prefix(0));
372 }
373
374 #[test]
375 fn from_region_alloc_content_prefix_round_trip() {
376 let p = tmp("content");
377 let region: SharedRegion<u64> = SharedRegion::create(&p, 64).unwrap();
378 let value: u64 = 0x0000_0000_0000_BEEF;
379 let u = SharedUmbraPointer::from_region_alloc_content_prefix(
380 ®ion, value,
381 ).unwrap();
382 // On little-endian: first 4 bytes of 0xBEEF = 0xEF 0xBE 0x00 0x00.
383 assert_eq!(u.prefix, 0x0000_BEEF);
384 assert_eq!(u.resolve(®ion).unwrap(), value);
385 drop(region);
386 std::fs::remove_file(&p).ok();
387 }
388
389 #[test]
390 fn from_region_alloc_hash_prefix_is_deterministic() {
391 let p = tmp("hash");
392 let region: SharedRegion<u64> = SharedRegion::create(&p, 64).unwrap();
393 let a = SharedUmbraPointer::from_region_alloc_hash_prefix(®ion, 42u64).unwrap();
394 let b = SharedUmbraPointer::from_region_alloc_hash_prefix(®ion, 42u64).unwrap();
395 // Same value → same prefix.
396 assert_eq!(a.prefix, b.prefix);
397 // Targets are different slots though.
398 assert_ne!(a.target, b.target);
399 let c = SharedUmbraPointer::from_region_alloc_hash_prefix(®ion, 43u64).unwrap();
400 // Different value → different prefix (with overwhelming probability).
401 assert_ne!(a.prefix, c.prefix);
402 drop(region);
403 std::fs::remove_file(&p).ok();
404 }
405
406 #[test]
407 fn explicit_prefix_constructor() {
408 let p = tmp("explicit");
409 let region: SharedRegion<u64> = SharedRegion::create(&p, 64).unwrap();
410 let u = SharedUmbraPointer::from_region_alloc(
411 ®ion, 12345u64, 0x1234_5678,
412 ).unwrap();
413 assert_eq!(u.prefix, 0x1234_5678);
414 assert_eq!(u.resolve(®ion).unwrap(), 12345);
415 drop(region);
416 std::fs::remove_file(&p).ok();
417 }
418
419 #[test]
420 fn dedup_scan_via_prefix_zero_region_reads() {
421 // Build 100 pointers with distinct prefixes. Scan for a
422 // prefix that doesn't match any. The scan must touch only
423 // the pointer array, never the region.
424 let p = tmp("dedup");
425 let region: SharedRegion<u64> = SharedRegion::create(&p, 256).unwrap();
426 let pointers: Vec<SharedUmbraPointer<u64>> = (0..100u64)
427 .map(|i| SharedUmbraPointer::from_region_alloc(
428 ®ion, i * 1000, (i + 1) as u32,
429 ).unwrap())
430 .collect();
431 let query = 999u32;
432 let matches: Vec<_> = pointers.iter()
433 .filter(|p| p.matches_prefix(query))
434 .collect();
435 assert!(matches.is_empty(), "no prefix in 1..=100 should equal 999");
436 // Sanity: a prefix that DOES match resolves correctly.
437 let hit: &SharedUmbraPointer<u64> = pointers.iter()
438 .find(|p| p.matches_prefix(42))
439 .expect("prefix 42 should exist (i=41)");
440 assert_eq!(hit.resolve(®ion).unwrap(), 41 * 1000);
441 drop(region);
442 std::fs::remove_file(&p).ok();
443 }
444
445 #[test]
446 fn cross_process_via_separate_region_handles() {
447 // Writer and reader open the same region; pointer values
448 // (byte-identical) resolve through either handle.
449 let p = tmp("cross");
450 let writer_region: SharedRegion<u64> = SharedRegion::create(&p, 32).unwrap();
451 let reader_region: SharedRegion<u64> = SharedRegion::open(&p, 32).unwrap();
452 let u = SharedUmbraPointer::from_region_alloc(
453 &writer_region, 7777u64, 0xABCD_EF01,
454 ).unwrap();
455 // The SharedUmbraPointer struct is Copy + Pod, so we can
456 // pretend we ferried it through shared memory by literal
457 // byte-copy. The destination MUST be aligned to align_of
458 // SharedUmbraPointer<u64> (16 bytes); a plain `[u8; 16]`
459 // has alignment 1 and would produce a misaligned read on
460 // architectures that fault on unaligned u64 access. Use
461 // MaybeUninit which inherits the destination type's
462 // alignment requirement.
463 let mut buf: std::mem::MaybeUninit<SharedUmbraPointer<u64>>
464 = std::mem::MaybeUninit::uninit();
465 // SAFETY: buf is the size_of::<SharedUmbraPointer<u64>>() == 16
466 // bytes correctly aligned, fully owned, and writable. Source
467 // is a valid SharedUmbraPointer<u64> by construction. The
468 // copy initialises every byte of buf.
469 unsafe {
470 std::ptr::copy_nonoverlapping(
471 &u as *const SharedUmbraPointer<u64> as *const u8,
472 buf.as_mut_ptr() as *mut u8,
473 std::mem::size_of::<SharedUmbraPointer<u64>>(),
474 );
475 }
476 // SAFETY: buf was fully initialised by the copy above; its
477 // bytes are a valid SharedUmbraPointer<u64> (the trait is
478 // Copy + has no Drop), so assume_init is sound.
479 let recovered: SharedUmbraPointer<u64> = unsafe { buf.assume_init() };
480 // Reader resolves the byte-recovered pointer through ITS
481 // mapping of the same region file.
482 assert_eq!(recovered.prefix, 0xABCD_EF01);
483 assert_eq!(recovered.resolve(&reader_region).unwrap(), 7777);
484 drop(writer_region);
485 drop(reader_region);
486 std::fs::remove_file(&p).ok();
487 }
488
489 // ============== Extension API tests ==============
490
491 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
492 #[repr(C)]
493 struct RegionId(u32);
494
495 impl UmbraExtension for RegionId {
496 const TAG: u8 = 1;
497 }
498
499 // Note: an 8-byte extension type like `struct MvccEpoch(u64)`
500 // would fail the compile-time size guard
501 // (ExtSizeCheck::<MvccEpoch>::CHECK fires the const_assert).
502 // Tests use the 6-byte Epoch48 variant below to stay within
503 // the 7-byte payload budget.
504
505 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
506 #[repr(C)]
507 struct Epoch48([u8; 6]); // 6 bytes - fits in 7
508 impl UmbraExtension for Epoch48 {
509 const TAG: u8 = 2;
510 }
511
512 #[test]
513 fn ext_starts_unset() {
514 let p: SharedUmbraPointer<u64> = SharedUmbraPointer::new(
515 OffsetPtr::new(0), 0x1234_5678,
516 );
517 assert_eq!(p.ext_tag(), 0);
518 assert_eq!(p.ext_payload_raw(), &[0u8; 7]);
519 }
520
521 #[test]
522 fn set_ext_then_ext_round_trip() {
523 let mut p: SharedUmbraPointer<u64> = SharedUmbraPointer::new(
524 OffsetPtr::new(7), 0xABCD,
525 );
526 p.set_ext(RegionId(42));
527 assert_eq!(p.ext_tag(), RegionId::TAG);
528 let r: Option<RegionId> = unsafe { p.ext::<RegionId>() };
529 assert_eq!(r, Some(RegionId(42)));
530 }
531
532 #[test]
533 fn ext_returns_none_when_tag_mismatch() {
534 let mut p: SharedUmbraPointer<u64> = SharedUmbraPointer::new(
535 OffsetPtr::new(7), 0xABCD,
536 );
537 p.set_ext(RegionId(42));
538 // Wrong type for the stored tag.
539 let r: Option<Epoch48> = unsafe { p.ext::<Epoch48>() };
540 assert_eq!(r, None,
541 "ext::<Epoch48>() must return None when stored tag is RegionId::TAG");
542 }
543
544 #[test]
545 fn ext_returns_none_when_unset() {
546 let p: SharedUmbraPointer<u64> = SharedUmbraPointer::new(
547 OffsetPtr::new(7), 0,
548 );
549 let r: Option<RegionId> = unsafe { p.ext::<RegionId>() };
550 assert_eq!(r, None);
551 }
552
553 #[test]
554 fn clear_ext_zeroes_tag_and_payload() {
555 let mut p: SharedUmbraPointer<u64> = SharedUmbraPointer::new(
556 OffsetPtr::new(7), 0,
557 );
558 p.set_ext(RegionId(123));
559 assert_ne!(p.ext_tag(), 0);
560 p.clear_ext();
561 assert_eq!(p.ext_tag(), 0);
562 assert_eq!(p.ext_payload_raw(), &[0u8; 7]);
563 }
564
565 #[test]
566 fn set_ext_overwrites_previous_extension() {
567 let mut p: SharedUmbraPointer<u64> = SharedUmbraPointer::new(
568 OffsetPtr::new(7), 0,
569 );
570 p.set_ext(RegionId(1));
571 p.set_ext(Epoch48([1, 2, 3, 4, 5, 6]));
572 assert_eq!(p.ext_tag(), Epoch48::TAG);
573 let r: Option<Epoch48> = unsafe { p.ext::<Epoch48>() };
574 assert_eq!(r, Some(Epoch48([1, 2, 3, 4, 5, 6])));
575 // Old RegionId is gone.
576 let old: Option<RegionId> = unsafe { p.ext::<RegionId>() };
577 assert_eq!(old, None);
578 }
579
580 #[test]
581 fn ext_does_not_affect_prefix_or_target() {
582 // Verify the extension bytes don't bleed into the
583 // target/prefix fields of the layout.
584 let mut p: SharedUmbraPointer<u64> = SharedUmbraPointer::new(
585 OffsetPtr::new(42), 0xDEAD_BEEF,
586 );
587 p.set_ext(Epoch48([0xFF; 6]));
588 assert_eq!(p.target, OffsetPtr::new(42));
589 assert_eq!(p.prefix, 0xDEAD_BEEF);
590 }
591
592 #[test]
593 fn pointers_fit_inside_shared_vec() {
594 // Verify the canonical composition pattern: store an array
595 // of SharedUmbraPointer<T> inside SharedVec, scan with
596 // prefix filter, resolve only the matches.
597 use crate::SharedVec;
598 let p_region = tmp("compose-region");
599 let p_vec = tmp("compose-vec");
600 let region: SharedRegion<u64> = SharedRegion::create(&p_region, 256).unwrap();
601 let pointers: SharedVec<SharedUmbraPointer<u64>> =
602 SharedVec::create(&p_vec, 256).unwrap();
603 for i in 0..50u64 {
604 let u = SharedUmbraPointer::from_region_alloc_hash_prefix(
605 ®ion, i * 10,
606 ).unwrap();
607 pointers.push_back(u).unwrap();
608 }
609 // Scan: for prefix p17 (the hash of 17 * 10 = 170), how
610 // many entries should match?
611 let target_prefix = hash_prefix_of(&170u64);
612 let snap = pointers.snapshot();
613 let hits: Vec<_> = snap.iter()
614 .enumerate()
615 .filter(|(_, u)| u.matches_prefix(target_prefix))
616 .collect();
617 // We may have zero or one collision; the resolved values
618 // for hits must all be valid u64s from the region.
619 for (_, u) in &hits {
620 let v = u.resolve(®ion).unwrap();
621 assert!((0..500u64).step_by(10).any(|x| x == v));
622 }
623 drop(region);
624 drop(pointers);
625 std::fs::remove_file(&p_region).ok();
626 std::fs::remove_file(&p_vec).ok();
627 }
628}