use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::path::Path;
use memmap2::{Advice, Mmap};
use super::RandomAccess;
pub struct MmapImage {
_file: File,
mmap: Mmap,
cursor: u64,
}
impl MmapImage {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let file = File::open(path)?;
let mmap = unsafe { Mmap::map(&file)? };
let _ = mmap.advise(Advice::Sequential);
Ok(Self {
_file: file,
mmap,
cursor: 0,
})
}
pub fn len(&self) -> u64 {
self.mmap.len() as u64
}
pub fn is_empty(&self) -> bool {
self.mmap.is_empty()
}
pub fn as_bytes(&self) -> &[u8] {
&self.mmap
}
}
impl Read for MmapImage {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let remaining = self.mmap.len() as u64 - self.cursor.min(self.mmap.len() as u64);
if remaining == 0 {
return Ok(0);
}
let n = buf.len().min(remaining as usize);
let off = self.cursor as usize;
buf[..n].copy_from_slice(&self.mmap[off..off + n]);
self.cursor += n as u64;
Ok(n)
}
}
impl Seek for MmapImage {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let new_pos: i64 = match pos {
SeekFrom::Start(n) => n as i64,
SeekFrom::Current(n) => self.cursor as i64 + n,
SeekFrom::End(n) => self.mmap.len() as i64 + n,
};
if new_pos < 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"seek before start",
));
}
self.cursor = new_pos as u64;
Ok(self.cursor)
}
}
impl RandomAccess for MmapImage {
fn len(&self) -> u64 {
self.mmap.len() as u64
}
fn read_at(&self, offset: u64, len: usize) -> io::Result<&[u8]> {
let off = offset as usize;
let end = off
.checked_add(len)
.ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "offset overflow"))?;
self.mmap
.get(off..end)
.ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "read past image end"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::path::PathBuf;
fn scratch_image(bytes: &[u8], tag: &str) -> PathBuf {
let dir = std::env::temp_dir();
let path = dir.join(format!(
"isomage-mmap-test-{}-{}.bin",
std::process::id(),
tag
));
let mut f = File::create(&path).unwrap();
f.write_all(bytes).unwrap();
f.sync_all().unwrap();
path
}
#[test]
fn opens_and_reads() {
let path = scratch_image(b"0123456789", "opens_and_reads");
let mut img = MmapImage::open(&path).unwrap();
assert_eq!(<MmapImage as RandomAccess>::len(&img), 10);
let mut buf = [0u8; 4];
img.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"0123");
img.seek(SeekFrom::Start(6)).unwrap();
img.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"6789");
assert_eq!(img.read_at(2, 4).unwrap(), b"2345");
assert_eq!(img.as_bytes().len(), 10);
std::fs::remove_file(&path).ok();
}
#[test]
fn seek_before_start_rejected() {
let path = scratch_image(b"abcd", "seek_before_start");
let mut img = MmapImage::open(&path).unwrap();
let err = img
.seek(SeekFrom::Start(0))
.and_then(|_| img.seek(SeekFrom::Current(-1)));
assert!(err.is_err());
std::fs::remove_file(&path).ok();
}
#[test]
fn read_at_past_end_returns_eof() {
let path = scratch_image(b"abc", "read_at_past_end");
let img = MmapImage::open(&path).unwrap();
assert_eq!(
img.read_at(0, 4).unwrap_err().kind(),
io::ErrorKind::UnexpectedEof
);
std::fs::remove_file(&path).ok();
}
#[test]
fn len_and_is_empty_methods() {
let path = scratch_image(b"hello", "len_is_empty");
let img = MmapImage::open(&path).unwrap();
assert_eq!(img.len(), 5, "len() should return file size");
assert!(!img.is_empty(), "5-byte file should not be empty");
std::fs::remove_file(&path).ok();
}
#[test]
fn read_at_end_of_file_returns_zero() {
let path = scratch_image(b"hi", "read_eof");
let mut img = MmapImage::open(&path).unwrap();
img.seek(SeekFrom::Start(2)).unwrap(); let mut buf = [0u8; 4];
let n = img.read(&mut buf).unwrap();
assert_eq!(n, 0, "read past end should return 0 bytes");
std::fs::remove_file(&path).ok();
}
#[test]
fn seek_from_end() {
let path = scratch_image(b"abcde", "seek_end");
let mut img = MmapImage::open(&path).unwrap();
let pos = img.seek(SeekFrom::End(-2)).unwrap();
assert_eq!(pos, 3, "SeekFrom::End(-2) on 5-byte file → position 3");
let mut buf = [0u8; 2];
img.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"de");
std::fs::remove_file(&path).ok();
}
}