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, OxiGdalError, Result};
33use crate::io::{ByteRange, DataSource};
34
35#[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#[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
58pub struct MmapDataSource {
79 mmap: Option<Mmap>,
81 len: usize,
83 path: PathBuf,
85 cursor: usize,
87}
88
89impl MmapDataSource {
90 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 let mmap = if file_len == 0 {
113 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 #[must_use]
130 #[inline]
131 pub fn len(&self) -> usize {
132 self.len
133 }
134
135 #[must_use]
137 #[inline]
138 pub fn is_empty(&self) -> bool {
139 self.len == 0
140 }
141
142 #[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 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 #[must_use]
174 pub fn path(&self) -> &Path {
175 &self.path
176 }
177}
178
179impl 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
198impl 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); }
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 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
244pub struct MmapDataSourceRw {
261 mmap: MmapMut,
263 len: usize,
265 path: PathBuf,
267 cursor: usize,
269}
270
271impl MmapDataSourceRw {
272 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 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 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 file.set_len(len as u64)
344 .map_err(|e| io_read_err(e, "set file length"))?;
345
346 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 #[must_use]
360 #[inline]
361 pub fn len(&self) -> usize {
362 self.len
363 }
364
365 #[must_use]
367 #[inline]
368 pub fn is_empty(&self) -> bool {
369 self.len == 0
370 }
371
372 pub fn flush(&self) -> Result<()> {
378 self.mmap.flush().map_err(|e| io_read_err(e, "mmap flush"))
379 }
380
381 #[must_use]
383 #[inline]
384 pub fn as_bytes(&self) -> &[u8] {
385 &self.mmap
386 }
387
388 #[must_use]
390 #[inline]
391 pub fn as_bytes_mut(&mut self) -> &mut [u8] {
392 &mut self.mmap
393 }
394
395 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 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 #[must_use]
435 pub fn path(&self) -> &Path {
436 &self.path
437 }
438}
439
440impl 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
459impl Read for MmapDataSourceRw {
462 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
463 if self.cursor >= self.len {
464 return Ok(0); }
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#[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 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 fn temp_rw_path(name: &str) -> PathBuf {
547 temp_dir().join(name)
548 }
549
550 #[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 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 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 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 let ok = src.read_at(0, 100);
610 assert!(ok.is_ok());
611
612 let err = src.read_at(1, 100);
614 assert!(err.is_err());
615 assert!(matches!(err, Err(OxiGdalError::OutOfBounds { .. })));
616
617 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 let ok = src.read_at(0, 0);
634 assert!(ok.is_ok());
635
636 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 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 #[test]
685 fn test_mmap_rw_create_and_write() {
686 let path = temp_rw_path("mmap_rw_create.bin");
687 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 let pattern: Vec<u8> = (0u8..=255u8).collect();
697 rw.write_at(0, &pattern)
698 .expect("write_at start should succeed");
699
700 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 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 let data = b"HELLO_WORLD";
731 rw.write_at(100, data).expect("write_at should succeed");
732
733 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 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 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 let payload = b"abcdefghij";
770 let written = rw.write(payload).expect("write should succeed");
771 assert_eq!(written, payload.len());
772
773 rw.seek(SeekFrom::Start(0))
775 .expect("seek to start should succeed");
776
777 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 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}