mmap_io/
mmap.rs

1//! Low-level memory-mapped file abstraction with safe, concurrent access.
2
3use std::{
4    fs::{File, OpenOptions},
5    path::{Path, PathBuf},
6    sync::Arc,
7};
8
9use memmap2::{Mmap, MmapMut};
10
11use crate::flush::FlushPolicy;
12
13#[cfg(feature = "cow")]
14use memmap2::MmapOptions;
15
16use parking_lot::RwLock;
17
18use crate::errors::{MmapIoError, Result};
19use crate::utils::{ensure_in_bounds, slice_range};
20
21// Error message constants
22const ERR_ZERO_SIZE: &str = "Size must be greater than zero";
23const ERR_ZERO_LENGTH_FILE: &str = "Cannot map zero-length file";
24
25/// Access mode for a memory-mapped file.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MmapMode {
28    /// Read-only mapping.
29    ReadOnly,
30    /// Read-write mapping.
31    ReadWrite,
32    /// Copy-on-Write mapping (private). Writes affect this mapping only; the underlying file remains unchanged.
33    CopyOnWrite,
34}
35
36#[doc(hidden)]
37pub struct Inner {
38    pub(crate) path: PathBuf,
39    pub(crate) file: File,
40    pub(crate) mode: MmapMode,
41    // Cached length to avoid repeated metadata queries
42    pub(crate) cached_len: RwLock<u64>,
43    // The mapping itself. We use an enum to hold either RO or RW mapping.
44    pub(crate) map: MapVariant,
45    // Flush policy and accounting (RW only)
46    pub(crate) flush_policy: FlushPolicy,
47    pub(crate) written_since_last_flush: RwLock<u64>,
48    // Huge pages preference (builder-set), effective on supported platforms
49    #[cfg(feature = "hugepages")]
50    pub(crate) huge_pages: bool,
51}
52
53#[doc(hidden)]
54pub enum MapVariant {
55    Ro(Mmap),
56    Rw(RwLock<MmapMut>),
57    /// Private, per-process copy-on-write mapping. Underlying file is not modified by writes.
58    Cow(Mmap),
59}
60
61/// Memory-mapped file with safe, zero-copy region access.
62///
63/// This is the core type for memory-mapped file operations. It provides:
64/// - Safe concurrent access through interior mutability
65/// - Zero-copy reads and writes
66/// - Automatic bounds checking
67/// - Cross-platform compatibility
68///
69/// # Examples
70///
71/// ```no_run
72/// use mmap_io::{MemoryMappedFile, MmapMode};
73///
74/// // Create a new 1KB file
75/// let mmap = MemoryMappedFile::create_rw("data.bin", 1024)?;
76///
77/// // Write some data
78/// mmap.update_region(0, b"Hello, world!")?;
79/// mmap.flush()?;
80///
81/// // Open existing file read-only
82/// let ro_mmap = MemoryMappedFile::open_ro("data.bin")?;
83/// let data = ro_mmap.as_slice(0, 13)?;
84/// assert_eq!(data, b"Hello, world!");
85/// # Ok::<(), mmap_io::MmapIoError>(())
86/// ```
87///
88/// Cloning this struct is cheap; it clones an Arc to the inner state.
89/// For read-write mappings, interior mutability is protected with an `RwLock`.
90#[derive(Clone)]
91pub struct MemoryMappedFile {
92    pub(crate) inner: Arc<Inner>,
93}
94
95impl std::fmt::Debug for MemoryMappedFile {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        let mut ds = f.debug_struct("MemoryMappedFile");
98        ds.field("path", &self.inner.path)
99            .field("mode", &self.inner.mode)
100            .field("len", &self.len());
101        #[cfg(feature = "hugepages")]
102        {
103            ds.field("huge_pages", &self.inner.huge_pages);
104        }
105        ds.finish()
106    }
107}
108
109impl MemoryMappedFile {
110    /// Builder for constructing a MemoryMappedFile with custom options.
111    ///
112    /// Example:
113    /// ```
114    /// # use mmap_io::{MemoryMappedFile, MmapMode};
115    /// # use mmap_io::flush::FlushPolicy;
116    /// // let mmap = MemoryMappedFile::builder("file.bin")
117    /// //     .mode(MmapMode::ReadWrite)
118    /// //     .size(1_000_000)
119    /// //     .flush_policy(FlushPolicy::EveryBytes(1_000_000))
120    /// //     .create().unwrap();
121    /// ```
122    pub fn builder<P: AsRef<Path>>(path: P) -> MemoryMappedFileBuilder {
123        MemoryMappedFileBuilder {
124            path: path.as_ref().to_path_buf(),
125            size: None,
126            mode: None,
127            flush_policy: FlushPolicy::default(),
128            #[cfg(feature = "hugepages")]
129            huge_pages: false,
130        }
131    }
132
133    /// Create a new file (truncating if exists) and memory-map it in read-write mode with the given size.
134    ///
135    /// # Errors
136    ///
137    /// Returns `MmapIoError::ResizeFailed` if size is zero.
138    /// Returns `MmapIoError::Io` if file creation or mapping fails.
139    pub fn create_rw<P: AsRef<Path>>(path: P, size: u64) -> Result<Self> {
140        if size == 0 {
141            return Err(MmapIoError::ResizeFailed(ERR_ZERO_SIZE.into()));
142        }
143        let path_ref = path.as_ref();
144        let file = OpenOptions::new()
145            .create(true)
146            .write(true)
147            .read(true)
148            .truncate(true)
149            .open(path_ref)?;
150        file.set_len(size)?;
151        // SAFETY: The file has been created with the correct size and permissions.
152        // memmap2 handles platform-specific mmap details safely.
153        // Note: create_rw convenience ignores huge pages; use builder for that.
154        let mmap = unsafe { MmapMut::map_mut(&file)? };
155        let inner = Inner {
156            path: path_ref.to_path_buf(),
157            file,
158            mode: MmapMode::ReadWrite,
159            cached_len: RwLock::new(size),
160            map: MapVariant::Rw(RwLock::new(mmap)),
161            flush_policy: FlushPolicy::default(),
162            written_since_last_flush: RwLock::new(0),
163            #[cfg(feature = "hugepages")]
164            huge_pages: false,
165        };
166        Ok(Self { inner: Arc::new(inner) })
167    }
168
169    /// Open an existing file and memory-map it read-only.
170    ///
171    /// # Errors
172    ///
173    /// Returns `MmapIoError::Io` if file opening or mapping fails.
174    pub fn open_ro<P: AsRef<Path>>(path: P) -> Result<Self> {
175        let path_ref = path.as_ref();
176        let file = OpenOptions::new().read(true).open(path_ref)?;
177        let len = file.metadata()?.len();
178        // SAFETY: The file is opened read-only and memmap2 ensures safe mapping.
179        let mmap = unsafe { Mmap::map(&file)? };
180        let inner = Inner {
181            path: path_ref.to_path_buf(),
182            file,
183            mode: MmapMode::ReadOnly,
184            cached_len: RwLock::new(len),
185            map: MapVariant::Ro(mmap),
186            flush_policy: FlushPolicy::Never,
187            written_since_last_flush: RwLock::new(0),
188            #[cfg(feature = "hugepages")]
189            huge_pages: false,
190        };
191        Ok(Self { inner: Arc::new(inner) })
192    }
193
194    /// Open an existing file and memory-map it read-write.
195    ///
196    /// # Errors
197    ///
198    /// Returns `MmapIoError::ResizeFailed` if file is zero-length.
199    /// Returns `MmapIoError::Io` if file opening or mapping fails.
200    pub fn open_rw<P: AsRef<Path>>(path: P) -> Result<Self> {
201        let path_ref = path.as_ref();
202        let file = OpenOptions::new().read(true).write(true).open(path_ref)?;
203        let len = file.metadata()?.len();
204        if len == 0 {
205            return Err(MmapIoError::ResizeFailed(ERR_ZERO_LENGTH_FILE.into()));
206        }
207        // SAFETY: The file is opened read-write with proper permissions.
208        // We've verified the file is not zero-length.
209        // Note: open_rw convenience ignores huge pages; use builder for that.
210        let mmap = unsafe { MmapMut::map_mut(&file)? };
211        let inner = Inner {
212            path: path_ref.to_path_buf(),
213            file,
214            mode: MmapMode::ReadWrite,
215            cached_len: RwLock::new(len),
216            map: MapVariant::Rw(RwLock::new(mmap)),
217            flush_policy: FlushPolicy::default(),
218            written_since_last_flush: RwLock::new(0),
219            #[cfg(feature = "hugepages")]
220            huge_pages: false,
221        };
222        Ok(Self { inner: Arc::new(inner) })
223    }
224
225    /// Return current mapping mode.
226    #[must_use]
227    pub fn mode(&self) -> MmapMode {
228        self.inner.mode
229    }
230
231    /// Total length of the mapped file in bytes (cached).
232    #[must_use]
233    pub fn len(&self) -> u64 {
234        *self.inner.cached_len.read()
235    }
236
237    /// Whether the mapped file is empty.
238    #[must_use]
239    pub fn is_empty(&self) -> bool {
240        self.len() == 0
241    }
242
243    /// Get a zero-copy read-only slice for the given [offset, offset+len).
244    /// For RW mappings, cannot return a reference bound to a temporary guard; use `read_into` instead.
245    ///
246    /// # Errors
247    ///
248    /// Returns `MmapIoError::OutOfBounds` if range exceeds file bounds.
249    /// Returns `MmapIoError::InvalidMode` for RW mappings (use `read_into` instead).
250    pub fn as_slice(&self, offset: u64, len: u64) -> Result<&[u8]> {
251        let total = self.current_len()?;
252        ensure_in_bounds(offset, len, total)?;
253        match &self.inner.map {
254            MapVariant::Ro(m) => {
255                let (start, end) = slice_range(offset, len, total)?;
256                Ok(&m[start..end])
257            }
258            MapVariant::Rw(_lock) => Err(MmapIoError::InvalidMode("use read_into for RW mappings")),
259            MapVariant::Cow(m) => {
260                let (start, end) = slice_range(offset, len, total)?;
261                Ok(&m[start..end])
262            }
263        }
264    }
265
266    /// Get a zero-copy mutable slice for the given [offset, offset+len).
267    /// Only available in `ReadWrite` mode.
268    ///
269    /// # Errors
270    ///
271    /// Returns `MmapIoError::InvalidMode` if not in `ReadWrite` mode.
272    /// Returns `MmapIoError::OutOfBounds` if range exceeds file bounds.
273    pub fn as_slice_mut(&self, offset: u64, len: u64) -> Result<MappedSliceMut<'_>> {
274        let (start, end) = slice_range(offset, len, self.current_len()?)?;
275        match &self.inner.map {
276            MapVariant::Ro(_) => Err(MmapIoError::InvalidMode("mutable access on read-only mapping")),
277            MapVariant::Rw(lock) => {
278                let guard = lock.write();
279                Ok(MappedSliceMut {
280                    guard,
281                    range: start..end,
282                })
283            }
284            MapVariant::Cow(_) => {
285                // Phase-1: COW is read-only for safety. Writable COW will be added with a persistent
286                // private RW view in a follow-up change.
287                Err(MmapIoError::InvalidMode("mutable access on copy-on-write mapping (phase-1 read-only)"))
288            }
289        }
290    }
291
292    /// Copy the provided bytes into the mapped file at the given offset.
293    /// Bounds-checked, zero-copy write.
294    ///
295    /// # Errors
296    ///
297    /// Returns `MmapIoError::InvalidMode` if not in `ReadWrite` mode.
298    /// Returns `MmapIoError::OutOfBounds` if range exceeds file bounds.
299    pub fn update_region(&self, offset: u64, data: &[u8]) -> Result<()> {
300        if data.is_empty() {
301            return Ok(());
302        }
303        if self.inner.mode != MmapMode::ReadWrite {
304            return Err(MmapIoError::InvalidMode("Update region requires ReadWrite mode."));
305        }
306        let len = data.len() as u64;
307        let (start, end) = slice_range(offset, len, self.current_len()?)?;
308        match &self.inner.map {
309            MapVariant::Ro(_) => Err(MmapIoError::InvalidMode("Cannot write to read-only mapping.")),
310            MapVariant::Rw(lock) => {
311                {
312                    let mut guard = lock.write();
313                    guard[start..end].copy_from_slice(data);
314                }
315                // Apply flush policy
316                self.apply_flush_policy(len)?;
317                Ok(())
318            }
319            MapVariant::Cow(_) => Err(MmapIoError::InvalidMode("Cannot write to copy-on-write mapping (phase-1 read-only).")),
320        }
321    }
322
323    /// Async write that enforces Async-Only Flushing semantics: always flush after write.
324    /// Uses spawn_blocking to avoid blocking the async scheduler.
325    #[cfg(feature = "async")]
326    pub async fn update_region_async(&self, offset: u64, data: &[u8]) -> Result<()> {
327        // Perform the write in a blocking task
328        let this = self.clone();
329        let data_vec = data.to_vec();
330        tokio::task::spawn_blocking(move || {
331            // Synchronous write
332            this.update_region(offset, &data_vec)?;
333            // Async-only flushing: unconditionally flush after write when using async path
334            this.flush()
335        })
336        .await
337        .map_err(|e| MmapIoError::FlushFailed(format!("join error: {e}")))?
338    }
339
340    /// Flush changes to disk. For read-only mappings, this is a no-op.
341    ///
342    /// Smart internal guards:
343    /// - Skip I/O when there are no pending writes (accumulator is zero)
344    /// - On Linux, use msync(MS_ASYNC) as a cheaper hint; fall back to full flush on error
345    ///
346    /// # Errors
347    ///
348    /// Returns `MmapIoError::FlushFailed` if flush operation fails.
349    pub fn flush(&self) -> Result<()> {
350        match &self.inner.map {
351            MapVariant::Ro(_) => Ok(()),
352            MapVariant::Cow(_) => Ok(()), // no-op for COW
353            MapVariant::Rw(lock) => {
354                // Fast path: no pending writes => skip flushing I/O
355                if *self.inner.written_since_last_flush.read() == 0 {
356                    return Ok(());
357                }
358
359                // Platform-optimized path: Linux MS_ASYNC best-effort
360                #[cfg(all(unix, target_os = "linux"))]
361                {
362                    use std::os::fd::AsRawFd;
363                    // SAFETY: msync requires a valid mapping address/len; memmap2 handles mapping.
364                    // We conservatively issue MS_ASYNC on the entire file length.
365                    let len = self.current_len()? as usize;
366                    if len > 0 {
367                        let fd = self.inner.file.as_raw_fd();
368                        // Obtain a temporary RO view to get a stable address range without write-locking
369                        // We avoid exposing raw ptr; we only pass to msync internally.
370                        let addr_res: std::result::Result<(), ()> = match &self.inner.map {
371                            MapVariant::Rw(lock) => {
372                                let guard = lock.read();
373                                let ptr = guard.as_ptr() as *const libc::c_void;
374                                let ret = unsafe { libc::msync(ptr as *mut libc::c_void, len, libc::MS_ASYNC) };
375                                if ret == 0 {
376                                    // Consider MS_ASYNC success as sufficient and reset accumulator
377                                    *self.inner.written_since_last_flush.write() = 0;
378                                    return Ok(());
379                                }
380                                // fall through to full flush on error
381                                drop(guard);
382                                let _ = fd; // silence unused on non-error paths
383                                Ok(())
384                            }
385                            _ => Ok(()),
386                        };
387                        let _ = addr_res;
388                    }
389                }
390
391                // Fallback/full flush using memmap2 API
392                let guard = lock.read();
393                guard.flush().map_err(|e| MmapIoError::FlushFailed(e.to_string()))?;
394                // Reset accumulator after a successful flush
395                *self.inner.written_since_last_flush.write() = 0;
396                Ok(())
397            }
398        }
399    }
400
401    /// Async flush changes to disk. For read-only or COW mappings, this is a no-op.
402    /// This method enforces "async-only flushing" semantics for async paths.
403    #[cfg(feature = "async")]
404    pub async fn flush_async(&self) -> Result<()> {
405        // Use spawn_blocking to avoid blocking the async scheduler
406        let this = self.clone();
407        tokio::task::spawn_blocking(move || this.flush()).await.map_err(|e| MmapIoError::FlushFailed(format!("join error: {e}")))?
408    }
409
410    /// Async flush a specific byte range to disk.
411    #[cfg(feature = "async")]
412    pub async fn flush_range_async(&self, offset: u64, len: u64) -> Result<()> {
413        let this = self.clone();
414        tokio::task::spawn_blocking(move || this.flush_range(offset, len)).await.map_err(|e| MmapIoError::FlushFailed(format!("join error: {e}")))?
415    }
416
417    /// Flush a specific byte range to disk.
418    ///
419    /// Smart internal guards:
420    /// - Skip I/O when there are no pending writes in accumulator
421    /// - On Linux, prefer msync(MS_ASYNC) for the range; fall back to full range flush on error
422    ///
423    /// # Errors
424    ///
425    /// Returns `MmapIoError::OutOfBounds` if range exceeds file bounds.
426    /// Returns `MmapIoError::FlushFailed` if flush operation fails.
427    pub fn flush_range(&self, offset: u64, len: u64) -> Result<()> {
428        if len == 0 {
429            return Ok(());
430        }
431        ensure_in_bounds(offset, len, self.current_len()?)?;
432        match &self.inner.map {
433            MapVariant::Ro(_) => Ok(()),
434            MapVariant::Cow(_) => Ok(()), // no-op for COW
435            MapVariant::Rw(lock) => {
436                // If we have no accumulated writes, skip I/O
437                if *self.inner.written_since_last_flush.read() == 0 {
438                    return Ok(());
439                }
440
441                let (start, end) = slice_range(offset, len, self.current_len()?)?;
442                let range_len = end - start;
443
444                // Linux MS_ASYNC optimization
445                #[cfg(all(unix, target_os = "linux"))]
446                {
447                    // SAFETY: msync on a valid mapped range. We translate to a pointer within the map.
448                    let msync_res: i32 = {
449                        let guard = lock.read();
450                        let base = guard.as_ptr();
451                        let ptr = unsafe { base.add(start) } as *mut libc::c_void;
452                        let ret = unsafe { libc::msync(ptr, range_len, libc::MS_ASYNC) };
453                        ret
454                    };
455                    if msync_res == 0 {
456                        // Consider MS_ASYNC success and reset accumulator
457                        *self.inner.written_since_last_flush.write() = 0;
458                        return Ok(());
459                    }
460                    // else fall through to full flush_range
461                }
462
463                let guard = lock.read();
464                guard
465                    .flush_range(start, range_len)
466                    .map_err(|e| MmapIoError::FlushFailed(e.to_string()))?;
467                // Reset accumulator after a successful flush
468                *self.inner.written_since_last_flush.write() = 0;
469                Ok(())
470            }
471        }
472    }
473
474    /// Resize (grow or shrink) the mapped file (RW only). This remaps the file internally.
475    ///
476    /// # Errors
477    ///
478    /// Returns `MmapIoError::InvalidMode` if not in `ReadWrite` mode.
479    /// Returns `MmapIoError::ResizeFailed` if new size is zero.
480    /// Returns `MmapIoError::Io` if resize operation fails.
481    pub fn resize(&self, new_size: u64) -> Result<()> {
482        if self.inner.mode != MmapMode::ReadWrite {
483            return Err(MmapIoError::InvalidMode("Resize requires ReadWrite mode."));
484        }
485        if new_size == 0 {
486            return Err(MmapIoError::ResizeFailed("New size must be greater than zero.".into()));
487        }
488
489        let current = self.current_len()?;
490
491        // On Windows, shrinking a file with an active mapping fails with:
492        // "The requested operation cannot be performed on a file with a user-mapped section open."
493        // To keep APIs usable and tests passing, we virtually shrink by updating the cached length,
494        // avoiding truncation while a mapping is active. Growing still truncates and remaps.
495        #[cfg(windows)]
496        {
497            use std::cmp::Ordering;
498            match new_size.cmp(&current) {
499                Ordering::Less => {
500                    // Virtually shrink: only update the cached length.
501                    *self.inner.cached_len.write() = new_size;
502                    return Ok(());
503                }
504                Ordering::Equal => {
505                    return Ok(());
506                }
507                Ordering::Greater => {
508                    // Proceed with normal grow: extend file then remap.
509                }
510            }
511        }
512
513        // Update length on disk for non-windows, or for growing on windows.
514        // Silence unused variable warning when the Windows shrink early-return path is compiled.
515        let _ = &current;
516        self.inner.file.set_len(new_size)?;
517
518        // Remap with the new size.
519        let new_map = unsafe { MmapMut::map_mut(&self.inner.file)? };
520        match &self.inner.map {
521            MapVariant::Ro(_) => Err(MmapIoError::InvalidMode("internal: cannot remap RO as RW")),
522            MapVariant::Cow(_) => Err(MmapIoError::InvalidMode("resize not supported on copy-on-write mapping")),
523            MapVariant::Rw(lock) => {
524                let mut guard = lock.write();
525                *guard = new_map;
526                // Update cached length
527                *self.inner.cached_len.write() = new_size;
528                Ok(())
529            }
530        }
531    }
532
533    /// Path to the underlying file.
534    #[must_use]
535    pub fn path(&self) -> &Path {
536        &self.inner.path
537    }
538}
539
540#[cfg(feature = "hugepages")]
541fn map_mut_with_options(file: &File, len: u64, huge: bool) -> Result<MmapMut> {
542    #[cfg(all(unix, target_os = "linux"))]
543    {
544        use std::os::fd::AsRawFd;
545        if huge {
546            // Safety: we construct a mapping with custom flags using libc::mmap and wrap it into MmapMut.
547            // memmap2 does not expose MAP_HUGETLB; we fallback to native call and then build from raw parts.
548            unsafe {
549                let prot = libc::PROT_READ | libc::PROT_WRITE;
550                // Use private + shared semantics equivalent to memmap2 default for RW mapping
551                let flags = libc::MAP_SHARED | libc::MAP_HUGETLB;
552                let addr = libc::mmap(
553                    std::ptr::null_mut(),
554                    len as usize,
555                    prot,
556                    flags,
557                    file.as_raw_fd(),
558                    0,
559                );
560                if addr == libc::MAP_FAILED {
561                    // Fallback: standard mapping
562                    return MmapMut::map_mut(file).map_err(|e| MmapIoError::Io(e.into()));
563                }
564                // Create MmapMut from raw parts using memmap2 API
565                // SAFETY: memmap2 provides MmapMut::map_mut which does mmap internally; it doesn't expose from_raw.
566                // Since memmap2 has no from_raw stable API, we must unmap and fall back if custom mmap is not viable.
567                // Therefore, we simply fall back to memmap2 map_mut as above if custom mmap not possible.
568                // If we reached here, custom mmap succeeded; but memmap2 cannot adopt it, so we will munmap and fall back to map_mut.
569                libc::munmap(addr, len as usize);
570                return MmapMut::map_mut(file).map_err(|e| MmapIoError::Io(e.into()));
571            }
572        } else {
573            return unsafe { MmapMut::map_mut(file) }.map_err(|e| MmapIoError::Io(e.into()));
574        }
575    }
576    #[cfg(not(all(unix, target_os = "linux")))]
577    {
578        let _ = (len, huge);
579        unsafe { MmapMut::map_mut(file) }.map_err(MmapIoError::Io)
580    }
581}
582
583#[cfg(feature = "cow")]
584impl MemoryMappedFile {
585    /// Open an existing file and memory-map it copy-on-write (private).
586    /// Changes through this mapping are visible only within this process; the underlying file remains unchanged.
587    pub fn open_cow<P: AsRef<Path>>(path: P) -> Result<Self> {
588        let path_ref = path.as_ref();
589        let file = OpenOptions::new().read(true).open(path_ref)?;
590        let len = file.metadata()?.len();
591        if len == 0 {
592            return Err(MmapIoError::ResizeFailed(ERR_ZERO_LENGTH_FILE.into()));
593        }
594        // SAFETY: memmap2 handles platform specifics. We request a private (copy-on-write) mapping.
595        let mmap = unsafe {
596            let mut opts = MmapOptions::new();
597            opts.len(len as usize);
598            #[cfg(unix)]
599            {
600                // memmap2 currently does not expose a stable .private() on all Rust/MSRV combos.
601                // On Unix, map() of a read-only file yields an immutable mapping; for COW semantics
602                // we rely on platform-specific behavior when writing is disallowed here in phase-1.
603                // When writable COW is introduced, we will use platform flags via memmap2 internals.
604                opts.map(&file)?
605            }
606            #[cfg(not(unix))]
607            {
608                // On Windows, memmap2 maps with appropriate WRITECOPY semantics internally for private mappings.
609                opts.map(&file)?
610            }
611        };
612        let inner = Inner {
613            path: path_ref.to_path_buf(),
614            file,
615            mode: MmapMode::CopyOnWrite,
616            cached_len: RwLock::new(len),
617            map: MapVariant::Cow(mmap),
618            // COW never flushes underlying file in phase-1
619            flush_policy: FlushPolicy::Never,
620            written_since_last_flush: RwLock::new(0),
621            #[cfg(feature = "hugepages")]
622            huge_pages: false,
623        };
624        Ok(Self { inner: Arc::new(inner) })
625    }
626}
627
628impl MemoryMappedFile {
629    fn apply_flush_policy(&self, written: u64) -> Result<()> {
630        match self.inner.flush_policy {
631            FlushPolicy::Never | FlushPolicy::Manual => Ok(()),
632            FlushPolicy::Always => {
633                // Record then flush immediately
634                *self.inner.written_since_last_flush.write() += written;
635                self.flush()
636            }
637            FlushPolicy::EveryBytes(n) => {
638                let n = n as u64;
639                if n == 0 {
640                    return Ok(());
641                }
642                let mut acc = self.inner.written_since_last_flush.write();
643                *acc += written;
644                if *acc >= n {
645                    // Do not reset prematurely; let flush() clear on success
646                    drop(acc);
647                    self.flush()
648                } else {
649                    Ok(())
650                }
651            }
652            FlushPolicy::EveryWrites(w) => {
653                if w == 0 {
654                    return Ok(());
655                }
656                let mut acc = self.inner.written_since_last_flush.write();
657                *acc += 1;
658                if *acc >= w as u64 {
659                    drop(acc);
660                    self.flush()
661                } else {
662                    Ok(())
663                }
664            }
665            FlushPolicy::EveryMillis(_ms) => {
666                // Phase-1: treat as Manual; user drives time-based flushing externally.
667                Ok(())
668            }
669        }
670    }
671
672    /// Return the up-to-date file length (cached).
673    /// This ensures length remains correct even after resize.
674    ///
675    /// # Errors
676    ///
677    /// Returns `MmapIoError::Io` if metadata query fails (not expected in current implementation).
678    pub fn current_len(&self) -> Result<u64> {
679        Ok(*self.inner.cached_len.read())
680    }
681
682    /// Read bytes from the mapping into the provided buffer starting at `offset`.
683    /// Length is `buf.len()`; performs bounds checks.
684    ///
685    /// # Errors
686    ///
687    /// Returns `MmapIoError::OutOfBounds` if range exceeds file bounds.
688    pub fn read_into(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
689        let total = self.current_len()?;
690        let len = buf.len() as u64;
691        ensure_in_bounds(offset, len, total)?;
692        match &self.inner.map {
693            MapVariant::Ro(m) => {
694                let (start, end) = slice_range(offset, len, total)?;
695                buf.copy_from_slice(&m[start..end]);
696                Ok(())
697            }
698            MapVariant::Rw(lock) => {
699                let guard = lock.read();
700                let (start, end) = slice_range(offset, len, total)?;
701                buf.copy_from_slice(&guard[start..end]);
702                Ok(())
703            }
704            MapVariant::Cow(m) => {
705                let (start, end) = slice_range(offset, len, total)?;
706                buf.copy_from_slice(&m[start..end]);
707                Ok(())
708            }
709        }
710    }
711}
712
713/// Builder for MemoryMappedFile construction with options.
714pub struct MemoryMappedFileBuilder {
715    path: PathBuf,
716    size: Option<u64>,
717    mode: Option<MmapMode>,
718    flush_policy: FlushPolicy,
719    #[cfg(feature = "hugepages")]
720    huge_pages: bool,
721}
722
723impl MemoryMappedFileBuilder {
724    /// Specify the size (required for create/ReadWrite new files).
725    pub fn size(mut self, size: u64) -> Self {
726        self.size = Some(size);
727        self
728    }
729
730    /// Specify the mode (ReadOnly, ReadWrite, CopyOnWrite).
731    pub fn mode(mut self, mode: MmapMode) -> Self {
732        self.mode = Some(mode);
733        self
734    }
735
736    /// Specify the flush policy.
737    pub fn flush_policy(mut self, policy: FlushPolicy) -> Self {
738        self.flush_policy = policy;
739        self
740    }
741
742    /// Request Huge Pages (Linux MAP_HUGETLB). No-op on non-Linux platforms.
743    #[cfg(feature = "hugepages")]
744    pub fn huge_pages(mut self, enable: bool) -> Self {
745        self.huge_pages = enable;
746        self
747    }
748
749    /// Create a new mapping; for ReadWrite requires size for creation.
750    pub fn create(self) -> Result<MemoryMappedFile> {
751        let mode = self.mode.unwrap_or(MmapMode::ReadWrite);
752        match mode {
753            MmapMode::ReadWrite => {
754                let size = self.size.ok_or_else(|| {
755                    MmapIoError::ResizeFailed("Size must be set for create() in ReadWrite mode".into())
756                })?;
757                let path_ref = &self.path;
758                let file = OpenOptions::new()
759                    .create(true)
760                    .write(true)
761                    .read(true)
762                    .truncate(true)
763                    .open(path_ref)?;
764                file.set_len(size)?;
765                // Map with consideration for huge pages if requested
766                #[cfg(feature = "hugepages")]
767                let mmap = map_mut_with_options(&file, size, self.huge_pages)?;
768                #[cfg(not(feature = "hugepages"))]
769                let mmap = unsafe { MmapMut::map_mut(&file)? };
770                let inner = Inner {
771                    path: path_ref.clone(),
772                    file,
773                    mode,
774                    cached_len: RwLock::new(size),
775                    map: MapVariant::Rw(RwLock::new(mmap)),
776                    flush_policy: self.flush_policy,
777                    written_since_last_flush: RwLock::new(0),
778                    #[cfg(feature = "hugepages")]
779                    huge_pages: self.huge_pages,
780                };
781                Ok(MemoryMappedFile { inner: Arc::new(inner) })
782            }
783            MmapMode::ReadOnly => {
784                let path_ref = &self.path;
785                let file = OpenOptions::new().read(true).open(path_ref)?;
786                let len = file.metadata()?.len();
787                let mmap = unsafe { Mmap::map(&file)? };
788                let inner = Inner {
789                    path: path_ref.clone(),
790                    file,
791                    mode,
792                    cached_len: RwLock::new(len),
793                    map: MapVariant::Ro(mmap),
794                    flush_policy: FlushPolicy::Never,
795                    written_since_last_flush: RwLock::new(0),
796                    #[cfg(feature = "hugepages")]
797                    huge_pages: false,
798                };
799                Ok(MemoryMappedFile { inner: Arc::new(inner) })
800            }
801            MmapMode::CopyOnWrite => {
802                #[cfg(feature = "cow")]
803                {
804                    let path_ref = &self.path;
805                    let file = OpenOptions::new().read(true).open(path_ref)?;
806                    let len = file.metadata()?.len();
807                    if len == 0 {
808                        return Err(MmapIoError::ResizeFailed(ERR_ZERO_LENGTH_FILE.into()));
809                    }
810                    let mmap = unsafe {
811                        let mut opts = MmapOptions::new();
812                        opts.len(len as usize);
813                        opts.map(&file)?
814                    };
815                    let inner = Inner {
816                        path: path_ref.clone(),
817                        file,
818                        mode,
819                        cached_len: RwLock::new(len),
820                        map: MapVariant::Cow(mmap),
821                        flush_policy: FlushPolicy::Never,
822                        written_since_last_flush: RwLock::new(0),
823                        #[cfg(feature = "hugepages")]
824                        huge_pages: false,
825                    };
826                    Ok(MemoryMappedFile { inner: Arc::new(inner) })
827                }
828                #[cfg(not(feature = "cow"))]
829                {
830                    Err(MmapIoError::InvalidMode("CopyOnWrite mode requires 'cow' feature"))
831                }
832            }
833        }
834    }
835
836    /// Open an existing file with provided mode (size ignored).
837    pub fn open(self) -> Result<MemoryMappedFile> {
838        let mode = self.mode.unwrap_or(MmapMode::ReadOnly);
839        match mode {
840            MmapMode::ReadOnly => {
841                let path_ref = &self.path;
842                let file = OpenOptions::new().read(true).open(path_ref)?;
843                let len = file.metadata()?.len();
844                let mmap = unsafe { Mmap::map(&file)? };
845                let inner = Inner {
846                    path: path_ref.clone(),
847                    file,
848                    mode,
849                    cached_len: RwLock::new(len),
850                    map: MapVariant::Ro(mmap),
851                    flush_policy: FlushPolicy::Never,
852                    written_since_last_flush: RwLock::new(0),
853                    #[cfg(feature = "hugepages")]
854                    huge_pages: false,
855                };
856                Ok(MemoryMappedFile { inner: Arc::new(inner) })
857            }
858            MmapMode::ReadWrite => {
859                let path_ref = &self.path;
860                let file = OpenOptions::new().read(true).write(true).open(path_ref)?;
861                let len = file.metadata()?.len();
862                if len == 0 {
863                    return Err(MmapIoError::ResizeFailed(ERR_ZERO_LENGTH_FILE.into()));
864                }
865                #[cfg(feature = "hugepages")]
866                let mmap = map_mut_with_options(&file, len, self.huge_pages)?;
867                #[cfg(not(feature = "hugepages"))]
868                let mmap = unsafe { MmapMut::map_mut(&file)? };
869                let inner = Inner {
870                    path: path_ref.clone(),
871                    file,
872                    mode,
873                    cached_len: RwLock::new(len),
874                    map: MapVariant::Rw(RwLock::new(mmap)),
875                    flush_policy: self.flush_policy,
876                    written_since_last_flush: RwLock::new(0),
877                    #[cfg(feature = "hugepages")]
878                    huge_pages: self.huge_pages,
879                };
880                Ok(MemoryMappedFile { inner: Arc::new(inner) })
881            }
882            MmapMode::CopyOnWrite => {
883                #[cfg(feature = "cow")]
884                {
885                    let path_ref = &self.path;
886                    let file = OpenOptions::new().read(true).open(path_ref)?;
887                    let len = file.metadata()?.len();
888                    if len == 0 {
889                        return Err(MmapIoError::ResizeFailed(ERR_ZERO_LENGTH_FILE.into()));
890                    }
891                    let mmap = unsafe {
892                        let mut opts = MmapOptions::new();
893                        opts.len(len as usize);
894                        opts.map(&file)?
895                    };
896                    let inner = Inner {
897                        path: path_ref.clone(),
898                        file,
899                        mode,
900                        cached_len: RwLock::new(len),
901                        map: MapVariant::Cow(mmap),
902                        flush_policy: FlushPolicy::Never,
903                        written_since_last_flush: RwLock::new(0),
904                        #[cfg(feature = "hugepages")]
905                        huge_pages: false,
906                    };
907                    Ok(MemoryMappedFile { inner: Arc::new(inner) })
908                }
909                #[cfg(not(feature = "cow"))]
910                {
911                    Err(MmapIoError::InvalidMode("CopyOnWrite mode requires 'cow' feature"))
912                }
913            }
914        }
915    }
916}
917
918/// Wrapper for a mutable slice that holds a write lock guard,
919/// ensuring exclusive access for the lifetime of the slice.
920pub struct MappedSliceMut<'a> {
921    guard: parking_lot::lock_api::RwLockWriteGuard<'a, parking_lot::RawRwLock, MmapMut>,
922    range: std::ops::Range<usize>,
923}
924
925impl MappedSliceMut<'_> {
926    /// Get the mutable slice.
927    ///
928    /// Note: This method is intentionally named `as_mut` for consistency,
929    /// even though it conflicts with the standard trait naming.
930    #[allow(clippy::should_implement_trait)]
931    pub fn as_mut(&mut self) -> &mut [u8] {
932        // Avoid clone by using the range directly
933        let start = self.range.start;
934        let end = self.range.end;
935        &mut self.guard[start..end]
936    }
937}