use std::fs::{self, File};
use std::path::{Path, PathBuf};
use memmap2::Mmap;
use crate::{Error, Result};
use crate::file_identity::{validate_file_identity, FileIdentity};
#[derive(Debug)]
pub struct MmapCorpus {
pub base: PathBuf,
pub mappings: Vec<MmapRegion>,
}
#[derive(Debug)]
pub struct MmapRegion {
mmap: Mmap,
path: PathBuf,
size: u64,
}
impl MmapCorpus {
pub fn open(dir: impl AsRef<Path>) -> Result<Self> {
Self::open_with_limits(dir, 1024 * 1024 * 1024, 10 * 1024 * 1024 * 1024)
}
pub fn open_with_limits(
dir: impl AsRef<Path>,
max_file_bytes: u64,
max_total_bytes: u64,
) -> Result<Self> {
let base = dir.as_ref().to_path_buf();
let mut file_paths = Vec::new();
collect_files(&base, &mut file_paths)?;
file_paths.sort();
let mut mappings = Vec::with_capacity(file_paths.len());
let mut total_bytes = 0u64;
for path in file_paths {
let file = File::open(&path).map_err(|source| Error::System {
operation: "open",
source,
})?;
let metadata = file.metadata().map_err(|source| Error::System {
operation: "metadata",
source,
})?;
let expected_identity = FileIdentity::from_metadata(&metadata);
let size = metadata.len();
if size > max_file_bytes {
return Err(Error::System {
operation: "open_with_limits",
source: std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("file size {size} exceeds limit of {max_file_bytes}"),
),
});
}
total_bytes = total_bytes.saturating_add(size);
if total_bytes > max_total_bytes {
return Err(Error::System {
operation: "open_with_limits",
source: std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("total corpus size exceeds limit of {max_total_bytes}"),
),
});
}
let mmap = unsafe { memmap2::MmapOptions::new().populate().map(&file) }.map_err(|source| {
Error::System {
operation: "mmap",
source,
}
})?;
validate_mapping_stability(&file, expected_identity, size)?;
advise_sequential(&mmap, &path)?;
mappings.push(MmapRegion { mmap, path, size });
}
Ok(Self { base, mappings })
}
pub fn iter(&self) -> impl Iterator<Item = (&Path, &[u8])> {
self.mappings
.iter()
.map(|region| (region.path.as_path(), region.mmap.as_ref()))
}
}
impl MmapRegion {
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn size(&self) -> u64 {
self.size
}
#[must_use]
pub fn as_slice(&self) -> &[u8] {
self.mmap.as_ref()
}
}
fn collect_files(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
let entries = fs::read_dir(dir).map_err(|source| Error::System {
operation: "read_dir",
source,
})?;
for entry in entries {
let entry = entry.map_err(|source| Error::System {
operation: "read_dir",
source,
})?;
let path = entry.path();
let file_type = entry.file_type().map_err(|source| Error::System {
operation: "file_type",
source,
})?;
if file_type.is_symlink() {
return Err(Error::System {
operation: "collect_files",
source: std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"symlinks are not permitted in corpus",
),
});
}
if file_type.is_dir() {
collect_files(&path, out)?;
} else if file_type.is_file() {
out.push(path);
} else {
return Err(Error::System {
operation: "collect_files",
source: std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unsupported file type in corpus: {}", path.display()),
),
});
}
}
Ok(())
}
#[cfg(target_os = "linux")]
fn advise_sequential(mmap: &Mmap, path: &Path) -> Result<()> {
if mmap.is_empty() {
return Ok(());
}
let ptr = mmap.as_ptr().cast::<libc::c_void>().cast_mut();
let sequential_result = unsafe { libc::madvise(ptr, mmap.len(), libc::MADV_SEQUENTIAL) };
if sequential_result != 0 {
return Err(Error::System {
operation: "madvise(SEQUENTIAL)",
source: std::io::Error::other(format!(
"{} (path: {})",
std::io::Error::last_os_error(),
path.display()
)),
});
}
let hugepage_result = unsafe { libc::madvise(ptr, mmap.len(), libc::MADV_HUGEPAGE) };
if hugepage_result != 0 {
return Err(Error::System {
operation: "madvise(HUGEPAGE)",
source: std::io::Error::other(format!(
"{} (path: {})",
std::io::Error::last_os_error(),
path.display()
)),
});
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn advise_sequential(_mmap: &Mmap, _path: &Path) -> Result<()> {
Ok(())
}
fn validate_mapping_stability(file: &File, expected: FileIdentity, expected_len: u64) -> Result<()> {
validate_file_identity(
file,
expected,
expected_len,
"corpus file changed during mapping; Fix: run corpus mapping on immutable input",
)
}
#[cfg(test)]
mod tests {
use super::MmapCorpus;
use std::fs;
#[test]
fn iterates_over_mapped_files() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(dir.path().join("a.txt"), b"alpha").expect("write a");
fs::write(dir.path().join("b.txt"), b"beta").expect("write b");
let corpus = MmapCorpus::open(dir.path()).expect("open corpus");
let collected: Vec<_> = corpus.iter().map(|(_, bytes)| bytes.to_vec()).collect();
assert_eq!(collected.len(), 2);
assert!(collected.iter().any(|bytes| bytes == b"alpha"));
assert!(collected.iter().any(|bytes| bytes == b"beta"));
}
}