1#![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#[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#[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
59pub struct MmapDataSource {
80 mmap: Option<Mmap>,
82 len: usize,
84 path: PathBuf,
86 cursor: usize,
88}
89
90impl MmapDataSource {
91 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 let mmap = if file_len == 0 {
114 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 #[must_use]
131 #[inline]
132 pub fn len(&self) -> usize {
133 self.len
134 }
135
136 #[must_use]
138 #[inline]
139 pub fn is_empty(&self) -> bool {
140 self.len == 0
141 }
142
143 #[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 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 #[must_use]
175 pub fn path(&self) -> &Path {
176 &self.path
177 }
178}
179
180impl 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 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 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
219impl 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); }
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 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
265pub struct MmapDataSourceRw {
282 mmap: MmapMut,
284 len: usize,
286 path: PathBuf,
288 cursor: usize,
290}
291
292impl MmapDataSourceRw {
293 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 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 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 file.set_len(len as u64)
365 .map_err(|e| io_read_err(e, "set file length"))?;
366
367 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 #[must_use]
381 #[inline]
382 pub fn len(&self) -> usize {
383 self.len
384 }
385
386 #[must_use]
388 #[inline]
389 pub fn is_empty(&self) -> bool {
390 self.len == 0
391 }
392
393 pub fn flush(&self) -> Result<()> {
399 self.mmap.flush().map_err(|e| io_read_err(e, "mmap flush"))
400 }
401
402 #[must_use]
404 #[inline]
405 pub fn as_bytes(&self) -> &[u8] {
406 &self.mmap
407 }
408
409 #[must_use]
411 #[inline]
412 pub fn as_bytes_mut(&mut self) -> &mut [u8] {
413 &mut self.mmap
414 }
415
416 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 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 #[must_use]
456 pub fn path(&self) -> &Path {
457 &self.path
458 }
459}
460
461impl 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 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 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
500impl Read for MmapDataSourceRw {
503 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
504 if self.cursor >= self.len {
505 return Ok(0); }
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#[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 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 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 fn temp_rw_path(name: &str) -> TempPath {
628 TempPath::new(name)
629 }
630
631 #[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 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 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 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 let ok = src.read_at(0, 100);
691 assert!(ok.is_ok());
692
693 let err = src.read_at(1, 100);
695 assert!(err.is_err());
696 assert!(matches!(err, Err(OxiGeoError::OutOfBounds { .. })));
697
698 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 let ok = src.read_at(0, 0);
715 assert!(ok.is_ok());
716
717 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 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 #[test]
766 fn test_mmap_rw_create_and_write() {
767 let path = temp_rw_path("mmap_rw_create.bin");
768 {
771 let mut rw = MmapDataSourceRw::create(&path, 1024)
772 .expect("MmapDataSourceRw::create should succeed");
773 assert_eq!(rw.len(), 1024);
774
775 let pattern: Vec<u8> = (0u8..=255u8).collect();
777 rw.write_at(0, &pattern)
778 .expect("write_at start should succeed");
779
780 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 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 let data = b"HELLO_WORLD";
810 rw.write_at(100, data).expect("write_at should succeed");
811
812 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 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 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 let payload = b"abcdefghij";
847 let written = rw.write(payload).expect("write should succeed");
848 assert_eq!(written, payload.len());
849
850 rw.seek(SeekFrom::Start(0))
852 .expect("seek to start should succeed");
853
854 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 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}