Skip to main content

oxigeo_core/io/
mmap.rs

1//! Memory-mapped `DataSource` implementations
2//!
3//! This module provides two structs that implement the [`DataSource`] trait (and
4//! `std::io::{Read, Seek}` / `std::io::{Read, Write, Seek}`) using the
5//! [`memmap2`] crate for zero-copy large-file access:
6//!
7//! * [`MmapDataSource`] — read-only mapping.
8//! * [`MmapDataSourceRw`] — read-write mapping with optional file creation.
9//!
10//! # Safety contract
11//!
12//! Memory-mapped I/O is inherently unsafe because the OS may alias the mapped
13//! region with the underlying file.  Both structs document the invariants at
14//! each `unsafe` site.  The primary responsibility placed on the *caller* is:
15//!
16//! > **Do not modify the mapped file through any other handle while a mapping
17//! > is live.**  Doing so triggers undefined behaviour in Rust because the
18//! > memory contents may change underneath an immutable reference.
19//!
20//! All internal operations are `Result`-returning; there are no `unwrap` calls
21//! in production code.
22
23// The entire module is `std`-only (memmap2 requires std).
24#![allow(unsafe_code)]
25
26use std::fs::{File, OpenOptions};
27use std::io::{self, Read, Seek, SeekFrom, Write};
28use std::path::{Path, PathBuf};
29
30use memmap2::{Mmap, MmapMut};
31
32use crate::error::{IoError, OxiGeoError, Result};
33use crate::io::traits::{dst_too_small, range_bounds_usize};
34use crate::io::{ByteRange, DataSource};
35
36// ---------------------------------------------------------------------------
37// Internal helpers
38// ---------------------------------------------------------------------------
39
40/// Build an `OxiGeoError::Io(IoError::Read { … })` from a `std::io::Error`.
41#[inline]
42fn io_read_err(e: io::Error, context: &str) -> OxiGeoError {
43    OxiGeoError::Io(IoError::Read {
44        message: format!("{context}: {e}"),
45    })
46}
47
48/// Build an out-of-bounds error for an attempted range read.
49#[inline]
50fn out_of_bounds_err(offset: usize, len: usize, mapped_len: usize) -> OxiGeoError {
51    OxiGeoError::OutOfBounds {
52        message: format!(
53            "read_at: offset ({offset}) + length ({len}) = {} exceeds mapping length ({mapped_len})",
54            offset.saturating_add(len)
55        ),
56    }
57}
58
59// ---------------------------------------------------------------------------
60// MmapDataSource (read-only)
61// ---------------------------------------------------------------------------
62
63/// A read-only [`DataSource`] backed by a memory-mapped file.
64///
65/// The mapping is created once at construction time using [`memmap2::Mmap`].
66/// Random-access reads through [`DataSource::read_range`] copy the requested
67/// bytes; zero-copy access is available via [`MmapDataSource::as_bytes`] and
68/// [`MmapDataSource::read_at`].
69///
70/// The struct also implements [`std::io::Read`] and [`std::io::Seek`] so that
71/// it can be passed to any reader that accepts `R: Read + Seek`.  The internal
72/// cursor used by those trait impls is independent of the `DataSource` API.
73///
74/// # Safety
75///
76/// Internally this calls `unsafe { memmap2::Mmap::map(&file) }`.  The
77/// invariant is: **the file must not be modified through any other handle while
78/// the mapping is live**.  Violating this invariant causes undefined behaviour.
79pub struct MmapDataSource {
80    /// The memory-mapped region.  `None` for zero-length files.
81    mmap: Option<Mmap>,
82    /// Total byte length of the mapped file (0 for empty files).
83    len: usize,
84    /// Path of the file, kept for `Debug` / error messages.
85    path: PathBuf,
86    /// Cursor position for `std::io::Read + Seek`.
87    cursor: usize,
88}
89
90impl MmapDataSource {
91    /// Opens `path` for read-only memory-mapped access.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if the file cannot be opened, its metadata cannot be
96    /// read, or `mmap` fails.
97    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
98        let path = path.as_ref().to_path_buf();
99        let file =
100            File::open(&path).map_err(|e| io_read_err(e, &format!("open '{}'", path.display())))?;
101
102        let metadata = file
103            .metadata()
104            .map_err(|e| io_read_err(e, "get file metadata"))?;
105
106        let file_len = metadata.len() as usize;
107
108        // SAFETY: We have just opened the file and hold the only handle to it
109        // within this struct.  We do not mutate the file elsewhere.  The file
110        // remains open (kept alive by `_file` inside `Mmap`) for the lifetime
111        // of the mapping.  The caller is responsible for not modifying the file
112        // externally while the mapping is live.
113        let mmap = if file_len == 0 {
114            // memmap2 returns EINVAL for zero-length files on Linux; treat as
115            // an empty mapping without calling `map`.
116            None
117        } else {
118            Some(unsafe { Mmap::map(&file) }.map_err(|e| io_read_err(e, "mmap read-only"))?)
119        };
120
121        Ok(Self {
122            mmap,
123            len: file_len,
124            path,
125            cursor: 0,
126        })
127    }
128
129    /// Returns the total mapped length in bytes (0 for empty files).
130    #[must_use]
131    #[inline]
132    pub fn len(&self) -> usize {
133        self.len
134    }
135
136    /// Returns `true` if the mapped file is empty.
137    #[must_use]
138    #[inline]
139    pub fn is_empty(&self) -> bool {
140        self.len == 0
141    }
142
143    /// Returns a byte slice of the entire mapping.
144    ///
145    /// For empty files this returns an empty slice.
146    #[must_use]
147    #[inline]
148    pub fn as_bytes(&self) -> &[u8] {
149        match &self.mmap {
150            Some(m) => m.as_ref(),
151            None => &[],
152        }
153    }
154
155    /// Returns a byte slice for `offset..offset+len` without moving the
156    /// internal cursor.
157    ///
158    /// # Errors
159    ///
160    /// Returns [`OxiGeoError::OutOfBounds`] if `offset + len > self.len()`.
161    pub fn read_at(&self, offset: usize, len: usize) -> Result<&[u8]> {
162        let end = offset
163            .checked_add(len)
164            .ok_or_else(|| OxiGeoError::OutOfBounds {
165                message: format!("read_at: offset ({offset}) + length ({len}) overflows usize"),
166            })?;
167        if end > self.len {
168            return Err(out_of_bounds_err(offset, len, self.len));
169        }
170        Ok(&self.as_bytes()[offset..end])
171    }
172
173    /// Returns the path of the underlying file.
174    #[must_use]
175    pub fn path(&self) -> &Path {
176        &self.path
177    }
178}
179
180// --- DataSource impl --------------------------------------------------------
181
182impl DataSource for MmapDataSource {
183    fn size(&self) -> Result<u64> {
184        Ok(self.len as u64)
185    }
186
187    fn read_range(&self, range: ByteRange) -> Result<Vec<u8>> {
188        let (offset, len) = range_bounds_usize(range)?;
189        let data = self.read_at(offset, len)?;
190        Ok(data.to_vec())
191    }
192
193    /// Copies straight out of the mapping into `dst` — no intermediate `Vec`.
194    ///
195    /// Callers that can work from a borrowed slice should prefer
196    /// [`DataSource::range_slice`], which copies nothing at all.
197    fn read_range_into(&self, range: ByteRange, dst: &mut [u8]) -> Result<usize> {
198        let (offset, len) = range_bounds_usize(range)?;
199        if dst.len() < len {
200            return Err(dst_too_small(len, dst.len()));
201        }
202        dst[..len].copy_from_slice(self.read_at(offset, len)?);
203        Ok(len)
204    }
205
206    /// Hands back a borrowed view of the mapping: reading a block through this
207    /// costs neither an allocation nor a copy.
208    fn range_slice(&self, range: ByteRange) -> Option<&[u8]> {
209        let start = usize::try_from(range.start).ok()?;
210        let len = usize::try_from(range.end.checked_sub(range.start)?).ok()?;
211        self.read_at(start, len).ok()
212    }
213
214    fn supports_range_requests(&self) -> bool {
215        true
216    }
217}
218
219// --- std::io::Read + Seek impl ----------------------------------------------
220
221impl Read for MmapDataSource {
222    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
223        let bytes = self.as_bytes();
224        if self.cursor >= self.len {
225            return Ok(0); // EOF
226        }
227        let available = self.len - self.cursor;
228        let to_copy = buf.len().min(available);
229        buf[..to_copy].copy_from_slice(&bytes[self.cursor..self.cursor + to_copy]);
230        self.cursor += to_copy;
231        Ok(to_copy)
232    }
233}
234
235impl Seek for MmapDataSource {
236    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
237        let new_cursor: i64 = match pos {
238            SeekFrom::Start(n) => n as i64,
239            SeekFrom::End(n) => self.len as i64 + n,
240            SeekFrom::Current(n) => self.cursor as i64 + n,
241        };
242        // Per the `Seek` contract, seeking to a negative position is an error,
243        // but seeking past the end is permitted (just sets cursor past end).
244        if new_cursor < 0 {
245            return Err(io::Error::new(
246                io::ErrorKind::InvalidInput,
247                "cannot seek to a negative position",
248            ));
249        }
250        self.cursor = new_cursor as usize;
251        Ok(self.cursor as u64)
252    }
253}
254
255impl std::fmt::Debug for MmapDataSource {
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        f.debug_struct("MmapDataSource")
258            .field("path", &self.path)
259            .field("len", &self.len)
260            .field("cursor", &self.cursor)
261            .finish()
262    }
263}
264
265// ---------------------------------------------------------------------------
266// MmapDataSourceRw (read-write)
267// ---------------------------------------------------------------------------
268
269/// A read-write [`DataSource`] backed by a memory-mapped file.
270///
271/// Supports reading, writing, flushing, and seeking via the standard traits.
272/// Use [`MmapDataSourceRw::open`] to map an existing file, or
273/// [`MmapDataSourceRw::create`] to create and immediately map a new
274/// zero-filled file of a given byte length.
275///
276/// # Safety
277///
278/// Internally this calls `unsafe { MmapMut::map_mut(&file) }`.  The same
279/// invariant applies as for [`MmapDataSource`]: **the file must not be
280/// accessed through any other handle while the mapping is live**.
281pub struct MmapDataSourceRw {
282    /// The mutable memory-mapped region.
283    mmap: MmapMut,
284    /// Total byte length of the mapped file.
285    len: usize,
286    /// Path of the file, kept for `Debug` / error messages.
287    path: PathBuf,
288    /// Cursor position for `std::io::{Read, Write, Seek}`.
289    cursor: usize,
290}
291
292impl MmapDataSourceRw {
293    /// Opens `path` for read-write memory-mapped access.
294    ///
295    /// The file must already exist and be non-empty.
296    ///
297    /// # Errors
298    ///
299    /// Returns an error if the file cannot be opened, its metadata cannot be
300    /// read, the file is empty, or `mmap_mut` fails.
301    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
302        let path = path.as_ref().to_path_buf();
303        let file = OpenOptions::new()
304            .read(true)
305            .write(true)
306            .open(&path)
307            .map_err(|e| io_read_err(e, &format!("open rw '{}'", path.display())))?;
308
309        let metadata = file
310            .metadata()
311            .map_err(|e| io_read_err(e, "get file metadata"))?;
312
313        let file_len = metadata.len() as usize;
314        if file_len == 0 {
315            return Err(OxiGeoError::InvalidParameter {
316                parameter: "path",
317                message: "cannot open a read-write mmap on an empty file; use create() instead"
318                    .to_string(),
319            });
320        }
321
322        // SAFETY: We've opened the file with read+write access and hold the
323        // sole File handle within this struct.  The caller must not access the
324        // file externally while the mapping is live.
325        let mmap =
326            unsafe { MmapMut::map_mut(&file) }.map_err(|e| io_read_err(e, "mmap read-write"))?;
327
328        Ok(Self {
329            mmap,
330            len: file_len,
331            path,
332            cursor: 0,
333        })
334    }
335
336    /// Creates a new file at `path`, extends it to `len` bytes, and maps it.
337    ///
338    /// If `path` already exists it is truncated.  The new file is zero-filled
339    /// by the OS.
340    ///
341    /// # Errors
342    ///
343    /// Returns an error if `len == 0`, the file cannot be created, `set_len`
344    /// fails, or `mmap_mut` fails.
345    pub fn create(path: impl AsRef<Path>, len: usize) -> Result<Self> {
346        if len == 0 {
347            return Err(OxiGeoError::InvalidParameter {
348                parameter: "len",
349                message: "cannot create a zero-length memory-mapped file".to_string(),
350            });
351        }
352
353        let path = path.as_ref().to_path_buf();
354        let file = OpenOptions::new()
355            .read(true)
356            .write(true)
357            .create(true)
358            .truncate(true)
359            .open(&path)
360            .map_err(|e| io_read_err(e, &format!("create '{}'", path.display())))?;
361
362        // Extend the file to `len` bytes BEFORE mapping; otherwise the mapping
363        // would be zero-length.
364        file.set_len(len as u64)
365            .map_err(|e| io_read_err(e, "set file length"))?;
366
367        // SAFETY: We just created the file, extended it to `len` bytes, and
368        // hold the only File handle.  The mapping covers the full file.
369        let mmap = unsafe { MmapMut::map_mut(&file) }.map_err(|e| io_read_err(e, "mmap create"))?;
370
371        Ok(Self {
372            mmap,
373            len,
374            path,
375            cursor: 0,
376        })
377    }
378
379    /// Returns the total mapped length in bytes.
380    #[must_use]
381    #[inline]
382    pub fn len(&self) -> usize {
383        self.len
384    }
385
386    /// Returns `true` if the mapped region is empty.
387    #[must_use]
388    #[inline]
389    pub fn is_empty(&self) -> bool {
390        self.len == 0
391    }
392
393    /// Flushes outstanding changes to disk synchronously.
394    ///
395    /// # Errors
396    ///
397    /// Returns an error if `msync` / `FlushViewOfFile` fails.
398    pub fn flush(&self) -> Result<()> {
399        self.mmap.flush().map_err(|e| io_read_err(e, "mmap flush"))
400    }
401
402    /// Returns an immutable byte slice of the entire mapping.
403    #[must_use]
404    #[inline]
405    pub fn as_bytes(&self) -> &[u8] {
406        &self.mmap
407    }
408
409    /// Returns a mutable byte slice of the entire mapping.
410    #[must_use]
411    #[inline]
412    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
413        &mut self.mmap
414    }
415
416    /// Returns a byte slice for `offset..offset+len` without moving the cursor.
417    ///
418    /// # Errors
419    ///
420    /// Returns [`OxiGeoError::OutOfBounds`] if `offset + len > self.len()`.
421    pub fn read_at(&self, offset: usize, len: usize) -> Result<&[u8]> {
422        let end = offset
423            .checked_add(len)
424            .ok_or_else(|| OxiGeoError::OutOfBounds {
425                message: format!("read_at: offset ({offset}) + length ({len}) overflows usize"),
426            })?;
427        if end > self.len {
428            return Err(out_of_bounds_err(offset, len, self.len));
429        }
430        Ok(&self.mmap[offset..end])
431    }
432
433    /// Overwrites `data.len()` bytes starting at `offset`.
434    ///
435    /// # Errors
436    ///
437    /// Returns [`OxiGeoError::OutOfBounds`] if `offset + data.len() > self.len()`.
438    pub fn write_at(&mut self, offset: usize, data: &[u8]) -> Result<()> {
439        let len = data.len();
440        let end = offset
441            .checked_add(len)
442            .ok_or_else(|| OxiGeoError::OutOfBounds {
443                message: format!(
444                    "write_at: offset ({offset}) + data length ({len}) overflows usize"
445                ),
446            })?;
447        if end > self.len {
448            return Err(out_of_bounds_err(offset, len, self.len));
449        }
450        self.mmap[offset..end].copy_from_slice(data);
451        Ok(())
452    }
453
454    /// Returns the path of the underlying file.
455    #[must_use]
456    pub fn path(&self) -> &Path {
457        &self.path
458    }
459}
460
461// --- DataSource impl (immutable reads) -------------------------------------
462
463impl DataSource for MmapDataSourceRw {
464    fn size(&self) -> Result<u64> {
465        Ok(self.len as u64)
466    }
467
468    fn read_range(&self, range: ByteRange) -> Result<Vec<u8>> {
469        let (offset, len) = range_bounds_usize(range)?;
470        let data = self.read_at(offset, len)?;
471        Ok(data.to_vec())
472    }
473
474    /// Copies straight out of the mapping into `dst` — no intermediate `Vec`.
475    fn read_range_into(&self, range: ByteRange, dst: &mut [u8]) -> Result<usize> {
476        let (offset, len) = range_bounds_usize(range)?;
477        if dst.len() < len {
478            return Err(dst_too_small(len, dst.len()));
479        }
480        dst[..len].copy_from_slice(self.read_at(offset, len)?);
481        Ok(len)
482    }
483
484    /// Hands back a borrowed view of the mapping (see
485    /// [`MmapDataSource::range_slice`](DataSource::range_slice)).
486    ///
487    /// The borrow is immutable and `&self`, so no write through
488    /// [`MmapDataSourceRw::write_at`] can be in flight while it is alive.
489    fn range_slice(&self, range: ByteRange) -> Option<&[u8]> {
490        let start = usize::try_from(range.start).ok()?;
491        let len = usize::try_from(range.end.checked_sub(range.start)?).ok()?;
492        self.read_at(start, len).ok()
493    }
494
495    fn supports_range_requests(&self) -> bool {
496        true
497    }
498}
499
500// --- std::io::{Read, Write, Seek} impl -------------------------------------
501
502impl Read for MmapDataSourceRw {
503    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
504        if self.cursor >= self.len {
505            return Ok(0); // EOF
506        }
507        let available = self.len - self.cursor;
508        let to_copy = buf.len().min(available);
509        buf[..to_copy].copy_from_slice(&self.mmap[self.cursor..self.cursor + to_copy]);
510        self.cursor += to_copy;
511        Ok(to_copy)
512    }
513}
514
515impl Write for MmapDataSourceRw {
516    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
517        if self.cursor >= self.len {
518            return Err(io::Error::new(
519                io::ErrorKind::WriteZero,
520                "write past end of memory-mapped region",
521            ));
522        }
523        let available = self.len - self.cursor;
524        let to_copy = buf.len().min(available);
525        self.mmap[self.cursor..self.cursor + to_copy].copy_from_slice(&buf[..to_copy]);
526        self.cursor += to_copy;
527        Ok(to_copy)
528    }
529
530    fn flush(&mut self) -> io::Result<()> {
531        self.mmap
532            .flush()
533            .map_err(|e| io::Error::other(format!("mmap flush failed: {e}")))
534    }
535}
536
537impl Seek for MmapDataSourceRw {
538    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
539        let new_cursor: i64 = match pos {
540            SeekFrom::Start(n) => n as i64,
541            SeekFrom::End(n) => self.len as i64 + n,
542            SeekFrom::Current(n) => self.cursor as i64 + n,
543        };
544        if new_cursor < 0 {
545            return Err(io::Error::new(
546                io::ErrorKind::InvalidInput,
547                "cannot seek to a negative position",
548            ));
549        }
550        self.cursor = new_cursor as usize;
551        Ok(self.cursor as u64)
552    }
553}
554
555impl std::fmt::Debug for MmapDataSourceRw {
556    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
557        f.debug_struct("MmapDataSourceRw")
558            .field("path", &self.path)
559            .field("len", &self.len)
560            .field("cursor", &self.cursor)
561            .finish()
562    }
563}
564
565// ---------------------------------------------------------------------------
566// Tests
567// ---------------------------------------------------------------------------
568
569#[cfg(test)]
570mod tests {
571    use super::*;
572    use std::env::temp_dir;
573    use std::fs;
574    use std::io::{Read, Seek, SeekFrom, Write};
575    use std::sync::atomic::{AtomicU64, Ordering};
576
577    /// Per-test scratch fixture inside the system temp dir (house policy: no
578    /// hardcoded absolute paths).
579    ///
580    /// The leaf name embeds the process id and a monotonic counter, so no two test
581    /// binaries — nor two concurrent runs of this one — can ever land on the same
582    /// file.  Dropping the guard removes the fixture, so a panicking test leaks
583    /// nothing.
584    struct TempPath(PathBuf);
585
586    impl TempPath {
587        fn new(name: &str) -> Self {
588            static COUNTER: AtomicU64 = AtomicU64::new(0);
589            let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
590            Self(temp_dir().join(format!(
591                "oxigeo_core_mmap_{}_{seq}_{name}",
592                std::process::id()
593            )))
594        }
595    }
596
597    impl std::ops::Deref for TempPath {
598        type Target = Path;
599
600        fn deref(&self) -> &Path {
601            &self.0
602        }
603    }
604
605    impl AsRef<Path> for TempPath {
606        fn as_ref(&self) -> &Path {
607            &self.0
608        }
609    }
610
611    impl Drop for TempPath {
612        fn drop(&mut self) {
613            let _ = fs::remove_file(&self.0);
614        }
615    }
616    // Helper: write `data` to a temp file and return its guard.
617    fn write_temp_file(name: &str, data: &[u8]) -> TempPath {
618        let path = TempPath::new(name);
619        let mut f = fs::File::create(&path).expect("test helper: failed to create temp file");
620        f.write_all(data)
621            .expect("test helper: failed to write temp data");
622        f.flush().expect("test helper: failed to flush temp file");
623        path
624    }
625
626    // Helper: create a uniquely named temp path for RW tests.
627    fn temp_rw_path(name: &str) -> TempPath {
628        TempPath::new(name)
629    }
630
631    // -----------------------------------------------------------------------
632    // MmapDataSource tests
633    // -----------------------------------------------------------------------
634
635    #[test]
636    fn test_mmap_read_small_file() {
637        let data: Vec<u8> = (0u8..=127u8).collect();
638        let path = write_temp_file("mmap_test_small.bin", &data);
639
640        let src = MmapDataSource::open(&path).expect("MmapDataSource::open should succeed");
641        assert_eq!(src.len(), 128);
642        assert!(!src.is_empty());
643        assert_eq!(src.as_bytes(), &data[..]);
644    }
645
646    #[test]
647    fn test_mmap_read_at() {
648        let data: Vec<u8> = (0u8..200u8).collect();
649        let path = write_temp_file("mmap_test_read_at.bin", &data);
650
651        let src = MmapDataSource::open(&path).expect("MmapDataSource::open should succeed");
652
653        // Read 10 bytes at offset 50
654        let slice = src
655            .read_at(50, 10)
656            .expect("read_at should succeed within bounds");
657        assert_eq!(slice, &data[50..60]);
658
659        // Read last byte
660        let last = src
661            .read_at(199, 1)
662            .expect("read_at last byte should succeed");
663        assert_eq!(last, &[199u8]);
664    }
665
666    #[test]
667    fn test_mmap_seek_and_read() {
668        let data: Vec<u8> = (0u8..100u8).collect();
669        let path = write_temp_file("mmap_test_seek.bin", &data);
670
671        let mut src = MmapDataSource::open(&path).expect("MmapDataSource::open should succeed");
672
673        // Seek to offset 40 and read 10 bytes
674        src.seek(SeekFrom::Start(40))
675            .expect("seek to 40 should succeed");
676        let mut buf = vec![0u8; 10];
677        src.read_exact(&mut buf)
678            .expect("read_exact after seek should succeed");
679        assert_eq!(&buf, &data[40..50]);
680    }
681
682    #[test]
683    fn test_mmap_out_of_bounds_err() {
684        let data = vec![0u8; 100];
685        let path = write_temp_file("mmap_test_oob.bin", &data);
686
687        let src = MmapDataSource::open(&path).expect("MmapDataSource::open should succeed");
688
689        // Exact boundary — should succeed
690        let ok = src.read_at(0, 100);
691        assert!(ok.is_ok());
692
693        // One byte over — should fail
694        let err = src.read_at(1, 100);
695        assert!(err.is_err());
696        assert!(matches!(err, Err(OxiGeoError::OutOfBounds { .. })));
697
698        // offset + len overflows for gigantic values
699        let overflow = src.read_at(usize::MAX, 1);
700        assert!(overflow.is_err());
701    }
702
703    #[test]
704    fn test_mmap_empty_file_ok() {
705        let path = write_temp_file("mmap_test_empty.bin", &[]);
706
707        let src =
708            MmapDataSource::open(&path).expect("MmapDataSource::open on empty file should succeed");
709        assert_eq!(src.len(), 0);
710        assert!(src.is_empty());
711        assert_eq!(src.as_bytes(), &[] as &[u8]);
712
713        // read_at 0 bytes at offset 0 is valid on an empty file
714        let ok = src.read_at(0, 0);
715        assert!(ok.is_ok());
716
717        // Any non-zero read must fail
718        let err = src.read_at(0, 1);
719        assert!(err.is_err());
720    }
721
722    #[test]
723    fn test_mmap_large_offset_seek() {
724        let data = vec![0u8; 64];
725        let path = write_temp_file("mmap_test_large_seek.bin", &data);
726
727        let mut src = MmapDataSource::open(&path).expect("MmapDataSource::open should succeed");
728
729        // Seeking past end is valid per Seek contract; the cursor is simply set
730        // past the end.  Subsequent reads return 0 bytes (EOF).
731        let pos = src
732            .seek(SeekFrom::Start(1_000_000))
733            .expect("seek past end should not error");
734        assert_eq!(pos, 1_000_000);
735
736        let mut buf = vec![0u8; 16];
737        let n = src
738            .read(&mut buf)
739            .expect("read after seek past end should not error");
740        assert_eq!(n, 0, "read after seek past end returns 0 bytes (EOF)");
741    }
742
743    #[test]
744    fn test_mmap_datasource_trait_read_range() {
745        let data: Vec<u8> = (0u8..=255u8).collect();
746        let path = write_temp_file("mmap_test_range.bin", &data);
747
748        let src = MmapDataSource::open(&path).expect("MmapDataSource::open should succeed");
749
750        let range = ByteRange::new(10, 30);
751        let bytes = src
752            .read_range(range)
753            .expect("DataSource::read_range should succeed");
754        assert_eq!(bytes, &data[10..30]);
755
756        let size = src.size().expect("DataSource::size should succeed");
757        assert_eq!(size, 256);
758        assert!(src.supports_range_requests());
759    }
760
761    // -----------------------------------------------------------------------
762    // MmapDataSourceRw tests
763    // -----------------------------------------------------------------------
764
765    #[test]
766    fn test_mmap_rw_create_and_write() {
767        let path = temp_rw_path("mmap_rw_create.bin");
768        // Remove if exists from a previous run
769
770        {
771            let mut rw = MmapDataSourceRw::create(&path, 1024)
772                .expect("MmapDataSourceRw::create should succeed");
773            assert_eq!(rw.len(), 1024);
774
775            // Write a recognisable pattern at the start
776            let pattern: Vec<u8> = (0u8..=255u8).collect();
777            rw.write_at(0, &pattern)
778                .expect("write_at start should succeed");
779
780            // Write another pattern near the end
781            let tail = b"END!";
782            rw.write_at(1020, tail)
783                .expect("write_at tail should succeed");
784
785            rw.flush().expect("flush should succeed");
786        }
787
788        // Reopen read-only and verify
789        let ro = MmapDataSource::open(&path)
790            .expect("re-opening created file as read-only should succeed");
791        assert_eq!(ro.len(), 1024);
792
793        let head = ro.read_at(0, 256).expect("read_at head should succeed");
794        let expected: Vec<u8> = (0u8..=255u8).collect();
795        assert_eq!(head, &expected[..]);
796
797        let tail = ro.read_at(1020, 4).expect("read_at tail should succeed");
798        assert_eq!(tail, b"END!");
799    }
800
801    #[test]
802    fn test_mmap_rw_write_at() {
803        let path = temp_rw_path("mmap_rw_write_at.bin");
804
805        let mut rw =
806            MmapDataSourceRw::create(&path, 256).expect("MmapDataSourceRw::create should succeed");
807
808        // Write at offset 100
809        let data = b"HELLO_WORLD";
810        rw.write_at(100, data).expect("write_at should succeed");
811
812        // Verify with read_at
813        let read_back = rw
814            .read_at(100, data.len())
815            .expect("read_at after write_at should succeed");
816        assert_eq!(read_back, data);
817    }
818
819    #[test]
820    fn test_mmap_rw_out_of_bounds() {
821        let path = temp_rw_path("mmap_rw_oob.bin");
822
823        let mut rw =
824            MmapDataSourceRw::create(&path, 128).expect("MmapDataSourceRw::create should succeed");
825
826        // write_at that extends past end should fail
827        let data = vec![1u8; 10];
828        let err = rw.write_at(120, &data);
829        assert!(err.is_err());
830        assert!(matches!(err, Err(OxiGeoError::OutOfBounds { .. })));
831
832        // read_at past end should also fail
833        let err = rw.read_at(120, 10);
834        assert!(err.is_err());
835        assert!(matches!(err, Err(OxiGeoError::OutOfBounds { .. })));
836    }
837
838    #[test]
839    fn test_mmap_rw_std_io_traits() {
840        let path = temp_rw_path("mmap_rw_io.bin");
841
842        let mut rw =
843            MmapDataSourceRw::create(&path, 64).expect("MmapDataSourceRw::create should succeed");
844
845        // Write via std::io::Write
846        let payload = b"abcdefghij";
847        let written = rw.write(payload).expect("write should succeed");
848        assert_eq!(written, payload.len());
849
850        // Seek back to start via std::io::Seek
851        rw.seek(SeekFrom::Start(0))
852            .expect("seek to start should succeed");
853
854        // Read via std::io::Read
855        let mut buf = vec![0u8; payload.len()];
856        rw.read_exact(&mut buf).expect("read_exact should succeed");
857        assert_eq!(&buf, payload);
858    }
859
860    #[test]
861    fn test_mmap_rw_datasource_trait() {
862        let path = temp_rw_path("mmap_rw_ds.bin");
863
864        let mut rw =
865            MmapDataSourceRw::create(&path, 512).expect("MmapDataSourceRw::create should succeed");
866
867        let fill: Vec<u8> = (0u8..=255u8).cycle().take(512).collect();
868        rw.write_at(0, &fill).expect("write_at fill should succeed");
869
870        // DataSource::read_range
871        let range = ByteRange::new(64, 128);
872        let bytes = rw.read_range(range).expect("read_range should succeed");
873        assert_eq!(bytes, &fill[64..128]);
874
875        assert_eq!(rw.size().expect("size should succeed"), 512);
876        assert!(rw.supports_range_requests());
877    }
878
879    #[test]
880    fn test_mmap_rw_create_zero_len_err() {
881        let path = temp_rw_path("mmap_rw_zero_len.bin");
882
883        let err = MmapDataSourceRw::create(&path, 0);
884        assert!(err.is_err());
885        assert!(matches!(
886            err,
887            Err(OxiGeoError::InvalidParameter {
888                parameter: "len",
889                ..
890            })
891        ));
892    }
893}