use crate::error::{BatchError, Result};
use std::fs::File;
use std::io::{BufReader, Cursor, Read};
use std::path::Path;
use memmap2::Mmap;
pub const MMAP_THRESHOLD: u64 = 4 * 1024 * 1024;
pub enum MmapReader {
Mmap(Cursor<Mmap>),
Buf(BufReader<File>),
}
impl MmapReader {
pub fn new(path: &Path) -> Result<Self> {
let file = File::open(path).map_err(|e| {
BatchError::IoError(std::io::Error::new(
e.kind(),
format!("cannot open '{}': {e}", path.display()),
))
})?;
let meta = file.metadata().map_err(|e| {
BatchError::IoError(std::io::Error::new(
e.kind(),
format!("cannot stat '{}': {e}", path.display()),
))
})?;
if meta.len() >= MMAP_THRESHOLD {
#[allow(unsafe_code)]
let mmap = unsafe {
Mmap::map(&file).map_err(|e| {
BatchError::IoError(std::io::Error::new(
e.kind(),
format!("cannot mmap '{}': {e}", path.display()),
))
})?
};
Ok(Self::Mmap(Cursor::new(mmap)))
} else {
Ok(Self::Buf(BufReader::new(file)))
}
}
#[must_use]
pub fn is_mmap(&self) -> bool {
matches!(self, Self::Mmap(_))
}
}
impl Read for MmapReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Self::Mmap(cursor) => cursor.read(buf),
Self::Buf(reader) => reader.read(buf),
}
}
}
pub fn open_smart(path: &Path) -> Result<Box<dyn Read>> {
let reader = MmapReader::new(path)?;
Ok(Box::new(reader))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Read;
fn write_temp(content: &[u8]) -> tempfile::NamedTempFile {
let tmp = tempfile::NamedTempFile::new().expect("temp file");
fs::write(tmp.path(), content).expect("write");
tmp
}
#[test]
fn test_mmap_reader_small_file_fallback() {
let tmp = write_temp(b"small content");
let reader = MmapReader::new(tmp.path()).expect("reader");
assert!(!reader.is_mmap(), "small file should use BufReader path");
}
#[test]
fn test_mmap_reader_content_matches_fs_read() {
let payload = vec![0xABu8; 1024];
let tmp = write_temp(&payload);
let mut reader = MmapReader::new(tmp.path()).expect("reader");
let mut got = Vec::new();
reader.read_to_end(&mut got).expect("read_to_end");
assert_eq!(got, payload);
}
#[test]
fn test_open_smart_small_file() {
let payload = b"hello world";
let tmp = write_temp(payload);
let mut reader = open_smart(tmp.path()).expect("open_smart");
let mut buf = Vec::new();
reader.read_to_end(&mut buf).expect("read");
assert_eq!(buf, payload);
}
#[test]
fn test_mmap_reader_large_file() {
let tmp_dir = std::env::temp_dir();
let path = tmp_dir.join(format!(
"oximedia_batch_mmap_test_{}.bin",
std::process::id()
));
let data: Vec<u8> = (0..5 * 1024 * 1024u64).map(|i| (i % 251) as u8).collect();
fs::write(&path, &data).expect("write large file");
let reader_result = MmapReader::new(&path);
let cleanup = || {
let _ = fs::remove_file(&path);
};
let mut reader = match reader_result {
Ok(r) => r,
Err(e) => {
cleanup();
panic!("failed to open large file reader: {e}");
}
};
assert!(reader.is_mmap(), "large file should use mmap path");
let mut got = Vec::new();
if let Err(e) = reader.read_to_end(&mut got) {
cleanup();
panic!("read_to_end failed: {e}");
}
cleanup();
assert_eq!(got, data, "content mismatch for large file");
}
}