1use std::fs::{File, OpenOptions};
64use std::mem::size_of;
65use std::path::Path;
66use std::sync::atomic::{AtomicU64, Ordering};
67
68use memmap2::{Mmap, MmapMut, MmapOptions};
69
70pub const ARENA_MAGIC: u64 = 0x4150_5341_524E_4132;
77
78pub const ARENA_MAGIC_V1: u64 = 0x4150_5341_524E_4131;
81
82pub const OFFSET_BITS: u32 = 40;
85pub const LEN_BITS: u32 = 24;
88
89const _: () = assert!(OFFSET_BITS + LEN_BITS == 64);
90
91pub const MAX_OFFSET: u64 = (1u64 << OFFSET_BITS) - 1;
93pub const MAX_LEN: u64 = (1u64 << LEN_BITS) - 1;
95
96enum Mapping {
99 Writable(MmapMut),
100 ReadOnly(Mmap),
101}
102
103impl Mapping {
104 #[inline]
105 fn as_ptr(&self) -> *const u8 {
106 match self {
107 Mapping::Writable(m) => m.as_ptr(),
108 Mapping::ReadOnly(m) => m.as_ptr(),
109 }
110 }
111
112 #[inline]
113 fn is_writable(&self) -> bool {
114 matches!(self, Mapping::Writable(_))
115 }
116
117 fn flush(&self) -> Result<(), std::io::Error> {
118 match self {
119 Mapping::Writable(m) => m.flush(),
120 Mapping::ReadOnly(_) => Ok(()),
121 }
122 }
123
124 fn flush_async(&self) -> Result<(), std::io::Error> {
125 match self {
126 Mapping::Writable(m) => m.flush_async(),
127 Mapping::ReadOnly(_) => Ok(()),
128 }
129 }
130}
131
132#[repr(C, align(64))]
133pub struct ArenaHeader {
134 pub magic: u64,
135 pub capacity_bytes: u64,
136 pub used_bytes: AtomicU64,
137 _pad: [u8; 40],
138}
139
140const _: () = {
141 assert!(size_of::<ArenaHeader>() == 64);
142};
143
144pub const fn arena_file_size(capacity_bytes: usize) -> usize {
145 size_of::<ArenaHeader>() + capacity_bytes
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum ArenaError {
150 Full,
151 InvalidRef,
152 InvalidUtf8,
153 LayoutMismatch,
154 ReadOnly,
156 IoError(std::io::ErrorKind),
157}
158
159impl From<std::io::Error> for ArenaError {
160 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173pub struct StringRef {
174 pub offset: u64,
175 pub len: u32,
176}
177
178impl StringRef {
179 #[inline]
180 pub fn to_u64(self) -> u64 {
181 ((self.offset & MAX_OFFSET) << LEN_BITS) | (self.len as u64 & MAX_LEN)
182 }
183 #[inline]
184 pub fn from_u64(v: u64) -> Self {
185 Self {
186 offset: v >> LEN_BITS,
187 len: (v & MAX_LEN) as u32,
188 }
189 }
190}
191
192pub struct SharedStringArena {
193 _file: File,
194 mmap: Mapping,
195 capacity_bytes: usize,
196 header_sidecar: subetha_core::HandshakeHeader,
197 ring_sidecar: Box<subetha_core::ObservationRing>,
198}
199
200unsafe impl Send for SharedStringArena {}
201unsafe impl Sync for SharedStringArena {}
202
203impl subetha_sidecar::AdaptiveInstance for SharedStringArena {
204 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
205 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
206 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
207 Box::new(subetha_sidecar::NoMigrationPolicy)
208 }
209}
210
211impl SharedStringArena {
212 pub fn create(
219 path: impl AsRef<Path>, capacity_bytes: usize,
220 ) -> Result<Self, ArenaError> {
221 Self::check_capacity(capacity_bytes)?;
222 if Self::region_format_tag(path.as_ref()) == Some(ARENA_MAGIC_V1) {
227 return Err(ArenaError::LayoutMismatch);
228 }
229 let (file, mmap) = crate::mmf_attach::create_or_attach(
230 path.as_ref(),
231 arena_file_size(capacity_bytes),
232 |ptr| unsafe { Self::init_region(ptr, capacity_bytes) },
233 |ptr| unsafe { (*(ptr as *const ArenaHeader)).magic == ARENA_MAGIC },
234 )?;
235 let this = Self {
236 _file: file, mmap: Mapping::Writable(mmap), capacity_bytes,
237 header_sidecar: subetha_core::HandshakeHeader::new(),
238 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
239 };
240 this.validate(capacity_bytes)?;
241 Ok(this)
242 }
243
244 pub fn reset(
248 path: impl AsRef<Path>, capacity_bytes: usize,
249 ) -> Result<Self, ArenaError> {
250 Self::check_capacity(capacity_bytes)?;
251 let (file, mmap) = crate::mmf_attach::reset(
252 path.as_ref(),
253 arena_file_size(capacity_bytes),
254 |ptr| unsafe { Self::init_region(ptr, capacity_bytes) },
255 )?;
256 Ok(Self {
257 _file: file, mmap: Mapping::Writable(mmap), capacity_bytes,
258 header_sidecar: subetha_core::HandshakeHeader::new(),
259 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
260 })
261 }
262
263 fn region_format_tag(path: &Path) -> Option<u64> {
268 use std::io::Read;
269 let mut f = File::open(path).ok()?;
270 let mut tag = [0u8; 8];
271 f.read_exact(&mut tag).ok()?;
272 Some(u64::from_le_bytes(tag))
273 }
274
275 fn check_capacity(capacity_bytes: usize) -> Result<(), ArenaError> {
280 if capacity_bytes < 1 || capacity_bytes as u64 > MAX_OFFSET {
281 return Err(ArenaError::LayoutMismatch);
282 }
283 Ok(())
284 }
285
286 unsafe fn init_region(ptr: *mut u8, capacity_bytes: usize) {
294 let hdr = ptr as *mut ArenaHeader;
295 unsafe {
296 (*hdr).capacity_bytes = capacity_bytes as u64;
297 std::ptr::write_volatile(&raw mut (*hdr).magic, ARENA_MAGIC);
298 }
299 }
300
301 pub fn open(
302 path: impl AsRef<Path>, expected_capacity_bytes: usize,
303 ) -> Result<Self, ArenaError> {
304 if expected_capacity_bytes as u64 > MAX_OFFSET {
307 return Err(ArenaError::LayoutMismatch);
308 }
309 let total = arena_file_size(expected_capacity_bytes);
310 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
311 if file.metadata()?.len() < total as u64 {
312 return Err(ArenaError::LayoutMismatch);
313 }
314 let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
315 let this = Self {
316 _file: file, mmap: Mapping::Writable(mmap),
317 capacity_bytes: expected_capacity_bytes,
318 header_sidecar: subetha_core::HandshakeHeader::new(),
319 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
320 };
321 this.validate(expected_capacity_bytes)?;
322 Ok(this)
323 }
324
325 pub fn open_read_only(
332 path: impl AsRef<Path>, expected_capacity_bytes: usize,
333 ) -> Result<Self, ArenaError> {
334 let total = arena_file_size(expected_capacity_bytes);
335 let file = OpenOptions::new().read(true).open(path.as_ref())?;
336 if file.metadata()?.len() < total as u64 {
337 return Err(ArenaError::LayoutMismatch);
338 }
339 let mmap = unsafe { MmapOptions::new().len(total).map(&file)? };
340 let this = Self {
341 _file: file, mmap: Mapping::ReadOnly(mmap),
342 capacity_bytes: expected_capacity_bytes,
343 header_sidecar: subetha_core::HandshakeHeader::new(),
344 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
345 };
346 this.validate(expected_capacity_bytes)?;
347 Ok(this)
348 }
349
350 fn validate(&self, expected_capacity_bytes: usize) -> Result<(), ArenaError> {
352 let hdr = self.header();
353 if hdr.magic != ARENA_MAGIC || hdr.capacity_bytes != expected_capacity_bytes as u64 {
354 return Err(ArenaError::LayoutMismatch);
355 }
356 Ok(())
357 }
358
359 #[inline]
361 pub fn is_writable(&self) -> bool {
362 self.mmap.is_writable()
363 }
364
365 #[inline]
366 pub fn capacity_bytes(&self) -> usize { self.capacity_bytes }
367
368 #[inline]
369 pub fn used_bytes(&self) -> usize {
370 self.header().used_bytes.load(Ordering::Acquire) as usize
371 }
372
373 #[inline]
374 pub fn remaining_bytes(&self) -> usize {
375 self.capacity_bytes.saturating_sub(self.used_bytes())
376 }
377
378 fn header(&self) -> &ArenaHeader {
379 unsafe { &*(self.mmap.as_ptr() as *const ArenaHeader) }
380 }
381
382 pub fn intern(&self, s: &str) -> Result<StringRef, ArenaError> {
388 self.intern_bytes(s.as_bytes())
389 }
390
391 pub fn intern_bytes(&self, bytes: &[u8]) -> Result<StringRef, ArenaError> {
396 if !self.mmap.is_writable() {
397 return Err(ArenaError::ReadOnly);
398 }
399 let len = bytes.len() as u64;
400 if len > MAX_LEN || len > self.capacity_bytes as u64 {
404 self.ring_sidecar
405 .push_op(crate::sidecar_ops::string_arena::OP_INTERN, 1);
406 return Err(ArenaError::Full);
407 }
408 let offset = self.header().used_bytes.fetch_add(len, Ordering::AcqRel);
409 if offset.saturating_add(len) > self.capacity_bytes as u64 {
410 self.header().used_bytes.fetch_sub(len, Ordering::AcqRel);
411 self.ring_sidecar
412 .push_op(crate::sidecar_ops::string_arena::OP_INTERN, 1);
413 return Err(ArenaError::Full);
414 }
415 if offset.saturating_add(len) > MAX_OFFSET {
422 self.header().used_bytes.fetch_sub(len, Ordering::AcqRel);
423 self.ring_sidecar
424 .push_op(crate::sidecar_ops::string_arena::OP_INTERN, 1);
425 return Err(ArenaError::Full);
426 }
427 let dst = unsafe {
428 self.mmap.as_ptr()
429 .add(size_of::<ArenaHeader>())
430 .add(offset as usize)
431 as *mut u8
432 };
433 unsafe {
434 std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len());
435 }
436 self.ring_sidecar
437 .push_op(crate::sidecar_ops::string_arena::OP_INTERN, 0);
438 Ok(StringRef { offset, len: len as u32 })
439 }
440
441 pub fn get_bytes(&self, r: StringRef) -> Result<&[u8], ArenaError> {
444 let end = r.offset.saturating_add(r.len as u64);
445 if end > self.header().used_bytes.load(Ordering::Acquire) {
446 self.ring_sidecar
447 .push_op(crate::sidecar_ops::string_arena::OP_GET_BYTES, 1);
448 return Err(ArenaError::InvalidRef);
449 }
450 if end > self.capacity_bytes as u64 {
451 self.ring_sidecar
452 .push_op(crate::sidecar_ops::string_arena::OP_GET_BYTES, 1);
453 return Err(ArenaError::InvalidRef);
454 }
455 self.ring_sidecar
456 .push_op(crate::sidecar_ops::string_arena::OP_GET_BYTES, 0);
457 let base = unsafe {
458 self.mmap.as_ptr()
459 .add(size_of::<ArenaHeader>())
460 .add(r.offset as usize)
461 };
462 Ok(unsafe { std::slice::from_raw_parts(base, r.len as usize) })
463 }
464
465 pub fn get(&self, r: StringRef) -> Result<&str, ArenaError> {
469 let bytes = self.get_bytes(r)?;
470 std::str::from_utf8(bytes).map_err(|_| ArenaError::InvalidUtf8)
471 }
472
473 pub fn intern_and_get(&self, s: &str) -> Result<(StringRef, &str), ArenaError> {
476 let r = self.intern(s)?;
477 let got = self.get(r)?;
478 Ok((r, got))
479 }
480
481 pub fn clear(&self) {
486 if !self.mmap.is_writable() {
487 return;
488 }
489 self.header().used_bytes.store(0, Ordering::Release);
490 self.ring_sidecar
491 .push_op(crate::sidecar_ops::string_arena::OP_CLEAR, 0);
492 }
493
494 pub fn flush(&self) -> Result<(), ArenaError> {
495 self.mmap.flush()?;
496 Ok(())
497 }
498
499 pub fn flush_async(&self) -> Result<(), ArenaError> {
503 self.mmap.flush_async()?;
504 Ok(())
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use std::sync::Arc;
512 use std::thread;
513
514 fn tmp(name: &str) -> std::path::PathBuf {
515 let mut p = std::env::temp_dir();
516 let pid = std::process::id();
517 p.push(format!("subetha-arena-{name}-{pid}.bin"));
518 p
519 }
520
521 #[test]
524 fn second_create_attaches_and_keeps_strings() {
525 let p = tmp("attach");
526 std::fs::remove_file(&p).ok();
527 let a = SharedStringArena::create(&p, 4096).unwrap();
528 let r = a.intern("held").unwrap();
529
530 let a2 = SharedStringArena::create(&p, 4096).unwrap();
531 assert_eq!(a2.get(r).unwrap(), "held", "attach lost an interned string");
532 assert!(matches!(
533 SharedStringArena::create(&p, 2048),
534 Err(ArenaError::LayoutMismatch),
535 ));
536
537 drop(a);
540 drop(a2);
541 let fresh = SharedStringArena::reset(&p, 4096).unwrap();
542 assert_eq!(fresh.used_bytes(), 0, "reset kept interned bytes");
543 drop(fresh);
544 std::fs::remove_file(&p).ok();
545 }
546
547 #[test]
548 fn a_read_only_arena_resolves_refs_and_refuses_interning() {
549 let p = tmp("readonly");
550 let r = {
551 let w = SharedStringArena::create(&p, 4096).unwrap();
552 let r = w.intern("notepad.exe").unwrap();
553 w.flush().unwrap();
554 r
555 };
556 let ro = SharedStringArena::open_read_only(&p, 4096).unwrap();
557 assert!(!ro.is_writable());
558 assert_eq!(ro.get(r), Ok("notepad.exe"));
559 assert_eq!(ro.get_bytes(r), Ok(&b"notepad.exe"[..]));
560 assert_eq!(ro.used_bytes(), 11);
561 assert_eq!(ro.intern("more"), Err(ArenaError::ReadOnly));
562 assert_eq!(ro.intern_bytes(b"more"), Err(ArenaError::ReadOnly));
563 ro.clear();
564 assert_eq!(ro.used_bytes(), 11, "clear on a read-only arena is inert");
565 ro.flush().unwrap();
566 std::fs::remove_file(&p).ok();
567 }
568
569 #[test]
570 fn a_read_only_open_still_validates_the_header() {
571 let p = tmp("readonly-mismatch");
572 {
573 let w = SharedStringArena::create(&p, 4096).unwrap();
574 w.intern("x").unwrap();
575 w.flush().unwrap();
576 }
577 assert_eq!(
578 SharedStringArena::open_read_only(&p, 2048).err(),
579 Some(ArenaError::LayoutMismatch)
580 );
581 std::fs::remove_file(&p).ok();
582 }
583
584 #[test]
585 fn create_initial_state_is_empty() {
586 let p = tmp("init");
587 let a = SharedStringArena::create(&p, 1024).unwrap();
588 assert_eq!(a.capacity_bytes(), 1024);
589 assert_eq!(a.used_bytes(), 0);
590 assert_eq!(a.remaining_bytes(), 1024);
591 std::fs::remove_file(&p).ok();
592 }
593
594 #[test]
595 fn intern_and_get_round_trip() {
596 let p = tmp("rt");
597 let a = SharedStringArena::create(&p, 1024).unwrap();
598 let r1 = a.intern("hello").unwrap();
599 let r2 = a.intern("world").unwrap();
600 assert_eq!(a.get(r1).unwrap(), "hello");
601 assert_eq!(a.get(r2).unwrap(), "world");
602 assert_eq!(a.used_bytes(), 10);
603 std::fs::remove_file(&p).ok();
604 }
605
606 #[test]
607 fn empty_string_interns_with_zero_len() {
608 let p = tmp("empty");
609 let a = SharedStringArena::create(&p, 16).unwrap();
610 let r = a.intern("").unwrap();
611 assert_eq!(r.len, 0);
612 assert_eq!(a.get(r).unwrap(), "");
613 assert_eq!(a.used_bytes(), 0);
614 std::fs::remove_file(&p).ok();
615 }
616
617 #[test]
618 fn full_arena_returns_error() {
619 let p = tmp("full");
620 let a = SharedStringArena::create(&p, 10).unwrap();
621 a.intern("hello").unwrap();
622 a.intern("world").unwrap();
623 assert_eq!(a.intern("more").err(), Some(ArenaError::Full));
624 assert_eq!(a.used_bytes(), 10);
626 std::fs::remove_file(&p).ok();
627 }
628
629 #[test]
630 fn string_too_large_returns_full() {
631 let p = tmp("too-large");
632 let a = SharedStringArena::create(&p, 8).unwrap();
633 let big = "x".repeat(100);
634 assert_eq!(a.intern(&big).err(), Some(ArenaError::Full));
635 assert_eq!(a.used_bytes(), 0);
636 std::fs::remove_file(&p).ok();
637 }
638
639 #[test]
640 fn string_ref_packs_and_unpacks() {
641 let r = StringRef { offset: 0x1234_5678, len: 42 };
642 let packed = r.to_u64();
643 let unpacked = StringRef::from_u64(packed);
644 assert_eq!(unpacked, r);
645 }
646
647 #[test]
648 fn cross_handle_visibility() {
649 let p = tmp("cross-handle");
650 let writer = SharedStringArena::create(&p, 1024).unwrap();
651 let reader = SharedStringArena::open(&p, 1024).unwrap();
652 let r = writer.intern("cross-process").unwrap();
653 assert_eq!(reader.get(r).unwrap(), "cross-process");
654 std::fs::remove_file(&p).ok();
655 }
656
657 #[test]
658 fn invalid_ref_beyond_used_rejected() {
659 let p = tmp("invalid");
660 let a = SharedStringArena::create(&p, 1024).unwrap();
661 a.intern("hi").unwrap(); let bad = StringRef { offset: 100, len: 5 };
663 assert_eq!(a.get(bad).err(), Some(ArenaError::InvalidRef));
664 std::fs::remove_file(&p).ok();
665 }
666
667 #[test]
668 fn concurrent_interners_get_distinct_refs() {
669 let p = tmp("concurrent");
670 let a: Arc<SharedStringArena> = Arc::new(SharedStringArena::create(&p, 4096).unwrap());
671 let n_threads = 4;
672 let per_thread = 20;
673 let mut handles = vec![];
674 for t in 0..n_threads {
675 let a = a.clone();
676 handles.push(thread::spawn(move || {
677 let mut refs = vec![];
678 for i in 0..per_thread {
679 let s = format!("thread-{t}-msg-{i:03}");
680 let r = a.intern(&s).unwrap();
681 refs.push((s, r));
682 }
683 refs
684 }));
685 }
686 let all: Vec<(String, StringRef)> = handles.into_iter()
687 .flat_map(|h| h.join().unwrap())
688 .collect();
689 for (expected, r) in &all {
691 let got = a.get(*r).unwrap();
692 assert_eq!(got, expected,
693 "ref offset={} len={} should resolve to {expected}",
694 r.offset, r.len);
695 }
696 let mut refs: Vec<StringRef> = all.iter().map(|(_, r)| *r).collect();
698 refs.sort_by_key(|r| r.offset);
699 for w in refs.windows(2) {
700 let r1_end = w[0].offset + w[0].len as u64;
701 assert!(r1_end <= w[1].offset,
702 "ref {:?} overlaps with ref {:?}", w[0], w[1]);
703 }
704 std::fs::remove_file(&p).ok();
705 }
706
707 #[test]
708 fn intern_and_get_helper_returns_both() {
709 let p = tmp("intern-and-get");
710 let a = SharedStringArena::create(&p, 1024).unwrap();
711 let (r, s) = a.intern_and_get("composite").unwrap();
712 assert_eq!(s, "composite");
713 assert_eq!(a.get(r).unwrap(), "composite");
714 std::fs::remove_file(&p).ok();
715 }
716
717 #[test]
718 fn utf8_validation_on_get() {
719 let p = tmp("utf8");
720 let a = SharedStringArena::create(&p, 128).unwrap();
721 let r = a.intern("hello").unwrap();
723 assert!(a.get(r).is_ok());
724 let r2 = a.intern_bytes(&[0xFF, 0xFE, 0xFD]).unwrap();
728 assert_eq!(a.get(r2).err(), Some(ArenaError::InvalidUtf8));
729 assert_eq!(a.get_bytes(r2).unwrap(), &[0xFF, 0xFE, 0xFD]);
730 std::fs::remove_file(&p).ok();
731 }
732
733 #[test]
734 fn clear_resets_used_bytes() {
735 let p = tmp("clear");
736 let a = SharedStringArena::create(&p, 128).unwrap();
737 a.intern("first").unwrap();
738 a.intern("second").unwrap();
739 assert!(a.used_bytes() > 0);
740 a.clear();
741 assert_eq!(a.used_bytes(), 0);
742 let r = a.intern("after-clear").unwrap();
744 assert_eq!(a.get(r).unwrap(), "after-clear");
745 assert_eq!(r.offset, 0);
746 std::fs::remove_file(&p).ok();
747 }
748
749 #[test]
750 fn disk_persistence_survives_reopen() {
751 let p = tmp("disk");
752 let r_persist;
753 {
754 let a = SharedStringArena::create(&p, 1024).unwrap();
755 r_persist = a.intern("persisted-string").unwrap();
756 a.flush().unwrap();
757 }
758 let a2 = SharedStringArena::open(&p, 1024).unwrap();
759 assert_eq!(a2.get(r_persist).unwrap(), "persisted-string");
760 let r2 = a2.intern("more-after-reopen").unwrap();
762 assert_eq!(a2.get(r2).unwrap(), "more-after-reopen");
763 std::fs::remove_file(&p).ok();
764 }
765
766 #[test]
771 fn create_refuses_a_capacity_a_ref_cannot_address() {
772 let past = MAX_OFFSET as usize + 1;
773 assert!(matches!(
774 SharedStringArena::create(tmp("too-big"), past),
775 Err(ArenaError::LayoutMismatch),
776 ));
777 assert!(matches!(
778 SharedStringArena::reset(tmp("too-big-reset"), past),
779 Err(ArenaError::LayoutMismatch),
780 ));
781 assert!(matches!(
782 SharedStringArena::create(tmp("zero-cap"), 0),
783 Err(ArenaError::LayoutMismatch),
784 ));
785 }
786
787 #[test]
790 fn open_refuses_a_capacity_a_ref_cannot_address() {
791 assert!(matches!(
792 SharedStringArena::open(tmp("open-too-big"), MAX_OFFSET as usize + 1),
793 Err(ArenaError::LayoutMismatch),
794 ));
795 }
796
797 #[test]
801 fn a_string_ref_round_trips_at_both_field_ceilings() {
802 for (offset, len) in [
803 (0u64, 0u32),
804 (MAX_OFFSET, MAX_LEN as u32),
805 (MAX_OFFSET, 0),
806 (0, MAX_LEN as u32),
807 (1, 1),
808 (MAX_OFFSET - 1, MAX_LEN as u32 - 1),
809 ] {
810 let r = StringRef { offset, len };
811 let back = StringRef::from_u64(r.to_u64());
812 assert_eq!(back, r, "offset {offset} len {len} did not round-trip");
813 }
814 assert_eq!(StringRef { offset: 0, len: MAX_LEN as u32 }.to_u64(), MAX_LEN);
817 assert_eq!(
818 StringRef { offset: MAX_OFFSET, len: 0 }.to_u64(),
819 MAX_OFFSET << LEN_BITS
820 );
821 }
822
823 #[test]
829 fn an_old_format_region_is_refused() {
830 let p = tmp("old-format");
831 std::fs::remove_file(&p).ok();
832 {
833 let a = SharedStringArena::create(&p, 1024).unwrap();
834 a.intern("written-under-the-new-layout").unwrap();
835 a.flush().unwrap();
836 }
837 {
839 use std::io::{Seek, SeekFrom, Write};
840 let mut f = OpenOptions::new().write(true).open(&p).unwrap();
841 f.seek(SeekFrom::Start(0)).unwrap();
842 f.write_all(&ARENA_MAGIC_V1.to_le_bytes()).unwrap();
843 f.flush().unwrap();
844 }
845 assert!(
846 matches!(
847 SharedStringArena::open(&p, 1024),
848 Err(ArenaError::LayoutMismatch)
849 ),
850 "an arena tagged with the previous layout must be refused"
851 );
852 let started = std::time::Instant::now();
855 assert!(
856 matches!(
857 SharedStringArena::create(&p, 1024),
858 Err(ArenaError::LayoutMismatch)
859 ),
860 "create must refuse the previous layout, not attach to it"
861 );
862 assert!(
863 started.elapsed() < std::time::Duration::from_secs(1),
864 "create should refuse immediately, not wait out the attach deadline"
865 );
866 std::fs::remove_file(&p).ok();
867 }
868
869 #[test]
870 fn deduplication_via_hashmap_composition() {
871 use crate::SharedHashMap;
874 use crate::shared_hash_map::fnv1a_64;
875
876 let p_arena = tmp("dedup-arena");
877 let p_index = tmp("dedup-index");
878 let arena = SharedStringArena::create(&p_arena, 256).unwrap();
879 let index: SharedHashMap<u64, u64> = SharedHashMap::create(&p_index, 32).unwrap();
880
881 let s = "deduplicate-me";
882 let h = fnv1a_64(s.as_bytes());
883
884 let r = if let Some(packed) = index.get(&h) {
886 StringRef::from_u64(packed)
887 } else {
888 let r = arena.intern(s).unwrap();
889 index.insert(h, r.to_u64()).unwrap();
890 r
891 };
892 let used_after_first = arena.used_bytes();
893
894 let r2 = if let Some(packed) = index.get(&h) {
896 StringRef::from_u64(packed)
897 } else {
898 let r = arena.intern(s).unwrap();
899 index.insert(h, r.to_u64()).unwrap();
900 r
901 };
902 assert_eq!(r, r2, "dedup should return the same ref");
903 assert_eq!(arena.used_bytes(), used_after_first,
904 "second intern should not consume more bytes");
905
906 std::fs::remove_file(&p_arena).ok();
907 std::fs::remove_file(&p_index).ok();
908 }
909}