Skip to main content

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            &region, 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(&region).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(&region, 42u64).unwrap();
394        let b = SharedUmbraPointer::from_region_alloc_hash_prefix(&region, 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(&region, 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            &region, 12345u64, 0x1234_5678,
412        ).unwrap();
413        assert_eq!(u.prefix, 0x1234_5678);
414        assert_eq!(u.resolve(&region).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                &region, 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(&region).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                &region, 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(&region).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}