Skip to main content

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